From 542082772a0e56495da9615f5345ef32636101bf Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 17 May 2026 23:10:42 +0200 Subject: [PATCH] =?UTF-8?q?refactor(big-bang):=20cards-native=20=E2=86=92?= =?UTF-8?q?=20wordeck-native?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 42 +++++----- PLAN.md | 56 ++++++------- README.md | 14 ++-- ShareExtension/ShareEditorView.swift | 2 +- ShareExtension/ShareViewController.swift | 2 +- Sources/App/RootView.swift | 26 +++--- ...NativeApp.swift => WordeckNativeApp.swift} | 8 +- ...tion.swift => WordeckAPI+Generation.swift} | 6 +- ...ace.swift => WordeckAPI+Marketplace.swift} | 4 +- .../API/{CardsAPI.swift => WordeckAPI.swift} | 12 +-- Sources/Core/Auth/AppConfig.swift | 10 +-- Sources/Core/Domain/CSVParser.swift | 2 +- Sources/Core/Domain/Deck.swift | 2 +- Sources/Core/Domain/MarketplacePublish.swift | 12 +-- Sources/Core/Intents/StudyAppIntents.swift | 2 +- .../Notifications/NotificationManager.swift | 4 +- Sources/Core/Sync/DeckListStore.swift | 4 +- Sources/Core/Sync/GradeQueue.swift | 4 +- Sources/Core/Sync/MediaCache.swift | 4 +- Sources/Core/Sync/PendingShareStore.swift | 2 +- Sources/Core/Sync/WidgetSnapshot.swift | 2 +- Sources/Core/Telemetry/Log.swift | 2 +- Sources/Core/Theme/CardSurface.swift | 6 +- Sources/Core/Theme/CardsBrand.swift | 25 ------ Sources/Core/Theme/WordeckBrand.swift | 25 ++++++ .../{CardsTheme.swift => WordeckTheme.swift} | 4 +- Sources/Features/Account/AccountView.swift | 44 +++++------ Sources/Features/Decks/DeckCoverTile.swift | 14 ++-- Sources/Features/Decks/DeckDetailView.swift | 54 ++++++------- Sources/Features/Decks/DeckListView.swift | 54 ++++++------- Sources/Features/Decks/DeckPrintView.swift | 4 +- .../Features/Decks/DeckSecondaryActions.swift | 38 ++++----- Sources/Features/Decks/DeckStackTile.swift | 14 ++-- .../Decks/PendingShareConsumeView.swift | 8 +- .../Editor/CSVImportFormSections.swift | 8 +- .../Editor/CardEditorMediaFields.swift | 6 +- .../Features/Editor/CardEditorPayload.swift | 8 +- Sources/Features/Editor/CardEditorView.swift | 14 ++-- Sources/Features/Editor/DeckEditorView.swift | 28 +++---- Sources/Features/Editor/MaskEditorView.swift | 16 ++-- Sources/Features/Marketplace/BrowseView.swift | 22 +++--- .../Features/Marketplace/ExploreView.swift | 28 +++---- .../Marketplace/MarketplacePublishView.swift | 22 +++--- .../Marketplace/MarketplaceStore.swift | 4 +- .../Features/Marketplace/PublicDeckView.swift | 54 ++++++------- .../Marketplace/ReportDeckSheet.swift | 8 +- .../Features/Media/AudioPlayerButton.swift | 8 +- Sources/Features/Media/RemoteImage.swift | 6 +- .../Settings/BlockedAuthorsView.swift | 16 ++-- Sources/Features/Settings/SettingsView.swift | 4 +- Sources/Features/Study/CardRenderer.swift | 24 +++--- .../Study/MultipleChoiceCardView.swift | 44 +++++------ Sources/Features/Study/RatingBar.swift | 30 +++---- Sources/Features/Study/StudySession.swift | 4 +- Sources/Features/Study/StudySessionView.swift | 24 +++--- Sources/Features/Study/TypingCardView.swift | 28 +++---- Sources/Resources/Localizable.xcstrings | 6 +- Sources/Resources/WordeckNative.entitlements | 24 ++++++ ...Tests.swift => WordeckNativeUITests.swift} | 8 +- Tests/UnitTests/ClozeTests.swift | 2 +- Tests/UnitTests/DeckDecodingTests.swift | 2 +- .../UnitTests/MarketplaceDecodingTests.swift | 2 +- Tests/UnitTests/MaskRegionsTests.swift | 79 ------------------- Tests/UnitTests/MutationEncodingTests.swift | 2 +- Tests/UnitTests/ReviewDecodingTests.swift | 2 +- Tests/UnitTests/TypingTests.swift | 2 +- ...veTests.swift => WordeckNativeTests.swift} | 12 +-- .../DueProvider.swift | 0 .../DueWidgetView.swift | 48 +++++++++++ .../Resources/Assets.xcassets/Contents.json | 0 .../CardsWidgetExtension.entitlements | 10 +++ Widgets/WordeckWidget/Resources/Info.plist | 29 +++++++ .../WordeckWidgetExtension.entitlements | 10 +++ .../WordeckDueWidget.swift} | 5 +- .../WordeckWidgetBundle.swift} | 4 +- devlog/2026-05-12/macher.md | 6 +- devlog/2026-05-12/spieler.md | 8 +- devlog/2026-05-13/data.json | 8 +- devlog/2026-05-13/macher.md | 10 +-- devlog/2026-05-13/spieler.md | 6 +- docs/MARKETING_COPY.md | 30 +++---- docs/RELEASE_CHECKLIST.md | 34 ++++---- project.yml | 72 ++++++++--------- 83 files changed, 691 insertions(+), 648 deletions(-) rename Sources/App/{CardsNativeApp.swift => WordeckNativeApp.swift} (78%) rename Sources/Core/API/{CardsAPI+Generation.swift => WordeckAPI+Generation.swift} (97%) rename Sources/Core/API/{CardsAPI+Marketplace.swift => WordeckAPI+Marketplace.swift} (98%) rename Sources/Core/API/{CardsAPI.swift => WordeckAPI.swift} (97%) delete mode 100644 Sources/Core/Theme/CardsBrand.swift create mode 100644 Sources/Core/Theme/WordeckBrand.swift rename Sources/Core/Theme/{CardsTheme.swift => WordeckTheme.swift} (93%) create mode 100644 Sources/Resources/WordeckNative.entitlements rename Tests/UITests/{CardsNativeUITests.swift => WordeckNativeUITests.swift} (70%) delete mode 100644 Tests/UnitTests/MaskRegionsTests.swift rename Tests/UnitTests/{CardsNativeTests.swift => WordeckNativeTests.swift} (66%) rename Widgets/{CardsWidget => WordeckWidget}/DueProvider.swift (100%) rename Widgets/{CardsWidget => WordeckWidget}/DueWidgetView.swift (63%) rename Widgets/{CardsWidget => WordeckWidget}/Resources/Assets.xcassets/Contents.json (100%) create mode 100644 Widgets/WordeckWidget/Resources/CardsWidgetExtension.entitlements create mode 100644 Widgets/WordeckWidget/Resources/Info.plist create mode 100644 Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements rename Widgets/{CardsWidget/CardsDueWidget.swift => WordeckWidget/WordeckDueWidget.swift} (84%) rename Widgets/{CardsWidget/CardsWidgetBundle.swift => WordeckWidget/WordeckWidgetBundle.swift} (82%) diff --git a/CLAUDE.md b/CLAUDE.md index cf17405..7ea8a1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# CLAUDE.md — cards-native repo +# CLAUDE.md — wordeck-native repo Guidance für Claude Code in diesem Repository. @@ -10,27 +10,27 @@ Guidance für Claude Code in diesem Repository. ## Was dieses Repo ist **Cards Native** — native SwiftUI-Universal-App (iOS / iPadOS / macOS) -für **Cardecky**, die Spaced-Repetition-Karten-App des Vereins -**mana e.V.** Web-Parität zu `cardecky.mana.how`, plus native iOS- +für **Wordeck**, die Spaced-Repetition-Karten-App des Vereins +**mana e.V.** Web-Parität zu `wordeck.com`, plus native iOS- Affordances (Widgets, Notifications, Universal-Links, Pencil). ``` HTTPS/JWT ┌──────────────────┐ - cards-api ◄───────────── │ cards-native │ SwiftUI - cardecky-api.mana.how │ (this repo) │ SwiftData (Cache) - │ ev.mana.cardecky │ WidgetKit (β-6) + cards-api ◄───────────── │ wordeck-native │ SwiftUI + api.wordeck.com │ (this repo) │ SwiftData (Cache) + │ ev.mana.wordeck │ WidgetKit (β-6) └──────────────────┘ ``` ## Status **Phase β-0 — Setup (2026-05-12).** Repo-Skelett, ManaCore + ManaTokens -als Package-Dependency, Login + Cardecky-API-Reachability-Probe. +als Package-Dependency, Login + Wordeck-API-Reachability-Probe. Phasen β-1 bis β-7 in `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`. ## Leitprinzip: Web-Parität -Die Web-App auf `cardecky.mana.how` ist Funktions-Referenz. Bei +Die Web-App auf `wordeck.com` ist Funktions-Referenz. Bei Konflikt zwischen Native und Web → **Web gewinnt**. Native ist Re-Implementation, kein neues Produkt. @@ -53,12 +53,12 @@ Beschlossen. Nicht ohne explizite Diskussion antasten. Auth-Implementierung ist verboten. 4. **Pure SwiftUI.** Keine externen UI-Libraries. AppKit/UIKit nur als Bridge wenn zwingend (z.B. `PencilKit` für Image-Occlusion). -5. **Bundle-ID `ev.mana.cardecky`.** Reverse-Domain mana-ev.ch. - Universal-Link-Domain: `cardecky.mana.how`. +5. **Bundle-ID `ev.mana.wordeck`.** Reverse-Domain mana-ev.ch. + Universal-Link-Domain: `wordeck.com`. 6. **Cards-Domain-Logik bleibt am Server.** SubIndex-Berechnung für Cloze, Image-Occlusion-Mask-Validation, Content-Hash — alles Server. Native zeigt nur, was vom Server kommt. -7. **`forest`-Theme.** Heute lokal in `CardsTheme.swift` nachgebaut +7. **`forest`-Theme.** Heute lokal in `WordeckTheme.swift` nachgebaut (Werte gespiegelt aus `mana/packages/themes/src/variants/forest.css`). Migration auf ManaTokens-Theme-Switch ist Phase ε. 8. **Web gewinnt bei Konflikt.** Eleganteres Native-Verhalten geht @@ -74,16 +74,16 @@ Beschlossen. Nicht ohne explizite Diskussion antasten. sind **nicht** im Git - **SwiftFormat** mit `.swiftformat` (4-space, 120-col, sorted imports) - **SwiftLint** mit `.swiftlint.yml` -- **Logging:** App-Subsystem `ev.mana.cardecky` via +- **Logging:** App-Subsystem `ev.mana.wordeck` via `Sources/Core/Telemetry/Log.swift`. ManaCore loggt parallel unter `ev.mana.core` - **Persistenz:** SwiftData für Deck/Card-Cache (ab β-1), JWT im Keychain (über ManaCore) - **Lokalisierung:** DE primary, EN fallback via `Localizable.xcstrings` -## Cardecky-API-Wire-Format +## Wordeck-API-Wire-Format -Wire-Format gegen `https://cardecky-api.mana.how/api/v1/*`. Quelle der +Wire-Format gegen `https://api.wordeck.com/api/v1/*`. Quelle der Wahrheit: `../cards/apps/api/src/routes/*.ts`. Bei neuem DTO verifizieren: @@ -95,14 +95,14 @@ verifizieren: ## Repo-Layout ``` -cards-native/ +wordeck-native/ ├── project.yml XcodeGen-Manifest (SOT) ├── PLAN.md Phase-Tracking (gekürzt aus Greenfield-Plan) ├── CLAUDE.md dieses File ├── README.md ├── .swiftformat, .swiftlint.yml ├── Sources/ -│ ├── App/ CardsNativeApp (@main), RootView +│ ├── App/ WordeckNativeApp (@main), RootView │ ├── Features/ │ │ ├── Account/ LoginView, AccountView (ab β-1) │ │ ├── Decks/ DashboardView (Placeholder), DeckList (β-1) @@ -113,19 +113,19 @@ cards-native/ │ │ └── Imports/ (β-3) │ ├── Core/ │ │ ├── Auth/ AppConfig (ManaAppConfig-Provider) -│ │ ├── API/ CardsAPI (AuthenticatedTransport-Wrapper) +│ │ ├── API/ WordeckAPI (AuthenticatedTransport-Wrapper) │ │ ├── Domain/ (Card-Type-Enums, Rating-Enum — ab β-2) │ │ ├── Storage/ (SwiftData-Models — ab β-1) │ │ ├── Sync/ (ReviewQueue, MediaCache — ab β-2/β-4) -│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.cardecky) -│ │ └── Theme/ CardsTheme (forest-Werte) +│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.wordeck) +│ │ └── Theme/ WordeckTheme (forest-Werte) │ ├── Widgets/ (WidgetKit-Extension — ab β-6) │ ├── ShareExtension/ (Save-as-Card — ab β-6) │ └── Resources/ │ ├── Assets.xcassets │ ├── Localizable.xcstrings │ ├── Info.plist (generiert, gitignored) -│ └── CardsNative.entitlements (generiert, gitignored) +│ └── WordeckNative.entitlements (generiert, gitignored) ├── Tests/ │ ├── UnitTests/ │ └── UITests/ @@ -156,7 +156,7 @@ cards-native/ **Workflow:** ```bash xcodegen generate -open CardsNative.xcodeproj +open WordeckNative.xcodeproj ``` **Vor jedem Commit:** diff --git a/PLAN.md b/PLAN.md index b538fe4..1a50c47 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,17 +1,17 @@ -# Plan — cards-native (SwiftUI Universal) +# Plan — wordeck-native (SwiftUI Universal) **Stand: 2026-05-13 — TestFlight Build 11 (v0.9.4).** Alle Phasen β-0 bis β-7 + Polish-Iterationen. 43 Unit-Tests + 1 UI-Test grün. -**Cardecky-Web-Look übernommen** (v0.9.0 ff.): Fan-Stack-Tiles +**Wordeck-Web-Look übernommen** (v0.9.0 ff.): Fan-Stack-Tiles (5:7 Aspect, 3 rotierte Background-Layer), CardSurface in md/lg/hero, RatingBar mit Good-Emphasis. Tap auf Tile = Study- Mode, Pencil-Icon unten rechts = Deck-Detail. Identische Tile- Optik in Decks- und Entdecken-Tab. -**App-Store-Connect:** Cardecky, App-ID 6769019526, Bundle -`ev.mana.cardecky`, Team `QP3GLU8PH3`. AASA + /privacy + /help -live unter `cardecky.mana.how`. +**App-Store-Connect:** Wordeck, App-ID 6769019526, Bundle +`ev.mana.wordeck`, Team `QP3GLU8PH3`. AASA + /privacy + /help +live unter `wordeck.com`. Pflicht-Check für β-2: Endurance-Test auf realem Gerät (200+ Karten mit Flugmodus zwischendurch) steht aus. @@ -23,14 +23,14 @@ mit Flugmodus zwischendurch) steht aus. ## Aktueller Stand ✅ **β-0 — Setup (2026-05-12, Tag `v0.1.0`)** -- Repo-Skelett unter `git.mana.how/till/cards-native` -- `project.yml` mit Bundle-ID `ev.mana.cardecky`, ManaSwiftCore via +- Repo-Skelett unter `git.mana.how/till/wordeck-native` +- `project.yml` mit Bundle-ID `ev.mana.wordeck`, ManaSwiftCore via `path: ../mana-swift-core` - `AppConfig` als `ManaAppConfig`-Provider: - Auth: `https://auth.mana.how` - - API: `https://cardecky-api.mana.how` - - Keychain-Service: `ev.mana.cardecky` -- `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus + - API: `https://api.wordeck.com` + - Keychain-Service: `ev.mana.wordeck` +- `WordeckTheme.swift` mit forest-Werten (lokal nachgebaut aus `mana/packages/themes/src/variants/forest.css`) - `LoginView` (Email/PW gegen mana-auth) - 3 Unit-Tests (AppConfig) @@ -38,22 +38,22 @@ mit Flugmodus zwischendurch) steht aus. ✅ **Post-β-7-Polish (2026-05-13, Tags v0.8.1 → v0.9.4)** Live-Fixes nach Apple-Submission und Real-Device-Tests: -- `v0.8.1` Cardecky-Rebrand (Bundle `ev.mana.cards` → `ev.mana.cardecky`, +- `v0.8.1` Wordeck-Rebrand (Bundle `ev.mana.cards` → `ev.mana.wordeck`, AASA + Docker-Compose-Env nachgezogen) - `v0.8.2` Archive-Polish — Versions-Sync zwischen Targets, iPad- Orientations - `v0.8.3` Sendable + AppIcon-Asset-Cleanup, /privacy + /help Stubs - `v0.8.4` PhotosPicker Sendable-Warning via Sub-View-Struct -- `v0.8.5` ITMS-90129-Fix: DisplayName Cards → Cardecky, Build 2 -- `v0.8.6` Cardecky-Rebrand User-facing Strings durchgängig +- `v0.8.5` ITMS-90129-Fix: DisplayName Cards → Wordeck, Build 2 +- `v0.8.6` Wordeck-Rebrand User-facing Strings durchgängig - `v0.8.7` PublicDeckOwner.pseudonym Bool statt String? (Decoder-Crash bei Marketplace-Deck-Open) -- `v0.8.8` Card-Liste in DeckDetailView + CardsAPI.listCards +- `v0.8.8` Card-Liste in DeckDetailView + WordeckAPI.listCards - `v0.8.9` URL-Query-Bug-Fix in ManaCore.AuthenticatedTransport (URL.appending(path:) encoded `?` → 404; gefixt via String-Concat, ManaCore v1.0.1). Behob alle "0-Karten"-Phänomene und das stille Schlucken von Query-Endpoints. -- `v0.9.0` Cardecky-Web-Design: Fan-Stack-Tiles, CardSurface in +- `v0.9.0` Wordeck-Web-Design: Fan-Stack-Tiles, CardSurface in 3 Sizes, RatingBar mit Good-Emphasis, horizontale Scroll-Sections - `v0.9.1` Multiple-Choice-Karten gerendert (Distractors via Server, Tap-Selektion, Reveal mit Korrekt/Falsch-Highlight) @@ -104,17 +104,17 @@ gleicht Schema-Drift in der Production-DB aus. - `WidgetSnapshot` Codable mit `topDecks` (Top-3 nach dueCount) und `totalDueCount` - `WidgetSnapshotStore` schreibt in App-Group-Container - `group.ev.mana.cardecky` + `group.ev.mana.wordeck` - `DeckListStore.refresh` ruft `updateWidgetSnapshot()` und `WidgetCenter.shared.reloadAllTimelines()` nach jedem Pull -- `CardsWidgetExtension`-Target (eigenes app-extension-Bundle): - `CardsWidgetBundle` + `CardsDueWidget` mit `StaticConfiguration`, +- `WordeckWidgetExtension`-Target (eigenes app-extension-Bundle): + `WordeckWidgetBundle` + `CardsDueWidget` mit `StaticConfiguration`, Support für systemSmall, systemMedium, accessoryCircular, accessoryInline, accessoryRectangular - `DueProvider` als `TimelineProvider`: liest Snapshot, plant Refresh alle 30 min (plus instant-Refresh via Haupt-App) - `DueWidgetView` mit Family-Switch, alle 5 Family-Layouts -- `com.apple.security.application-groups: group.ev.mana.cardecky` +- `com.apple.security.application-groups: group.ev.mana.wordeck` im Haupt- und Widget-Entitlement - `WidgetSnapshot.swift` in beiden Targets via XcodeGen-source-array (single-source-of-truth) @@ -128,7 +128,7 @@ Widget decken 90% des Native-Polish ab. `PublicDeckDetail`, `ExploreResponse`, `BrowseResponse`, `SubscribeResponse` Codable-DTOs mit snake_case - `MarketplaceSort` Enum (recent/popular/trending) mit deutschen Labels -- `CardsAPI`: explore(), browseMarketplace(query:sort:language:), +- `WordeckAPI`: explore(), browseMarketplace(query:sort:language:), publicDeck(slug:), subscribe(slug:), unsubscribe(slug:) - `MarketplaceStore` @Observable mit Explore-State + Browse-State - `ExploreView` mit Featured + Trending Carousels, Browse-Link @@ -138,13 +138,13 @@ Widget decken 90% des Native-Polish ab. - `PublicDeckCard` + `BrowseRow` Komponenten mit forest-Theme - `RootView` → TabBar (Decks / Entdecken / Account) statt Single-View - Universal-Link-Handler in `RootView` (onOpenURL + onContinueUserActivity): - `https://cardecky.mana.how/d/` und `cards://d/` → Explore-Tab + `https://wordeck.com/d/` und `cards://d/` → 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) diff --git a/README.md b/README.md index 2967c18..4ab75c5 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -# cards-native +# wordeck-native -Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Cardecky — +Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Wordeck — die Spaced-Repetition-Karten-App des Vereins **mana e.V.** > **Web-App-Parität.** Die existierende Web-App auf -> `cardecky.mana.how` ist Funktions- und Verhaltens-Referenz. +> `wordeck.com` ist Funktions- und Verhaltens-Referenz. > Native bringt kein neues Produkt, sondern die App in einer Form, > die iOS-Hardware besser nutzt. ## Status -**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Cardecky-API- +**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Wordeck-API- Reachability-Check. Vollständiger Phasen-Plan in `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`. ``` HTTPS/JWT ┌──────────────────┐ - cards-api ◄───────────── │ cards-native │ SwiftUI - cardecky-api.mana.how │ ev.mana.cardecky │ WidgetKit (β-6) + cards-api ◄───────────── │ wordeck-native │ SwiftUI + api.wordeck.com │ ev.mana.wordeck │ WidgetKit (β-6) └──────────────────┘ │ ┌─────────────────────────────────────────┐ @@ -30,7 +30,7 @@ Reachability-Check. Vollständiger Phasen-Plan in ```bash xcodegen generate -open CardsNative.xcodeproj # iPhone-17-Simulator +open WordeckNative.xcodeproj # iPhone-17-Simulator ``` Konventionen, Invarianten, Phasen-Disziplin: [`CLAUDE.md`](CLAUDE.md). diff --git a/ShareExtension/ShareEditorView.swift b/ShareExtension/ShareEditorView.swift index f7b214b..0c84e3a 100644 --- a/ShareExtension/ShareEditorView.swift +++ b/ShareExtension/ShareEditorView.swift @@ -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) } diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index 81104f0..73b98a4 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -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)) } } } diff --git a/Sources/App/RootView.swift b/Sources/App/RootView.swift index 6bcfe71..4e7fb35 100644 --- a/Sources/App/RootView.swift +++ b/Sources/App/RootView.swift @@ -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/` → Explore-Tab + PublicDeckView - /// - `https://cardecky.mana.how/auth/reset?token=…` → ManaResetPasswordView + /// - `https://wordeck.com/d/` → Explore-Tab + PublicDeckView + /// - `https://wordeck.com/auth/reset?token=…` → ManaResetPasswordView /// - `cards://study/` → 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") } } diff --git a/Sources/App/CardsNativeApp.swift b/Sources/App/WordeckNativeApp.swift similarity index 78% rename from Sources/App/CardsNativeApp.swift rename to Sources/App/WordeckNativeApp.swift index dfb6626..220182a 100644 --- a/Sources/App/CardsNativeApp.swift +++ b/Sources/App/WordeckNativeApp.swift @@ -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) } diff --git a/Sources/Core/API/CardsAPI+Generation.swift b/Sources/Core/API/WordeckAPI+Generation.swift similarity index 97% rename from Sources/Core/API/CardsAPI+Generation.swift rename to Sources/Core/API/WordeckAPI+Generation.swift index c656c54..a95a444 100644 --- a/Sources/Core/API/CardsAPI+Generation.swift +++ b/Sources/Core/API/WordeckAPI+Generation.swift @@ -1,9 +1,9 @@ import Foundation import ManaCore -/// AI-Deck-Generierung + Multipart-Helpers — ausgelagert aus `CardsAPI`, +/// AI-Deck-Generierung + Multipart-Helpers — ausgelagert aus `WordeckAPI`, /// damit der Haupt-Actor unter der Type-Body-Length-Grenze bleibt. -extension CardsAPI { +extension WordeckAPI { /// `POST /api/v1/decks/generate` — KI generiert Deck aus Prompt. /// Rate-Limit serverseitig 10/min. Antwort dauert typisch 10–60s /// (synchron, kein Streaming). @@ -28,7 +28,7 @@ extension CardsAPI { count: Int, url: String? ) async throws -> DeckGenerateResponse { - let boundary = "cards-native-\(UUID().uuidString)" + let boundary = "wordeck-native-\(UUID().uuidString)" let body = makeFromImageMultipartBody( files: files, language: language, diff --git a/Sources/Core/API/CardsAPI+Marketplace.swift b/Sources/Core/API/WordeckAPI+Marketplace.swift similarity index 98% rename from Sources/Core/API/CardsAPI+Marketplace.swift rename to Sources/Core/API/WordeckAPI+Marketplace.swift index cf78fc7..bac253f 100644 --- a/Sources/Core/API/CardsAPI+Marketplace.swift +++ b/Sources/Core/API/WordeckAPI+Marketplace.swift @@ -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] { diff --git a/Sources/Core/API/CardsAPI.swift b/Sources/Core/API/WordeckAPI.swift similarity index 97% rename from Sources/Core/API/CardsAPI.swift rename to Sources/Core/API/WordeckAPI.swift index 5c9ce6e..f4915db 100644 --- a/Sources/Core/API/CardsAPI.swift +++ b/Sources/Core/API/WordeckAPI.swift @@ -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, diff --git a/Sources/Core/Auth/AppConfig.swift b/Sources/Core/Auth/AppConfig.swift index d268ba5..e52977e 100644 --- a/Sources/Core/Auth/AppConfig.swift +++ b/Sources/Core/Auth/AppConfig.swift @@ -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")! } diff --git a/Sources/Core/Domain/CSVParser.swift b/Sources/Core/Domain/CSVParser.swift index 3f44cb7..0750dc0 100644 --- a/Sources/Core/Domain/CSVParser.swift +++ b/Sources/Core/Domain/CSVParser.swift @@ -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 { diff --git a/Sources/Core/Domain/Deck.swift b/Sources/Core/Domain/Deck.swift index 6e7dacc..c7f2a3c 100644 --- a/Sources/Core/Domain/Deck.swift +++ b/Sources/Core/Domain/Deck.swift @@ -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 } diff --git a/Sources/Core/Domain/MarketplacePublish.swift b/Sources/Core/Domain/MarketplacePublish.swift index a5323c3..e19238a 100644 --- a/Sources/Core/Domain/MarketplacePublish.swift +++ b/Sources/Core/Domain/MarketplacePublish.swift @@ -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)" } } } diff --git a/Sources/Core/Intents/StudyAppIntents.swift b/Sources/Core/Intents/StudyAppIntents.swift index ac300df..b2587c9 100644 --- a/Sources/Core/Intents/StudyAppIntents.swift +++ b/Sources/Core/Intents/StudyAppIntents.swift @@ -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 diff --git a/Sources/Core/Notifications/NotificationManager.swift b/Sources/Core/Notifications/NotificationManager.swift index e3bcf6f..f96471e 100644 --- a/Sources/Core/Notifications/NotificationManager.swift +++ b/Sources/Core/Notifications/NotificationManager.swift @@ -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 diff --git a/Sources/Core/Sync/DeckListStore.swift b/Sources/Core/Sync/DeckListStore.swift index 28b1a37..a56ce8c 100644 --- a/Sources/Core/Sync/DeckListStore.swift +++ b/Sources/Core/Sync/DeckListStore.swift @@ -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 } diff --git a/Sources/Core/Sync/GradeQueue.swift b/Sources/Core/Sync/GradeQueue.swift index d95910c..b88c370 100644 --- a/Sources/Core/Sync/GradeQueue.swift +++ b/Sources/Core/Sync/GradeQueue.swift @@ -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 } diff --git a/Sources/Core/Sync/MediaCache.swift b/Sources/Core/Sync/MediaCache.swift index e9ab815..2406f8d 100644 --- a/Sources/Core/Sync/MediaCache.swift +++ b/Sources/Core/Sync/MediaCache.swift @@ -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] diff --git a/Sources/Core/Sync/PendingShareStore.swift b/Sources/Core/Sync/PendingShareStore.swift index 9aefb47..8e753d7 100644 --- a/Sources/Core/Sync/PendingShareStore.swift +++ b/Sources/Core/Sync/PendingShareStore.swift @@ -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? { diff --git a/Sources/Core/Sync/WidgetSnapshot.swift b/Sources/Core/Sync/WidgetSnapshot.swift index a0bf27b..9f4f0cc 100644 --- a/Sources/Core/Sync/WidgetSnapshot.swift +++ b/Sources/Core/Sync/WidgetSnapshot.swift @@ -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? { diff --git a/Sources/Core/Telemetry/Log.swift b/Sources/Core/Telemetry/Log.swift index 5970cf3..b5ea049 100644 --- a/Sources/Core/Telemetry/Log.swift +++ b/Sources/Core/Telemetry/Log.swift @@ -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) diff --git a/Sources/Core/Theme/CardSurface.swift b/Sources/Core/Theme/CardSurface.swift index f2c3a60..b60d520 100644 --- a/Sources/Core/Theme/CardSurface.swift +++ b/Sources/Core/Theme/CardSurface.swift @@ -43,10 +43,10 @@ struct CardSurface: 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: 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 { diff --git a/Sources/Core/Theme/CardsBrand.swift b/Sources/Core/Theme/CardsBrand.swift deleted file mode 100644 index 73227a2..0000000 --- a/Sources/Core/Theme/CardsBrand.swift +++ /dev/null @@ -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 - ) -} diff --git a/Sources/Core/Theme/WordeckBrand.swift b/Sources/Core/Theme/WordeckBrand.swift new file mode 100644 index 0000000..5327a2b --- /dev/null +++ b/Sources/Core/Theme/WordeckBrand.swift @@ -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 + ) +} diff --git a/Sources/Core/Theme/CardsTheme.swift b/Sources/Core/Theme/WordeckTheme.swift similarity index 93% rename from Sources/Core/Theme/CardsTheme.swift rename to Sources/Core/Theme/WordeckTheme.swift index 46e3855..bb99147 100644 --- a/Sources/Core/Theme/CardsTheme.swift +++ b/Sources/Core/Theme/WordeckTheme.swift @@ -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.` /// (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 diff --git a/Sources/Features/Account/AccountView.swift b/Sources/Features/Account/AccountView.swift index dabd824..8c7093d 100644 --- a/Sources/Features/Account/AccountView.swift +++ b/Sources/Features/Account/AccountView.swift @@ -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) ) } } diff --git a/Sources/Features/Decks/DeckCoverTile.swift b/Sources/Features/Decks/DeckCoverTile.swift index 77aa5bd..5f76b25 100644 --- a/Sources/Features/Decks/DeckCoverTile.swift +++ b/Sources/Features/Decks/DeckCoverTile.swift @@ -37,15 +37,15 @@ struct DeckCoverTile: 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: 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: 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) } } diff --git a/Sources/Features/Decks/DeckDetailView.swift b/Sources/Features/Decks/DeckDetailView.swift index f764952..c9e9000 100644 --- a/Sources/Features/Decks/DeckDetailView.swift +++ b/Sources/Features/Decks/DeckDetailView.swift @@ -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) ) } diff --git a/Sources/Features/Decks/DeckListView.swift b/Sources/Features/Decks/DeckListView.swift index b2a0705..56cfd43 100644 --- a/Sources/Features/Decks/DeckListView.swift +++ b/Sources/Features/Decks/DeckListView.swift @@ -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() diff --git a/Sources/Features/Decks/DeckPrintView.swift b/Sources/Features/Decks/DeckPrintView.swift index 57b7bdf..5c6eadc 100644 --- a/Sources/Features/Decks/DeckPrintView.swift +++ b/Sources/Features/Decks/DeckPrintView.swift @@ -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 { diff --git a/Sources/Features/Decks/DeckSecondaryActions.swift b/Sources/Features/Decks/DeckSecondaryActions.swift index 84dfd99..d9325ff 100644 --- a/Sources/Features/Decks/DeckSecondaryActions.swift +++ b/Sources/Features/Decks/DeckSecondaryActions.swift @@ -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) } diff --git a/Sources/Features/Decks/DeckStackTile.swift b/Sources/Features/Decks/DeckStackTile.swift index 19617e1..ed20760 100644 --- a/Sources/Features/Decks/DeckStackTile.swift +++ b/Sources/Features/Decks/DeckStackTile.swift @@ -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) diff --git a/Sources/Features/Decks/PendingShareConsumeView.swift b/Sources/Features/Decks/PendingShareConsumeView.swift index 00266bb..d869632 100644 --- a/Sources/Features/Decks/PendingShareConsumeView.swift +++ b/Sources/Features/Decks/PendingShareConsumeView.swift @@ -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, diff --git a/Sources/Features/Editor/CSVImportFormSections.swift b/Sources/Features/Editor/CSVImportFormSections.swift index 3ce4d0e..d652b50 100644 --- a/Sources/Features/Editor/CSVImportFormSections.swift +++ b/Sources/Features/Editor/CSVImportFormSections.swift @@ -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) } } diff --git a/Sources/Features/Editor/CardEditorMediaFields.swift b/Sources/Features/Editor/CardEditorMediaFields.swift index 77cd3b8..1f30645 100644 --- a/Sources/Features/Editor/CardEditorMediaFields.swift +++ b/Sources/Features/Editor/CardEditorMediaFields.swift @@ -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) } } diff --git a/Sources/Features/Editor/CardEditorPayload.swift b/Sources/Features/Editor/CardEditorPayload.swift index 989de04..7a3b947 100644 --- a/Sources/Features/Editor/CardEditorPayload.swift +++ b/Sources/Features/Editor/CardEditorPayload.swift @@ -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 diff --git a/Sources/Features/Editor/CardEditorView.swift b/Sources/Features/Editor/CardEditorView.swift index ef2898e..4132bcb 100644 --- a/Sources/Features/Editor/CardEditorView.swift +++ b/Sources/Features/Editor/CardEditorView.swift @@ -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) diff --git a/Sources/Features/Editor/DeckEditorView.swift b/Sources/Features/Editor/DeckEditorView.swift index 16eb230..aa8f9f1 100644 --- a/Sources/Features/Editor/DeckEditorView.swift +++ b/Sources/Features/Editor/DeckEditorView.swift @@ -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 diff --git a/Sources/Features/Editor/MaskEditorView.swift b/Sources/Features/Editor/MaskEditorView.swift index 2edf644..2aad021 100644 --- a/Sources/Features/Editor/MaskEditorView.swift +++ b/Sources/Features/Editor/MaskEditorView.swift @@ -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) } diff --git a/Sources/Features/Marketplace/BrowseView.swift b/Sources/Features/Marketplace/BrowseView.swift index a96f961..c364631 100644 --- a/Sources/Features/Marketplace/BrowseView.swift +++ b/Sources/Features/Marketplace/BrowseView.swift @@ -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) } diff --git a/Sources/Features/Marketplace/ExploreView.swift b/Sources/Features/Marketplace/ExploreView.swift index f204527..c174564 100644 --- a/Sources/Features/Marketplace/ExploreView.swift +++ b/Sources/Features/Marketplace/ExploreView.swift @@ -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) } } } diff --git a/Sources/Features/Marketplace/MarketplacePublishView.swift b/Sources/Features/Marketplace/MarketplacePublishView.swift index b98f7c6..729d172 100644 --- a/Sources/Features/Marketplace/MarketplacePublishView.swift +++ b/Sources/Features/Marketplace/MarketplacePublishView.swift @@ -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/.") + Text("Der Slug wird Teil der Marketplace-URL: wordeck.com/d/.") } } @@ -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 diff --git a/Sources/Features/Marketplace/MarketplaceStore.swift b/Sources/Features/Marketplace/MarketplaceStore.swift index 6bceb0c..90bec5b 100644 --- a/Sources/Features/Marketplace/MarketplaceStore.swift +++ b/Sources/Features/Marketplace/MarketplaceStore.swift @@ -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 { diff --git a/Sources/Features/Marketplace/PublicDeckView.swift b/Sources/Features/Marketplace/PublicDeckView.swift index 0dd3bd7..9cbcc83 100644 --- a/Sources/Features/Marketplace/PublicDeckView.swift +++ b/Sources/Features/Marketplace/PublicDeckView.swift @@ -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 diff --git a/Sources/Features/Marketplace/ReportDeckSheet.swift b/Sources/Features/Marketplace/ReportDeckSheet.swift index 58007e1..026c7be 100644 --- a/Sources/Features/Marketplace/ReportDeckSheet.swift +++ b/Sources/Features/Marketplace/ReportDeckSheet.swift @@ -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)) } diff --git a/Sources/Features/Media/AudioPlayerButton.swift b/Sources/Features/Media/AudioPlayerButton.swift index 6566abf..1aec08b 100644 --- a/Sources/Features/Media/AudioPlayerButton.swift +++ b/Sources/Features/Media/AudioPlayerButton.swift @@ -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) diff --git a/Sources/Features/Media/RemoteImage.swift b/Sources/Features/Media/RemoteImage.swift index 67cc041..52fd790 100644 --- a/Sources/Features/Media/RemoteImage.swift +++ b/Sources/Features/Media/RemoteImage.swift @@ -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) { diff --git a/Sources/Features/Settings/BlockedAuthorsView.swift b/Sources/Features/Settings/BlockedAuthorsView.swift index 0b70d02..1a5f7af 100644 --- a/Sources/Features/Settings/BlockedAuthorsView.swift +++ b/Sources/Features/Settings/BlockedAuthorsView.swift @@ -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 } diff --git a/Sources/Features/Settings/SettingsView.swift b/Sources/Features/Settings/SettingsView.swift index 9a36d9c..feed9c5 100644 --- a/Sources/Features/Settings/SettingsView.swift +++ b/Sources/Features/Settings/SettingsView.swift @@ -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") } } diff --git a/Sources/Features/Study/CardRenderer.swift b/Sources/Features/Study/CardRenderer.swift index 1ad002d..14f0d1b 100644 --- a/Sources/Features/Study/CardRenderer.swift +++ b/Sources/Features/Study/CardRenderer.swift @@ -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) } } diff --git a/Sources/Features/Study/MultipleChoiceCardView.swift b/Sources/Features/Study/MultipleChoiceCardView.swift index b7d58d1..545d7ef 100644 --- a/Sources/Features/Study/MultipleChoiceCardView.swift +++ b/Sources/Features/Study/MultipleChoiceCardView.swift @@ -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] = [] diff --git a/Sources/Features/Study/RatingBar.swift b/Sources/Features/Study/RatingBar.swift index d31b127..833734f 100644 --- a/Sources/Features/Study/RatingBar.swift +++ b/Sources/Features/Study/RatingBar.swift @@ -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) { diff --git a/Sources/Features/Study/StudySession.swift b/Sources/Features/Study/StudySession.swift index 3dfe5e5..bacbb6a 100644 --- a/Sources/Features/Study/StudySession.swift +++ b/Sources/Features/Study/StudySession.swift @@ -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) } diff --git a/Sources/Features/Study/StudySessionView.swift b/Sources/Features/Study/StudySessionView.swift index 7a31faf..169a12a 100644 --- a/Sources/Features/Study/StudySessionView.swift +++ b/Sources/Features/Study/StudySessionView.swift @@ -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() } diff --git a/Sources/Features/Study/TypingCardView.swift b/Sources/Features/Study/TypingCardView.swift index f3eceb7..5cdcaa1 100644 --- a/Sources/Features/Study/TypingCardView.swift +++ b/Sources/Features/Study/TypingCardView.swift @@ -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 } } diff --git a/Sources/Resources/Localizable.xcstrings b/Sources/Resources/Localizable.xcstrings index 682888c..ce84952 100644 --- a/Sources/Resources/Localizable.xcstrings +++ b/Sources/Resources/Localizable.xcstrings @@ -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/." : { + "Der Slug wird Teil der Marketplace-URL: wordeck.com/d/." : { }, "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." : { diff --git a/Sources/Resources/WordeckNative.entitlements b/Sources/Resources/WordeckNative.entitlements new file mode 100644 index 0000000..c1ded93 --- /dev/null +++ b/Sources/Resources/WordeckNative.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.developer.associated-domains + + applinks:wordeck.com + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.ev.mana.wordeck + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)ev.mana.wordeck + + + diff --git a/Tests/UITests/CardsNativeUITests.swift b/Tests/UITests/WordeckNativeUITests.swift similarity index 70% rename from Tests/UITests/CardsNativeUITests.swift rename to Tests/UITests/WordeckNativeUITests.swift index e81bea4..1134668 100644 --- a/Tests/UITests/CardsNativeUITests.swift +++ b/Tests/UITests/WordeckNativeUITests.swift @@ -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") } } diff --git a/Tests/UnitTests/ClozeTests.swift b/Tests/UnitTests/ClozeTests.swift index 2cff5f3..ef24430 100644 --- a/Tests/UnitTests/ClozeTests.swift +++ b/Tests/UnitTests/ClozeTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Cloze") struct ClozeTests { diff --git a/Tests/UnitTests/DeckDecodingTests.swift b/Tests/UnitTests/DeckDecodingTests.swift index 1f3d912..d2309a6 100644 --- a/Tests/UnitTests/DeckDecodingTests.swift +++ b/Tests/UnitTests/DeckDecodingTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Deck-JSON-Decoding") struct DeckDecodingTests { diff --git a/Tests/UnitTests/MarketplaceDecodingTests.swift b/Tests/UnitTests/MarketplaceDecodingTests.swift index c5aad0f..2fd5701 100644 --- a/Tests/UnitTests/MarketplaceDecodingTests.swift +++ b/Tests/UnitTests/MarketplaceDecodingTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Marketplace-JSON-Decoding") struct MarketplaceDecodingTests { diff --git a/Tests/UnitTests/MaskRegionsTests.swift b/Tests/UnitTests/MaskRegionsTests.swift deleted file mode 100644 index 2eb8069..0000000 --- a/Tests/UnitTests/MaskRegionsTests.swift +++ /dev/null @@ -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) - } -} diff --git a/Tests/UnitTests/MutationEncodingTests.swift b/Tests/UnitTests/MutationEncodingTests.swift index 29dddbe..6dc26fb 100644 --- a/Tests/UnitTests/MutationEncodingTests.swift +++ b/Tests/UnitTests/MutationEncodingTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Mutation Body Encoding") struct MutationEncodingTests { diff --git a/Tests/UnitTests/ReviewDecodingTests.swift b/Tests/UnitTests/ReviewDecodingTests.swift index 71c35f3..c1bba29 100644 --- a/Tests/UnitTests/ReviewDecodingTests.swift +++ b/Tests/UnitTests/ReviewDecodingTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Review-JSON-Decoding") struct ReviewDecodingTests { diff --git a/Tests/UnitTests/TypingTests.swift b/Tests/UnitTests/TypingTests.swift index 86f0265..4bd9d85 100644 --- a/Tests/UnitTests/TypingTests.swift +++ b/Tests/UnitTests/TypingTests.swift @@ -1,6 +1,6 @@ import Foundation import Testing -@testable import CardsNative +@testable import WordeckNative @Suite("Typing-Match") struct TypingTests { diff --git a/Tests/UnitTests/CardsNativeTests.swift b/Tests/UnitTests/WordeckNativeTests.swift similarity index 66% rename from Tests/UnitTests/CardsNativeTests.swift rename to Tests/UnitTests/WordeckNativeTests.swift index b979c91..32d4b7b 100644 --- a/Tests/UnitTests/CardsNativeTests.swift +++ b/Tests/UnitTests/WordeckNativeTests.swift @@ -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") } } diff --git a/Widgets/CardsWidget/DueProvider.swift b/Widgets/WordeckWidget/DueProvider.swift similarity index 100% rename from Widgets/CardsWidget/DueProvider.swift rename to Widgets/WordeckWidget/DueProvider.swift diff --git a/Widgets/CardsWidget/DueWidgetView.swift b/Widgets/WordeckWidget/DueWidgetView.swift similarity index 63% rename from Widgets/CardsWidget/DueWidgetView.swift rename to Widgets/WordeckWidget/DueWidgetView.swift index ee99536..a29c0c2 100644 --- a/Widgets/CardsWidget/DueWidgetView.swift +++ b/Widgets/WordeckWidget/DueWidgetView.swift @@ -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() diff --git a/Widgets/CardsWidget/Resources/Assets.xcassets/Contents.json b/Widgets/WordeckWidget/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Widgets/CardsWidget/Resources/Assets.xcassets/Contents.json rename to Widgets/WordeckWidget/Resources/Assets.xcassets/Contents.json diff --git a/Widgets/WordeckWidget/Resources/CardsWidgetExtension.entitlements b/Widgets/WordeckWidget/Resources/CardsWidgetExtension.entitlements new file mode 100644 index 0000000..0723c65 --- /dev/null +++ b/Widgets/WordeckWidget/Resources/CardsWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ev.mana.wordeck + + + diff --git a/Widgets/WordeckWidget/Resources/Info.plist b/Widgets/WordeckWidget/Resources/Info.plist new file mode 100644 index 0000000..b0ae30e --- /dev/null +++ b/Widgets/WordeckWidget/Resources/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Wordeck Widget + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 11 + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements b/Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements new file mode 100644 index 0000000..0723c65 --- /dev/null +++ b/Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ev.mana.wordeck + + + diff --git a/Widgets/CardsWidget/CardsDueWidget.swift b/Widgets/WordeckWidget/WordeckDueWidget.swift similarity index 84% rename from Widgets/CardsWidget/CardsDueWidget.swift rename to Widgets/WordeckWidget/WordeckDueWidget.swift index 99023f6..cb3a997 100644 --- a/Widgets/CardsWidget/CardsDueWidget.swift +++ b/Widgets/WordeckWidget/WordeckDueWidget.swift @@ -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, diff --git a/Widgets/CardsWidget/CardsWidgetBundle.swift b/Widgets/WordeckWidget/WordeckWidgetBundle.swift similarity index 82% rename from Widgets/CardsWidget/CardsWidgetBundle.swift rename to Widgets/WordeckWidget/WordeckWidgetBundle.swift index c51370f..cbb9f89 100644 --- a/Widgets/CardsWidget/CardsWidgetBundle.swift +++ b/Widgets/WordeckWidget/WordeckWidgetBundle.swift @@ -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() } } diff --git a/devlog/2026-05-12/macher.md b/devlog/2026-05-12/macher.md index 0144364..6de61ae 100644 --- a/devlog/2026-05-12/macher.md +++ b/devlog/2026-05-12/macher.md @@ -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 diff --git a/devlog/2026-05-12/spieler.md b/devlog/2026-05-12/spieler.md index d839067..5ffc4b2 100644 --- a/devlog/2026-05-12/spieler.md +++ b/devlog/2026-05-12/spieler.md @@ -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. diff --git a/devlog/2026-05-13/data.json b/devlog/2026-05-13/data.json index 47dc633..2bd5d15 100644 --- a/devlog/2026-05-13/data.json +++ b/devlog/2026-05-13/data.json @@ -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, diff --git a/devlog/2026-05-13/macher.md b/devlog/2026-05-13/macher.md index 93f7168..af21a89 100644 --- a/devlog/2026-05-13/macher.md +++ b/devlog/2026-05-13/macher.md @@ -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 diff --git a/devlog/2026-05-13/spieler.md b/devlog/2026-05-13/spieler.md index eb78079..1d7798e 100644 --- a/devlog/2026-05-13/spieler.md +++ b/devlog/2026-05-13/spieler.md @@ -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. diff --git a/docs/MARKETING_COPY.md b/docs/MARKETING_COPY.md index c327555..7eb9a69 100644 --- a/docs/MARKETING_COPY.md +++ b/docs/MARKETING_COPY.md @@ -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`). diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index c8f965e..5d433c0 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -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/` in +- [ ] **Universal-Link:** `https://wordeck.com/d/` 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 diff --git a/project.yml b/project.yml index 9f2283b..f1cb631 100644 --- a/project.yml +++ b/project.yml @@ -1,4 +1,4 @@ -name: CardsNative +name: WordeckNative options: bundleIdPrefix: ev.mana @@ -28,12 +28,12 @@ settings: DEAD_CODE_STRIPPING: "YES" CLANG_ENABLE_MODULES: "YES" # mana e.V. Apple-Developer-Team-ID. Muss matchen mit dem AASA- - # Eintrag auf https://cardecky.mana.how/.well-known/apple-app-site-association + # Eintrag auf https://wordeck.com/.well-known/apple-app-site-association # (siehe cards/infrastructure/docker-compose.production.yml). DEVELOPMENT_TEAM: QP3GLU8PH3 targets: - CardsNative: + WordeckNative: type: application supportedDestinations: [iOS, macOS] dependencies: @@ -43,7 +43,7 @@ targets: product: ManaTokens - package: ManaSwiftUI product: ManaAuthUI - - target: CardsWidgetExtension + - target: WordeckWidgetExtension embed: true - target: CardsShareExtension embed: true @@ -54,14 +54,14 @@ targets: - path: Sources/Resources excludes: - "Info.plist" - - "CardsNative.entitlements" + - "WordeckNative.entitlements" info: path: Sources/Resources/Info.plist properties: CFBundleShortVersionString: "0.1.0" CFBundleVersion: "11" CFBundleDevelopmentRegion: de - CFBundleDisplayName: Cardecky + CFBundleDisplayName: Wordeck LSApplicationCategoryType: "public.app-category.education" UILaunchScreen: {} UISupportedInterfaceOrientations: @@ -74,28 +74,28 @@ targets: - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight CFBundleURLTypes: - - CFBundleURLName: ev.mana.cardecky + - CFBundleURLName: ev.mana.wordeck CFBundleURLSchemes: - cards NSUserActivityTypes: - NSUserActivityTypeBrowsingWeb - NSPhotoLibraryUsageDescription: "Cardecky greift auf deine Fotos zu, damit du Bilder zu Image-Occlusion-Karten hinzufügen kannst." + NSPhotoLibraryUsageDescription: "Wordeck greift auf deine Fotos zu, damit du Bilder zu Image-Occlusion-Karten hinzufügen kannst." ITSAppUsesNonExemptEncryption: false entitlements: - path: Sources/Resources/CardsNative.entitlements + path: Sources/Resources/WordeckNative.entitlements properties: com.apple.security.app-sandbox: true com.apple.security.network.client: true com.apple.security.files.user-selected.read-write: true keychain-access-groups: - - $(AppIdentifierPrefix)ev.mana.cardecky + - $(AppIdentifierPrefix)ev.mana.wordeck com.apple.developer.associated-domains: - - applinks:cardecky.mana.how + - applinks:wordeck.com com.apple.security.application-groups: - - group.ev.mana.cardecky + - group.ev.mana.wordeck settings: base: - PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck CODE_SIGN_STYLE: Automatic ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor @@ -127,80 +127,80 @@ targets: path: ShareExtension/Resources/CardsShareExtension.entitlements properties: com.apple.security.application-groups: - - group.ev.mana.cardecky + - group.ev.mana.wordeck settings: base: - PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.share + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.share CODE_SIGN_STYLE: Automatic SKIP_INSTALL: "YES" - CardsWidgetExtension: + WordeckWidgetExtension: type: app-extension supportedDestinations: [iOS] sources: - - path: Widgets/CardsWidget + - path: Widgets/WordeckWidget excludes: - "Resources/Info.plist" - - "Resources/CardsWidgetExtension.entitlements" + - "Resources/WordeckWidgetExtension.entitlements" - path: Sources/Core/Sync/WidgetSnapshot.swift info: - path: Widgets/CardsWidget/Resources/Info.plist + path: Widgets/WordeckWidget/Resources/Info.plist properties: - CFBundleDisplayName: Cardecky Widget + CFBundleDisplayName: Wordeck Widget CFBundleShortVersionString: "0.1.0" CFBundleVersion: "11" NSExtension: NSExtensionPointIdentifier: com.apple.widgetkit-extension entitlements: - path: Widgets/CardsWidget/Resources/CardsWidgetExtension.entitlements + path: Widgets/WordeckWidget/Resources/WordeckWidgetExtension.entitlements properties: com.apple.security.application-groups: - - group.ev.mana.cardecky + - group.ev.mana.wordeck dependencies: - sdk: WidgetKit.framework - sdk: SwiftUI.framework settings: base: - PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.widget + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.widget CODE_SIGN_STYLE: Automatic SKIP_INSTALL: "YES" - INFOPLIST_KEY_CFBundleDisplayName: Cardecky Widget + INFOPLIST_KEY_CFBundleDisplayName: Wordeck Widget - CardsNativeTests: + WordeckNativeTests: type: bundle.unit-test supportedDestinations: [iOS, macOS] sources: - Tests/UnitTests dependencies: - - target: CardsNative + - target: WordeckNative settings: base: - PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.tests + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.tests GENERATE_INFOPLIST_FILE: "YES" - CardsNativeUITests: + WordeckNativeUITests: type: bundle.ui-testing supportedDestinations: [iOS, macOS] sources: - Tests/UITests dependencies: - - target: CardsNative + - target: WordeckNative settings: base: - PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cardecky.uitests + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.wordeck.uitests GENERATE_INFOPLIST_FILE: "YES" schemes: - CardsNative: + WordeckNative: build: targets: - CardsNative: all - CardsNativeTests: [test] - CardsNativeUITests: [test] + WordeckNative: all + WordeckNativeTests: [test] + WordeckNativeUITests: [test] test: targets: - - CardsNativeTests - - CardsNativeUITests + - WordeckNativeTests + - WordeckNativeUITests gatherCoverageData: false run: config: Debug