v0.4.0 — Phase β-3 Editor

Voller Editor-Flow für Decks und 5 Card-Types (basic, basic-reverse,
cloze, typing, multiple-choice). image-occlusion + audio-front kommen
mit β-4 (Media). Anki-Import bleibt vorerst aus (Web parsed client-
side, gibt keinen Server-Import-Endpoint zu rufen).

- DeckCreateBody/UpdateBody, CardCreateBody/UpdateBody Encodable
  mit snake_case-CodingKeys, nil-Felder werden weggelassen
- CardFieldsBuilder mit Type-spezifischen Pflicht-Feld-Konstruktoren
- CardsAPI: createDeck/updateDeck/deleteDeck +
  createCard/updateCard/deleteCard
- DeckEditorView (Create + Edit in einer View): Color-Picker mit
  8-Preset-Palette, Category-Picker (11 Kats, deutsche Labels),
  Visibility-Segmented-Control
- CardEditorView mit Type-Picker und dynamischen Feldern je Typ.
  Cloze-Sektion zeigt Live-Cluster-Count und Hint-Syntax-Hinweis.
  image-occlusion/audio-front zeigen β-4-Placeholder
- DeckDetailView mit Action-Buttons (Lernen, Karte hinzufügen,
  Bearbeiten, Löschen mit Confirmation)
- DeckListView: "+"-Button im Toolbar (Leading) für Create-Sheet
- 7 neue Encoding-Tests (24 Unit-Tests + 1 UI-Test grün)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-13 00:24:43 +02:00
parent 3b861af3fb
commit cf1160b270
9 changed files with 930 additions and 19 deletions

View file

@ -54,6 +54,84 @@ actor CardsAPI {
return try decoder.decode(DueReviewsResponse.self, from: data).total
}
// MARK: - Deck-Mutations
/// `POST /api/v1/decks` Deck anlegen.
@discardableResult
func createDeck(_ body: DeckCreateBody) async throws -> Deck {
let data = try makeJSON(body)
let (responseData, http) = try await transport.request(
path: "/api/v1/decks",
method: "POST",
body: data
)
try ensureOK(http, data: responseData)
return try decoder.decode(Deck.self, from: responseData)
}
/// `PATCH /api/v1/decks/:id` Deck-Felder ändern.
@discardableResult
func updateDeck(id: String, body: DeckUpdateBody) async throws -> Deck {
let data = try makeJSON(body)
let (responseData, http) = try await transport.request(
path: "/api/v1/decks/\(id)",
method: "PATCH",
body: data
)
try ensureOK(http, data: responseData)
return try decoder.decode(Deck.self, from: responseData)
}
/// `DELETE /api/v1/decks/:id` Deck löschen (kaskadiert Cards + Reviews).
func deleteDeck(id: String) async throws {
let (data, http) = try await transport.request(
path: "/api/v1/decks/\(id)",
method: "DELETE"
)
try ensureOK(http, data: data)
}
// MARK: - Card-Mutations
/// `POST /api/v1/cards` Karte anlegen. Server validiert `fields`
/// gegen den Card-Type und erstellt automatisch Reviews
/// (1 für basic, 2 für basic-reverse, N für cloze).
@discardableResult
func createCard(_ body: CardCreateBody) async throws -> Card {
let data = try makeJSON(body)
let (responseData, http) = try await transport.request(
path: "/api/v1/cards",
method: "POST",
body: data
)
try ensureOK(http, data: responseData)
return try decoder.decode(Card.self, from: responseData)
}
/// `PATCH /api/v1/cards/:id` nur `fields` und `media_refs`
/// sind änderbar.
@discardableResult
func updateCard(id: String, body: CardUpdateBody) async throws -> Card {
let data = try makeJSON(body)
let (responseData, http) = try await transport.request(
path: "/api/v1/cards/\(id)",
method: "PATCH",
body: data
)
try ensureOK(http, data: responseData)
return try decoder.decode(Card.self, from: responseData)
}
/// `DELETE /api/v1/cards/:id` Karte + zugehörige Reviews löschen
/// (Cascade auf DB-Ebene).
func deleteCard(id: String) async throws {
let (data, http) = try await transport.request(
path: "/api/v1/cards/\(id)",
method: "DELETE"
)
try ensureOK(http, data: data)
}
// MARK: - Study
/// `GET /api/v1/reviews/due?deck_id=...&limit=500` fällige Reviews