226 lines
6.5 KiB
Swift
226 lines
6.5 KiB
Swift
// ── Timer Store ────────────────────────────────────────────────
|
|
// Observable store managing all timers — equivalent of Zustand store
|
|
// Persists to UserDefaults (SwiftData migration in future)
|
|
|
|
import Foundation
|
|
import Combine
|
|
import ActivityKit
|
|
|
|
@MainActor
|
|
final class TimerStore: ObservableObject {
|
|
// MARK: - Published State
|
|
|
|
@Published var timers: [CMTimer] = []
|
|
@Published var now: Date = Date()
|
|
|
|
// MARK: - Private
|
|
|
|
private var tickTimer: Timer?
|
|
private let persistenceKey = "chronomind-timers"
|
|
private let notifications = CMNotificationManager.shared
|
|
private let sharedData = SharedTimerDataManager.shared
|
|
private let liveActivity = LiveActivityManager.shared
|
|
|
|
// MARK: - Init
|
|
|
|
init() {
|
|
loadTimers()
|
|
startTicking()
|
|
syncToSharedData()
|
|
}
|
|
|
|
deinit {
|
|
tickTimer?.invalidate()
|
|
}
|
|
|
|
// MARK: - CRUD
|
|
|
|
func addAlarm(_ params: CreateAlarmParams) -> CMTimer {
|
|
let timer = createAlarm(params)
|
|
timers.append(timer)
|
|
notifications.scheduleNotifications(for: timer)
|
|
saveTimers()
|
|
liveActivity.startActivity(for: timer)
|
|
return timer
|
|
}
|
|
|
|
func addCountdown(_ params: CreateCountdownParams) -> CMTimer {
|
|
let timer = createCountdown(params)
|
|
timers.append(timer)
|
|
notifications.scheduleNotifications(for: timer)
|
|
saveTimers()
|
|
liveActivity.startActivity(for: timer)
|
|
return timer
|
|
}
|
|
|
|
func addPomodoro(_ params: CreatePomodoroParams = CreatePomodoroParams()) -> CMTimer {
|
|
let timer = createPomodoro(params)
|
|
timers.append(timer)
|
|
notifications.scheduleNotifications(for: timer)
|
|
saveTimers()
|
|
liveActivity.startActivity(for: timer)
|
|
return timer
|
|
}
|
|
|
|
func removeTimer(_ id: String) {
|
|
liveActivity.endActivity(for: id)
|
|
timers.removeAll { $0.id == id }
|
|
notifications.removeNotifications(for: id)
|
|
saveTimers()
|
|
}
|
|
|
|
// MARK: - State Transitions
|
|
|
|
func pause(_ id: String) {
|
|
updateTimer(id) { pauseTimer($0) }
|
|
}
|
|
|
|
func resume(_ id: String) {
|
|
updateTimer(id) { t in
|
|
let resumed = resumeTimer(t)
|
|
self.notifications.scheduleNotifications(for: resumed)
|
|
return resumed
|
|
}
|
|
}
|
|
|
|
func fire(_ id: String) {
|
|
updateTimer(id) { fireTimer($0) }
|
|
}
|
|
|
|
func snooze(_ id: String, minutes: Int) {
|
|
updateTimer(id) { t in
|
|
let snoozed = snoozeTimer(t, snoozeMinutes: minutes)
|
|
self.notifications.scheduleNotifications(for: snoozed)
|
|
return snoozed
|
|
}
|
|
}
|
|
|
|
func dismiss(_ id: String) {
|
|
liveActivity.endActivity(for: id)
|
|
updateTimer(id) { dismissTimer($0) }
|
|
}
|
|
|
|
func complete(_ id: String) {
|
|
liveActivity.endActivity(for: id)
|
|
updateTimer(id) { completeTimer($0) }
|
|
}
|
|
|
|
func advancePom(_ id: String) {
|
|
guard let index = timers.firstIndex(where: { $0.id == id }) else { return }
|
|
if let next = advancePomodoro(timers[index]) {
|
|
timers[index] = next
|
|
notifications.scheduleNotifications(for: next)
|
|
saveTimers()
|
|
}
|
|
}
|
|
|
|
// MARK: - Tick
|
|
|
|
func tick() {
|
|
let currentTime = Date()
|
|
now = currentTime
|
|
|
|
var changed = false
|
|
|
|
for i in timers.indices {
|
|
// Check if timer should fire
|
|
if shouldTimerFire(timers[i], now: currentTime) {
|
|
timers[i] = fireTimer(timers[i])
|
|
changed = true
|
|
// Haptic feedback
|
|
HapticEngine.fire(urgency: timers[i].urgency)
|
|
continue
|
|
}
|
|
|
|
// Check cascade warnings
|
|
let newlyFired = checkWarnings(&timers[i].warnings, now: currentTime)
|
|
if !newlyFired.isEmpty {
|
|
changed = true
|
|
// Update state to warning if still active
|
|
if timers[i].state == .active {
|
|
timers[i].state = .warning
|
|
}
|
|
// Haptic feedback for warning
|
|
HapticEngine.warning(urgency: timers[i].urgency)
|
|
}
|
|
}
|
|
|
|
if changed {
|
|
saveTimers()
|
|
}
|
|
|
|
// Update Live Activity for the next firing timer
|
|
if let next = nextFiringTimer {
|
|
liveActivity.updateActivity(for: next)
|
|
}
|
|
}
|
|
|
|
// MARK: - Queries
|
|
|
|
func getTimer(_ id: String) -> CMTimer? {
|
|
timers.first { $0.id == id }
|
|
}
|
|
|
|
var activeTimers: [CMTimer] {
|
|
timers.filter { [.active, .warning, .snoozed, .paused, .firing].contains($0.state) }
|
|
}
|
|
|
|
var nextFiringTimer: CMTimer? {
|
|
timers
|
|
.filter { [.active, .warning].contains($0.state) }
|
|
.sorted { $0.targetTime < $1.targetTime }
|
|
.first
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private func updateTimer(_ id: String, updater: (CMTimer) -> CMTimer) {
|
|
guard let index = timers.firstIndex(where: { $0.id == id }) else { return }
|
|
timers[index] = updater(timers[index])
|
|
saveTimers()
|
|
}
|
|
|
|
private func startTicking() {
|
|
tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
Task { @MainActor in
|
|
self?.tick()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Persistence (UserDefaults for now, SwiftData later)
|
|
|
|
private func saveTimers() {
|
|
guard let data = try? JSONEncoder().encode(timers) else { return }
|
|
UserDefaults.standard.set(data, forKey: persistenceKey)
|
|
syncToSharedData()
|
|
}
|
|
|
|
private func loadTimers() {
|
|
guard let data = UserDefaults.standard.data(forKey: persistenceKey),
|
|
let saved = try? JSONDecoder().decode([CMTimer].self, from: data) else { return }
|
|
timers = saved
|
|
}
|
|
|
|
// MARK: - App Group Sync
|
|
|
|
/// Sync timer data to App Group for widgets and watchOS
|
|
private func syncToSharedData() {
|
|
sharedData.writeTimers(timers)
|
|
sharedData.writeActiveTimer(nextFiringTimer)
|
|
reloadWidgets()
|
|
}
|
|
|
|
/// Reload all WidgetKit timelines when data changes
|
|
private func reloadWidgets() {
|
|
// Post notification that widget-aware code can observe
|
|
NotificationCenter.default.post(name: .chronoMindTimersDidChange, object: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Notification Names
|
|
|
|
extension Notification.Name {
|
|
static let chronoMindTimersDidChange = Notification.Name("chronoMindTimersDidChange")
|
|
}
|