cards-native/Sources/Features/Study/RatingBar.swift
Till JS 3b861af3fb v0.3.0 — Phase β-2 Study-Loop
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>
2026-05-13 00:16:11 +02:00

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
}
}