diff --git a/Conjuga/Conjuga/Views/Verbs/VerbListView.swift b/Conjuga/Conjuga/Views/Verbs/VerbListView.swift index ebcb7c8..8c5d4bd 100644 --- a/Conjuga/Conjuga/Views/Verbs/VerbListView.swift +++ b/Conjuga/Conjuga/Views/Verbs/VerbListView.swift @@ -22,19 +22,38 @@ enum IrregularityCategory: String, CaseIterable, Identifiable { struct VerbListView: View { @Environment(\.modelContext) private var modelContext + @Environment(\.cloudModelContextProvider) private var cloudModelContextProvider @Environment(ReflexiveVerbStore.self) private var reflexiveStore @State private var verbs: [Verb] = [] @State private var irregularityByVerbId: [Int: Set] = [:] @State private var searchText = "" - @State private var selectedLevel: String? + @State private var progress: UserProgress? @State private var selectedIrregularity: IrregularityCategory? @State private var reflexiveOnly: Bool = false @State private var selectedVerb: Verb? + private var cloudContext: ModelContext { cloudModelContextProvider() } + + /// Levels currently enabled in `UserProgress` — the same set that drives + /// what Practice picks from. The Verbs tab reads and writes the same + /// state so changes here propagate to Practice and vice versa. + private var selectedLevels: Set { + progress?.selectedVerbLevels ?? [] + } + + /// True when the user has every available level enabled (or none, which + /// we treat as "no filter applied" on the Verbs list specifically). + private var allLevelsActive: Bool { + selectedLevels.isEmpty || selectedLevels.count == VerbLevel.allCases.count + } + private var filteredVerbs: [Verb] { var result = verbs - if let level = selectedLevel { - result = result.filter { VerbLevelGroup.matches($0.level, selectedLevel: level) } + if !allLevelsActive { + let activeRaws = Set(selectedLevels.map(\.rawValue)) + result = result.filter { verb in + activeRaws.contains { VerbLevelGroup.matches(verb.level, selectedLevel: $0) } + } } if let category = selectedIrregularity { result = result.filter { verb in @@ -55,7 +74,7 @@ struct VerbListView: View { return result } - private let levels = ["basic", "elementary", "intermediate", "advanced", "expert"] + private let levels: [VerbLevel] = VerbLevel.allCases var body: some View { NavigationSplitView { @@ -76,15 +95,15 @@ struct VerbListView: View { Menu { Section("Level") { Button { - selectedLevel = nil + setAllLevels(enabled: true) } label: { - Label("All Levels", systemImage: selectedLevel == nil ? "checkmark" : "") + Label("All Levels", systemImage: allLevelsActive ? "checkmark" : "") } ForEach(levels, id: \.self) { level in Button { - selectedLevel = level + toggleLevel(level) } label: { - Label(level.capitalized, systemImage: selectedLevel == level ? "checkmark" : "") + Label(level.displayName, systemImage: selectedLevels.contains(level) ? "checkmark" : "") } } } @@ -116,8 +135,14 @@ struct VerbListView: View { } } } - .task { loadVerbs() } - .onAppear { loadVerbs() } + .task { + loadVerbs() + loadProgress() + } + .onAppear { + loadVerbs() + loadProgress() + } } detail: { if let verb = selectedVerb { VerbDetailView(verb: verb) @@ -128,15 +153,15 @@ struct VerbListView: View { } private var hasActiveFilter: Bool { - selectedLevel != nil || selectedIrregularity != nil || reflexiveOnly + !allLevelsActive || selectedIrregularity != nil || reflexiveOnly } @ViewBuilder private var activeFilterBar: some View { HStack(spacing: 8) { - if let level = selectedLevel { - filterChip(text: level.capitalized, systemImage: "graduationcap") { - selectedLevel = nil + if !allLevelsActive { + filterChip(text: levelChipLabel, systemImage: "graduationcap") { + setAllLevels(enabled: true) } } if let cat = selectedIrregularity { @@ -192,6 +217,40 @@ struct VerbListView: View { print("[VerbListView] loaded \(verbs.count) verbs, \(irregularityByVerbId.count) flagged irregular (container: \(ObjectIdentifier(container)))") } + // MARK: - Level filter (shared with Practice via UserProgress) + + private var levelChipLabel: String { + let names = selectedLevels + .sorted { $0.rawValue < $1.rawValue } + .map(\.displayName) + if names.isEmpty { return "No levels" } + if names.count == 1 { return names[0] } + return "\(names.count) levels" + } + + private func loadProgress() { + progress = ReviewStore.fetchOrCreateUserProgress(context: cloudContext) + } + + private func toggleLevel(_ level: VerbLevel) { + guard let progress else { return } + let enabled = !progress.selectedVerbLevels.contains(level) + progress.setLevelEnabled(level, enabled: enabled) + try? cloudContext.save() + } + + private func setAllLevels(enabled: Bool) { + guard let progress else { return } + if enabled { + progress.selectedVerbLevels = Set(VerbLevel.allCases) + } else { + // Practice treats an empty set as "no verbs", so guard against + // leaving the user with nothing — keep at least `basic`. + progress.selectedVerbLevels = [.basic] + } + try? cloudContext.save() + } + private func buildIrregularityIndex(context: ModelContext) -> [Int: Set] { let spans = (try? context.fetch(FetchDescriptor())) ?? [] var index: [Int: Set] = [:]