Files
Spanish/Conjuga/ConjugaWidget/DailyProgressWidget.swift
Trey t 4b467ec136 Initial commit: Conjuga Spanish conjugation app
Includes SwiftData dual-store architecture (local reference + CloudKit user data),
JSON-based data seeding, 20 tense guides, 20 grammar notes, SRS review system,
course vocabulary, and widget support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:58:33 -05:00

103 lines
3.2 KiB
Swift

import WidgetKit
import SwiftUI
import SharedModels
struct DailyProgressEntry: TimelineEntry {
let date: Date
let data: WidgetData
}
struct DailyProgressProvider: TimelineProvider {
func placeholder(in context: Context) -> DailyProgressEntry {
DailyProgressEntry(date: Date(), data: .placeholder)
}
func getSnapshot(in context: Context, completion: @escaping (DailyProgressEntry) -> Void) {
if context.isPreview {
completion(DailyProgressEntry(date: Date(), data: .placeholder))
return
}
completion(DailyProgressEntry(date: Date(), data: WidgetDataReader.read()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<DailyProgressEntry>) -> Void) {
let data = WidgetDataReader.read()
var entries: [DailyProgressEntry] = []
let now = Date()
for offset in 0..<8 {
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
entries.append(DailyProgressEntry(date: date, data: data))
}
completion(Timeline(entries: entries, policy: .atEnd))
}
}
struct DailyProgressWidget: Widget {
let kind = "DailyProgressWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: DailyProgressProvider()) { entry in
DailyProgressWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Daily Progress")
.description("Track your daily practice goal and streak.")
.supportedFamilies([.systemSmall])
}
}
struct DailyProgressWidgetView: View {
let entry: DailyProgressEntry
var body: some View {
VStack(spacing: 8) {
// Progress ring
ZStack {
Circle()
.stroke(.quaternary, lineWidth: 6)
Circle()
.trim(from: 0, to: entry.data.progressPercent)
.stroke(.orange, style: StrokeStyle(lineWidth: 6, lineCap: .round))
.rotationEffect(.degrees(-90))
VStack(spacing: 0) {
Text("\(entry.data.todayCount)")
.font(.title2.bold().monospacedDigit())
Text("/\(entry.data.dailyGoal)")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.frame(width: 70, height: 70)
// Streak
if entry.data.currentStreak > 0 {
HStack(spacing: 3) {
Image(systemName: "flame.fill")
.foregroundStyle(.orange)
.font(.caption2)
Text("\(entry.data.currentStreak)d")
.font(.caption2.bold())
}
}
// Due cards
if entry.data.dueCardCount > 0 {
Text("\(entry.data.dueCardCount) due")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
#Preview(as: .systemSmall) {
DailyProgressWidget()
} timeline: {
DailyProgressEntry(date: Date(), data: .placeholder)
}