learning_ai_clock/ios/ChronoMindWidgets/TimerLiveActivity.swift

216 lines
8.4 KiB
Swift

// Timer Live Activity
// Dynamic Island + Lock Screen Live Activity for active timer countdown
import SwiftUI
import WidgetKit
import ActivityKit
// MARK: - Live Activity Widget
struct TimerLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerActivityAttributes.self) { context in
// Lock Screen Live Activity
lockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded regions
DynamicIslandExpandedRegion(.leading) {
expandedLeading(context: context)
}
DynamicIslandExpandedRegion(.trailing) {
expandedTrailing(context: context)
}
DynamicIslandExpandedRegion(.center) {
expandedCenter(context: context)
}
DynamicIslandExpandedRegion(.bottom) {
expandedBottom(context: context)
}
} compactLeading: {
// Compact leading urgency dot + label
HStack(spacing: 4) {
Circle()
.fill(colorFromHex(context.state.urgencyHex))
.frame(width: 8, height: 8)
Text(context.attributes.timerLabel)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.frame(maxWidth: 60)
}
} compactTrailing: {
// Compact trailing countdown
Text(context.state.targetTime, style: .timer)
.font(.system(size: 12, weight: .bold, design: .monospaced))
.foregroundStyle(colorFromHex(context.state.urgencyHex))
.frame(minWidth: 40)
.multilineTextAlignment(.trailing)
} minimal: {
// Minimal just countdown
Text(context.state.targetTime, style: .timer)
.font(.system(size: 11, weight: .bold, design: .monospaced))
.multilineTextAlignment(.center)
}
}
}
// MARK: - Lock Screen View
@ViewBuilder
private func lockScreenView(context: ActivityViewContext<TimerActivityAttributes>) -> some View {
let urgencyColor = colorFromHex(context.state.urgencyHex)
VStack(spacing: 8) {
HStack {
// Urgency indicator + label
HStack(spacing: 6) {
Circle()
.fill(urgencyColor)
.frame(width: 10, height: 10)
Text(context.attributes.timerLabel)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white)
}
Spacer()
// Pomodoro round indicator
if let round = context.state.pomodoroRound,
let total = context.state.pomodoroTotal {
Text(context.state.isBreak ? "Break" : "Round \(round)/\(total)")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.white.opacity(0.7))
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(urgencyColor.opacity(0.3))
.clipShape(Capsule())
}
}
HStack(alignment: .bottom) {
// Large countdown
Text(context.state.targetTime, style: .timer)
.font(.system(size: 36, weight: .bold, design: .monospaced))
.foregroundStyle(.white)
.contentTransition(.numericText())
Spacer()
// Target time
VStack(alignment: .trailing, spacing: 2) {
Text("fires at")
.font(.system(size: 10))
.foregroundStyle(.white.opacity(0.5))
Text(context.state.targetTime, style: .time)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.white.opacity(0.8))
}
}
// Urgency progress bar
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2)
.fill(.white.opacity(0.15))
.frame(height: 3)
RoundedRectangle(cornerRadius: 2)
.fill(urgencyColor)
.frame(width: max(0, geo.size.width * progressFraction(context.state)), height: 3)
}
}
.frame(height: 3)
}
.padding(16)
.activityBackgroundTint(Color.black.opacity(0.85))
.activitySystemActionForegroundColor(.white)
}
// MARK: - Dynamic Island Expanded Views
@ViewBuilder
private func expandedLeading(context: ActivityViewContext<TimerActivityAttributes>) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(context.attributes.timerLabel)
.font(.system(size: 14, weight: .semibold))
.lineLimit(1)
if let round = context.state.pomodoroRound,
let total = context.state.pomodoroTotal {
Text(context.state.isBreak ? "Break" : "Round \(round)/\(total)")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder
private func expandedTrailing(context: ActivityViewContext<TimerActivityAttributes>) -> some View {
Text(context.state.targetTime, style: .timer)
.font(.system(size: 20, weight: .bold, design: .monospaced))
.foregroundStyle(colorFromHex(context.state.urgencyHex))
.contentTransition(.numericText())
.multilineTextAlignment(.trailing)
}
@ViewBuilder
private func expandedCenter(context: ActivityViewContext<TimerActivityAttributes>) -> some View {
EmptyView()
}
@ViewBuilder
private func expandedBottom(context: ActivityViewContext<TimerActivityAttributes>) -> some View {
HStack(spacing: 16) {
// Fires at
HStack(spacing: 4) {
Image(systemName: "bell.fill")
.font(.system(size: 10))
.foregroundStyle(.secondary)
Text(context.state.targetTime, style: .time)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.secondary)
}
Spacer()
// Type indicator
HStack(spacing: 4) {
Image(systemName: typeIcon(context.attributes.timerType))
.font(.system(size: 10))
.foregroundStyle(.secondary)
Text(context.attributes.timerType.capitalized)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.secondary)
}
}
}
// MARK: - Helpers
private func progressFraction(_ state: TimerActivityAttributes.ContentState) -> CGFloat {
let remaining = state.targetTime.timeIntervalSinceNow
let total = max(1, TimeInterval(state.remainingSeconds))
let fraction = 1.0 - (remaining / total)
return CGFloat(min(1, max(0, fraction)))
}
private func colorFromHex(_ hex: String) -> Color {
switch hex {
case "#FF4757": return Color(red: 1.0, green: 0.28, blue: 0.34)
case "#FF9F43": return Color(red: 1.0, green: 0.62, blue: 0.26)
case "#FECA57": return Color(red: 0.99, green: 0.79, blue: 0.34)
case "#2ED573": return Color(red: 0.18, green: 0.84, blue: 0.45)
case "#A5B1C7": return Color(red: 0.65, green: 0.69, blue: 0.78)
default: return Color(red: 0.36, green: 0.55, blue: 0.93) // accent
}
}
private func typeIcon(_ type: String) -> String {
switch type {
case "alarm": return "alarm.fill"
case "countdown": return "timer"
case "pomodoro": return "target"
case "event": return "calendar"
default: return "clock"
}
}
}