Widget word-of-day picks from master verb list filtered by user level
Previously the widget was picking from course VocabCards, which could land on any course week and was showing unrelated phrases instead of the verbs the user is actually studying. Now the widget uses a new VerbStore.fetchVerbOfDay helper that: - Expands the user's selectedLevel via VerbLevelGroup.dataLevels - Runs a FetchDescriptor<Verb> filtered by those levels, sorted by rank - Uses fetchCount + fetchOffset for a deterministic daily pick The main app mirrors UserProgress.selectedLevel into the shared app group UserDefaults (key "selectedVerbLevel") on every WidgetDataService update, so the widget process can read it without touching the cloud store. WordOfDay.weekNumber was replaced with a more flexible subtitle: String so widgets can display "Level: Basic" instead of course week numbers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ struct CombinedEntry: TimelineEntry {
|
||||
}
|
||||
|
||||
struct CombinedProvider: TimelineProvider {
|
||||
private static let previewWord = WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1)
|
||||
private static let previewWord = WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic")
|
||||
|
||||
func placeholder(in context: Context) -> CombinedEntry {
|
||||
CombinedEntry(date: Date(), word: Self.previewWord, data: .placeholder)
|
||||
@@ -60,29 +60,22 @@ struct CombinedProvider: TimelineProvider {
|
||||
) 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 defaults = UserDefaults(suiteName: "group.com.conjuga.app")
|
||||
let wordOffset = defaults?.integer(forKey: "wordOffset") ?? 0
|
||||
let selectedLevel = defaults?.string(forKey: "selectedVerbLevel") ?? "basic"
|
||||
|
||||
let deckId = card.deckId
|
||||
let deckDescriptor = FetchDescriptor<CourseDeck>(
|
||||
predicate: #Predicate<CourseDeck> { $0.id == deckId }
|
||||
guard let verb = VerbStore.fetchVerbOfDay(
|
||||
for: date,
|
||||
dayOffset: wordOffset,
|
||||
selectedLevel: selectedLevel,
|
||||
context: context
|
||||
) else { return nil }
|
||||
|
||||
return WordOfDay(
|
||||
spanish: verb.infinitive,
|
||||
english: verb.english,
|
||||
subtitle: "Level: \(selectedLevel.capitalized)"
|
||||
)
|
||||
let deck = (try? context.fetch(deckDescriptor))?.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +116,7 @@ struct CombinedWidgetView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Text("Week \(word.weekNumber)")
|
||||
Text(word.subtitle)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
|
||||
@@ -233,7 +226,7 @@ struct CombinedWidgetView: View {
|
||||
} timeline: {
|
||||
CombinedEntry(
|
||||
date: Date(),
|
||||
word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1),
|
||||
word: WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic"),
|
||||
data: .placeholder
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ struct WordOfDayEntry: TimelineEntry {
|
||||
|
||||
struct WordOfDayProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> WordOfDayEntry {
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1))
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic"))
|
||||
}
|
||||
|
||||
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)))
|
||||
completion(WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic")))
|
||||
return
|
||||
}
|
||||
completion(WordOfDayEntry(date: Date(), word: fetchWordOfDay(for: Date())))
|
||||
@@ -51,28 +51,22 @@ struct WordOfDayProvider: TimelineProvider {
|
||||
) 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<CourseDeck>(
|
||||
predicate: #Predicate<CourseDeck> { $0.id == deckId }
|
||||
)
|
||||
let deck = (try? context.fetch(descriptor))?.first
|
||||
let week = deck?.weekNumber ?? 1
|
||||
let defaults = UserDefaults(suiteName: "group.com.conjuga.app")
|
||||
let wordOffset = defaults?.integer(forKey: "wordOffset") ?? 0
|
||||
let selectedLevel = defaults?.string(forKey: "selectedVerbLevel") ?? "basic"
|
||||
|
||||
// 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)
|
||||
guard let verb = VerbStore.fetchVerbOfDay(
|
||||
for: date,
|
||||
dayOffset: wordOffset,
|
||||
selectedLevel: selectedLevel,
|
||||
context: context
|
||||
) else { return nil }
|
||||
|
||||
return WordOfDay(
|
||||
spanish: verb.infinitive,
|
||||
english: verb.english,
|
||||
subtitle: "Level: \(selectedLevel.capitalized)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +129,7 @@ struct WordOfDayWidgetView: View {
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Week \(word.weekNumber)")
|
||||
Text(word.subtitle)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
@@ -168,9 +162,9 @@ struct WordOfDayWidgetView: View {
|
||||
}
|
||||
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
Image(systemName: "tag")
|
||||
.font(.caption2)
|
||||
Text("Week \(word.weekNumber)")
|
||||
Text(word.subtitle)
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -184,11 +178,11 @@ struct WordOfDayWidgetView: View {
|
||||
#Preview(as: .systemSmall) {
|
||||
WordOfDayWidget()
|
||||
} timeline: {
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1))
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic"))
|
||||
}
|
||||
|
||||
#Preview(as: .systemMedium) {
|
||||
WordOfDayWidget()
|
||||
} timeline: {
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", weekNumber: 1))
|
||||
WordOfDayEntry(date: Date(), word: WordOfDay(spanish: "hablar", english: "to speak", subtitle: "Level: Basic"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user