general:made logging consistent
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import os.log
|
||||
|
||||
@MainActor
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
@@ -20,43 +19,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
private var hasStartedTimers = false
|
||||
private var isSettingsWindowOpen = false
|
||||
private var isOnboardingWindowOpen = false
|
||||
|
||||
// Logging manager
|
||||
private let logger = LoggingManager.shared
|
||||
|
||||
|
||||
// Convenience accessor for settings
|
||||
private var settingsManager: any SettingsProviding {
|
||||
serviceContainer.settingsManager
|
||||
}
|
||||
|
||||
|
||||
override init() {
|
||||
self.serviceContainer = ServiceContainer.shared
|
||||
self.windowManager = WindowManager.shared
|
||||
super.init()
|
||||
|
||||
|
||||
// Setup window close observers
|
||||
setupWindowCloseObservers()
|
||||
}
|
||||
|
||||
|
||||
/// Initializer for testing with injectable dependencies
|
||||
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
|
||||
self.serviceContainer = serviceContainer
|
||||
self.windowManager = windowManager
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Set activation policy to hide dock icon
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
|
||||
// Initialize logging
|
||||
logger.configureLogging()
|
||||
logger.appLogger.info("🚀 Application did finish launching")
|
||||
|
||||
// Get timer engine from service container
|
||||
logInfo("🚀 Application did finish launching")
|
||||
|
||||
timerEngine = serviceContainer.timerEngine
|
||||
|
||||
// Setup smart mode services through container
|
||||
serviceContainer.setupSmartModeServices()
|
||||
|
||||
// Check if onboarding needs to be shown automatically
|
||||
@@ -87,7 +79,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] smartMode in
|
||||
guard let self = self else { return }
|
||||
self.serviceContainer.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes)
|
||||
self.serviceContainer.idleService?.updateThreshold(
|
||||
minutes: smartMode.idleThresholdMinutes)
|
||||
self.serviceContainer.usageTrackingService?.updateResetThreshold(
|
||||
minutes: smartMode.usageResetAfterMinutes)
|
||||
|
||||
@@ -110,7 +103,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
private func startTimers() {
|
||||
guard !hasStartedTimers else { return }
|
||||
hasStartedTimers = true
|
||||
logger.appLogger.info("Starting timers")
|
||||
logInfo("Starting timers")
|
||||
timerEngine?.start()
|
||||
observeReminderEvents()
|
||||
}
|
||||
@@ -128,13 +121,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
// Also observe smart mode settings
|
||||
observeSmartModeSettings()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
logger.appLogger.info(" applicationWill terminate")
|
||||
logInfo(" applicationWill terminate")
|
||||
settingsManager.saveImmediately()
|
||||
timerEngine?.stop()
|
||||
}
|
||||
@@ -156,13 +149,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
@objc private func systemWillSleep() {
|
||||
logger.systemLogger.info("System will sleep")
|
||||
logInfo("System will sleep")
|
||||
timerEngine?.handleSystemSleep()
|
||||
settingsManager.saveImmediately()
|
||||
}
|
||||
|
||||
@objc private func systemDidWake() {
|
||||
logger.systemLogger.info("System did wake")
|
||||
logInfo("System did wake")
|
||||
timerEngine?.handleSystemWake()
|
||||
}
|
||||
|
||||
@@ -185,21 +178,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
self?.timerEngine?.dismissReminder()
|
||||
}
|
||||
windowManager.showReminderWindow(view, windowType: .overlay)
|
||||
|
||||
|
||||
case .blinkTriggered:
|
||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
||||
let view = BlinkReminderView(sizePercentage: sizePercentage) { [weak self] in
|
||||
self?.timerEngine?.dismissReminder()
|
||||
}
|
||||
windowManager.showReminderWindow(view, windowType: .subtle)
|
||||
|
||||
|
||||
case .postureTriggered:
|
||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
||||
let view = PostureReminderView(sizePercentage: sizePercentage) { [weak self] in
|
||||
self?.timerEngine?.dismissReminder()
|
||||
}
|
||||
windowManager.showReminderWindow(view, windowType: .subtle)
|
||||
|
||||
|
||||
case .userTimerTriggered(let timer):
|
||||
if timer.type == .overlay {
|
||||
let view = UserTimerOverlayReminderView(timer: timer) { [weak self] in
|
||||
@@ -208,7 +201,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
windowManager.showReminderWindow(view, windowType: .overlay)
|
||||
} else {
|
||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
||||
let view = UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) { [weak self] in
|
||||
let view = UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) {
|
||||
[weak self] in
|
||||
self?.timerEngine?.dismissReminder()
|
||||
}
|
||||
windowManager.showReminderWindow(view, windowType: .subtle)
|
||||
@@ -228,7 +222,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
handleMenuDismissal()
|
||||
isSettingsWindowOpen = true
|
||||
|
||||
@@ -247,7 +241,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
handleMenuDismissal()
|
||||
// Explicitly set the flag to true when we're about to show the onboarding window
|
||||
isOnboardingWindowOpen = true
|
||||
@@ -262,7 +256,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil)
|
||||
windowManager.dismissOverlayReminder()
|
||||
}
|
||||
|
||||
|
||||
private func setupWindowCloseObservers() {
|
||||
// Observe settings window closing
|
||||
NotificationCenter.default.addObserver(
|
||||
@@ -271,7 +265,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
name: Notification.Name("SettingsWindowDidClose"),
|
||||
object: nil
|
||||
)
|
||||
|
||||
|
||||
// Observe onboarding window closing
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@@ -280,11 +274,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@objc private func settingsWindowDidClose() {
|
||||
isSettingsWindowOpen = false
|
||||
}
|
||||
|
||||
|
||||
@objc private func onboardingWindowDidClose() {
|
||||
// Reset the flag when we receive the close notification
|
||||
isOnboardingWindowOpen = false
|
||||
|
||||
@@ -24,7 +24,12 @@ class EyeTrackingService: NSObject, ObservableObject {
|
||||
@Published var debugRightPupilRatio: Double?
|
||||
@Published var debugYaw: Double?
|
||||
@Published var debugPitch: Double?
|
||||
@Published var enableDebugLogging: Bool = false
|
||||
@Published var enableDebugLogging: Bool = false {
|
||||
didSet {
|
||||
// Sync with PupilDetector's diagnostic logging
|
||||
PupilDetector.enableDiagnosticLogging = enableDebugLogging
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle for debug logging
|
||||
private var lastDebugLogTime: Date = .distantPast
|
||||
@@ -228,6 +233,10 @@ class EyeTrackingService: NSObject, ObservableObject {
|
||||
result.faceDetected = true
|
||||
let face = observations.first!
|
||||
|
||||
// Always extract yaw/pitch from face, even if landmarks aren't available
|
||||
result.debugYaw = face.yaw?.doubleValue ?? 0.0
|
||||
result.debugPitch = face.pitch?.doubleValue ?? 0.0
|
||||
|
||||
guard let landmarks = face.landmarks else {
|
||||
return result
|
||||
}
|
||||
@@ -660,6 +669,9 @@ extension EyeTrackingService: AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
// Advance frame counter for pupil detector frame skipping
|
||||
PupilDetector.advanceFrame()
|
||||
|
||||
let request = VNDetectFaceLandmarksRequest { [weak self] request, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
|
||||
@@ -46,15 +46,12 @@ final class LoggingManager {
|
||||
// MARK: - Initialization
|
||||
|
||||
private init() {
|
||||
// Private initializer to enforce singleton pattern
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Configure the logging system for verbose output when needed
|
||||
func configureLogging() {
|
||||
// For now, we'll use standard OSLog behavior.
|
||||
// This can be extended in the future to support runtime log level changes.
|
||||
//nothing needed for now
|
||||
}
|
||||
|
||||
/// Convenience method for debug logging
|
||||
@@ -86,8 +83,6 @@ final class LoggingManager {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Global Convenience Functions
|
||||
|
||||
/// Log an info message using the shared LoggingManager
|
||||
public func logInfo(_ message: String, category: String = "General") {
|
||||
LoggingManager.shared.info(message, category: category)
|
||||
|
||||
@@ -63,7 +63,9 @@ final class PupilCalibration: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func findBestThreshold(eyeData: UnsafePointer<UInt8>, width: Int, height: Int) -> Int {
|
||||
private nonisolated func findBestThreshold(
|
||||
eyeData: UnsafePointer<UInt8>, width: Int, height: Int
|
||||
) -> Int {
|
||||
let averageIrisSize = 0.48
|
||||
var bestThreshold = 50
|
||||
var bestDiff = Double.greatestFiniteMagnitude
|
||||
@@ -91,7 +93,9 @@ final class PupilCalibration: @unchecked Sendable {
|
||||
return bestThreshold
|
||||
}
|
||||
|
||||
private nonisolated static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int) -> Double {
|
||||
private nonisolated static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int)
|
||||
-> Double
|
||||
{
|
||||
let margin = 5
|
||||
guard width > margin * 2, height > margin * 2 else { return 0 }
|
||||
|
||||
@@ -145,15 +149,17 @@ final class PupilDetector: @unchecked Sendable {
|
||||
|
||||
nonisolated(unsafe) static var enableDebugImageSaving = false
|
||||
nonisolated(unsafe) static var enablePerformanceLogging = false
|
||||
nonisolated(unsafe) static var enableDiagnosticLogging = false
|
||||
nonisolated(unsafe) static var frameSkipCount = 10 // Process every Nth frame
|
||||
|
||||
// MARK: - State (protected by lock)
|
||||
|
||||
private nonisolated(unsafe) static var _debugImageCounter = 0
|
||||
private nonisolated(unsafe) static var _frameCounter = 0
|
||||
private nonisolated(unsafe) static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (
|
||||
nil, nil
|
||||
)
|
||||
private nonisolated(unsafe) static var _lastPupilPositions:
|
||||
(left: PupilPosition?, right: PupilPosition?) = (
|
||||
nil, nil
|
||||
)
|
||||
private nonisolated(unsafe) static var _metrics = PupilDetectorMetrics()
|
||||
|
||||
nonisolated(unsafe) static let calibration = PupilCalibration()
|
||||
@@ -170,7 +176,8 @@ final class PupilDetector: @unchecked Sendable {
|
||||
set { _frameCounter = newValue }
|
||||
}
|
||||
|
||||
private nonisolated static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) {
|
||||
private nonisolated static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?)
|
||||
{
|
||||
get { _lastPupilPositions }
|
||||
set { _lastPupilPositions = newValue }
|
||||
}
|
||||
@@ -218,6 +225,11 @@ final class PupilDetector: @unchecked Sendable {
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Call once per video frame to enable proper frame skipping
|
||||
nonisolated static func advanceFrame() {
|
||||
frameCounter += 1
|
||||
}
|
||||
|
||||
/// Detects pupil position with frame skipping for performance
|
||||
/// Returns cached result on skipped frames
|
||||
nonisolated static func detectPupil(
|
||||
@@ -252,7 +264,7 @@ final class PupilDetector: @unchecked Sendable {
|
||||
let elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
|
||||
metrics.recordProcessingTime(elapsed)
|
||||
if metrics.processedFrameCount % 30 == 0 {
|
||||
print(
|
||||
logDebug(
|
||||
"👁 PupilDetector: \(String(format: "%.2f", elapsed))ms (avg: \(String(format: "%.2f", metrics.averageProcessingTimeMs))ms)"
|
||||
)
|
||||
}
|
||||
@@ -266,10 +278,18 @@ final class PupilDetector: @unchecked Sendable {
|
||||
imageSize: imageSize
|
||||
)
|
||||
|
||||
guard eyePoints.count >= 6 else { return nil }
|
||||
guard eyePoints.count >= 6 else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug("👁 PupilDetector: Failed - eyePoints.count=\(eyePoints.count) < 6")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 2: Create eye region bounding box with margin
|
||||
guard let eyeRegion = createEyeRegion(from: eyePoints, imageSize: imageSize) else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug("👁 PupilDetector: Failed - createEyeRegion returned nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,6 +305,9 @@ final class PupilDetector: @unchecked Sendable {
|
||||
let eyeBuf = eyeBuffer,
|
||||
let tmpBuf = tempBuffer
|
||||
else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug("👁 PupilDetector: Failed - buffers not allocated")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -293,6 +316,9 @@ final class PupilDetector: @unchecked Sendable {
|
||||
extractGrayscaleDataOptimized(
|
||||
from: pixelBuffer, to: grayBuffer, width: frameWidth, height: frameHeight)
|
||||
else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug("👁 PupilDetector: Failed - grayscale extraction failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -301,7 +327,13 @@ final class PupilDetector: @unchecked Sendable {
|
||||
let eyeHeight = Int(eyeRegion.frame.height)
|
||||
|
||||
// Early exit for tiny regions (less than 10x10 pixels)
|
||||
guard eyeWidth >= 10, eyeHeight >= 10 else { return nil }
|
||||
guard eyeWidth >= 10, eyeHeight >= 10 else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug(
|
||||
"👁 PupilDetector: Failed - eye region too small (\(eyeWidth)x\(eyeHeight))")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
guard
|
||||
isolateEyeWithMaskOptimized(
|
||||
@@ -313,6 +345,9 @@ final class PupilDetector: @unchecked Sendable {
|
||||
output: eyeBuf
|
||||
)
|
||||
else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug("👁 PupilDetector: Failed - isolateEyeWithMask failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -352,9 +387,20 @@ final class PupilDetector: @unchecked Sendable {
|
||||
height: eyeHeight
|
||||
)
|
||||
else {
|
||||
if enableDiagnosticLogging {
|
||||
logDebug(
|
||||
"👁 PupilDetector: Failed - findPupilFromContours returned nil (not enough dark pixels)"
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if enableDiagnosticLogging {
|
||||
logDebug(
|
||||
"👁 PupilDetector: Success - centroid at (\(String(format: "%.1f", centroidX)), \(String(format: "%.1f", centroidY))) in \(eyeWidth)x\(eyeHeight) region"
|
||||
)
|
||||
}
|
||||
|
||||
let pupilPosition = PupilPosition(x: CGFloat(centroidX), y: CGFloat(centroidY))
|
||||
|
||||
// Cache result
|
||||
@@ -738,7 +784,9 @@ final class PupilDetector: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func createEyeRegion(from points: [CGPoint], imageSize: CGSize) -> EyeRegion? {
|
||||
private nonisolated static func createEyeRegion(from points: [CGPoint], imageSize: CGSize)
|
||||
-> EyeRegion?
|
||||
{
|
||||
guard !points.isEmpty else { return nil }
|
||||
|
||||
let margin: CGFloat = 5
|
||||
@@ -792,10 +840,12 @@ final class PupilDetector: @unchecked Sendable {
|
||||
|
||||
CGImageDestinationAddImage(destination, cgImage, nil)
|
||||
CGImageDestinationFinalize(destination)
|
||||
print("💾 Saved debug image: \(url.path)")
|
||||
logDebug("💾 Saved debug image: \(url.path)")
|
||||
}
|
||||
|
||||
private nonisolated static func createCGImage(from data: UnsafePointer<UInt8>, width: Int, height: Int)
|
||||
private nonisolated static func createCGImage(
|
||||
from data: UnsafePointer<UInt8>, width: Int, height: Int
|
||||
)
|
||||
-> CGImage?
|
||||
{
|
||||
let mutableData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
@MainActor
|
||||
class TimerEngine: ObservableObject {
|
||||
@@ -17,20 +16,17 @@ class TimerEngine: ObservableObject {
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var sleepStartTime: Date?
|
||||
|
||||
|
||||
/// Time provider for deterministic testing (defaults to system time)
|
||||
private let timeProvider: TimeProviding
|
||||
|
||||
|
||||
// For enforce mode integration
|
||||
private var enforceModeService: EnforceModeService?
|
||||
|
||||
|
||||
// Smart Mode services
|
||||
private var fullscreenService: FullscreenDetectionService?
|
||||
private var idleService: IdleMonitoringService?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// Logging manager
|
||||
private let logger = LoggingManager.shared.timerLogger
|
||||
|
||||
convenience init(
|
||||
settingsManager: any SettingsProviding,
|
||||
@@ -51,19 +47,19 @@ class TimerEngine: ObservableObject {
|
||||
self.settingsProvider = settingsManager
|
||||
self.enforceModeService = enforceModeService ?? EnforceModeService.shared
|
||||
self.timeProvider = timeProvider
|
||||
|
||||
|
||||
Task { @MainActor in
|
||||
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
|
||||
@@ -72,7 +68,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
// Subscribe to idle state changes
|
||||
idleService?.$isIdle
|
||||
.sink { [weak self] isIdle in
|
||||
@@ -82,31 +78,31 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
private func handleFullscreenChange(isFullscreen: Bool) {
|
||||
guard settingsProvider.settings.smartMode.autoPauseOnFullscreen else { return }
|
||||
|
||||
|
||||
if isFullscreen {
|
||||
pauseAllTimers(reason: .fullscreen)
|
||||
logger.info("⏸️ Timers paused: fullscreen detected")
|
||||
logInfo("⏸️ Timers paused: fullscreen detected")
|
||||
} else {
|
||||
resumeAllTimers(reason: .fullscreen)
|
||||
logger.info("▶️ Timers resumed: fullscreen exited")
|
||||
logInfo("▶️ Timers resumed: fullscreen exited")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleIdleChange(isIdle: Bool) {
|
||||
guard settingsProvider.settings.smartMode.autoPauseOnIdle else { return }
|
||||
|
||||
|
||||
if isIdle {
|
||||
pauseAllTimers(reason: .idle)
|
||||
logger.info("⏸️ Timers paused: user idle")
|
||||
logInfo("⏸️ Timers paused: user idle")
|
||||
} else {
|
||||
resumeAllTimers(reason: .idle)
|
||||
logger.info("▶️ Timers resumed: user active")
|
||||
logInfo("▶️ Timers resumed: user active")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func pauseAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(reason)
|
||||
@@ -114,7 +110,7 @@ class TimerEngine: ObservableObject {
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func resumeAllTimers(reason: PauseReason) {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(reason)
|
||||
@@ -129,12 +125,12 @@ class TimerEngine: ObservableObject {
|
||||
updateConfigurations()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Initial start - create all timer states
|
||||
stop()
|
||||
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
|
||||
// Add built-in timers
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsProvider.timerConfiguration(for: timerType)
|
||||
@@ -148,7 +144,7 @@ class TimerEngine: ObservableObject {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add user timers
|
||||
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
@@ -159,7 +155,7 @@ class TimerEngine: ObservableObject {
|
||||
isActive: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
|
||||
@@ -171,27 +167,27 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if enforce mode is active and should affect timer behavior
|
||||
func checkEnforceMode() {
|
||||
// Deprecated - camera is now activated in handleTick before timer triggers
|
||||
}
|
||||
|
||||
|
||||
private func updateConfigurations() {
|
||||
logger.debug("Updating timer configurations")
|
||||
logDebug("Updating timer configurations")
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
|
||||
// Update built-in timers
|
||||
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
|
||||
logger.debug("Timer interval changed")
|
||||
logDebug("Timer interval changed")
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
@@ -204,7 +200,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
} else {
|
||||
// Timer was just enabled - create new state
|
||||
logger.debug("Timer enabled")
|
||||
logDebug("Timer enabled")
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: config.intervalSeconds,
|
||||
@@ -215,18 +211,18 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
// If config.enabled is false and timer exists, it will be removed
|
||||
}
|
||||
|
||||
|
||||
// Update user timers
|
||||
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
|
||||
logger.debug("User timer interval changed")
|
||||
logDebug("User timer interval changed")
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
@@ -239,7 +235,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
} else {
|
||||
// New timer - create state
|
||||
logger.debug("User timer created")
|
||||
logDebug("User timer created")
|
||||
newStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds,
|
||||
@@ -250,7 +246,7 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
// If timer is disabled, it will be removed
|
||||
}
|
||||
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
}
|
||||
@@ -276,14 +272,14 @@ class TimerEngine: ObservableObject {
|
||||
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)
|
||||
@@ -293,17 +289,18 @@ class TimerEngine: ObservableObject {
|
||||
|
||||
func skipNext(identifier: TimerIdentifier) {
|
||||
guard let state = timerStates[identifier] else { return }
|
||||
|
||||
|
||||
let intervalSeconds: Int
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
let config = settingsProvider.timerConfiguration(for: type)
|
||||
intervalSeconds = config.intervalSeconds
|
||||
case .user(let id):
|
||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else { return }
|
||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id })
|
||||
else { return }
|
||||
intervalSeconds = userTimer.intervalMinutes * 60
|
||||
}
|
||||
|
||||
|
||||
timerStates[identifier] = TimerState(
|
||||
identifier: identifier,
|
||||
intervalSeconds: intervalSeconds,
|
||||
@@ -319,7 +316,7 @@ class TimerEngine: ObservableObject {
|
||||
let identifier = reminder.identifier
|
||||
skipNext(identifier: identifier)
|
||||
resumeTimer(identifier: identifier)
|
||||
|
||||
|
||||
enforceModeService?.handleReminderDismissed()
|
||||
}
|
||||
|
||||
@@ -327,7 +324,7 @@ class TimerEngine: ObservableObject {
|
||||
for (identifier, state) in timerStates {
|
||||
guard !state.isPaused else { continue }
|
||||
guard state.isActive else { continue }
|
||||
|
||||
|
||||
if state.targetDate < timeProvider.now() - 3.0 {
|
||||
skipNext(identifier: identifier)
|
||||
continue
|
||||
@@ -340,12 +337,13 @@ class TimerEngine: ObservableObject {
|
||||
if case .builtIn(.lookAway) = identifier {
|
||||
if enforceModeService?.shouldEnforceBreak(for: identifier) == true {
|
||||
Task { @MainActor in
|
||||
await enforceModeService?.startCameraForLookawayTimer(secondsRemaining: updatedState.remainingSeconds)
|
||||
await enforceModeService?.startCameraForLookawayTimer(
|
||||
secondsRemaining: updatedState.remainingSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
triggerReminder(for: identifier)
|
||||
break
|
||||
@@ -357,7 +355,7 @@ class TimerEngine: ObservableObject {
|
||||
func triggerReminder(for identifier: TimerIdentifier) {
|
||||
// Pause only the timer that triggered
|
||||
pauseTimer(identifier: identifier)
|
||||
|
||||
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
switch type {
|
||||
@@ -384,16 +382,16 @@ class TimerEngine: ObservableObject {
|
||||
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
|
||||
return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
|
||||
}
|
||||
|
||||
|
||||
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
|
||||
|
||||
/// Handles system sleep event
|
||||
/// - Saves current time for elapsed calculation
|
||||
/// - Pauses all active timers
|
||||
func handleSystemSleep() {
|
||||
logger.debug("System going to sleep")
|
||||
logDebug("System going to sleep")
|
||||
sleepStartTime = timeProvider.now()
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.system)
|
||||
@@ -401,24 +399,24 @@ class TimerEngine: ObservableObject {
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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
|
||||
func handleSystemWake() {
|
||||
logger.debug("System waking up")
|
||||
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)
|
||||
@@ -427,18 +425,19 @@ class TimerEngine: ObservableObject {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,6 @@ struct MenuBarContentView: View {
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(MenuBarHoverButtonStyle())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
Spacer()
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user