general: settings menu cleanup

This commit is contained in:
Michael Freno
2026-01-14 13:35:28 -05:00
parent f43696c2e8
commit 64f41f8bef
4 changed files with 271 additions and 127 deletions

View File

@@ -0,0 +1,44 @@
//
// SettingsSection.swift
// Gaze
//
// Created by Mike Freno on 1/14/26.
//
import Foundation
enum SettingsSection: Int, CaseIterable, Identifiable {
case general = 0
case lookAway = 1
case blink = 2
case posture = 3
case enforceMode = 4
case userTimers = 5
case smartMode = 6
var id: Int { rawValue }
var title: String {
switch self {
case .general: return "General"
case .lookAway: return "Look Away"
case .blink: return "Blink"
case .posture: return "Posture"
case .enforceMode: return "Enforce Mode"
case .userTimers: return "User Timers"
case .smartMode: return "Smart Mode"
}
}
var iconName: String {
switch self {
case .general: return "gearshape.fill"
case .lookAway: return "eye.fill"
case .blink: return "eye.circle.fill"
case .posture: return "figure.stand"
case .enforceMode: return "video.fill"
case .userTimers: return "plus.circle"
case .smartMode: return "brain.fill"
}
}
}

View File

@@ -9,64 +9,92 @@ import SwiftUI
struct SettingsWindowView: View { struct SettingsWindowView: View {
@ObservedObject var settingsManager: SettingsManager @ObservedObject var settingsManager: SettingsManager
@State private var currentTab: Int @State private var selectedSection: SettingsSection
init(settingsManager: SettingsManager, initialTab: Int = 0) { init(settingsManager: SettingsManager, initialTab: Int = 0) {
self.settingsManager = settingsManager self.settingsManager = settingsManager
_currentTab = State(initialValue: initialTab) _selectedSection = State(initialValue: SettingsSection(rawValue: initialTab) ?? .general)
} }
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
TabView(selection: $currentTab) { if #available(macOS 15.0, *) {
TabView(selection: $selectedSection) {
Tab(
SettingsSection.general.title,
systemImage: SettingsSection.general.iconName,
value: SettingsSection.general
) {
GeneralSetupView(
settingsManager: settingsManager,
isOnboarding: false
)
}
Tab(
SettingsSection.lookAway.title,
systemImage: SettingsSection.lookAway.iconName,
value: SettingsSection.lookAway
) {
LookAwaySetupView(settingsManager: settingsManager) LookAwaySetupView(settingsManager: settingsManager)
.tag(0)
.tabItem {
Label("Look Away", systemImage: "eye.fill")
} }
Tab(
SettingsSection.blink.title, systemImage: SettingsSection.blink.iconName,
value: SettingsSection.blink
) {
BlinkSetupView(settingsManager: settingsManager) BlinkSetupView(settingsManager: settingsManager)
.tag(1)
.tabItem {
Label("Blink", systemImage: "eye.circle.fill")
} }
Tab(
SettingsSection.posture.title,
systemImage: SettingsSection.posture.iconName,
value: SettingsSection.posture
) {
PostureSetupView(settingsManager: settingsManager) PostureSetupView(settingsManager: settingsManager)
.tag(2)
.tabItem {
Label("Posture", systemImage: "figure.stand")
}
EnforceModeSetupView(settingsManager: settingsManager)
.tag(3)
.tabItem {
Label("Enforce Mode", systemImage: "video.fill")
} }
Tab(
SettingsSection.userTimers.title,
systemImage: SettingsSection.userTimers.iconName,
value: SettingsSection.userTimers
) {
UserTimersView( UserTimersView(
userTimers: Binding( userTimers: Binding(
get: { settingsManager.settings.userTimers }, get: { settingsManager.settings.userTimers },
set: { settingsManager.settings.userTimers = $0 } set: { settingsManager.settings.userTimers = $0 }
) )
) )
.tag(4) }
.tabItem { Tab(
Label("User Timers", systemImage: "plus.circle") SettingsSection.enforceMode.title,
systemImage: SettingsSection.enforceMode.iconName,
value: SettingsSection.enforceMode
) {
EnforceModeSetupView(settingsManager: settingsManager)
} }
Tab(
SettingsSection.smartMode.title,
systemImage: SettingsSection.smartMode.iconName,
value: SettingsSection.smartMode
) {
SmartModeSetupView(settingsManager: settingsManager) SmartModeSetupView(settingsManager: settingsManager)
.tag(5)
.tabItem {
Label("Smart Mode", systemImage: "brain.fill")
} }
}
GeneralSetupView( .tabViewStyle(.sidebarAdaptable)
settingsManager: settingsManager, } else {
isOnboarding: false // Fallback for macOS 14 and earlier
) NavigationSplitView {
.tag(6) List(SettingsSection.allCases, selection: $selectedSection) { section in
.tabItem { NavigationLink(value: section) {
Label("General", systemImage: "gearshape.fill") Label(section.title, systemImage: section.iconName)
}
}
.navigationTitle("Settings")
.listStyle(.sidebar)
} detail: {
detailView(for: selectedSection)
} }
} }
@@ -92,24 +120,54 @@ struct SettingsWindowView: View {
} }
#if APPSTORE #if APPSTORE
.frame( .frame(
minWidth: 750, minWidth: 1000,
minHeight: 700 minHeight: 700
) )
#else #else
.frame( .frame(
minWidth: 750, minWidth: 1000,
minHeight: 900 minHeight: 900
) )
#endif #endif
.onReceive( .onReceive(
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab")) NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
) { notification in ) { notification in
if let tab = notification.object as? Int { if let tab = notification.object as? Int,
currentTab = tab let section = SettingsSection(rawValue: tab)
{
selectedSection = section
} }
} }
} }
@ViewBuilder
private func detailView(for section: SettingsSection) -> some View {
switch section {
case .general:
GeneralSetupView(
settingsManager: settingsManager,
isOnboarding: false
)
case .lookAway:
LookAwaySetupView(settingsManager: settingsManager)
case .blink:
BlinkSetupView(settingsManager: settingsManager)
case .posture:
PostureSetupView(settingsManager: settingsManager)
case .enforceMode:
EnforceModeSetupView(settingsManager: settingsManager)
case .userTimers:
UserTimersView(
userTimers: Binding(
get: { settingsManager.settings.userTimers },
set: { settingsManager.settings.userTimers = $0 }
)
)
case .smartMode:
SmartModeSetupView(settingsManager: settingsManager)
}
}
private func closeWindow() { private func closeWindow() {
if let window = NSApplication.shared.windows.first(where: { $0.title == "Settings" }) { if let window = NSApplication.shared.windows.first(where: { $0.title == "Settings" }) {
window.close() window.close()

View File

@@ -143,7 +143,6 @@ struct MenuBarContentView: View {
Divider() Divider()
// Quit
Button(action: onQuit) { Button(action: onQuit) {
HStack { HStack {
Image(systemName: "power") Image(systemName: "power")
@@ -190,7 +189,8 @@ struct MenuBarContentView: View {
.padding(.top, 8) .padding(.top, 8)
// Show all timers using unified identifier system // Show all timers using unified identifier system
ForEach(getSortedTimerIdentifiers(timerEngine: timerEngine), id: \.self) { identifier in ForEach(getSortedTimerIdentifiers(timerEngine: timerEngine), id: \.self) {
identifier in
if timerEngine.timerStates[identifier] != nil { if timerEngine.timerStates[identifier] != nil {
TimerStatusRowWithIndividualControls( TimerStatusRowWithIndividualControls(
identifier: identifier, identifier: identifier,
@@ -339,7 +339,8 @@ struct TimerStatusRowWithIndividualControls: View {
case .builtIn(let type): case .builtIn(let type):
return type.displayName return type.displayName
case .user(let id): case .user(let id):
return settingsManager.settings.userTimers.first(where: { $0.id == id })?.title ?? "User Timer" return settingsManager.settings.userTimers.first(where: { $0.id == id })?.title
?? "User Timer"
} }
} }
@@ -361,7 +362,8 @@ struct TimerStatusRowWithIndividualControls: View {
case .posture: return .orange case .posture: return .orange
} }
case .user(let id): case .user(let id):
return settingsManager.settings.userTimers.first(where: { $0.id == id })?.color ?? .purple return settingsManager.settings.userTimers.first(where: { $0.id == id })?.color
?? .purple
} }
} }
@@ -370,7 +372,8 @@ struct TimerStatusRowWithIndividualControls: View {
case .builtIn(let type): case .builtIn(let type):
return type.tooltipText return type.tooltipText
case .user(let id): case .user(let id):
guard let timer = settingsManager.settings.userTimers.first(where: { $0.id == id }) else { guard let timer = settingsManager.settings.userTimers.first(where: { $0.id == id })
else {
return "User Timer" return "User Timer"
} }
let typeText = timer.type == .subtle ? "Subtle" : "Overlay" let typeText = timer.type == .subtle ? "Subtle" : "Overlay"
@@ -440,7 +443,9 @@ struct TimerStatusRowWithIndividualControls: View {
colorScheme: colorScheme colorScheme: colorScheme
) )
.help("Trigger \(displayName) reminder now (dev)") .help("Trigger \(displayName) reminder now (dev)")
.accessibilityIdentifier("trigger_\(displayName.replacingOccurrences(of: " ", with: "_"))") .accessibilityIdentifier(
"trigger_\(displayName.replacingOccurrences(of: " ", with: "_"))"
)
.onHover { hovering in .onHover { hovering in
isHoveredDevTrigger = hovering isHoveredDevTrigger = hovering
} }

View File

@@ -28,52 +28,67 @@ struct SmartModeSetupView: View {
.padding(.top, 20) .padding(.top, 20)
.padding(.bottom, 30) .padding(.bottom, 30)
// Vertically centered content
Spacer() Spacer()
VStack(spacing: 24) { VStack(spacing: 24) {
// Auto-pause on fullscreen toggle // Auto-pause on fullscreen toggle
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: Binding( HStack {
get: { settingsManager.settings.smartMode.autoPauseOnFullscreen }, VStack(alignment: .leading, spacing: 4) {
set: { settingsManager.settings.smartMode.autoPauseOnFullscreen = $0 }
)) {
HStack { HStack {
Image(systemName: "arrow.up.left.and.arrow.down.right") Image(systemName: "arrow.up.left.and.arrow.down.right")
.foregroundColor(.blue) .foregroundColor(.blue)
Text("Auto-pause on Fullscreen") Text("Auto-pause on Fullscreen")
.font(.headline) .font(.headline)
} }
} Text(
.toggleStyle(.switch) "Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)"
)
Text("Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.leading, 28) }
Spacer()
Toggle(
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.autoPauseOnFullscreen },
set: {
settingsManager.settings.smartMode.autoPauseOnFullscreen = $0
}
)
)
.labelsHidden()
}
} }
.padding() .padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8)) .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
// Auto-pause on idle toggle with threshold slider // Auto-pause on idle toggle with threshold slider
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: Binding( HStack {
get: { settingsManager.settings.smartMode.autoPauseOnIdle }, VStack(alignment: .leading, spacing: 4) {
set: { settingsManager.settings.smartMode.autoPauseOnIdle = $0 }
)) {
HStack { HStack {
Image(systemName: "moon.zzz.fill") Image(systemName: "moon.zzz.fill")
.foregroundColor(.indigo) .foregroundColor(.indigo)
Text("Auto-pause on Idle") Text("Auto-pause on Idle")
.font(.headline) .font(.headline)
} }
} Text(
.toggleStyle(.switch) "Timers will pause when you're inactive for more than the threshold below"
)
Text("Timers will pause when you're inactive for more than the threshold below")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.leading, 28) }
Spacer()
Toggle(
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.autoPauseOnIdle },
set: { settingsManager.settings.smartMode.autoPauseOnIdle = $0 }
)
)
.labelsHidden()
}
if settingsManager.settings.smartMode.autoPauseOnIdle { if settingsManager.settings.smartMode.autoPauseOnIdle {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -81,22 +96,29 @@ struct SmartModeSetupView: View {
Text("Idle Threshold:") Text("Idle Threshold:")
.font(.subheadline) .font(.subheadline)
Spacer() Spacer()
Text("\(settingsManager.settings.smartMode.idleThresholdMinutes) min") Text(
"\(settingsManager.settings.smartMode.idleThresholdMinutes) min"
)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Slider( Slider(
value: Binding( value: Binding(
get: { Double(settingsManager.settings.smartMode.idleThresholdMinutes) }, get: {
set: { settingsManager.settings.smartMode.idleThresholdMinutes = Int($0) } Double(
settingsManager.settings.smartMode.idleThresholdMinutes)
},
set: {
settingsManager.settings.smartMode.idleThresholdMinutes =
Int($0)
}
), ),
in: 1...30, in: 1...30,
step: 1 step: 1
) )
} }
.padding(.top, 8) .padding(.top, 8)
.padding(.leading, 28)
} }
} }
.padding() .padding()
@@ -104,23 +126,30 @@ struct SmartModeSetupView: View {
// Usage tracking toggle with reset threshold // Usage tracking toggle with reset threshold
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Toggle(isOn: Binding( HStack {
get: { settingsManager.settings.smartMode.trackUsage }, VStack(alignment: .leading, spacing: 4) {
set: { settingsManager.settings.smartMode.trackUsage = $0 }
)) {
HStack { HStack {
Image(systemName: "chart.line.uptrend.xyaxis") Image(systemName: "chart.line.uptrend.xyaxis")
.foregroundColor(.green) .foregroundColor(.green)
Text("Track Usage Statistics") Text("Track Usage Statistics")
.font(.headline) .font(.headline)
} }
} Text(
.toggleStyle(.switch) "Monitor active and idle time, with automatic reset after the specified duration"
)
Text("Monitor active and idle time, with automatic reset after the specified duration")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.leading, 28) }
Spacer()
Toggle(
"",
isOn: Binding(
get: { settingsManager.settings.smartMode.trackUsage },
set: { settingsManager.settings.smartMode.trackUsage = $0 }
)
)
.labelsHidden()
}
if settingsManager.settings.smartMode.trackUsage { if settingsManager.settings.smartMode.trackUsage {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@@ -128,22 +157,30 @@ struct SmartModeSetupView: View {
Text("Reset After:") Text("Reset After:")
.font(.subheadline) .font(.subheadline)
Spacer() Spacer()
Text("\(settingsManager.settings.smartMode.usageResetAfterMinutes) min") Text(
"\(settingsManager.settings.smartMode.usageResetAfterMinutes) min"
)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Slider( Slider(
value: Binding( value: Binding(
get: { Double(settingsManager.settings.smartMode.usageResetAfterMinutes) }, get: {
set: { settingsManager.settings.smartMode.usageResetAfterMinutes = Int($0) } Double(
settingsManager.settings.smartMode
.usageResetAfterMinutes)
},
set: {
settingsManager.settings.smartMode.usageResetAfterMinutes =
Int($0)
}
), ),
in: 15...240, in: 15...240,
step: 15 step: 15
) )
} }
.padding(.top, 8) .padding(.top, 8)
.padding(.leading, 28)
} }
} }
.padding() .padding()