From 4bb67c2cd33a6f6125d21981fcd5eab1a1bb1c12 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 26 Oct 2025 23:48:40 +0530 Subject: [PATCH] Working Headphone HUDs and other stuff --- .../Live activities/InlineHUD.swift | 78 +++++++++++++++---- .../LockScreen/LockScreenWeatherWidget.swift | 14 ++-- .../components/Settings/SettingsView.swift | 1 + .../managers/BluetoothAudioManager.swift | 2 + .../managers/SystemChangesObserver.swift | 3 + .../managers/SystemMediaControllers.swift | 32 ++++++++ DynamicIsland/models/Constants.swift | 1 + 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index e9c267c..cbdb549 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -21,6 +21,7 @@ struct InlineHUD: View { @Default(.useSmoothColorGradient) var useSmoothColorGradient @Default(.progressBarStyle) var progressBarStyle @Default(.showProgressPercentages) var showProgressPercentages + @Default(.useCircularBluetoothBatteryIndicator) var useCircularBluetoothBatteryIndicator @Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText @Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @@ -28,19 +29,27 @@ struct InlineHUD: View { @State private var displayName: String = "" var body: some View { + let useCircularIndicator = useCircularBluetoothBatteryIndicator + let hasBatteryLevel = value > 0 + let baseInfoWidth: CGFloat = { if type == .bluetoothAudio { - return showBluetoothDeviceNameMarquee ? 100 : 58 + return showBluetoothDeviceNameMarquee ? 100 : 52 } return 100 }() let infoWidth = baseInfoWidth - (hoverAnimation ? 0 : 12) + gestureProgress / 2 let baseTrailingWidth: CGFloat = { if type == .bluetoothAudio { - if showBluetoothDeviceNameMarquee { - return 100 + if !hasBatteryLevel { + return showBluetoothDeviceNameMarquee ? 100 : 70 + } + + if useCircularIndicator { + return showBluetoothBatteryPercentageText ? 100 : 70 + } else { + return showBluetoothBatteryPercentageText ? 112 : 82 } - return showBluetoothBatteryPercentageText ? 88 : 64 } return 100 }() @@ -104,7 +113,7 @@ struct InlineHUD: View { font: .system(size: 13, weight: .medium), nsFont: .body, textColor: .white, - minDuration: 0.5, + minDuration: 0.2, frameWidth: infoWidth ) } @@ -142,14 +151,23 @@ struct InlineHUD: View { .contentTransition(.interpolate) } else if (type == .bluetoothAudio) { // Bluetooth device battery display - HStack(spacing: 8) { - if value > 0 { - CircularBatteryIndicator( - value: value, - useColorCoding: useColorCodedBatteryDisplay && progressBarStyle != .segmented, - smoothGradient: useSmoothColorGradient - ) - .allowsHitTesting(false) + HStack(spacing: useCircularIndicator ? 8 : 6) { + if hasBatteryLevel { + if useCircularIndicator { + CircularBatteryIndicator( + value: value, + useColorCoding: useColorCodedBatteryDisplay && progressBarStyle != .segmented, + smoothGradient: useSmoothColorGradient + ) + .allowsHitTesting(false) + } else { + LinearBatteryIndicator( + value: value, + useColorCoding: useColorCodedBatteryDisplay && progressBarStyle != .segmented, + smoothGradient: useSmoothColorGradient + ) + .allowsHitTesting(false) + } if showBluetoothBatteryPercentageText { Text("\(Int(value * 100))%") @@ -247,6 +265,40 @@ struct InlineHUD: View { } } + private struct LinearBatteryIndicator: View { + let value: CGFloat + let useColorCoding: Bool + let smoothGradient: Bool + + private let trackWidth: CGFloat = 54 + private let trackHeight: CGFloat = 6 + + private var clampedValue: CGFloat { + min(max(value, 0), 1) + } + + private var fillColor: Color { + if useColorCoding { + return ColorCodedProgressBar.paletteColor(for: clampedValue, mode: .battery, smoothGradient: smoothGradient) + } + return .white + } + + var body: some View { + ZStack(alignment: .leading) { + Capsule() + .fill(Color.white.opacity(0.18)) + .frame(width: trackWidth, height: trackHeight) + + Capsule() + .fill(fillColor) + .frame(width: trackWidth * clampedValue, height: trackHeight) + } + .frame(width: trackWidth, height: trackHeight) + .animation(.smooth(duration: 0.18), value: clampedValue) + } + } + func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: diff --git a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift index 8c5ee35..d4bcc66 100644 --- a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift @@ -4,6 +4,7 @@ struct LockScreenWeatherWidget: View { let snapshot: LockScreenWeatherSnapshot var body: some View { + let segments = infoSegments HStack(spacing: 8) { Image(systemName: snapshot.symbolName) .font(.system(size: 26, weight: .medium)) @@ -12,12 +13,15 @@ struct LockScreenWeatherWidget: View { .font(.system(size: 24, weight: .semibold, design: .rounded)) .kerning(-0.4) - ForEach(Array(infoSegments.enumerated()), id: \.offset) { _, segment in - HStack(spacing: 4) { - Text("•") - .font(.system(size: 22, weight: .semibold, design: .rounded)) - segmentView(for: segment) + if !segments.isEmpty { + Color.clear.frame(width: 6) + } + + ForEach(Array(segments.enumerated()), id: \.offset) { index, segment in + if index > 0 { + Color.clear.frame(width: 12) } + segmentView(for: segment) } } .foregroundStyle(Color.white.opacity(0.65)) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index dd9dd76..be3e6c6 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -675,6 +675,7 @@ struct HUD: View { Section { Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) + Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator) Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText) Defaults.Toggle("Scroll device name in HUD", key: .showBluetoothDeviceNameMarquee) } header: { diff --git a/DynamicIsland/managers/BluetoothAudioManager.swift b/DynamicIsland/managers/BluetoothAudioManager.swift index 9896fce..5ee4e7e 100644 --- a/DynamicIsland/managers/BluetoothAudioManager.swift +++ b/DynamicIsland/managers/BluetoothAudioManager.swift @@ -1015,6 +1015,8 @@ class BluetoothAudioManager: ObservableObject { } else { 0.0 // 0 means battery info not available } + + HUDSuppressionCoordinator.shared.suppressVolumeHUD(for: 1.5) // Show HUD via coordinator coordinator.toggleSneakPeek( diff --git a/DynamicIsland/managers/SystemChangesObserver.swift b/DynamicIsland/managers/SystemChangesObserver.swift index 8e1ca19..44441ec 100644 --- a/DynamicIsland/managers/SystemChangesObserver.swift +++ b/DynamicIsland/managers/SystemChangesObserver.swift @@ -93,6 +93,9 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate { // MARK: - HUD Dispatch private func sendVolumeNotification(value: Float) { + if HUDSuppressionCoordinator.shared.shouldSuppressVolumeHUD { + return + } coordinator?.toggleSneakPeek( status: true, type: .volume, diff --git a/DynamicIsland/managers/SystemMediaControllers.swift b/DynamicIsland/managers/SystemMediaControllers.swift index 31d1b48..9e41d55 100644 --- a/DynamicIsland/managers/SystemMediaControllers.swift +++ b/DynamicIsland/managers/SystemMediaControllers.swift @@ -9,6 +9,38 @@ extension Notification.Name { static let systemAudioRouteDidChange = Notification.Name("DynamicIsland.systemAudioRouteDidChange") } +final class HUDSuppressionCoordinator { + static let shared = HUDSuppressionCoordinator() + + private let queue = DispatchQueue(label: "com.dynamicisland.hud-suppression") + private var volumeSuppressedUntil: Date? + + func suppressVolumeHUD(for interval: TimeInterval) { + guard interval > 0 else { return } + queue.sync { + let proposed = Date().addingTimeInterval(interval) + if let current = volumeSuppressedUntil { + volumeSuppressedUntil = max(current, proposed) + } else { + volumeSuppressedUntil = proposed + } + } + } + + var shouldSuppressVolumeHUD: Bool { + queue.sync { + guard let expiration = volumeSuppressedUntil else { + return false + } + if Date() < expiration { + return true + } + volumeSuppressedUntil = nil + return false + } + } +} + final class SystemVolumeController { static let shared = SystemVolumeController() diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 5821b2c..87f6d67 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -391,6 +391,7 @@ extension Defaults.Keys { static let useColorCodedBatteryDisplay = Key("useColorCodedBatteryDisplay", default: true) static let useColorCodedVolumeDisplay = Key("useColorCodedVolumeDisplay", default: false) static let useSmoothColorGradient = Key("useSmoothColorGradient", default: true) + static let useCircularBluetoothBatteryIndicator = Key("useCircularBluetoothBatteryIndicator", default: true) static let showBluetoothBatteryPercentageText = Key("showBluetoothBatteryPercentageText", default: true) static let showBluetoothDeviceNameMarquee = Key("showBluetoothDeviceNameMarquee", default: true)