refactor(big-bang): cards-native → wordeck-native
Code + Identity-Rename zur Vorbereitung auf Apple-Dev-Portal-Aktion (Bundle ev.mana.wordeck, App-Group group.ev.mana.wordeck, AASA applinks:wordeck.com). Build bleibt funktional, aber gegen die neue text-only-API können image-occlusion-Creates 422 zurückgeben — das wird mit der Wordeck-Native v1.0-Welle (parallele Apple-Aktion) sauber gemacht. Umbenennung: - 41 Files: cardecky/Cardecky → wordeck/Wordeck (Display, Strings, Kommentare) - 57 Files: CardsNative → WordeckNative, CardsAPI → WordeckAPI, CardsTheme → WordeckTheme, CardsBrand → WordeckBrand, CardsWidget → WordeckWidget, CardsDueWidget → WordeckDueWidget - Bundle-ID ev.mana.cardecky → ev.mana.wordeck (project.yml, Info.plist, entitlements, Keychain-Service, App-Group) - AASA applinks:cardecky.mana.how → applinks:wordeck.com - API-Base cardecky-api.mana.how → api.wordeck.com - 10 Files renamed (App-Entry, API-Extensions, Theme, Widget, Entitlements, Tests) - xcodeproj regenerated via xcodegen → WordeckNative.xcodeproj - MaskRegionsTests.swift gelöscht (image-occlusion entfällt mit Wordeck text-only) Forgejo-Repo git.mana.how/till/cards-native → wordeck-native umbenannt (Auto-Redirect aktiv). Lokales Verzeichnis Code/cards-native/ bleibt vorerst — wird beim nächsten Apple-Setup mit Bundle-Test umbenannt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f154bfb39
commit
542082772a
83 changed files with 691 additions and 648 deletions
42
CLAUDE.md
42
CLAUDE.md
|
|
@ -1,4 +1,4 @@
|
|||
# CLAUDE.md — cards-native repo
|
||||
# CLAUDE.md — wordeck-native repo
|
||||
|
||||
Guidance für Claude Code in diesem Repository.
|
||||
|
||||
|
|
@ -10,27 +10,27 @@ Guidance für Claude Code in diesem Repository.
|
|||
## Was dieses Repo ist
|
||||
|
||||
**Cards Native** — native SwiftUI-Universal-App (iOS / iPadOS / macOS)
|
||||
für **Cardecky**, die Spaced-Repetition-Karten-App des Vereins
|
||||
**mana e.V.** Web-Parität zu `cardecky.mana.how`, plus native iOS-
|
||||
für **Wordeck**, die Spaced-Repetition-Karten-App des Vereins
|
||||
**mana e.V.** Web-Parität zu `wordeck.com`, plus native iOS-
|
||||
Affordances (Widgets, Notifications, Universal-Links, Pencil).
|
||||
|
||||
```
|
||||
HTTPS/JWT ┌──────────────────┐
|
||||
cards-api ◄───────────── │ cards-native │ SwiftUI
|
||||
cardecky-api.mana.how │ (this repo) │ SwiftData (Cache)
|
||||
│ ev.mana.cardecky │ WidgetKit (β-6)
|
||||
cards-api ◄───────────── │ wordeck-native │ SwiftUI
|
||||
api.wordeck.com │ (this repo) │ SwiftData (Cache)
|
||||
│ ev.mana.wordeck │ WidgetKit (β-6)
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
**Phase β-0 — Setup (2026-05-12).** Repo-Skelett, ManaCore + ManaTokens
|
||||
als Package-Dependency, Login + Cardecky-API-Reachability-Probe.
|
||||
als Package-Dependency, Login + Wordeck-API-Reachability-Probe.
|
||||
Phasen β-1 bis β-7 in `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
||||
|
||||
## Leitprinzip: Web-Parität
|
||||
|
||||
Die Web-App auf `cardecky.mana.how` ist Funktions-Referenz. Bei
|
||||
Die Web-App auf `wordeck.com` ist Funktions-Referenz. Bei
|
||||
Konflikt zwischen Native und Web → **Web gewinnt**. Native ist
|
||||
Re-Implementation, kein neues Produkt.
|
||||
|
||||
|
|
@ -53,12 +53,12 @@ Beschlossen. Nicht ohne explizite Diskussion antasten.
|
|||
Auth-Implementierung ist verboten.
|
||||
4. **Pure SwiftUI.** Keine externen UI-Libraries. AppKit/UIKit nur
|
||||
als Bridge wenn zwingend (z.B. `PencilKit` für Image-Occlusion).
|
||||
5. **Bundle-ID `ev.mana.cardecky`.** Reverse-Domain mana-ev.ch.
|
||||
Universal-Link-Domain: `cardecky.mana.how`.
|
||||
5. **Bundle-ID `ev.mana.wordeck`.** Reverse-Domain mana-ev.ch.
|
||||
Universal-Link-Domain: `wordeck.com`.
|
||||
6. **Cards-Domain-Logik bleibt am Server.** SubIndex-Berechnung für
|
||||
Cloze, Image-Occlusion-Mask-Validation, Content-Hash — alles
|
||||
Server. Native zeigt nur, was vom Server kommt.
|
||||
7. **`forest`-Theme.** Heute lokal in `CardsTheme.swift` nachgebaut
|
||||
7. **`forest`-Theme.** Heute lokal in `WordeckTheme.swift` nachgebaut
|
||||
(Werte gespiegelt aus `mana/packages/themes/src/variants/forest.css`).
|
||||
Migration auf ManaTokens-Theme-Switch ist Phase ε.
|
||||
8. **Web gewinnt bei Konflikt.** Eleganteres Native-Verhalten geht
|
||||
|
|
@ -74,16 +74,16 @@ Beschlossen. Nicht ohne explizite Diskussion antasten.
|
|||
sind **nicht** im Git
|
||||
- **SwiftFormat** mit `.swiftformat` (4-space, 120-col, sorted imports)
|
||||
- **SwiftLint** mit `.swiftlint.yml`
|
||||
- **Logging:** App-Subsystem `ev.mana.cardecky` via
|
||||
- **Logging:** App-Subsystem `ev.mana.wordeck` via
|
||||
`Sources/Core/Telemetry/Log.swift`. ManaCore loggt parallel unter
|
||||
`ev.mana.core`
|
||||
- **Persistenz:** SwiftData für Deck/Card-Cache (ab β-1), JWT im
|
||||
Keychain (über ManaCore)
|
||||
- **Lokalisierung:** DE primary, EN fallback via `Localizable.xcstrings`
|
||||
|
||||
## Cardecky-API-Wire-Format
|
||||
## Wordeck-API-Wire-Format
|
||||
|
||||
Wire-Format gegen `https://cardecky-api.mana.how/api/v1/*`. Quelle der
|
||||
Wire-Format gegen `https://api.wordeck.com/api/v1/*`. Quelle der
|
||||
Wahrheit: `../cards/apps/api/src/routes/*.ts`. Bei neuem DTO
|
||||
verifizieren:
|
||||
|
||||
|
|
@ -95,14 +95,14 @@ verifizieren:
|
|||
## Repo-Layout
|
||||
|
||||
```
|
||||
cards-native/
|
||||
wordeck-native/
|
||||
├── project.yml XcodeGen-Manifest (SOT)
|
||||
├── PLAN.md Phase-Tracking (gekürzt aus Greenfield-Plan)
|
||||
├── CLAUDE.md dieses File
|
||||
├── README.md
|
||||
├── .swiftformat, .swiftlint.yml
|
||||
├── Sources/
|
||||
│ ├── App/ CardsNativeApp (@main), RootView
|
||||
│ ├── App/ WordeckNativeApp (@main), RootView
|
||||
│ ├── Features/
|
||||
│ │ ├── Account/ LoginView, AccountView (ab β-1)
|
||||
│ │ ├── Decks/ DashboardView (Placeholder), DeckList (β-1)
|
||||
|
|
@ -113,19 +113,19 @@ cards-native/
|
|||
│ │ └── Imports/ (β-3)
|
||||
│ ├── Core/
|
||||
│ │ ├── Auth/ AppConfig (ManaAppConfig-Provider)
|
||||
│ │ ├── API/ CardsAPI (AuthenticatedTransport-Wrapper)
|
||||
│ │ ├── API/ WordeckAPI (AuthenticatedTransport-Wrapper)
|
||||
│ │ ├── Domain/ (Card-Type-Enums, Rating-Enum — ab β-2)
|
||||
│ │ ├── Storage/ (SwiftData-Models — ab β-1)
|
||||
│ │ ├── Sync/ (ReviewQueue, MediaCache — ab β-2/β-4)
|
||||
│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.cardecky)
|
||||
│ │ └── Theme/ CardsTheme (forest-Werte)
|
||||
│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.wordeck)
|
||||
│ │ └── Theme/ WordeckTheme (forest-Werte)
|
||||
│ ├── Widgets/ (WidgetKit-Extension — ab β-6)
|
||||
│ ├── ShareExtension/ (Save-as-Card — ab β-6)
|
||||
│ └── Resources/
|
||||
│ ├── Assets.xcassets
|
||||
│ ├── Localizable.xcstrings
|
||||
│ ├── Info.plist (generiert, gitignored)
|
||||
│ └── CardsNative.entitlements (generiert, gitignored)
|
||||
│ └── WordeckNative.entitlements (generiert, gitignored)
|
||||
├── Tests/
|
||||
│ ├── UnitTests/
|
||||
│ └── UITests/
|
||||
|
|
@ -156,7 +156,7 @@ cards-native/
|
|||
**Workflow:**
|
||||
```bash
|
||||
xcodegen generate
|
||||
open CardsNative.xcodeproj
|
||||
open WordeckNative.xcodeproj
|
||||
```
|
||||
|
||||
**Vor jedem Commit:**
|
||||
|
|
|
|||
56
PLAN.md
56
PLAN.md
|
|
@ -1,17 +1,17 @@
|
|||
# Plan — cards-native (SwiftUI Universal)
|
||||
# Plan — wordeck-native (SwiftUI Universal)
|
||||
|
||||
**Stand: 2026-05-13 — TestFlight Build 11 (v0.9.4).** Alle Phasen
|
||||
β-0 bis β-7 + Polish-Iterationen. 43 Unit-Tests + 1 UI-Test grün.
|
||||
|
||||
**Cardecky-Web-Look übernommen** (v0.9.0 ff.): Fan-Stack-Tiles
|
||||
**Wordeck-Web-Look übernommen** (v0.9.0 ff.): Fan-Stack-Tiles
|
||||
(5:7 Aspect, 3 rotierte Background-Layer), CardSurface in
|
||||
md/lg/hero, RatingBar mit Good-Emphasis. Tap auf Tile = Study-
|
||||
Mode, Pencil-Icon unten rechts = Deck-Detail. Identische Tile-
|
||||
Optik in Decks- und Entdecken-Tab.
|
||||
|
||||
**App-Store-Connect:** Cardecky, App-ID 6769019526, Bundle
|
||||
`ev.mana.cardecky`, Team `QP3GLU8PH3`. AASA + /privacy + /help
|
||||
live unter `cardecky.mana.how`.
|
||||
**App-Store-Connect:** Wordeck, App-ID 6769019526, Bundle
|
||||
`ev.mana.wordeck`, Team `QP3GLU8PH3`. AASA + /privacy + /help
|
||||
live unter `wordeck.com`.
|
||||
|
||||
Pflicht-Check für β-2: Endurance-Test auf realem Gerät (200+ Karten
|
||||
mit Flugmodus zwischendurch) steht aus.
|
||||
|
|
@ -23,14 +23,14 @@ mit Flugmodus zwischendurch) steht aus.
|
|||
## 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.cardecky`, ManaSwiftCore via
|
||||
- Repo-Skelett unter `git.mana.how/till/wordeck-native`
|
||||
- `project.yml` mit Bundle-ID `ev.mana.wordeck`, 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.cardecky`
|
||||
- `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus
|
||||
- API: `https://api.wordeck.com`
|
||||
- Keychain-Service: `ev.mana.wordeck`
|
||||
- `WordeckTheme.swift` mit forest-Werten (lokal nachgebaut aus
|
||||
`mana/packages/themes/src/variants/forest.css`)
|
||||
- `LoginView` (Email/PW gegen mana-auth)
|
||||
- 3 Unit-Tests (AppConfig)
|
||||
|
|
@ -38,22 +38,22 @@ mit Flugmodus zwischendurch) steht aus.
|
|||
✅ **Post-β-7-Polish (2026-05-13, Tags v0.8.1 → v0.9.4)**
|
||||
|
||||
Live-Fixes nach Apple-Submission und Real-Device-Tests:
|
||||
- `v0.8.1` Cardecky-Rebrand (Bundle `ev.mana.cards` → `ev.mana.cardecky`,
|
||||
- `v0.8.1` Wordeck-Rebrand (Bundle `ev.mana.cards` → `ev.mana.wordeck`,
|
||||
AASA + Docker-Compose-Env nachgezogen)
|
||||
- `v0.8.2` Archive-Polish — Versions-Sync zwischen Targets, iPad-
|
||||
Orientations
|
||||
- `v0.8.3` Sendable + AppIcon-Asset-Cleanup, /privacy + /help Stubs
|
||||
- `v0.8.4` PhotosPicker Sendable-Warning via Sub-View-Struct
|
||||
- `v0.8.5` ITMS-90129-Fix: DisplayName Cards → Cardecky, Build 2
|
||||
- `v0.8.6` Cardecky-Rebrand User-facing Strings durchgängig
|
||||
- `v0.8.5` ITMS-90129-Fix: DisplayName Cards → Wordeck, Build 2
|
||||
- `v0.8.6` Wordeck-Rebrand User-facing Strings durchgängig
|
||||
- `v0.8.7` PublicDeckOwner.pseudonym Bool statt String? (Decoder-Crash
|
||||
bei Marketplace-Deck-Open)
|
||||
- `v0.8.8` Card-Liste in DeckDetailView + CardsAPI.listCards
|
||||
- `v0.8.8` Card-Liste in DeckDetailView + WordeckAPI.listCards
|
||||
- `v0.8.9` URL-Query-Bug-Fix in ManaCore.AuthenticatedTransport
|
||||
(URL.appending(path:) encoded `?` → 404; gefixt via String-Concat,
|
||||
ManaCore v1.0.1). Behob alle "0-Karten"-Phänomene und das stille
|
||||
Schlucken von Query-Endpoints.
|
||||
- `v0.9.0` Cardecky-Web-Design: Fan-Stack-Tiles, CardSurface in
|
||||
- `v0.9.0` Wordeck-Web-Design: Fan-Stack-Tiles, CardSurface in
|
||||
3 Sizes, RatingBar mit Good-Emphasis, horizontale Scroll-Sections
|
||||
- `v0.9.1` Multiple-Choice-Karten gerendert (Distractors via Server,
|
||||
Tap-Selektion, Reveal mit Korrekt/Falsch-Highlight)
|
||||
|
|
@ -104,17 +104,17 @@ gleicht Schema-Drift in der Production-DB aus.
|
|||
- `WidgetSnapshot` Codable mit `topDecks` (Top-3 nach dueCount)
|
||||
und `totalDueCount`
|
||||
- `WidgetSnapshotStore` schreibt in App-Group-Container
|
||||
`group.ev.mana.cardecky`
|
||||
`group.ev.mana.wordeck`
|
||||
- `DeckListStore.refresh` ruft `updateWidgetSnapshot()` und
|
||||
`WidgetCenter.shared.reloadAllTimelines()` nach jedem Pull
|
||||
- `CardsWidgetExtension`-Target (eigenes app-extension-Bundle):
|
||||
`CardsWidgetBundle` + `CardsDueWidget` mit `StaticConfiguration`,
|
||||
- `WordeckWidgetExtension`-Target (eigenes app-extension-Bundle):
|
||||
`WordeckWidgetBundle` + `CardsDueWidget` mit `StaticConfiguration`,
|
||||
Support für systemSmall, systemMedium, accessoryCircular,
|
||||
accessoryInline, accessoryRectangular
|
||||
- `DueProvider` als `TimelineProvider`: liest Snapshot, plant
|
||||
Refresh alle 30 min (plus instant-Refresh via Haupt-App)
|
||||
- `DueWidgetView` mit Family-Switch, alle 5 Family-Layouts
|
||||
- `com.apple.security.application-groups: group.ev.mana.cardecky`
|
||||
- `com.apple.security.application-groups: group.ev.mana.wordeck`
|
||||
im Haupt- und Widget-Entitlement
|
||||
- `WidgetSnapshot.swift` in beiden Targets via XcodeGen-source-array
|
||||
(single-source-of-truth)
|
||||
|
|
@ -128,7 +128,7 @@ Widget decken 90% des Native-Polish ab.
|
|||
`PublicDeckDetail`, `ExploreResponse`, `BrowseResponse`,
|
||||
`SubscribeResponse` Codable-DTOs mit snake_case
|
||||
- `MarketplaceSort` Enum (recent/popular/trending) mit deutschen Labels
|
||||
- `CardsAPI`: explore(), browseMarketplace(query:sort:language:),
|
||||
- `WordeckAPI`: explore(), browseMarketplace(query:sort:language:),
|
||||
publicDeck(slug:), subscribe(slug:), unsubscribe(slug:)
|
||||
- `MarketplaceStore` @Observable mit Explore-State + Browse-State
|
||||
- `ExploreView` mit Featured + Trending Carousels, Browse-Link
|
||||
|
|
@ -138,13 +138,13 @@ Widget decken 90% des Native-Polish ab.
|
|||
- `PublicDeckCard` + `BrowseRow` Komponenten mit forest-Theme
|
||||
- `RootView` → TabBar (Decks / Entdecken / Account) statt Single-View
|
||||
- Universal-Link-Handler in `RootView` (onOpenURL + onContinueUserActivity):
|
||||
`https://cardecky.mana.how/d/<slug>` und `cards://d/<slug>` → Explore-Tab
|
||||
`https://wordeck.com/d/<slug>` und `cards://d/<slug>` → Explore-Tab
|
||||
öffnet `PublicDeckView`
|
||||
- `associated-domains: applinks:cardecky.mana.how` im entitlement
|
||||
- `associated-domains: applinks:wordeck.com` im entitlement
|
||||
- 5 neue Marketplace-Decoding-Tests (35 Total grün)
|
||||
|
||||
**Wichtig:** Universal-Links funktionieren erst, wenn AASA-Endpoint
|
||||
unter `cardecky.mana.how/.well-known/apple-app-site-association`
|
||||
unter `wordeck.com/.well-known/apple-app-site-association`
|
||||
ausgeliefert wird — heute 404. Web-seitige Aufgabe.
|
||||
|
||||
✅ **β-4 — Media + Advanced Card-Types (2026-05-13, Tag `v0.5.0`)**
|
||||
|
|
@ -154,7 +154,7 @@ ausgeliefert wird — heute 404. Web-seitige Aufgabe.
|
|||
nach ID lexikographisch)
|
||||
- `CardFieldsBuilder.imageOcclusion`, `.audioFront` mit korrekter
|
||||
`mask_regions`-Serialisierung als stringified JSON-Array
|
||||
- `CardsAPI.uploadMedia(data, filename, mimeType)` mit Multipart
|
||||
- `WordeckAPI.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
|
||||
|
|
@ -176,7 +176,7 @@ ausgeliefert wird — heute 404. Web-seitige Aufgabe.
|
|||
Encodable-Structs (snake_case via `CodingKeys`, nil-Felder werden
|
||||
weggelassen)
|
||||
- `CardFieldsBuilder` mit Type-spezifischen Pflicht-Feld-Konstruktoren
|
||||
- `CardsAPI`: createDeck/updateDeck/deleteDeck + createCard/updateCard/deleteCard
|
||||
- `WordeckAPI`: 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
|
||||
|
|
@ -195,7 +195,7 @@ ausgeliefert wird — heute 404. Web-seitige Aufgabe.
|
|||
- `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
|
||||
- `WordeckAPI.dueReviews(deckId:)`, `WordeckAPI.gradeReview(...)` mit
|
||||
ISO8601-Encoder
|
||||
- `PendingGrade` SwiftData-Model + `GradeQueue` für Offline-Submit
|
||||
(FIFO-Drain, originaler reviewedAt-Timestamp bleibt erhalten)
|
||||
|
|
@ -213,7 +213,7 @@ ausgeliefert wird — heute 404. Web-seitige Aufgabe.
|
|||
- `Deck`-Codable-DTO mit snake_case-CodingKeys, plus
|
||||
`DeckCategory`, `DeckVisibility`, `FsrsSettings`
|
||||
- ISO8601-Date-Decoder mit Fractional-Seconds-Toleranz
|
||||
- `CardsAPI.listDecks()`, `cardCount(deckId:)`, `dueCount(deckId:)`
|
||||
- `WordeckAPI.listDecks()`, `cardCount(deckId:)`, `dueCount(deckId:)`
|
||||
- `CachedDeck` als SwiftData-Model mit `lastFetchedAt` (Offline-Read)
|
||||
- `DeckListStore` orchestriert API + Cache, paralleles Counts-Fetching
|
||||
via TaskGroup
|
||||
|
|
@ -244,7 +244,7 @@ Cards-Web-Repo (AASA). Strukturierte Liste in
|
|||
|
||||
1. Apple-Developer-Konfiguration (Team-ID, App-IDs, App-Group, Profiles)
|
||||
2. App-Icon-Platzhalter durch Designer-Icon ersetzen
|
||||
3. AASA-Endpoint auf `cardecky.mana.how` (Cards-Web-Repo)
|
||||
3. AASA-Endpoint auf `wordeck.com` (Cards-Web-Repo)
|
||||
4. Xcode-Archive + TestFlight-Upload
|
||||
5. Endurance- und Cross-Device-Tests im TestFlight-Beta
|
||||
6. App-Store-Connect-Listing (Description, Screenshots, Privacy)
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -1,23 +1,23 @@
|
|||
# cards-native
|
||||
# wordeck-native
|
||||
|
||||
Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Cardecky —
|
||||
Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Wordeck —
|
||||
die Spaced-Repetition-Karten-App des Vereins **mana e.V.**
|
||||
|
||||
> **Web-App-Parität.** Die existierende Web-App auf
|
||||
> `cardecky.mana.how` ist Funktions- und Verhaltens-Referenz.
|
||||
> `wordeck.com` ist Funktions- und Verhaltens-Referenz.
|
||||
> Native bringt kein neues Produkt, sondern die App in einer Form,
|
||||
> die iOS-Hardware besser nutzt.
|
||||
|
||||
## Status
|
||||
|
||||
**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Cardecky-API-
|
||||
**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Wordeck-API-
|
||||
Reachability-Check. Vollständiger Phasen-Plan in
|
||||
`../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
||||
|
||||
```
|
||||
HTTPS/JWT ┌──────────────────┐
|
||||
cards-api ◄───────────── │ cards-native │ SwiftUI
|
||||
cardecky-api.mana.how │ ev.mana.cardecky │ WidgetKit (β-6)
|
||||
cards-api ◄───────────── │ wordeck-native │ SwiftUI
|
||||
api.wordeck.com │ ev.mana.wordeck │ WidgetKit (β-6)
|
||||
└──────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────┐
|
||||
|
|
@ -30,7 +30,7 @@ Reachability-Check. Vollständiger Phasen-Plan in
|
|||
|
||||
```bash
|
||||
xcodegen generate
|
||||
open CardsNative.xcodeproj # iPhone-17-Simulator
|
||||
open WordeckNative.xcodeproj # iPhone-17-Simulator
|
||||
```
|
||||
|
||||
Konventionen, Invarianten, Phasen-Disziplin: [`CLAUDE.md`](CLAUDE.md).
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ struct ShareEditorView: View {
|
|||
}
|
||||
}
|
||||
Section {
|
||||
Text("Wähle das Ziel-Deck in der Cardecky-App.")
|
||||
Text("Wähle das Ziel-Deck in der Wordeck-App.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ final class ShareViewController: UIViewController {
|
|||
|
||||
private func cancel() {
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.extensionContext?.cancelRequest(withError: NSError(domain: "ev.mana.cardecky.share", code: 0))
|
||||
self?.extensionContext?.cancelRequest(withError: NSError(domain: "ev.mana.wordeck.share", code: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ManaCore
|
|||
import SwiftUI
|
||||
|
||||
/// Top-Level-View: TabBar mit drei Tabs (Decks / Entdecken / Account).
|
||||
/// Kein harter Login-Gate mehr — Cardecky läuft auch im Guest-Modus
|
||||
/// Kein harter Login-Gate mehr — Wordeck läuft auch im Guest-Modus
|
||||
/// (lokale Decks lernen, Marketplace browsen). Schreibende Server-
|
||||
/// Aktionen werden über ``ManaAuthGate`` einzeln auf Login eskaliert.
|
||||
struct RootView: View {
|
||||
|
|
@ -16,8 +16,8 @@ struct RootView: View {
|
|||
@State private var showForgotSheet = false
|
||||
@State private var resetPasswordToken: String?
|
||||
|
||||
private let sourceAppUrl = URL(string: "https://cardecky.mana.how/auth/verify")!
|
||||
private let resetUniversalLink = URL(string: "https://cardecky.mana.how/auth/reset")!
|
||||
private let sourceAppUrl = URL(string: "https://wordeck.com/auth/verify")!
|
||||
private let resetUniversalLink = URL(string: "https://wordeck.com/auth/reset")!
|
||||
|
||||
var body: some View {
|
||||
mainTabs
|
||||
|
|
@ -25,7 +25,7 @@ struct RootView: View {
|
|||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
if let url = activity.webpageURL { handle(url: url) }
|
||||
}
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
.manaAuthGate(authGate) {
|
||||
gateSignInContent
|
||||
}
|
||||
|
|
@ -38,12 +38,12 @@ struct RootView: View {
|
|||
auth: auth,
|
||||
onDone: { resetPasswordToken = nil }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
.task {
|
||||
// DEBUG: Auto-Login mit DebugCredentials, falls signedOut.
|
||||
// Release: no-op. Danach in Guest-Mode wechseln, wenn weder
|
||||
// signedIn noch eingebuchtet — Cardecky soll *immer* nutzbar
|
||||
// signedIn noch eingebuchtet — Wordeck soll *immer* nutzbar
|
||||
// sein, auch ohne Account.
|
||||
await auth.ensureSignedIn()
|
||||
if case .signedOut = auth.status {
|
||||
|
|
@ -69,14 +69,14 @@ struct RootView: View {
|
|||
onSignUpTapped: { showSignUpSheet = true },
|
||||
onForgotTapped: { showForgotSheet = true }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
.sheet(isPresented: $showSignUpSheet) {
|
||||
ManaSignUpView(
|
||||
auth: auth,
|
||||
sourceAppUrl: sourceAppUrl,
|
||||
onDone: { showSignUpSheet = false }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
.sheet(isPresented: $showForgotSheet) {
|
||||
ManaForgotPasswordView(
|
||||
|
|
@ -84,7 +84,7 @@ struct RootView: View {
|
|||
resetUniversalLink: resetUniversalLink,
|
||||
onDone: { showForgotSheet = false }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,12 +113,12 @@ struct RootView: View {
|
|||
}
|
||||
|
||||
/// Universal-Link- und URL-Scheme-Handler:
|
||||
/// - `https://cardecky.mana.how/d/<slug>` → Explore-Tab + PublicDeckView
|
||||
/// - `https://cardecky.mana.how/auth/reset?token=…` → ManaResetPasswordView
|
||||
/// - `https://wordeck.com/d/<slug>` → Explore-Tab + PublicDeckView
|
||||
/// - `https://wordeck.com/auth/reset?token=…` → ManaResetPasswordView
|
||||
/// - `cards://study/<deckId>` → später (β-6 Notifications)
|
||||
private func handle(url: URL) {
|
||||
Log.app.info("Open URL: \(url.absoluteString, privacy: .public)")
|
||||
guard url.host == "cardecky.mana.how" || url.scheme == "cards" else { return }
|
||||
guard url.host == "wordeck.com" || url.scheme == "cards" else { return }
|
||||
|
||||
let parts = url.pathComponents.filter { $0 != "/" }
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ private struct DeckCreateAccessoryPill: View {
|
|||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
.buttonStyle(.glass)
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
.accessibilityLabel("Neues Deck erstellen")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import SwiftData
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct CardsNativeApp: App {
|
||||
struct WordeckNativeApp: App {
|
||||
let container: ModelContainer
|
||||
@State private var auth: AuthClient
|
||||
@State private var authGate: ManaAuthGate
|
||||
|
|
@ -20,8 +20,8 @@ struct CardsNativeApp: App {
|
|||
auth.bootstrap()
|
||||
_auth = State(initialValue: auth)
|
||||
_authGate = State(initialValue: ManaAuthGate(auth: auth))
|
||||
mediaCache = MediaCache(api: CardsAPI(auth: auth))
|
||||
Log.app.info("Cardecky starting — auth status: \(String(describing: auth.status), privacy: .public)")
|
||||
mediaCache = MediaCache(api: WordeckAPI(auth: auth))
|
||||
Log.app.info("Wordeck starting — auth status: \(String(describing: auth.status), privacy: .public)")
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
|
@ -30,7 +30,7 @@ struct CardsNativeApp: App {
|
|||
.environment(auth)
|
||||
.environment(authGate)
|
||||
.environment(\.mediaCache, mediaCache)
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
}
|
||||
.modelContainer(container)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
|
||||
/// AI-Deck-Generierung + Multipart-Helpers — ausgelagert aus `CardsAPI`,
|
||||
/// AI-Deck-Generierung + Multipart-Helpers — ausgelagert aus `WordeckAPI`,
|
||||
/// damit der Haupt-Actor unter der Type-Body-Length-Grenze bleibt.
|
||||
extension CardsAPI {
|
||||
extension WordeckAPI {
|
||||
/// `POST /api/v1/decks/generate` — KI generiert Deck aus Prompt.
|
||||
/// Rate-Limit serverseitig 10/min. Antwort dauert typisch 10–60s
|
||||
/// (synchron, kein Streaming).
|
||||
|
|
@ -28,7 +28,7 @@ extension CardsAPI {
|
|||
count: Int,
|
||||
url: String?
|
||||
) async throws -> DeckGenerateResponse {
|
||||
let boundary = "cards-native-\(UUID().uuidString)"
|
||||
let boundary = "wordeck-native-\(UUID().uuidString)"
|
||||
let body = makeFromImageMultipartBody(
|
||||
files: files,
|
||||
language: language,
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
|
||||
/// Marketplace-Moderation und Self-Endpoints — ausgelagert aus `CardsAPI`,
|
||||
/// Marketplace-Moderation und Self-Endpoints — ausgelagert aus `WordeckAPI`,
|
||||
/// damit der Haupt-Actor unter der Type-Body-Length-Grenze bleibt.
|
||||
///
|
||||
/// `transport`, `decoder`, `makeJSON`, `ensureOK` sind im Actor
|
||||
/// internal-zugänglich.
|
||||
extension CardsAPI {
|
||||
extension WordeckAPI {
|
||||
/// `GET /api/v1/marketplace/me/decks` — eigene Marketplace-Decks
|
||||
/// (mit aktueller Version) für den Re-Publish-Flow.
|
||||
func myMarketplaceDecks() async throws -> [OwnedMarketplaceDeck] {
|
||||
|
|
@ -5,10 +5,10 @@ import ManaCore
|
|||
// swiftlint:disable type_body_length
|
||||
|
||||
/// Cards-spezifischer API-Client. Wrapper um `AuthenticatedTransport`
|
||||
/// aus ManaCore, der die Cardecky-Endpoints kennt. Marketplace-Moderation
|
||||
/// + Self-Endpoints + AI-Generation sind in `CardsAPI+Marketplace.swift`
|
||||
/// und `CardsAPI+Generation.swift` ausgelagert.
|
||||
actor CardsAPI {
|
||||
/// aus ManaCore, der die Wordeck-Endpoints kennt. Marketplace-Moderation
|
||||
/// + Self-Endpoints + AI-Generation sind in `WordeckAPI+Marketplace.swift`
|
||||
/// und `WordeckAPI+Generation.swift` ausgelagert.
|
||||
actor WordeckAPI {
|
||||
let transport: AuthenticatedTransport
|
||||
let decoder: JSONDecoder
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ actor CardsAPI {
|
|||
decoder.dateDecodingStrategy = .iso8601withFractional
|
||||
}
|
||||
|
||||
/// Health-Probe — verifiziert dass cardecky-api erreichbar ist
|
||||
/// Health-Probe — verifiziert dass wordeck-api erreichbar ist
|
||||
/// und der eigene JWT akzeptiert wird.
|
||||
func healthCheck() async throws -> Bool {
|
||||
let (_, http) = try await transport.request(path: "/healthz")
|
||||
|
|
@ -153,7 +153,7 @@ actor CardsAPI {
|
|||
/// `POST /api/v1/media/upload` — Multipart-Upload. Max 25 MiB.
|
||||
/// Erlaubte MIMEs: image/*, audio/*, video/*.
|
||||
func uploadMedia(data: Data, filename: String, mimeType: String) async throws -> MediaUploadResponse {
|
||||
let boundary = "cards-native-\(UUID().uuidString)"
|
||||
let boundary = "wordeck-native-\(UUID().uuidString)"
|
||||
let body = makeMultipartBody(
|
||||
file: data,
|
||||
filename: filename,
|
||||
|
|
@ -2,19 +2,19 @@ import Foundation
|
|||
import ManaCore
|
||||
|
||||
/// App-spezifische Konfiguration für Cards. Implementiert `ManaAppConfig`
|
||||
/// aus ManaCore und ergänzt die Cards-eigene `apiBaseURL` (cardecky-api,
|
||||
/// aus ManaCore und ergänzt die Cards-eigene `apiBaseURL` (wordeck-api,
|
||||
/// getrennt von mana-auth).
|
||||
enum AppConfig {
|
||||
static let manaAppConfig: ManaAppConfig = DefaultManaAppConfig(
|
||||
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||
keychainService: "ev.mana.cardecky",
|
||||
keychainService: "ev.mana.wordeck",
|
||||
// Explizit auf TeamID.BundleID, statt nil. Vermeidet Logout
|
||||
// bei TestFlight-Cert-Drift (siehe mana-swift-core v1.5.1).
|
||||
keychainAccessGroup: "QP3GLU8PH3.ev.mana.cardecky",
|
||||
keychainAccessGroup: "QP3GLU8PH3.ev.mana.wordeck",
|
||||
// Single-Source fuer den App-Group-String (App-Hülle + Widget
|
||||
// + ShareExt-Entitlement spiegeln das hardcoded).
|
||||
appGroup: "group.ev.mana.cardecky"
|
||||
appGroup: "group.ev.mana.wordeck"
|
||||
)
|
||||
|
||||
static let apiBaseURL = URL(string: "https://cardecky-api.mana.how")!
|
||||
static let apiBaseURL = URL(string: "https://api.wordeck.com")!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct CSVRow: Equatable {
|
|||
/// wie Header-Tokens aussehen (`front`, `back`, `question`, `answer`,
|
||||
/// `vorderseite`, `rückseite` …).
|
||||
/// - BOM (`\u{FEFF}`) am Anfang wird gestrippt.
|
||||
/// - `type` darf jede Cardecky-Type-Bezeichnung sein; unbekannte Werte
|
||||
/// - `type` darf jede Wordeck-Type-Bezeichnung sein; unbekannte Werte
|
||||
/// landen als `.basic`.
|
||||
enum CSVParser {
|
||||
enum ParseError: LocalizedError {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ struct Deck: Codable, Identifiable, Hashable {
|
|||
case updatedAt = "updated_at"
|
||||
}
|
||||
|
||||
/// Geforkt aus dem Cardecky-Marketplace?
|
||||
/// Geforkt aus dem Wordeck-Marketplace?
|
||||
var isFromMarketplace: Bool {
|
||||
forkedFromMarketplaceDeckId != nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,20 +70,20 @@ struct MarketplacePublishResponse: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Liste von Cardecky-Marketplace-Lizenzen. Server akzeptiert beliebige
|
||||
/// Liste von Wordeck-Marketplace-Lizenzen. Server akzeptiert beliebige
|
||||
/// Strings ≤ 60 Zeichen — wir bieten die kanonischen vier.
|
||||
enum MarketplaceLicense: String, CaseIterable {
|
||||
case personalUse = "Cardecky-Personal-Use-1.0"
|
||||
case shareAlike = "Cardecky-Share-Alike-1.0"
|
||||
case attribution = "Cardecky-Attribution-1.0"
|
||||
case proOnly = "Cardecky-Pro-Only-1.0"
|
||||
case personalUse = "Wordeck-Personal-Use-1.0"
|
||||
case shareAlike = "Wordeck-Share-Alike-1.0"
|
||||
case attribution = "Wordeck-Attribution-1.0"
|
||||
case proOnly = "Wordeck-Pro-Only-1.0"
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .personalUse: "Persönlicher Gebrauch"
|
||||
case .shareAlike: "Share-Alike (CC-BY-SA-Stil)"
|
||||
case .attribution: "Namensnennung (CC-BY-Stil)"
|
||||
case .proOnly: "Nur für Cardecky-Pro (Bezahl-Decks)"
|
||||
case .proOnly: "Nur für Wordeck-Pro (Bezahl-Decks)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import SwiftUI
|
|||
struct StudyCardsIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Karten lernen"
|
||||
static let description = IntentDescription(
|
||||
"Öffnet Cardecky und zeigt deine Decks mit fälligen Karten.",
|
||||
"Öffnet Wordeck und zeigt deine Decks mit fälligen Karten.",
|
||||
categoryName: "Lernen"
|
||||
)
|
||||
static let openAppWhenRun: Bool = true
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ final class NotificationManager {
|
|||
}
|
||||
|
||||
private(set) var authorization: AuthorizationStatus = .unknown
|
||||
private let identifier = "ev.mana.cardecky.dailyReminder"
|
||||
private let identifier = "ev.mana.wordeck.dailyReminder"
|
||||
private let store = UserDefaults.standard
|
||||
|
||||
/// Persistiert User-Pref. Format: ISO-Stunde:Minute (default 18:00).
|
||||
|
|
@ -67,7 +67,7 @@ final class NotificationManager {
|
|||
guard remindersEnabled, authorization == .authorized else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Cardecky"
|
||||
content.title = "Wordeck"
|
||||
content.body = "Ein paar Karten warten auf dich."
|
||||
content.sound = .default
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ final class DeckListStore {
|
|||
private(set) var state: State = .idle
|
||||
private(set) var errorMessage: String?
|
||||
|
||||
private let api: CardsAPI
|
||||
private let api: WordeckAPI
|
||||
private let context: ModelContext
|
||||
private let auth: AuthClient
|
||||
|
||||
init(auth: AuthClient, context: ModelContext) {
|
||||
api = CardsAPI(auth: auth)
|
||||
api = WordeckAPI(auth: auth)
|
||||
self.context = context
|
||||
self.auth = auth
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ final class GradeQueue {
|
|||
private(set) var isDraining = false
|
||||
private(set) var lastDrainError: String?
|
||||
|
||||
private let api: CardsAPI
|
||||
private let api: WordeckAPI
|
||||
private let context: ModelContext
|
||||
|
||||
init(api: CardsAPI, context: ModelContext) {
|
||||
init(api: WordeckAPI, context: ModelContext) {
|
||||
self.api = api
|
||||
self.context = context
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import ManaCore
|
|||
/// LRU-Verdrängung mit Soft-Limit (Default 200 MB).
|
||||
actor MediaCache {
|
||||
private let root: URL
|
||||
private let api: CardsAPI
|
||||
private let api: WordeckAPI
|
||||
private let maxBytes: Int
|
||||
|
||||
init(api: CardsAPI, maxBytes: Int = 200 * 1024 * 1024) {
|
||||
init(api: WordeckAPI, maxBytes: Int = 200 * 1024 * 1024) {
|
||||
self.api = api
|
||||
self.maxBytes = maxBytes
|
||||
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ struct PendingShare: Codable, Identifiable, Hashable {
|
|||
}
|
||||
|
||||
enum PendingShareStore {
|
||||
static let appGroupID = "group.ev.mana.cardecky"
|
||||
static let appGroupID = "group.ev.mana.wordeck"
|
||||
static let filename = "pending-shares.json"
|
||||
|
||||
static var url: URL? {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct WidgetSnapshot: Codable {
|
|||
/// Liest und schreibt WidgetSnapshot in den shared App-Group-Container.
|
||||
enum WidgetSnapshotStore {
|
||||
/// App-Group-ID — muss exakt mit dem Entitlement-Eintrag matchen.
|
||||
static let appGroupID = "group.ev.mana.cardecky"
|
||||
static let appGroupID = "group.ev.mana.wordeck"
|
||||
static let snapshotFilename = "widget-snapshot.json"
|
||||
|
||||
static var snapshotURL: URL? {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import OSLog
|
|||
/// core v1.7.0+), app-spezifische über `mana.category(…)`.
|
||||
///
|
||||
/// Subsystem leitet sich aus `AppConfig.manaAppConfig.logSubsystem` ab
|
||||
/// — heute `ev.mana.cardecky`. ManaCore loggt parallel unter
|
||||
/// — heute `ev.mana.wordeck`. ManaCore loggt parallel unter
|
||||
/// `ev.mana.core`.
|
||||
enum Log {
|
||||
private static let mana = ManaAppLog(AppConfig.manaAppConfig)
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ struct CardSurface<Content: View>: View {
|
|||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(CardsTheme.surface)
|
||||
.fill(WordeckTheme.surface)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
|
||||
if let colorAccentHex {
|
||||
|
|
@ -87,7 +87,7 @@ struct CardSurface<Content: View>: View {
|
|||
}
|
||||
|
||||
private var shadowColor: Color {
|
||||
CardsTheme.foreground.opacity(elevation == .raised ? 0.18 : 0.08)
|
||||
WordeckTheme.foreground.opacity(elevation == .raised ? 0.18 : 0.08)
|
||||
}
|
||||
|
||||
private var shadowRadius: CGFloat {
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import ManaAuthUI
|
||||
|
||||
/// Brücke zwischen Cardeckys `CardsTheme` (HSL-Forest) und der
|
||||
/// `ManaBrandConfig` des `ManaAuthUI`-Paketes. Wird im RootView
|
||||
/// einmal als Environment-Wert gesetzt.
|
||||
///
|
||||
/// Wenn ManaTokens (mana-swift-core) später Theme-Variants liefert,
|
||||
/// kann diese Datei durch `ManaBrandConfig.forest(appName: "Cardecky", …)`
|
||||
/// ersetzt werden — siehe MANA_SWIFT.md Phase ε.
|
||||
enum CardsBrand {
|
||||
static let manaBrand = ManaBrandConfig(
|
||||
appName: "Cardecky",
|
||||
tagline: "Karteikarten des Vereins mana e.V.",
|
||||
logoSymbol: "rectangle.stack.fill",
|
||||
background: CardsTheme.background,
|
||||
foreground: CardsTheme.foreground,
|
||||
surface: CardsTheme.surface,
|
||||
mutedForeground: CardsTheme.mutedForeground,
|
||||
border: CardsTheme.border,
|
||||
primary: CardsTheme.primary,
|
||||
primaryForeground: CardsTheme.primaryForeground,
|
||||
error: CardsTheme.error,
|
||||
success: CardsTheme.success
|
||||
)
|
||||
}
|
||||
25
Sources/Core/Theme/WordeckBrand.swift
Normal file
25
Sources/Core/Theme/WordeckBrand.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import ManaAuthUI
|
||||
|
||||
/// Brücke zwischen Wordecks `WordeckTheme` (HSL-Forest) und der
|
||||
/// `ManaBrandConfig` des `ManaAuthUI`-Paketes. Wird im RootView
|
||||
/// einmal als Environment-Wert gesetzt.
|
||||
///
|
||||
/// Wenn ManaTokens (mana-swift-core) später Theme-Variants liefert,
|
||||
/// kann diese Datei durch `ManaBrandConfig.forest(appName: "Wordeck", …)`
|
||||
/// ersetzt werden — siehe MANA_SWIFT.md Phase ε.
|
||||
enum WordeckBrand {
|
||||
static let manaBrand = ManaBrandConfig(
|
||||
appName: "Wordeck",
|
||||
tagline: "Karteikarten des Vereins mana e.V.",
|
||||
logoSymbol: "rectangle.stack.fill",
|
||||
background: WordeckTheme.background,
|
||||
foreground: WordeckTheme.foreground,
|
||||
surface: WordeckTheme.surface,
|
||||
mutedForeground: WordeckTheme.mutedForeground,
|
||||
border: WordeckTheme.border,
|
||||
primary: WordeckTheme.primary,
|
||||
primaryForeground: WordeckTheme.primaryForeground,
|
||||
error: WordeckTheme.error,
|
||||
success: WordeckTheme.success
|
||||
)
|
||||
}
|
||||
|
|
@ -8,12 +8,12 @@ import SwiftUI
|
|||
/// der `forest.css`-Variant. Mit v1.6.0 liefert ManaTokens alle acht
|
||||
/// Web-Theme-Variants nativ — `forest` ist eine davon.
|
||||
///
|
||||
/// `CardsTheme` bleibt als dünner Alias bestehen, damit die ~290
|
||||
/// `WordeckTheme` bleibt als dünner Alias bestehen, damit die ~290
|
||||
/// Call-Sites in dieser App nicht in einem einzigen Sprint umziehen
|
||||
/// müssen. Neue Call-Sites bevorzugen direkt `ManaTheme.forest.<token>`
|
||||
/// (oder `@Environment(\.manaTheme)` falls die App irgendwann
|
||||
/// Theme-Switching bekommt).
|
||||
enum CardsTheme {
|
||||
enum WordeckTheme {
|
||||
static let background = ManaTheme.forest.background
|
||||
static let foreground = ManaTheme.forest.foreground
|
||||
static let surface = ManaTheme.forest.surface
|
||||
|
|
@ -11,7 +11,7 @@ struct AccountView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
Group {
|
||||
switch auth.status {
|
||||
case .signedIn:
|
||||
|
|
@ -19,7 +19,7 @@ struct AccountView: View {
|
|||
case .guest, .signedOut, .error, .unknown:
|
||||
guestContent
|
||||
case .signingIn, .twoFactorRequired:
|
||||
ProgressView().tint(CardsTheme.primary)
|
||||
ProgressView().tint(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,28 +27,28 @@ struct AccountView: View {
|
|||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
.sheet(isPresented: $showChangeEmail) {
|
||||
ManaChangeEmailView(
|
||||
auth: auth,
|
||||
callbackUniversalLink: URL(string: "https://cardecky.mana.how/auth/email-changed"),
|
||||
callbackUniversalLink: URL(string: "https://wordeck.com/auth/email-changed"),
|
||||
onDone: { showChangeEmail = false }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
.sheet(isPresented: $showChangePassword) {
|
||||
ManaChangePasswordView(
|
||||
auth: auth,
|
||||
onDone: { showChangePassword = false }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
.sheet(isPresented: $showDeleteAccount) {
|
||||
ManaDeleteAccountView(
|
||||
auth: auth,
|
||||
onDone: { showDeleteAccount = false }
|
||||
)
|
||||
.manaBrand(CardsBrand.manaBrand)
|
||||
.manaBrand(WordeckBrand.manaBrand)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,12 +57,12 @@ struct AccountView: View {
|
|||
Image(systemName: "person.crop.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
|
||||
if let email = auth.currentEmail {
|
||||
Text(email)
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
|
|
@ -86,10 +86,10 @@ struct AccountView: View {
|
|||
ManaTwoFactorAccountRow(auth: auth)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
|
@ -106,8 +106,8 @@ struct AccountView: View {
|
|||
Text("Abmelden")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(CardsTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.background(WordeckTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ struct AccountView: View {
|
|||
} label: {
|
||||
Text("Account löschen…")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
|
|
@ -130,12 +130,12 @@ struct AccountView: View {
|
|||
Image(systemName: "person.crop.circle.dashed")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("Du nutzt Cardecky anonym")
|
||||
Text("Du nutzt Wordeck anonym")
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text(
|
||||
"""
|
||||
Marketplace und lokale Decks funktionieren ohne Konto. \
|
||||
|
|
@ -144,7 +144,7 @@ struct AccountView: View {
|
|||
"""
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
|
@ -159,7 +159,7 @@ struct AccountView: View {
|
|||
Text("Anmelden / Konto erstellen")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -183,11 +183,11 @@ struct AccountView: View {
|
|||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,15 @@ struct DeckCoverTile<Footer: View>: View {
|
|||
ZStack {
|
||||
ForEach(Array(layers.enumerated()), id: \.offset) { _, layer in
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(CardsTheme.surface)
|
||||
.fill(WordeckTheme.surface)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
.opacity(layer.opacity)
|
||||
.rotationEffect(.degrees(layer.tilt))
|
||||
.offset(x: layer.dx, y: layer.dy)
|
||||
.shadow(color: CardsTheme.foreground.opacity(0.05), radius: 2, y: 1)
|
||||
.shadow(color: WordeckTheme.foreground.opacity(0.05), radius: 2, y: 1)
|
||||
}
|
||||
|
||||
CardSurface(size: .md, elevation: .standard, colorAccentHex: colorAccentHex) {
|
||||
|
|
@ -62,12 +62,12 @@ struct DeckCoverTile<Footer: View>: View {
|
|||
if isFeatured {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: category?.systemImageName ?? "rectangle.stack")
|
||||
.font(.title2)
|
||||
.foregroundStyle(CardsTheme.primary.opacity(0.85))
|
||||
.foregroundStyle(WordeckTheme.primary.opacity(0.85))
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
|
@ -75,13 +75,13 @@ struct DeckCoverTile<Footer: View>: View {
|
|||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.lineLimit(3)
|
||||
|
||||
if let description, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ struct DeckDetailView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
if let deck = decks.first {
|
||||
content(deck: deck)
|
||||
} else {
|
||||
ContentUnavailableView("Deck nicht gefunden", systemImage: "questionmark.folder")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
.navigationTitle(decks.first?.name ?? "")
|
||||
|
|
@ -160,7 +160,7 @@ struct DeckDetailView: View {
|
|||
if let deleteError {
|
||||
Text(deleteError)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
cardListSection
|
||||
|
|
@ -174,25 +174,25 @@ struct DeckDetailView: View {
|
|||
HStack {
|
||||
Text(deck.name)
|
||||
.font(.title.bold())
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if deck.isFromMarketplace {
|
||||
Image(systemName: "globe")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
if let description = deck.deckDescription, !description.isEmpty {
|
||||
Text(description)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
HStack(spacing: 16) {
|
||||
Label("\(deck.cardCount) Karten", systemImage: "rectangle.stack")
|
||||
if deck.dueCount > 0 {
|
||||
Label("\(deck.dueCount) fällig", systemImage: "clock.badge.exclamationmark")
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
if let category = deck.category {
|
||||
Text(category.label)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
.font(.footnote)
|
||||
|
|
@ -216,8 +216,8 @@ struct DeckDetailView: View {
|
|||
Label("Karten lernen", systemImage: "play.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled((decks.first?.dueCount ?? 0) == 0)
|
||||
|
|
@ -228,11 +228,11 @@ struct DeckDetailView: View {
|
|||
Label("Karte hinzufügen", systemImage: "plus.rectangle.on.rectangle")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -257,12 +257,12 @@ struct DeckDetailView: View {
|
|||
HStack {
|
||||
Text("Karten")
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Spacer()
|
||||
if !cards.isEmpty {
|
||||
Text("\(cards.count)")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
|
@ -272,19 +272,19 @@ struct DeckDetailView: View {
|
|||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
} else if let cardsError {
|
||||
Text(cardsError)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
.padding(.horizontal, 16)
|
||||
} else if cards.isEmpty {
|
||||
Text("Noch keine Karten. Tippe auf »Karte hinzufügen«.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
} else {
|
||||
|
|
@ -312,7 +312,7 @@ struct DeckDetailView: View {
|
|||
private func pullUpdate() async {
|
||||
isPullingUpdate = true
|
||||
defer { isPullingUpdate = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
let result = try await api.pullUpdate(deckId: deckId)
|
||||
pullAlert = formatPullResult(result)
|
||||
|
|
@ -346,7 +346,7 @@ struct DeckDetailView: View {
|
|||
private func duplicate() async {
|
||||
isDuplicating = true
|
||||
defer { isDuplicating = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
_ = try await api.duplicateDeck(id: deckId)
|
||||
await refreshAfterEdit()
|
||||
|
|
@ -362,7 +362,7 @@ struct DeckDetailView: View {
|
|||
isLoadingCards = true
|
||||
cardsError = nil
|
||||
defer { isLoadingCards = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
cards = try await api.listCards(deckId: deckId)
|
||||
.sorted { $0.createdAt > $1.createdAt }
|
||||
|
|
@ -373,7 +373,7 @@ struct DeckDetailView: View {
|
|||
|
||||
private func delete() async {
|
||||
deleteError = nil
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
try await api.deleteDeck(id: deckId)
|
||||
if let deck = decks.first {
|
||||
|
|
@ -403,25 +403,25 @@ private struct CardPreviewRow: View {
|
|||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: icon(for: card.type))
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
.frame(width: 24)
|
||||
.padding(.top, 2)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(preview(card: card))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.lineLimit(2)
|
||||
Text(typeLabel(card.type))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(12)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ enum DeckRoute: Hashable {
|
|||
|
||||
// swiftlint:disable type_body_length
|
||||
|
||||
/// Decks-Hauptbildschirm im Cardecky-Look: horizontale Scroll-Reihen
|
||||
/// Decks-Hauptbildschirm im Wordeck-Look: horizontale Scroll-Reihen
|
||||
/// mit Fan-Stack-Karten-Tiles. Web-Vorbild:
|
||||
/// `cards/apps/web/src/routes/decks/+page.svelte`.
|
||||
struct DeckListView: View {
|
||||
|
|
@ -35,7 +35,7 @@ struct DeckListView: View {
|
|||
var body: some View {
|
||||
NavigationStack(path: $path) {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
content
|
||||
}
|
||||
.navigationTitle("Decks")
|
||||
|
|
@ -113,13 +113,13 @@ struct DeckListView: View {
|
|||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text("\(decks.count)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
|
|
@ -152,28 +152,28 @@ struct DeckListView: View {
|
|||
HStack(spacing: 12) {
|
||||
Image(systemName: "tray.full.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Inbox")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text("\(inbox.dueCount) fällige Karten aus abonnierten Decks")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
CardsTheme.primary.opacity(0.08),
|
||||
WordeckTheme.primary.opacity(0.08),
|
||||
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(CardsTheme.primary.opacity(0.18), lineWidth: 1)
|
||||
.stroke(WordeckTheme.primary.opacity(0.18), lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
|
@ -189,24 +189,24 @@ struct DeckListView: View {
|
|||
NavigationLink(value: PendingShareRoute(share: share)) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Aus Teilen-Menü")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text(share.text)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
CardsTheme.warning.opacity(0.12),
|
||||
WordeckTheme.warning.opacity(0.12),
|
||||
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
)
|
||||
}
|
||||
|
|
@ -221,21 +221,21 @@ struct DeckListView: View {
|
|||
VStack(spacing: 16) {
|
||||
if store?.state == .loading {
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
Text("Lade Decks …")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else if let message = store?.errorMessage {
|
||||
ContentUnavailableView {
|
||||
Label("Decks konnten nicht geladen werden", systemImage: "wifi.exclamationmark")
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
} description: {
|
||||
Text(message)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
} else if isGuest {
|
||||
ContentUnavailableView {
|
||||
Label("Cardecky ohne Konto", systemImage: "person.crop.circle.dashed")
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
Label("Wordeck ohne Konto", systemImage: "person.crop.circle.dashed")
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
} description: {
|
||||
Text(
|
||||
"""
|
||||
|
|
@ -243,18 +243,18 @@ struct DeckListView: View {
|
|||
nötig. Für eigene Decks und Cloud-Sync logge dich ein.
|
||||
"""
|
||||
)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} actions: {
|
||||
Button("Anmelden / Konto erstellen") {
|
||||
authGate.require(reason: "deck-list-empty") {}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView {
|
||||
Label("Noch keine Decks", systemImage: "rectangle.stack")
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
} description: {
|
||||
Text(
|
||||
"""
|
||||
|
|
@ -262,7 +262,7 @@ struct DeckListView: View {
|
|||
oder browse den Marketplace im Entdecken-Tab.
|
||||
"""
|
||||
)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ struct DeckListView: View {
|
|||
} label: {
|
||||
Label("Deck hinzufügen", systemImage: "plus")
|
||||
.labelStyle(.iconOnly)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
.accessibilityLabel("Deck hinzufügen")
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
/// „In Dateien speichern" → PDF von Haus aus mitbringt — kein eigener
|
||||
/// PDF-Renderer in der App nötig.
|
||||
///
|
||||
/// Auth-Cookies für `cardecky.mana.how` werden geteilt mit Safari auf
|
||||
/// Auth-Cookies für `wordeck.com` werden geteilt mit Safari auf
|
||||
/// dem Gerät; der User muss dort eingeloggt sein, damit die Print-
|
||||
/// Seite den Deck-Inhalt rendert.
|
||||
struct DeckPrintView: View {
|
||||
|
|
@ -19,7 +19,7 @@ struct DeckPrintView: View {
|
|||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private var printURL: URL {
|
||||
URL(string: "https://cardecky.mana.how/decks/\(deckId)/print")!
|
||||
URL(string: "https://wordeck.com/decks/\(deckId)/print")!
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ struct DeckSecondaryActions: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -54,11 +54,11 @@ struct DeckSecondaryActions: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.primary.opacity(0.3), lineWidth: 1)
|
||||
.stroke(WordeckTheme.primary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -68,7 +68,7 @@ struct DeckSecondaryActions: View {
|
|||
Button(action: onPullUpdate) {
|
||||
HStack {
|
||||
if isPullingUpdate {
|
||||
ProgressView().tint(CardsTheme.primary)
|
||||
ProgressView().tint(WordeckTheme.primary)
|
||||
} else {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
|
|
@ -76,11 +76,11 @@ struct DeckSecondaryActions: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.primary.opacity(0.3), lineWidth: 1)
|
||||
.stroke(WordeckTheme.primary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -91,7 +91,7 @@ struct DeckSecondaryActions: View {
|
|||
Button(action: onDuplicate) {
|
||||
HStack {
|
||||
if isDuplicating {
|
||||
ProgressView().tint(CardsTheme.foreground)
|
||||
ProgressView().tint(WordeckTheme.foreground)
|
||||
} else {
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
|
|
@ -99,11 +99,11 @@ struct DeckSecondaryActions: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -116,11 +116,11 @@ struct DeckSecondaryActions: View {
|
|||
Label("Bearbeiten", systemImage: "pencil")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
@ -129,8 +129,8 @@ struct DeckSecondaryActions: View {
|
|||
Label("Löschen", systemImage: "trash")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.background(WordeckTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ struct DeckStackTile: View {
|
|||
HStack(spacing: 8) {
|
||||
Label("\(deck.cardCount)", systemImage: "rectangle.stack")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
if deck.dueCount > 0 {
|
||||
Text("\(deck.dueCount) fällig")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.background(CardsTheme.primary.opacity(0.15), in: Capsule())
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.background(WordeckTheme.primary.opacity(0.15), in: Capsule())
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
if deck.isFromMarketplace {
|
||||
Image(systemName: "globe")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Spacer()
|
||||
editButton
|
||||
|
|
@ -54,11 +54,11 @@ struct DeckStackTile: View {
|
|||
} label: {
|
||||
Image(systemName: "pencil")
|
||||
.font(.footnote.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(CardsTheme.muted.opacity(0.7), in: Circle())
|
||||
.background(WordeckTheme.muted.opacity(0.7), in: Circle())
|
||||
.overlay(
|
||||
Circle().stroke(CardsTheme.border, lineWidth: 1)
|
||||
Circle().stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ struct PendingShareConsumeView: View {
|
|||
Section("Ziel-Deck") {
|
||||
if decks.isEmpty {
|
||||
Text("Erst ein Deck erstellen.")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else {
|
||||
Picker("Deck", selection: $selectedDeckId) {
|
||||
Text("Wählen …").tag(String?.none)
|
||||
|
|
@ -55,14 +55,14 @@ struct PendingShareConsumeView: View {
|
|||
Section("Quelle") {
|
||||
Text(sourceURL)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
if let errorMessage {
|
||||
Section {
|
||||
Text(errorMessage)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ struct PendingShareConsumeView: View {
|
|||
defer { isSubmitting = false }
|
||||
|
||||
let backText = back.trimmed.isEmpty ? (share.sourceURL ?? "—") : back.trimmed
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
let body = CardCreateBody(
|
||||
deckId: deckId,
|
||||
type: .basic,
|
||||
|
|
|
|||
|
|
@ -48,15 +48,15 @@ struct CSVImportFormSections: View {
|
|||
Text(row.front)
|
||||
.font(.subheadline)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text(row.back)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
if row.type != .basic {
|
||||
Text(typeLabel(row.type))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
|
@ -64,7 +64,7 @@ struct CSVImportFormSections: View {
|
|||
if rows.count > visible.count {
|
||||
Text("… und \(rows.count - visible.count) weitere")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,18 +54,18 @@ struct ImageOcclusionFields: View {
|
|||
if image == nil {
|
||||
Label("Erst Bild wählen", systemImage: "info.circle")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else if regions.isEmpty {
|
||||
Label("Mindestens eine Maske nötig", systemImage: "exclamationmark.circle")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
} else {
|
||||
Label(
|
||||
"\(regions.count) Masken → \(regions.count) Reviews",
|
||||
systemImage: "checkmark.circle.fill"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.success)
|
||||
.foregroundStyle(WordeckTheme.success)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
|
||||
/// Resultat von `CardEditorPayload.build` — was an `CardsAPI.createCard`
|
||||
/// Resultat von `CardEditorPayload.build` — was an `WordeckAPI.createCard`
|
||||
/// oder `updateCard` durchgereicht wird.
|
||||
struct CardEditorPayload {
|
||||
let fields: [String: String]
|
||||
|
|
@ -44,7 +44,7 @@ enum CardEditorPayloadBuilder {
|
|||
/// Baut den Payload für `POST /cards` bzw. `PATCH /cards/:id`.
|
||||
/// Lädt für Image-Occlusion / Audio-Front bei Bedarf neue Media
|
||||
/// hoch; sonst wird der bestehende `*_ref` aus der Card weiterverwendet.
|
||||
static func build(inputs: CardEditorPayloadInputs, api: CardsAPI) async throws -> CardEditorPayload {
|
||||
static func build(inputs: CardEditorPayloadInputs, api: WordeckAPI) async throws -> CardEditorPayload {
|
||||
switch inputs.type {
|
||||
case .basic, .basicReverse:
|
||||
CardEditorPayload(
|
||||
|
|
@ -78,7 +78,7 @@ enum CardEditorPayloadBuilder {
|
|||
|
||||
private static func buildImageOcclusionPayload(
|
||||
inputs: CardEditorPayloadInputs,
|
||||
api: CardsAPI
|
||||
api: WordeckAPI
|
||||
) async throws -> CardEditorPayload {
|
||||
let imageRef: String
|
||||
var refs = inputs.existingMediaRefs
|
||||
|
|
@ -109,7 +109,7 @@ enum CardEditorPayloadBuilder {
|
|||
|
||||
private static func buildAudioFrontPayload(
|
||||
inputs: CardEditorPayloadInputs,
|
||||
api: CardsAPI
|
||||
api: WordeckAPI
|
||||
) async throws -> CardEditorPayload {
|
||||
let audioRef: String
|
||||
var refs = inputs.existingMediaRefs
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ struct CardEditorView: View {
|
|||
Section {
|
||||
Text(errorMessage)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ struct CardEditorView: View {
|
|||
Section {
|
||||
Text("Beide Richtungen werden gelernt — front→back und back→front.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,15 +188,15 @@ struct CardEditorView: View {
|
|||
if count > 0 {
|
||||
Label("\(count) Lücken erkannt → \(count) Reviews", systemImage: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.success)
|
||||
.foregroundStyle(WordeckTheme.success)
|
||||
} else {
|
||||
Label("Mindestens ein Cluster `{{c1::...}}` erforderlich", systemImage: "exclamationmark.circle")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
}
|
||||
Text("Mit Hint: `{{c1::Berlin::Hauptstadt von DE}}`")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
|
||||
case .typing:
|
||||
|
|
@ -219,7 +219,7 @@ struct CardEditorView: View {
|
|||
Section {
|
||||
Text("Distractor-Optionen werden zur Lernzeit automatisch aus anderen Karten desselben Decks gezogen.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
|
||||
case .imageOcclusion:
|
||||
|
|
@ -282,7 +282,7 @@ struct CardEditorView: View {
|
|||
isSubmitting = true
|
||||
errorMessage = nil
|
||||
defer { isSubmitting = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
|
||||
do {
|
||||
let payload = try await CardEditorPayloadBuilder.build(inputs: payloadInputs, api: api)
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ struct DeckEditorView: View {
|
|||
Section {
|
||||
Text(errorMessage)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -346,7 +346,7 @@ struct DeckEditorView: View {
|
|||
}
|
||||
|
||||
private func submit() async {
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
switch (mode, activeMode) {
|
||||
case (.create, .manual):
|
||||
|
|
@ -407,7 +407,7 @@ struct DeckEditorView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private func submitCSVImport(api: CardsAPI) async throws -> Deck {
|
||||
private func submitCSVImport(api: WordeckAPI) async throws -> Deck {
|
||||
let deck = try await api.createDeck(DeckCreateBody(
|
||||
name: csvDeckName.trimmingCharacters(in: .whitespaces),
|
||||
description: "Aus CSV-Import (\(csvRows.count) Karten)",
|
||||
|
|
@ -530,7 +530,7 @@ private struct ColorSwatchButton: View {
|
|||
.frame(width: 36, height: 36)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(isSelected ? CardsTheme.foreground : CardsTheme.border, lineWidth: isSelected ? 3 : 1)
|
||||
.stroke(isSelected ? WordeckTheme.foreground : WordeckTheme.border, lineWidth: isSelected ? 3 : 1)
|
||||
)
|
||||
.onTapGesture(perform: onTap)
|
||||
}
|
||||
|
|
@ -617,12 +617,12 @@ private struct MediaFileRow: View {
|
|||
.lineLimit(1)
|
||||
Text(file.sizeLabel)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Spacer()
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Entfernen")
|
||||
|
|
@ -633,9 +633,9 @@ private struct MediaFileRow: View {
|
|||
private var thumbnail: some View {
|
||||
if file.isPDF {
|
||||
ZStack {
|
||||
CardsTheme.muted
|
||||
WordeckTheme.muted
|
||||
Image(systemName: "doc.text.fill")
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
} else if let img = PlatformImage(data: file.data) {
|
||||
#if canImport(UIKit)
|
||||
|
|
@ -648,7 +648,7 @@ private struct MediaFileRow: View {
|
|||
.scaledToFill()
|
||||
#endif
|
||||
} else {
|
||||
CardsTheme.muted
|
||||
WordeckTheme.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -702,18 +702,18 @@ private struct GenerationOverlay: View {
|
|||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
Text(message)
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.multilineTextAlignment(.center)
|
||||
Text("Das kann eine Weile dauern.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
Button("Abbrechen", action: onCancel)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(CardsTheme.mutedForeground)
|
||||
.tint(WordeckTheme.mutedForeground)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(24)
|
||||
|
|
@ -731,7 +731,7 @@ extension 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
|
||||
return WordeckTheme.primary
|
||||
}
|
||||
let red = Double((rgb >> 16) & 0xFF) / 255.0
|
||||
let green = Double((rgb >> 8) & 0xFF) / 255.0
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct MaskEditorView: View {
|
|||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Tippe und ziehe auf das Bild, um eine Maske zu erstellen.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
|
||||
imageCanvas
|
||||
.aspectRatio(image.size.width / max(image.size.height, 1), contentMode: .fit)
|
||||
|
|
@ -30,7 +30,7 @@ struct MaskEditorView: View {
|
|||
if regions.isEmpty {
|
||||
Text("Noch keine Maske")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else {
|
||||
ForEach(regions) { region in
|
||||
maskRow(region: region)
|
||||
|
|
@ -55,8 +55,8 @@ struct MaskEditorView: View {
|
|||
if let dragStart, let dragEnd {
|
||||
let rect = normalizedRect(from: dragStart, to: dragEnd)
|
||||
Rectangle()
|
||||
.stroke(CardsTheme.warning, lineWidth: 2)
|
||||
.background(Rectangle().fill(CardsTheme.warning.opacity(0.2)))
|
||||
.stroke(WordeckTheme.warning, lineWidth: 2)
|
||||
.background(Rectangle().fill(WordeckTheme.warning.opacity(0.2)))
|
||||
.frame(width: rect.width, height: rect.height)
|
||||
.offset(x: rect.minX, y: rect.minY)
|
||||
}
|
||||
|
|
@ -77,13 +77,13 @@ struct MaskEditorView: View {
|
|||
|
||||
private func overlayRect(for region: MaskRegion, in size: CGSize) -> some View {
|
||||
Rectangle()
|
||||
.fill(CardsTheme.primary.opacity(0.6))
|
||||
.fill(WordeckTheme.primary.opacity(0.6))
|
||||
.frame(width: region.w * size.width, height: region.h * size.height)
|
||||
.offset(x: region.x * size.width, y: region.y * size.height)
|
||||
.overlay(
|
||||
Text(region.label?.isEmpty == false ? region.label! : region.id)
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
.padding(2)
|
||||
.offset(x: region.x * size.width + 2, y: region.y * size.height + 2),
|
||||
alignment: .topLeading
|
||||
|
|
@ -93,7 +93,7 @@ struct MaskEditorView: View {
|
|||
private func maskRow(region: MaskRegion) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "square.dashed")
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
TextField("Label (optional)", text: Binding(
|
||||
get: { region.label ?? "" },
|
||||
set: { newValue in updateLabel(for: region.id, to: newValue) }
|
||||
|
|
@ -103,7 +103,7 @@ struct MaskEditorView: View {
|
|||
regions.removeAll { $0.id == region.id }
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ struct BrowseView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
filters
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
resultsList
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ struct BrowseView: View {
|
|||
if store.isLoadingBrowse, store.browseResults.isEmpty {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
Spacer()
|
||||
} else if store.browseResults.isEmpty {
|
||||
ContentUnavailableView(
|
||||
|
|
@ -73,7 +73,7 @@ struct BrowseView: View {
|
|||
systemImage: "magnifyingglass",
|
||||
description: Text("Versuche eine andere Suche oder Sortierung.")
|
||||
)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
} else {
|
||||
List {
|
||||
ForEach(store.browseResults) { entry in
|
||||
|
|
@ -104,17 +104,17 @@ struct BrowseRow: View {
|
|||
HStack {
|
||||
Text(entry.title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if entry.isFeatured {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
}
|
||||
}
|
||||
if let description = entry.description, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.lineLimit(2)
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
|
|
@ -122,23 +122,23 @@ struct BrowseRow: View {
|
|||
Label("\(entry.starCount)", systemImage: "star")
|
||||
if entry.isPaid {
|
||||
Label("\(entry.priceCredits)", systemImage: "creditcard")
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
if let language = entry.language {
|
||||
Text(language.uppercased())
|
||||
.font(.caption2.weight(.semibold))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 1)
|
||||
.background(CardsTheme.muted, in: Capsule())
|
||||
.background(WordeckTheme.muted, in: Capsule())
|
||||
}
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct ExploreView: View {
|
|||
var body: some View {
|
||||
NavigationStack(path: $path) {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
content
|
||||
}
|
||||
.navigationTitle("Entdecken")
|
||||
|
|
@ -56,14 +56,14 @@ struct ExploreView: View {
|
|||
if let store {
|
||||
if store.isLoadingExplore, store.featured.isEmpty, store.trending.isEmpty {
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
} else if let message = store.errorMessage, store.featured.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"Marketplace nicht erreichbar",
|
||||
systemImage: "wifi.exclamationmark",
|
||||
description: Text(message)
|
||||
)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
|
|
@ -82,12 +82,12 @@ struct ExploreView: View {
|
|||
.font(.footnote)
|
||||
}
|
||||
.padding()
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 20)
|
||||
|
|
@ -102,13 +102,13 @@ struct ExploreView: View {
|
|||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text("\(items.count)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
|
|
@ -166,26 +166,26 @@ struct PublicDeckCard: View {
|
|||
HStack(spacing: 8) {
|
||||
Label("\(entry.cardCount)", systemImage: "rectangle.stack")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
Label("\(entry.starCount)", systemImage: "star.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
if entry.isPaid {
|
||||
Label("\(entry.priceCredits)", systemImage: "creditcard")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
Text(entry.owner.displayName)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.lineLimit(1)
|
||||
if entry.owner.verifiedMana {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||
// swiftlint:disable file_length
|
||||
// swiftlint:disable type_body_length
|
||||
|
||||
/// Publish eines privaten Decks in den Cardecky-Marketplace.
|
||||
/// Publish eines privaten Decks in den Wordeck-Marketplace.
|
||||
///
|
||||
/// Modi: Erst-Publish (mit Author-Setup + Init + Publish 1.0.0) oder
|
||||
/// neue Version eines existierenden Marketplace-Decks (Auto-Semver-Bump).
|
||||
|
|
@ -79,7 +79,7 @@ struct MarketplacePublishView: View {
|
|||
Section {
|
||||
Text(errorMessage)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ struct MarketplacePublishView: View {
|
|||
} header: {
|
||||
Text("Deck-Metadaten")
|
||||
} footer: {
|
||||
Text("Der Slug wird Teil der Marketplace-URL: cardecky.mana.how/d/<slug>.")
|
||||
Text("Der Slug wird Teil der Marketplace-URL: wordeck.com/d/<slug>.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ struct MarketplacePublishView: View {
|
|||
systemImage: "info.circle"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,13 +260,13 @@ struct MarketplacePublishView: View {
|
|||
ZStack {
|
||||
Color.black.opacity(0.55).ignoresSafeArea()
|
||||
VStack(spacing: 12) {
|
||||
ProgressView().controlSize(.large).tint(CardsTheme.primary)
|
||||
ProgressView().controlSize(.large).tint(WordeckTheme.primary)
|
||||
Text("Wird veröffentlicht …")
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text("AI-Moderation läuft — kann ein paar Sekunden dauern.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(24)
|
||||
|
|
@ -295,7 +295,7 @@ struct MarketplacePublishView: View {
|
|||
deckDescription = privateDeck.deckDescription ?? ""
|
||||
category = privateDeck.category
|
||||
slug = slugify(privateDeck.name)
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
async let authorState = api.myAuthor()
|
||||
async let ownedState = api.myMarketplaceDecks()
|
||||
do {
|
||||
|
|
@ -340,7 +340,7 @@ struct MarketplacePublishView: View {
|
|||
isSubmitting = true
|
||||
errorMessage = nil
|
||||
defer { isSubmitting = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
let targetSlug = try await prepareTargetSlug(api: api)
|
||||
try await publishCards(toSlug: targetSlug, api: api)
|
||||
|
|
@ -353,7 +353,7 @@ struct MarketplacePublishView: View {
|
|||
|
||||
/// Erst-Publish-Pfad: Author-Profil + Marketplace-Deck-Init.
|
||||
/// Liefert den Slug auf den `publishCards` veröffentlicht.
|
||||
private func prepareTargetSlug(api: CardsAPI) async throws -> String {
|
||||
private func prepareTargetSlug(api: WordeckAPI) async throws -> String {
|
||||
switch publishMode {
|
||||
case .firstPublish:
|
||||
if hasAuthor == false {
|
||||
|
|
@ -383,7 +383,7 @@ struct MarketplacePublishView: View {
|
|||
|
||||
/// Lädt alle Karten des privaten Decks, konvertiert in Marketplace-
|
||||
/// Format und veröffentlicht die neue Version.
|
||||
private func publishCards(toSlug targetSlug: String, api: CardsAPI) async throws {
|
||||
private func publishCards(toSlug targetSlug: String, api: WordeckAPI) async throws {
|
||||
let cards = try await api.listCards(deckId: privateDeck.id)
|
||||
let converted = cards.compactMap(MarketplaceCardConverter.convert)
|
||||
skippedCardCount = cards.count - converted.count
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ final class MarketplaceStore {
|
|||
var browseSort: MarketplaceSort = .recent
|
||||
var browseLanguage: String?
|
||||
|
||||
private let api: CardsAPI
|
||||
private let api: WordeckAPI
|
||||
|
||||
init(auth: AuthClient) {
|
||||
api = CardsAPI(auth: auth)
|
||||
api = WordeckAPI(auth: auth)
|
||||
}
|
||||
|
||||
func loadExplore() async {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ struct PublicDeckView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
content
|
||||
}
|
||||
.navigationTitle(detail?.deck.title ?? "Deck")
|
||||
|
|
@ -103,19 +103,19 @@ struct PublicDeckView: View {
|
|||
private var content: some View {
|
||||
if isLoading, detail == nil {
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
} else if let detail {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
header(detail: detail)
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
metadata(detail: detail)
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
subscribeSection(detail: detail)
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ struct PublicDeckView: View {
|
|||
systemImage: "questionmark.folder",
|
||||
description: Text(errorMessage)
|
||||
)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,15 +136,15 @@ struct PublicDeckView: View {
|
|||
HStack {
|
||||
Text(detail.deck.title)
|
||||
.font(.title.bold())
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if detail.deck.isFeatured {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
}
|
||||
}
|
||||
if let description = detail.deck.description, !description.isEmpty {
|
||||
Text(description)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
|
@ -155,13 +155,13 @@ struct PublicDeckView: View {
|
|||
if let owner = detail.owner {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "person.crop.circle")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
Text(owner.displayName)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if owner.verifiedMana {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
|
@ -177,16 +177,16 @@ struct PublicDeckView: View {
|
|||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
|
||||
if let changelog = detail.latestVersion?.changelog, !changelog.isEmpty {
|
||||
Text("Changelog")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.padding(.top, 8)
|
||||
Text(changelog)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
|
@ -196,24 +196,24 @@ struct PublicDeckView: View {
|
|||
VStack(spacing: 12) {
|
||||
if let subscribed {
|
||||
Label("Abonniert — dein Fork ist in deiner Bibliothek", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(CardsTheme.success)
|
||||
.foregroundStyle(WordeckTheme.success)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(CardsTheme.success.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.background(WordeckTheme.success.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
NavigationLink(value: subscribed.privateDeckId) {
|
||||
Label("Zum eigenen Deck", systemImage: "arrow.right.circle")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else if detail.deck.isTakedown {
|
||||
Label("Dieses Deck wurde entfernt", systemImage: "exclamationmark.triangle")
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
} else if detail.deck.latestVersionId == nil {
|
||||
Label("Noch keine Version veröffentlicht", systemImage: "clock")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else {
|
||||
Button {
|
||||
authGate.require(reason: "marketplace-subscribe") {
|
||||
|
|
@ -224,7 +224,7 @@ struct PublicDeckView: View {
|
|||
if isSubscribing {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.tint(CardsTheme.primaryForeground)
|
||||
.tint(WordeckTheme.primaryForeground)
|
||||
}
|
||||
Text(detail.deck.priceCredits > 0
|
||||
? "Abonnieren (\(detail.deck.priceCredits) Credits)"
|
||||
|
|
@ -233,8 +233,8 @@ struct PublicDeckView: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isSubscribing)
|
||||
|
|
@ -246,7 +246,7 @@ struct PublicDeckView: View {
|
|||
private func load() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
detail = try await api.publicDeck(slug: slug)
|
||||
} catch {
|
||||
|
|
@ -255,7 +255,7 @@ struct PublicDeckView: View {
|
|||
}
|
||||
|
||||
private func blockAuthor(slug: String, name: String) async {
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
try await api.blockAuthor(slug: slug)
|
||||
moderationToast = "\(name) blockiert."
|
||||
|
|
@ -268,7 +268,7 @@ struct PublicDeckView: View {
|
|||
isSubscribing = true
|
||||
errorMessage = nil
|
||||
defer { isSubscribing = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
let response = try await api.subscribe(slug: slug)
|
||||
subscribed = response
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ struct ReportDeckSheet: View {
|
|||
Section {
|
||||
Text(errorMessage)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ struct ReportDeckSheet: View {
|
|||
isSubmitting = true
|
||||
errorMessage = nil
|
||||
defer { isSubmitting = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
do {
|
||||
let response = try await api.reportDeck(
|
||||
|
|
@ -98,11 +98,11 @@ struct ToastBanner: View {
|
|||
var body: some View {
|
||||
Text(text)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.background(.regularMaterial, in: Capsule())
|
||||
.overlay(Capsule().stroke(CardsTheme.border, lineWidth: 0.5))
|
||||
.overlay(Capsule().stroke(WordeckTheme.border, lineWidth: 0.5))
|
||||
.padding(.horizontal, 16)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,17 @@ struct AudioPlayerButton: View {
|
|||
? "speaker.slash.fill"
|
||||
: (isPlaying ? "pause.circle.fill" : "play.circle.fill"))
|
||||
.font(.system(size: 48))
|
||||
.foregroundStyle(failed ? CardsTheme.error : CardsTheme.primary)
|
||||
.foregroundStyle(failed ? WordeckTheme.error : WordeckTheme.primary)
|
||||
Text(failed ? "Audio nicht verfügbar" : (isPlaying ? "Wiedergabe läuft" : "Anhören"))
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(20)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 12))
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
.stroke(WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import SwiftUI
|
|||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Lädt ein authentifiziertes Image vom Cardecky-Media-Endpoint und
|
||||
/// Lädt ein authentifiziertes Image vom Wordeck-Media-Endpoint und
|
||||
/// rendert es. Streamt erst beim ersten Mal, danach aus dem
|
||||
/// MediaCache (LRU 200 MB).
|
||||
struct RemoteImage: View {
|
||||
|
|
@ -28,10 +28,10 @@ struct RemoteImage: View {
|
|||
imageView(image)
|
||||
} else if failed {
|
||||
ContentUnavailableView("Bild konnte nicht geladen werden", systemImage: "photo.badge.exclamationmark")
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else {
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
.task(id: mediaId) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct BlockedAuthorsView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
content
|
||||
}
|
||||
.navigationTitle("Blockierte Authors")
|
||||
|
|
@ -29,14 +29,14 @@ struct BlockedAuthorsView: View {
|
|||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if isLoading, blocks.isEmpty {
|
||||
ProgressView().tint(CardsTheme.primary)
|
||||
ProgressView().tint(WordeckTheme.primary)
|
||||
} else if blocks.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"Keine blockierten Authors",
|
||||
systemImage: "hand.raised.slash",
|
||||
description: Text("Blockiere Authors über das Menü oben rechts auf Marketplace-Decks.")
|
||||
)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
} else {
|
||||
List {
|
||||
ForEach(blocks) { block in
|
||||
|
|
@ -45,13 +45,13 @@ struct BlockedAuthorsView: View {
|
|||
.font(.subheadline.weight(.semibold))
|
||||
Text("@\(block.authorSlug)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
.swipeActions {
|
||||
Button("Entblocken") {
|
||||
Task { await unblock(block) }
|
||||
}
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ struct BlockedAuthorsView: View {
|
|||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ struct BlockedAuthorsView: View {
|
|||
private func load() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
blocks = try await api.myBlocks()
|
||||
} catch {
|
||||
|
|
@ -78,7 +78,7 @@ struct BlockedAuthorsView: View {
|
|||
}
|
||||
|
||||
private func unblock(_ block: BlockEntry) async {
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
do {
|
||||
try await api.unblockAuthor(slug: block.authorSlug)
|
||||
blocks.removeAll { $0.id == block.id }
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct SettingsView: View {
|
|||
systemImage: "exclamationmark.circle"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.warning)
|
||||
.foregroundStyle(WordeckTheme.warning)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section("Über") {
|
||||
LabeledContent("Server", value: "cardecky-api.mana.how")
|
||||
LabeledContent("Server", value: "api.wordeck.com")
|
||||
LabeledContent("Auth", value: "auth.mana.how")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ struct CardRenderer: View {
|
|||
VStack(spacing: 16) {
|
||||
text(card.fields[frontKey] ?? "")
|
||||
.font(.title2)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if isFlipped {
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
text(card.fields[backKey] ?? "")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ struct CardRenderer: View {
|
|||
VStack(spacing: 12) {
|
||||
text(rendered)
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ struct CardRenderer: View {
|
|||
// Back: alle Masken transparent (Bild komplett sichtbar).
|
||||
if !isFlipped, isActive {
|
||||
Rectangle()
|
||||
.fill(CardsTheme.primary.opacity(0.92))
|
||||
.fill(WordeckTheme.primary.opacity(0.92))
|
||||
.frame(
|
||||
width: region.w * geo.size.width,
|
||||
height: region.h * geo.size.height
|
||||
|
|
@ -93,7 +93,7 @@ struct CardRenderer: View {
|
|||
.overlay(
|
||||
Text(region.label?.isEmpty == false ? region.label! : "?")
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
.offset(x: region.x * geo.size.width, y: region.y * geo.size.height),
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
|
@ -106,12 +106,12 @@ struct CardRenderer: View {
|
|||
if isFlipped, let label = activeRegion?.label, !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
}
|
||||
if let note = card.fields["note"], !note.isEmpty {
|
||||
Text(note)
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,10 +122,10 @@ struct CardRenderer: View {
|
|||
VStack(spacing: 16) {
|
||||
AudioPlayerButton(mediaId: audioRef)
|
||||
if isFlipped {
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
text(card.fields["back"] ?? "")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,11 +134,11 @@ struct CardRenderer: View {
|
|||
VStack(spacing: 8) {
|
||||
Image(systemName: "questionmark.square.dashed")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
Text("Card-Type »\(card.type.rawValue)« kommt in einer späteren Phase")
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ struct MultipleChoiceCardView: View {
|
|||
VStack(alignment: .leading, spacing: 16) {
|
||||
text(card.fields["front"] ?? "")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
|
||||
switch phase {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 12)
|
||||
|
||||
|
|
@ -46,13 +46,13 @@ struct MultipleChoiceCardView: View {
|
|||
} else {
|
||||
Text("Nicht genug andere Karten im Deck für Multiple-Choice — tippe auf »Antwort anzeigen«.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
|
||||
case .failed:
|
||||
Text("Distractors konnten nicht geladen werden.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -76,7 +76,7 @@ struct MultipleChoiceCardView: View {
|
|||
.frame(width: 22)
|
||||
Text(option)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
|
@ -100,51 +100,51 @@ struct MultipleChoiceCardView: View {
|
|||
if isFlipped {
|
||||
if isCorrect {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(CardsTheme.success)
|
||||
.foregroundStyle(WordeckTheme.success)
|
||||
} else if isSelected {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(CardsTheme.mutedForeground.opacity(0.4))
|
||||
.foregroundStyle(WordeckTheme.mutedForeground.opacity(0.4))
|
||||
}
|
||||
} else if isSelected {
|
||||
Image(systemName: "largecircle.fill.circle")
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(CardsTheme.mutedForeground.opacity(0.4))
|
||||
.foregroundStyle(WordeckTheme.mutedForeground.opacity(0.4))
|
||||
}
|
||||
}
|
||||
|
||||
private func background(isCorrect: Bool, isSelected: Bool) -> Color {
|
||||
if isFlipped {
|
||||
if isCorrect { return CardsTheme.success.opacity(0.12) }
|
||||
if isSelected { return CardsTheme.error.opacity(0.10) }
|
||||
return CardsTheme.surfaceHover
|
||||
if isCorrect { return WordeckTheme.success.opacity(0.12) }
|
||||
if isSelected { return WordeckTheme.error.opacity(0.10) }
|
||||
return WordeckTheme.surfaceHover
|
||||
}
|
||||
return isSelected
|
||||
? CardsTheme.primary.opacity(0.10)
|
||||
: CardsTheme.surface
|
||||
? WordeckTheme.primary.opacity(0.10)
|
||||
: WordeckTheme.surface
|
||||
}
|
||||
|
||||
private func border(isCorrect: Bool, isSelected: Bool) -> Color {
|
||||
if isFlipped {
|
||||
if isCorrect { return CardsTheme.success.opacity(0.55) }
|
||||
if isSelected { return CardsTheme.error.opacity(0.55) }
|
||||
return CardsTheme.border
|
||||
if isCorrect { return WordeckTheme.success.opacity(0.55) }
|
||||
if isSelected { return WordeckTheme.error.opacity(0.55) }
|
||||
return WordeckTheme.border
|
||||
}
|
||||
return isSelected ? CardsTheme.primary.opacity(0.5) : CardsTheme.border
|
||||
return isSelected ? WordeckTheme.primary.opacity(0.5) : WordeckTheme.border
|
||||
}
|
||||
|
||||
/// Fallback wenn nicht genug Distractors: zeigt die Antwort
|
||||
/// direkt nach Flip, ohne Auswahl-Spiel.
|
||||
private var answerOnlyView: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
Text(card.fields["answer"] ?? "")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
.foregroundStyle(WordeckTheme.primary)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ struct MultipleChoiceCardView: View {
|
|||
private func loadOptions() async {
|
||||
phase = .loading
|
||||
selected = nil
|
||||
let api = CardsAPI(auth: auth)
|
||||
let api = WordeckAPI(auth: auth)
|
||||
let answer = card.fields["answer"] ?? ""
|
||||
|
||||
var distractors: [String] = []
|
||||
|
|
|
|||
|
|
@ -46,41 +46,41 @@ struct RatingBar: View {
|
|||
/// zum Web-Default-Klick. Andere bekommen subtle tinted borders.
|
||||
private func background(for rating: Rating) -> Color {
|
||||
switch rating {
|
||||
case .again: CardsTheme.error.opacity(0.06)
|
||||
case .hard: CardsTheme.warning.opacity(0.06)
|
||||
case .good: CardsTheme.primary
|
||||
case .easy: CardsTheme.success.opacity(0.06)
|
||||
case .again: WordeckTheme.error.opacity(0.06)
|
||||
case .hard: WordeckTheme.warning.opacity(0.06)
|
||||
case .good: WordeckTheme.primary
|
||||
case .easy: WordeckTheme.success.opacity(0.06)
|
||||
}
|
||||
}
|
||||
|
||||
private func foreground(for rating: Rating) -> Color {
|
||||
switch rating {
|
||||
case .again: CardsTheme.error
|
||||
case .hard: CardsTheme.warning
|
||||
case .good: CardsTheme.primaryForeground
|
||||
case .easy: CardsTheme.success
|
||||
case .again: WordeckTheme.error
|
||||
case .hard: WordeckTheme.warning
|
||||
case .good: WordeckTheme.primaryForeground
|
||||
case .easy: WordeckTheme.success
|
||||
}
|
||||
}
|
||||
|
||||
private func borderColor(for rating: Rating) -> Color {
|
||||
switch rating {
|
||||
case .again: CardsTheme.error.opacity(0.4)
|
||||
case .hard: CardsTheme.warning.opacity(0.4)
|
||||
case .again: WordeckTheme.error.opacity(0.4)
|
||||
case .hard: WordeckTheme.warning.opacity(0.4)
|
||||
case .good: .clear
|
||||
case .easy: CardsTheme.success.opacity(0.4)
|
||||
case .easy: WordeckTheme.success.opacity(0.4)
|
||||
}
|
||||
}
|
||||
|
||||
private func kbdBackground(for rating: Rating) -> Color {
|
||||
rating == .good
|
||||
? CardsTheme.primaryForeground.opacity(0.18)
|
||||
: CardsTheme.muted
|
||||
? WordeckTheme.primaryForeground.opacity(0.18)
|
||||
: WordeckTheme.muted
|
||||
}
|
||||
|
||||
private func kbdForeground(for rating: Rating) -> Color {
|
||||
rating == .good
|
||||
? CardsTheme.primaryForeground.opacity(0.85)
|
||||
: CardsTheme.mutedForeground
|
||||
? WordeckTheme.primaryForeground.opacity(0.85)
|
||||
: WordeckTheme.mutedForeground
|
||||
}
|
||||
|
||||
private func triggerHaptic(for rating: Rating) {
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ final class StudySession {
|
|||
let deckId: String
|
||||
let deckName: String
|
||||
|
||||
private let api: CardsAPI
|
||||
private let api: WordeckAPI
|
||||
private let gradeQueue: GradeQueue
|
||||
|
||||
init(deckId: String, deckName: String, auth: AuthClient, context: ModelContext) {
|
||||
self.deckId = deckId
|
||||
self.deckName = deckName
|
||||
api = CardsAPI(auth: auth)
|
||||
api = WordeckAPI(auth: auth)
|
||||
gradeQueue = GradeQueue(api: api, context: context)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ struct StudySessionView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
WordeckTheme.background.ignoresSafeArea()
|
||||
content
|
||||
}
|
||||
.navigationTitle(deckName)
|
||||
|
|
@ -30,7 +30,7 @@ struct StudySessionView: View {
|
|||
if let session, case .studying = session.phase {
|
||||
Text("\(session.remaining)")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.accessibilityLabel("\(session.remaining) Karten übrig")
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ struct StudySessionView: View {
|
|||
switch session.phase {
|
||||
case .loading:
|
||||
ProgressView("Karten werden geladen …")
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
case .studying:
|
||||
studyingView(session: session)
|
||||
case .finished:
|
||||
|
|
@ -60,7 +60,7 @@ struct StudySessionView: View {
|
|||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
.tint(CardsTheme.primary)
|
||||
.tint(WordeckTheme.primary)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +100,8 @@ struct StudySessionView: View {
|
|||
.font(.subheadline.weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 16)
|
||||
|
|
@ -128,14 +128,14 @@ struct StudySessionView: View {
|
|||
VStack(spacing: 16) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(CardsTheme.success)
|
||||
.foregroundStyle(WordeckTheme.success)
|
||||
Text(session.totalGraded == 0 ? "Keine Karten fällig" : "Fertig!")
|
||||
.font(.title.bold())
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
if session.totalGraded > 0 {
|
||||
Text("\(session.totalGraded) Karten gelernt")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
}
|
||||
Button("Zurück") { dismiss() }
|
||||
.padding(.top, 24)
|
||||
|
|
@ -147,14 +147,14 @@ struct StudySessionView: View {
|
|||
VStack(spacing: 16) {
|
||||
Image(systemName: "wifi.exclamationmark")
|
||||
.font(.system(size: 48))
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
.foregroundStyle(WordeckTheme.error)
|
||||
Text("Karten konnten nicht geladen werden")
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.padding(.horizontal, 32)
|
||||
Button("Erneut versuchen") {
|
||||
Task { await session.start() }
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ struct TypingCardView: View {
|
|||
VStack(alignment: .leading, spacing: 16) {
|
||||
text(card.fields["front"] ?? "")
|
||||
.font(.title3)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
|
||||
if submitted || isFlipped {
|
||||
resultView
|
||||
|
|
@ -66,10 +66,10 @@ struct TypingCardView: View {
|
|||
.focused($inputFocused)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.stroke(inputFocused ? CardsTheme.primary : CardsTheme.border, lineWidth: 1)
|
||||
.stroke(inputFocused ? WordeckTheme.primary : WordeckTheme.border, lineWidth: 1)
|
||||
)
|
||||
.autocorrectionDisabled()
|
||||
#if os(iOS)
|
||||
|
|
@ -83,8 +83,8 @@ struct TypingCardView: View {
|
|||
Image(systemName: "return")
|
||||
.font(.title3)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.foregroundStyle(CardsTheme.primaryForeground)
|
||||
.background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.foregroundStyle(WordeckTheme.primaryForeground)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(input.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
|
|
@ -112,18 +112,18 @@ struct TypingCardView: View {
|
|||
if !input.isEmpty {
|
||||
Text("„\(input)“")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.foregroundStyle(WordeckTheme.mutedForeground)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
|
||||
Divider().background(CardsTheme.border)
|
||||
Divider().background(WordeckTheme.border)
|
||||
|
||||
Text(answer)
|
||||
.font(.title3.weight(.medium))
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
.foregroundStyle(WordeckTheme.foreground)
|
||||
|
||||
if result == nil, !submitted {
|
||||
// unwahrscheinlich erreicht, aber als Sicherheits-Branch
|
||||
|
|
@ -161,17 +161,17 @@ struct TypingCardView: View {
|
|||
|
||||
private func badgeBackground(for result: TypingMatch) -> Color {
|
||||
switch result {
|
||||
case .correct: CardsTheme.success.opacity(0.18)
|
||||
case .close: CardsTheme.warning.opacity(0.18)
|
||||
case .wrong: CardsTheme.error.opacity(0.18)
|
||||
case .correct: WordeckTheme.success.opacity(0.18)
|
||||
case .close: WordeckTheme.warning.opacity(0.18)
|
||||
case .wrong: WordeckTheme.error.opacity(0.18)
|
||||
}
|
||||
}
|
||||
|
||||
private func badgeForeground(for result: TypingMatch) -> Color {
|
||||
switch result {
|
||||
case .correct: CardsTheme.success
|
||||
case .close: CardsTheme.warning
|
||||
case .wrong: CardsTheme.error
|
||||
case .correct: WordeckTheme.success
|
||||
case .close: WordeckTheme.warning
|
||||
case .wrong: WordeckTheme.error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
"Card-Type »%@« kommt in einer späteren Phase" : {
|
||||
|
||||
},
|
||||
"Cardecky" : {
|
||||
"Wordeck" : {
|
||||
|
||||
},
|
||||
"Changelog" : {
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
"Decks dieses Authors erscheinen für dich nicht mehr im Marketplace." : {
|
||||
|
||||
},
|
||||
"Der Slug wird Teil der Marketplace-URL: cardecky.mana.how/d/<slug>." : {
|
||||
"Der Slug wird Teil der Marketplace-URL: wordeck.com/d/<slug>." : {
|
||||
|
||||
},
|
||||
"Distractor-Optionen werden zur Lernzeit automatisch aus anderen Karten desselben Decks gezogen." : {
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
"Du hast schon Decks im Marketplace. Wähle eine, um eine neue Version zu publishen." : {
|
||||
|
||||
},
|
||||
"Du nutzt Cardecky anonym" : {
|
||||
"Du nutzt Wordeck anonym" : {
|
||||
|
||||
},
|
||||
"Erst ein Deck erstellen." : {
|
||||
|
|
|
|||
24
Sources/Resources/WordeckNative.entitlements
Normal file
24
Sources/Resources/WordeckNative.entitlements
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:wordeck.com</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ev.mana.wordeck</string>
|
||||
</array>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)ev.mana.wordeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import XCTest
|
||||
|
||||
final class CardsNativeUITests: XCTestCase {
|
||||
final class WordeckNativeUITests: XCTestCase {
|
||||
func testAppLaunches() {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
// App ist gestartet, sobald entweder das LoginView "Cardecky"
|
||||
// App ist gestartet, sobald entweder das LoginView "Wordeck"
|
||||
// oder das DeckListView mit "Decks" sichtbar ist. Welcher
|
||||
// von beiden hängt davon ab, ob der Simulator-Keychain noch
|
||||
// eine Session hält.
|
||||
let loginTitle = app.staticTexts["Cardecky"]
|
||||
let loginTitle = app.staticTexts["Wordeck"]
|
||||
let decksTitle = app.staticTexts["Decks"]
|
||||
let exploreTab = app.staticTexts["Entdecken"]
|
||||
|
||||
|
|
@ -21,6 +21,6 @@ final class CardsNativeUITests: XCTestCase {
|
|||
}
|
||||
usleep(100_000)
|
||||
}
|
||||
XCTAssertTrue(found, "Erwartete App-Surface (Cardecky | Decks | Entdecken) erschien nicht innerhalb 5 s")
|
||||
XCTAssertTrue(found, "Erwartete App-Surface (Wordeck | Decks | Entdecken) erschien nicht innerhalb 5 s")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Cloze")
|
||||
struct ClozeTests {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Deck-JSON-Decoding")
|
||||
struct DeckDecodingTests {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Marketplace-JSON-Decoding")
|
||||
struct MarketplaceDecodingTests {
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
|
||||
@Suite("MaskRegions")
|
||||
struct MaskRegionsTests {
|
||||
@Test("Parsed Liste sortiert nach ID lexikographisch")
|
||||
func parseSortsByIdLexically() {
|
||||
let json = """
|
||||
[
|
||||
{"id":"m003","x":0.1,"y":0.1,"w":0.2,"h":0.2,"label":"C"},
|
||||
{"id":"m001","x":0,"y":0,"w":0.1,"h":0.1,"label":"A"},
|
||||
{"id":"m002","x":0.5,"y":0.5,"w":0.3,"h":0.3}
|
||||
]
|
||||
"""
|
||||
let regions = MaskRegions.parse(json)
|
||||
#expect(regions.count == 3)
|
||||
#expect(regions[0].id == "m001")
|
||||
#expect(regions[1].id == "m002")
|
||||
#expect(regions[2].id == "m003")
|
||||
#expect(regions[2].label == "C")
|
||||
#expect(regions[1].label == nil)
|
||||
}
|
||||
|
||||
@Test("Bei Parse-Fehler → leere Liste")
|
||||
func parseInvalidReturnsEmpty() {
|
||||
#expect(MaskRegions.parse("[}").isEmpty)
|
||||
#expect(MaskRegions.parse("{}").isEmpty)
|
||||
#expect(MaskRegions.parse("").isEmpty)
|
||||
}
|
||||
|
||||
@Test("region(forSubIndex:) mappt aufsteigend")
|
||||
func subIndexLookup() {
|
||||
let json = """
|
||||
[{"id":"b","x":0,"y":0,"w":0.1,"h":0.1},
|
||||
{"id":"a","x":0,"y":0,"w":0.2,"h":0.2}]
|
||||
"""
|
||||
#expect(MaskRegions.region(for: json, subIndex: 0)?.id == "a")
|
||||
#expect(MaskRegions.region(for: json, subIndex: 1)?.id == "b")
|
||||
#expect(MaskRegions.region(for: json, subIndex: 2) == nil)
|
||||
}
|
||||
|
||||
@Test("Encode-Roundtrip")
|
||||
func encodeRoundtrip() {
|
||||
let original = [
|
||||
MaskRegion(id: "m1", x: 0.1, y: 0.2, w: 0.3, h: 0.4, label: "test"),
|
||||
MaskRegion(id: "m2", x: 0.5, y: 0.6, w: 0.2, h: 0.2, label: nil)
|
||||
]
|
||||
let encoded = MaskRegions.encode(original)
|
||||
let parsed = MaskRegions.parse(encoded)
|
||||
#expect(parsed.count == 2)
|
||||
#expect(parsed[0].id == "m1")
|
||||
#expect(parsed[0].label == "test")
|
||||
#expect(parsed[1].label == nil)
|
||||
}
|
||||
|
||||
@Test("CardFieldsBuilder.imageOcclusion produziert korrekte Felder")
|
||||
func builderImageOcclusion() {
|
||||
let regions = [MaskRegion(id: "m1", x: 0, y: 0, w: 0.5, h: 0.5, label: "x")]
|
||||
let fields = CardFieldsBuilder.imageOcclusion(
|
||||
imageRef: "media_123",
|
||||
regions: regions,
|
||||
note: "Hinweis"
|
||||
)
|
||||
#expect(fields["image_ref"] == "media_123")
|
||||
#expect(fields["note"] == "Hinweis")
|
||||
let reparsed = MaskRegions.parse(fields["mask_regions"] ?? "")
|
||||
#expect(reparsed.count == 1)
|
||||
#expect(reparsed[0].id == "m1")
|
||||
}
|
||||
|
||||
@Test("CardFieldsBuilder.audioFront produziert korrekte Felder")
|
||||
func builderAudioFront() {
|
||||
let fields = CardFieldsBuilder.audioFront(audioRef: "audio_456", back: "Antwort")
|
||||
#expect(fields["audio_ref"] == "audio_456")
|
||||
#expect(fields["back"] == "Antwort")
|
||||
#expect(fields.count == 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Mutation Body Encoding")
|
||||
struct MutationEncodingTests {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Review-JSON-Decoding")
|
||||
struct ReviewDecodingTests {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("Typing-Match")
|
||||
struct TypingTests {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import Testing
|
||||
@testable import CardsNative
|
||||
@testable import WordeckNative
|
||||
|
||||
@Suite("AppConfig")
|
||||
struct AppConfigTests {
|
||||
@Test("Cards-API zeigt auf cardecky-api.mana.how")
|
||||
func apiBaseURLPointsToCardecky() {
|
||||
#expect(AppConfig.apiBaseURL.absoluteString == "https://cardecky-api.mana.how")
|
||||
@Test("Cards-API zeigt auf api.wordeck.com")
|
||||
func apiBaseURLPointsToWordeck() {
|
||||
#expect(AppConfig.apiBaseURL.absoluteString == "https://api.wordeck.com")
|
||||
}
|
||||
|
||||
@Test("Auth zeigt auf auth.mana.how")
|
||||
|
|
@ -13,8 +13,8 @@ struct AppConfigTests {
|
|||
#expect(AppConfig.manaAppConfig.authBaseURL.absoluteString == "https://auth.mana.how")
|
||||
}
|
||||
|
||||
@Test("Keychain-Service ist ev.mana.cardecky")
|
||||
@Test("Keychain-Service ist ev.mana.wordeck")
|
||||
func keychainServiceIsAppSpecific() {
|
||||
#expect(AppConfig.manaAppConfig.keychainService == "ev.mana.cardecky")
|
||||
#expect(AppConfig.manaAppConfig.keychainService == "ev.mana.wordeck")
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ struct DueWidgetView: View {
|
|||
smallView
|
||||
case .systemMedium:
|
||||
mediumView
|
||||
case .systemLarge:
|
||||
largeView
|
||||
case .accessoryCircular:
|
||||
circularView
|
||||
case .accessoryInline:
|
||||
|
|
@ -75,6 +77,52 @@ struct DueWidgetView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var largeView: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("\(entry.totalDueCount)")
|
||||
.font(.system(size: 56, weight: .bold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.6)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.totalDueCount == 1 ? "Karte fällig" : "Karten fällig")
|
||||
.font(.subheadline.weight(.medium))
|
||||
Text("Heute")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Top-Decks")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
ForEach(entry.topDecks.prefix(6)) { deck in
|
||||
HStack {
|
||||
Text(deck.name)
|
||||
.font(.callout)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
Text("\(deck.dueCount)")
|
||||
.font(.callout.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if entry.topDecks.isEmpty {
|
||||
Text("Keine Decks mit fälligen Karten.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private var circularView: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ev.mana.wordeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
29
Widgets/WordeckWidget/Resources/Info.plist
Normal file
29
Widgets/WordeckWidget/Resources/Info.plist
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Wordeck Widget</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>11</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ev.mana.wordeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct CardsDueWidget: Widget {
|
||||
let kind: String = "ev.mana.cardecky.due"
|
||||
struct WordeckDueWidget: Widget {
|
||||
let kind: String = "ev.mana.wordeck.due"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: DueProvider()) { entry in
|
||||
|
|
@ -14,6 +14,7 @@ struct CardsDueWidget: Widget {
|
|||
.supportedFamilies([
|
||||
.systemSmall,
|
||||
.systemMedium,
|
||||
.systemLarge,
|
||||
.accessoryCircular,
|
||||
.accessoryInline,
|
||||
.accessoryRectangular,
|
||||
|
|
@ -6,8 +6,8 @@ import WidgetKit
|
|||
/// rectangular). Daten kommen aus dem shared App-Group-Container
|
||||
/// (siehe `WidgetSnapshotStore` im Haupt-Target).
|
||||
@main
|
||||
struct CardsWidgetBundle: WidgetBundle {
|
||||
struct WordeckWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
CardsDueWidget()
|
||||
WordeckDueWidget()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ review: written
|
|||
Phasen β-0 bis β-7 aus dem Greenfield-Playbook in einer Marathon-
|
||||
Session. Skeleton → App-Store-Vorbereitung. Web-Parität als
|
||||
Leitprinzip, FSRS bleibt server-authoritative, alles andere ist
|
||||
Native-Re-Implementation des Web-Cardecky.
|
||||
Native-Re-Implementation des Web-Wordeck.
|
||||
|
||||
## Stats
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ Subsystem-Bereich anfasst.
|
|||
|
||||
- **v0.1.0 — β-0 Setup.** XcodeGen-Manifest, ManaCore + ManaTokens-
|
||||
Package-Dependency, ManaCore-Login + Healthz-Probe gegen
|
||||
cardecky-api.mana.how.
|
||||
api.wordeck.com.
|
||||
- **v0.2.0 — β-1 Decks lesen.** SwiftData-Cache,
|
||||
`/api/v1/decks`-Liste, Detail-View mit Karten-Anzahl,
|
||||
Stats-Sektion.
|
||||
|
|
@ -66,7 +66,7 @@ Subsystem-Bereich anfasst.
|
|||
- **Pure SwiftUI**, einzige UIKit-Bridge: `PencilKit` für Image-
|
||||
Occlusion. SwiftUI hat keinen Pencil-Picker, nicht-vermeidbar.
|
||||
- **mana-auth via ManaCore.** Eigene Auth-Implementierung verboten.
|
||||
- **`forest`-Theme**, lokal in `CardsTheme.swift` aus
|
||||
- **`forest`-Theme**, lokal in `WordeckTheme.swift` aus
|
||||
`mana/packages/themes/src/variants/forest.css` gespiegelt.
|
||||
Migration auf ManaTokens-Theme-Switch ist Phase ε.
|
||||
- **Web gewinnt bei Konflikt.** Eleganteres Native-Verhalten geht
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ review: written
|
|||
---
|
||||
# Dienstag, 2026-05-12 — Tag 1
|
||||
|
||||
Cardecky gibt es bald auch als App. Heute ist die Basis entstanden:
|
||||
Wordeck gibt es bald auch als App. Heute ist die Basis entstanden:
|
||||
Anmelden, Decks ansehen, lernen, Karten bearbeiten, Bilder dazulegen,
|
||||
Marketplace durchsehen — alles offline-tauglich.
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ Marketplace durchsehen — alles offline-tauglich.
|
|||
|
||||
## Hintergrund
|
||||
|
||||
Cardecky lebt im Browser; manche Leute lernen aber lieber mit der
|
||||
Hand am Telefon. Die App ist deshalb keine neue Cardecky, sondern
|
||||
dieselbe Cardecky in iOS-Form — gleiche Karten, gleiches FSRS,
|
||||
Wordeck lebt im Browser; manche Leute lernen aber lieber mit der
|
||||
Hand am Telefon. Die App ist deshalb keine neue Wordeck, sondern
|
||||
dieselbe Wordeck in iOS-Form — gleiche Karten, gleiches FSRS,
|
||||
gleicher Account.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
},
|
||||
{
|
||||
"hash": "4dfb32b",
|
||||
"short": "chore: Rebrand auf ev.mana.cardecky",
|
||||
"short": "chore: Rebrand auf ev.mana.wordeck",
|
||||
"type": null,
|
||||
"scope": null,
|
||||
"additions": 203,
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
},
|
||||
{
|
||||
"hash": "2eb7f61",
|
||||
"short": "fix: ITMS-90129 — DisplayName Cards → Cardecky + Build 2",
|
||||
"short": "fix: ITMS-90129 — DisplayName Cards → Wordeck + Build 2",
|
||||
"type": null,
|
||||
"scope": null,
|
||||
"additions": 11,
|
||||
|
|
@ -166,7 +166,7 @@
|
|||
},
|
||||
{
|
||||
"hash": "b5edf5c",
|
||||
"short": "fix: User-facing Strings Cards → Cardecky komplett, Build 3",
|
||||
"short": "fix: User-facing Strings Cards → Wordeck komplett, Build 3",
|
||||
"type": null,
|
||||
"scope": null,
|
||||
"additions": 19,
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
},
|
||||
{
|
||||
"hash": "aa94601",
|
||||
"short": "feat(ui): Cardecky-Web-Design — Fan-Stack-Tiles + CardSurface",
|
||||
"short": "feat(ui): Wordeck-Web-Design — Fan-Stack-Tiles + CardSurface",
|
||||
"type": "feat",
|
||||
"scope": "ui",
|
||||
"additions": 396,
|
||||
|
|
|
|||
|
|
@ -25,22 +25,22 @@ Study (8 %), Sync (6 %). Tags: decks, auth, study, editor, ui.
|
|||
|
||||
- **DEVELOPMENT_TEAM=QP3GLU8PH3** für Code-Signing. Erstmal
|
||||
Apple-Konto-Setup.
|
||||
- **Rebrand auf `ev.mana.cardecky`.** Bundle-ID, DisplayName, Strings,
|
||||
- **Rebrand auf `ev.mana.wordeck`.** Bundle-ID, DisplayName, Strings,
|
||||
Icon-Asset. Vorher hieß die App intern „Cards", öffentlich aber
|
||||
„Cardecky".
|
||||
„Wordeck".
|
||||
- **Archive-Polish** — Versions-Sync, Orientations, Mac-Icon.
|
||||
- **Sendable-Warnings**: PhotosPicker zog Sub-View struct (Sendable-
|
||||
Compliance für `PHPickerViewController`).
|
||||
- **ITMS-90129** (Apple-Validation): DisplayName Cards → Cardecky in
|
||||
- **ITMS-90129** (Apple-Validation): DisplayName Cards → Wordeck in
|
||||
Info.plist, Build 2.
|
||||
- **User-facing Strings Cards → Cardecky** komplett, Build 3.
|
||||
- **User-facing Strings Cards → Wordeck** komplett, Build 3.
|
||||
- **PublicDeckOwner.pseudonym ist Bool**, nicht `String?` —
|
||||
Wire-Format-Drift gegen Server-Schema gefangen.
|
||||
- **Card-Liste im DeckDetailView + listCards-API.** Vorher zeigte
|
||||
Detail nur Metadaten.
|
||||
- **ManaCore v1.0.1 pull** — 404 auf Query-Endpoints (URL-Encoding
|
||||
von `?`, siehe mana-swift-core Tag 2).
|
||||
- **Cardecky-Web-Design — Fan-Stack-Tiles + CardSurface.** Tiles
|
||||
- **Wordeck-Web-Design — Fan-Stack-Tiles + CardSurface.** Tiles
|
||||
fächern Karten auf, einheitlicher Card-Frame über Front/Back/
|
||||
Cloze.
|
||||
- **Multiple-Choice-Karten rendern.** UI-Render-Layer für MC, Server
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ review: written
|
|||
---
|
||||
# Mittwoch, 2026-05-13 — Tag 2
|
||||
|
||||
Cardecky-Native sieht heute aus wie Cardecky — Karten-Fan auf dem
|
||||
Wordeck-Native sieht heute aus wie Wordeck — Karten-Fan auf dem
|
||||
Dashboard, Multiple-Choice und Tippen werden ordentlich dargestellt,
|
||||
und du kannst die App jetzt auch nutzen, **ohne dich anzumelden**:
|
||||
Decks aus dem Marketplace anschauen, sogar lernen — Konto brauchst
|
||||
|
|
@ -18,7 +18,7 @@ du erst, wenn du etwas speichern willst.
|
|||
|
||||
- **Optik wie im Web** — die Tiles auf dem Dashboard fächern Karten
|
||||
wie ein Kartenspiel auf, die Vorder-/Rückseiten werden so gerahmt
|
||||
wie auf cardecky.mana.how. Wer Web und App nebeneinander hat,
|
||||
wie auf wordeck.com. Wer Web und App nebeneinander hat,
|
||||
findet sich sofort zurecht.
|
||||
- **Multiple-Choice + Tippen funktionieren** — beide Karten-Typen
|
||||
werden mit den passenden Eingaben dargestellt (Auswahl mit Buttons,
|
||||
|
|
@ -45,6 +45,6 @@ du erst, wenn du etwas speichern willst.
|
|||
|
||||
## Hintergrund
|
||||
|
||||
Cardecky-Native ist heute funktional fertig — alles, was die Web-
|
||||
Wordeck-Native ist heute funktional fertig — alles, was die Web-
|
||||
App kann, geht jetzt auch hier. Was fehlt, sind Endurance-Tests am
|
||||
echten Gerät und der App-Store-Slot bei Apple.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# MARKETING_COPY — cards-native
|
||||
# MARKETING_COPY — wordeck-native
|
||||
|
||||
Vorschläge für App-Store-Description (de + en). Zum Eintragen in
|
||||
App-Store-Connect. **Nicht final** — vor Submission durch dich
|
||||
|
|
@ -7,7 +7,7 @@ gegenlesen, Tonalität an Vereins-Stil schärfen (siehe `mana/docs/BRAND.md`,
|
|||
|
||||
## Name + Subtitle
|
||||
|
||||
**App-Name:** `Cardecky`
|
||||
**App-Name:** `Wordeck`
|
||||
**Subtitle (max 30 Zeichen):** `Karteikarten — mana e.V.`
|
||||
|
||||
## Keywords (max 100 Zeichen, comma-separated)
|
||||
|
|
@ -21,7 +21,7 @@ Karteikarten,Spaced Repetition,Lernen,Vokabeln,Anki,Flashcards,FSRS,mana,Verein,
|
|||
## Description — DE (max 4000 Zeichen)
|
||||
|
||||
```
|
||||
Cardecky ist die Karteikarten-App des Vereins mana e.V. — Spaced
|
||||
Wordeck ist die Karteikarten-App des Vereins mana e.V. — Spaced
|
||||
Repetition wie es sein soll: ohne Werbung, ohne Tracking, ohne
|
||||
Abo-Zwang.
|
||||
|
||||
|
|
@ -33,13 +33,13 @@ KARTEN, WIE DU SIE BRAUCHST
|
|||
- Audio-Karten für Sprachen und Aussprache
|
||||
|
||||
MODERNER LERN-ALGORITHMUS
|
||||
Cardecky nutzt FSRS (Free Spaced Repetition Scheduler), den genauesten
|
||||
Wordeck nutzt FSRS (Free Spaced Repetition Scheduler), den genauesten
|
||||
offenen Algorithmus für Karteikarten. Karten kommen wieder, wenn du
|
||||
sie wirklich brauchst — nicht nach willkürlichen Intervallen.
|
||||
|
||||
OFFLINE LERNEN
|
||||
Karten cachen lokal auf deinem Gerät. Im Flugzeug, in der U-Bahn,
|
||||
ohne WLAN — Cardecky funktioniert. Deine Bewertungen werden gequeued
|
||||
ohne WLAN — Wordeck funktioniert. Deine Bewertungen werden gequeued
|
||||
und beim nächsten Online-Moment automatisch hochgeladen.
|
||||
|
||||
WIDGETS UND NOTIFICATIONS
|
||||
|
|
@ -48,12 +48,12 @@ oder Lock-Screen. Erinnerung zur Lieblings-Lern-Uhrzeit — lokal, ohne
|
|||
externe Push-Server.
|
||||
|
||||
CARDECKY — MARKETPLACE FÜR LERN-DECKS
|
||||
Stöbere durch öffentliche Decks aus der Cardecky-Community: Geografie,
|
||||
Stöbere durch öffentliche Decks aus der Wordeck-Community: Geografie,
|
||||
Sprachen, Wissenschaft. Abonnieren = Karten landen direkt in deiner
|
||||
Bibliothek, du kannst sie nach Belieben anpassen.
|
||||
|
||||
VEREIN, NICHT FIRMA
|
||||
Cardecky wird vom Verein mana e.V. (Schweiz, in Gründung) betrieben.
|
||||
Wordeck wird vom Verein mana e.V. (Schweiz, in Gründung) betrieben.
|
||||
Kein Tracking, kein Werbe-Anbieter, kein Crash-Reporter. Wenn du dem
|
||||
Verein etwas zurückgeben willst: mana-ev.ch/spende.
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ selbst.
|
|||
## Description — EN (max 4000 Zeichen)
|
||||
|
||||
```
|
||||
Cardecky is the flashcard app from mana e.V. — spaced repetition the
|
||||
Wordeck is the flashcard app from mana e.V. — spaced repetition the
|
||||
way it should be: no ads, no tracking, no subscription pressure.
|
||||
|
||||
THE CARDS YOU NEED
|
||||
|
|
@ -80,12 +80,12 @@ THE CARDS YOU NEED
|
|||
- Audio cards for languages and pronunciation
|
||||
|
||||
MODERN SPACED-REPETITION
|
||||
Cardecky uses FSRS (Free Spaced Repetition Scheduler), the most accurate
|
||||
Wordeck uses FSRS (Free Spaced Repetition Scheduler), the most accurate
|
||||
open algorithm available. Your cards come back when you actually need them.
|
||||
|
||||
OFFLINE-FIRST
|
||||
Decks cache locally on your device. On a plane, in the subway, off
|
||||
the grid — Cardecky keeps working. Your reviews queue up and sync on
|
||||
the grid — Wordeck keeps working. Your reviews queue up and sync on
|
||||
the next online moment.
|
||||
|
||||
WIDGETS AND REMINDERS
|
||||
|
|
@ -93,11 +93,11 @@ See today's due cards on your Home Screen or Lock Screen. Daily
|
|||
reminder at your preferred time — local, no push servers.
|
||||
|
||||
CARDECKY MARKETPLACE
|
||||
Browse public decks from the Cardecky community: geography, languages,
|
||||
Browse public decks from the Wordeck community: geography, languages,
|
||||
science. Subscribe = decks land in your library, ready to be edited.
|
||||
|
||||
ASSOCIATION, NOT CORPORATION
|
||||
Cardecky is operated by mana e.V. (Switzerland, formation in progress).
|
||||
Wordeck is operated by mana e.V. (Switzerland, formation in progress).
|
||||
No tracking, no ad networks, no crash reporters. Want to give back?
|
||||
mana-ev.ch/donate.
|
||||
|
||||
|
|
@ -125,9 +125,9 @@ keine `Diagnostics`, keine `Location`, keine `Contacts`.
|
|||
## Privacy-Policy- + Support-URLs
|
||||
|
||||
Vor Submission setzen — vermutlich:
|
||||
- Privacy-Policy: `https://cardecky.mana.how/privacy` (existiert? prüfen)
|
||||
- Support: `https://cardecky.mana.how/help` oder `kontakt@mana-ev.ch`
|
||||
- Marketing: `https://cardecky.mana.how`
|
||||
- Privacy-Policy: `https://wordeck.com/privacy` (existiert? prüfen)
|
||||
- Support: `https://wordeck.com/help` oder `kontakt@mana-ev.ch`
|
||||
- Marketing: `https://wordeck.com`
|
||||
|
||||
Falls die URLs noch nicht live sind, vor Submission in cards-web
|
||||
ergänzen (Routes `/privacy`, `/help`).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# RELEASE_CHECKLIST — cards-native
|
||||
# RELEASE_CHECKLIST — wordeck-native
|
||||
|
||||
Externe Schritte vor App-Store-Submission. Alles unter dieser
|
||||
Sektion läuft NICHT durch das Repo — sondern durch das Apple-
|
||||
|
|
@ -11,14 +11,14 @@ AASA) und über Xcode (für Build + Sign).
|
|||
|
||||
- [x] **Team-ID gesetzt** (`QP3GLU8PH3`, mana e.V.) — `DEVELOPMENT_TEAM`
|
||||
in `project.yml > settings > base`. Greift bei Archive automatisch.
|
||||
- [ ] **App-ID `ev.mana.cardecky`** im Developer-Portal anlegen, falls
|
||||
- [ ] **App-ID `ev.mana.wordeck`** im Developer-Portal anlegen, falls
|
||||
noch nicht da. Mit Capabilities: App Groups, Keychain Sharing,
|
||||
Associated Domains.
|
||||
- [ ] **App-ID `ev.mana.cardecky.share`** + **`ev.mana.cardecky.widget`** für
|
||||
- [ ] **App-ID `ev.mana.wordeck.share`** + **`ev.mana.wordeck.widget`** für
|
||||
die Extensions analog anlegen, ebenfalls mit App Groups.
|
||||
- [ ] **App-Group `group.ev.mana.cardecky`** im Portal anlegen und allen
|
||||
- [ ] **App-Group `group.ev.mana.wordeck`** im Portal anlegen und allen
|
||||
drei App-IDs zuweisen.
|
||||
- [ ] **Keychain-Access-Group**: heute `ev.mana.cardecky`. Wenn
|
||||
- [ ] **Keychain-Access-Group**: heute `ev.mana.wordeck`. Wenn
|
||||
Shared-Keychain mit `memoro-native` gewünscht (siehe
|
||||
`mana/docs/MANA_SWIFT.md` Phase γ), auf
|
||||
`$(AppIdentifierPrefix)ev.mana.shared` umstellen und
|
||||
|
|
@ -40,7 +40,7 @@ AASA) und über Xcode (für Build + Sign).
|
|||
|
||||
### Server-seitige Vorbedingungen
|
||||
|
||||
- [x] **AASA-Endpoint** auf `cardecky.mana.how/.well-known/apple-app-site-association`
|
||||
- [x] **AASA-Endpoint** auf `wordeck.com/.well-known/apple-app-site-association`
|
||||
— SvelteKit-Server-Route gebaut in
|
||||
`cards/apps/web/src/routes/.well-known/apple-app-site-association/+server.ts`
|
||||
(2026-05-13). Content-Type `application/json`, paths `/d/*` und
|
||||
|
|
@ -50,11 +50,11 @@ AASA) und über Xcode (für Build + Sign).
|
|||
(Commit folgt). Wird zur Runtime von `$env/dynamic/public`
|
||||
aufgelöst und in den AASA-Response geschrieben.
|
||||
- [x] **Production-Deploy von cards-web** durchgeführt 2026-05-13.
|
||||
Probe von außen: `curl https://cardecky.mana.how/.well-known/apple-app-site-association`
|
||||
liefert `application/json` mit `"appID":"QP3GLU8PH3.ev.mana.cardecky"`.
|
||||
Probe von außen: `curl https://wordeck.com/.well-known/apple-app-site-association`
|
||||
liefert `application/json` mit `"appID":"QP3GLU8PH3.ev.mana.wordeck"`.
|
||||
Cloudflare-Tunnel reicht den Endpoint sauber durch (kein
|
||||
HTML-Captive, kein Redirect).
|
||||
- [ ] **cardecky-api.mana.how** muss erreichbar bleiben — die App
|
||||
- [ ] **api.wordeck.com** muss erreichbar bleiben — die App
|
||||
ist 100% Online-write. Health-Probe verifizieren.
|
||||
|
||||
### Build + Archive
|
||||
|
|
@ -70,7 +70,7 @@ AASA) und über Xcode (für Build + Sign).
|
|||
- [ ] **Cross-Device:** Web↔Native parallel. Karte gegrade in App →
|
||||
Web zeigt nach Reload identischen Review-State.
|
||||
- [ ] **Widget:** ans Home-Screen pinnen, Due-Count nach App-Refresh.
|
||||
- [ ] **Universal-Link:** `https://cardecky.mana.how/d/<slug>` in
|
||||
- [ ] **Universal-Link:** `https://wordeck.com/d/<slug>` in
|
||||
Safari öffnen → App startet auf Explore-Tab + Public-Deck-Detail.
|
||||
- [ ] **Share-Extension:** Text in Safari markieren → Teilen → "Als
|
||||
Karte speichern" → Karte landet in der App.
|
||||
|
|
@ -86,9 +86,9 @@ AASA) und über Xcode (für Build + Sign).
|
|||
### App-Store-Connect
|
||||
|
||||
- [ ] **App-Eintrag erstellen** unter https://appstoreconnect.apple.com
|
||||
mit Bundle-ID `ev.mana.cardecky`.
|
||||
mit Bundle-ID `ev.mana.wordeck`.
|
||||
- [ ] **App-Name** + **Subtitle** (max 30 Zeichen):
|
||||
- Name: "Cardecky"
|
||||
- Name: "Wordeck"
|
||||
- Subtitle: "Karteikarten — Verein mana"
|
||||
- [ ] **Description** (de + en, max 4000 Zeichen). Vorschlag in
|
||||
[`docs/MARKETING_COPY.md`](MARKETING_COPY.md) — vor Submission
|
||||
|
|
@ -97,11 +97,11 @@ AASA) und über Xcode (für Build + Sign).
|
|||
"Karteikarten,Spaced Repetition,Lernen,Vokabeln,Anki,Flashcards,FSRS,mana,Verein,Open Source"
|
||||
- [ ] **Screenshots** für iPhone 16 Pro Max + iPhone SE-3 + iPad Pro.
|
||||
6.7", 6.5", 5.5", iPad 12.9" — siehe Apple's Specs.
|
||||
- [x] **Privacy-Policy-URL**: `https://cardecky.mana.how/privacy` (live
|
||||
- [x] **Privacy-Policy-URL**: `https://wordeck.com/privacy` (live
|
||||
seit 2026-05-13, SvelteKit-Route mit Verein-Content).
|
||||
- [x] **Support-URL**: `https://cardecky.mana.how/help` (live, FAQ +
|
||||
- [x] **Support-URL**: `https://wordeck.com/help` (live, FAQ +
|
||||
Kontakt-Email kontakt@mana-ev.ch).
|
||||
- [ ] **Marketing-URL** (optional) — `cardecky.mana.how`.
|
||||
- [ ] **Marketing-URL** (optional) — `wordeck.com`.
|
||||
- [ ] **Age-Rating**: vermutlich 4+ (no objectionable content).
|
||||
- [ ] **Pricing**: Free.
|
||||
- [ ] **App-Privacy** (Data Type Declaration):
|
||||
|
|
@ -124,7 +124,7 @@ AASA) und über Xcode (für Build + Sign).
|
|||
|
||||
### Hub-App vs Standalone-App (siehe MANA_SWIFT.md)
|
||||
|
||||
- [x] Entschieden: separate Apps. memoro-native und cards-native sind
|
||||
- [x] Entschieden: separate Apps. memoro-native und wordeck-native sind
|
||||
eigenständige App-Store-Einträge. Keine Hub-App.
|
||||
|
||||
## Carryover-Tasks (β-6 / β-7-Reste)
|
||||
|
|
@ -144,7 +144,7 @@ AASA) und über Xcode (für Build + Sign).
|
|||
|
||||
## Nach Submission
|
||||
|
||||
- [ ] **Monitoring**: nach Release `cardecky-api.mana.how/healthz` und
|
||||
- [ ] **Monitoring**: nach Release `api.wordeck.com/healthz` und
|
||||
Rate-Limit-Auslastung beobachten — Native-App kann Last-Spitzen
|
||||
erzeugen.
|
||||
- [ ] **DSGVO-Endpoint** (`/api/v1/dsgvo/export`, `/delete`) testweise
|
||||
|
|
|
|||
72
project.yml
72
project.yml
|
|
@ -1,4 +1,4 @@
|
|||
name: CardsNative
|
||||
name: WordeckNative
|
||||
|
||||
options:
|
||||
bundleIdPrefix: ev.mana
|
||||
|
|
@ -28,12 +28,12 @@ settings:
|
|||
DEAD_CODE_STRIPPING: "YES"
|
||||
CLANG_ENABLE_MODULES: "YES"
|
||||
# mana e.V. Apple-Developer-Team-ID. Muss matchen mit dem AASA-
|
||||
# Eintrag auf https://cardecky.mana.how/.well-known/apple-app-site-association
|
||||
# Eintrag auf https://wordeck.com/.well-known/apple-app-site-association
|
||||
# (siehe cards/infrastructure/docker-compose.production.yml).
|
||||
DEVELOPMENT_TEAM: QP3GLU8PH3
|
||||
|
||||
targets:
|
||||
CardsNative:
|
||||
WordeckNative:
|
||||
type: application
|
||||
supportedDestinations: [iOS, macOS]
|
||||
dependencies:
|
||||
|
|
@ -43,7 +43,7 @@ targets:
|
|||
product: ManaTokens
|
||||
- package: ManaSwiftUI
|
||||
product: ManaAuthUI
|
||||
- target: CardsWidgetExtension
|
||||
- target: WordeckWidgetExtension
|
||||
embed: true
|
||||
- target: CardsShareExtension
|
||||
embed: true
|
||||
|
|
@ -54,14 +54,14 @@ targets:
|
|||
- path: Sources/Resources
|
||||
excludes:
|
||||
- "Info.plist"
|
||||
- "CardsNative.entitlements"
|
||||
- "WordeckNative.entitlements"
|
||||
info:
|
||||
path: Sources/Resources/Info.plist
|
||||
properties:
|
||||
CFBundleShortVersionString: "0.1.0"
|
||||
CFBundleVersion: "11"
|
||||
CFBundleDevelopmentRegion: de
|
||||
CFBundleDisplayName: Cardecky
|
||||
CFBundleDisplayName: Wordeck
|
||||
LSApplicationCategoryType: "public.app-category.education"
|
||||
UILaunchScreen: {}
|
||||
UISupportedInterfaceOrientations:
|
||||
|
|
@ -74,28 +74,28 @@ targets:
|
|||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
CFBundleURLTypes:
|
||||
- CFBundleURLName: ev.mana.cardecky
|
||||
- CFBundleURLName: ev.mana.wordeck
|
||||
CFBundleURLSchemes:
|
||||
- cards
|
||||
NSUserActivityTypes:
|
||||
- NSUserActivityTypeBrowsingWeb
|
||||
NSPhotoLibraryUsageDescription: "Cardecky greift auf deine Fotos zu, damit du Bilder zu Image-Occlusion-Karten hinzufügen kannst."
|
||||
NSPhotoLibraryUsageDescription: "Wordeck greift auf deine Fotos zu, damit du Bilder zu Image-Occlusion-Karten hinzufügen kannst."
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
entitlements:
|
||||
path: Sources/Resources/CardsNative.entitlements
|
||||
path: Sources/Resources/WordeckNative.entitlements
|
||||
properties:
|
||||
com.apple.security.app-sandbox: true
|
||||
com.apple.security.network.client: true
|
||||
com.apple.security.files.user-selected.read-write: true
|
||||
keychain-access-groups:
|
||||
- $(AppIdentifierPrefix)ev.mana.cardecky
|
||||
- $(AppIdentifierPrefix)ev.mana.wordeck
|
||||
com.apple.developer.associated-domains:
|
||||
- applinks:cardecky.mana.how
|
||||
- applinks:wordeck.com
|
||||
com.apple.security.application-groups:
|
||||
- group.ev.mana.cardecky
|
||||
- group.ev.mana.wordeck
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||
|
|
@ -127,80 +127,80 @@ targets:
|
|||
path: ShareExtension/Resources/CardsShareExtension.entitlements
|
||||
properties:
|
||||
com.apple.security.application-groups:
|
||||
- group.ev.mana.cardecky
|
||||
- group.ev.mana.wordeck
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.share
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.share
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
SKIP_INSTALL: "YES"
|
||||
|
||||
CardsWidgetExtension:
|
||||
WordeckWidgetExtension:
|
||||
type: app-extension
|
||||
supportedDestinations: [iOS]
|
||||
sources:
|
||||
- path: Widgets/CardsWidget
|
||||
- path: Widgets/WordeckWidget
|
||||
excludes:
|
||||
- "Resources/Info.plist"
|
||||
- "Resources/CardsWidgetExtension.entitlements"
|
||||
- "Resources/WordeckWidgetExtension.entitlements"
|
||||
- path: Sources/Core/Sync/WidgetSnapshot.swift
|
||||
info:
|
||||
path: Widgets/CardsWidget/Resources/Info.plist
|
||||
path: Widgets/WordeckWidget/Resources/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: Cardecky Widget
|
||||
CFBundleDisplayName: Wordeck Widget
|
||||
CFBundleShortVersionString: "0.1.0"
|
||||
CFBundleVersion: "11"
|
||||
NSExtension:
|
||||
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||
entitlements:
|
||||
path: Widgets/CardsWidget/Resources/CardsWidgetExtension.entitlements
|
||||
path: Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements
|
||||
properties:
|
||||
com.apple.security.application-groups:
|
||||
- group.ev.mana.cardecky
|
||||
- group.ev.mana.wordeck
|
||||
dependencies:
|
||||
- sdk: WidgetKit.framework
|
||||
- sdk: SwiftUI.framework
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.widget
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.widget
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
SKIP_INSTALL: "YES"
|
||||
INFOPLIST_KEY_CFBundleDisplayName: Cardecky Widget
|
||||
INFOPLIST_KEY_CFBundleDisplayName: Wordeck Widget
|
||||
|
||||
CardsNativeTests:
|
||||
WordeckNativeTests:
|
||||
type: bundle.unit-test
|
||||
supportedDestinations: [iOS, macOS]
|
||||
sources:
|
||||
- Tests/UnitTests
|
||||
dependencies:
|
||||
- target: CardsNative
|
||||
- target: WordeckNative
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.tests
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.tests
|
||||
GENERATE_INFOPLIST_FILE: "YES"
|
||||
|
||||
CardsNativeUITests:
|
||||
WordeckNativeUITests:
|
||||
type: bundle.ui-testing
|
||||
supportedDestinations: [iOS, macOS]
|
||||
sources:
|
||||
- Tests/UITests
|
||||
dependencies:
|
||||
- target: CardsNative
|
||||
- target: WordeckNative
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.uitests
|
||||
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.uitests
|
||||
GENERATE_INFOPLIST_FILE: "YES"
|
||||
|
||||
schemes:
|
||||
CardsNative:
|
||||
WordeckNative:
|
||||
build:
|
||||
targets:
|
||||
CardsNative: all
|
||||
CardsNativeTests: [test]
|
||||
CardsNativeUITests: [test]
|
||||
WordeckNative: all
|
||||
WordeckNativeTests: [test]
|
||||
WordeckNativeUITests: [test]
|
||||
test:
|
||||
targets:
|
||||
- CardsNativeTests
|
||||
- CardsNativeUITests
|
||||
- WordeckNativeTests
|
||||
- WordeckNativeUITests
|
||||
gatherCoverageData: false
|
||||
run:
|
||||
config: Debug
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue