// ── Timer List Widget (Medium) ──────────────────────────────── // Home screen medium widget showing next 3 timers as mini-timeline import SwiftUI import WidgetKit import AppIntents // MARK: - Timeline Provider struct TimerListProvider: AppIntentTimelineProvider { typealias Entry = TimerListEntry typealias Intent = TimerListIntent func placeholder(in context: Context) -> TimerListEntry { TimerListEntry(date: Date(), timers: Self.placeholderTimers) } func snapshot(for configuration: TimerListIntent, in context: Context) async -> TimerListEntry { let timers = SharedTimerDataManager.shared.readActiveSnapshots() return TimerListEntry(date: Date(), timers: Array(timers.prefix(3))) } func timeline(for configuration: TimerListIntent, in context: Context) async -> Timeline { let timers = Array(SharedTimerDataManager.shared.readActiveSnapshots().prefix(3)) let now = Date() var entries: [TimerListEntry] = [] for minuteOffset in stride(from: 0, through: 30, by: 5) { let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)! entries.append(TimerListEntry(date: entryDate, timers: timers)) } let reloadDate: Date if let firstTimer = timers.first { reloadDate = min(firstTimer.targetTime, now.addingTimeInterval(30 * 60)) } else { reloadDate = now.addingTimeInterval(30 * 60) } return Timeline(entries: entries, policy: .after(reloadDate)) } static let placeholderTimers: [TimerSnapshot] = [ TimerSnapshot( id: "p1", 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 ), TimerSnapshot( id: "p2", label: "Dentist", type: .alarm, urgency: .standard, state: .active, targetTime: Date().addingTimeInterval(7200), duration: 7200, startedAt: Date(), elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 2, firedWarnings: 0 ), TimerSnapshot( id: "p3", label: "Pick up laundry", type: .countdown, urgency: .gentle, state: .active, targetTime: Date().addingTimeInterval(10800), duration: 10800, startedAt: Date(), elapsedBeforePause: 0, snoozeCount: 0, category: nil, pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil, nextWarningTime: nil, totalWarnings: 1, firedWarnings: 0 ), ] } // MARK: - Intent struct TimerListIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource = "Timer List" static var description: IntentDescription = "Shows your next 3 upcoming timers" } // MARK: - Entry struct TimerListEntry: TimelineEntry { let date: Date let timers: [TimerSnapshot] } // MARK: - Widget View struct TimerListWidgetView: View { let entry: TimerListEntry var body: some View { if entry.timers.isEmpty { emptyContent } else { timerListContent } } private var timerListContent: some View { VStack(alignment: .leading, spacing: 0) { // Header HStack { Image(systemName: "clock.fill") .font(.system(size: 11)) .foregroundStyle(.secondary) Text("UPCOMING") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.secondary) .tracking(0.5) Spacer() Text("\(entry.timers.count) timer\(entry.timers.count == 1 ? "" : "s")") .font(.system(size: 10, weight: .medium)) .foregroundStyle(.tertiary) } .padding(.bottom, 6) // Timer rows ForEach(Array(entry.timers.enumerated()), id: \.element.id) { index, timer in if index > 0 { Divider() .padding(.vertical, 2) } timerRow(timer) } if entry.timers.count < 3 { Spacer() } } .padding() .containerBackground(for: .widget) { Color(.systemBackground) } } private func timerRow(_ timer: TimerSnapshot) -> some View { HStack(spacing: 8) { // Urgency bar RoundedRectangle(cornerRadius: 2) .fill(urgencyColor(timer.urgency)) .frame(width: 3, height: 32) VStack(alignment: .leading, spacing: 2) { Text(timer.label) .font(.system(size: 13, weight: .medium)) .foregroundStyle(.primary) .lineLimit(1) .privacySensitive() Text(formatTime(timer.targetTime)) .font(.system(size: 11)) .foregroundStyle(.secondary) } Spacer() // Countdown if timer.state == .paused { Text("Paused") .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundStyle(.secondary) } else { Text(timer.targetTime, style: .timer) .font(.system(size: 14, weight: .semibold, design: .monospaced)) .foregroundStyle(urgencyColor(timer.urgency)) .multilineTextAlignment(.trailing) .frame(minWidth: 50, alignment: .trailing) .privacySensitive() } } .widgetURL(URL(string: "chronomind://timer/\(timer.id)")) } private var emptyContent: some View { VStack(spacing: 8) { Image(systemName: "clock") .font(.system(size: 32)) .foregroundStyle(.secondary) Text("No Active Timers") .font(.system(size: 14, weight: .medium)) .foregroundStyle(.secondary) Text("Tap to create one") .font(.system(size: 12)) .foregroundStyle(.tertiary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .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 TimerListWidget: Widget { let kind: String = "TimerListWidget" var body: some WidgetConfiguration { AppIntentConfiguration( kind: kind, intent: TimerListIntent.self, provider: TimerListProvider() ) { entry in TimerListWidgetView(entry: entry) } .configurationDisplayName("Timer List") .description("Shows your next 3 upcoming timers in a mini-timeline") .supportedFamilies([.systemMedium]) } }