Merge pull request #13 from Ebullioscopic/feature/stats

Add Shortcut Toggle
This commit is contained in:
Hariharan Mudaliar 2025-08-12 14:02:34 +05:30 • committed by GitHub
commit 79b16570b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 225 additions and 233 deletions

View file

@ -203,6 +203,8 @@ struct ContentView: View {
} }
} }
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) .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) .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5)
.background(dragDetector) .background(dragDetector)
.environmentObject(vm) .environmentObject(vm)
@ -309,6 +311,7 @@ struct ContentView: View {
ZStack { ZStack {
if vm.notchState == .open { if vm.notchState == .open {
Group {
switch coordinator.currentView { switch coordinator.currentView {
case .home: case .home:
NotchHomeView(albumArtNamespace: albumArtNamespace) NotchHomeView(albumArtNamespace: albumArtNamespace)
@ -320,7 +323,14 @@ struct ContentView: View {
NotchStatsView() 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) .zIndex(1)
.allowsHitTesting(vm.notchState == .open) .allowsHitTesting(vm.notchState == .open)
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0) .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)

View file

@ -373,6 +373,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
// Only execute if shortcuts are enabled
guard Defaults[.enableShortcuts] else { return }
self.coordinator.toggleSneakPeek( self.coordinator.toggleSneakPeek(
status: !self.coordinator.sneakPeek.show, status: !self.coordinator.sneakPeek.show,
type: .music, type: .music,
@ -383,6 +387,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
// Only execute if shortcuts are enabled
guard Defaults[.enableShortcuts] else { return }
let mouseLocation = NSEvent.mouseLocation let mouseLocation = NSEvent.mouseLocation
var viewModel = self.vm var viewModel = self.vm
@ -418,6 +425,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
KeyboardShortcuts.onKeyDown(for: .startDemoTimer) { [weak self] in KeyboardShortcuts.onKeyDown(for: .startDemoTimer) { [weak self] in
guard let self = self else { return } 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 // Start a 5-minute demo timer
TimerManager.shared.startDemoTimer(duration: 300) TimerManager.shared.startDemoTimer(duration: 300)
} }
@ -425,6 +439,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
KeyboardShortcuts.onKeyDown(for: .clipboardHistoryPanel) { [weak self] in KeyboardShortcuts.onKeyDown(for: .clipboardHistoryPanel) { [weak self] in
guard let self = self else { return } 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 // Only open clipboard if the feature is enabled
guard Defaults[.enableClipboardManager] else { return } guard Defaults[.enableClipboardManager] else { return }

View file

@ -277,9 +277,15 @@
}, },
"Clipboard" : { "Clipboard" : {
},
"Clipboard feature is disabled" : {
}, },
"Clipboard History" : { "Clipboard History" : {
},
"Clipboard History:" : {
}, },
"Clipboard Manager" : { "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." : { "Enable graph visibility in Settings → Stats to view performance data." : {
@ -592,6 +601,9 @@
}, },
"Installed extensions" : { "Installed extensions" : {
},
"Keyboard shortcuts are disabled" : {
}, },
"Last Updated" : { "Last Updated" : {
@ -727,6 +739,9 @@
}, },
"Name" : { "Name" : {
},
"Navigation" : {
}, },
"Need Restart" : { "Need Restart" : {
@ -818,6 +833,9 @@
}, },
"Open System Settings" : { "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" : { "Pause" : {
@ -1035,7 +1053,7 @@
"Start Monitoring" : { "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" : { "Stats" : {
@ -1086,6 +1104,9 @@
}, },
"Timer Feature" : { "Timer Feature" : {
},
"Timer feature is disabled" : {
}, },
"Timer Sound" : { "Timer Sound" : {
@ -1095,6 +1116,9 @@
}, },
"Toggle Sneak Peek:" : { "Toggle Sneak Peek:" : {
},
"Toggle the Dynamic Island open or closed from anywhere." : {
}, },
"Transform your notch truly yours" : { "Transform your notch truly yours" : {
@ -1148,6 +1172,9 @@
}, },
"What's New" : { "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." : { "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {

View file

@ -118,142 +118,31 @@ struct NotchStatsView: View {
return graphs return graphs
} }
// New vertical expansion layout system // Simplified and more consistent layout system
@ViewBuilder @ViewBuilder
var statsGridLayout: some View { var statsGridLayout: some View {
let graphCount = availableGraphs.count let graphCount = availableGraphs.count
switch graphCount { // Use LazyVGrid for all cases - more predictable layout behavior
case 1...3: LazyVGrid(
// Single row for 1-3 graphs columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(graphCount, 3)),
HStack(spacing: 12) { spacing: 12
) {
ForEach(0..<graphCount, id: \.self) { index in ForEach(0..<graphCount, id: \.self) { index in
graphView(for: availableGraphs[index]) UnifiedStatsCard(graphData: availableGraphs[index])
.frame(maxWidth: .infinity)
.transition(.asymmetric( .transition(.asymmetric(
insertion: .scale.combined(with: .opacity), insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity) removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
)) ))
} }
} }
.animation(.easeInOut(duration: 0.25), value: graphCount) .animation(.easeInOut(duration: 0.4), value: graphCount)
case 4:
// 2x2 quadrants for 4 graphs
VStack(spacing: 12) {
HStack(spacing: 12) {
graphView(for: availableGraphs[0])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
graphView(for: availableGraphs[1])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
HStack(spacing: 12) {
graphView(for: availableGraphs[2])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
graphView(for: availableGraphs[3])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
}
.animation(.easeInOut(duration: 0.25), value: graphCount)
case 5:
// 3 on top, 2 on bottom (taking half space each)
VStack(spacing: 12) {
// Top row: 3 graphs
HStack(spacing: 12) {
graphView(for: availableGraphs[0])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
graphView(for: availableGraphs[1])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
graphView(for: availableGraphs[2])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
// Bottom row: 2 graphs centered
HStack(spacing: 12) {
Spacer()
graphView(for: availableGraphs[3])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
graphView(for: availableGraphs[4])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
Spacer()
}
}
.animation(.easeInOut(duration: 0.25), value: graphCount)
default:
// Fallback for more than 5 graphs (shouldn't happen with current settings)
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(3, graphCount)), spacing: 12) {
ForEach(0..<graphCount, id: \.self) { index in
graphView(for: availableGraphs[index])
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
}
.animation(.easeInOut(duration: 0.25), value: graphCount)
}
} }
// Helper function to create graph views // Helper function to create graph views using unified component
@ViewBuilder @ViewBuilder
func graphView(for graphData: GraphData) -> some View { func graphView(for graphData: GraphData) -> some View {
if let singleData = graphData as? SingleGraphData { UnifiedStatsCard(graphData: graphData)
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
)
}
} }
var body: some View { var body: some View {
@ -297,17 +186,17 @@ struct NotchStatsView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding() .padding()
} else { } else {
// Stats content with vertical expansion layout // Stats content with improved animation coordination
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
VStack(spacing: 12) { VStack(spacing: 12) {
// New vertical expansion layout // Simplified consistent layout
statsGridLayout statsGridLayout
} }
.padding(16) .padding(16)
.animation(.easeInOut(duration: 0.25), value: availableGraphs.count) .animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
.transition(.asymmetric( .transition(.asymmetric(
insertion: .scale.combined(with: .opacity), insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity) removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
)) ))
// Live indicator and controls in top-right corner // Live indicator and controls in top-right corner
@ -365,25 +254,24 @@ struct NotchStatsView: View {
.onDisappear { .onDisappear {
// Keep monitoring running when tab is not visible // 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 { // Unified Stats Card Component - handles both single and dual data types
let title: String struct UnifiedStatsCard: View {
let value: String let graphData: GraphData
let data: [Double]
let color: Color
let icon: String
var body: some View { var body: some View {
VStack(spacing: 6) { VStack(spacing: 6) {
// Header - more compact // Header - consistent across all card types
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: icon) Image(systemName: graphData.icon)
.foregroundColor(color) .foregroundColor(graphData.color)
.font(.caption2) .font(.caption2)
Text(title) Text(graphData.title)
.font(.caption2) .font(.caption2)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -391,76 +279,47 @@ struct StatCard: View {
Spacer() Spacer()
} }
// Current value - larger and more prominent // Values section - adapts based on graph type
Text(value) Group {
if let singleData = graphData as? SingleGraphData {
Text(singleData.value)
.font(.title3) .font(.title3)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(.primary) .foregroundColor(.primary)
.frame(height: 22) // Match the height of dual values in DualStatCard } else if let dualData = graphData as? DualGraphData {
HStack(spacing: 6) {
// Mini graph - use more height Text(dualData.positiveValue)
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()
}
// Dual values - horizontal layout to match single value height
HStack(spacing: 8) {
Text(positiveValue)
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(positiveColor) .foregroundColor(dualData.positiveColor)
Text("•") Text("•")
.font(.caption2) .font(.caption2)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text(negativeValue) Text(dualData.negativeValue)
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(negativeColor) .foregroundColor(dualData.negativeColor)
} }
.frame(height: 22) // Match the height of .title3 text in StatCard }
}
.frame(height: 22) // Fixed height for consistent card sizing
// Dual quadrant graph // 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( DualQuadrantGraph(
positiveData: positiveData, positiveData: dualData.positiveData,
negativeData: negativeData, negativeData: dualData.negativeData,
positiveColor: positiveColor, positiveColor: dualData.positiveColor,
negativeColor: negativeColor negativeColor: dualData.negativeColor
) )
.frame(height: 50) }
}
.frame(height: 50) // Fixed height for consistent card sizing
} }
.padding(10) .padding(10)
.background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .background(Color(NSColor.controlBackgroundColor).opacity(0.5))

View file

@ -1112,10 +1112,27 @@ struct Appearance: View {
} }
struct Shortcuts: View { struct Shortcuts: View {
@Default(.enableTimerFeature) var enableTimerFeature
@Default(.enableClipboardManager) var enableClipboardManager
@Default(.enableShortcuts) var enableShortcuts
var body: some View { var body: some View {
Form { Form {
Section {
Defaults.Toggle("Enable global keyboard shortcuts", key: .enableShortcuts)
} header: {
Text("General")
} footer: {
Text("When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
}
if enableShortcuts {
Section { Section {
KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek)
.disabled(!enableShortcuts)
} header: { } header: {
Text("Media") Text("Media")
} footer: { } footer: {
@ -1124,19 +1141,78 @@ struct Shortcuts: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .font(.caption)
} }
Section { Section {
KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
} .disabled(!enableShortcuts)
Section {
KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer)
} header: { } header: {
Text("Timer") Text("Navigation")
} footer: { } footer: {
Text("Starts a 5-minute demo timer to test the timer live activity feature.") Text("Toggle the Dynamic Island open or closed from anywhere.")
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .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") .navigationTitle("Shortcuts")
} }

View file

@ -171,6 +171,9 @@ extension Defaults.Keys {
static let clipboardHistorySize = Key<Int>("clipboardHistorySize", default: 3) static let clipboardHistorySize = Key<Int>("clipboardHistorySize", default: 3)
static let showClipboardIcon = Key<Bool>("showClipboardIcon", default: true) static let showClipboardIcon = Key<Bool>("showClipboardIcon", default: true)
// MARK: Keyboard Shortcuts
static let enableShortcuts = Key<Bool>("enableShortcuts", default: true)
// Helper to determine the default media controller based on macOS version // Helper to determine the default media controller based on macOS version
static var defaultMediaController: MediaControllerType { static var defaultMediaController: MediaControllerType {
if #available(macOS 15.4, *) { if #available(macOS 15.4, *) {