import SwiftUI import SharedModels import SwiftData struct VocabReviewView: View { @Environment(\.cloudModelContextProvider) private var cloudModelContextProvider @Environment(\.modelContext) private var localContext @Environment(\.dismiss) private var dismiss @State private var dueCards: [CourseReviewCard] = [] @State private var currentIndex = 0 @State private var isRevealed = false @State private var sessionCorrect = 0 @State private var sessionTotal = 0 @State private var isFinished = false private var cloudContext: ModelContext { cloudModelContextProvider() } var body: some View { VStack(spacing: 20) { if isFinished || dueCards.isEmpty { finishedView } else if let card = dueCards[safe: currentIndex] { cardView(card) } } .padding() .adaptiveContainer(maxWidth: 600) .navigationTitle("Vocab Review") .navigationBarTitleDisplayMode(.inline) .onAppear(perform: loadDueCards) } // MARK: - Card View @ViewBuilder private func cardView(_ card: CourseReviewCard) -> some View { VStack(spacing: 24) { // Progress Text("\(currentIndex + 1) of \(dueCards.count)") .font(.caption.weight(.medium)) .foregroundStyle(.secondary) ProgressView(value: Double(currentIndex), total: Double(dueCards.count)) .tint(.teal) Spacer() // Front (Spanish) Text(card.front) .font(.largeTitle.bold()) .multilineTextAlignment(.center) if isRevealed { // Back (English) Text(card.back) .font(.title2) .foregroundStyle(.secondary) .transition(.opacity.combined(with: .move(edge: .bottom))) Spacer() // Rating buttons HStack(spacing: 12) { ratingButton("Again", color: .red, quality: .again) ratingButton("Hard", color: .orange, quality: .hard) ratingButton("Good", color: .green, quality: .good) ratingButton("Easy", color: .blue, quality: .easy) } } else { Spacer() Button { withAnimation { isRevealed = true } } label: { Text("Show Answer") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) .tint(.teal) } } } // MARK: - Finished View private var finishedView: some View { VStack(spacing: 20) { Spacer() Image(systemName: dueCards.isEmpty ? "checkmark.seal.fill" : "star.fill") .font(.system(size: 60)) .foregroundStyle(dueCards.isEmpty ? .green : .yellow) if dueCards.isEmpty { Text("All caught up!") .font(.title2.bold()) Text("No vocabulary cards are due for review.") .font(.subheadline) .foregroundStyle(.secondary) } else { Text("\(sessionCorrect) / \(sessionTotal)") .font(.system(size: 48, weight: .bold).monospacedDigit()) Text("Review complete!") .font(.title3) .foregroundStyle(.secondary) } Spacer() } } // MARK: - Helpers private func ratingButton(_ label: String, color: Color, quality: ReviewQuality) -> some View { Button { rate(quality: quality) } label: { Text(label) .font(.subheadline.weight(.semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 10) } .buttonStyle(.bordered) .tint(color) } private func rate(quality: ReviewQuality) { guard let card = dueCards[safe: currentIndex] else { return } let store = CourseReviewStore(context: cloudContext) let result = SRSEngine.review( quality: quality, currentEase: card.easeFactor, currentInterval: card.interval, currentReps: card.repetitions ) card.easeFactor = result.easeFactor card.interval = result.interval card.repetitions = result.repetitions card.dueDate = SRSEngine.nextDueDate(interval: result.interval) card.lastReviewDate = Date() try? cloudContext.save() sessionTotal += 1 if quality != .again { sessionCorrect += 1 } isRevealed = false if currentIndex + 1 < dueCards.count { currentIndex += 1 } else { withAnimation { isFinished = true } } } private func loadDueCards() { let now = Date() let descriptor = FetchDescriptor( predicate: #Predicate { $0.dueDate <= now }, sortBy: [SortDescriptor(\CourseReviewCard.dueDate)] ) dueCards = (try? cloudContext.fetch(descriptor)) ?? [] } static func dueCount(context: ModelContext) -> Int { let now = Date() let descriptor = FetchDescriptor( predicate: #Predicate { $0.dueDate <= now } ) return (try? context.fetchCount(descriptor)) ?? 0 } } private extension Collection { subscript(safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } }