import ManaCore import SwiftUI /// Card-Create-View. Type-Picker oben, type-spezifische Felder unten. /// Deckt basic, basic-reverse, cloze, typing, multiple-choice ab — /// image-occlusion und audio-front kommen in β-4 (brauchen Media). struct CardEditorView: View { let deckId: String let onCreated: (Card) -> Void @Environment(AuthClient.self) private var auth @Environment(\.dismiss) private var dismiss @State private var type: CardType = .basic @State private var front: String = "" @State private var back: String = "" @State private var clozeText: String = "" @State private var typingAnswer: String = "" @State private var multipleChoiceAnswer: String = "" @State private var isSubmitting = false @State private var errorMessage: String? /// β-3-Card-Types (β-4 ergänzt image-occlusion + audio-front). private static let supportedTypes: [CardType] = [ .basic, .basicReverse, .cloze, .typing, .multipleChoice, ] var body: some View { Form { Section("Card-Type") { Picker("Typ", selection: $type) { ForEach(Self.supportedTypes, id: \.self) { t in Text(label(for: t)).tag(t) } } .pickerStyle(.menu) } typeFields if let errorMessage { Section { Text(errorMessage) .font(.footnote) .foregroundStyle(CardsTheme.error) } } } .navigationTitle("Neue Karte") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Erstellen") { Task { await submit() } } .disabled(!canSubmit || isSubmitting) } } } @ViewBuilder private var typeFields: some View { switch type { case .basic, .basicReverse: Section("Vorderseite") { TextField("Front", text: $front, axis: .vertical) .lineLimit(2 ... 6) } Section("Rückseite") { TextField("Back", text: $back, axis: .vertical) .lineLimit(2 ... 6) } if type == .basicReverse { Section { Text("Beide Richtungen werden gelernt — front→back und back→front.") .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) } } case .cloze: Section("Cloze-Text") { TextField("Beispiel: Die Hauptstadt von {{c1::Frankreich}} ist {{c2::Paris}}.", text: $clozeText, axis: .vertical) .lineLimit(3 ... 8) .autocorrectionDisabled() .textInputAutocapitalization(.sentences) .monospaced() } Section { let count = Cloze.subIndexCount(clozeText) if count > 0 { Label("\(count) Lücken erkannt → \(count) Reviews", systemImage: "checkmark.circle.fill") .font(.caption) .foregroundStyle(CardsTheme.success) } else { Label("Mindestens ein Cluster `{{c1::...}}` erforderlich", systemImage: "exclamationmark.circle") .font(.caption) .foregroundStyle(CardsTheme.warning) } Text("Mit Hint: `{{c1::Berlin::Hauptstadt von DE}}`") .font(.caption2) .foregroundStyle(CardsTheme.mutedForeground) } case .typing: Section("Frage") { TextField("Front", text: $front, axis: .vertical) .lineLimit(2 ... 4) } Section("Erwartete Antwort") { TextField("Answer", text: $typingAnswer) } case .multipleChoice: Section("Frage") { TextField("Front", text: $front, axis: .vertical) .lineLimit(2 ... 4) } Section("Richtige Antwort") { TextField("Answer", text: $multipleChoiceAnswer) } Section { Text("Distractor-Optionen werden zur Lernzeit automatisch aus anderen Karten desselben Decks gezogen.") .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) } case .imageOcclusion, .audioFront: Section { Label("Dieser Typ kommt in Phase β-4 (Media)", systemImage: "clock") .foregroundStyle(CardsTheme.mutedForeground) } } } private var canSubmit: Bool { switch type { case .basic, .basicReverse: !front.trimmed.isEmpty && !back.trimmed.isEmpty case .cloze: Cloze.subIndexCount(clozeText) > 0 case .typing: !front.trimmed.isEmpty && !typingAnswer.trimmed.isEmpty case .multipleChoice: !front.trimmed.isEmpty && !multipleChoiceAnswer.trimmed.isEmpty case .imageOcclusion, .audioFront: false } } private func submit() async { isSubmitting = true errorMessage = nil defer { isSubmitting = false } let api = CardsAPI(auth: auth) let fields: [String: String] switch type { case .basic, .basicReverse: fields = CardFieldsBuilder.basic(front: front.trimmed, back: back.trimmed) case .cloze: fields = CardFieldsBuilder.cloze(text: clozeText.trimmed) case .typing: fields = CardFieldsBuilder.typing(front: front.trimmed, answer: typingAnswer.trimmed) case .multipleChoice: fields = CardFieldsBuilder.multipleChoice(front: front.trimmed, answer: multipleChoiceAnswer.trimmed) case .imageOcclusion, .audioFront: return // disabled } let body = CardCreateBody(deckId: deckId, type: type, fields: fields, mediaRefs: nil) do { let card = try await api.createCard(body) onCreated(card) dismiss() } catch { errorMessage = (error as? LocalizedError)?.errorDescription ?? String(describing: error) } } private func label(for type: CardType) -> String { switch type { case .basic: "Einfach (Vorder/Rück)" case .basicReverse: "Beidseitig" case .cloze: "Lückentext" case .typing: "Eintippen" case .multipleChoice: "Multiple Choice" case .imageOcclusion: "Bild-Verdeckung" case .audioFront: "Audio" } } } private extension String { var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } }