184 lines
6.1 KiB
Swift
184 lines
6.1 KiB
Swift
// ── Next Timer Widget (Small) ─────────────────────────────────
|
|
// Home screen small widget showing next timer + countdown
|
|
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
// MARK: - Timeline Provider
|
|
|
|
struct NextTimerProvider: AppIntentTimelineProvider {
|
|
typealias Entry = NextTimerEntry
|
|
typealias Intent = NextTimerIntent
|
|
|
|
func placeholder(in context: Context) -> NextTimerEntry {
|
|
NextTimerEntry(
|
|
date: Date(),
|
|
timer: TimerSnapshot(
|
|
id: "placeholder",
|
|
label: "Team Standup",
|
|
type: .alarm,
|
|
urgency: .important,
|
|
state: .active,
|
|
targetTime: Date().addingTimeInterval(3600),
|
|
duration: 3600,
|
|
startedAt: Date(),
|
|
elapsedBeforePause: 0,
|
|
snoozeCount: 0,
|
|
category: nil,
|
|
pomodoroCurrentRound: nil,
|
|
pomodoroTotalRounds: nil,
|
|
pomodoroIsBreak: nil,
|
|
nextWarningTime: Date().addingTimeInterval(1800),
|
|
totalWarnings: 3,
|
|
firedWarnings: 1
|
|
)
|
|
)
|
|
}
|
|
|
|
func snapshot(for configuration: NextTimerIntent, in context: Context) async -> NextTimerEntry {
|
|
let timer = SharedTimerDataManager.shared.readNextFiringTimer()
|
|
return NextTimerEntry(date: Date(), timer: timer)
|
|
}
|
|
|
|
func timeline(for configuration: NextTimerIntent, in context: Context) async -> Timeline<NextTimerEntry> {
|
|
let timer = SharedTimerDataManager.shared.readNextFiringTimer()
|
|
let now = Date()
|
|
|
|
// Create entries every 5 minutes for the next 30 minutes
|
|
var entries: [NextTimerEntry] = []
|
|
for minuteOffset in stride(from: 0, through: 30, by: 5) {
|
|
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
|
|
entries.append(NextTimerEntry(date: entryDate, timer: timer))
|
|
}
|
|
|
|
// Reload after 30 minutes or when timer fires
|
|
let reloadDate: Date
|
|
if let timer = timer {
|
|
reloadDate = min(timer.targetTime, now.addingTimeInterval(30 * 60))
|
|
} else {
|
|
reloadDate = now.addingTimeInterval(30 * 60)
|
|
}
|
|
|
|
return Timeline(entries: entries, policy: .after(reloadDate))
|
|
}
|
|
}
|
|
|
|
// MARK: - Intent (no config needed, just a placeholder for AppIntentTimelineProvider)
|
|
|
|
import AppIntents
|
|
|
|
struct NextTimerIntent: WidgetConfigurationIntent {
|
|
static var title: LocalizedStringResource = "Next Timer"
|
|
static var description: IntentDescription = "Shows your next upcoming timer"
|
|
}
|
|
|
|
// MARK: - Entry
|
|
|
|
struct NextTimerEntry: TimelineEntry {
|
|
let date: Date
|
|
let timer: TimerSnapshot?
|
|
}
|
|
|
|
// MARK: - Widget View
|
|
|
|
struct NextTimerWidgetView: View {
|
|
@Environment(\.widgetFamily) var family
|
|
let entry: NextTimerEntry
|
|
|
|
var body: some View {
|
|
if let timer = entry.timer {
|
|
timerContent(timer)
|
|
} else {
|
|
emptyContent
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func timerContent(_ timer: TimerSnapshot) -> some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
// Urgency indicator + label
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(urgencyColor(timer.urgency))
|
|
.frame(width: 8, height: 8)
|
|
Text(timer.label)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(.primary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Countdown using SwiftUI Date-relative text
|
|
if timer.state == .paused {
|
|
Text("Paused")
|
|
.font(.system(size: 28, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(timer.targetTime, style: .timer)
|
|
.font(.system(size: 28, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(.primary)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
|
|
// Target time
|
|
Text(formatTime(timer.targetTime))
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.containerBackground(for: .widget) {
|
|
urgencyColor(timer.urgency).opacity(0.1)
|
|
}
|
|
.widgetURL(URL(string: "chronomind://timer/\(timer.id)"))
|
|
}
|
|
|
|
private var emptyContent: some View {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "clock")
|
|
.font(.system(size: 28))
|
|
.foregroundStyle(.secondary)
|
|
Text("No Timers")
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
Text("Tap to create one")
|
|
.font(.system(size: 11))
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.padding()
|
|
.containerBackground(for: .widget) {
|
|
Color(.systemBackground)
|
|
}
|
|
.widgetURL(URL(string: "chronomind://create"))
|
|
}
|
|
|
|
private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
|
|
switch urgency {
|
|
case .critical: return Color(red: 1.0, green: 0.28, blue: 0.34)
|
|
case .important: return Color(red: 1.0, green: 0.62, blue: 0.26)
|
|
case .standard: return Color(red: 0.99, green: 0.79, blue: 0.34)
|
|
case .gentle: return Color(red: 0.18, green: 0.84, blue: 0.45)
|
|
case .passive: return Color(red: 0.36, green: 0.55, blue: 0.93)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Widget Definition
|
|
|
|
struct NextTimerWidget: Widget {
|
|
let kind: String = "NextTimerWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(
|
|
kind: kind,
|
|
intent: NextTimerIntent.self,
|
|
provider: NextTimerProvider()
|
|
) { entry in
|
|
NextTimerWidgetView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Next Timer")
|
|
.description("Shows your next upcoming timer with countdown")
|
|
.supportedFamilies([.systemSmall])
|
|
}
|
|
}
|