learning_ai_clock/ios/ChronoMindMac/MacTimerStore.swift

181 lines
5.3 KiB
Swift

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