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>
This commit is contained in:
153
Conjuga/ConjugaWidget/WeekProgressWidget.swift
Normal file
153
Conjuga/ConjugaWidget/WeekProgressWidget.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
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<WeekProgressEntry>) -> 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)
|
||||
}
|
||||
Reference in New Issue
Block a user