Working Headphone HUDs and other stuff

This commit is contained in:
Hariharan Mudaliar 2025-10-26 23:48:40 +05:30
commit 4bb67c2cd3
7 changed files with 113 additions and 18 deletions

View file

@ -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 {
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:

View file

@ -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))

View file

@ -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: {

View file

@ -1016,6 +1016,8 @@ class BluetoothAudioManager: ObservableObject {
0.0 // 0 means battery info not available
}
HUDSuppressionCoordinator.shared.suppressVolumeHUD(for: 1.5)
// Show HUD via coordinator
coordinator.toggleSneakPeek(
status: true,

View file

@ -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,

View file

@ -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()

View file

@ -391,6 +391,7 @@ extension Defaults.Keys {
static let useColorCodedBatteryDisplay = Key<Bool>("useColorCodedBatteryDisplay", default: true)
static let useColorCodedVolumeDisplay = Key<Bool>("useColorCodedVolumeDisplay", default: false)
static let useSmoothColorGradient = Key<Bool>("useSmoothColorGradient", default: true)
static let useCircularBluetoothBatteryIndicator = Key<Bool>("useCircularBluetoothBatteryIndicator", default: true)
static let showBluetoothBatteryPercentageText = Key<Bool>("showBluetoothBatteryPercentageText", default: true)
static let showBluetoothDeviceNameMarquee = Key<Bool>("showBluetoothDeviceNameMarquee", default: true)