Bluetooth HUD Animations

This commit is contained in:
Hariharan Mudaliar 2026-02-04 23:48:48 +05:30
commit 2453ca89ff
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
12 changed files with 366 additions and 6 deletions

View file

@ -21,8 +21,65 @@
*/
import SwiftUI
import AppKit
import AVFoundation
import Defaults
// MARK: - Inline HUD looping .mov icon
private final class LoopingPlayerController {
let player: AVQueuePlayer
private var looper: AVPlayerLooper?
init(url: URL) {
let item = AVPlayerItem(url: url)
self.player = AVQueuePlayer()
self.player.isMuted = true
self.player.actionAtItemEnd = .none
self.looper = AVPlayerLooper(player: self.player, templateItem: item)
self.player.play()
}
deinit {
player.pause()
looper = nil
}
}
private struct LoopingVideoIcon: NSViewRepresentable {
let url: URL
let size: CGSize
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: NSRect(origin: .zero, size: size))
view.wantsLayer = true
let layer = AVPlayerLayer()
layer.videoGravity = .resizeAspect
layer.frame = view.bounds
view.layer?.addSublayer(layer)
context.coordinator.attach(layer: layer, url: url)
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
// No-op; the animation loops via AVPlayerLooper.
}
func makeCoordinator() -> Coordinator { Coordinator() }
final class Coordinator {
private var controller: LoopingPlayerController?
func attach(layer: AVPlayerLayer, url: URL) {
controller = LoopingPlayerController(url: url)
layer.player = controller?.player
}
}
}
struct InlineHUD: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Binding var type: SneakContentType
@ -39,6 +96,7 @@ struct InlineHUD: View {
@Default(.useCircularBluetoothBatteryIndicator) var useCircularBluetoothBatteryIndicator
@Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText
@Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee
@Default(.useBluetoothHUD3DIcon) var useBluetoothHUD3DIcon
@Default(.enableMinimalisticUI) var enableMinimalisticUI
@Default(.showCapsLockLabel) var showCapsLockLabel
@Default(.capsLockIndicatorTintMode) var capsLockTintMode
@ -173,10 +231,17 @@ struct InlineHUD: View {
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .bluetoothAudio:
Image(systemName: icon.isEmpty ? "bluetooth" : icon)
.symbolRenderingMode(.hierarchical)
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
if useBluetoothHUD3DIcon,
let deviceType = bluetoothManager.lastConnectedDevice?.deviceType,
let url = Bundle.main.url(forResource: deviceType.inlineHUDAnimationBaseName, withExtension: "mov") {
LoopingVideoIcon(url: url, size: CGSize(width: 20, height: 20))
.frame(width: 20, height: 20, alignment: .center)
} else {
Image(systemName: icon.isEmpty ? "bluetooth" : icon)
.symbolRenderingMode(.hierarchical)
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
}
case .capsLock:
Image(systemName: "capslock.fill")
.symbolRenderingMode(.hierarchical)