From 166c053621342f26ac98df8009ee0a7350df8512 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 8 Feb 2026 00:13:15 +0530 Subject: [PATCH] Update Notch slider for volume/brightness feedback by kryoscopic --- DynamicIsland/ContentView.swift | 4 +- .../Notch/MinimalisticMusicPlayerView.swift | 128 +++++++++++++++++- .../components/Notch/NotchHomeView.swift | 126 ++++++++++++++++- 3 files changed, 253 insertions(+), 5 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index cc795e4..033e69a 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -583,7 +583,7 @@ struct ContentView: View { .frame(width: 76, alignment: .trailing) } .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) - } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { + } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && !coordinator.sneakPeek.type.isExtensionPayload && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) .transition( coordinator.sneakPeek.type == .capsLock @@ -631,7 +631,7 @@ struct ContentView: View { } if coordinator.sneakPeek.show { - if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && !Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { + if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && !Defaults[.inlineHUD] && ((coordinator.sneakPeek.type != .volume && coordinator.sneakPeek.type != .brightness && coordinator.sneakPeek.type != .backlight) || vm.notchState == .closed) { SystemEventIndicatorModifier(eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { _ in // }) diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index f5cf76a..6fa3009 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -35,6 +35,9 @@ struct MinimalisticMusicPlayerView: View { @ObservedObject private var reminderManager = ReminderLiveActivityManager.shared @ObservedObject private var timerManager = TimerManager.shared @ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared + @State private var hudValue: Double = 0 + @State private var hudDragging: Bool = false + @State private var hudLastDragged: Date = .distantPast @Default(.enableReminderLiveActivity) private var enableReminderLiveActivity @Default(.enableLyrics) private var enableLyrics @Default(.timerPresets) private var timerPresets @@ -85,8 +88,13 @@ struct MinimalisticMusicPlayerView: View { .padding(.top, 6) // Compact playback controls - playbackControls - .padding(.top, 4) + if shouldShowControlHUDRow { + controlHUDRow + .padding(.top, 4) + } else { + playbackControls + .padding(.top, 4) + } if enableLyrics { lyricsView @@ -672,6 +680,122 @@ private struct MinimalisticReminderDetailsView: View { .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 2) } + + private var shouldShowControlHUDRow: Bool { + guard vm.notchState == .open else { return false } + guard coordinator.sneakPeek.show else { return false } + guard Defaults[.enableSystemHUD] else { return false } + guard !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] else { return false } + + switch coordinator.sneakPeek.type { + case .volume: + return Defaults[.enableVolumeHUD] + case .brightness: + return Defaults[.enableBrightnessHUD] + case .backlight: + return Defaults[.enableKeyboardBacklightHUD] + default: + return false + } + } + + private var controlHUDRow: some View { + HStack(alignment: .center, spacing: 10) { + if !controlLeftIconName.isEmpty { + Image(systemName: controlLeftIconName) + .font(.system(size: 16, weight: .semibold)) + .frame(width: 22, height: 22, alignment: .center) + } + + controlHUDSlider + + if !controlRightIconName.isEmpty { + Image(systemName: controlRightIconName) + .font(.system(size: 16, weight: .semibold)) + .frame(width: 22, height: 22, alignment: .center) + } + } + .frame(maxWidth: .infinity, alignment: .center) + .frame(height: 60, alignment: .center) + .onAppear { syncHUDValueIfNeeded(force: true) } + .onChange(of: coordinator.sneakPeek.value) { _, _ in + syncHUDValueIfNeeded(force: false) + } + } + + private var controlHUDSlider: some View { + VStack(spacing: 0) { + Spacer(minLength: 0) + CustomSlider( + value: Binding( + get: { hudValue }, + set: { newValue in + hudValue = newValue + updateControlHUDValue(newValue) + } + ), + range: 0...1, + color: .white, + dragging: $hudDragging, + lastDragged: $hudLastDragged, + onValueChange: { newValue in + updateControlHUDValue(newValue) + }, + thumbSize: 10, + restingTrackHeight: 4, + draggingTrackHeight: 7 + ) + .frame(height: 7) + Spacer(minLength: 0) + } + .frame(height: 22) + } + + private func syncHUDValueIfNeeded(force: Bool) { + guard shouldShowControlHUDRow else { return } + guard force || !hudDragging else { return } + hudValue = Double(coordinator.sneakPeek.value) + } + + private func updateControlHUDValue(_ newValue: Double) { + let clamped = max(0, min(1, newValue)) + switch coordinator.sneakPeek.type { + case .volume: + SystemVolumeController.shared.setVolume(Float(clamped)) + case .brightness: + SystemBrightnessController.shared.setBrightness(Float(clamped)) + case .backlight: + SystemKeyboardBacklightController.shared.setLevel(Float(clamped)) + default: + break + } + } + + private var controlLeftIconName: String { + switch coordinator.sneakPeek.type { + case .volume: + return SystemVolumeController.shared.isMuted ? "speaker.slash" : "speaker.wave.1" + case .brightness: + return "sun.min.fill" + case .backlight: + return "light.min" + default: + return "" + } + } + + private var controlRightIconName: String { + switch coordinator.sneakPeek.type { + case .volume: + return SystemVolumeController.shared.isMuted ? "" : "speaker.wave.3" + case .brightness: + return "sun.max.fill" + case .backlight: + return "light.max" + default: + return "" + } + } private var playPauseButton: some View { MinimalisticSquircircleButton( diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 9cc8f9b..7e68b84 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -129,10 +129,15 @@ struct AlbumArtView: View { } struct MusicControlsView: View { + @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var musicManager = MusicManager.shared + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @State private var sliderValue: Double = 0 @State private var dragging: Bool = false @State private var lastDragged: Date = .distantPast + @State private var hudValue: Double = 0 + @State private var hudDragging: Bool = false + @State private var hudLastDragged: Date = .distantPast @Default(.showShuffleAndRepeat) private var showCustomControls @Default(.musicControlSlots) private var slotConfig @Default(.showMediaOutputControl) private var showMediaOutputControl @@ -144,7 +149,11 @@ struct MusicControlsView: View { var body: some View { VStack(alignment: .leading) { songInfoAndSlider - playbackControls + if shouldShowControlHUDRow { + controlHUDRow + } else { + playbackControls + } } .buttonStyle(PlainButtonStyle()) .frame(maxWidth: .infinity, alignment: .leading) @@ -238,6 +247,121 @@ struct MusicControlsView: View { .frame(maxWidth: .infinity, alignment: .center) } + private var shouldShowControlHUDRow: Bool { + guard vm.notchState == .open else { return false } + guard coordinator.sneakPeek.show else { return false } + guard Defaults[.enableSystemHUD] else { return false } + guard !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] else { return false } + + switch coordinator.sneakPeek.type { + case .volume: + return Defaults[.enableVolumeHUD] + case .brightness: + return Defaults[.enableBrightnessHUD] + case .backlight: + return Defaults[.enableKeyboardBacklightHUD] + default: + return false + } + } + + private var controlHUDRow: some View { + HStack(alignment: .center, spacing: 10) { + if !controlLeftIconName.isEmpty { + Image(systemName: controlLeftIconName) + .font(.system(size: 16, weight: .semibold)) + .frame(width: 22, height: 22, alignment: .center) + } + + controlHUDSlider + + if !controlRightIconName.isEmpty { + Image(systemName: controlRightIconName) + .font(.system(size: 16, weight: .semibold)) + .frame(width: 22, height: 22, alignment: .center) + } + } + .frame(maxWidth: .infinity, alignment: .center) + .onAppear { syncHUDValueIfNeeded(force: true) } + .onChange(of: coordinator.sneakPeek.value) { _, _ in + syncHUDValueIfNeeded(force: false) + } + } + + private var controlHUDSlider: some View { + VStack(spacing: 0) { + Spacer(minLength: 0) + CustomSlider( + value: Binding( + get: { hudValue }, + set: { newValue in + hudValue = newValue + updateControlHUDValue(newValue) + } + ), + range: 0...1, + color: .white, + dragging: $hudDragging, + lastDragged: $hudLastDragged, + onValueChange: { newValue in + updateControlHUDValue(newValue) + }, + thumbSize: 10, + restingTrackHeight: 4, + draggingTrackHeight: 7 + ) + .frame(height: 7) + Spacer(minLength: 0) + } + .frame(height: 22) + } + + private func syncHUDValueIfNeeded(force: Bool) { + guard shouldShowControlHUDRow else { return } + guard force || !hudDragging else { return } + hudValue = Double(coordinator.sneakPeek.value) + } + + private func updateControlHUDValue(_ newValue: Double) { + let clamped = max(0, min(1, newValue)) + switch coordinator.sneakPeek.type { + case .volume: + SystemVolumeController.shared.setVolume(Float(clamped)) + case .brightness: + SystemBrightnessController.shared.setBrightness(Float(clamped)) + case .backlight: + SystemKeyboardBacklightController.shared.setLevel(Float(clamped)) + default: + break + } + } + + private var controlLeftIconName: String { + switch coordinator.sneakPeek.type { + case .volume: + return SystemVolumeController.shared.isMuted ? "speaker.slash" : "speaker.wave.1" + case .brightness: + return "sun.min.fill" + case .backlight: + return "light.min" + default: + return "" + } + } + + private var controlRightIconName: String { + switch coordinator.sneakPeek.type { + case .volume: + return SystemVolumeController.shared.isMuted ? "" : "speaker.wave.3" + case .brightness: + return "sun.max.fill" + case .backlight: + return "light.max" + default: + return "" + } + } + private var brandAccentColor: Color { musicManager.brandAccentColor }