146 lines
4.7 KiB
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()
|
|
}
|
|
}
|