diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 86481a9..035a487 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -187,7 +187,7 @@ private func showReminderWindow(_ content: AnyView) { } // Public method to open settings window - func openSettings() { + func openSettings(tab: Int = 0) { // If window already exists, just bring it to front if let existingWindow = settingsWindowController?.window { existingWindow.makeKeyAndOrderFront(nil) @@ -207,7 +207,7 @@ private func showReminderWindow(_ content: AnyView) { window.setFrameAutosaveName("SettingsWindow") window.isReleasedWhenClosed = false window.contentView = NSHostingView( - rootView: SettingsWindowView(settingsManager: settingsManager!) + rootView: SettingsWindowView(settingsManager: settingsManager!, initialTab: tab) ) let windowController = NSWindowController(window: window) diff --git a/Gaze/GazeApp.swift b/Gaze/GazeApp.swift index 71f9d5e..fafadb3 100644 --- a/Gaze/GazeApp.swift +++ b/Gaze/GazeApp.swift @@ -42,7 +42,8 @@ struct GazeApp: App { timerEngine: timerEngine, settingsManager: settingsManager, onQuit: { NSApplication.shared.terminate(nil) }, - onOpenSettings: { appDelegate.openSettings() } + onOpenSettings: { appDelegate.openSettings() }, + onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) } ) } } diff --git a/Gaze/Models/TimerType.swift b/Gaze/Models/TimerType.swift index 9c554d3..738b6f3 100644 --- a/Gaze/Models/TimerType.swift +++ b/Gaze/Models/TimerType.swift @@ -35,4 +35,26 @@ enum TimerType: String, Codable, CaseIterable, Identifiable { return "figure.stand" } } + + var tabIndex: Int { + switch self { + case .lookAway: + return 0 + case .blink: + return 1 + case .posture: + return 2 + } + } + + var tooltipText: String { + switch self { + case .lookAway: + return "Full screen reminder" + case .blink: + return "Subtle reminder" + case .posture: + return "Subtle reminder" + } + } } diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 35a214d..eb84912 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -48,6 +48,7 @@ struct MenuBarContentView: View { @ObservedObject var settingsManager: SettingsManager var onQuit: () -> Void var onOpenSettings: () -> Void + var onOpenSettingsTab: (Int) -> Void var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -65,33 +66,41 @@ struct MenuBarContentView: View { Divider() // Timer Status - if !timerEngine.timerStates.isEmpty { - VStack(alignment: .leading, spacing: 12) { - Text("Active Timers") - .font(.caption) - .foregroundColor(.secondary) - .padding(.horizontal) - .padding(.top, 8) + VStack(alignment: .leading, spacing: 12) { + Text("Active Timers") + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) + .padding(.top, 8) - ForEach(TimerType.allCases) { timerType in - if let state = timerEngine.timerStates[timerType] { - TimerStatusRow( - type: timerType, - state: state, - onSkip: { - timerEngine.skipNext(type: timerType) - }, - onDevTrigger: { - timerEngine.triggerReminder(for: timerType) - } - ) - } + ForEach(TimerType.allCases) { timerType in + if let state = timerEngine.timerStates[timerType] { + TimerStatusRow( + type: timerType, + state: state, + onSkip: { + timerEngine.skipNext(type: timerType) + }, + onDevTrigger: { + timerEngine.triggerReminder(for: timerType) + }, + onTap: { + onOpenSettingsTab(timerType.tabIndex) + } + ) + } else { + InactiveTimerRow( + type: timerType, + onTap: { + onOpenSettingsTab(timerType.tabIndex) + } + ) } } - .padding(.bottom, 8) - - Divider() } + .padding(.bottom, 8) + + Divider() // Controls VStack(spacing: 4) { @@ -158,74 +167,85 @@ struct TimerStatusRow: View { let state: TimerState var onSkip: () -> Void var onDevTrigger: (() -> Void)? = nil + var onTap: (() -> Void)? = nil @State private var isHoveredSkip = false @State private var isHoveredDevTrigger = false @State private var isHoveredBody = false var body: some View { - HStack { - Image(systemName: type.iconName) - .foregroundColor(iconColor) - .frame(width: 20) + Button(action: { + onTap?() + }) { + HStack { + Image(systemName: type.iconName) + .foregroundColor(iconColor) + .frame(width: 20) - VStack(alignment: .leading, spacing: 2) { - Text(type.displayName) - .font(.subheadline) - .fontWeight(.medium) - Text(timeRemaining) - .font(.caption) - .foregroundColor(.secondary) - .monospacedDigit() - } - - Spacer() - - #if DEBUG - if let onDevTrigger = onDevTrigger { - Button(action: onDevTrigger) { - Image(systemName: "bolt.fill") - .font(.caption) - .foregroundColor(.yellow) - .padding(6) - } - .buttonStyle(.plain) - .glassEffect( - isHoveredDevTrigger ? .regular.tint(.yellow) : .regular, - in: .circle - ) - .help("Trigger \(type.displayName) reminder now (dev)") - .onHover { hovering in - isHoveredDevTrigger = hovering - } + VStack(alignment: .leading, spacing: 2) { + Text(type.displayName) + .font(.subheadline) + .fontWeight(.medium) + Text(timeRemaining) + .font(.caption) + .foregroundColor(.secondary) + .monospacedDigit() } - #endif - Button(action: onSkip) { - Image(systemName: "forward.fill") - .font(.caption) - .foregroundColor(.accentColor) - .padding(6) - } - .buttonStyle(.plain) - .glassEffect( - isHoveredSkip ? .regular.tint(.accentColor) : .regular, - in: .circle - ) - .help("Skip to next \(type.displayName) reminder") - .onHover { hovering in - isHoveredSkip = hovering + Spacer() + + #if DEBUG + if let onDevTrigger = onDevTrigger { + Button(action: onDevTrigger) { + Image(systemName: "bolt.fill") + .font(.caption) + .foregroundColor(.yellow) + .padding(6) + } + .buttonStyle(.plain) + .glassEffect( + isHoveredDevTrigger ? .regular.tint(.yellow) : .regular, + in: .circle + ) + .help("Trigger \(type.displayName) reminder now (dev)") + .onHover { hovering in + isHoveredDevTrigger = hovering + } + } + #endif + + Button(action: onSkip) { + Image(systemName: "forward.fill") + .font(.caption) + .foregroundColor(.accentColor) + .padding(6) + } + .buttonStyle(.plain) + .glassEffect( + isHoveredSkip ? .regular.tint(.accentColor.opacity(0.5)) : .regular, + in: .circle + ) + .help("Skip to next \(type.displayName) reminder") + .onHover { hovering in + isHoveredSkip = hovering + } } + .padding(.horizontal, 8) + .padding(.vertical, 6) } - .padding(.horizontal, 8) - .padding(.vertical, 6) + .buttonStyle(.plain) .glassEffect( - isHoveredBody ? .regular.tint(.accentColor) : .regular, + isHoveredBody ? .regular.tint(.accentColor.opacity(0.5)) : .regular, in: .rect(cornerRadius: 6) ) .padding(.horizontal, 8) .onHover { hovering in isHoveredBody = hovering } + .help(tooltipText) + } + + private var tooltipText: String { + type.tooltipText } private var iconColor: Color { @@ -253,6 +273,48 @@ struct TimerStatusRow: View { } } +struct InactiveTimerRow: View { + let type: TimerType + var onTap: () -> Void + @State private var isHovered = false + + var body: some View { + Button(action: onTap) { + HStack { + Image(systemName: type.iconName) + .foregroundColor(.secondary) + .frame(width: 20) + + VStack(alignment: .leading, spacing: 2) { + Text(type.displayName) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + } + + Spacer() + + Image(systemName: "plus.circle") + .font(.title3) + .foregroundColor(.accentColor) + .padding(6) + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + } + .buttonStyle(.plain) + .glassEffect( + isHovered ? .regular.tint(.accentColor.opacity(0.5)) : .regular, + in: .rect(cornerRadius: 6) + ) + .padding(.horizontal, 8) + .onHover { hovering in + isHovered = hovering + } + .help("Enable \(type.displayName) reminders") + } +} + #Preview("Menu Bar Content") { let settingsManager = SettingsManager.shared let timerEngine = TimerEngine(settingsManager: settingsManager) @@ -260,6 +322,7 @@ struct TimerStatusRow: View { timerEngine: timerEngine, settingsManager: settingsManager, onQuit: {}, - onOpenSettings: {} + onOpenSettings: {}, + onOpenSettingsTab: { _ in } ) } diff --git a/Gaze/Views/Onboarding/OnboardingContainerView.swift b/Gaze/Views/Onboarding/OnboardingContainerView.swift index f1abf6b..a03ed72 100644 --- a/Gaze/Views/Onboarding/OnboardingContainerView.swift +++ b/Gaze/Views/Onboarding/OnboardingContainerView.swift @@ -25,7 +25,7 @@ struct OnboardingContainerView: View { @State private var lookAwayEnabled = true @State private var lookAwayIntervalMinutes = 20 @State private var lookAwayCountdownSeconds = 20 - @State private var blinkEnabled = true + @State private var blinkEnabled = false @State private var blinkIntervalMinutes = 5 @State private var postureEnabled = true @State private var postureIntervalMinutes = 30 diff --git a/Gaze/Views/Reminders/LookAwayReminderView.swift b/Gaze/Views/Reminders/LookAwayReminderView.swift index d193b43..cf46138 100644 --- a/Gaze/Views/Reminders/LookAwayReminderView.swift +++ b/Gaze/Views/Reminders/LookAwayReminderView.swift @@ -41,7 +41,7 @@ struct LookAwayReminderView: View { LottieView( animationName: AnimationAsset.lookAway.fileName, loopMode: .loop, - animationSpeed: 1.0 + animationSpeed: 0.75 ) .frame(width: 200, height: 200) .padding(.vertical, 30) diff --git a/Gaze/Views/SettingsWindowView.swift b/Gaze/Views/SettingsWindowView.swift index 2696964..e4c2471 100644 --- a/Gaze/Views/SettingsWindowView.swift +++ b/Gaze/Views/SettingsWindowView.swift @@ -9,7 +9,7 @@ import SwiftUI struct SettingsWindowView: View { @ObservedObject var settingsManager: SettingsManager - @State private var currentTab = 0 + @State private var currentTab: Int @State private var lookAwayEnabled: Bool @State private var lookAwayIntervalMinutes: Int @State private var lookAwayCountdownSeconds: Int @@ -19,9 +19,10 @@ struct SettingsWindowView: View { @State private var postureIntervalMinutes: Int @State private var launchAtLogin: Bool - init(settingsManager: SettingsManager) { + init(settingsManager: SettingsManager, initialTab: Int = 0) { self.settingsManager = settingsManager + _currentTab = State(initialValue: initialTab) _lookAwayEnabled = State(initialValue: settingsManager.settings.lookAwayTimer.enabled) _lookAwayIntervalMinutes = State(initialValue: settingsManager.settings.lookAwayTimer.intervalSeconds / 60) _lookAwayCountdownSeconds = State(initialValue: settingsManager.settings.lookAwayCountdownSeconds) diff --git a/run b/run index ac29b68..f6b7444 100755 --- a/run +++ b/run @@ -8,6 +8,22 @@ ACTION=${1:-run} VERBOSE=false OUTPUT_FILE="" +# Function to kill any existing Gaze processes +kill_existing_gaze_processes() { + echo "🔍 Checking for existing Gaze processes..." + + # Find and kill any running Gaze processes + pids=$(pgrep -f "Gaze.app") + if [ -n "$pids" ]; then + echo "🛑 Killing existing Gaze processes (PID(s): $pids)..." + kill $pids 2>/dev/null + # Wait a moment for processes to terminate + sleep 1 + else + echo "✅ No existing Gaze processes found" + fi +} + # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in @@ -72,6 +88,10 @@ elif [ "$ACTION" = "test" ]; then elif [ "$ACTION" = "run" ]; then echo "Building and running Gaze application..." + + # Kill any existing Gaze processes first + kill_existing_gaze_processes + # Always build first, then run run_with_output "xcodebuild -project Gaze.xcodeproj -scheme Gaze -configuration Debug build"