general: settings menu cleanup
This commit is contained in:
44
Gaze/Models/SettingsSection.swift
Normal file
44
Gaze/Models/SettingsSection.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user