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:
parent
3b861af3fb
commit
cf1160b270
9 changed files with 930 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
56
Sources/Core/Domain/CardMutations.swift
Normal file
56
Sources/Core/Domain/CardMutations.swift
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import Foundation
|
||||
|
||||
/// Body für `POST /api/v1/cards`. Aus `CardCreateSchema`.
|
||||
///
|
||||
/// `fields` ist type-abhängig — Server validiert via
|
||||
/// `validateFieldsForType()`. Pflicht-Keys pro Type:
|
||||
/// - basic, basic-reverse: `front`, `back`
|
||||
/// - cloze: `text` (mit `{{cN::...}}`-Clustern)
|
||||
/// - typing: `front`, `answer`
|
||||
/// - multiple-choice: `front`, `answer`
|
||||
/// - image-occlusion: `image_ref`, `mask_regions` (β-4)
|
||||
/// - audio-front: `audio_ref`, `back` (β-4)
|
||||
struct CardCreateBody: Encodable, Sendable {
|
||||
let deckId: String
|
||||
let type: CardType
|
||||
let fields: [String: String]
|
||||
let mediaRefs: [String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deckId = "deck_id"
|
||||
case type
|
||||
case fields
|
||||
case mediaRefs = "media_refs"
|
||||
}
|
||||
}
|
||||
|
||||
/// Body für `PATCH /api/v1/cards/:id`. Nur `fields` und `media_refs` —
|
||||
/// Type und deck_id sind immutable (Server-Schema).
|
||||
struct CardUpdateBody: Encodable, Sendable {
|
||||
var fields: [String: String]?
|
||||
var mediaRefs: [String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case fields
|
||||
case mediaRefs = "media_refs"
|
||||
}
|
||||
}
|
||||
|
||||
/// Hilfs-Builder für Card-Type-spezifische `fields`-Dictionaries.
|
||||
enum CardFieldsBuilder {
|
||||
static func basic(front: String, back: String) -> [String: String] {
|
||||
["front": front, "back": back]
|
||||
}
|
||||
|
||||
static func cloze(text: String) -> [String: String] {
|
||||
["text": text]
|
||||
}
|
||||
|
||||
static func typing(front: String, answer: String) -> [String: String] {
|
||||
["front": front, "answer": answer]
|
||||
}
|
||||
|
||||
static func multipleChoice(front: String, answer: String) -> [String: String] {
|
||||
["front": front, "answer": answer]
|
||||
}
|
||||
}
|
||||
38
Sources/Core/Domain/DeckMutations.swift
Normal file
38
Sources/Core/Domain/DeckMutations.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import Foundation
|
||||
|
||||
/// Body für `POST /api/v1/decks`. Aus `DeckCreateSchema` in
|
||||
/// `cards/packages/cards-domain/src/schemas/deck.ts`.
|
||||
struct DeckCreateBody: Encodable, Sendable {
|
||||
let name: String
|
||||
let description: String?
|
||||
let color: String?
|
||||
let category: DeckCategory?
|
||||
let visibility: DeckVisibility?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case description
|
||||
case color
|
||||
case category
|
||||
case visibility
|
||||
}
|
||||
}
|
||||
|
||||
/// Body für `PATCH /api/v1/decks/:id`. Alle Felder optional plus `archived`.
|
||||
struct DeckUpdateBody: Encodable, Sendable {
|
||||
var name: String?
|
||||
var description: String?
|
||||
var color: String?
|
||||
var category: DeckCategory?
|
||||
var visibility: DeckVisibility?
|
||||
var archived: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case description
|
||||
case color
|
||||
case category
|
||||
case visibility
|
||||
case archived
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue