feat: smart mode

This commit is contained in:
Michael Freno
2026-01-14 12:15:46 -05:00
parent 205a889a38
commit f43696c2e8
11 changed files with 712 additions and 13 deletions

View File

@@ -20,12 +20,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var hasStartedTimers = false private var hasStartedTimers = false
// Smart Mode services
private var fullscreenService: FullscreenDetectionService?
private var idleService: IdleMonitoringService?
private var usageTrackingService: UsageTrackingService?
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
// Set activation policy to hide dock icon // Set activation policy to hide dock icon
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
timerEngine = TimerEngine(settingsManager: settingsManager) timerEngine = TimerEngine(settingsManager: settingsManager)
// Initialize Smart Mode services
setupSmartModeServices()
// Initialize update manager after onboarding is complete // Initialize update manager after onboarding is complete
if settingsManager.settings.hasCompletedOnboarding { if settingsManager.settings.hasCompletedOnboarding {
updateManager = UpdateManager.shared 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() { func onboardingCompleted() {
startTimers() startTimers()

View File

@@ -42,6 +42,8 @@ struct AppSettings: Codable, Equatable, Hashable {
var subtleReminderSize: ReminderSize var subtleReminderSize: ReminderSize
var smartMode: SmartModeSettings
var hasCompletedOnboarding: Bool var hasCompletedOnboarding: Bool
var launchAtLogin: Bool var launchAtLogin: Bool
var playSounds: Bool var playSounds: Bool
@@ -56,6 +58,7 @@ struct AppSettings: Codable, Equatable, Hashable {
enabled: true, intervalSeconds: 30 * 60), enabled: true, intervalSeconds: 30 * 60),
userTimers: [UserTimer] = [], userTimers: [UserTimer] = [],
subtleReminderSize: ReminderSize = .medium, subtleReminderSize: ReminderSize = .medium,
smartMode: SmartModeSettings = .defaults,
hasCompletedOnboarding: Bool = false, hasCompletedOnboarding: Bool = false,
launchAtLogin: Bool = false, launchAtLogin: Bool = false,
playSounds: Bool = true playSounds: Bool = true
@@ -66,6 +69,7 @@ struct AppSettings: Codable, Equatable, Hashable {
self.postureTimer = postureTimer self.postureTimer = postureTimer
self.userTimers = userTimers self.userTimers = userTimers
self.subtleReminderSize = subtleReminderSize self.subtleReminderSize = subtleReminderSize
self.smartMode = smartMode
self.hasCompletedOnboarding = hasCompletedOnboarding self.hasCompletedOnboarding = hasCompletedOnboarding
self.launchAtLogin = launchAtLogin self.launchAtLogin = launchAtLogin
self.playSounds = playSounds self.playSounds = playSounds
@@ -79,6 +83,7 @@ struct AppSettings: Codable, Equatable, Hashable {
postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60), postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60),
userTimers: [], userTimers: [],
subtleReminderSize: .medium, subtleReminderSize: .medium,
smartMode: .defaults,
hasCompletedOnboarding: false, hasCompletedOnboarding: false,
launchAtLogin: false, launchAtLogin: false,
playSounds: true playSounds: true

View File

@@ -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
}

View File

@@ -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
)
}
}

View File

@@ -11,14 +11,16 @@ struct TimerState: Equatable, Hashable {
let identifier: TimerIdentifier let identifier: TimerIdentifier
var remainingSeconds: Int var remainingSeconds: Int
var isPaused: Bool var isPaused: Bool
var pauseReasons: Set<PauseReason>
var isActive: Bool var isActive: Bool
var targetDate: Date 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) { init(identifier: TimerIdentifier, intervalSeconds: Int, isPaused: Bool = false, isActive: Bool = true) {
self.identifier = identifier self.identifier = identifier
self.remainingSeconds = intervalSeconds self.remainingSeconds = intervalSeconds
self.isPaused = isPaused self.isPaused = isPaused
self.pauseReasons = []
self.isActive = isActive self.isActive = isActive
self.targetDate = Date().addingTimeInterval(Double(intervalSeconds)) self.targetDate = Date().addingTimeInterval(Double(intervalSeconds))
self.originalIntervalSeconds = intervalSeconds self.originalIntervalSeconds = intervalSeconds

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -20,6 +20,11 @@ class TimerEngine: ObservableObject {
// For enforce mode integration // For enforce mode integration
private var enforceModeService: EnforceModeService? private var enforceModeService: EnforceModeService?
// Smart Mode services
private var fullscreenService: FullscreenDetectionService?
private var idleService: IdleMonitoringService?
private var cancellables = Set<AnyCancellable>()
init(settingsManager: SettingsManager) { init(settingsManager: SettingsManager) {
self.settingsManager = settingsManager self.settingsManager = settingsManager
self.enforceModeService = EnforceModeService.shared self.enforceModeService = EnforceModeService.shared
@@ -29,6 +34,72 @@ class TimerEngine: ObservableObject {
} }
} }
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() { func start() {
// If timers are already running, just update configurations without resetting // If timers are already running, just update configurations without resetting
if timerSubscription != nil { if timerSubscription != nil {
@@ -163,23 +234,33 @@ class TimerEngine: ObservableObject {
} }
func pause() { func pause() {
for (id, _) in timerStates { for (id, var state) in timerStates {
timerStates[id]?.isPaused = true state.pauseReasons.insert(.manual)
state.isPaused = true
timerStates[id] = state
} }
} }
func resume() { func resume() {
for (id, _) in timerStates { for (id, var state) in timerStates {
timerStates[id]?.isPaused = false state.pauseReasons.remove(.manual)
state.isPaused = !state.pauseReasons.isEmpty
timerStates[id] = state
} }
} }
func pauseTimer(identifier: TimerIdentifier) { 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) { 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) { func skipNext(identifier: TimerIdentifier) {
@@ -285,7 +366,11 @@ class TimerEngine: ObservableObject {
/// - Pauses all active timers /// - Pauses all active timers
func handleSystemSleep() { func handleSystemSleep() {
sleepStartTime = Date() sleepStartTime = Date()
pause() for (id, var state) in timerStates {
state.pauseReasons.insert(.system)
state.isPaused = true
timerStates[id] = state
}
} }
/// Handles system wake event /// Handles system wake event
@@ -305,11 +390,15 @@ class TimerEngine: ObservableObject {
let elapsedSeconds = Int(Date().timeIntervalSince(sleepStart)) let elapsedSeconds = Int(Date().timeIntervalSince(sleepStart))
guard elapsedSeconds >= 1 else { guard elapsedSeconds >= 1 else {
resume() for (id, var state) in timerStates {
state.pauseReasons.remove(.system)
state.isPaused = !state.pauseReasons.isEmpty
timerStates[id] = state
}
return return
} }
for (identifier, state) in timerStates where state.isActive && !state.isPaused { for (identifier, state) in timerStates where state.isActive {
var updatedState = state var updatedState = state
updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds) updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds)
@@ -317,9 +406,9 @@ class TimerEngine: ObservableObject {
updatedState.remainingSeconds = 1 updatedState.remainingSeconds = 1
} }
updatedState.pauseReasons.remove(.system)
updatedState.isPaused = !updatedState.pauseReasons.isEmpty
timerStates[identifier] = updatedState timerStates[identifier] = updatedState
} }
resume()
} }
} }

View File

@@ -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<AnyCancellable>()
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)
}
}
}

View File

@@ -54,11 +54,17 @@ struct SettingsWindowView: View {
Label("User Timers", systemImage: "plus.circle") Label("User Timers", systemImage: "plus.circle")
} }
SmartModeSetupView(settingsManager: settingsManager)
.tag(5)
.tabItem {
Label("Smart Mode", systemImage: "brain.fill")
}
GeneralSetupView( GeneralSetupView(
settingsManager: settingsManager, settingsManager: settingsManager,
isOnboarding: false isOnboarding: false
) )
.tag(5) .tag(6)
.tabItem { .tabItem {
Label("General", systemImage: "gearshape.fill") Label("General", systemImage: "gearshape.fill")
} }

View File

@@ -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)
}