// ── Mac Timer Store ─────────────────────────────────────────── // macOS-specific timer store that shares engine logic with iOS // Reads from shared App Group UserDefaults for cross-device sync import Foundation import Combine import UserNotifications @MainActor final class MacTimerStore: ObservableObject { static let shared = MacTimerStore() @Published var timers: [CMTimer] = [] @Published var now: Date = Date() private var tickTimer: Timer? private let persistenceKey = "chronomind-timers" private let sharedDefaults: UserDefaults? var activeTimers: [CMTimer] { timers.filter { isTimerActive($0) } } var nextFiringTimer: CMTimer? { activeTimers .filter { $0.state == .active || $0.state == .warning } .sorted { $0.targetTime < $1.targetTime } .first } private init() { sharedDefaults = UserDefaults(suiteName: "group.com.chronomind.shared") loadTimers() startTicking() requestNotificationPermission() } deinit { tickTimer?.invalidate() } // MARK: - CRUD func addCountdown(label: String, durationSeconds: TimeInterval) { let timer = createCountdown(CreateCountdownParams( label: label, durationSeconds: durationSeconds )) timers.append(timer) scheduleNotification(for: timer) saveTimers() } func addAlarm(label: String, targetTime: Date, urgency: UrgencyLevel = .standard) { let timer = createAlarm(CreateAlarmParams( label: label, targetTime: targetTime, urgency: urgency )) timers.append(timer) scheduleNotification(for: timer) saveTimers() } func removeTimer(_ id: String) { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id]) timers.removeAll { $0.id == id } saveTimers() } func pause(_ id: String) { updateTimer(id) { pauseTimer($0) } } func resume(_ id: String) { updateTimer(id) { t in let resumed = resumeTimer(t) self.scheduleNotification(for: resumed) return resumed } } func snooze(_ id: String, minutes: Int) { updateTimer(id) { t in let snoozed = snoozeTimer(t, snoozeMinutes: minutes) self.scheduleNotification(for: snoozed) return snoozed } } func dismiss(_ id: String) { updateTimer(id) { dismissTimer($0) } } func complete(_ id: String) { updateTimer(id) { completeTimer($0) } } // MARK: - Tick private func startTicking() { tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Task { @MainActor in self?.tick() } } } private func tick() { now = Date() var changed = false for i in timers.indices { if shouldTimerFire(timers[i], now: now) { timers[i] = fireTimer(timers[i]) changed = true } let newlyFired = checkWarnings(&timers[i].warnings, now: now) if !newlyFired.isEmpty { changed = true if timers[i].state == .active { timers[i].state = .warning } } } if changed { saveTimers() } } // MARK: - Persistence private func loadTimers() { // Try shared defaults first (synced from iOS), then local let defaults = sharedDefaults ?? UserDefaults.standard guard let data = defaults.data(forKey: persistenceKey) else { return } if let decoded = try? JSONDecoder().decode([CMTimer].self, from: data) { timers = decoded } } private func saveTimers() { guard let data = try? JSONEncoder().encode(timers) else { return } UserDefaults.standard.set(data, forKey: persistenceKey) sharedDefaults?.set(data, forKey: persistenceKey) } // MARK: - Notifications private func requestNotificationPermission() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in } } private func scheduleNotification(for timer: CMTimer) { let content = UNMutableNotificationContent() content.title = timer.label content.body = "Timer fired!" content.sound = .default content.categoryIdentifier = "TIMER_FIRED" let interval = timer.targetTime.timeIntervalSinceNow guard interval > 0 else { return } let trigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: false) let request = UNNotificationRequest(identifier: timer.id, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) } // MARK: - Helpers private func updateTimer(_ id: String, transform: (CMTimer) -> CMTimer) { guard let index = timers.firstIndex(where: { $0.id == id }) else { return } timers[index] = transform(timers[index]) saveTimers() } func getTimer(_ id: String) -> CMTimer? { timers.first { $0.id == id } } }