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:
Till JS 2026-05-17 23:10:42 +02:00
parent 4f154bfb39
commit 542082772a
83 changed files with 691 additions and 648 deletions

View file

@ -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
View file

@ -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)

View file

@ -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).

View file

@ -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)
}

View file

@ -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))
}
}
}

View file

@ -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")
}
}

View file

@ -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)
}

View file

@ -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 1060s
/// (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,

View file

@ -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] {

View file

@ -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,

View file

@ -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")!
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)"
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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]

View file

@ -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? {

View file

@ -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? {

View file

@ -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)

View file

@ -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 {

View file

@ -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
)
}

View 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
)
}

View file

@ -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

View file

@ -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)
)
}
}

View file

@ -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)
}
}

View file

@ -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)
)
}

View file

@ -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()

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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))
}

View file

@ -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)

View file

@ -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) {

View file

@ -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 }

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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] = []

View file

@ -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) {

View file

@ -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)
}

View file

@ -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() }

View file

@ -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
}
}

View file

@ -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." : {

View 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>

View file

@ -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")
}
}

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Cloze")
struct ClozeTests {

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Deck-JSON-Decoding")
struct DeckDecodingTests {

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Marketplace-JSON-Decoding")
struct MarketplaceDecodingTests {

View file

@ -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)
}
}

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Mutation Body Encoding")
struct MutationEncodingTests {

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Review-JSON-Decoding")
struct ReviewDecodingTests {

View file

@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import CardsNative
@testable import WordeckNative
@Suite("Typing-Match")
struct TypingTests {

View file

@ -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")
}
}

View file

@ -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()

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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

View file

@ -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.

View file

@ -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`).

View file

@ -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

View file

@ -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