From 53f8b297c94cf6b0de1b77d74afa0eb85b5eddc5 Mon Sep 17 00:00:00 2001 From: delli Date: Thu, 8 Jan 2026 16:51:12 +0100 Subject: [PATCH] just 2 small features --- DynamicIsland/ContentView.swift | 4 + .../DynamicIslandViewCoordinator.swift | 6 +- DynamicIsland/Localizable.xcstrings | 3 + .../Live activities/InlineHUD.swift | 20 ++++- .../LockScreenLiveActivityOverlay.swift | 14 ++- .../components/Settings/SettingsView.swift | 5 ++ DynamicIsland/managers/CapsLockManager.swift | 85 +++++++++++++++++++ .../LockScreenLiveActivityWindowManager.swift | 2 +- DynamicIsland/models/Constants.swift | 4 + 9 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 DynamicIsland/managers/CapsLockManager.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 206c6a2..bbf2eaf 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -28,6 +28,7 @@ struct ContentView: View { @ObservedObject var privacyManager = PrivacyIndicatorManager.shared @ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared @ObservedObject var lockScreenManager = LockScreenManager.shared + @ObservedObject var capsLockManager = CapsLockManager.shared @State private var downloadManager = DownloadManager.shared @Default(.enableStatsFeature) var enableStatsFeature @@ -485,6 +486,9 @@ struct ContentView: View { } 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) + } 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) } 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/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 2bc22e6..66aec7e 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -24,6 +24,7 @@ enum SneakContentType { case bluetoothAudio case privacy case lockScreen + case capsLock } struct sneakPeek { @@ -190,7 +191,7 @@ class DynamicIslandViewCoordinator: ObservableObject { return } DispatchQueue.main.async { - withAnimation(.smooth) { + withAnimation(.smooth(duration: 0.3)) { self.sneakPeek.show = status self.sneakPeek.type = type self.sneakPeek.value = value @@ -205,6 +206,9 @@ class DynamicIslandViewCoordinator: ObservableObject { // Helper function to manage sneakPeek timer using Swift Concurrency private func scheduleSneakPeekHide(after duration: TimeInterval) { sneakPeekTask?.cancel() + + // Don't schedule auto-hide if duration is infinite (for persistent indicators like Caps Lock) + guard duration.isFinite else { return } sneakPeekTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 459d989..abf9094 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -4622,6 +4622,9 @@ } } } + }, + "Caps Lock" : { + }, "Center" : { "extractionState" : "stale", diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index fed58ea..4e4a2ec 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -128,6 +128,12 @@ struct InlineHUD: View { .symbolRenderingMode(.hierarchical) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) + case .capsLock: + Image(systemName: "capslock.fill") + .symbolRenderingMode(.hierarchical) + .contentTransition(.interpolate) + .frame(width: 20, height: 15, alignment: .center) + .foregroundStyle(Defaults[.capsLockIndicatorUseGreenColor] ? .green : .white) default: EmptyView() } @@ -147,7 +153,7 @@ struct InlineHUD: View { frameWidth: infoWidth ) } - } else { + } else if type != .capsLock { Text(Type2Name(type)) .font(.subheadline) .fontWeight(.medium) @@ -180,6 +186,16 @@ struct InlineHUD: View { .multilineTextAlignment(.trailing) .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) } else if (type == .bluetoothAudio) { if hasBatteryLevel { let indicatorSpacing: CGFloat = { @@ -374,6 +390,8 @@ struct InlineHUD: View { return "Mic" case .bluetoothAudio: return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth" + case .capsLock: + return "Caps Lock" default: return "" } diff --git a/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift b/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift index 7b0b796..50aba39 100644 --- a/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift +++ b/DynamicIsland/components/LockScreen/LockScreenLiveActivityOverlay.swift @@ -26,6 +26,8 @@ struct LockScreenLiveActivityOverlay: View { Self.collapsedScale(for: notchSize) } + @State private var isHovering: Bool = false + var body: some View { HStack(spacing: 0) { Color.clear @@ -52,8 +54,18 @@ struct LockScreenLiveActivityOverlay: View { ) ) .frame(width: totalWidth, height: notchSize.height) - .scaleEffect(x: max(model.scale, collapsedScale), y: 1, anchor: .center) + .scaleEffect(x: max(model.scale, collapsedScale) * (isHovering ? 1.03 : 1.0), + y: 1 * (isHovering ? 1.03 : 1.0), + anchor: .center) .opacity(model.opacity) + .onHover { hovering in + withAnimation(.smooth(duration: 0.2)) { + isHovering = hovering + } + if hovering { + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default) + } + } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 59de231..51b7f39 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1735,6 +1735,11 @@ 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) diff --git a/DynamicIsland/managers/CapsLockManager.swift b/DynamicIsland/managers/CapsLockManager.swift new file mode 100644 index 0000000..49d5c9e --- /dev/null +++ b/DynamicIsland/managers/CapsLockManager.swift @@ -0,0 +1,85 @@ +// +// CapsLockManager.swift +// DynamicIsland +// +// Created for Caps Lock indicator feature +// Monitors Caps Lock state and integrates with Dynamic Island +// + +import Foundation +import Combine +import AppKit +import Defaults + +@MainActor +class CapsLockManager: ObservableObject { + static let shared = CapsLockManager() + + @Published var isCapsLockActive: Bool = false + + private var localEventMonitor: Any? + private var globalEventMonitor: Any? + private let coordinator = DynamicIslandViewCoordinator.shared + + private init() { + // Get initial state + isCapsLockActive = NSEvent.modifierFlags.contains(.capsLock) + + // Monitor flag changes when app is focused + localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + self?.handleFlagsChanged(event) + return event + } + + // Monitor flag changes globally (even when app is not focused) + globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + Task { @MainActor in + self?.handleFlagsChanged(event) + } + } + + print("CapsLockManager: ✅ Initialized with Caps Lock \(isCapsLockActive ? "ON" : "OFF")") + } + + deinit { + if let monitor = localEventMonitor { + NSEvent.removeMonitor(monitor) + } + if let monitor = globalEventMonitor { + NSEvent.removeMonitor(monitor) + } + } + + private func handleFlagsChanged(_ event: NSEvent) { + let newState = event.modifierFlags.contains(.capsLock) + + guard newState != isCapsLockActive else { return } + + isCapsLockActive = newState + + print("CapsLockManager: Caps Lock \(newState ? "ACTIVATED" : "DEACTIVATED")") + + // Only show/hide if feature is enabled + guard Defaults[.enableCapsLockIndicator] else { return } + + if newState { + // Show inline indicator + coordinator.toggleSneakPeek( + status: true, + type: .capsLock, + duration: .infinity, // Stay visible until deactivated + value: 1.0, + icon: "" + ) + } else { + // Hide indicator + coordinator.toggleSneakPeek( + status: false, + type: .capsLock, + duration: 0, + value: 0, + icon: "" + ) + } + } +} diff --git a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift index 4cc22a2..dda84e4 100644 --- a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift +++ b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift @@ -68,7 +68,7 @@ class LockScreenLiveActivityWindowManager { window.isOpaque = false window.backgroundColor = .clear window.isReleasedWhenClosed = false - window.ignoresMouseEvents = true + window.ignoresMouseEvents = false window.hasShadow = false window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4f00e16..718a97c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -738,6 +738,10 @@ extension Defaults.Keys { static let enableLockScreenLiveActivity = Key("enableLockScreenLiveActivity", default: true) static let enableLockSounds = Key("enableLockSounds", default: true) + // MARK: Caps Lock Indicator + static let enableCapsLockIndicator = Key("enableCapsLockIndicator", default: true) + static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) + // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false)