import WidgetKit import SwiftUI import SwiftData import SharedModels struct WordOfDayEntry: TimelineEntry { let date: Date let word: WordOfDay? } struct WordOfDayProvider: TimelineProvider { func placeholder(in context: Context) -> WordOfDayEntry { WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1)) } func getSnapshot(in context: Context, completion: @escaping (WordOfDayEntry) -> Void) { if context.isPreview { completion(WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1))) return } completion(WordOfDayEntry(date: Date(), word: fetchWordOfDay(for: Date()))) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entry = WordOfDayEntry(date: Date(), word: fetchWordOfDay(for: Date())) let tomorrow = Calendar.current.startOfDay( for: Calendar.current.date(byAdding: .day, value: 1, to: Date())! ) completion(Timeline(entries: [entry], policy: .after(tomorrow))) } private func fetchWordOfDay(for date: Date) -> WordOfDay? { guard let localURL = SharedStore.localStoreURL() else { return nil } // MUST declare all 6 local entities to match the main app's schema. // Declaring a subset would cause SwiftData to destructively migrate the store // on open, dropping the entities not listed here. let config = ModelConfiguration( "local", schema: Schema([ Verb.self, VerbForm.self, IrregularSpan.self, TenseGuide.self, CourseDeck.self, VocabCard.self, ]), url: localURL, cloudKitDatabase: .none ) guard let container = try? ModelContainer( for: Verb.self, VerbForm.self, IrregularSpan.self, TenseGuide.self, CourseDeck.self, VocabCard.self, configurations: config ) else { return nil } let context = ModelContext(container) let wordOffset = UserDefaults(suiteName: "group.com.conjuga.app")?.integer(forKey: "wordOffset") ?? 0 guard let card = CourseCardStore.fetchWordOfDayCard(for: date, wordOffset: wordOffset, context: context) else { return nil } let deckId = card.deckId let descriptor = FetchDescriptor( predicate: #Predicate { $0.id == deckId } ) let deck = (try? context.fetch(descriptor))?.first let week = deck?.weekNumber ?? 1 // If the deck is reversed (English on front), swap so spanish is always Spanish. let spanish: String let english: String if deck?.isReversed == true { spanish = card.back english = card.front } else { spanish = card.front english = card.back } return WordOfDay(spanish: spanish, english: english, weekNumber: week) } } struct WordOfDayWidget: Widget { let kind = "WordOfDayWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: WordOfDayProvider()) { entry in WordOfDayWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Word of the Day") .description("Learn a new Spanish word every day.") .supportedFamilies([.systemSmall, .systemMedium]) } } struct WordOfDayWidgetView: View { @Environment(\.widgetFamily) var family let entry: WordOfDayEntry var body: some View { if let word = entry.word { switch family { case .systemSmall: smallView(word: word) case .systemMedium: mediumView(word: word) default: smallView(word: word) } } else { VStack { Image(systemName: "textformat.abc") .font(.title) .foregroundStyle(.secondary) Text("Open Conjuga to start") .font(.caption) .foregroundStyle(.secondary) } } } private func smallView(word: WordOfDay) -> some View { VStack(spacing: 8) { Text("Word of the Day") .font(.caption2) .foregroundStyle(.secondary) .textCase(.uppercase) Text(word.spanish) .font(.title2.bold()) .minimumScaleFactor(0.6) .lineLimit(1) Text(word.english) .font(.subheadline) .foregroundStyle(.secondary) .minimumScaleFactor(0.6) .lineLimit(2) .multilineTextAlignment(.center) Text("Week \(word.weekNumber)") .font(.caption2) .foregroundStyle(.orange) } } private func mediumView(word: WordOfDay) -> some View { HStack(spacing: 16) { VStack(spacing: 4) { Text("WORD OF THE DAY") .font(.caption2.weight(.semibold)) .foregroundStyle(.orange) Text(word.spanish) .font(.largeTitle.bold()) .minimumScaleFactor(0.5) .lineLimit(1) } .frame(maxWidth: .infinity) Divider() VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 2) { Text("English") .font(.caption2) .foregroundStyle(.secondary) Text(word.english) .font(.headline) .lineLimit(2) } HStack { Image(systemName: "calendar") .font(.caption2) Text("Week \(word.weekNumber)") .font(.caption2) } .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 4) } } #Preview(as: .systemSmall) { WordOfDayWidget() } timeline: { WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1)) } #Preview(as: .systemMedium) { WordOfDayWidget() } timeline: { WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1)) }