aab64116b3
- Settings: split the single session-size picker into separate Verbs / Nouns / Adjectives pickers. Nouns and adjectives previously shared one hidden limit; they now use nounSessionCardLimit / adjectiveSessionCardLimit. - LexemePool.sessionCardLimit is now per part-of-speech. - Multiple-choice views (verb/noun/adjective) gained a kind param so Review Learned can run as multiple choice, not just flashcards. The cram pass drives the in-session queue only and leaves the long-term SRS schedule untouched. - PracticeView: each section now offers Review Learned — Flashcards and Review Learned — Multiple Choice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
200 lines
8.2 KiB
Swift
200 lines
8.2 KiB
Swift
import SwiftUI
|
|
import SharedModels
|
|
import SwiftData
|
|
|
|
struct SettingsView: View {
|
|
@Environment(\.cloudModelContextProvider) private var cloudModelContextProvider
|
|
@State private var progress: UserProgress?
|
|
|
|
@State private var dailyGoal: Double = 50
|
|
@State private var showVosotros: Bool = true
|
|
@State private var autoFillStem: Bool = false
|
|
|
|
/// Cards per study session, per word type. 999 = "All" (no cap).
|
|
@AppStorage("vocabSessionCardLimit") private var vocabSessionCardLimit: Int = 20
|
|
@AppStorage("nounSessionCardLimit") private var nounSessionCardLimit: Int = 20
|
|
@AppStorage("adjectiveSessionCardLimit") private var adjectiveSessionCardLimit: Int = 20
|
|
private let vocabSessionSizes: [Int] = [10, 15, 20, 25, 30, 50, 999]
|
|
|
|
private let levels = VerbLevel.allCases
|
|
private let irregularCategories: [IrregularSpan.SpanCategory] = [
|
|
.spelling, .stemChange, .uniqueIrregular
|
|
]
|
|
private var cloudModelContext: ModelContext { cloudModelContextProvider() }
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("Practice") {
|
|
VStack(alignment: .leading) {
|
|
Text("Daily Goal: \(Int(dailyGoal)) cards")
|
|
Slider(value: $dailyGoal, in: 10...200, step: 10)
|
|
}
|
|
.onChange(of: dailyGoal) { _, newValue in
|
|
progress?.dailyGoal = Int(newValue)
|
|
saveProgress()
|
|
}
|
|
|
|
Toggle("Include vosotros", isOn: $showVosotros)
|
|
.onChange(of: showVosotros) { _, newValue in
|
|
progress?.showVosotros = newValue
|
|
saveProgress()
|
|
}
|
|
|
|
Toggle("Auto-fill verb stem (Full Table)", isOn: $autoFillStem)
|
|
.onChange(of: autoFillStem) { _, newValue in
|
|
progress?.autoFillStem = newValue
|
|
saveProgress()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
sessionSizePicker("Verbs per session", selection: $vocabSessionCardLimit)
|
|
sessionSizePicker("Nouns per session", selection: $nounSessionCardLimit)
|
|
sessionSizePicker("Adjectives per session", selection: $adjectiveSessionCardLimit)
|
|
} header: {
|
|
Text("Cards Per Session")
|
|
} footer: {
|
|
Text("How many cards each flashcard or multiple-choice session draws, per word type. Overdue cards are pulled first, then new ones.")
|
|
}
|
|
|
|
Section {
|
|
ForEach(levels, id: \.self) { level in
|
|
Toggle(level.displayName, isOn: Binding(
|
|
get: {
|
|
progress?.selectedVerbLevels.contains(level) ?? false
|
|
},
|
|
set: { enabled in
|
|
guard let progress else { return }
|
|
progress.setLevelEnabled(level, enabled: enabled)
|
|
saveProgress()
|
|
}
|
|
))
|
|
}
|
|
} header: {
|
|
Text("Verb Levels")
|
|
} footer: {
|
|
Text("Practice pulls only from verbs whose level is enabled. Turn on multiple to mix.")
|
|
}
|
|
|
|
Section {
|
|
ForEach(LexemeLevel.allCases, id: \.self) { level in
|
|
Toggle(level.displayName, isOn: Binding(
|
|
get: {
|
|
progress?.selectedLexemeLevels.contains(level) ?? false
|
|
},
|
|
set: { enabled in
|
|
guard let progress else { return }
|
|
progress.setLexemeLevelEnabled(level, enabled: enabled)
|
|
saveProgress()
|
|
}
|
|
))
|
|
}
|
|
} header: {
|
|
Text("Vocabulary Levels")
|
|
} footer: {
|
|
Text("Noun and adjective flashcards pull only from the enabled CEFR levels. New first-time installs default to A1 + A2.")
|
|
}
|
|
|
|
Section {
|
|
ForEach(TenseInfo.all) { tense in
|
|
Toggle(tense.english, isOn: Binding(
|
|
get: {
|
|
progress?.enabledTenseIDs.contains(tense.id) ?? false
|
|
},
|
|
set: { enabled in
|
|
guard let progress else { return }
|
|
progress.setTenseEnabled(tense.id, enabled: enabled)
|
|
saveProgress()
|
|
}
|
|
))
|
|
}
|
|
} header: {
|
|
Text("Tenses")
|
|
}
|
|
|
|
Section {
|
|
ForEach(irregularCategories, id: \.self) { category in
|
|
Toggle(category.rawValue, isOn: Binding(
|
|
get: {
|
|
progress?.enabledIrregularCategories.contains(category) ?? false
|
|
},
|
|
set: { enabled in
|
|
guard let progress else { return }
|
|
progress.setIrregularCategoryEnabled(category, enabled: enabled)
|
|
saveProgress()
|
|
}
|
|
))
|
|
}
|
|
} header: {
|
|
Text("Irregular Types")
|
|
} footer: {
|
|
Text("Leave all off to include regular and irregular verbs. Enable any to restrict practice to those irregularity types.")
|
|
}
|
|
|
|
Section {
|
|
Toggle("Reflexive verbs only", isOn: Binding(
|
|
get: { progress?.showReflexiveVerbsOnly ?? false },
|
|
set: { enabled in
|
|
progress?.showReflexiveVerbsOnly = enabled
|
|
saveProgress()
|
|
}
|
|
))
|
|
} header: {
|
|
Text("Reflexive")
|
|
} footer: {
|
|
Text("When on, practice pulls only from the curated list of common reflexive verbs.")
|
|
}
|
|
|
|
Section("Stats") {
|
|
if let progress {
|
|
LabeledContent("Total Reviewed", value: "\(progress.totalReviewed)")
|
|
LabeledContent("Current Streak", value: "\(progress.currentStreak) days")
|
|
LabeledContent("Longest Streak", value: "\(progress.longestStreak) days")
|
|
}
|
|
}
|
|
|
|
Section("Reference") {
|
|
NavigationLink("How Features Work") {
|
|
FeatureReferenceView()
|
|
}
|
|
NavigationLink("Downloaded Videos") {
|
|
DownloadedVideosView()
|
|
}
|
|
}
|
|
|
|
Section("About") {
|
|
LabeledContent("Version", value: "1.0.0")
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
.onAppear(perform: loadProgress)
|
|
}
|
|
}
|
|
|
|
private func sessionSizePicker(_ title: String, selection: Binding<Int>) -> some View {
|
|
Picker(title, selection: selection) {
|
|
ForEach(vocabSessionSizes, id: \.self) { size in
|
|
Text(size == 999 ? "All" : "\(size)").tag(size)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadProgress() {
|
|
let resolved = ReviewStore.fetchOrCreateUserProgress(context: cloudModelContext)
|
|
progress = resolved
|
|
dailyGoal = Double(resolved.dailyGoal)
|
|
showVosotros = resolved.showVosotros
|
|
autoFillStem = resolved.autoFillStem
|
|
}
|
|
|
|
private func saveProgress() {
|
|
try? cloudModelContext.save()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SettingsView()
|
|
.modelContainer(for: UserProgress.self, inMemory: true)
|
|
}
|