mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #239 from Ebullioscopic/feature/liveactivity
Update CAPS Lock live activity
This commit is contained in:
commit
c2d9b851d6
10 changed files with 273 additions and 25 deletions
|
|
@ -28,6 +28,7 @@ struct ContentView: View {
|
||||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||||
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
||||||
@ObservedObject var lockScreenManager = LockScreenManager.shared
|
@ObservedObject var lockScreenManager = LockScreenManager.shared
|
||||||
|
@ObservedObject var capsLockManager = CapsLockManager.shared
|
||||||
@State private var downloadManager = DownloadManager.shared
|
@State private var downloadManager = DownloadManager.shared
|
||||||
|
|
||||||
@Default(.enableStatsFeature) var enableStatsFeature
|
@Default(.enableStatsFeature) var enableStatsFeature
|
||||||
|
|
@ -484,7 +485,14 @@ struct ContentView: View {
|
||||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
.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) {
|
} 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)
|
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(.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 {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked {
|
||||||
MusicLiveActivity()
|
MusicLiveActivity()
|
||||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed {
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
Defaults.Keys.migrateProgressBarStyle()
|
Defaults.Keys.migrateProgressBarStyle()
|
||||||
Defaults.Keys.migrateMusicAuxControls()
|
Defaults.Keys.migrateMusicAuxControls()
|
||||||
Defaults.Keys.migrateMusicControlSlots()
|
Defaults.Keys.migrateMusicControlSlots()
|
||||||
|
Defaults.Keys.migrateCapsLockTintMode()
|
||||||
|
|
||||||
// Initialize idle animations (load bundled + built-in face)
|
// Initialize idle animations (load bundled + built-in face)
|
||||||
idleAnimationManager.initializeDefaultAnimations()
|
idleAnimationManager.initializeDefaultAnimations()
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ enum SneakContentType {
|
||||||
case bluetoothAudio
|
case bluetoothAudio
|
||||||
case privacy
|
case privacy
|
||||||
case lockScreen
|
case lockScreen
|
||||||
|
case capsLock
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sneakPeek {
|
struct sneakPeek {
|
||||||
|
|
@ -190,7 +191,7 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth(duration: 0.3)) {
|
||||||
self.sneakPeek.show = status
|
self.sneakPeek.show = status
|
||||||
self.sneakPeek.type = type
|
self.sneakPeek.type = type
|
||||||
self.sneakPeek.value = value
|
self.sneakPeek.value = value
|
||||||
|
|
@ -205,6 +206,9 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
// Helper function to manage sneakPeek timer using Swift Concurrency
|
// Helper function to manage sneakPeek timer using Swift Concurrency
|
||||||
private func scheduleSneakPeekHide(after duration: TimeInterval) {
|
private func scheduleSneakPeekHide(after duration: TimeInterval) {
|
||||||
sneakPeekTask?.cancel()
|
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
|
sneakPeekTask = Task { [weak self] in
|
||||||
try? await Task.sleep(for: .seconds(duration))
|
try? await Task.sleep(for: .seconds(duration))
|
||||||
|
|
|
||||||
|
|
@ -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." : {
|
"Adds the AirPlay/route picker button back to the customizable controls palette." : {
|
||||||
|
|
||||||
|
|
@ -4622,6 +4625,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Caps Lock" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Caps Lock color" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Caps Lock Indicator" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Center" : {
|
"Center" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ struct InlineHUD: View {
|
||||||
@Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText
|
@Default(.showBluetoothBatteryPercentageText) var showBluetoothBatteryPercentageText
|
||||||
@Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee
|
@Default(.showBluetoothDeviceNameMarquee) var showBluetoothDeviceNameMarquee
|
||||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
@Default(.showCapsLockLabel) var showCapsLockLabel
|
||||||
|
@Default(.capsLockIndicatorTintMode) var capsLockTintMode
|
||||||
@ObservedObject var bluetoothManager = BluetoothAudioManager.shared
|
@ObservedObject var bluetoothManager = BluetoothAudioManager.shared
|
||||||
|
|
||||||
@State private var displayName: String = ""
|
@State private var displayName: String = ""
|
||||||
|
|
@ -32,55 +34,87 @@ struct InlineHUD: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let useCircularIndicator = useCircularBluetoothBatteryIndicator
|
let useCircularIndicator = useCircularBluetoothBatteryIndicator
|
||||||
let hasBatteryLevel = value > 0
|
let hasBatteryLevel = value > 0
|
||||||
|
let capsLockAccentColor = capsLockTintMode.color
|
||||||
|
|
||||||
let baseInfoWidth: CGFloat = {
|
let baseInfoWidth: CGFloat = {
|
||||||
guard type == .bluetoothAudio else { return 100 }
|
if type == .bluetoothAudio {
|
||||||
if showBluetoothDeviceNameMarquee {
|
if showBluetoothDeviceNameMarquee {
|
||||||
return enableMinimalisticUI ? 128 : 140
|
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 = {
|
let infoWidth: CGFloat = {
|
||||||
var width = baseInfoWidth + gestureProgress / 2
|
var width = baseInfoWidth + gestureProgress / 2
|
||||||
if !hoverAnimation { width -= 8 }
|
if !hoverAnimation { width -= 8 }
|
||||||
let minimum: CGFloat = {
|
let minimum: CGFloat = {
|
||||||
guard type == .bluetoothAudio else { return 88 }
|
if type == .bluetoothAudio {
|
||||||
if showBluetoothDeviceNameMarquee {
|
if showBluetoothDeviceNameMarquee {
|
||||||
return enableMinimalisticUI ? 112 : 120
|
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)
|
return max(width, minimum)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let baseTrailingWidth: CGFloat = {
|
let baseTrailingWidth: CGFloat = {
|
||||||
guard type == .bluetoothAudio else { return 100 }
|
if type == .bluetoothAudio {
|
||||||
if !hasBatteryLevel {
|
if !hasBatteryLevel {
|
||||||
return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 104 : 118) : (enableMinimalisticUI ? 74 : 88)
|
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 {
|
if type == .capsLock {
|
||||||
return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 108 : 120) : (enableMinimalisticUI ? 72 : 84)
|
if showCapsLockLabel {
|
||||||
|
return enableMinimalisticUI ? 84 : 96
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 118 : 136) : (enableMinimalisticUI ? 92 : 108)
|
return 100
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let trailingWidth: CGFloat = {
|
let trailingWidth: CGFloat = {
|
||||||
var width = baseTrailingWidth + gestureProgress / 2
|
var width = baseTrailingWidth + gestureProgress / 2
|
||||||
if !hoverAnimation { width -= 8 }
|
if !hoverAnimation { width -= 8 }
|
||||||
let minimum: CGFloat = {
|
let minimum: CGFloat = {
|
||||||
guard type == .bluetoothAudio else { return 90 }
|
if type == .bluetoothAudio {
|
||||||
if !hasBatteryLevel {
|
if !hasBatteryLevel {
|
||||||
return showBluetoothDeviceNameMarquee ? (enableMinimalisticUI ? 96 : 110) : (enableMinimalisticUI ? 62 : 88)
|
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 {
|
if type == .capsLock {
|
||||||
return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 92 : 110) : (enableMinimalisticUI ? 56 : 72)
|
return showCapsLockLabel ? (enableMinimalisticUI ? 68 : 80) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return showBluetoothBatteryPercentageText ? (enableMinimalisticUI ? 104 : 120) : (enableMinimalisticUI ? 72 : 90)
|
return 90
|
||||||
}()
|
}()
|
||||||
return max(width, minimum)
|
return max(width, minimum)
|
||||||
}()
|
}()
|
||||||
|
|
@ -128,6 +162,12 @@ struct InlineHUD: View {
|
||||||
.symbolRenderingMode(.hierarchical)
|
.symbolRenderingMode(.hierarchical)
|
||||||
.contentTransition(.interpolate)
|
.contentTransition(.interpolate)
|
||||||
.frame(width: 20, height: 15, alignment: .center)
|
.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(capsLockAccentColor)
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +187,7 @@ struct InlineHUD: View {
|
||||||
frameWidth: infoWidth
|
frameWidth: infoWidth
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else if type != .capsLock {
|
||||||
Text(Type2Name(type))
|
Text(Type2Name(type))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
|
|
@ -180,6 +220,18 @@ struct InlineHUD: View {
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
.contentTransition(.interpolate)
|
.contentTransition(.interpolate)
|
||||||
|
} else if (type == .capsLock) {
|
||||||
|
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) {
|
} else if (type == .bluetoothAudio) {
|
||||||
if hasBatteryLevel {
|
if hasBatteryLevel {
|
||||||
let indicatorSpacing: CGFloat = {
|
let indicatorSpacing: CGFloat = {
|
||||||
|
|
@ -374,6 +426,8 @@ struct InlineHUD: View {
|
||||||
return "Mic"
|
return "Mic"
|
||||||
case .bluetoothAudio:
|
case .bluetoothAudio:
|
||||||
return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth"
|
return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth"
|
||||||
|
case .capsLock:
|
||||||
|
return "Caps Lock"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ struct LockScreenLiveActivityOverlay: View {
|
||||||
Self.collapsedScale(for: notchSize)
|
Self.collapsedScale(for: notchSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@State private var isHovering: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Color.clear
|
Color.clear
|
||||||
|
|
@ -52,8 +54,18 @@ struct LockScreenLiveActivityOverlay: View {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.frame(width: totalWidth, height: notchSize.height)
|
.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)
|
.opacity(model.opacity)
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation(.smooth(duration: 0.2)) {
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
if hovering {
|
||||||
|
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2538,6 +2538,7 @@ struct LiveActivitiesSettings: View {
|
||||||
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
||||||
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
|
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
|
||||||
@Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent
|
@Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent
|
||||||
|
@Default(.capsLockIndicatorTintMode) var capsLockTintMode
|
||||||
|
|
||||||
private func highlightID(_ title: String) -> String {
|
private func highlightID(_ title: String) -> String {
|
||||||
SettingsTab.liveActivities.highlightID(for: title)
|
SettingsTab.liveActivities.highlightID(for: title)
|
||||||
|
|
@ -2639,6 +2640,28 @@ struct LiveActivitiesSettings: View {
|
||||||
Text("Listens for Focus session changes via distributed notifications")
|
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"))
|
||||||
|
|
||||||
|
Picker("Caps Lock color", selection: $capsLockTintMode) {
|
||||||
|
ForEach(CapsLockIndicatorTintMode.allCases) { mode in
|
||||||
|
Text(mode.displayName).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.disabled(!Defaults[.enableCapsLockIndicator])
|
||||||
|
.settingsHighlight(id: highlightID("Caps Lock color"))
|
||||||
|
} header: {
|
||||||
|
Text("Caps Lock Indicator")
|
||||||
|
} footer: {
|
||||||
|
Text("Adds a notch HUD when Caps Lock is enabled, with optional label and tint controls.")
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection)
|
Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection)
|
||||||
.settingsHighlight(id: highlightID("Enable Camera Detection"))
|
.settingsHighlight(id: highlightID("Enable Camera Detection"))
|
||||||
|
|
|
||||||
89
DynamicIsland/managers/CapsLockManager.swift
Normal file
89
DynamicIsland/managers/CapsLockManager.swift
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@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 let capsLockAnimation = Animation.spring(response: 0.32, dampingFraction: 0.85)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
withAnimation(capsLockAnimation) {
|
||||||
|
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: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,7 @@ class LockScreenLiveActivityWindowManager {
|
||||||
window.isOpaque = false
|
window.isOpaque = false
|
||||||
window.backgroundColor = .clear
|
window.backgroundColor = .clear
|
||||||
window.isReleasedWhenClosed = false
|
window.isReleasedWhenClosed = false
|
||||||
window.ignoresMouseEvents = true
|
window.ignoresMouseEvents = false
|
||||||
window.hasShadow = false
|
window.hasShadow = false
|
||||||
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||||
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,36 @@ enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
var id: String { self.rawValue }
|
var id: String { self.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CapsLockIndicatorTintMode: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
|
case green
|
||||||
|
case accent
|
||||||
|
case white
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .green:
|
||||||
|
return "Green"
|
||||||
|
case .accent:
|
||||||
|
return "Accent"
|
||||||
|
case .white:
|
||||||
|
return "White"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .green:
|
||||||
|
return .green
|
||||||
|
case .accent:
|
||||||
|
return .accentColor
|
||||||
|
case .white:
|
||||||
|
return .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
|
enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
case hierarchical = "Hierarchical"
|
case hierarchical = "Hierarchical"
|
||||||
case gradient = "Gradient"
|
case gradient = "Gradient"
|
||||||
|
|
@ -738,6 +768,13 @@ extension Defaults.Keys {
|
||||||
static let enableLockScreenLiveActivity = Key<Bool>("enableLockScreenLiveActivity", default: true)
|
static let enableLockScreenLiveActivity = Key<Bool>("enableLockScreenLiveActivity", default: true)
|
||||||
static let enableLockSounds = Key<Bool>("enableLockSounds", default: true)
|
static let enableLockSounds = Key<Bool>("enableLockSounds", default: true)
|
||||||
|
|
||||||
|
// MARK: Caps Lock Indicator
|
||||||
|
static let enableCapsLockIndicator = Key<Bool>("enableCapsLockIndicator", default: true)
|
||||||
|
static let capsLockIndicatorUseGreenColor = Key<Bool>("capsLockIndicatorUseGreenColor", default: false) // Legacy toggle
|
||||||
|
static let capsLockIndicatorTintMode = Key<CapsLockIndicatorTintMode>("capsLockIndicatorTintMode", default: .white)
|
||||||
|
static let didMigrateCapsLockTintMode = Key<Bool>("didMigrateCapsLockTintMode", default: false)
|
||||||
|
static let showCapsLockLabel = Key<Bool>("showCapsLockLabel", default: true)
|
||||||
|
|
||||||
// MARK: ImageService
|
// MARK: ImageService
|
||||||
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
||||||
|
|
||||||
|
|
@ -788,6 +825,14 @@ extension Defaults.Keys {
|
||||||
normalizeMusicAuxControls()
|
normalizeMusicAuxControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func migrateCapsLockTintMode() {
|
||||||
|
guard Defaults[.didMigrateCapsLockTintMode] == false else { return }
|
||||||
|
|
||||||
|
let legacyGreen = Defaults[.capsLockIndicatorUseGreenColor]
|
||||||
|
Defaults[.capsLockIndicatorTintMode] = legacyGreen ? .green : .white
|
||||||
|
Defaults[.didMigrateCapsLockTintMode] = true
|
||||||
|
}
|
||||||
|
|
||||||
static func migrateMusicControlSlots() {
|
static func migrateMusicControlSlots() {
|
||||||
guard Defaults[.didMigrateMusicControlSlots] == false else { return }
|
guard Defaults[.didMigrateMusicControlSlots] == false else { return }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue