import WidgetKit import SwiftUI import SharedModels struct WeekProgressEntry: TimelineEntry { let date: Date let data: WidgetData } struct WeekProgressProvider: TimelineProvider { func placeholder(in context: Context) -> WeekProgressEntry { WeekProgressEntry(date: Date(), data: .placeholder) } func getSnapshot(in context: Context, completion: @escaping (WeekProgressEntry) -> Void) { if context.isPreview { completion(WeekProgressEntry(date: Date(), data: .placeholder)) return } completion(WeekProgressEntry(date: Date(), data: WidgetDataReader.read())) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let data = WidgetDataReader.read() var entries: [WeekProgressEntry] = [] let now = Date() for offset in 0..<8 { let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)! entries.append(WeekProgressEntry(date: date, data: data)) } completion(Timeline(entries: entries, policy: .atEnd)) } } struct WeekProgressWidget: Widget { let kind = "WeekProgressWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: WeekProgressProvider()) { entry in WeekProgressWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Week Progress") .description("See your current week and latest test score.") .supportedFamilies([.systemMedium]) } } struct WeekProgressWidgetView: View { let entry: WeekProgressEntry var body: some View { HStack(spacing: 16) { // Left: Week + Streak VStack(spacing: 10) { VStack(spacing: 2) { Text("WEEK") .font(.caption2.weight(.semibold)) .foregroundStyle(.secondary) Text("\(entry.data.currentWeek)") .font(.system(size: 40, weight: .bold, design: .rounded)) } if entry.data.currentStreak > 0 { HStack(spacing: 3) { Image(systemName: "flame.fill") .foregroundStyle(.orange) Text("\(entry.data.currentStreak)") .fontWeight(.bold) } .font(.caption) } } .frame(maxWidth: .infinity) Divider() // Right: Stats VStack(alignment: .leading, spacing: 10) { // Latest test score if let score = entry.data.latestTestScore, let week = entry.data.latestTestWeek { HStack(spacing: 8) { Image(systemName: scoreIcon(score)) .foregroundStyle(scoreColor(score)) .font(.title3) VStack(alignment: .leading, spacing: 1) { Text("Week \(week) Test") .font(.caption2) .foregroundStyle(.secondary) Text("\(score)%") .font(.headline.bold()) .foregroundStyle(scoreColor(score)) } } } // Today's progress HStack(spacing: 8) { Image(systemName: "checkmark.circle") .foregroundStyle(.blue) .font(.title3) VStack(alignment: .leading, spacing: 1) { Text("Today") .font(.caption2) .foregroundStyle(.secondary) Text("\(entry.data.todayCount) / \(entry.data.dailyGoal)") .font(.subheadline.weight(.medium)) } } // Due cards if entry.data.dueCardCount > 0 { HStack(spacing: 8) { Image(systemName: "clock.badge.exclamationmark") .foregroundStyle(.orange) .font(.title3) VStack(alignment: .leading, spacing: 1) { Text("Due") .font(.caption2) .foregroundStyle(.secondary) Text("\(entry.data.dueCardCount) cards") .font(.subheadline.weight(.medium)) } } } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 4) } private func scoreColor(_ score: Int) -> Color { if score >= 90 { return .green } if score >= 70 { return .orange } return .red } private func scoreIcon(_ score: Int) -> String { if score >= 90 { return "star.fill" } if score >= 70 { return "hand.thumbsup.fill" } return "arrow.clockwise" } } #Preview(as: .systemMedium) { WeekProgressWidget() } timeline: { WeekProgressEntry(date: Date(), data: .placeholder) }