import SwiftUI /// Rendert die Karten-Inhalte je nach `CardType`. Front-/Back-Seite /// werden über `isFlipped` gesteuert. /// /// β-2 deckt `basic`, `basic-reverse`, `cloze` ab. Restliche Typen /// zeigen einen Placeholder mit Hinweis auf die kommende Phase. struct CardRenderer: View { let card: ReviewCard let subIndex: Int let isFlipped: Bool var body: some View { Group { switch card.type { case .basic: basicView(front: "front", back: "back") case .basicReverse: // sub_index 0 = front→back, sub_index 1 = back→front if subIndex == 0 { basicView(front: "front", back: "back") } else { basicView(front: "back", back: "front") } case .cloze: clozeView case .imageOcclusion: imageOcclusionView case .audioFront: audioFrontView case .multipleChoice: MultipleChoiceCardView(card: card, isFlipped: isFlipped) case .typing: TypingCardView(card: card, isFlipped: isFlipped) } } .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) } private func basicView(front frontKey: String, back backKey: String) -> some View { VStack(spacing: 16) { text(card.fields[frontKey] ?? "") .font(.title2) .foregroundStyle(CardsTheme.foreground) if isFlipped { Divider().background(CardsTheme.border) text(card.fields[backKey] ?? "") .font(.title3) .foregroundStyle(CardsTheme.mutedForeground) } } } @ViewBuilder private var clozeView: some View { let raw = card.fields["text"] ?? "" let clusterId = Cloze.clusterId(for: raw, subIndex: subIndex) ?? 1 let rendered = isFlipped ? Cloze.renderAnswer(raw, activeClusterId: clusterId) : Cloze.renderPrompt(raw, activeClusterId: clusterId) VStack(spacing: 12) { text(rendered) .font(.title3) .foregroundStyle(CardsTheme.foreground) } } @ViewBuilder private var imageOcclusionView: some View { let imageRef = card.fields["image_ref"] ?? "" let maskJSON = card.fields["mask_regions"] ?? "[]" let regions = MaskRegions.parse(maskJSON) let activeRegion = regions.indices.contains(subIndex) ? regions[subIndex] : nil VStack(spacing: 12) { GeometryReader { geo in ZStack(alignment: .topLeading) { RemoteImage(mediaId: imageRef, contentMode: .fit) .frame(width: geo.size.width, height: geo.size.height) ForEach(regions) { region in let isActive = region.id == activeRegion?.id // Front: aktive Maske opak, andere transparent. // Back: alle Masken transparent (Bild komplett sichtbar). if !isFlipped, isActive { Rectangle() .fill(CardsTheme.primary.opacity(0.92)) .frame( width: region.w * geo.size.width, height: region.h * geo.size.height ) .offset(x: region.x * geo.size.width, y: region.y * geo.size.height) .overlay( Text(region.label?.isEmpty == false ? region.label! : "?") .font(.caption.weight(.bold)) .foregroundStyle(CardsTheme.primaryForeground) .offset(x: region.x * geo.size.width, y: region.y * geo.size.height), alignment: .topLeading ) } } } } .aspectRatio(4 / 3, contentMode: .fit) if isFlipped, let label = activeRegion?.label, !label.isEmpty { Text(label) .font(.title3.weight(.semibold)) .foregroundStyle(CardsTheme.primary) } if let note = card.fields["note"], !note.isEmpty { Text(note) .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) } } } @ViewBuilder private var audioFrontView: some View { let audioRef = card.fields["audio_ref"] ?? "" VStack(spacing: 16) { AudioPlayerButton(mediaId: audioRef) if isFlipped { Divider().background(CardsTheme.border) text(card.fields["back"] ?? "") .font(.title3) .foregroundStyle(CardsTheme.foreground) } } } private var placeholderView: some View { VStack(spacing: 8) { Image(systemName: "questionmark.square.dashed") .font(.largeTitle) .foregroundStyle(CardsTheme.mutedForeground) Text("Card-Type »\(card.type.rawValue)« kommt in einer späteren Phase") .font(.caption) .multilineTextAlignment(.center) .foregroundStyle(CardsTheme.mutedForeground) } } /// Markdown-Bold (`**...**`) wird auf SwiftUI's AttributedString gemappt. private func text(_ markdown: String) -> some View { let attributed = (try? AttributedString( markdown: markdown, options: AttributedString.MarkdownParsingOptions( interpretedSyntax: .inlineOnlyPreservingWhitespace ) )) ?? AttributedString(markdown) return Text(attributed) .multilineTextAlignment(.center) } }