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)
.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)

View file

@ -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 }

View file

@ -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." : {

View file

@ -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..<graphCount, id: \.self) { index in
graphView(for: availableGraphs[index])
.frame(maxWidth: .infinity)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
// Use LazyVGrid for all cases - more predictable layout behavior
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(graphCount, 3)),
spacing: 12
) {
ForEach(0..<graphCount, id: \.self) { index in
UnifiedStatsCard(graphData: availableGraphs[index])
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
}
.animation(.easeInOut(duration: 0.25), 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)
}
.animation(.easeInOut(duration: 0.4), value: graphCount)
}
// Helper function to create graph views
// Helper function to create graph views using unified component
@ViewBuilder
func graphView(for graphData: GraphData) -> 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
// 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)
// 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)
}
}
Text("•")
.font(.caption2)
.foregroundColor(.secondary)
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()
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))

View file

@ -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")

View file

@ -171,6 +171,9 @@ extension Defaults.Keys {
static let clipboardHistorySize = Key<Int>("clipboardHistorySize", default: 3)
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
static var defaultMediaController: MediaControllerType {
if #available(macOS 15.4, *) {