239 lines
7.8 KiB
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)
|
|
}
|
|
}
|
|
|