mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #13 from Ebullioscopic/feature/stats
Add Shortcut Toggle
This commit is contained in:
commit
79b16570b5
6 changed files with 225 additions and 233 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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." : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, *) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue