Alle 7 Card-Types werden gerendert und können erstellt werden. image-occlusion mit Touch-Drag-Mask-Editor (kein PencilKit — Server- Schema erlaubt nur Rechtecke), audio-front mit AVAudioPlayer und File-Picker. - MediaUploadResponse-DTO, MaskRegion-Codable mit 0..1-Coordinates - MaskRegions.parse/encode (1:1-Port aus cards-domain, Sortierung nach ID lexikographisch) - CardFieldsBuilder.imageOcclusion mit stringified-JSON-mask_regions + audioFront - CardsAPI.uploadMedia (Multipart, 25 MiB) + fetchMedia (streamed) - MediaCache actor mit LRU 200 MB (contentModificationDate-Eviction) - mediaCache Environment-Key - RemoteImage + AudioPlayerButton SwiftUI-Views - CardRenderer: imageOcclusion (Mask-Overlay über RemoteImage) + audioFront (AudioPlayerButton + back-Text auf Flip) - MaskEditorView: Touch-Drag-Rechteck, Label-Edit, Delete - CardEditorView erweitert: PhotosPicker für Image, fileImporter für Audio, Magic-Byte-MIME-Detection - 6 neue Tests für MaskRegions (30 Total grün) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
8.3 KiB
Markdown
172 lines
8.3 KiB
Markdown
# Plan — cards-native (SwiftUI Universal)
|
|
|
|
**Stand: 2026-05-13 — Phasen β-0 bis β-4 abgeschlossen.**
|
|
Alle 7 Card-Types werden gerendert und können erstellt werden,
|
|
inklusive image-occlusion (Touch-Drag-Mask-Editor) und audio-front
|
|
(File-Picker + AVAudioPlayer). MediaCache mit LRU 200 MB.
|
|
30 Unit-Tests + 1 UI-Test grün.
|
|
|
|
Pflicht-Check für β-2: Endurance-Test auf realem Gerät (200+ Karten
|
|
mit Flugmodus zwischendurch) steht aus — Aufgabe für Till.
|
|
|
|
> **SOT:** `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
|
> Dieses File ist die App-lokale Status-Spur, das Greenfield-Doc
|
|
> hat die ganze Architektur-Begründung.
|
|
|
|
## Aktueller Stand
|
|
|
|
✅ **β-0 — Setup (2026-05-12, Tag `v0.1.0`)**
|
|
- Repo-Skelett unter `git.mana.how/till/cards-native`
|
|
- `project.yml` mit Bundle-ID `ev.mana.cards`, ManaSwiftCore via
|
|
`path: ../mana-swift-core`
|
|
- `AppConfig` als `ManaAppConfig`-Provider:
|
|
- Auth: `https://auth.mana.how`
|
|
- API: `https://cardecky-api.mana.how`
|
|
- Keychain-Service: `ev.mana.cards`
|
|
- `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus
|
|
`mana/packages/themes/src/variants/forest.css`)
|
|
- `LoginView` (Email/PW gegen mana-auth)
|
|
- 3 Unit-Tests (AppConfig)
|
|
|
|
✅ **β-4 — Media + Advanced Card-Types (2026-05-13, Tag `v0.5.0`)**
|
|
- `MediaUploadResponse` DTO + `MediaKind`-Enum
|
|
- `MaskRegion` Codable mit 0..1-Coordinates, `MaskRegions.parse/encode`-
|
|
Helpers (1:1-Port aus `cards-domain/image-occlusion.ts` — Sortierung
|
|
nach ID lexikographisch)
|
|
- `CardFieldsBuilder.imageOcclusion`, `.audioFront` mit korrekter
|
|
`mask_regions`-Serialisierung als stringified JSON-Array
|
|
- `CardsAPI.uploadMedia(data, filename, mimeType)` mit Multipart
|
|
(25 MiB max), `.fetchMedia(id)` für streamed bytes
|
|
- `MediaCache` actor mit LRU 200 MB (sortiert nach `contentModificationDate`)
|
|
- `mediaCache`-Environment-Key, im App-Entrypoint instantiiert
|
|
- `RemoteImage` View — authentifiziertes Image-Loading mit ProgressView
|
|
+ Failure-State
|
|
- `AudioPlayerButton` — AVAudioPlayer-Wrapper mit Play/Pause-Toggle,
|
|
AVAudioSession-Setup für iOS
|
|
- `CardRenderer.imageOcclusionView`: AsyncImage + opake Maske über aktiver
|
|
Region (Frontside), Label-Reveal auf Backside
|
|
- `CardRenderer.audioFrontView`: AudioPlayerButton + back-Text auf Flip
|
|
- `MaskEditorView`: Touch-Drag-to-Create-Rectangle, Liste mit Label-Edit
|
|
+ Delete, 0..1-Normalisierung beim Commit
|
|
- `CardEditorView` erweitert: PhotosPicker für Image, fileImporter für
|
|
Audio, Magic-Byte-MIME-Detection (JPEG/PNG/GIF/WebP)
|
|
- 6 neue Tests für MaskRegions-Parse/Encode + Field-Builder (30 Total)
|
|
|
|
✅ **β-3 — Editor (2026-05-13, Tag `v0.4.0`)**
|
|
- `DeckCreateBody`, `DeckUpdateBody`, `CardCreateBody`, `CardUpdateBody`
|
|
Encodable-Structs (snake_case via `CodingKeys`, nil-Felder werden
|
|
weggelassen)
|
|
- `CardFieldsBuilder` mit Type-spezifischen Pflicht-Feld-Konstruktoren
|
|
- `CardsAPI`: createDeck/updateDeck/deleteDeck + createCard/updateCard/deleteCard
|
|
- `DeckEditorView` für Create + Edit in einer View (mode-switch),
|
|
Color-Picker mit 8-Preset-Palette aus forest-Theme, Category-Picker
|
|
(11 Kategorien mit deutschen Labels), Visibility-Segmented-Control
|
|
- `CardEditorView` mit Type-Picker (basic, basic-reverse, cloze,
|
|
typing, multiple-choice) und dynamischen Feldern je Typ. Cloze-View
|
|
zeigt Live-Cluster-Count und Hint-Syntax-Hinweis. image-occlusion
|
|
und audio-front zeigen β-4-Placeholder
|
|
- `DeckDetailView` mit 4 Action-Buttons (Lernen, Karte hinzufügen,
|
|
Bearbeiten, Löschen), Confirmation-Dialog für Delete
|
|
- DeckListView: "+"-Button im Toolbar (Leading), Sheet für Create
|
|
- 7 zusätzliche Encoding-Tests (24 Unit-Tests total)
|
|
|
|
✅ **β-2 — Study-Loop (2026-05-13, Tag `v0.3.0`)**
|
|
- `Card`, `Review`, `DueReview` Codable-DTOs, `CardType`-Enum (alle 7 Typen)
|
|
- `Rating`-Enum: `again | hard | good | easy` mit deutschen Labels
|
|
- `Cloze`-Helpers (extractClusterIds, subIndexCount, clusterId,
|
|
renderPrompt, renderAnswer, hint) — 1:1-Port aus
|
|
`cards/packages/cards-domain/src/cloze.ts`
|
|
- `CardsAPI.dueReviews(deckId:)`, `CardsAPI.gradeReview(...)` mit
|
|
ISO8601-Encoder
|
|
- `PendingGrade` SwiftData-Model + `GradeQueue` für Offline-Submit
|
|
(FIFO-Drain, originaler reviewedAt-Timestamp bleibt erhalten)
|
|
- `StudySession` als @Observable State-Machine
|
|
(loading/studying/finished/failed)
|
|
- `CardRenderer`: basic, basic-reverse (sub-index-abhängig), cloze
|
|
client-rendered. image-occlusion/audio-front/typing/multiple-choice
|
|
zeigen Placeholder (β-3/β-4)
|
|
- `RatingBar` mit Haptic-Feedback (medium für again/hard/good,
|
|
heavy für easy, soft beim Flip)
|
|
- `StudySessionView` vollbild aus DeckListView per NavigationLink
|
|
- 9 zusätzliche Tests (Cloze 8x, Review/DueReview-Decoding 3x)
|
|
|
|
✅ **β-1 — Decks lesen (2026-05-13, Tag `v0.2.0`)**
|
|
- `Deck`-Codable-DTO mit snake_case-CodingKeys, plus
|
|
`DeckCategory`, `DeckVisibility`, `FsrsSettings`
|
|
- ISO8601-Date-Decoder mit Fractional-Seconds-Toleranz
|
|
- `CardsAPI.listDecks()`, `cardCount(deckId:)`, `dueCount(deckId:)`
|
|
- `CachedDeck` als SwiftData-Model mit `lastFetchedAt` (Offline-Read)
|
|
- `DeckListStore` orchestriert API + Cache, paralleles Counts-Fetching
|
|
via TaskGroup
|
|
- `DeckListView` mit Pull-to-Refresh, Card/Due-Counts, deck.color-Streifen,
|
|
Inbox-Banner für Marketplace-Forks
|
|
- `AccountView` mit Sign-out-Button
|
|
- iOS-Simulator-Build + Tests grün (6 Unit-Tests, 1 UI-Test)
|
|
|
|
## Phasen (Detail in Greenfield-Plan)
|
|
|
|
| Phase | Status | Inhalt |
|
|
|---|---|---|
|
|
| β-0 | ✅ 2026-05-12 | Setup, Login, API-Probe |
|
|
| β-1 | ✅ 2026-05-13 | Decks lesen, SwiftData-Cache, Pull-to-Refresh |
|
|
| β-2 | ✅ 2026-05-13 | Study-Loop, Offline-Grade-Queue (Endurance-Test offen) |
|
|
| β-3 | ✅ 2026-05-13 | Editor: Deck-CRUD + Card-Create (5 Types); Anki-Import auf β-3-ext verschoben |
|
|
| β-4 | ✅ 2026-05-13 | Media-Upload, image-occlusion (Touch-Mask-Editor), audio-front (AVAudioPlayer) |
|
|
| β-5 | — | Marketplace, Universal-Links |
|
|
| β-6 | — | Native-Polish (Widgets, Notifications, Share-Extension) |
|
|
| β-7 | — | App-Store-Submission |
|
|
|
|
## Nächste Schritte für β-5 (Marketplace)
|
|
|
|
Aus Greenfield-Plan-Sektion "Phase β-5":
|
|
|
|
1. `ExploreView`: GET `/api/v1/marketplace/explore` — Featured/Trending
|
|
2. `BrowseView`: GET `/api/v1/marketplace/decks/browse` mit Filter-Bar
|
|
3. `PublicDeckView`: GET `/api/v1/marketplace/decks/:slug` — Detail mit
|
|
Subscribe-Button (= POST `/subscribe/:slug`, Auto-Fork)
|
|
4. Subscribed-Decks-Liste als zweite Section in `DeckListView`
|
|
5. **Universal-Links**: `cardecky.mana.how/d/:slug` öffnet App direkt
|
|
|
|
**Erfolgskriterium:** Drei Live-Decks (geografie-welt-top30, english-a2,
|
|
periodensystem-elemente) sichtbar, subscribebar, lernbar.
|
|
|
|
**Vorbedingung:** AASA auf `cardecky.mana.how/.well-known/apple-app-site-association`
|
|
muss aufgesetzt werden — heute 404. Aufgabe ans Cards-Web-Repo.
|
|
|
|
## Notizen aus β-4
|
|
|
|
- **PencilKit für Mask-Editor explizit nicht genutzt.** Web macht
|
|
Image-Occlusion-Masks per Touch-Drag-Rechteck (kein Freihand). Server-
|
|
Schema (`MaskRegion = {id, x, y, w, h, label}`) erlaubt nur Rechtecke,
|
|
PencilKit-Strokes wären dafür übersteigert. Wenn später Polygon-Masks
|
|
oder Freihand-Skizzen dazu kommen, kann PencilKit nachgereicht werden.
|
|
- **Apple-Pencil-Support** trotzdem grundsätzlich da: SwiftUI's
|
|
`DragGesture` reagiert auf Pencil-Eingaben genauso wie auf Finger.
|
|
|
|
## Verschoben auf β-3-Extension oder später
|
|
|
|
- **Anki-Import** (`.apkg`-Parser): Web parsed client-side und ruft
|
|
`POST /cards` pro Karte. Native bräuchte eigenen Swift-Parser für
|
|
Anki-Pakete (Plist/sqlite/.apkg) — eigener Brocken, nicht
|
|
blockierend für andere Phasen.
|
|
- **Card-Edit** (PATCH /cards/:id): Card-Create reicht für Web-Parität
|
|
in v1, Edit kann später nachgereicht werden.
|
|
- **Distractor-Vorschau** für Multiple-Choice-Editor: Server liefert
|
|
Distractors zur Lernzeit (`/decks/:deckId/distractors`), Editor
|
|
zeigt sie nicht — Web macht das auch nicht.
|
|
|
|
## Pflicht-Tests für β-2 (vor β-3-Start)
|
|
|
|
- [ ] Endurance-Test auf realem Gerät: 200+ Karten lernen, Flugmodus
|
|
zwischendurch — alle Grades landen am Server nach Reconnect.
|
|
- [ ] Cross-Check mit Web: Karte gegrade in Native → Web zeigt
|
|
identischen Review-State nach Reload.
|
|
|
|
## Cross-Refs
|
|
|
|
- `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — Greenfield-Plan SOT
|
|
- `../mana/docs/MANA_SWIFT.md` — Plattform-SOT
|
|
- `../cards/CLAUDE.md` — Cards-Repo
|
|
- `../cards/STATUS.md` — Web-Phasenstand (Referenz)
|
|
- `../mana-swift-core/CLAUDE.md` — ManaCore-Konventionen
|
|
- `CLAUDE.md` — Repo-Konventionen
|