diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index bbf2eaf..ebcc50e 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -485,10 +485,14 @@ struct ContentView: View { .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 != .volume || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) - .transition(.opacity) + .transition( + coordinator.sneakPeek.type == .capsLock + ? .move(edge: .trailing).combined(with: .opacity) + : .opacity + ) } else if vm.notchState == .closed && capsLockManager.isCapsLockActive && Defaults[.enableCapsLockIndicator] && !vm.hideOnClosed && !lockScreenManager.isLocked { InlineHUD(type: .constant(.capsLock), value: .constant(1.0), icon: .constant(""), hoverAnimation: $isHovering, gestureProgress: $gestureProgress) - .transition(.opacity) + .transition(.move(edge: .trailing).combined(with: .opacity)) } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked { MusicLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index abf9094..84fc92e 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2362,6 +2362,9 @@ } } } + }, + "Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls." : { + }, "Adds the AirPlay/route picker button back to the customizable controls palette." : { @@ -4625,6 +4628,9 @@ }, "Caps Lock" : { + }, + "Caps Lock Indicator" : { + }, "Center" : { "extractionState" : "stale", diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 4e4a2ec..217b465 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -25,6 +25,7 @@ struct InlineHUD: View { @Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText @Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee @Default(.enableMinimalisticUI) var enableMinimalisticUI + @Default(.showCapsLockLabel) var showCapsLockLabel @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" @@ -32,55 +33,87 @@ struct InlineHUD: View { var body: some View { let useCircularIndicator = useCircularBluetoothBatteryIndicator let hasBatteryLevel = value > 0 + let capsLockAccentColor: Color = Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white let baseInfoWidth: CGFloat = { - guard type == .bluetoothAudio else { return 100 } - if showBluetoothDeviceNameMarquee { - return enableMinimalisticUI ? 128 : 140 + if type == .bluetoothAudio { + if showBluetoothDeviceNameMarquee { + return enableMinimalisticUI ? 128 : 140 + } + return enableMinimalisticUI ? 64 : 72 } - return enableMinimalisticUI ? 64 : 72 + + if type == .capsLock && !showCapsLockLabel { + return enableMinimalisticUI ? 56 : 64 + } + + return 100 }() let infoWidth: CGFloat = { var width = baseInfoWidth + gestureProgress / 2 if !hoverAnimation { width -= 8 } let minimum: CGFloat = { - guard type == .bluetoothAudio else { return 88 } - if showBluetoothDeviceNameMarquee { - return enableMinimalisticUI ? 112 : 120 + if type == .bluetoothAudio { + if showBluetoothDeviceNameMarquee { + return enableMinimalisticUI ? 112 : 120 + } + return enableMinimalisticUI ? 56 : 68 } - return enableMinimalisticUI ? 56 : 68 + + if type == .capsLock && !showCapsLockLabel { + return enableMinimalisticUI ? 44 : 52 + } + + return 88 }() return max(width, minimum) }() let baseTrailingWidth: CGFloat = { - guard type == .bluetoothAudio else { return 100 } - if !hasBatteryLevel { - return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 104 : 118) : (enableMinimalisticUI ? 74 : 88) + if type == .bluetoothAudio { + if !hasBatteryLevel { + return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 104 : 118) : (enableMinimalisticUI ? 74 : 88) + } + + if useCircularIndicator { + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 108 : 120) : (enableMinimalisticUI ? 72 : 84) + } + + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 118 : 136) : (enableMinimalisticUI ? 92 : 108) } - if useCircularIndicator { - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 108 : 120) : (enableMinimalisticUI ? 72 : 84) + if type == .capsLock { + if showCapsLockLabel { + return enableMinimalisticUI ? 84 : 96 + } + return 0 } - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 118 : 136) : (enableMinimalisticUI ? 92 : 108) + return 100 }() let trailingWidth: CGFloat = { var width = baseTrailingWidth + gestureProgress / 2 if !hoverAnimation { width -= 8 } let minimum: CGFloat = { - guard type == .bluetoothAudio else { return 90 } - if !hasBatteryLevel { - return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 96 : 110) : (enableMinimalisticUI ? 62 : 88) + if type == .bluetoothAudio { + if !hasBatteryLevel { + return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 96 : 110) : (enableMinimalisticUI ? 62 : 88) + } + + if useCircularIndicator { + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 92 : 110) : (enableMinimalisticUI ? 56 : 72) + } + + return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 104 : 120) : (enableMinimalisticUI ? 72 : 90) } - if useCircularIndicator { - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 92 : 110) : (enableMinimalisticUI ? 56 : 72) + if type == .capsLock { + return showCapsLockLabel ? (enableMinimalisticUI ? 68 : 80) : 0 } - return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 104 : 120) : (enableMinimalisticUI ? 72 : 90) + return 90 }() return max(width, minimum) }() @@ -133,7 +166,7 @@ struct InlineHUD: View { .symbolRenderingMode(.hierarchical) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) - .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) + .foregroundStyle(capsLockAccentColor) default: EmptyView() } @@ -187,15 +220,17 @@ struct InlineHUD: View { .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) } else if (type == .capsLock) { - Text("Caps Lock") - .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) - .font(.subheadline) - .fontWeight(.medium) - .lineLimit(1) - .allowsTightening(true) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity, alignment: .trailing) - .contentTransition(.interpolate) + if showCapsLockLabel { + Text("Caps Lock") + .foregroundStyle(capsLockAccentColor) + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(1) + .allowsTightening(true) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity, alignment: .trailing) + .contentTransition(.interpolate) + } } else if (type == .bluetoothAudio) { if hasBatteryLevel { let indicatorSpacing: CGFloat = { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 51b7f39..34d406b 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1735,11 +1735,6 @@ private struct DevicesSettingsView: View { Section { Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections) .settingsHighlight(id: highlightID("Show Bluetooth device connections")) - Defaults.Toggle("Show Caps Lock Indicator", key: .enableCapsLockIndicator) - .settingsHighlight(id: highlightID("Show Caps Lock Indicator")) - Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) - .disabled(!Defaults[.enableCapsLockIndicator]) - .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator) .settingsHighlight(id: highlightID("Use circular battery indicator")) Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText) @@ -2644,6 +2639,23 @@ struct LiveActivitiesSettings: View { Text("Listens for Focus session changes via distributed notifications") } + Section { + Defaults.Toggle("Show Caps Lock Indicator", key: .enableCapsLockIndicator) + .settingsHighlight(id: highlightID("Show Caps Lock Indicator")) + + Defaults.Toggle("Show Caps Lock label", key: .showCapsLockLabel) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Show Caps Lock label")) + + Defaults.Toggle("Use green color for Caps Lock text", key: .capsLockIndicatorUseGreenColor) + .disabled(!Defaults[.enableCapsLockIndicator]) + .settingsHighlight(id: highlightID("Use green color for Caps Lock text")) + } header: { + Text("Caps Lock Indicator") + } footer: { + Text("Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls.") + } + Section { Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection) .settingsHighlight(id: highlightID("Enable Camera Detection")) diff --git a/DynamicIsland/managers/CapsLockManager.swift b/DynamicIsland/managers/CapsLockManager.swift index 49d5c9e..489d037 100644 --- a/DynamicIsland/managers/CapsLockManager.swift +++ b/DynamicIsland/managers/CapsLockManager.swift @@ -10,6 +10,7 @@ import Foundation import Combine import AppKit import Defaults +import SwiftUI @MainActor class CapsLockManager: ObservableObject { @@ -20,6 +21,7 @@ class CapsLockManager: ObservableObject { private var localEventMonitor: Any? private var globalEventMonitor: Any? private let coordinator = DynamicIslandViewCoordinator.shared + private let capsLockAnimation = Animation.spring(response: 0.32, dampingFraction: 0.85) private init() { // Get initial state @@ -55,7 +57,9 @@ class CapsLockManager: ObservableObject { guard newState != isCapsLockActive else { return } - isCapsLockActive = newState + withAnimation(capsLockAnimation) { + isCapsLockActive = newState + } print("CapsLockManager: Caps Lock \(newState ? "ACTIVATED" : "DEACTIVATED")") diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 718a97c..b54873d 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -741,6 +741,7 @@ extension Defaults.Keys { // MARK: Caps Lock Indicator static let enableCapsLockIndicator = Key("enableCapsLockIndicator", default: true) static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) + static let showCapsLockLabel = Key("showCapsLockLabel", default: true) // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false)