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

View File

@@ -143,7 +143,6 @@ struct MenuBarContentView: View {
Divider()
// Quit
Button(action: onQuit) {
HStack {
Image(systemName: "power")
@@ -190,7 +189,8 @@ struct MenuBarContentView: View {
.padding(.top, 8)
// 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 {
TimerStatusRowWithIndividualControls(
identifier: identifier,
@@ -339,7 +339,8 @@ struct TimerStatusRowWithIndividualControls: View {
case .builtIn(let type):
return type.displayName
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 .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):
return type.tooltipText
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"
}
let typeText = timer.type == .subtle ? "Subtle" : "Overlay"
@@ -440,7 +443,9 @@ struct TimerStatusRowWithIndividualControls: View {
colorScheme: colorScheme
)
.help("Trigger \(displayName) reminder now (dev)")
.accessibilityIdentifier("trigger_\(displayName.replacingOccurrences(of: " ", with: "_"))")
.accessibilityIdentifier(
"trigger_\(displayName.replacingOccurrences(of: " ", with: "_"))"
)
.onHover { hovering in
isHoveredDevTrigger = hovering
}

View File

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