diff --git a/DynamicIsland/components/ColorCodedProgressBar.swift b/DynamicIsland/components/ColorCodedProgressBar.swift index 21c608e..8d620bc 100644 --- a/DynamicIsland/components/ColorCodedProgressBar.swift +++ b/DynamicIsland/components/ColorCodedProgressBar.swift @@ -87,6 +87,13 @@ extension ColorCodedProgressBar { } return AnyShapeStyle(ColorCodedPalette.discreteColor(for: intensity)) } + + /// Returns the palette color that matches the configured thresholds. Useful for non-linear indicators. + static func paletteColor(for value: CGFloat, mode: ColorCodingMode, smoothGradient: Bool) -> Color { + let normalized = min(max(value, 0), 1) + let intensity = mode.intensity(for: normalized) + return ColorCodedPalette.color(for: intensity, smooth: smoothGradient) + } } // MARK: - Palette Helpers @@ -95,6 +102,13 @@ private enum ColorCodedPalette { private static let greenComponents: (CGFloat, CGFloat, CGFloat) = (0.2, 0.78, 0.35) private static let yellowComponents: (CGFloat, CGFloat, CGFloat) = (0.93, 0.75, 0.2) private static let redComponents: (CGFloat, CGFloat, CGFloat) = (0.95, 0.36, 0.3) + + static func color(for intensity: CGFloat, smooth: Bool) -> Color { + if smooth { + return smoothColor(for: intensity) + } + return discreteColor(for: intensity) + } static func discreteColor(for intensity: CGFloat) -> Color { let clamped = min(max(intensity, 0), 1) diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 523c79b..e9c267c 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -21,12 +21,32 @@ struct InlineHUD: View { @Default(.useSmoothColorGradient) var useSmoothColorGradient @Default(.progressBarStyle) var progressBarStyle @Default(.showProgressPercentages) var showProgressPercentages + @Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText + @Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" var body: some View { - HStack { + let baseInfoWidth: CGFloat = { + if type == .bluetoothAudio { + return showBluetoothDeviceNameMarquee ? 100 : 58 + } + return 100 + }() + let infoWidth = baseInfoWidth - (hoverAnimation ? 0 : 12) + gestureProgress / 2 + let baseTrailingWidth: CGFloat = { + if type == .bluetoothAudio { + if showBluetoothDeviceNameMarquee { + return 100 + } + return showBluetoothBatteryPercentageText ? 88 : 64 + } + return 100 + }() + let trailingWidth = baseTrailingWidth - (hoverAnimation ? 0 : 12) + gestureProgress / 2 + + return HStack { HStack(spacing: 5) { Group { switch (type) { @@ -78,22 +98,15 @@ struct InlineHUD: View { // Use marquee text for device names to handle long names if type == .bluetoothAudio { - MarqueeText( - $displayName, - font: .system(size: 13, weight: .medium), - nsFont: .body, - textColor: .white, - minDuration: 0.5, - frameWidth: 85 - (hoverAnimation ? 0 : 12) + gestureProgress / 2 - ) - .onAppear { - displayName = Type2Name(type) - } - .onChange(of: type) { _, _ in - displayName = Type2Name(type) - } - .onChange(of: bluetoothManager.lastConnectedDevice?.name) { _, _ in - displayName = Type2Name(type) + if showBluetoothDeviceNameMarquee { + MarqueeText( + $displayName, + font: .system(size: 13, weight: .medium), + nsFont: .body, + textColor: .white, + minDuration: 0.5, + frameWidth: infoWidth + ) } } else { Text(Type2Name(type)) @@ -104,7 +117,7 @@ struct InlineHUD: View { .contentTransition(.numericText()) } } - .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading) + .frame(width: infoWidth, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading) Rectangle() .fill(.black) @@ -129,22 +142,22 @@ struct InlineHUD: View { .contentTransition(.interpolate) } else if (type == .bluetoothAudio) { // Bluetooth device battery display - HStack(spacing: 4) { + HStack(spacing: 8) { if value > 0 { - Group { - if useColorCodedBatteryDisplay && progressBarStyle != .segmented { - DraggableProgressBar(value: .constant(value), colorMode: .battery) - } else { - DraggableProgressBar(value: .constant(value)) - } - } - .frame(width: 60) + CircularBatteryIndicator( + value: value, + useColorCoding: useColorCodedBatteryDisplay && progressBarStyle != .segmented, + smoothGradient: useSmoothColorGradient + ) .allowsHitTesting(false) - Text("\(Int(value * 100))%") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.white) - .lineLimit(1) + + if showBluetoothBatteryPercentageText { + Text("\(Int(value * 100))%") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.white) + .lineLimit(1) + } } else { // No battery info available Text("Connected") @@ -189,11 +202,51 @@ struct InlineHUD: View { } } .padding(.trailing, 4) - .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center) + .frame(width: trailingWidth, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center) } .frame(height: vm.closedNotchSize.height + (hoverAnimation ? 8 : 0), alignment: .center) + .onAppear { + displayName = Type2Name(type) + } + .onChange(of: type) { _, _ in + displayName = Type2Name(type) + } + .onChange(of: bluetoothManager.lastConnectedDevice?.name) { _, _ in + displayName = Type2Name(type) + } } + private struct CircularBatteryIndicator: View { + let value: CGFloat + let useColorCoding: Bool + let smoothGradient: Bool + + private var clampedValue: CGFloat { + min(max(value, 0), 1) + } + + private var indicatorColor: Color { + if useColorCoding { + return ColorCodedProgressBar.paletteColor(for: clampedValue, mode: .battery, smoothGradient: smoothGradient) + } + return .white + } + + var body: some View { + ZStack { + Circle() + .stroke(Color.white.opacity(0.18), lineWidth: 2.6) + + Circle() + .trim(from: 0, to: max(clampedValue, 0.015)) + .rotation(.degrees(-90)) + .stroke(indicatorColor, style: StrokeStyle(lineWidth: 2.8, lineCap: .round)) + } + .frame(width: 22, height: 22) + .animation(.smooth(duration: 0.18), value: clampedValue) + } + } + func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index d7d7677..dd9dd76 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -675,6 +675,8 @@ struct HUD: View { Section { Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) + Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText) + Defaults.Toggle("Scroll device name in HUD", key: .showBluetoothDeviceNameMarquee) } header: { Text("Bluetooth Audio Devices") } footer: { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 8b16715..5821b2c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -391,6 +391,8 @@ 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 showBluetoothBatteryPercentageText = Key("showBluetoothBatteryPercentageText", default: true) + static let showBluetoothDeviceNameMarquee = Key("showBluetoothDeviceNameMarquee", default: true) // MARK: Stats Feature static let enableStatsFeature = Key("enableStatsFeature", default: false)