learning_ai_clock/ios/ChronoMind/LiveActivity/TimerActivityAttributes.swift

146 lines
4.7 KiB
Swift

// Live Activity Attributes
// ActivityKit data model for Dynamic Island + Lock Screen Live Activities
// Shared between main app and widget extension
import Foundation
import ActivityKit
// MARK: - Timer Live Activity
struct TimerActivityAttributes: ActivityAttributes {
/// Static data that doesn't change during the activity
struct ContentState: Codable, Hashable {
let remainingSeconds: Int
let targetTime: Date
let state: String // CMTimerState raw value
let urgencyHex: String // color hex for urgency
let pomodoroRound: Int? // current round (nil if not pomodoro)
let pomodoroTotal: Int? // total rounds
let isBreak: Bool // pomodoro break phase
}
// Fixed for the lifetime of the activity
let timerId: String
let timerLabel: String
let timerType: String // CMTimerType raw value
let urgency: String // UrgencyLevel raw value
let createdAt: Date
}
// MARK: - Activity Manager
@MainActor
final class LiveActivityManager {
static let shared = LiveActivityManager()
private var currentActivity: Activity<TimerActivityAttributes>?
private init() {}
// MARK: - Start
/// Start a Live Activity for an active timer
func startActivity(for timer: CMTimer) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
// End any existing activity first
endActivity()
let urgencyConfig = getUrgencyConfig(timer.urgency)
let remaining = Int(max(0, timer.targetTime.timeIntervalSinceNow))
let attributes = TimerActivityAttributes(
timerId: timer.id,
timerLabel: timer.label,
timerType: timer.type.rawValue,
urgency: timer.urgency.rawValue,
createdAt: timer.createdAt
)
let state = TimerActivityAttributes.ContentState(
remainingSeconds: remaining,
targetTime: timer.targetTime,
state: timer.state.rawValue,
urgencyHex: urgencyConfig.colorHex,
pomodoroRound: timer.pomodoroState?.currentRound,
pomodoroTotal: timer.pomodoroConfig?.rounds,
isBreak: timer.pomodoroState?.isBreak ?? false
)
let content = ActivityContent(state: state, staleDate: timer.targetTime.addingTimeInterval(60))
do {
currentActivity = try Activity.request(
attributes: attributes,
content: content,
pushType: nil
)
} catch {
// Live Activity not available silent fallback
}
}
// MARK: - Update
/// Update the Live Activity with new timer state
func updateActivity(for timer: CMTimer) {
guard let activity = currentActivity, activity.activityState == .active else {
// If no active activity but timer is active, start one
if [.active, .warning].contains(timer.state) {
startActivity(for: timer)
}
return
}
let urgencyConfig = getUrgencyConfig(timer.urgency)
let remaining = Int(max(0, timer.targetTime.timeIntervalSinceNow))
let state = TimerActivityAttributes.ContentState(
remainingSeconds: remaining,
targetTime: timer.targetTime,
state: timer.state.rawValue,
urgencyHex: urgencyConfig.colorHex,
pomodoroRound: timer.pomodoroState?.currentRound,
pomodoroTotal: timer.pomodoroConfig?.rounds,
isBreak: timer.pomodoroState?.isBreak ?? false
)
let content = ActivityContent(state: state, staleDate: timer.targetTime.addingTimeInterval(60))
Task {
await activity.update(content)
}
}
// MARK: - End
/// End the current Live Activity
func endActivity() {
guard let activity = currentActivity else { return }
let finalState = TimerActivityAttributes.ContentState(
remainingSeconds: 0,
targetTime: Date(),
state: CMTimerState.dismissed.rawValue,
urgencyHex: "#5B8DEE",
pomodoroRound: nil,
pomodoroTotal: nil,
isBreak: false
)
let content = ActivityContent(state: finalState, staleDate: nil)
Task {
await activity.end(content, dismissalPolicy: .immediate)
}
currentActivity = nil
}
/// End activity for a specific timer ID
func endActivity(for timerId: String) {
guard let activity = currentActivity,
activity.attributes.timerId == timerId else { return }
endActivity()
}
}