import SwiftUI import SharedModels import SwiftData struct PracticeView: View { @Environment(\.modelContext) private var modelContext @Environment(\.cloudModelContextProvider) private var cloudModelContextProvider @State private var viewModel = PracticeViewModel() @State private var speechService = SpeechService() @State private var isPracticing = false @State private var userProgress: UserProgress? private var cloudModelContext: ModelContext { cloudModelContextProvider() } var body: some View { NavigationStack { Group { if isPracticing { practiceSessionView } else { practiceHomeView } } .navigationTitle("Practice") .navigationBarTitleDisplayMode(.inline) .onAppear(perform: loadProgress) .onChange(of: isPracticing) { _, practicing in if !practicing { loadProgress() } } .toolbar { if isPracticing { ToolbarItem(placement: .cancellationAction) { Button("Done") { withAnimation { isPracticing = false } } } ToolbarItem(placement: .primaryAction) { sessionStatsLabel } } } } } // MARK: - Home View private var practiceHomeView: some View { ScrollView { VStack(spacing: 28) { // Daily progress if let progress = userProgress { VStack(spacing: 8) { DailyProgressRing( current: progress.todayCount, goal: progress.dailyGoal ) .frame(width: 160, height: 160) Text("\(progress.todayCount) / \(progress.dailyGoal)") .font(.title3.weight(.semibold)) .foregroundStyle(.secondary) if progress.currentStreak > 0 { Label("\(progress.currentStreak) day streak", systemImage: "flame.fill") .font(.subheadline.weight(.medium)) .foregroundStyle(.orange) } } .padding(.top, 8) } // Mode selection VStack(spacing: 12) { Text("Choose a Mode") .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) ForEach(PracticeMode.allCases) { mode in ModeButton(mode: mode) { viewModel.practiceMode = mode viewModel.focusMode = .none viewModel.sessionCorrect = 0 viewModel.sessionTotal = 0 viewModel.loadNextCard( localContext: modelContext, cloudContext: cloudModelContext ) withAnimation { isPracticing = true } } } } .padding(.horizontal) // Quick Actions VStack(spacing: 12) { Text("Quick Actions") .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) // Weak verbs focus Button { viewModel.practiceMode = .flashcard viewModel.focusMode = .weakVerbs viewModel.sessionCorrect = 0 viewModel.sessionTotal = 0 viewModel.loadNextCard( localContext: modelContext, cloudContext: cloudModelContext ) withAnimation { isPracticing = true } } label: { HStack(spacing: 14) { Image(systemName: "exclamationmark.triangle") .font(.title3) .foregroundStyle(.red) .frame(width: 32) VStack(alignment: .leading, spacing: 2) { Text("Weak Verbs") .font(.subheadline.weight(.semibold)) Text("Focus on verbs you struggle with") .font(.caption) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.tertiary) } .padding(.horizontal, 16) .padding(.vertical, 12) } .tint(.primary) .glassEffect(in: RoundedRectangle(cornerRadius: 14)) // Irregularity drills Menu { Button("Spelling Changes (c→qu, z→c, ...)") { startIrregularityDrill(.spelling) } Button("Stem Changes (o→ue, e→ie, ...)") { startIrregularityDrill(.stemChange) } Button("Unique Irregulars (ser, ir, ...)") { startIrregularityDrill(.uniqueIrregular) } } label: { HStack(spacing: 14) { Image(systemName: "wand.and.stars") .font(.title3) .foregroundStyle(.purple) .frame(width: 32) VStack(alignment: .leading, spacing: 2) { Text("Irregularity Drills") .font(.subheadline.weight(.semibold)) Text("Practice by irregularity type") .font(.caption) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.tertiary) } .padding(.horizontal, 16) .padding(.vertical, 12) } .tint(.primary) .glassEffect(in: RoundedRectangle(cornerRadius: 14)) } .padding(.horizontal) // Session stats summary if viewModel.sessionTotal > 0 && !isPracticing { VStack(spacing: 8) { Text("Last Session") .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 20) { StatItem(label: "Reviewed", value: "\(viewModel.sessionTotal)") StatItem(label: "Correct", value: "\(viewModel.sessionCorrect)") StatItem( label: "Accuracy", value: "\(Int(viewModel.sessionAccuracy * 100))%" ) } .padding() .frame(maxWidth: .infinity) .glassEffect(in: RoundedRectangle(cornerRadius: 16)) } .padding(.horizontal) } } .padding(.vertical) .adaptiveContainer() } } // MARK: - Practice Session View @ViewBuilder private var practiceSessionView: some View { if !viewModel.hasCards { ContentUnavailableView( "No Cards Available", systemImage: "rectangle.on.rectangle.slash", description: Text("Add some verbs to your practice deck to get started.") ) } else { switch viewModel.practiceMode { case .flashcard: FlashcardView(viewModel: viewModel, speechService: speechService) case .typing: TypingView(viewModel: viewModel, speechService: speechService) case .multipleChoice: MultipleChoiceView(viewModel: viewModel, speechService: speechService) case .fullTable: FullTableView(speechService: speechService) case .handwriting: HandwritingView(viewModel: viewModel, speechService: speechService) case .sentenceBuilder: SentenceBuilderView() } } } // MARK: - Session Stats Label private var sessionStatsLabel: some View { HStack(spacing: 4) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("\(viewModel.sessionCorrect)/\(viewModel.sessionTotal)") .font(.subheadline.weight(.medium)) .contentTransition(.numericText()) } } } // MARK: - Mode Button private struct ModeButton: View { let mode: PracticeMode let action: () -> Void private var description: String { switch mode { case .flashcard: "Reveal answers at your own pace" case .typing: "Type the conjugation from memory" case .multipleChoice: "Pick the correct form from options" case .fullTable: "Conjugate all persons for a verb + tense" case .handwriting: "Write the answer with Apple Pencil" case .sentenceBuilder: "Arrange Spanish words in correct order" } } var body: some View { Button(action: action) { HStack(spacing: 16) { Image(systemName: mode.icon) .font(.title2) .frame(width: 40) VStack(alignment: .leading, spacing: 2) { Text(mode.label) .font(.headline) Text(description) .font(.caption) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.subheadline) .foregroundStyle(.tertiary) } .padding(.horizontal, 16) .padding(.vertical, 14) } .tint(.primary) .glassEffect(in: RoundedRectangle(cornerRadius: 16)) } } // MARK: - Stat Item private struct StatItem: View { let label: String let value: String var body: some View { VStack(spacing: 4) { Text(value) .font(.title2.bold().monospacedDigit()) .contentTransition(.numericText()) Text(label) .font(.caption) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) } } extension PracticeView { fileprivate func startIrregularityDrill(_ filter: IrregularityFilter) { viewModel.practiceMode = .flashcard viewModel.focusMode = .irregularity(filter) viewModel.sessionCorrect = 0 viewModel.sessionTotal = 0 viewModel.loadNextCard(localContext: modelContext, cloudContext: cloudModelContext) withAnimation { isPracticing = true } } private func loadProgress() { let progress = ReviewStore.fetchOrCreateUserProgress(context: cloudModelContext) userProgress = progress try? cloudModelContext.save() } } #Preview { PracticeView() .modelContainer(for: [UserProgress.self, ReviewCard.self, Verb.self], inMemory: true) }