Working stuff

This commit is contained in:
Hariharan Mudaliar 2025-10-26 23:18:07 +05:30
commit 9ea2cce485
4 changed files with 104 additions and 33 deletions

View file

@ -87,6 +87,13 @@ extension ColorCodedProgressBar {
} }
return AnyShapeStyle(ColorCodedPalette.discreteColor(for: intensity)) 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 // 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 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 yellowComponents: (CGFloat, CGFloat, CGFloat) = (0.93, 0.75, 0.2)
private static let redComponents: (CGFloat, CGFloat, CGFloat) = (0.95, 0.36, 0.3) 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 { static func discreteColor(for intensity: CGFloat) -> Color {
let clamped = min(max(intensity, 0), 1) let clamped = min(max(intensity, 0), 1)

View file

@ -21,12 +21,32 @@ struct InlineHUD: View {
@Default(.useSmoothColorGradient) var useSmoothColorGradient @Default(.useSmoothColorGradient) var useSmoothColorGradient
@Default(.progressBarStyle) var progressBarStyle @Default(.progressBarStyle) var progressBarStyle
@Default(.showProgressPercentages) var showProgressPercentages @Default(.showProgressPercentages) var showProgressPercentages
@Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText
@Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee
@ObservedObject var bluetoothManager = BluetoothAudioManager.shared @ObservedObject var bluetoothManager = BluetoothAudioManager.shared
@State private var displayName: String = "" @State private var displayName: String = ""
var body: some View { 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) { HStack(spacing: 5) {
Group { Group {
switch (type) { switch (type) {
@ -78,22 +98,15 @@ struct InlineHUD: View {
// Use marquee text for device names to handle long names // Use marquee text for device names to handle long names
if type == .bluetoothAudio { if type == .bluetoothAudio {
MarqueeText( if showBluetoothDeviceNameMarquee {
$displayName, MarqueeText(
font: .system(size: 13, weight: .medium), $displayName,
nsFont: .body, font: .system(size: 13, weight: .medium),
textColor: .white, nsFont: .body,
minDuration: 0.5, textColor: .white,
frameWidth: 85 - (hoverAnimation ? 0 : 12) + gestureProgress / 2 minDuration: 0.5,
) frameWidth: infoWidth
.onAppear { )
displayName = Type2Name(type)
}
.onChange(of: type) { _, _ in
displayName = Type2Name(type)
}
.onChange(of: bluetoothManager.lastConnectedDevice?.name) { _, _ in
displayName = Type2Name(type)
} }
} else { } else {
Text(Type2Name(type)) Text(Type2Name(type))
@ -104,7 +117,7 @@ struct InlineHUD: View {
.contentTransition(.numericText()) .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() Rectangle()
.fill(.black) .fill(.black)
@ -129,22 +142,22 @@ struct InlineHUD: View {
.contentTransition(.interpolate) .contentTransition(.interpolate)
} else if (type == .bluetoothAudio) { } else if (type == .bluetoothAudio) {
// Bluetooth device battery display // Bluetooth device battery display
HStack(spacing: 4) { HStack(spacing: 8) {
if value > 0 { if value > 0 {
Group { CircularBatteryIndicator(
if useColorCodedBatteryDisplay && progressBarStyle != .segmented { value: value,
DraggableProgressBar(value: .constant(value), colorMode: .battery) useColorCoding: useColorCodedBatteryDisplay && progressBarStyle != .segmented,
} else { smoothGradient: useSmoothColorGradient
DraggableProgressBar(value: .constant(value)) )
}
}
.frame(width: 60)
.allowsHitTesting(false) .allowsHitTesting(false)
Text("\(Int(value * 100))%")
.font(.caption) if showBluetoothBatteryPercentageText {
.fontWeight(.medium) Text("\(Int(value * 100))%")
.foregroundStyle(.white) .font(.caption)
.lineLimit(1) .fontWeight(.medium)
.foregroundStyle(.white)
.lineLimit(1)
}
} else { } else {
// No battery info available // No battery info available
Text("Connected") Text("Connected")
@ -189,11 +202,51 @@ struct InlineHUD: View {
} }
} }
.padding(.trailing, 4) .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) .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 { func SpeakerSymbol(_ value: CGFloat) -> String {
switch(value) { switch(value) {
case 0: case 0:

View file

@ -675,6 +675,8 @@ struct HUD: View {
Section { Section {
Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) 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: { } header: {
Text("Bluetooth Audio Devices") Text("Bluetooth Audio Devices")
} footer: { } footer: {

View file

@ -391,6 +391,8 @@ extension Defaults.Keys {
static let useColorCodedBatteryDisplay = Key<Bool>("useColorCodedBatteryDisplay", default: true) static let useColorCodedBatteryDisplay = Key<Bool>("useColorCodedBatteryDisplay", default: true)
static let useColorCodedVolumeDisplay = Key<Bool>("useColorCodedVolumeDisplay", default: false) static let useColorCodedVolumeDisplay = Key<Bool>("useColorCodedVolumeDisplay", default: false)
static let useSmoothColorGradient = Key<Bool>("useSmoothColorGradient", default: true) static let useSmoothColorGradient = Key<Bool>("useSmoothColorGradient", default: true)
static let showBluetoothBatteryPercentageText = Key<Bool>("showBluetoothBatteryPercentageText", default: true)
static let showBluetoothDeviceNameMarquee = Key<Bool>("showBluetoothDeviceNameMarquee", default: true)
// MARK: Stats Feature // MARK: Stats Feature
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false) static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)