import ManaCore import SwiftData import SwiftUI #if canImport(UIKit) import UIKit #endif /// Vollbild-Study-View. Wird per Navigation aus DeckListView geöffnet. struct StudySessionView: View { let deckId: String let deckName: String @Environment(AuthClient.self) private var auth @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss @State private var session: StudySession? var body: some View { ZStack { CardsTheme.background.ignoresSafeArea() content } .navigationTitle(deckName) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .topBarTrailing) { if let session, case .studying = session.phase { Text("\(session.remaining)") .font(.subheadline.weight(.semibold)) .foregroundStyle(CardsTheme.mutedForeground) .accessibilityLabel("\(session.remaining) Karten übrig") } } } .task { if session == nil { let s = StudySession(deckId: deckId, deckName: deckName, auth: auth, context: context) session = s await s.start() } } } @ViewBuilder private var content: some View { if let session { switch session.phase { case .loading: ProgressView("Karten werden geladen …") .tint(CardsTheme.primary) case .studying: studyingView(session: session) case .finished: finishedView(session: session) case let .failed(message): failedView(message: message, session: session) } } else { ProgressView() .tint(CardsTheme.primary) } } private func studyingView(session: StudySession) -> some View { VStack(spacing: 16) { if let due = session.current { cardSurface(due: due, isFlipped: session.isFlipped) .onTapGesture { flipHaptic() session.flip() } if session.isFlipped { RatingBar { rating in Task { await session.grade(rating) } } .transition(.move(edge: .bottom).combined(with: .opacity)) } else { Button { flipHaptic() session.flip() } label: { Text("Antwort anzeigen") .font(.subheadline.weight(.semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10)) .foregroundStyle(CardsTheme.primaryForeground) } .buttonStyle(.plain) .padding(.horizontal, 16) } } } .padding(.bottom, 20) .animation(.easeInOut(duration: 0.2), value: session.isFlipped) .animation(.easeInOut(duration: 0.2), value: session.currentIndex) } private func cardSurface(due: DueReview, isFlipped: Bool) -> some View { RoundedRectangle(cornerRadius: 16) .fill(CardsTheme.surface) .overlay( CardRenderer( card: due.card, subIndex: due.review.subIndex, isFlipped: isFlipped ) ) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(CardsTheme.border, lineWidth: 1) ) .padding(.horizontal, 16) .padding(.top, 12) } private func finishedView(session: StudySession) -> some View { VStack(spacing: 16) { Image(systemName: "checkmark.seal.fill") .font(.system(size: 64)) .foregroundStyle(CardsTheme.success) Text(session.totalGraded == 0 ? "Keine Karten fällig" : "Fertig!") .font(.title.bold()) .foregroundStyle(CardsTheme.foreground) if session.totalGraded > 0 { Text("\(session.totalGraded) Karten gelernt") .font(.subheadline) .foregroundStyle(CardsTheme.mutedForeground) } Button("Zurück") { dismiss() } .padding(.top, 24) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private func failedView(message: String, session: StudySession) -> some View { VStack(spacing: 16) { Image(systemName: "wifi.exclamationmark") .font(.system(size: 48)) .foregroundStyle(CardsTheme.error) Text("Karten konnten nicht geladen werden") .font(.headline) .foregroundStyle(CardsTheme.foreground) Text(message) .font(.caption) .multilineTextAlignment(.center) .foregroundStyle(CardsTheme.mutedForeground) .padding(.horizontal, 32) Button("Erneut versuchen") { Task { await session.start() } } .padding(.top, 16) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private func flipHaptic() { #if canImport(UIKit) UIImpactFeedbackGenerator(style: .soft).impactOccurred() #endif } }