import ManaCore import SwiftUI /// Deck-Create und Deck-Edit in einer View. `existing == nil` → Create- /// Modus mit "Erstellen"-Button. Sonst Edit-Modus mit "Speichern". struct DeckEditorView: View { enum Mode: Sendable { case create case edit(deckId: String) } let mode: Mode let onSaved: (Deck) -> Void @Environment(AuthClient.self) private var auth @Environment(\.dismiss) private var dismiss @State private var name: String @State private var description: String @State private var color: String @State private var category: DeckCategory? @State private var visibility: DeckVisibility @State private var isSubmitting = false @State private var errorMessage: String? /// Vorgefüllte Farbpalette aus dem forest-Theme. User können /// freie Hex-Werte später via Picker setzen (β-3-extension). private static let presetColors: [String] = [ "#10803D", // forest primary light "#1E3A2F", // forest dark "#D97706", // amber "#DC2626", // red "#2563EB", // blue "#7C3AED", // violet "#0D9488", // teal "#737373", // neutral ] init(mode: Mode, existing: CachedDeck? = nil, onSaved: @escaping (Deck) -> Void) { self.mode = mode self.onSaved = onSaved _name = State(initialValue: existing?.name ?? "") _description = State(initialValue: existing?.deckDescription ?? "") _color = State(initialValue: existing?.color ?? Self.presetColors[0]) _category = State(initialValue: existing?.category) _visibility = State(initialValue: DeckVisibility(rawValue: existing?.visibilityRaw ?? "private") ?? .private) } var body: some View { Form { Section("Name") { TextField("Deck-Name", text: $name) .textInputAutocapitalization(.sentences) } Section("Beschreibung") { TextField("optional", text: $description, axis: .vertical) .lineLimit(2 ... 4) } Section("Farbe") { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(Self.presetColors, id: \.self) { hex in colorSwatch(hex) } } .padding(.vertical, 4) } } Section("Kategorie") { Picker("Kategorie", selection: $category) { Text("Keine").tag(DeckCategory?.none) ForEach(DeckCategory.allCases, id: \.self) { cat in Text(cat.label).tag(DeckCategory?.some(cat)) } } } Section("Sichtbarkeit") { Picker("Sichtbarkeit", selection: $visibility) { Text("Privat").tag(DeckVisibility.private) Text("Space").tag(DeckVisibility.space) Text("Öffentlich").tag(DeckVisibility.public) } .pickerStyle(.segmented) } if let errorMessage { Section { Text(errorMessage) .font(.footnote) .foregroundStyle(CardsTheme.error) } } } .navigationTitle(isCreate ? "Neues Deck" : "Deck bearbeiten") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(isCreate ? "Erstellen" : "Speichern") { Task { await submit() } } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty || isSubmitting) } } } private var isCreate: Bool { if case .create = mode { return true } return false } @ViewBuilder private func colorSwatch(_ hex: String) -> some View { let isSelected = color == hex Circle() .fill(Color.swatchFromHex(hex)) .frame(width: 36, height: 36) .overlay( Circle() .stroke(isSelected ? CardsTheme.foreground : CardsTheme.border, lineWidth: isSelected ? 3 : 1) ) .onTapGesture { color = hex } } private func submit() async { isSubmitting = true errorMessage = nil defer { isSubmitting = false } let api = CardsAPI(auth: auth) do { switch mode { case .create: let body = DeckCreateBody( name: name.trimmingCharacters(in: .whitespaces), description: nonEmpty(description), color: color, category: category, visibility: visibility ) let deck = try await api.createDeck(body) onSaved(deck) dismiss() case let .edit(deckId): let body = DeckUpdateBody( name: name.trimmingCharacters(in: .whitespaces), description: nonEmpty(description), color: color, category: category, visibility: visibility ) let deck = try await api.updateDeck(id: deckId, body: body) onSaved(deck) dismiss() } } catch { errorMessage = (error as? LocalizedError)?.errorDescription ?? String(describing: error) } } private func nonEmpty(_ s: String) -> String? { let trimmed = s.trimmingCharacters(in: .whitespaces) return trimmed.isEmpty ? nil : trimmed } } extension Color { static func swatchFromHex(_ hex: String) -> Color { var trimmed = hex.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.hasPrefix("#") { trimmed = String(trimmed.dropFirst()) } guard let rgb = UInt32(trimmed, radix: 16) else { return CardsTheme.primary } let r = Double((rgb >> 16) & 0xFF) / 255.0 let g = Double((rgb >> 8) & 0xFF) / 255.0 let b = Double(rgb & 0xFF) / 255.0 return Color(red: r, green: g, blue: b) } }