// // InlineHUDs.swift // DynamicIsland // // Created by Richard Kunkli on 14/09/2024. // import SwiftUI import Defaults struct InlineHUD: View { @EnvironmentObject var vm: DynamicIslandViewModel @Binding var type: SneakContentType @Binding var value: CGFloat @Binding var icon: String @Binding var hoverAnimation: Bool @Binding var gestureProgress: CGFloat @Default(.useColorCodedBatteryDisplay) var useColorCodedBatteryDisplay @Default(.useColorCodedVolumeDisplay) var useColorCodedVolumeDisplay @Default(.useSmoothColorGradient) var useSmoothColorGradient @Default(.progressBarStyle) var progressBarStyle @Default(.showProgressPercentages) var showProgressPercentages @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" var body: some View { HStack { HStack(spacing: 5) { Group { switch (type) { case .volume: if icon.isEmpty { // Show headphone icon if Bluetooth audio is connected, otherwise speaker let baseIcon = bluetoothManager.isBluetoothAudioConnected ? "headphones" : SpeakerSymbol(value) Image(systemName: baseIcon) .contentTransition(.interpolate) .symbolVariant(value > 0 ? .none : .slash) .frame(width: 20, height: 15, alignment: .leading) } else { Image(systemName: icon) .contentTransition(.interpolate) .opacity(value.isZero ? 0.6 : 1) .scaleEffect(value.isZero ? 0.85 : 1) .frame(width: 20, height: 15, alignment: .leading) } case .brightness: Image(systemName: BrightnessSymbol(value)) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .backlight: Image(systemName: "keyboard") .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .mic: Image(systemName: "mic") .symbolRenderingMode(.hierarchical) .symbolVariant(value > 0 ? .none : .slash) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .timer: Image(systemName: "timer") .symbolRenderingMode(.hierarchical) .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) default: EmptyView() } } .foregroundStyle(.white) .symbolVariant(.fill) // 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) } } else { Text(Type2Name(type)) .font(.subheadline) .fontWeight(.medium) .lineLimit(1) .allowsTightening(true) .contentTransition(.numericText()) } } .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading) Rectangle() .fill(.black) .frame(width: vm.closedNotchSize.width - 20) HStack { if (type == .mic) { Text(value.isZero ? "muted" : "unmuted") .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) } else if (type == .timer) { Text(TimerManager.shared.formattedRemainingTime()) .foregroundStyle(TimerManager.shared.timerColor) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) } else if (type == .bluetoothAudio) { // Bluetooth device battery display HStack(spacing: 4) { if value > 0 { Group { if useColorCodedBatteryDisplay && progressBarStyle != .segmented { DraggableProgressBar(value: .constant(value), colorMode: .battery) } else { DraggableProgressBar(value: .constant(value)) } } .frame(width: 60) .allowsHitTesting(false) Text("\(Int(value * 100))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.white) .lineLimit(1) } else { // No battery info available Text("Connected") .font(.caption) .fontWeight(.medium) .foregroundStyle(.gray) .lineLimit(1) } } .frame(maxWidth: .infinity, alignment: .trailing) } else { // Volume and brightness displays Group { if type == .volume { Group { if value.isZero { Text("muted") .font(.caption) .fontWeight(.medium) .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) .contentTransition(.numericText()) } else { HStack(spacing: 6) { DraggableProgressBar(value: $value, colorMode: .volume) PercentageLabel(value: value, isVisible: showProgressPercentages) } .transition(.opacity.combined(with: .scale)) } } .animation(.smooth(duration: 0.2), value: value.isZero) } else { HStack(spacing: 6) { DraggableProgressBar(value: $value) PercentageLabel(value: value, isVisible: showProgressPercentages) } } } .frame(maxWidth: .infinity, alignment: .trailing) } } .padding(.trailing, 4) .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center) } .frame(height: vm.closedNotchSize.height + (hoverAnimation ? 8 : 0), alignment: .center) } func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: return "speaker" case 0...0.3: return "speaker.wave.1" case 0.3...0.8: return "speaker.wave.2" case 0.8...1: return "speaker.wave.3" default: return "speaker.wave.2" } } func BrightnessSymbol(_ value: CGFloat) -> String { switch(value) { case 0...0.6: return "sun.min" case 0.6...1: return "sun.max" default: return "sun.min" } } func Type2Name(_ type: SneakContentType) -> String { switch(type) { case .volume: return "Volume" case .brightness: return "Brightness" case .backlight: return "Backlight" case .mic: return "Mic" case .bluetoothAudio: return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth" default: return "" } } } #Preview { InlineHUD(type: .constant(.brightness), value: .constant(0.4), icon: .constant(""), hoverAnimation: .constant(false), gestureProgress: .constant(0)) .padding(.horizontal, 8) .background(Color.black) .padding() .environmentObject(DynamicIslandViewModel()) }