From 5e206ec20b7278ae86dae3ebc8f7c25b2d8093c9 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Tue, 12 Aug 2025 14:01:29 +0530 Subject: [PATCH] Add Shortcut Toggle --- DynamicIsland/ContentView.swift | 28 +- DynamicIsland/DynamicIslandApp.swift | 17 ++ DynamicIsland/Localizable.xcstrings | 29 +- .../components/Notch/NotchStatsView.swift | 275 +++++------------- .../components/Settings/SettingsView.swift | 106 ++++++- DynamicIsland/models/Constants.swift | 3 + 6 files changed, 225 insertions(+), 233 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 708d01e..1291285 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -203,6 +203,8 @@ struct ContentView: View { } } .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) + .animation(.easeInOut(duration: 0.4), value: dynamicNotchSize) + .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5) .background(dragDetector) .environmentObject(vm) @@ -309,18 +311,26 @@ struct ContentView: View { ZStack { if vm.notchState == .open { - switch coordinator.currentView { - case .home: - NotchHomeView(albumArtNamespace: albumArtNamespace) - case .shelf: - NotchShelfView() - case .timer: - NotchTimerView() - case .stats: - NotchStatsView() + Group { + switch coordinator.currentView { + case .home: + NotchHomeView(albumArtNamespace: albumArtNamespace) + case .shelf: + NotchShelfView() + case .timer: + NotchTimerView() + case .stats: + NotchStatsView() + } } + .transition(.asymmetric( + insertion: .scale(scale: 0.8).combined(with: .opacity).animation(.easeInOut(duration: 0.4)), + removal: .scale(scale: 0.8).combined(with: .opacity).animation(.easeInOut(duration: 0.4)) + )) + .id(coordinator.currentView) // Force SwiftUI to treat each view as unique } } + .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .zIndex(1) .allowsHitTesting(vm.notchState == .open) .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 72897f9..613c2cd 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -373,6 +373,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in guard let self = self else { return } + + // Only execute if shortcuts are enabled + guard Defaults[.enableShortcuts] else { return } + self.coordinator.toggleSneakPeek( status: !self.coordinator.sneakPeek.show, type: .music, @@ -383,6 +387,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in guard let self = self else { return } + // Only execute if shortcuts are enabled + guard Defaults[.enableShortcuts] else { return } + let mouseLocation = NSEvent.mouseLocation var viewModel = self.vm @@ -418,6 +425,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { KeyboardShortcuts.onKeyDown(for: .startDemoTimer) { [weak self] in guard let self = self else { return } + + // Only execute if shortcuts are enabled + guard Defaults[.enableShortcuts] else { return } + + // Only start timer if the timer feature is enabled + guard Defaults[.enableTimerFeature] else { return } + // Start a 5-minute demo timer TimerManager.shared.startDemoTimer(duration: 300) } @@ -425,6 +439,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { KeyboardShortcuts.onKeyDown(for: .clipboardHistoryPanel) { [weak self] in guard let self = self else { return } + // Only execute if shortcuts are enabled + guard Defaults[.enableShortcuts] else { return } + // Only open clipboard if the feature is enabled guard Defaults[.enableClipboardManager] else { return } diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index a7c7d42..8ec7b0f 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -277,9 +277,15 @@ }, "Clipboard" : { + }, + "Clipboard feature is disabled" : { + }, "Clipboard History" : { + }, + "Clipboard History:" : { + }, "Clipboard Manager" : { @@ -444,6 +450,9 @@ } } } + }, + "Enable global keyboard shortcuts above to customize your shortcuts." : { + }, "Enable graph visibility in Settings → Stats to view performance data." : { @@ -592,6 +601,9 @@ }, "Installed extensions" : { + }, + "Keyboard shortcuts are disabled" : { + }, "Last Updated" : { @@ -727,6 +739,9 @@ }, "Name" : { + }, + "Navigation" : { + }, "Need Restart" : { @@ -818,6 +833,9 @@ }, "Open System Settings" : { + }, + "Opens the clipboard history panel. Default is Cmd+Shift+V (similar to Windows+V on PC). Only works when clipboard feature is enabled." : { + }, "Pause" : { @@ -1035,7 +1053,7 @@ "Start Monitoring" : { }, - "Starts a 5-minute demo timer to test the timer live activity feature." : { + "Starts a 5-minute demo timer to test the timer live activity feature. Only works when timer feature is enabled." : { }, "Stats" : { @@ -1086,6 +1104,9 @@ }, "Timer Feature" : { + }, + "Timer feature is disabled" : { + }, "Timer Sound" : { @@ -1095,6 +1116,9 @@ }, "Toggle Sneak Peek:" : { + }, + "Toggle the Dynamic Island open or closed from anywhere." : { + }, "Transform your notch truly yours" : { @@ -1148,6 +1172,9 @@ }, "What's New" : { + }, + "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { + }, "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 8c2ec38..8bfc75a 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -118,142 +118,31 @@ struct NotchStatsView: View { return graphs } - // New vertical expansion layout system + // Simplified and more consistent layout system @ViewBuilder var statsGridLayout: some View { let graphCount = availableGraphs.count - switch graphCount { - case 1...3: - // Single row for 1-3 graphs - HStack(spacing: 12) { - ForEach(0.. some View { - if let singleData = graphData as? SingleGraphData { - StatCard( - title: singleData.title, - value: singleData.value, - data: singleData.data, - color: singleData.color, - icon: singleData.icon - ) - } else if let dualData = graphData as? DualGraphData { - DualStatCard( - title: dualData.title, - positiveValue: dualData.positiveValue, - negativeValue: dualData.negativeValue, - positiveData: dualData.positiveData, - negativeData: dualData.negativeData, - positiveColor: dualData.positiveColor, - negativeColor: dualData.negativeColor, - icon: dualData.icon - ) - } + UnifiedStatsCard(graphData: graphData) } var body: some View { @@ -297,17 +186,17 @@ struct NotchStatsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } else { - // Stats content with vertical expansion layout + // Stats content with improved animation coordination ZStack(alignment: .topTrailing) { VStack(spacing: 12) { - // New vertical expansion layout + // Simplified consistent layout statsGridLayout } .padding(16) - .animation(.easeInOut(duration: 0.25), value: availableGraphs.count) + .animation(.easeInOut(duration: 0.4), value: availableGraphs.count) .transition(.asymmetric( - insertion: .scale.combined(with: .opacity), - removal: .scale.combined(with: .opacity) + insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)), + removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)) )) // Live indicator and controls in top-right corner @@ -365,25 +254,24 @@ struct NotchStatsView: View { .onDisappear { // Keep monitoring running when tab is not visible } + .animation(.easeInOut(duration: 0.4), value: enableStatsFeature) + .animation(.easeInOut(duration: 0.4), value: availableGraphs.count) } } -struct StatCard: View { - let title: String - let value: String - let data: [Double] - let color: Color - let icon: String +// Unified Stats Card Component - handles both single and dual data types +struct UnifiedStatsCard: View { + let graphData: GraphData var body: some View { VStack(spacing: 6) { - // Header - more compact + // Header - consistent across all card types HStack(spacing: 4) { - Image(systemName: icon) - .foregroundColor(color) + Image(systemName: graphData.icon) + .foregroundColor(graphData.color) .font(.caption2) - Text(title) + Text(graphData.title) .font(.caption2) .fontWeight(.medium) .foregroundColor(.secondary) @@ -391,76 +279,47 @@ struct StatCard: View { Spacer() } - // Current value - larger and more prominent - Text(value) - .font(.title3) - .fontWeight(.bold) - .foregroundColor(.primary) - .frame(height: 22) // Match the height of dual values in DualStatCard - - // Mini graph - use more height - MiniGraph(data: data, color: color) - .frame(height: 50) - } - .padding(10) - .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -struct DualStatCard: View { - let title: String - let positiveValue: String - let negativeValue: String - let positiveData: [Double] - let negativeData: [Double] - let positiveColor: Color - let negativeColor: Color - let icon: String - - var body: some View { - VStack(spacing: 6) { - // Header - HStack(spacing: 4) { - Image(systemName: icon) - .foregroundColor(positiveColor) - .font(.caption2) - - Text(title) - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(.secondary) - - Spacer() + // Values section - adapts based on graph type + Group { + if let singleData = graphData as? SingleGraphData { + Text(singleData.value) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(.primary) + } else if let dualData = graphData as? DualGraphData { + HStack(spacing: 6) { + Text(dualData.positiveValue) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(dualData.positiveColor) + + Text("•") + .font(.caption2) + .foregroundColor(.secondary) + + Text(dualData.negativeValue) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(dualData.negativeColor) + } + } } + .frame(height: 22) // Fixed height for consistent card sizing - // Dual values - horizontal layout to match single value height - HStack(spacing: 8) { - Text(positiveValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(positiveColor) - - Text("•") - .font(.caption2) - .foregroundColor(.secondary) - - Text(negativeValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(negativeColor) + // Graph section - adapts based on graph type + Group { + if let singleData = graphData as? SingleGraphData { + MiniGraph(data: singleData.data, color: singleData.color) + } else if let dualData = graphData as? DualGraphData { + DualQuadrantGraph( + positiveData: dualData.positiveData, + negativeData: dualData.negativeData, + positiveColor: dualData.positiveColor, + negativeColor: dualData.negativeColor + ) + } } - .frame(height: 22) // Match the height of .title3 text in StatCard - - // Dual quadrant graph - DualQuadrantGraph( - positiveData: positiveData, - negativeData: negativeData, - positiveColor: positiveColor, - negativeColor: negativeColor - ) - .frame(height: 50) + .frame(height: 50) // Fixed height for consistent card sizing } .padding(10) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 7d0bde2..3103c72 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1112,30 +1112,106 @@ struct Appearance: View { } struct Shortcuts: View { + @Default(.enableTimerFeature) var enableTimerFeature + @Default(.enableClipboardManager) var enableClipboardManager + @Default(.enableShortcuts) var enableShortcuts + var body: some View { Form { Section { - KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) + Defaults.Toggle("Enable global keyboard shortcuts", key: .enableShortcuts) } header: { - Text("Media") + Text("General") } footer: { - Text("Sneak Peek shows the media title and artist under the notch for a few seconds.") + Text("When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls.") .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) .font(.caption) } - Section { - KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) - } - Section { - KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) - } header: { - Text("Timer") - } footer: { - Text("Starts a 5-minute demo timer to test the timer live activity feature.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) + + if enableShortcuts { + Section { + KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) + .disabled(!enableShortcuts) + } header: { + Text("Media") + } footer: { + Text("Sneak Peek shows the media title and artist under the notch for a few seconds.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + + Section { + KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) + .disabled(!enableShortcuts) + } header: { + Text("Navigation") + } footer: { + Text("Toggle the Dynamic Island open or closed from anywhere.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + + Section { + HStack { + VStack(alignment: .leading) { + KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) + .disabled(!enableShortcuts || !enableTimerFeature) + if !enableTimerFeature { + Text("Timer feature is disabled") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + } + Spacer() + } + } header: { + Text("Timer") + } footer: { + Text("Starts a 5-minute demo timer to test the timer live activity feature. Only works when timer feature is enabled.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + + Section { + HStack { + VStack(alignment: .leading) { + KeyboardShortcuts.Recorder("Clipboard History:", name: .clipboardHistoryPanel) + .disabled(!enableShortcuts || !enableClipboardManager) + if !enableClipboardManager { + Text("Clipboard feature is disabled") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + } + Spacer() + } + } header: { + Text("Clipboard") + } footer: { + Text("Opens the clipboard history panel. Default is Cmd+Shift+V (similar to Windows+V on PC). Only works when clipboard feature is enabled.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } else { + Section { + VStack(alignment: .leading, spacing: 8) { + Text("Keyboard shortcuts are disabled") + .font(.headline) + .foregroundStyle(.secondary) + + Text("Enable global keyboard shortcuts above to customize your shortcuts.") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 8) + } } } .navigationTitle("Shortcuts") diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 0cb1b52..a97a13b 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -171,6 +171,9 @@ extension Defaults.Keys { static let clipboardHistorySize = Key("clipboardHistorySize", default: 3) static let showClipboardIcon = Key("showClipboardIcon", default: true) + // MARK: Keyboard Shortcuts + static let enableShortcuts = Key("enableShortcuts", default: true) + // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType { if #available(macOS 15.4, *) {