checkpoint
This commit is contained in:
64
Gaze/Services/ReminderManager.swift
Normal file
64
Gaze/Services/ReminderManager.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// ReminderManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages reminder triggering and dismissal logic for timers.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class ReminderManager: ObservableObject {
|
||||
@Published var activeReminder: ReminderEvent?
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var enforceModeService: EnforceModeService?
|
||||
private var timerEngine: TimerEngine?
|
||||
|
||||
init(
|
||||
settingsProvider: any SettingsProviding,
|
||||
enforceModeService: EnforceModeService? = nil
|
||||
) {
|
||||
self.settingsProvider = settingsProvider
|
||||
self.enforceModeService = enforceModeService ?? EnforceModeService.shared
|
||||
}
|
||||
|
||||
func setTimerEngine(_ engine: TimerEngine) {
|
||||
self.timerEngine = engine
|
||||
}
|
||||
|
||||
func triggerReminder(for identifier: TimerIdentifier) {
|
||||
// Pause only the timer that triggered
|
||||
timerEngine?.pauseTimer(identifier: identifier)
|
||||
|
||||
// Unified approach to handle all timer types - no more special handling
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
switch type {
|
||||
case .lookAway:
|
||||
activeReminder = .lookAwayTriggered(
|
||||
countdownSeconds: settingsProvider.settings.lookAwayCountdownSeconds)
|
||||
case .blink:
|
||||
activeReminder = .blinkTriggered
|
||||
case .posture:
|
||||
activeReminder = .postureTriggered
|
||||
}
|
||||
case .user(let id):
|
||||
if let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) {
|
||||
activeReminder = .userTimerTriggered(userTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismissReminder() {
|
||||
guard let reminder = activeReminder else { return }
|
||||
activeReminder = nil
|
||||
|
||||
let identifier = reminder.identifier
|
||||
timerEngine?.skipNext(identifier: identifier)
|
||||
timerEngine?.resumeTimer(identifier: identifier)
|
||||
|
||||
enforceModeService?.handleReminderDismissed()
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ final class ServiceContainer {
|
||||
}
|
||||
let engine = TimerEngine(
|
||||
settingsManager: settingsManager,
|
||||
enforceModeService: enforceModeService
|
||||
enforceModeService: enforceModeService,
|
||||
timeProvider: isTestEnvironment ? MockTimeProvider() : SystemTimeProvider()
|
||||
)
|
||||
_timerEngine = engine
|
||||
return engine
|
||||
|
||||
72
Gaze/Services/SmartModeManager.swift
Normal file
72
Gaze/Services/SmartModeManager.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// SmartModeManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Handles smart mode features like idle detection and fullscreen detection.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class SmartModeManager {
|
||||
private var fullscreenService: FullscreenDetectionService?
|
||||
private var idleService: IdleMonitoringService?
|
||||
private var timerEngine: TimerEngine?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func setupSmartMode(
|
||||
timerEngine: TimerEngine,
|
||||
fullscreenService: FullscreenDetectionService?,
|
||||
idleService: IdleMonitoringService?
|
||||
) {
|
||||
self.timerEngine = timerEngine
|
||||
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 let timerEngine = timerEngine else { return }
|
||||
guard timerEngine.settingsProviderForTesting.settings.smartMode.autoPauseOnFullscreen else { return }
|
||||
|
||||
if isFullscreen {
|
||||
timerEngine.pauseAllTimers(reason: .fullscreen)
|
||||
logInfo("⏸️ Timers paused: fullscreen detected")
|
||||
} else {
|
||||
timerEngine.resumeAllTimers(reason: .fullscreen)
|
||||
logInfo("▶️ Timers resumed: fullscreen exited")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIdleChange(isIdle: Bool) {
|
||||
guard let timerEngine = timerEngine else { return }
|
||||
guard timerEngine.settingsProviderForTesting.settings.smartMode.autoPauseOnIdle else { return }
|
||||
|
||||
if isIdle {
|
||||
timerEngine.pauseAllTimers(reason: .idle)
|
||||
logInfo("⏸️ Timers paused: user idle")
|
||||
} else {
|
||||
timerEngine.resumeAllTimers(reason: .idle)
|
||||
logInfo("▶️ Timers resumed: user active")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ class TimerEngine: ObservableObject {
|
||||
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let settingsProvider: any SettingsProviding
|
||||
|
||||
// Expose the settings provider for components that need it (like SmartModeManager)
|
||||
var settingsProviderForTesting: any SettingsProviding {
|
||||
return settingsProvider
|
||||
}
|
||||
private var sleepStartTime: Date?
|
||||
|
||||
/// Time provider for deterministic testing (defaults to system time)
|
||||
@@ -103,7 +108,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func pauseAllTimers(reason: PauseReason) {
|
||||
func pauseAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(reason)
|
||||
state.isPaused = true
|
||||
@@ -111,7 +116,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func resumeAllTimers(reason: PauseReason) {
|
||||
func resumeAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(reason)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
@@ -394,57 +399,20 @@ func skipNext(identifier: TimerIdentifier) {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
|
||||
/// Handles system sleep event
|
||||
/// - Saves current time for elapsed calculation
|
||||
/// - Pauses all active timers
|
||||
// System sleep/wake handling is now managed by SystemSleepManager
|
||||
// This method is kept for compatibility but will be removed in future versions
|
||||
/// Handles system sleep event - deprecated
|
||||
@available(*, deprecated, message: "Use SystemSleepManager instead")
|
||||
func handleSystemSleep() {
|
||||
logDebug("System going to sleep")
|
||||
sleepStartTime = timeProvider.now()
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.system)
|
||||
state.isPaused = true
|
||||
timerStates[id] = state
|
||||
}
|
||||
logDebug("System going to sleep (deprecated)")
|
||||
// This functionality has been moved to SystemSleepManager
|
||||
}
|
||||
|
||||
/// Handles system wake event
|
||||
/// - Calculates elapsed time during sleep
|
||||
/// - Adjusts remaining time for all active timers
|
||||
/// - Timers that expired during sleep will trigger immediately (1s delay)
|
||||
/// - Resumes all timers
|
||||
|
||||
/// Handles system wake event - deprecated
|
||||
@available(*, deprecated, message: "Use SystemSleepManager instead")
|
||||
func handleSystemWake() {
|
||||
logDebug("System waking up")
|
||||
guard let sleepStart = sleepStartTime else {
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
sleepStartTime = nil
|
||||
}
|
||||
|
||||
let elapsedSeconds = Int(timeProvider.now().timeIntervalSince(sleepStart))
|
||||
|
||||
guard elapsedSeconds >= 1 else {
|
||||
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 {
|
||||
var updatedState = state
|
||||
updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds)
|
||||
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
updatedState.remainingSeconds = 1
|
||||
}
|
||||
|
||||
updatedState.pauseReasons.remove(.system)
|
||||
updatedState.isPaused = !updatedState.pauseReasons.isEmpty
|
||||
timerStates[identifier] = updatedState
|
||||
}
|
||||
logDebug("System waking up (deprecated)")
|
||||
// This functionality has been moved to SystemSleepManager
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
248
Gaze/Services/TimerManager.swift
Normal file
248
Gaze/Services/TimerManager.swift
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// TimerManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages timer creation, state updates, and lifecycle operations.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class TimerManager: ObservableObject {
|
||||
@Published var timerStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let timeProvider: TimeProviding
|
||||
|
||||
init(
|
||||
settingsManager: any SettingsProviding,
|
||||
timeProvider: TimeProviding
|
||||
) {
|
||||
self.settingsProvider = settingsManager
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
|
||||
func start() {
|
||||
// If timers are already running, just update configurations without resetting
|
||||
if timerSubscription != nil {
|
||||
updateConfigurations()
|
||||
return
|
||||
}
|
||||
|
||||
// Initial start - create all timer states
|
||||
stop()
|
||||
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Add built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsProvider.timerConfiguration(for: timerType)
|
||||
if config.enabled {
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: userTimer.intervalMinutes * 60,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
|
||||
timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleTick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timerSubscription?.cancel()
|
||||
timerSubscription = nil
|
||||
timerStates.removeAll()
|
||||
}
|
||||
|
||||
func updateConfigurations() {
|
||||
// Update configurations from settings
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Update built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsProvider.timerConfiguration(for: timerType)
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
|
||||
if config.enabled {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Timer exists - check if interval changed
|
||||
if existingState.originalIntervalSeconds != config.intervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: existingState.isPaused,
|
||||
isActive: true
|
||||
)
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// Timer was just enabled - create new state
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
// If config.enabled is false and timer exists, it will be removed
|
||||
}
|
||||
|
||||
// Update user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
let newIntervalSeconds = userTimer.intervalMinutes * 60
|
||||
|
||||
if userTimer.enabled {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Check if interval changed
|
||||
if existingState.originalIntervalSeconds != newIntervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
isPaused: existingState.isPaused,
|
||||
isActive: true
|
||||
)
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// New timer - create state
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
isPaused: false,
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
}
|
||||
// If timer is disabled, it will be removed
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
}
|
||||
|
||||
private func handleTick() {
|
||||
for (identifier, state) in timerStates {
|
||||
guard !state.isPaused else { continue }
|
||||
guard state.isActive else { continue }
|
||||
|
||||
if state.targetDate < timeProvider.now() - 3.0 {
|
||||
// Timer has expired but with some grace period
|
||||
continue
|
||||
}
|
||||
|
||||
timerStates[identifier]?.remainingSeconds -= 1
|
||||
|
||||
if let updatedState = timerStates[identifier] {
|
||||
// Update remaining seconds for the timer
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
// This would normally trigger a reminder in a full implementation,
|
||||
// but we're decomposing it to separate components
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func resume() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func pauseTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func resumeTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func skipNext(identifier: TimerIdentifier) {
|
||||
guard let state = timerStates[identifier] else { return }
|
||||
|
||||
// Unified approach to get interval - no more separate handling for user timers
|
||||
let intervalSeconds = getTimerInterval(for: identifier)
|
||||
|
||||
timerStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: intervalSeconds,
|
||||
isPaused: state.isPaused,
|
||||
isActive: state.isActive
|
||||
)
|
||||
}
|
||||
|
||||
/// Unified way to get interval for any timer type
|
||||
private func getTimerInterval(for identifier: TimerIdentifier) -> Int {
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
let config = settingsProvider.timerConfiguration(for: type)
|
||||
return config.intervalSeconds
|
||||
case .user(let id):
|
||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else {
|
||||
return 0
|
||||
}
|
||||
return userTimer.intervalMinutes * 60
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeRemaining(for identifier: TimerIdentifier) -> TimeInterval {
|
||||
guard let state = timerStates[identifier] else { return 0 }
|
||||
return TimeInterval(state.remainingSeconds)
|
||||
}
|
||||
|
||||
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
|
||||
return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
|
||||
}
|
||||
|
||||
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user