Voller Lern-Flow mit Web-Parität: fällige Karten via /reviews/due laden, flip + rate (4 Buttons + Haptic), Grades via Offline-Queue ans Server-FSRS schicken. - Card/Review/DueReview DTOs mit snake_case + camelCase-deckId- Sonderfall im embedded card-Subobjekt - CardType-Enum (alle 7 Typen), Rating-Enum mit deutschen Labels - Cloze-Helper 1:1-Port aus cards-domain (extractClusterIds, subIndexCount, clusterId, renderPrompt/Answer, hint) - CardsAPI.dueReviews(deckId:) + gradeReview(cardId,subIndex,rating,reviewedAt) - PendingGrade SwiftData-Model + GradeQueue (FIFO-Drain, originaler Timestamp bleibt, bei Netzfehler in Queue, Retry beim nächsten Drain) - StudySession @Observable State-Machine - CardRenderer für basic, basic-reverse, cloze; Placeholder für image-occlusion/audio-front/typing/multiple-choice (β-3/β-4) - RatingBar mit UIImpactFeedbackGenerator (medium/heavy) - StudySessionView per NavigationLink aus DeckListView - 9 neue Tests (Cloze: 8, Review-Decoding: 3), insgesamt 17 grün Server-authoritative FSRS bleibt — kein ts-fsrs-Port. Endurance-Test auf realem Gerät steht aus (siehe PLAN.md). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
Swift
63 lines
1.9 KiB
Swift
import SwiftUI
|
|
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#endif
|
|
|
|
/// Vier Rating-Buttons unten am Bildschirm. Tap → onRate(rating)
|
|
/// plus Haptic-Feedback.
|
|
struct RatingBar: View {
|
|
let onRate: (Rating) -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
ForEach(Rating.allCases, id: \.self) { rating in
|
|
Button {
|
|
triggerHaptic(for: rating)
|
|
onRate(rating)
|
|
} label: {
|
|
VStack(spacing: 2) {
|
|
Text(rating.label)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(rating.shortcut)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 14)
|
|
.background(background(for: rating), in: RoundedRectangle(cornerRadius: 10))
|
|
.foregroundStyle(foreground(for: rating))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
}
|
|
|
|
private func background(for rating: Rating) -> Color {
|
|
switch rating {
|
|
case .again: CardsTheme.error.opacity(0.12)
|
|
case .hard: CardsTheme.warning.opacity(0.12)
|
|
case .good: CardsTheme.primary.opacity(0.12)
|
|
case .easy: CardsTheme.success.opacity(0.12)
|
|
}
|
|
}
|
|
|
|
private func foreground(for rating: Rating) -> Color {
|
|
switch rating {
|
|
case .again: CardsTheme.error
|
|
case .hard: CardsTheme.warning
|
|
case .good: CardsTheme.primary
|
|
case .easy: CardsTheme.success
|
|
}
|
|
}
|
|
|
|
private func triggerHaptic(for rating: Rating) {
|
|
#if canImport(UIKit)
|
|
let generator = UIImpactFeedbackGenerator(
|
|
style: rating == .easy ? .heavy : .medium
|
|
)
|
|
generator.impactOccurred()
|
|
#endif
|
|
}
|
|
}
|