TheNoiseClock/TheNoiseClockWidget/AlarmLiveActivityWidget.swift

239 lines
7.8 KiB
Swift

//
// AlarmLiveActivityWidget.swift
// TheNoiseClockWidget
//
// Created by Matt Bruce on 2/2/26.
//
import AlarmKit
import AppIntents
import SwiftUI
import WidgetKit
/// Live Activity widget for alarm countdown and alerting states.
/// Uses AlarmKit's AlarmAttributes for automatic countdown management.
struct AlarmLiveActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: AlarmAttributes<NoiseClockAlarmMetadata>.self) { context in
// Lock Screen presentation
LockScreenAlarmView(
attributes: context.attributes,
state: context.state
)
} dynamicIsland: { context in
DynamicIsland {
// Expanded regions - shown when long-pressed or alerting
DynamicIslandExpandedRegion(.leading) {
if let metadata = context.attributes.metadata {
AlarmTitleView(metadata: metadata)
} else {
Text("Alarm")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.trailing) {
AlarmProgressView(state: context.state)
}
DynamicIslandExpandedRegion(.bottom) {
ExpandedAlarmView(
attributes: context.attributes,
state: context.state
)
}
} compactLeading: {
// Compact leading - alarm icon during countdown
Image(systemName: "alarm.fill")
.foregroundStyle(context.attributes.tintColor)
} compactTrailing: {
// Compact trailing - countdown text
CountdownTextView(state: context.state)
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
} minimal: {
// Minimal - just an alarm icon
Image(systemName: "alarm.fill")
.foregroundStyle(context.attributes.tintColor)
}
}
}
}
// MARK: - Lock Screen View
struct LockScreenAlarmView: View {
let attributes: AlarmAttributes<NoiseClockAlarmMetadata>
let state: AlarmPresentationState
private var alarmLabel: String {
attributes.metadata?.label ?? "Alarm"
}
private var alarmId: String {
attributes.metadata?.alarmId ?? ""
}
var body: some View {
VStack(spacing: 12) {
// Alarm label
Text(alarmLabel)
.font(.headline)
.foregroundStyle(.primary)
// Content based on state
if case .countdown(let countdown) = state.mode {
// Countdown state - show timer
VStack(spacing: 4) {
Text("Alarm in")
.font(.caption)
.foregroundStyle(.secondary)
Text(timerInterval: Date.now...countdown.fireDate, countsDown: true)
.font(.system(size: 48, weight: .bold, design: .rounded))
.monospacedDigit()
.foregroundStyle(attributes.tintColor)
}
} else if case .paused = state.mode {
Text("Paused")
.font(.title3)
.foregroundStyle(.secondary)
} else {
// Alerting state - show ringing UI with action buttons
VStack(spacing: 16) {
Image(systemName: "alarm.waves.left.and.right.fill")
.font(.system(size: 40))
.foregroundStyle(attributes.tintColor)
.symbolEffect(.bounce.byLayer, options: .repeating)
Text("Alarm Ringing")
.font(.title3.weight(.semibold))
// Action buttons
HStack(spacing: 20) {
// Snooze button
Button(intent: SnoozeAlarmIntent(alarmId: alarmId)) {
Label("Snooze", systemImage: "zzz")
.font(.callout.weight(.medium))
}
.buttonStyle(.bordered)
.tint(.blue)
// Stop button
Button(intent: StopAlarmIntent(alarmId: alarmId)) {
Label("Stop", systemImage: "stop.fill")
.font(.callout.weight(.medium))
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
}
}
}
.padding()
.frame(maxWidth: .infinity)
}
}
// MARK: - Expanded Dynamic Island View
struct ExpandedAlarmView: View {
let attributes: AlarmAttributes<NoiseClockAlarmMetadata>
let state: AlarmPresentationState
private var alarmId: String {
attributes.metadata?.alarmId ?? ""
}
private var isAlerting: Bool {
if case .countdown = state.mode { return false }
if case .paused = state.mode { return false }
return true
}
var body: some View {
if isAlerting {
// Alerting state - show action buttons
HStack(spacing: 16) {
Button(intent: SnoozeAlarmIntent(alarmId: alarmId)) {
Text("Snooze")
.font(.caption.weight(.medium))
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.blue)
Button(intent: StopAlarmIntent(alarmId: alarmId)) {
Text("Stop")
.font(.caption.weight(.medium))
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
} else {
// Countdown state - show countdown info
HStack {
CountdownTextView(state: state)
.font(.headline)
Spacer()
AlarmProgressView(state: state)
.frame(maxHeight: 30)
}
}
}
}
// MARK: - Countdown Text View
struct CountdownTextView: View {
let state: AlarmPresentationState
var body: some View {
if case .countdown(let countdown) = state.mode {
Text(timerInterval: Date.now...countdown.fireDate, countsDown: true)
.monospacedDigit()
.lineLimit(1)
} else if case .paused = state.mode {
Text("Paused")
} else {
Text("Now!")
.fontWeight(.bold)
}
}
}
// MARK: - Progress View
struct AlarmProgressView: View {
let state: AlarmPresentationState
var body: some View {
if case .countdown(let countdown) = state.mode {
ProgressView(
timerInterval: Date.now...countdown.fireDate,
label: { EmptyView() },
currentValueLabel: { Text("") }
)
.progressViewStyle(.circular)
} else if case .paused = state.mode {
Image(systemName: "pause.fill")
} else {
Image(systemName: "alarm.waves.left.and.right.fill")
.symbolEffect(.pulse)
}
}
}
// MARK: - Title View
struct AlarmTitleView: View {
let metadata: NoiseClockAlarmMetadata
var body: some View {
Text(metadata.label)
.font(.caption)
.lineLimit(1)
}
}