From f43696c2e83868c6200aaf1f3ab24b1ed90d5da9 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 14 Jan 2026 12:15:46 -0500 Subject: [PATCH] feat: smart mode --- Gaze/AppDelegate.swift | 39 ++++ Gaze/Models/AppSettings.swift | 5 + Gaze/Models/PauseReason.swift | 15 ++ Gaze/Models/SmartModeSettings.swift | 40 +++++ Gaze/Models/TimerState.swift | 4 +- .../Services/FullscreenDetectionService.swift | 102 +++++++++++ Gaze/Services/IdleMonitoringService.swift | 67 +++++++ Gaze/Services/TimerEngine.swift | 111 ++++++++++-- Gaze/Services/UsageTrackingService.swift | 170 ++++++++++++++++++ .../Views/Containers/SettingsWindowView.swift | 8 +- Gaze/Views/Setup/SmartModeSetupView.swift | 164 +++++++++++++++++ 11 files changed, 712 insertions(+), 13 deletions(-) create mode 100644 Gaze/Models/PauseReason.swift create mode 100644 Gaze/Models/SmartModeSettings.swift create mode 100644 Gaze/Services/FullscreenDetectionService.swift create mode 100644 Gaze/Services/IdleMonitoringService.swift create mode 100644 Gaze/Services/UsageTrackingService.swift create mode 100644 Gaze/Views/Setup/SmartModeSetupView.swift diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 8c64d55..b955cee 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -20,12 +20,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var cancellables = Set() private var hasStartedTimers = false + // Smart Mode services + private var fullscreenService: FullscreenDetectionService? + private var idleService: IdleMonitoringService? + private var usageTrackingService: UsageTrackingService? + func applicationDidFinishLaunching(_ notification: Notification) { // Set activation policy to hide dock icon NSApplication.shared.setActivationPolicy(.accessory) timerEngine = TimerEngine(settingsManager: settingsManager) + // Initialize Smart Mode services + setupSmartModeServices() + // Initialize update manager after onboarding is complete if settingsManager.settings.hasCompletedOnboarding { updateManager = UpdateManager.shared @@ -41,6 +49,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } } + private func setupSmartModeServices() { + fullscreenService = FullscreenDetectionService() + idleService = IdleMonitoringService( + idleThresholdMinutes: settingsManager.settings.smartMode.idleThresholdMinutes + ) + usageTrackingService = UsageTrackingService( + resetThresholdMinutes: settingsManager.settings.smartMode.usageResetAfterMinutes + ) + + // Connect idle service to usage tracking + if let idleService = idleService { + usageTrackingService?.setupIdleMonitoring(idleService) + } + + // Connect services to timer engine + timerEngine?.setupSmartMode( + fullscreenService: fullscreenService, + idleService: idleService + ) + + // Observe smart mode settings changes + settingsManager.$settings + .map { $0.smartMode } + .removeDuplicates() + .sink { [weak self] smartMode in + self?.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes) + self?.usageTrackingService?.updateResetThreshold(minutes: smartMode.usageResetAfterMinutes) + } + .store(in: &cancellables) + } + func onboardingCompleted() { startTimers() diff --git a/Gaze/Models/AppSettings.swift b/Gaze/Models/AppSettings.swift index 655bfe9..c0123a1 100644 --- a/Gaze/Models/AppSettings.swift +++ b/Gaze/Models/AppSettings.swift @@ -41,6 +41,8 @@ struct AppSettings: Codable, Equatable, Hashable { var userTimers: [UserTimer] var subtleReminderSize: ReminderSize + + var smartMode: SmartModeSettings var hasCompletedOnboarding: Bool var launchAtLogin: Bool @@ -56,6 +58,7 @@ struct AppSettings: Codable, Equatable, Hashable { enabled: true, intervalSeconds: 30 * 60), userTimers: [UserTimer] = [], subtleReminderSize: ReminderSize = .medium, + smartMode: SmartModeSettings = .defaults, hasCompletedOnboarding: Bool = false, launchAtLogin: Bool = false, playSounds: Bool = true @@ -66,6 +69,7 @@ struct AppSettings: Codable, Equatable, Hashable { self.postureTimer = postureTimer self.userTimers = userTimers self.subtleReminderSize = subtleReminderSize + self.smartMode = smartMode self.hasCompletedOnboarding = hasCompletedOnboarding self.launchAtLogin = launchAtLogin self.playSounds = playSounds @@ -79,6 +83,7 @@ struct AppSettings: Codable, Equatable, Hashable { postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60), userTimers: [], subtleReminderSize: .medium, + smartMode: .defaults, hasCompletedOnboarding: false, launchAtLogin: false, playSounds: true diff --git a/Gaze/Models/PauseReason.swift b/Gaze/Models/PauseReason.swift new file mode 100644 index 0000000..8d1c5fe --- /dev/null +++ b/Gaze/Models/PauseReason.swift @@ -0,0 +1,15 @@ +// +// PauseReason.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import Foundation + +enum PauseReason: Codable, Equatable, Hashable { + case manual + case fullscreen + case idle + case system +} diff --git a/Gaze/Models/SmartModeSettings.swift b/Gaze/Models/SmartModeSettings.swift new file mode 100644 index 0000000..6008399 --- /dev/null +++ b/Gaze/Models/SmartModeSettings.swift @@ -0,0 +1,40 @@ +// +// SmartModeSettings.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import Foundation + +struct SmartModeSettings: Codable, Equatable, Hashable { + var autoPauseOnFullscreen: Bool + var autoPauseOnIdle: Bool + var trackUsage: Bool + var idleThresholdMinutes: Int + var usageResetAfterMinutes: Int + + init( + autoPauseOnFullscreen: Bool = false, + autoPauseOnIdle: Bool = false, + trackUsage: Bool = false, + idleThresholdMinutes: Int = 5, + usageResetAfterMinutes: Int = 60 + ) { + self.autoPauseOnFullscreen = autoPauseOnFullscreen + self.autoPauseOnIdle = autoPauseOnIdle + self.trackUsage = trackUsage + self.idleThresholdMinutes = idleThresholdMinutes + self.usageResetAfterMinutes = usageResetAfterMinutes + } + + static var defaults: SmartModeSettings { + SmartModeSettings( + autoPauseOnFullscreen: false, + autoPauseOnIdle: false, + trackUsage: false, + idleThresholdMinutes: 5, + usageResetAfterMinutes: 60 + ) + } +} diff --git a/Gaze/Models/TimerState.swift b/Gaze/Models/TimerState.swift index 3d66002..a7a2685 100644 --- a/Gaze/Models/TimerState.swift +++ b/Gaze/Models/TimerState.swift @@ -11,14 +11,16 @@ struct TimerState: Equatable, Hashable { let identifier: TimerIdentifier var remainingSeconds: Int var isPaused: Bool + var pauseReasons: Set var isActive: Bool var targetDate: Date - let originalIntervalSeconds: Int // Store original interval for comparison + let originalIntervalSeconds: Int init(identifier: TimerIdentifier, intervalSeconds: Int, isPaused: Bool = false, isActive: Bool = true) { self.identifier = identifier self.remainingSeconds = intervalSeconds self.isPaused = isPaused + self.pauseReasons = [] self.isActive = isActive self.targetDate = Date().addingTimeInterval(Double(intervalSeconds)) self.originalIntervalSeconds = intervalSeconds diff --git a/Gaze/Services/FullscreenDetectionService.swift b/Gaze/Services/FullscreenDetectionService.swift new file mode 100644 index 0000000..5f4833b --- /dev/null +++ b/Gaze/Services/FullscreenDetectionService.swift @@ -0,0 +1,102 @@ +// +// FullscreenDetectionService.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import AppKit +import Combine +import Foundation + +@MainActor +class FullscreenDetectionService: ObservableObject { + @Published private(set) var isFullscreenActive = false + + private var observers: [NSObjectProtocol] = [] + + init() { + setupObservers() + } + + deinit { + let notificationCenter = NSWorkspace.shared.notificationCenter + observers.forEach { notificationCenter.removeObserver($0) } + } + + private func setupObservers() { + let workspace = NSWorkspace.shared + let notificationCenter = workspace.notificationCenter + + // Monitor when applications enter fullscreen + let didEnterObserver = notificationCenter.addObserver( + forName: NSWorkspace.activeSpaceDidChangeNotification, + object: workspace, + queue: .main + ) { [weak self] _ in + Task { @MainActor in + self?.checkFullscreenState() + } + } + observers.append(didEnterObserver) + + // Monitor when active application changes + let didActivateObserver = notificationCenter.addObserver( + forName: NSWorkspace.didActivateApplicationNotification, + object: workspace, + queue: .main + ) { [weak self] _ in + Task { @MainActor in + self?.checkFullscreenState() + } + } + observers.append(didActivateObserver) + + // Initial check + checkFullscreenState() + } + + private func checkFullscreenState() { + guard let frontmostApp = NSWorkspace.shared.frontmostApplication else { + isFullscreenActive = false + return + } + + // Check if any window of the frontmost application is fullscreen + let options = CGWindowListOption(arrayLiteral: .optionOnScreenOnly, .excludeDesktopElements) + let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? [] + + let frontmostPID = frontmostApp.processIdentifier + + for window in windowList { + guard let ownerPID = window[kCGWindowOwnerPID as String] as? pid_t, + ownerPID == frontmostPID, + let bounds = window[kCGWindowBounds as String] as? [String: CGFloat], + let layer = window[kCGWindowLayer as String] as? Int else { + continue + } + + // Check if window is fullscreen by comparing bounds to screen size + if let screen = NSScreen.main { + let screenFrame = screen.frame + let windowWidth = bounds["Width"] ?? 0 + let windowHeight = bounds["Height"] ?? 0 + + // Window is considered fullscreen if it matches screen dimensions + // and is at a normal window layer (0) + if layer == 0 && + abs(windowWidth - screenFrame.width) < 1 && + abs(windowHeight - screenFrame.height) < 1 { + isFullscreenActive = true + return + } + } + } + + isFullscreenActive = false + } + + func forceUpdate() { + checkFullscreenState() + } +} diff --git a/Gaze/Services/IdleMonitoringService.swift b/Gaze/Services/IdleMonitoringService.swift new file mode 100644 index 0000000..0d8ed8e --- /dev/null +++ b/Gaze/Services/IdleMonitoringService.swift @@ -0,0 +1,67 @@ +// +// IdleMonitoringService.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import AppKit +import Combine +import Foundation + +@MainActor +class IdleMonitoringService: ObservableObject { + @Published private(set) var isIdle = false + @Published private(set) var idleTimeSeconds: TimeInterval = 0 + + private var timer: Timer? + private var idleThresholdSeconds: TimeInterval + + init(idleThresholdMinutes: Int = 5) { + self.idleThresholdSeconds = TimeInterval(idleThresholdMinutes * 60) + startMonitoring() + } + + deinit { + timer?.invalidate() + } + + func updateThreshold(minutes: Int) { + idleThresholdSeconds = TimeInterval(minutes * 60) + checkIdleState() + } + + private func startMonitoring() { + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.checkIdleState() + } + } + } + + private func checkIdleState() { + idleTimeSeconds = CGEventSource.secondsSinceLastEventType( + .combinedSessionState, + eventType: .mouseMoved + ) + + // Also check keyboard events and use the minimum + let keyboardIdleTime = CGEventSource.secondsSinceLastEventType( + .combinedSessionState, + eventType: .keyDown + ) + + idleTimeSeconds = min(idleTimeSeconds, keyboardIdleTime) + + let wasIdle = isIdle + isIdle = idleTimeSeconds >= idleThresholdSeconds + + if wasIdle != isIdle { + print("🔄 Idle state changed: \(isIdle ? "IDLE" : "ACTIVE") (idle: \(Int(idleTimeSeconds))s, threshold: \(Int(idleThresholdSeconds))s)") + } + } + + func forceUpdate() { + checkIdleState() + } +} diff --git a/Gaze/Services/TimerEngine.swift b/Gaze/Services/TimerEngine.swift index d77efe1..b22ccee 100644 --- a/Gaze/Services/TimerEngine.swift +++ b/Gaze/Services/TimerEngine.swift @@ -19,6 +19,11 @@ class TimerEngine: ObservableObject { // For enforce mode integration private var enforceModeService: EnforceModeService? + + // Smart Mode services + private var fullscreenService: FullscreenDetectionService? + private var idleService: IdleMonitoringService? + private var cancellables = Set() init(settingsManager: SettingsManager) { self.settingsManager = settingsManager @@ -28,6 +33,72 @@ class TimerEngine: ObservableObject { self.enforceModeService?.setTimerEngine(self) } } + + func setupSmartMode( + fullscreenService: FullscreenDetectionService?, + idleService: IdleMonitoringService? + ) { + self.fullscreenService = fullscreenService + self.idleService = idleService + + // Subscribe to fullscreen state changes + fullscreenService?.$isFullscreenActive + .sink { [weak self] isFullscreen in + Task { @MainActor in + self?.handleFullscreenChange(isFullscreen: isFullscreen) + } + } + .store(in: &cancellables) + + // Subscribe to idle state changes + idleService?.$isIdle + .sink { [weak self] isIdle in + Task { @MainActor in + self?.handleIdleChange(isIdle: isIdle) + } + } + .store(in: &cancellables) + } + + private func handleFullscreenChange(isFullscreen: Bool) { + guard settingsManager.settings.smartMode.autoPauseOnFullscreen else { return } + + if isFullscreen { + pauseAllTimers(reason: .fullscreen) + print("⏸️ Timers paused: fullscreen detected") + } else { + resumeAllTimers(reason: .fullscreen) + print("▶️ Timers resumed: fullscreen exited") + } + } + + private func handleIdleChange(isIdle: Bool) { + guard settingsManager.settings.smartMode.autoPauseOnIdle else { return } + + if isIdle { + pauseAllTimers(reason: .idle) + print("⏸️ Timers paused: user idle") + } else { + resumeAllTimers(reason: .idle) + print("▶️ Timers resumed: user active") + } + } + + private func pauseAllTimers(reason: PauseReason) { + for (id, var state) in timerStates { + state.pauseReasons.insert(reason) + state.isPaused = true + timerStates[id] = state + } + } + + private func resumeAllTimers(reason: PauseReason) { + for (id, var state) in timerStates { + state.pauseReasons.remove(reason) + state.isPaused = !state.pauseReasons.isEmpty + timerStates[id] = state + } + } func start() { // If timers are already running, just update configurations without resetting @@ -163,23 +234,33 @@ class TimerEngine: ObservableObject { } func pause() { - for (id, _) in timerStates { - timerStates[id]?.isPaused = true + for (id, var state) in timerStates { + state.pauseReasons.insert(.manual) + state.isPaused = true + timerStates[id] = state } } func resume() { - for (id, _) in timerStates { - timerStates[id]?.isPaused = false + for (id, var state) in timerStates { + state.pauseReasons.remove(.manual) + state.isPaused = !state.pauseReasons.isEmpty + timerStates[id] = state } } func pauseTimer(identifier: TimerIdentifier) { - timerStates[identifier]?.isPaused = true + guard var state = timerStates[identifier] else { return } + state.pauseReasons.insert(.manual) + state.isPaused = true + timerStates[identifier] = state } func resumeTimer(identifier: TimerIdentifier) { - timerStates[identifier]?.isPaused = false + guard var state = timerStates[identifier] else { return } + state.pauseReasons.remove(.manual) + state.isPaused = !state.pauseReasons.isEmpty + timerStates[identifier] = state } func skipNext(identifier: TimerIdentifier) { @@ -285,7 +366,11 @@ class TimerEngine: ObservableObject { /// - Pauses all active timers func handleSystemSleep() { sleepStartTime = Date() - pause() + for (id, var state) in timerStates { + state.pauseReasons.insert(.system) + state.isPaused = true + timerStates[id] = state + } } /// Handles system wake event @@ -305,11 +390,15 @@ class TimerEngine: ObservableObject { let elapsedSeconds = Int(Date().timeIntervalSince(sleepStart)) guard elapsedSeconds >= 1 else { - resume() + for (id, var state) in timerStates { + state.pauseReasons.remove(.system) + state.isPaused = !state.pauseReasons.isEmpty + timerStates[id] = state + } return } - for (identifier, state) in timerStates where state.isActive && !state.isPaused { + for (identifier, state) in timerStates where state.isActive { var updatedState = state updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds) @@ -317,9 +406,9 @@ class TimerEngine: ObservableObject { updatedState.remainingSeconds = 1 } + updatedState.pauseReasons.remove(.system) + updatedState.isPaused = !updatedState.pauseReasons.isEmpty timerStates[identifier] = updatedState } - - resume() } } diff --git a/Gaze/Services/UsageTrackingService.swift b/Gaze/Services/UsageTrackingService.swift new file mode 100644 index 0000000..378fbc7 --- /dev/null +++ b/Gaze/Services/UsageTrackingService.swift @@ -0,0 +1,170 @@ +// +// UsageTrackingService.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import Combine +import Foundation + +struct UsageStatistics: Codable { + var totalActiveSeconds: TimeInterval + var totalIdleSeconds: TimeInterval + var lastResetDate: Date + var sessionStartDate: Date + + var totalActiveMinutes: Int { + Int(totalActiveSeconds / 60) + } + + var totalIdleMinutes: Int { + Int(totalIdleSeconds / 60) + } +} + +@MainActor +class UsageTrackingService: ObservableObject { + @Published private(set) var statistics: UsageStatistics + + private var lastUpdateTime: Date + private var wasIdle: Bool = false + private var cancellables = Set() + private let userDefaults = UserDefaults.standard + private let statisticsKey = "gazeUsageStatistics" + private var resetThresholdMinutes: Int + + private var idleService: IdleMonitoringService? + + init(resetThresholdMinutes: Int = 60) { + self.resetThresholdMinutes = resetThresholdMinutes + self.lastUpdateTime = Date() + + if let data = userDefaults.data(forKey: statisticsKey), + let decoded = try? JSONDecoder().decode(UsageStatistics.self, from: data) { + self.statistics = decoded + } else { + self.statistics = UsageStatistics( + totalActiveSeconds: 0, + totalIdleSeconds: 0, + lastResetDate: Date(), + sessionStartDate: Date() + ) + } + + checkForReset() + startTracking() + } + + func setupIdleMonitoring(_ idleService: IdleMonitoringService) { + self.idleService = idleService + + idleService.$isIdle + .sink { [weak self] isIdle in + Task { @MainActor in + self?.updateTracking(isIdle: isIdle) + } + } + .store(in: &cancellables) + } + + func updateResetThreshold(minutes: Int) { + resetThresholdMinutes = minutes + checkForReset() + } + + private func startTracking() { + Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.tick() + } + } + } + + private func tick() { + let now = Date() + let elapsed = now.timeIntervalSince(lastUpdateTime) + lastUpdateTime = now + + let isCurrentlyIdle = idleService?.isIdle ?? false + + if isCurrentlyIdle { + statistics.totalIdleSeconds += elapsed + } else { + statistics.totalActiveSeconds += elapsed + } + + wasIdle = isCurrentlyIdle + + checkForReset() + save() + } + + private func updateTracking(isIdle: Bool) { + let now = Date() + let elapsed = now.timeIntervalSince(lastUpdateTime) + + if wasIdle { + statistics.totalIdleSeconds += elapsed + } else { + statistics.totalActiveSeconds += elapsed + } + + lastUpdateTime = now + wasIdle = isIdle + save() + } + + private func checkForReset() { + let totalMinutes = statistics.totalActiveMinutes + statistics.totalIdleMinutes + + if totalMinutes >= resetThresholdMinutes { + reset() + print("♻️ Usage statistics reset after \(totalMinutes) minutes (threshold: \(resetThresholdMinutes))") + } + } + + func reset() { + statistics = UsageStatistics( + totalActiveSeconds: 0, + totalIdleSeconds: 0, + lastResetDate: Date(), + sessionStartDate: Date() + ) + lastUpdateTime = Date() + save() + } + + private func save() { + if let encoded = try? JSONEncoder().encode(statistics) { + userDefaults.set(encoded, forKey: statisticsKey) + } + } + + func getFormattedActiveTime() -> String { + formatDuration(seconds: Int(statistics.totalActiveSeconds)) + } + + func getFormattedIdleTime() -> String { + formatDuration(seconds: Int(statistics.totalIdleSeconds)) + } + + func getFormattedTotalTime() -> String { + let total = Int(statistics.totalActiveSeconds + statistics.totalIdleSeconds) + return formatDuration(seconds: total) + } + + private func formatDuration(seconds: Int) -> String { + let hours = seconds / 3600 + let minutes = (seconds % 3600) / 60 + let secs = seconds % 60 + + if hours > 0 { + return String(format: "%dh %dm %ds", hours, minutes, secs) + } else if minutes > 0 { + return String(format: "%dm %ds", minutes, secs) + } else { + return String(format: "%ds", secs) + } + } +} diff --git a/Gaze/Views/Containers/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift index cc9fea1..b31862d 100644 --- a/Gaze/Views/Containers/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -54,11 +54,17 @@ struct SettingsWindowView: View { Label("User Timers", systemImage: "plus.circle") } + SmartModeSetupView(settingsManager: settingsManager) + .tag(5) + .tabItem { + Label("Smart Mode", systemImage: "brain.fill") + } + GeneralSetupView( settingsManager: settingsManager, isOnboarding: false ) - .tag(5) + .tag(6) .tabItem { Label("General", systemImage: "gearshape.fill") } diff --git a/Gaze/Views/Setup/SmartModeSetupView.swift b/Gaze/Views/Setup/SmartModeSetupView.swift new file mode 100644 index 0000000..df800e5 --- /dev/null +++ b/Gaze/Views/Setup/SmartModeSetupView.swift @@ -0,0 +1,164 @@ +// +// SmartModeSetupView.swift +// Gaze +// +// Created by Mike Freno on 1/14/26. +// + +import SwiftUI + +struct SmartModeSetupView: View { + @ObservedObject var settingsManager: SettingsManager + + var body: some View { + VStack(spacing: 0) { + // Fixed header section + VStack(spacing: 16) { + Image(systemName: "brain.fill") + .font(.system(size: 60)) + .foregroundColor(.purple) + + Text("Smart Mode") + .font(.system(size: 28, weight: .bold)) + + Text("Automatically manage timers based on your activity") + .font(.subheadline) + .foregroundColor(.secondary) + } + .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) + } + } + .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) + } + } + .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) { + HStack { + Text("Idle Threshold:") + .font(.subheadline) + Spacer() + 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) } + ), + in: 1...30, + step: 1 + ) + } + .padding(.top, 8) + .padding(.leading, 28) + } + } + .padding() + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8)) + + // 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) + } + } + .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) { + HStack { + Text("Reset After:") + .font(.subheadline) + Spacer() + 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) } + ), + in: 15...240, + step: 15 + ) + } + .padding(.top, 8) + .padding(.leading, 28) + } + } + .padding() + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8)) + } + .frame(maxWidth: 600) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding() + .background(.clear) + } +} + +#Preview { + SmartModeSetupView(settingsManager: SettingsManager.shared) +}