181 lines
5.3 KiB
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 }
|
|
}
|
|
}
|