From 0b2ae167b792587e0e1bfb76b7fd0db2e5060023 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 13 May 2026 01:13:27 +0200 Subject: [PATCH] =?UTF-8?q?v0.8.0=20=E2=80=94=20Phase=20=CE=B2-7=20App-Sto?= =?UTF-8?q?re-Vorbereitung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature-komplett für TestFlight. App-Icon-Platzhalter, Siri-Shortcut, Share-Extension, Release-Checklist mit allen externen Apple-Schritten. - scripts/make-appicon.swift: CoreGraphics-basierter Generator für 1024×1024 forest-green PNG mit "C"-Letter und Karten-Stack-Schatten - Asset-Catalog auf Single-Size-AppIcon-Pattern umgestellt - StudyCardsIntent + CardsAppShortcuts (App Intents): Siri- Shortcut "Karten lernen mit Cards" / "Mit Cards lernen" - CardsShareExtension Target: ShareViewController (UIKit-Bootstrap + SwiftUI-Hosting), ShareEditorView mit Text-Edit - PendingShare + PendingShareStore shared in App-Group group.ev.mana.cards - DeckListView zeigt PendingShare-Banner; Tap navigiert zu PendingShareConsumeView mit Deck-Picker + Front/Back-Felder, Submit → POST /cards, danach store.remove - Info.plist: NSPhotoLibraryUsageDescription für Image-Occlusion- Picker, NSUserActivityTypes für Universal-Links - docs/RELEASE_CHECKLIST.md mit externen Schritten: Apple-Developer- Portal, App-IDs, App-Group, AASA, Xcode-Archive, TestFlight-Plan, App-Store-Connect-Felder, Compliance-Verifikation - UI-Test robuster (akzeptiert Login oder Decks/Entdecken als Launch-Erfolg, unabhängig vom Simulator-Keychain-State) - 35 Tests + 1 UI-Test grün, alle drei Targets bauen App-Store-Submission selbst ist externe Aktion und passiert nicht durch dieses Repo — Schritte in docs/RELEASE_CHECKLIST.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 + PLAN.md | 53 ++++-- .../CardsShareExtension.entitlements | 10 ++ ShareExtension/Resources/Info.plist | 41 +++++ ShareExtension/ShareEditorView.swift | 60 +++++++ ShareExtension/ShareViewController.swift | 78 +++++++++ Sources/Core/Intents/StudyAppIntents.swift | 38 +++++ Sources/Core/Sync/PendingShareStore.swift | 61 +++++++ Sources/Features/Decks/DeckListView.swift | 44 +++++ .../Decks/PendingShareConsumeView.swift | 116 +++++++++++++ .../AppIcon.appiconset/AppIcon-1024.png | Bin 0 -> 46118 bytes .../AppIcon.appiconset/Contents.json | 49 +----- Tests/UITests/CardsNativeUITests.swift | 19 ++- docs/RELEASE_CHECKLIST.md | 153 ++++++++++++++++++ project.yml | 36 +++++ scripts/make-appicon.swift | 82 ++++++++++ 16 files changed, 783 insertions(+), 59 deletions(-) create mode 100644 ShareExtension/Resources/CardsShareExtension.entitlements create mode 100644 ShareExtension/Resources/Info.plist create mode 100644 ShareExtension/ShareEditorView.swift create mode 100644 ShareExtension/ShareViewController.swift create mode 100644 Sources/Core/Intents/StudyAppIntents.swift create mode 100644 Sources/Core/Sync/PendingShareStore.swift create mode 100644 Sources/Features/Decks/PendingShareConsumeView.swift create mode 100644 Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png create mode 100644 docs/RELEASE_CHECKLIST.md create mode 100644 scripts/make-appicon.swift diff --git a/.gitignore b/.gitignore index 8a4826f..4136871 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ xcuserdata/ *.xcodeproj Sources/Resources/Info.plist Sources/Resources/CardsNative.entitlements +Widgets/CardsWidget/Resources/Info.plist +Widgets/CardsWidget/Resources/CardsWidgetExtension.entitlements diff --git a/PLAN.md b/PLAN.md index 3cef369..61aa4a5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,9 +1,10 @@ # Plan — cards-native (SwiftUI Universal) -**Stand: 2026-05-13 — Phasen β-0 bis β-6 abgeschlossen.** -Alle 7 Card-Types + Marketplace + Native-Polish (Keyboard-Shortcuts, -Daily-Reminder-Notifications, WidgetKit-Extension mit App-Group). -35 Unit-Tests + 1 UI-Test grün, Widget-Build grün. +**Stand: 2026-05-13 — Phasen β-0 bis β-7 abgeschlossen.** +Feature-komplett für TestFlight. Alle 7 Card-Types + Marketplace ++ Keyboard/Daily-Reminder/Widget + Siri-Shortcut + Share-Extension ++ App-Icon-Platzhalter + Release-Checklist. 35 Unit-Tests + 1 UI-Test +grün, alle drei Targets (Haupt-App + Widget + Share) bauen. Pflicht-Check für β-2: Endurance-Test auf realem Gerät (200+ Karten mit Flugmodus zwischendurch) steht aus — Aufgabe für Till. @@ -27,6 +28,28 @@ mit Flugmodus zwischendurch) steht aus — Aufgabe für Till. - `LoginView` (Email/PW gegen mana-auth) - 3 Unit-Tests (AppConfig) +✅ **β-7 — App-Store-Vorbereitung (2026-05-13, Tag `v0.8.0`)** +- App-Icon-Platzhalter: `scripts/make-appicon.swift` generiert 1024×1024 + PNG aus CoreGraphics (forest-green + "C"-Letter). Asset-Catalog auf + Single-Size-Pattern umgestellt. **Vor App-Store-Submit durch Designer- + Icon ersetzen** (siehe `docs/RELEASE_CHECKLIST.md`). +- `StudyCardsIntent` + `CardsAppShortcuts` (App Intents Framework): + Siri-Shortcut "Karten lernen mit Cards" / "Mit Cards lernen", öffnet + die App, App-Shortcut-Provider macht ihn ohne Konfiguration sichtbar. +- `CardsShareExtension`-Target (app-extension): empfängt Text/URL aus + Safari/Mail-Share-Sheets, SwiftUI-Mini-Editor, persistiert + `PendingShare` in App-Group. Haupt-App zeigt Banner in DeckListView, + Tap → `PendingShareConsumeView` mit Deck-Picker + Front/Back-Felder, + Submit → `POST /cards`, danach `PendingShareStore.remove`. +- `PendingShare` + `PendingShareStore` shared in beiden Targets. +- `NSPhotoLibraryUsageDescription` + `NSUserActivityTypes` in Info.plist + ergänzt für Image-Occlusion-Picker und Universal-Links. +- `docs/RELEASE_CHECKLIST.md` — externe Schritte: Apple-Developer- + Portal-Konfiguration, AASA-Endpoint, TestFlight-Test-Plan, App-Store- + Connect-Felder, Compliance-Verifikation. +- UI-Test robuster gegen Keychain-State (akzeptiert sowohl Login als + auch Decks/Entdecken als gestartete App). + ✅ **β-6 — Native-Polish (2026-05-13, Tag `v0.7.0`)** - Keyboard-Shortcuts in `StudySessionView`: Space = flip, 1/2/3/4 = again/hard/good/easy (über hidden Buttons mit @@ -169,18 +192,22 @@ ausgeliefert wird — heute 404. Web-seitige Aufgabe. | β-4 | ✅ 2026-05-13 | Media-Upload, image-occlusion (Touch-Mask-Editor), audio-front (AVAudioPlayer) | | β-5 | ✅ 2026-05-13 | Marketplace (Explore/Browse/Subscribe) + TabBar + Universal-Link-Handler (AASA server-side pending) | | β-6 | ✅ 2026-05-13 | Keyboard-Shortcuts + Daily-Reminders + WidgetKit (Siri/Share deferred auf β-7) | -| β-7 | — | App-Store-Submission | +| β-7 | ✅ 2026-05-13 | App-Icon-Platzhalter + Siri-Shortcut + Share-Extension + Release-Checklist (externe Apple-Schritte siehe docs/RELEASE_CHECKLIST.md) | -## Nächste Schritte für β-7 (App-Store-Vorbereitung) +## Nächste Schritte: TestFlight + App-Store -Aus Greenfield-Plan-Sektion "Phase β-7": +Alle remaining steps sind **externe Aktionen** außerhalb des Repos — +Apple-Developer-Portal, App-Store-Connect, Xcode-Archive, das +Cards-Web-Repo (AASA). Strukturierte Liste in +[`docs/RELEASE_CHECKLIST.md`](docs/RELEASE_CHECKLIST.md): -1. App-Icon (drei Größen iOS, plus macOS-Idiom) -2. Localized App-Store-Screenshots -3. TestFlight-Build, eine Woche Beta-Test -4. App-Store-Submission unter `ev.mana.cards`, Verein-Developer-Account -5. (β-6-Carryover) Siri-Shortcuts via App Intents -6. (β-6-Carryover) Share-Extension "Save as Card" +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) +4. Xcode-Archive + TestFlight-Upload +5. Endurance- und Cross-Device-Tests im TestFlight-Beta +6. App-Store-Connect-Listing (Description, Screenshots, Privacy) +7. Submission ## Notizen aus β-4 diff --git a/ShareExtension/Resources/CardsShareExtension.entitlements b/ShareExtension/Resources/CardsShareExtension.entitlements new file mode 100644 index 0000000..19e0259 --- /dev/null +++ b/ShareExtension/Resources/CardsShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ev.mana.cards + + + diff --git a/ShareExtension/Resources/Info.plist b/ShareExtension/Resources/Info.plist new file mode 100644 index 0000000..228f71f --- /dev/null +++ b/ShareExtension/Resources/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Als Karte speichern + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/ShareExtension/ShareEditorView.swift b/ShareExtension/ShareEditorView.swift new file mode 100644 index 0000000..842afa3 --- /dev/null +++ b/ShareExtension/ShareEditorView.swift @@ -0,0 +1,60 @@ +import SwiftUI + +/// Mini-Editor in der Share-Extension. User kann den Text noch +/// anpassen, dann "Speichern" → PendingShare landet in der Haupt-App. +struct ShareEditorView: View { + let initialText: String + let sourceURL: String? + let onSave: (String) -> Void + let onCancel: () -> Void + + @State private var text: String + + init( + text: String, + sourceURL: String?, + onSave: @escaping (String) -> Void, + onCancel: @escaping () -> Void + ) { + initialText = text + self.sourceURL = sourceURL + self.onSave = onSave + self.onCancel = onCancel + _text = State(initialValue: text) + } + + var body: some View { + NavigationStack { + Form { + Section("Vorderseite") { + TextField("Text", text: $text, axis: .vertical) + .lineLimit(4 ... 10) + } + if let sourceURL { + Section("Quelle") { + Text(sourceURL) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + Section { + Text("Wähle das Ziel-Deck in der Cards-App.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .navigationTitle("Als Karte speichern") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abbrechen", action: onCancel) + } + ToolbarItem(placement: .confirmationAction) { + Button("Speichern") { onSave(text) } + .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + } + } +} diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000..d7b5cea --- /dev/null +++ b/ShareExtension/ShareViewController.swift @@ -0,0 +1,78 @@ +import SwiftUI +import UIKit +import UniformTypeIdentifiers + +/// Share-Extension-Entrypoint. Liest geteilten Text/URL aus +/// `extensionContext`, persistiert als `PendingShare` in der App-Group, +/// schließt sich nach Bestätigung. +final class ShareViewController: UIViewController { + private var sharedText: String = "" + private var sharedURL: String? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + loadSharedContent { [weak self] in + self?.presentEditor() + } + } + + private func loadSharedContent(completion: @escaping () -> Void) { + guard let item = extensionContext?.inputItems.first as? NSExtensionItem, + let providers = item.attachments + else { + completion() + return + } + + let group = DispatchGroup() + for provider in providers { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + group.enter() + provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in + if let url = item as? URL { + self.sharedURL = url.absoluteString + if self.sharedText.isEmpty { self.sharedText = url.absoluteString } + } + group.leave() + } + } else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + group.enter() + provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in + if let text = item as? String { self.sharedText = text } + group.leave() + } + } + } + group.notify(queue: .main, execute: completion) + } + + private func presentEditor() { + let host = UIHostingController(rootView: ShareEditorView( + text: sharedText, + sourceURL: sharedURL, + onSave: { [weak self] cleanedText in + self?.saveAndClose(text: cleanedText) + }, + onCancel: { [weak self] in + self?.cancel() + } + )) + host.modalPresentationStyle = .formSheet + present(host, animated: true) + } + + private func saveAndClose(text: String) { + let share = PendingShare(text: text, sourceURL: sharedURL) + PendingShareStore.append(share) + dismiss(animated: true) { [weak self] in + self?.extensionContext?.completeRequest(returningItems: nil) + } + } + + private func cancel() { + dismiss(animated: true) { [weak self] in + self?.extensionContext?.cancelRequest(withError: NSError(domain: "ev.mana.cards.share", code: 0)) + } + } +} diff --git a/Sources/Core/Intents/StudyAppIntents.swift b/Sources/Core/Intents/StudyAppIntents.swift new file mode 100644 index 0000000..a98efc2 --- /dev/null +++ b/Sources/Core/Intents/StudyAppIntents.swift @@ -0,0 +1,38 @@ +import AppIntents +import SwiftUI + +/// Siri-Shortcut: "Karten lernen". Öffnet die App im Decks-Tab. +/// Tatsächliche Tab-Aktivierung passiert via `cards://` URL-Scheme, das +/// RootView bereits handhabt. +struct StudyCardsIntent: AppIntent { + static let title: LocalizedStringResource = "Karten lernen" + static let description = IntentDescription( + "Öffnet Cards und zeigt deine Decks mit fälligen Karten.", + categoryName: "Lernen" + ) + static let openAppWhenRun: Bool = true + + @MainActor + func perform() async throws -> some IntentResult { + // Die App ist beim Ausführen schon geöffnet (openAppWhenRun); + // weitere Navigation passiert in RootView via Deep-Link, falls + // ein Parameter dazukommt. v1: simple App-Open. + .result() + } +} + +/// Macht Intents direkt nach Install in Siri/Shortcuts-App sichtbar. +struct CardsAppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: StudyCardsIntent(), + phrases: [ + "Karten lernen mit \(.applicationName)", + "Mit \(.applicationName) lernen", + "\(.applicationName) öffnen", + ], + shortTitle: "Karten lernen", + systemImageName: "rectangle.stack" + ) + } +} diff --git a/Sources/Core/Sync/PendingShareStore.swift b/Sources/Core/Sync/PendingShareStore.swift new file mode 100644 index 0000000..87eca02 --- /dev/null +++ b/Sources/Core/Sync/PendingShareStore.swift @@ -0,0 +1,61 @@ +import Foundation + +/// Inbox für Share-Extension. Die Extension persistiert hier, die +/// Haupt-App liest beim Start und zeigt einen Banner mit +/// "→ Als Karte speichern". Shared App-Group-Container. +struct PendingShare: Codable, Identifiable, Hashable, Sendable { + let id: String + let text: String + let sourceURL: String? + let capturedAt: Date + + init(text: String, sourceURL: String? = nil, capturedAt: Date = .now) { + id = "\(capturedAt.timeIntervalSince1970)-\(UUID().uuidString.prefix(8))" + self.text = text + self.sourceURL = sourceURL + self.capturedAt = capturedAt + } +} + +enum PendingShareStore { + static let appGroupID = "group.ev.mana.cards" + static let filename = "pending-shares.json" + + static var url: URL? { + FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroupID)? + .appendingPathComponent(filename) + } + + /// FIFO-Liste aller noch nicht konsumierten Shares. + static func readAll() -> [PendingShare] { + guard let url, let data = try? Data(contentsOf: url) else { return [] } + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return (try? decoder.decode([PendingShare].self, from: data)) ?? [] + } + + /// Append-only-Write. Bei Concurrent-Writes aus Extension + Haupt-App + /// kann ein Eintrag verloren gehen — akzeptabel, weil Extension nur + /// schreibt wenn User aktiv "Teilen" tippt. + static func append(_ share: PendingShare) { + guard let url else { return } + var all = readAll() + all.append(share) + write(all) + } + + /// Entfernt einen Eintrag (wenn die Haupt-App ihn als Karte gespeichert hat). + static func remove(id: String) { + let all = readAll().filter { $0.id != id } + write(all) + } + + private static func write(_ shares: [PendingShare]) { + guard let url else { return } + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + guard let data = try? encoder.encode(shares) else { return } + try? data.write(to: url, options: .atomic) + } +} diff --git a/Sources/Features/Decks/DeckListView.swift b/Sources/Features/Decks/DeckListView.swift index a1830a2..fff8437 100644 --- a/Sources/Features/Decks/DeckListView.swift +++ b/Sources/Features/Decks/DeckListView.swift @@ -12,6 +12,7 @@ struct DeckListView: View { @State private var store: DeckListStore? @State private var showAccount = false @State private var showCreate = false + @State private var pendingShares: [PendingShare] = [] var body: some View { NavigationStack { @@ -23,6 +24,12 @@ struct DeckListView: View { .navigationDestination(for: String.self) { deckId in DeckDetailView(deckId: deckId) } + .navigationDestination(for: PendingShareRoute.self) { route in + PendingShareConsumeView(share: route.share, onDone: { + PendingShareStore.remove(id: route.share.id) + pendingShares = PendingShareStore.readAll() + }) + } .toolbar { toolbar } .refreshable { await store?.refresh() @@ -39,6 +46,10 @@ struct DeckListView: View { store = DeckListStore(auth: auth, context: context) } await store?.refresh() + pendingShares = PendingShareStore.readAll() + } + .onAppear { + pendingShares = PendingShareStore.readAll() } .sheet(isPresented: $showAccount) { NavigationStack { @@ -59,6 +70,7 @@ struct DeckListView: View { emptyState } else { List { + pendingShareSection inboxBannerSection ownDecksSection } @@ -67,6 +79,38 @@ struct DeckListView: View { } } + @ViewBuilder + private var pendingShareSection: some View { + if !pendingShares.isEmpty { + Section { + ForEach(pendingShares) { share in + NavigationLink(value: PendingShareRoute(share: share)) { + HStack(spacing: 12) { + Image(systemName: "square.and.arrow.down") + .foregroundStyle(CardsTheme.primary) + VStack(alignment: .leading, spacing: 2) { + Text("Aus Teilen-Menü") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(CardsTheme.foreground) + Text(share.text) + .font(.caption) + .foregroundStyle(CardsTheme.mutedForeground) + .lineLimit(2) + } + Spacer() + } + .padding() + .background(CardsTheme.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 10)) + } + .buttonStyle(.plain) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) + } + } + } + } + private var emptyState: some View { VStack(spacing: 16) { if store?.state == .loading { diff --git a/Sources/Features/Decks/PendingShareConsumeView.swift b/Sources/Features/Decks/PendingShareConsumeView.swift new file mode 100644 index 0000000..00266bb --- /dev/null +++ b/Sources/Features/Decks/PendingShareConsumeView.swift @@ -0,0 +1,116 @@ +import ManaCore +import SwiftData +import SwiftUI + +struct PendingShareRoute: Hashable { + let share: PendingShare +} + +/// User wählt Ziel-Deck, optional Back-Text. Submit → CardCreate +/// via API, anschließend wird die PendingShare aus dem Store entfernt. +struct PendingShareConsumeView: View { + let share: PendingShare + let onDone: () -> Void + + @Environment(AuthClient.self) private var auth + @Environment(\.dismiss) private var dismiss + @Query(sort: \CachedDeck.updatedAt, order: .reverse) private var decks: [CachedDeck] + + @State private var selectedDeckId: String? + @State private var front: String + @State private var back: String = "" + @State private var isSubmitting = false + @State private var errorMessage: String? + + init(share: PendingShare, onDone: @escaping () -> Void) { + self.share = share + self.onDone = onDone + _front = State(initialValue: share.text) + } + + var body: some View { + Form { + Section("Ziel-Deck") { + if decks.isEmpty { + Text("Erst ein Deck erstellen.") + .foregroundStyle(CardsTheme.mutedForeground) + } else { + Picker("Deck", selection: $selectedDeckId) { + Text("Wählen …").tag(String?.none) + ForEach(decks) { deck in + Text(deck.name).tag(String?.some(deck.id)) + } + } + } + } + Section("Vorderseite") { + TextField("Front", text: $front, axis: .vertical) + .lineLimit(2 ... 6) + } + Section("Rückseite") { + TextField("Back (optional, default: Quell-URL)", text: $back, axis: .vertical) + .lineLimit(2 ... 4) + } + if let sourceURL = share.sourceURL { + Section("Quelle") { + Text(sourceURL) + .font(.caption) + .foregroundStyle(CardsTheme.mutedForeground) + } + } + if let errorMessage { + Section { + Text(errorMessage) + .font(.footnote) + .foregroundStyle(CardsTheme.error) + } + } + } + .navigationTitle("Geteilten Inhalt speichern") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Verwerfen", role: .destructive) { + PendingShareStore.remove(id: share.id) + onDone() + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Speichern") { Task { await submit() } } + .disabled(selectedDeckId == nil || front.trimmed.isEmpty || isSubmitting) + } + } + } + + private func submit() async { + guard let deckId = selectedDeckId else { return } + isSubmitting = true + errorMessage = nil + defer { isSubmitting = false } + + let backText = back.trimmed.isEmpty ? (share.sourceURL ?? "—") : back.trimmed + let api = CardsAPI(auth: auth) + let body = CardCreateBody( + deckId: deckId, + type: .basic, + fields: CardFieldsBuilder.basic(front: front.trimmed, back: backText), + mediaRefs: nil + ) + do { + _ = try await api.createCard(body) + onDone() + dismiss() + } catch { + errorMessage = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + } + } +} + +private extension String { + var trimmed: String { + trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..76f939ec485c20d45694fb6918f255dbb6e1ca60 GIT binary patch literal 46118 zcmeFZ`9GB3|35y)$PCF&k&Jy`il}T;i0o08tQC>6RMsqG%2LTvA+o2UtRZBbs7SVw zE$fga>)5w3^F5d7`Fg&8`2GW*=MT@@bGlsDxz2f<$8vw%ACGh0H!{#-pyQ&0!C(w$ zv^9)jFa-FCfFbvQ|6zF~?t=eO-7?lX1;s27{-;cdsB|Y1IGxhWaK8h-o?Vy2GdS!Fc$pxbuQS-|B+gB zP|zL>Tr-aU-`9aC8UH1+|3vwpD7V$~Kdt<0eE%8vzZ}+ozH&Dt{1 zwb55`DS5MP?a3;Isr|89W}}W>PsFXQl~RS81%3Z$M_LGcWMm}v+aiURoA>LXk=WFX zp2cr(ryp&}Go#yX18~vF-3!N+pgcnI+Pd0VH_H8rvk{F z=y7~qc`{m&DzyG|QN8y5Lp%&4Jy>Bl;+aGmRhyMz2D4KI#(RY!5Nl|=zv0k&SRm_8 zlt(MzNc708CVW`t?s0%Pn+{0iE-S$ua zZdE01UhaDY?-uB?6duv|lsn+A1*@IlbzYt~r0c&=GZ>`(f~y{nslCK4rKxmljO7S(rFjR*0Ps7do}0 z3`TGzL;8uSS%$>0()XTr>2Cz$pCdgug_v$IX!#r^hZ|BuONy7b>Ug}~F;NDq!qi{6u`GlI%#=*%*$^vc7IFf>bTv7NIxVnQFpGid5)He=Z16O=b0m-Cb((c07*U9ahq>LXQSi0r^ zY`f7-9I48bobpv82!jN05)S8KX>hZU>+bDJ&+V1m)h`|#NyUsj@KpotNoDp~JAg47 ztA)5?AC|$e#73#Z29mA3%~qdu2{)vAhmdOgf36n^1S#nJS)!nj?4i#R>tky5pBKjs zNe8)kvp1Y6>wfuftLFgFLsz zqeN6EIHe5ccNQl`H|=lxc&l&SLoxJQj+pF)t9u@Xe0%#gM}??SXIH8zZn(H==i3f; zt!*${PG<6D|wvMOL?xh9!4IRO=-*!+4LIJ*hvxYoke4L+uJ*ygrukx zQbrkYkKTT3ygYXWL$;Ardtg^T;r{UTg9MB^PyvQ{iv-Lve7$s1^Zids3*%Omg&wfe zZQ)~j5aJBDl!57(uLH7=14e3!4iw0>l2k0n7Xp6--N)#F=f%!j=J5dax|*8YA6}Au zcd&5$@Y1sV;EMP1m9Ta1hqa-*362X;cH+d$?|qm111#k11ouWoni>c+3?`~n2Ye0A zq%S_I%`MO-3>K(xvRCgXTyB$AV!Xrq)WvaHO7-+zEbS&^vUb11?r;^rNHH5ltLp21 z&j}*jRO^1l`s%%2QX?o_A-xkUrmA*fstgl2fX&QXOXZzZi}tJtAkMA1qG3AhjAq0CdsB~=Gj69!=Brl{zNy< zvY8!adC45#+0GkUs6@Tw!4NVdP$#|D{8^)py&IJ+^Q_dcH(Z~^X*`{>F106MYVbwv zfzUr5L)$oQ!A5)&E`nuzT2p$2Su`U^rQ+fFSd)61jY9P=8--ZR&%>j=jZyMWiwS#X zw3`)nP0xk3VX$H}illNrkjs&L=&MHBb+F|Q*;)Q{e9@GQZ;@hZW-j=3=b5w;hCfn8 zQ{k@mc*b}c-0gWCn@3+@QrB7tlFgWxj~7HAVk)Y)cpIvhTV*a>=HHFOPLcSQ447AB z++gGJnxe<|F%}>(ZZtmQ%XS()$5DE_QH`{ETrsz!>9fe6M_AbQH%KGPHe(cN;Cb zf`?WtdX3@mTyhwn8e^{7CT+v6{h-@U;I^tVvnv^I&AjYP=YB5*mhEz}Y%!0YSut2fCY7n3lG%N>3L+CXHcy=db3Zg z%;{!4?A#F?@`s>oPdYi4UageSVtAD1`bGG8Fw1i|duF%^>KD%tX|kX>Wx<5bYQCoD z&F9@$=}AS`lqODFlKlg-4Jb(`PUtda8G;zBs(6jLTRn{(yFN}C+Uqe#fy?_{@00nP zgGez?%%qS;zS|j6Ug+P6LpT`08IXF_T8gp-=TR>NWjoaNEt@YnlqP&>JuJt(%S3pb zwtFMvyRhoaYAul_$J{H9`$8iR6J9<16muSKvNdAVfv%8R`gop_h}HSvnn;Y<1^RY$ z0N}hHj2dNtdDFzhzfaeKwXwZogWtEXd*1u;M`An1>sZ2H#wM)@u40k|FOVxIorAIB z_5-E6l=XN&y-Zuv9!#M8$*9W|dfQuoJK7fbC}-A+ip_wi(+YF?Xjkk-5pes7bl1d} zEDen7lg`7mD_>lXn}{F#vKzDLq@!&kU;nJz+^q1UPl+HR?KkBRa0D0Y#H{lGZ${0Zj`$%`(#o>q?LO+fxrUK*nrX6ojLt>y@4W~Tv>rx!|`E#N{dStSxy`i<+c6#c)_ z{luI#T9u*%XUlZu@2LSHhiqM`lje+@Mg+Zintc6(f6{>PA${4NlBV5#WXdu~i(9?D z?g?|Ys4hoJYNq)Dt!^%oH+~LpK0&F;cbx2P64a6WJEwYt^VXYszw*e~#?WlQ+GN4M z@FRZmwe^k?DPG5V_v}Q}v{<!u^mFOmiOyoVzGNtzz}2{bC%^T-NfGd3&(G;?pV(&4x){lvw&`~8L-c) zfs)g9j5z7A&>6x%%Sk{~c1f5V{rV_;59au!Xt*}bK@N_qgkb$$v7W;FRf$JSem4b+ zDY@CZe`kI>f1Y>l|1&)XFukL87Jog=pS64h zTF!KB^y99;c0}(C83@(a=l@;{8j_;9gox|Er)P8*Opn2wV+RZGuH`Kqh#CL0oqw+5 zxPY^9#ux5nH@k83-g!XH{wL}ky#(y{f1=)vq1z_&pH~0pK>lBHAXP^tukA#Ev>|xW zMrO$IExFo0+<}V*E8Xk2#;?g0t#9>LrRQggERCsE_=(r7UQydD9NhG~UDUNW?nfbi zv(}ZGn|hB@?sqMCV#`~Mx|J(ca(W6?_eMmp zS(*B5`+PJM{nxIEQbVp@^gW*HZc**<0oveCa^qn_hN}CL6fzg6uw7xKX?4mp`FDDz zQuW=<&-L|nXwJ-8C?P;mE$RWh0=aG*x`56t4A#lF$#=i2;~VgkK5FNxM~Q-nugCBfR-`r?P;p4j?% z`nWm8+j& zkoD>X)pLO}cU~58oI7M_>ouG=qf&kOR@wSDKM4k0as;k}yEAxxTJ6tlwa=4v2xuZX5Mn~K?4#H(c2I={kKW=qFa4_P2g89JKHPh1xw;<;I|A@RNdXmet)Z|bBXG=egE}Civ#ITf$lJ% zB|ciIyoJyj<}0GSXmMe2aoMn?x?jzc(CFUyT%dNLC;UQ*3bi*#8V%}h^k^a%>sziK zt56}Vc98_FZcw7cqbzwV+{PuZ{hU`y{m(4c};;!ZaV8+y03(?u8q1_r3^38RI0KN0Hs$zwcMe|`=iOLj zB(t(sJSJAsY*fo1Bs=={L=>sTuA+CvC?fTn_%^$_pgMHGZS=H5vLK6%~8j`aQGu zX|US~#v+fh_Fz~X;_M|Li+FwQIy{2Dsd3*(s~XYlkZvR+twC!0TH0x}Q3yUcO09V6 zC)HrUT0Nf{TQ}ea{lQG;JM=!Xg6+_|et9464Zv4#E)_E2O6Y!JrDd$VZu-m;3}5>b zTk}gV^0%GA@d(rreulZxr`9a~bYJi9>IhK1A#$t01^yZUwig%HZ;s_s8MTx*zKH-s zyL>DWgFiw3;$2v*1ESe#HTQbnyQqK3!PJqaWUpJkJe8_neFx!LnCr}S*%E^wd<=NV zgvA2Q4E}A?X{FgBNmak%8^g%qj4DFE!Zs6;9|ei{F;7BG;7!l9 zX&$qX4(_gxY~|)CK>^Sup!fQT_Bm?w{Uf@Cv0&8PA4e5%0W^R>@R|IeWv05Mc5VIj z#et!w0L}$E9FiumU`&J{XCW|D*fGeE7+RtkkBdgd0OB?+$y5KR1Vhj{37NT9C?}YF z#CHrrnB81Hz;!Jlqa|N?-?PJR-yNC-nTjxhZ}Iv4n{_z{h{=sSG<_Fj$DAKj?FDal z$a;Rz3#?^2hWG@N3@&)qT0~%C;p5WB6)G1dG9Aa4Q`v5pS7cj#y@Y7wEAZ+KCB9F! zGC6)^sAPHdseqUT(>*<~-Gz5h-cX)+#=Yor2-*{5 zjl(P=46MueD{pqR*A+1;N$p))I_hR8z%2R*-_Y$eDh=nYB!m+>>t`j-UNPn~(gU-& z)Tw!H8HnV^_!TL51<)L?vp%QNq;UxWcL_8SEp%$$OEiQD>V2=T?Ui8GK0jo++P`1b zXN0qkqU!JW2C*RAb`y*e$J&eGrn%i)CY8v)4d^Qn9D7^#MGY?^|Pyk+iW02u!uG>Y(|7+{wnPFEuEK8)A+z}XhBL)j+oDc{#iU%pv z#6p2qdg`iui!z>Xg}S_Oy@e)lGWoMi$k*8o*ZthQZksDE(U=1?2dRuczj>ATWDlnN z-BaHuU@^}SSL46a=Ok4etNi_OJb3&NIi{_*-(Okme ztrG_xxUk-{0eiH;qGP8B*3~boEMyb~gskG+dFIT}3u;D2H`V#??iandYx45)VvL_k z;3DaNl`@mY6RTNN1E&;g#|Se#QCcv>C9p^L-a21O(Z*2Q@gK-%%`ygxs3RIKB;wvO ze7T5`+=_CPwu!ZxWf3-#$3>`HO6Uhg^(}P}Csraf=?#?;bW$M8oLE5Sodz@E`XFgw z=LUXMX_8XP@$H55kGFn`$?UC{F;6)WLPD__*wr_|&Qzr2_OD zm_2hg|Et@;wM@nq9m0u#AJcS*6s88oW$fl}8G6IxYodY(B%76;{dIZLYI0X_P35c{ z;ZbfBr}6~?^F$69!Ewu_>OI_*x*Mu>f#l>ku5SDwTi|B#(_lwwwWII*g!Is+IMS&k z84F(npOuC7+D)vF#TrrHc3+)^34WW19OVEz`+iEILIHUUp2pCia8?_q**+dxnzC?* z_&s(!G-Y=7)MrsqIN}X!ncv7W;_NjwW=Kama_z+6DAX}HI{lJJ^qcK2noB$2VQCoE zG?~gV;8A<4_F7bx=-!nTP8kd{Guo&WUpiZHxN_RID6}rZAjYglWJ2`Fm`mvM!ZtAn>zp-3PgS+MFlH5G<_b6`tyAi*o_u{$nC$ zLUHdsP}5>V$RHIJ5=SWzXx}nx02P=Ioe_esSZ@fzBi^aYZ`{X*&r9`&;!sSYCHUp2 zz5@fk%{3w1yw=v!7)`A-P2hHzd)f0vv(K(a}N^^k6=A`Neoh7cZr$ zt!w7`+hplL7VYzP{{G7T8iuNu*F8bAYBr2NmcujJ*u2#MtYq{1Y7 z*;<=PEwA!2`^7I!>&yEts=yIC!fhQL>^8T5y(d`oxKL9zHd;fX21xH@kIa_fgd~bQ zfZ1YzETBHATz0Uz*8(C2RB;YP`}Sfz#w64OnPnkRBg|U1I3FhNO~1B6t*BoT?~Bmk z1CoFUA4A|zyV;TQqJU{br*$Yvx25t7Q(m#An!JBEh;g&6A7KWdZ64`AxgsIaMcbf; zPC&*0q983NR$opg6ZXQw+XBFkG=BGi$0N18$;*ue{ww_az@KDUpw*;?mhMLGb=w*X zt7VX8F)vDG2f(e(6*ueU*0us+xU@x3mDy95n!5Ot_|0)El#!Aw6NW@;NJT3$scrRn z2P>xfT%BknZ_~M$8$fIiEV&;6{ZA8u8>*~LxUbU>^4NI&@Rd3+n1(>U5CP7GHE^n! zGtAIxp`^g)Gu3aZhIp`oM}XDQFc!TCpjg^j94@80v15y})l&!=fBsrbUUVS?o3dHkRUPGvo5QFLEGS}r!Aw%DI3z6-`e|mZkI0+hI0dzAElNJ z6*Lma09#&aa2t2v2XHJKAiVoFH@knwo*wC79N+?V>kIG1Mp&G}@x*SY{a+3(>t&2w z=EKGVD3n%hD5ZJd<{*S*0Sb3BGxYf?)|zicP;)y5RV~}3aZhuUMUvi5G!Wz50z^sf zGuAUdbGEBEctKzT@rZg!cn4(ETtZ(xP7P)G>ekgLFLu@4+NUPqvDsO}Dpa5k|3cq% zAf9>QGzK|BC_n0LPjTaCN(~Z#4QD6Z6X76wwtZ9>1ABtj!I8xK+>{BAiGYZ|c6*-&$J#h-SMP#_JJ+MtZL0LDuE2EZ_(q!`Y z@_hZn-@0vpa4`b#OkNNBweR$U>?z)BCfo-;mqW{KgL?9)eM|vyg?WT9I6)*5LpUqt zl1i7#_(Oy^z<|>%@%?Hlq~-PjrU7rVOelzp6*jBEP%7~gK$Nyaj+T7jVO<)VTTLMZ zHJPpCf^Nb)DNluCRPbhIAHsv8TCB?4YBEdPL4yoP<($D}-@KLtg)|2YCyN>T0DL zvh~YssSC8@58<>1iL9#YNu?p-amfcSuU!JnA^_9?-^S|U!D5kw5Q&z(lw=-NzmI)) zZ$N@`#$BzVCd|o@Vxvl=<{^hfl(%2 zV7$$|N9Rh^ukh?AuT`Y({nH2m8X2mAEEn}7pq0fTe}y%Y+cn)#A_ph@d*Wc+F)$D) zD`(5X>%J$@*IHIDrxyfxWR1)tA;Qan6}aRcadBSYbHv=2DqqH`lLOXd9DIc|gYd3! zP8l^JfBp(-Jn4f)Mb)GjkUdgf9e9BQ4?aV=2R%)T42FYm>$d7j z&fs2|W5Z2wEirj8$BzO-{6X_SbY38kg8Z`G?Y!j4JGOMKR)Nm^2Xvo+b+ghhtuBR9 zo^BksrWlri|Vk-<1?zH(x|>TuoR6UMT3B| zYvH$SId50o=D%+q4{j7q0Xs17%+C);u&^JyIq-N|hk_kaz~1k){RvD%iuhfim4V_h zZ+Qw^de=O&vk(C!xdPVL-StrddQk=bYPe~rq3uRwf&Cb%#I1J^NXwX>;MaAd7_pnh ziXkb|ZkI2=Ni+xHDKx0&c=%b`vrOpd0(?(+)EDWS%iI^^B(_8lYkvj<%oxDzxso=tMKZpkadC%Q_wz-qp;f^OXW zu_MBmG#FgQ>c*APgPp3Jh2of_!9Y=(;1GHverA=vf1C!&Thm;CeYj+l@Buk1^AU@* zl6ePi4NR#Ume$;vVTD8zitjbRUR`^ik=pq%{ozUQQe%3bRg1|5ok$q&IjVA{JG?eE zW${_t%w&hAM1&8dW<&jsRpQE&dzq(GLWn?uZMzj6u%rsaHdKKEZAIW8Kk`fjgxT}Q z%O{^Zh$y-l2|)419?VNfA`3)V(O3BV&I{kvu*=i?oVR8!|uW zvbk3oR4al>>xQq>P!_^Xw%b(F0^seNduq3R__xKl61R*J;Dc~TfI9d^A2Xqt?wJgM zD22rOuVjzxwfO-AbT-ERFuLNW&m}xWE4yTltkASN!&>aE^d&sIa zF+6XjM)xBtm}jx=5dP||+WOQjOTy3r+G6ZT3FJNCP{vpwdWs)|cEz=G2U%48=PipApHcf& z&Tdz~CX(=tzPh!k$&1fgXzad%XGHG-gE>jZw-=5W=4@z{+v+X~lxvzzJ%l}S1QO{N zL<;s}0wFd##ilUl1*p{FegY9VFBSq1gIJMpZ(H&u*cLHT=wCGt%=yb~6D{L3ffO+{ zZUZZK^KoisDsbZoK@cbSd=iB)Y0B_h8;<9skGuosz91+B$slGMg8B~~s5v-x#TzLL zp=(VLBiOQ~9YKGYa9eMO$@`%}=39)eKjZiO@zJPXoHV3t`hm7`hW!aZ%rv0HChC48 z_;e6vo@O9mCA-#yQMG&x_aGYl5#fbOs##hiVhwH&*Xwf^Qh*z^9^#Dj zy8>mwZX0ELAYupCNnbe{Gahz}gSU5sp$AeQI%C0*+hihb z3VAHrw)GRFS!{?(KmGK^n}V3MtMJ7l#cNbaKs-+CDGN`8V&@bvs0(h_>A@ahhMGle zxNORXb6D*8X`l&pr{W)0e;|HvBbN2KZFrE%>G43E|;NlO? zcQ;u-LsdQ;9QljWcdNa~)+9(9xFaBG01JyhcVvM#XxM#QdQYHO6Lr(Gv$F*coVg2a zoiMANP^p36nPbhG6z3yFvS8#!w5+$?m9?r&lHLL1N7UfT3aI#X z->rih9(+y>9SBQe3;ez2NC0*K!)eeg(AT6?t;K5;Jyihl8pI6IYYwBDL709x;K~^1 zR4B_WoB5V)(0K{bFg$PS)@0jYyJe)kJ>)WhY2sa>arzOU$x?iLtMx$q+urPyZG(9L z|LQ11S9AOOqOqQJ3lPOQjBe4Nv}74oxwZ5(KS!>*p$|(7U_q_yL&lbt?sj5)oJD3st7DMIsM`j=SmFR?HHklaLe0< zJ8)s`u8H(DuwEpaHSib{>@2)ga2Hme&IOaYsmO{lm);^icrIJ_qX73BY?TNiH+Vj5)9yc%+7F8*!;9j40m zW4PGV^|^I_R$M%^=(^ODtwn-^EvYEJmO}y(H|QKKu;lA!h z{?K;>$7>q-+t|x%{aXD30NAaP6#J493_9qRN^>d{`<5p939qK>mi41R^Vd(JyQSJM z#k-!?ULQ}A%Lv~({PucJpCjr1E$LOy5zLXbu7ak8WI2RiT26!6u-7nY!ah!v!^~F{unSUZ2$1l#L0% zO6)G-paV@mz5Rzi-iTL6U%-))x()=~_574NctI5Vqa*L(E! z1dMQWYChI;1xI;9>&9TF=ArB3(R*6D0`;qF|1t01ZwbTdhA2a|Umx8uUgEg>7!ibx z=el35_o2%7tcfMf`%u?;dYDTh_IA8mCf$})@6kM`k9Yw5pizv@J%K+~%Xzy{;u)oD zF;MMbjh%1hnS06?aD-{+O*0jD-L*-TE;CiVm@RJd6^=I**%C=5P7W^*xPL2rQdihR zpLo3TEmBY1z4fzJ?Wfd$FhLF8#`VX^>^5~Yne*i%ZW>Q|ZR z%W)ABjmA#YjHO%Fan5bntSzVvGDgi2(`#ZYP6p-%rCNHPER$6_f=`_@)yY)x3PiNt zB2O{a@kFV`D`@cdT!RT3PiTDq0n>ib_WgP?2+C8}Esx#|><{@`xJZslDt&Q2QC1p9 zY>TU}6>;4(UHcf7@2-laT?wPkY(~kbO;$GM_Cl#N4tc$B8X%bywi_X8Qhv`Cz5*9G zy=Rvu`OXZyXJmn5@wd`cav?F4HKo68zp1e7BTOJ)O;rCvMB_~D>H2!%3r$llpx1WM z_XA&jnAyRAlRiJcv#J|rR=l*kn`eqWHKb@5aAS>>JecdR-UKC^hmlLcj(Pr$dvOmQVa|d+$6F1H zz3Uxvqk|fwX3rJhX`yQ(y6aQ6xG$#khP|Uz#^qjbXc?#A8Ela?3(bt7`2&6d<~5gZ z!7b_&hsOvmghpD|eUuxUrRpHvKQ7qpnBb22}Slf4C`ay(C14Sg{!5`K5C3pVM;* zscsuOqi@AFjjld00kt1|qJh7G&!@AT!2rA;d&1;dPqQEp)Uw$+coWweY4+Tc>KIL$ zXUS>T8bApAbaM?CVg+>4K^p7h-Zy+&*YmwUY91UFzaF#By~Wt?$Y~OTlSYiV2Fg# z6Zi?IjY{I!^u3_eX6gMC9C!3j>D$Lc#T1g^mX~?a^1Q|o0||XC*o*UXg^p9mph7eA zKq*eJkgxZ-2k_yhN$fV>eV-VjE(gie*`YeD|00>Y z&TMx5WJA;*bLCrszE5H;OlTHDzbyf{5wYp&G+DMsA{2<}0Z6{&weTJF&qkcs07es+ zt-#)1OO~^-X`2W>Z;xy4A&{{?%k_2PGMi7_dp9=255DPsCP^I08IbuhJ4arTPKE8s;;24PuUzR6;8WK{R;VOQVW+GH*1q<{>|L-_B6r5}?vtSmr-mqXsS@*CfQ0AXgSfPSW&0>|d zoj307v}!ZuHelwt0q0(->NPl9q%2~@48>03{oXueZ=n(`aJDbQ5kF;CBp}9f!K78X zX0v>&W%W`LJdcXWv0tMKOQi*y9{iMzKx#-68_yfk1LFOVFGsCg7~Z9amP}Hns8x=Q z@YNub?&CG!pv+B}b0Zo142+XS`(~Yj?#DmIZauCuVAQq399iFjf!q;Fa^4*o2DL<2 z9L?RLI=7US;qtE1OI+ZTL!szw@OO0JAPJotr9}&{G3)8kgNJK1fV7_Sdb-9d$zEQ4sti#;e87Lwo!xU1 z09r}hNRX#RbpqHtdiI}%b`VYYwnc9WES_ujMZYl z_7XF@ZHDPQNT7z0As{_@17#kjHUkJ9apFq5E!RT0YyxsxO7@5X*FZw@IHhFTvN!$h zqbkSEWB@I_-KFktGp zo*v|GvA)?iE8}aB#$OP)IYxX#@iknVkg^py1sGdjliyK!TcwRIf`E-<8J^gatmfk7 zGv8R05pD?xvKULC^L@*)JUDyALNB=rAWxwu`5M`r4Q^AbeXUh;R-0`u9r^ZW;GT5; zqba<>PXp##YoAF?O&$E;?7*`2X(TmvYwA~4@sW;m(uhk?YF%|_7^K$x#5?SU4D0I@ zVR!|-|JCKyLQB%tjfOdbD?gxCD}OK5Uaz^8jrY3G>8%7oVL%Y++@NLy8vX@@TS2p% zC|1!_4A$FiEyARKCEq_bS1n_Hex8^zrOFz(&W+`SP={LdQPdBy?FgwW4eY)7ZfWQ7 zI~|Su(YEwZ`33hJX#f*3Hp;+7zB^sQ>Eaj}vmKjC- zR~RJ`$##Bd&~4Ln)G6rg4gH|!omuB^NHw^ke7>cNlsfy2fYjt4suuGN0l}$SjG^tf zS{B1ZnuYM3D#A4^_kwyZKzY*cJ=TK^ZWRjxd^JWd#iVel$0td}EODS0HUz5F+FKw1 zk9iIr)^tL)a_F91=7lEGV|uI7u}C6)k0M3sbeL|+>!n6P;%}vDR`SrZsHb8=@6YkM z&pz(-(3gP0{(KAIGQ^y|G5^jFQY@!G7%4&zB0mRwYK78Ed)H&61f@xBiaAvhl`CK8 zZY9>#n&|{>vaYS=4kED&{f4t{aa}s%kb|?@TkzsjEC=o$ux@_wI3pC4@umG;Sg{qnjb9W__b4wSz+5+!om}qg5T=L0>7qS(=^swRdjvlO)LTi)cs; zKdzr00jKJ2;uP%%$*ZeNCH_hZMtV5lx|wX2)#gy#VI>@Ln6u1TCx^BG!1PD^#eZiq zm%nUt%jkiS~Q5!QHvUkQnpl(`QlNQBp@Y0863m+l^>K4g;{*I=3D9RR&B91uiU;*GdsBC zwD$fh<%n{*%jNq;F&Eyzu}Rj9LtnkIY8^{Y8kK5z?69R7-H*k~?NMx?W}Alj#6(90 ze?s5H6VGh(e)8%Q8GBbTnnOPq%-4EkwtO3~&p;_^s4IW1o;XrJD|7Ak!fj(aQ;K{h zRW~;vEaJgb>L*;{0TW7N<2U{3;^{%V+-wWgo49wDiovOy-))0871g+Wg=IJc%N`3YdevoZ)Qx1lpNCsla74b{|1+P+`*@dUFP|O-^t%(xqY?oJ zip_;HbL1sEGV0}DIr}w6duTPE+~wYTJ51wg)(hj;r-mj$283OxNzJ)*Cj6tgteg_V z(4CheAGxYQOYuFBWuS+LQOSTsx~sB9;Eb#|7V9y({sN!rT64sIEHc}NN!xfxKoNWN z7j}={D7!ls#`l_p99$D3scw3H1W`b9NK>Mfj&^U$^GDGDdKZ&=v8>=wOW@p_sE{e% z_i_$*jK@<__e3j}L4=VLbmma$ol7_jSB^;9wB@ucmT);RaA<%O!59oW+`f&W-Qmql z`?@hKN@=Gx!K1DR^nH^Rk|ur%OEDgvegUS-3>E&&*$*>ABqor4@zi^f zQO>o}-s%IY*sgb@hUY>N&P;2y))emQvJJnZITyc~BS0j&E}8z|2@Llbc;1*7(cjTV zr*SXkID;J}mEJyl{pG6KA43BL>cQ57GR$bAAMk3Rca2Kp3u4SLktfo&pwiyrOc)Hu z54Mi&{fSEg(PjATDBsf)vkP74=qf%ysYSCb1+$-v=M10qah5ELp?Mz5+BvIrnWOoR zA;a_#@i$B>2F^h02n;2AUeys}hL5r|wzYMY@=k)xw>K&x4)_HYkZwWMPSwRH+0Tx@czZ(9y?PHOOu}vv@g0=Uv@<)8 z2r)(AulaBC>gRHymQr0eQr){k!?+LBmLnds`^$l~Aa*#X^Z)W%Lnc5%{607_APDXSd z#@)TrL=O_|pKoV+0pIo;=)en;YY8m(P7QsMXz`k=5f$$+kDepXiqx*09kfw+xIfU^6#~S|0kqHzrcr^r6k)AhzV`YY`W1=Zmq7G=vd?-E`Tu4 zM9j~lSm}ONaN49Xur4m#;(iCHoySl?B<~mKF|oqQ?K^QqGO?u zNJqd4!W5>pk1m6B-l}A#NET#LiMuAMFd9(5ngGU#-VXVwh34BX0;gYZ*E?E69Gr}} z_Fd1W6LA48dqS0iO!7;0{?HJ5y%hg^`UGs<(_Ig;+n2y@B|IbagD@TN_S>6vLSe}^ z%L^P24Tq>W$>I4<{E?@IvR7?*t2f)CGVYy^enOu#CLi<#*94gI-F~_tj2t4Ric~C{ z;Z|@!{Xj+tZZsNG5O715EVJ_E%!FLbXVkmrPkh?F<+&#fmO zY+~7}{iEn9nwA(Gr_B&;6iKkJ%*&H7!Z!2^o>{gJ{|ht5f^I7SbFB;B1DxYoKo-?! z9`g6ds57rggBx0{%?;9VB^MxLw1zI1`d2A`5q$RL8?7XR91p`&r{$OWX)3_C%-kqp z2T_N)p=N7ta`Krys^}8OT0O7zp;<3NO}wt?iZ(*JVyc4hb|#VkzG?g>I5;)APeexf z61J!k94ErP0tzA@@(m3rd4A$^cE_WI<(~AC(Y9-ITWX|JH~}^;+yWBx=LQ>+*B&eg z#yAOQmNVHskA13uTLF(2*Y}tL>H#w(M$5w<*KBj_gOyCZG_k7-D-B@&0~LmO)_bRi zMoI(a0Bq7^&Bv^=AN#}%+kEZ=IQ_pZamXJ6#Nk-SuA*n2{7hgalzgSUNVG8@i)a)c zYU#DDTBRL5BlC|xqneh2*UBjBw z@yCy70MbyMycEQI{@S1sr(Pn@LKt*#Ef+A(P9bj&1Aba(DCbjlZ4&Old8upZWr$AK zUM5xFid&CH-aK>q%I`j-UK-NdrDhc8@wn+S=z$eXD1Cuqa;gHGHWlH5gW*t!ly{_J z)R@n4FB~If;wxXxRx0TDX+c8GFCJIwN`A5dAM{F=3z*Xukk>TrCtXknut`&@m>C3~ zz7JG7PW93#Om~AWu3p97YHMym%upuWwqkrEl{{?W0oPK3qsyP6;xUq7x8I)ANdoOL z?iiox##pS#)e6jc_v4#tDM9Mh0KIUf%#{(0o1>Kr#x@E(Cx)^FZ8KxSbXY$ZA z&T@egKPZqMpI*AaQF>hvo)}gVQb`4Mt1iNAtGdp@lJidmFz3Ykxi`qfT;m6cSP@j+ zlX6xD%~32qud*p-qinsW*3&5fSAFIv@qrozvkj>4mE;BVSMj-RgY>rR;9Xb3xuRe= zF&YDlGJZ*r6gQ4aGCjpdJ|aZhl3F!_FozKk6H(yW`}q<=;K4JRq&-G*37}6V3!F?( zqZ14ViJukDQAX|Edupy+CXkv^4)xF0o#uPMhXgL$XJt@vwgRX50?6bMhe0=jP8wi1 zA{-S3+VT3x`Tpm54soL>?jxh4x_YO9`v+j4l1BOFz_8C<&f@?cLxqIvR}rVvlHm2t zYVwlF@6wXK2qkr3`)_#qxPQHjqSsEw9Hz2XPB1tFq}A3qr9zr@K@=EX$NC#>;@(T%sdQe5(jQ5T7N?z8^41wEEs3`p!IR z0_7>_aPG4&IbcVs;OpE8v5Rp(D$1AX#3Af%($)|RN+Qzk?d5(L4RQC0>zPy<7fxJc z6@^;pF6s)%!be#FmwaPXSA71V0;3DP?S%mAVY=@kM%Mwoz*eF9MAN$=hgu}j_ z0Jt3j(jrM6Kq}zKod*p=(9fnz<{RyRgjA2eBp6+ZZw9Jzlh=F<86-L-?DP*bOVfah zNSYrS+t(y{_O$VP#}NU7-!)fS5PuPHwc9^DLHV~`l~4K>Lr{QTC6K{ouH)%5b3BP$d6VENyE%joI_<* zRS&6dKFR(X^VkX*a{(U$8irR^C*-+O-R|Fy8|ra9DF_T=@ZUFXWt|l z8;*f~V+O|SHcwC7C80C~+*?7VM8vg%UoyoPBY&8F2?g;E7Z-_MP*;=F10a5r{S2pq z@b+-fbIE?Y!cW-pV`T$Eb%sc35Er(B^O0uHQ|>@j5d;e-Xhs=U3R*%Hs}GTc>Ps)n zm3$M%HLLg5j%L>kzRK+ZXa$T46^M&~_j@LE^&s&4Z0?&isH@MD=09}|(gUS?{?NMZ z&s{J8Z9yR}ueuLad~&GGg8pvr{&!U65sB_=SnjtjP(>fQu}uT0mhdGmkp2YFv>aQ@ z3b0+$DchY%=_+|Ny+Z1S9BX3yq+VC>+zrV%*J;j04!&x05)s; zB;4PAjy4V)_=t*o?VY-xg;LyCtMK470%zy-fXZd=!tH|WcAYW~5GKVBJMoC`9LHQg zq+cv3RdWeuaRB77Vyr0lYiQ3P8GZ*@xpvWPA!uI4nQ(x!!lMl?eL;1U-g_=4yL-j~ z4TbD1Kp1l#b9tl$i*exXK2a$Evi6n27XpmC8c+yU`6bR~7;!;j2p*KX>@q$9)sFTb z;&vGq1}V#ql+$RyeNvdCg18?vaV=I%90sY0C)BSlc$2Z)pnUtS=o>&5AH89}S)D>K zfm9e1Uj#6!lnag9?Pj3a5-?QgnT@Y07+nO2z&S!AT*X`PY(?dRv%SS7Tihi8?``WP z2m#Kwc9@*F>(aXZqNd22^iAt4K9X)CHtGtg3(#{ z!?cpR@{0>EXZFEHvtK{_vmaDo_o43rJ`*muG&}q%!Xd4N4zx=ak->q9KPuuB0_G!e zF5(ROEN--jQ}~(h`|pRicu=J(UbgQaKW6wlmx}Fg6*{Hh3oXED_x9;N?e#DUpiFmK zTKmQuUc=>20f11`mAO4RFL)RuC$QTAXXPtgpNZTo_kguWvWEt z=L5x@4w&c#{g1l_>J$p*CC3&4LICLNVGel=wRsAL#O$7yx03;zQiUPu1YrWeu4J|5 zI$d+&JwONK_~Cho(4oKTBn$yus&7mPBti=oV=rP}bweNY?#rA}_(vzZK}Fuqo1ql| zLQ7DV@oZ;z{sg!m>2u0={;G)+)a14(8KHr`3f_D(hB7;ig#9@?1=fx^4%D&H!pwr% zDI0(1Ll4BdSe9PBoXc!p1=Q|n;pyEo${wK4KW*_S6BTZP2IPYQC~qN98nR|f*_Dru z2#BKPk9F>0Hfj3c1zTgeKZpB4(TF|w25RRX9*`YP_@EFNbRbUs3h#a_%Mm0vcM_si z>Lv*K9{%i^5p>w&F^A~|#7{A(V`!!?$>p6ko$%nnuZ1nLL(L|i~J~A;J%R* zU&p!m1ip__DzQ78M(8lW>_%fG#uC9{kk}hH)2)4t#5$~XVCt1P6QlxV+Tyb`Tp8C@nXEQH%!zc0HL+3 zkx=dT?8zxsZ7VzS&~#h>X#V3P(|i6LgxMXAmENaI^=-s)Fd837xx}OS&3^$3t1dKM zidjQkwwD=4Ey0myBpgyO#a}4q^m#@b`O|3S!Wl=Ca4QHBg8oxX~ScooxUC#*qcqjAz4KVMPOO51fvCB4g*u z2A={@NHZC+JB*VC5HPdK6bqW^$`XSy=%545`pPBEVz5U~&w?cO&vc-Jd#?lw!Sq>C z;JZ6Wm3}f;LW4+Lm*PGb22f{u8!Tb)SIj49DsSbpwm2h@!Lr#{u22@_F-i?WGX`qn zg2qdu1G)&lh5(QdndZuI3YH@KsA`fU=n=c#b{7=1{uAJZlfgxxo06}|gNzmPy6Xux zOp`}wSEJ)~fSASk7Ew&`MtB~W@QeG-p#rey;!o{%?c$%#5>S{sxEXW}7LyVAcc1ix z(tuc_*Fj4v==+!D(?2KI>Eca~&!Q7H-&x7u^Hne$i3>IhHwxXd>o19L&{p5KBNr;T z32-QSRQ8}9iIu*L9{*9{aPtlD;hZ-@rn`ec-@sVzwRnPA0Cj57dTFR6bW@O@{;# zyQ=%YA^~0q;K*kLF4);v-GW^;D2I0CRS`cFN#^IML6URP=ej&u!~UO{0J;Sqqrm~% zU--e-IrIcsK}QrND$|sI*X7CYe9#0XDA_gw`ohfyd0@|F(*H{IJOB;n^c_wDyvQ*z zb(1rwO8?SkKMm}1cD&E-)-Hp4ez-Y8?fjtnN~#E=hkdqHKd`NI<09x~uI4}*IT{1CNpAlYYLsz)DX9e=hmeMs z{r^4>jt&gc0+YfrIP3}`3*zc7h?G1zk`IS~LbXg=(~fAvbC9vG$*vpwE%m9G_xFGl6pz zVj9w*hlta64``#^eaZh!1AlK1URC&QRa^eQzOwSqJizBSD5_x7ch6cwTDSXUK9C9^ zz$;dKo9S<$mb_O#vllq+?gU~I$`2D;K+7FC8AT2N0du~~XwR;~I0J=w6I%$r-Moo> z_%Lc_@fqhZI4yK9+vfT1LgYbvQro>0Pn4iE^nP#HKL@`5?uBcvZn~RnQ4*(6Rn}yX3l~ zHSZvua=ijRxs}2OfHU~`2@bT0u-Nnc1MFV|`+UW61hnq|NppeE*r>3kMZdX&irbB| zP?;dv-BbXBMgU+iO*4xyiagPHXXnrVYpCEmi0P>D1m>t9daBPR5|3DR?2rpOW1-Pn z4syV5`QoEDa5rD5b@$pvYCv6#e^gW!`m)~(`*#!J6g3FU;1sN8cFSVS&{d4hu zEhz#5JI@`h;rHpUbNzqqy=7RGT^BYCBS;xE0tzT8N~jOH7?{U1({mXlpYtOa!UbWA4uC;#=@l^=g z4t%-ze>7i~k0`N~7ib!830pbJ{-oS3R7cFU0jPuPXqtjI8gP;t6$kR(sTuyUa)|%pjIgnihEM_A2Wf# z@2EKT@nt6B(+b?idn@u9sw#{gvfINDvPLbRyNOi3(KVdTCl36miTX#-fIr+!9T!3I zUI1w;2w|TR>pPOp!^^ABpq_@iR(8+%LcR@-UDrR}1N`F#r6pUJ)aY48zB2DABKmUq z6I_4w>t8!2_}R|PVhA;===D{kotW=GUklPyMt5255hTlKChy?Q)5b3nK+fn(uMF*V zkP?9axVhjR4LO&nxlz-zakOEQ-k^n&Sgno>(1|sj zaUz_Ml>Rv(twK!uS(+ge$|G~Jn-Hf#k$-Ew?&kuOPs7FJg0Fsof5rSi0C;Sd6)VI^ z1GW$*Y5p4~`5_CFcLuUk{G@th@z;_c3w_*${{9Zp91r^V#9NCO&GA%JxBVU;2L7MI zpKnj8p>&Le=;gp`K+GH<>*hd-cF?FYs1qgf*Ju9;B+ea;V*!6+rrEp+0S%d0W}+Gj z+G%=IB{d0L=F!-jG;py&C-ItqqobAKIGmC4z=euQ8~cE$Dm|!n&lUwC$60qj^zP|!GtpLI&&1V3pl_LhsRJ`LCRK}nR5$S3F(7pFj24ieS`~D93dyqo4U`eCS zw0#xJeVB(_oc{(Wl3?~$2j;1WQ7e=^rC(!+CUZ~ya_!ac9b6}^51q%L6u{&_CVvo!>z@iRw#LF z6F}3`PJG)t?fs*n)Xo|MZ4PT$e12dH{%fgunv1*bRl!^}hhhandb^u9wFHB)6jX<{ z2s6E8(ML^PF5t)fqf+%oc(lJWi0NTp6YVDxuC5mGc|~;9jDjN6kbFde97Fy1DkRJ1 zlgP+teD)oA!rEcf6VQXz&*M^hWz3M;K1Fzw^Gx8@sI`sbTFjfdar>U$*~TrYrnOt{ z#Z8SRbTP+{ow*jQm4_4jmh75cdFMa$PB{U!7=Eblk*kWr__Jx^T6KO~g zGdLqJ+Ip{Yq-Eq?$CqN0JhQq z2zuh#5#uA0+a6gZrVV^+i%{D@E&&ks{QGBdKomz~>nce(c+Sd}y&hh;ap)=+%N~X zcZ@|SG*zy$TznL0QEOUi8u^!-1F;_t=R0`5Wa70QAKAV;sp3X277wLPNGoUs?W14n z4Xi?Bh!UBce=`uthnN6lcplWN<0(>NL4SPU8j&2|$8*&aWEUtoV$6M?Vmn9=^%!4! z?^hR@&tajbWKIInv zC2V8D;XF(y5Q@Hy(jKn@(JuhyB)O`#yu4<$ASL!n)rb(Y7KjV;hHupg*bW)A)W@KM z6610nst4snLUUU=*kJk{i2gjyIO|d0eu?vLFS~IX*2LQT_?)(7RXLbI1YP3D?6oeU z;!9fTE@B>m)%}(7Op#nu1$)K2m49MFWnZ$^W#b!4AP(XA)An%K?;G-q{@Zg}hL7YE zk8s5m?+E^ht8VNp;z4h9QlhzJ;#E+?mx_m8OCHX+Q5R9y%0d*upLC!6i!!XR)iMu{ zng<3)?)~oFyGh>LFAvl>IMysBvE6uS`Qd83D@tL`PVM?Qi*{+HdDu^x)HGtXml{eh zp(Ga|{W=R<)C!|cYq21kPF8#$IYT|ZiY!(wEAYKcyc7x!>o)03>H2I_TWN_=Vj4q>; z+J{HxarN(KDHi-6B>nCqCc>Ph)FJvDc+*Y7WQ-7*?|6bYGz!d#IRnjMj6X%s`nU%} zz2c=z@gY)b(B3y#De-B>t=R&%?+J{v^04JWv}JHuMkpa!lwA0~Rg|XzIY~0{s!{6HhE%p;c3R(}Yxz(f| z_OKplr_ToiTZX7>36@);Md-KI3HSRAzjQi_9Z#(lA*FSRV8M>WL@LlhtvELD*}0s0 zrWKLTN%|TL9UeoCgJIq&S_*n3=NX(=%Y14Yhk31B6Z?T{(?a+aek~pv9l$U9pQQ*x z7m~V?Z4EysW!!h#i6M?{drDyc`Xl;$YM`^M9R>k>Y|Lz@=p+;)K2+n%0d_VIiPR*I zuYrpCVp~IW$l|GMpYC_T|4vNjk5!X=dd0{BO4^-qZL<&EC`e{+ozEmurv#K@w>z=M z5Erx$PyN$>bMe5_@mVm!YDkIbRSj;c#^?lm=6cecvI6gCA`tvslTKDVhmg-`2UkIVh zkh(W@SZTSd&^;J4Ls`HARMu;2-uoP5X)}ZjkWNnW@T!0go{j8IA{b}Hc2K+r^B!uV zFDn-xh5|vne{0ww7?o^t$M~izM!$NFns?4aXJRSY9)vm5eq~f8{v!N9gPkZ~f4bCA zcI1}m6z@f~R*>=oB_uvR@>=3f&DkUlM^ps;1uMe6#NwRPu)GIu?*LDy z=bWd*bWx>E&H3pvHl&rxv=Z?=3JJ(RW-s6RE9@}l7gfnfJl(Z8nO?dAkA>+Xx>}IU z$DnSM`$gdp-OX6A{7yH2n`rHo#z1n6UKX8IxbRc^E{JEkg(8Fp!NbWj%g# z)er4Lf*+V4i{RCHVXbcDk!LpBt2Bm~VK{&oA-srtKXZhLQ3okb8|0s`*2fviY;~Qq z!hEN5^v^%Kvh{%M9L+H+tbtLDA_HJVh8d_m!gL4t0L-C<(XJqW0e z(8a|7FGO?T*Twt`2jkUCD%ZL}O&9(os;Ej_8>C78=v_tlq~5N3nn)+;O5J1hV|Zx_ zMma^2NdU}y7h&hqF1^29f3)9j`pucFdUc}Q(`E0wd4!FBW!QD$Yx1l->6oCnnt$zl z(sza@51{TMmIopg5n3?_K>0CBY(*b#-guK#g>p0T;V7S|Wy*8bYN1Ka$66fnum;!O z%K8q~*`IIenKsd_hPe(nBHABb;iQ(_brSOvwkZxo{Q474$DB+8=O!~A1&48l`h)T+ zl&H4|L_7>U89c?*bME8z44_#UD`kMuUB-4bVV%k`6$3ML9Tq;{7pgf` zEDVWEc+@yCIfq;vk#Iz`uAnHzK$MU%$-{dNMbd>Fe^+VAfyzKC=sd9Lx=&MLYfWM) zy2BWEh@nZt6$&Ee$7@nhPn)WxJW7=I!nXJDaB}bAWcBJ+<&|oMxqvRjSqq8I+D*m4_3M3KxHOE z;TiBUHE7wq+R$+p(-_>iUO89Fy(m{_313DgIw}`K>?C#-#fQ1d# zd?h*OGzFJ{Z3CxMO=9O`@4QLMqd8+=5n0K!etZ^b-9LuUrzoRnX*Ar{ff(vD1t)75 zxCEqWjl_dNh_br4gz04#63H?wTBbi*MPRp7U9G0yq*EAy1wDefw|aq3rJK-kcmKI- z-J}_=l}Q9Ep~pnzzu^+H`2BDZ80IWF&h}5PG5L+ad(=q8yz7E8ggIuu10np5N)9-_~942d#;>e!Am+i`5h~ zW|$nb(L?1@JI_&!-KVl}W!8?k+n+s-(P_zl$3IaE0LW8P+^Ax~g)5wDVj$>IG#0My z?E*7hqv9TmWeXX@1_s3?+}GpzFm!8|L>k|$Mkhd-2ntpH+D#XwP7WNcOW?MDBSqe7 zw8&Y-ZXueP8>n#iXx0O!dq%gNy6jKObVQ;BNG~ODN^M4q$@$cqYkkDa$jI1DMC{Jjve5dFvJ}pV~lAMI0>5#rIg6gRRyW@mD{BpH02W z7eBBBj+72WHj4gs6x6%gP7RR_$9`CLjx9-$3FR1T%gkoN?0}YyXTpQK(FHaDiYZy52|v)0Tw$6YX4sIU=|#}PB)8p zu=f|wwTpo1_z_E1Q#wQDcl_{RLIYLT$^CsE?G>h`2KshJ~WUmM}adM|RXOF>vbTrzOG^LS0pa zrimO8*L*<~lu2Ht8NSh{o&Te3y`+>&Dz)Hd<5%JI_Vx^yv4T-J68D}UEca_kV%)!W zyQD^H3Y}qwNb3vy{x5`go z^YQZui-^!m$9UWzJ@=B97?lFJ#e0N1oLuOgNHn=da>1=DXE-G8PVUm?O@nGxh7OmW z_v=3%%=tDR5^9Z|)QE_)x$W#FF%2Bj@EdF-*SH5Vucg#+>!4QX);DDGt5K1PpY#U_ zv*GPE=aR0c>i5y|d{!SAZt}3d-uoaVsC?7l42BTnqFi-04A?lvaO&ez`cQc_{v&Fi z%##J_HC*<`&UUdG+(T2t3){VCT)qXb$c4ICkuD}0890om{|n5dA=sR4$aX_8LW7(f$~@*W^OpFWwABZ`Y=;CLHci^sLH=8A;GB@N z+w|ExK1J_!>m`oi4ePU&qR?mI_rHf<0dlIP(m<+{0prxBYu*a=+D%Oe529(SjvQ+wGjTBOFAHJrl@{;;qR!>pwde4*5)D zNPY9kL{utzV?Sf>v1(PV&2EVXu~4~lY&~5*R#TQ%jnc@B!mek`uGcB`O@(>gJT=X@ zSor5f@!f?Y2Hk?1ooa-g(wnpf;7$^w3^!-Ef41~%V-=tCrDFRfw^kN-70TbI4F8Hpv_Z(MuZe*UBxlbj0~22+r4;8&lwjB1KLD&6$s48oG}P8i z!Pb0&7hO^Mx?vkj@cCJGDO;%3shBWpeZoN$DDB+ z{r2a8wI%=#BGBT$nJ7vh3Gq3~PhhoYOGJW8>JeysgsqL10A?RF(%6q3vQ$U2VuDcR zUIXWbdB<=v*Sj6!g2XmdpdHam-svO#;!98Lz8SQ1Gd}U_=A0k!C$(GQbjme-n1jUG zr$8BXF?Q!bp{RBG`&r`(S87I~YFU8pwUkT$AP;te>}Y@XMP*so6iJ@hqy26h{ncmO z*h)6^Ju@+2Q7w?0Qu*G&LQ(8AO*_x9-v6e(+M}MWZ&|*))qq2K`jYgq0in1J|1Mo3 zvr=?fb3Z|Rig0PHd}aCSkJs#PRw3k3I>884p-7;@LZsR{53loq{)qp(3v+*#l$eX- z-<}H1q?*USG@vuYZI}DHlRC0uw8*D(Dy$ctOg9kTyH#T{jeUV3rCcI@P{5GyoFmEm zw@k~cCW!%PHiX?|inxQ^QrW28Qf}2IdckW*zb3XY*QX!`ZlSWdFhshLsO^4XwLX~W zbE~$|YTdKWLIVC!>l!nMJ!%4RM~0k`{1K+aH1#-MPMe!!u3}@mEw&TDmN$9!&R^^j zWYEcbz&=~6rT07dVqb4j^%@)#@ zN7_3XhKq|>{j*bZ&J|^gapnz?w5?kCHd+qm-+&OY;Bt`WRZNQb46}{I(6gU0+g|H= za)+zETNLCfmzA2~aF1X< zL(~cDw^*gam(F-@ehMl2Vc5+d+E*D@x>7Si$_-7+$SvV4^_kh%5Gwh~(Q` }3f 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 literal 0 HcmV?d00001 diff --git a/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index ffdfe15..79657a3 100644 --- a/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "AppIcon-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,58 +25,15 @@ "value" : "tinted" } ], + "filename" : "AppIcon-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { + "filename" : "AppIcon-1024.png", "idiom" : "mac", "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", "size" : "512x512" } ], diff --git a/Tests/UITests/CardsNativeUITests.swift b/Tests/UITests/CardsNativeUITests.swift index 1e6d3dd..41b0ee2 100644 --- a/Tests/UITests/CardsNativeUITests.swift +++ b/Tests/UITests/CardsNativeUITests.swift @@ -4,6 +4,23 @@ final class CardsNativeUITests: XCTestCase { func testAppLaunches() throws { let app = XCUIApplication() app.launch() - XCTAssertTrue(app.staticTexts["Cards"].waitForExistence(timeout: 5)) + // App ist gestartet, sobald entweder das LoginView "Cards" + // 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["Cards"] + let decksTitle = app.staticTexts["Decks"] + let exploreTab = app.staticTexts["Entdecken"] + + let deadline = Date().addingTimeInterval(5) + var found = false + while Date() < deadline { + if loginTitle.exists || decksTitle.exists || exploreTab.exists { + found = true + break + } + usleep(100_000) + } + XCTAssertTrue(found, "Erwartete App-Surface (Cards | Decks | Entdecken) erschien nicht innerhalb 5 s") } } diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md new file mode 100644 index 0000000..ed743bf --- /dev/null +++ b/docs/RELEASE_CHECKLIST.md @@ -0,0 +1,153 @@ +# RELEASE_CHECKLIST — cards-native + +Externe Schritte vor App-Store-Submission. Alles unter dieser +Sektion läuft NICHT durch das Repo — sondern durch das Apple- +Developer-Portal, App-Store-Connect, das Cards-Web-Repo (für +AASA) und über Xcode (für Build + Sign). + +## Vor TestFlight (intern, kein Apple-Review) + +### Apple-Developer-Konfiguration + +- [ ] **Team-ID prüfen** für den mana-e.V.-Apple-Developer-Account. + Eintragen in `project.yml > settings > base > DEVELOPMENT_TEAM` + (oder per-Scheme in Xcode unter "Signing & Capabilities"). +- [ ] **App-ID `ev.mana.cards`** im Developer-Portal anlegen, falls + noch nicht da. Mit Capabilities: App Groups, Keychain Sharing, + Associated Domains. +- [ ] **App-ID `ev.mana.cards.share`** + **`ev.mana.cards.widget`** für + die Extensions analog anlegen, ebenfalls mit App Groups. +- [ ] **App-Group `group.ev.mana.cards`** im Portal anlegen und allen + drei App-IDs zuweisen. +- [ ] **Keychain-Access-Group**: heute `ev.mana.cards`. Wenn + Shared-Keychain mit `memoro-native` gewünscht (siehe + `mana/docs/MANA_SWIFT.md` Phase γ), auf + `$(AppIdentifierPrefix)ev.mana.shared` umstellen und + `AppConfig.manaAppConfig.keychainAccessGroup` setzen. +- [ ] **Provisioning Profiles** für alle drei Targets generieren + (Development + Distribution). Bei "Automatic Signing" macht + Xcode das beim ersten Build selbst, wenn das Team-ID stimmt. + +### Asset-Polish + +- [ ] **AppIcon ersetzen.** Heute Platzhalter aus + `scripts/make-appicon.swift` (forest-green "C"). Vor Submission + durch ein vom Designer erstelltes Icon austauschen + (`Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png`). +- [ ] **Marketing-Versionsnummer** in `project.yml` setzen + (`MARKETING_VERSION` → "1.0.0"). +- [ ] **Build-Nummer** monoton inkrementieren bei jedem TestFlight- + Upload (`CURRENT_PROJECT_VERSION`). + +### Server-seitige Vorbedingungen + +- [ ] **AASA-Endpoint** auf `cardecky.mana.how/.well-known/apple-app-site-association` + ausliefern (heute 404). Format: + ```json + { + "applinks": { + "apps": [], + "details": [{ + "appID": ".ev.mana.cards", + "paths": ["/d/*"] + }] + } + } + ``` + Content-Type muss `application/json` sein (nicht `text/html`). + Aufgabe ans Cards-Web-Repo. +- [ ] **cardecky-api.mana.how** muss erreichbar bleiben — die App + ist 100% Online-write. Health-Probe verifizieren. + +### Build + Archive + +- [ ] `xcodegen generate` +- [ ] In Xcode: Product → Archive (Destination "Any iOS Device") +- [ ] Im Organizer: Validate App +- [ ] Distribute App → TestFlight & App Store + +### TestFlight-Test-Plan + +- [ ] **Endurance:** 200+ Karten lernen, Flugmodus zwischendurch. +- [ ] **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 + 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. +- [ ] **Siri-Shortcut:** "Hey Siri, Karten lernen" → App öffnet. +- [ ] **Daily-Reminder:** Tagesgrenze überqueren, Notification kommt + zur konfigurierten Uhrzeit. +- [ ] **Login/Logout:** kompletter Auth-Roundtrip. +- [ ] **Offline-Grades:** Reviews offline machen, online gehen, + `PendingGrade`-Queue läuft leer. + +## Vor App-Store-Submission (öffentliches Review) + +### App-Store-Connect + +- [ ] **App-Eintrag erstellen** unter https://appstoreconnect.apple.com + mit Bundle-ID `ev.mana.cards`. +- [ ] **App-Name** + **Subtitle** (max 30 Zeichen): + - Name: "Cards" + - Subtitle: "Karteikarten — Verein mana" +- [ ] **Description** (de + en, max 4000 Zeichen). Vorschlag siehe + `docs/MARKETING_COPY.md` (existiert noch nicht — TODO). +- [ ] **Keywords** (max 100 Zeichen, comma-separated): + "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. +- [ ] **Privacy-Policy-URL** — vermutlich `cardecky.mana.how/privacy` + oder `mana-ev.ch/privacy`. **Verifizieren.** +- [ ] **Support-URL** — `cardecky.mana.how/help` oder Verein-Kontakt. +- [ ] **Marketing-URL** (optional) — `cardecky.mana.how`. +- [ ] **Age-Rating**: vermutlich 4+ (no objectionable content). +- [ ] **Pricing**: Free. +- [ ] **App-Privacy** (Data Type Declaration): + - Email-Adresse (für Login) + - User-Inhalt (Karten, Decks) + - Nutzungsdaten (FSRS-Reviews) + - **Nicht** für Tracking verwendet. + +### Compliance / Verein-Werte + +- [ ] **Lese `mana/docs/COMPLIANCE.md`** und prüfe, dass keine + Telemetrie, kein Crash-Reporter, kein SaaS-Tracker in Submission- + Build (Stripe + APNs sind die akzeptierten Ausnahmen — Cards + nutzt keines davon heute). +- [ ] **Subscriptions / In-App-Purchases**: Marketplace-Paid-Decks + sind über `mana-credits` abgewickelt, nicht über StoreKit. Im + App-Store-Connect "no In-App-Purchases" auswählen, **außer** + wir wollen vor Submission StoreKit für `mana-credits` einführen + (separate Architektur-Frage). + +### Hub-App vs Standalone-App (siehe MANA_SWIFT.md) + +- [x] Entschieden: separate Apps. memoro-native und cards-native sind + eigenständige App-Store-Einträge. Keine Hub-App. + +## Carryover-Tasks (β-6 / β-7-Reste) + +- [x] Siri-Shortcut via App Intents (`StudyCardsIntent`) — funktional, + v1: nur "App öffnen". Erweiterung "Direkt in Default-Deck-Study" + kann später kommen. +- [x] Share-Extension "Save as Card" — Pragma-Lösung: Extension + speichert `PendingShare` in App-Group, Haupt-App zeigt Banner, + User wählt Ziel-Deck und Back-Text. Kein direkter API-Call aus + der Extension (Auth-State wäre kompliziert, Pragma reicht für v1). +- [ ] **Card-Edit** (PATCH `/cards/:id`): heute deferred, ergänzen wenn + Beta-User danach fragen. +- [ ] **Anki-Import**: heute deferred, weil Web client-side parsed + und kein Server-Endpoint existiert. Native bräuchte eigenen + `.apkg`-Parser (sqlite-basiert) — eigener Sprint. + +## Nach Submission + +- [ ] **Monitoring**: nach Release `cardecky-api.mana.how/healthz` und + Rate-Limit-Auslastung beobachten — Native-App kann Last-Spitzen + erzeugen. +- [ ] **DSGVO-Endpoint** (`/api/v1/dsgvo/export`, `/delete`) testweise + aus Native triggerbar machen (β-7-Extension oder β-8). +- [ ] **Update-Cadence**: erstmal alle 2-4 Wochen ein Build. + Build-Nummer monoton, Marketing-Version semver. diff --git a/project.yml b/project.yml index 7f41c84..f78d34b 100644 --- a/project.yml +++ b/project.yml @@ -36,6 +36,8 @@ targets: product: ManaTokens - target: CardsWidgetExtension embed: true + - target: CardsShareExtension + embed: true sources: - path: Sources/App - path: Sources/Features @@ -57,6 +59,9 @@ targets: - CFBundleURLName: ev.mana.cards CFBundleURLSchemes: - cards + NSUserActivityTypes: + - NSUserActivityTypeBrowsingWeb + NSPhotoLibraryUsageDescription: "Cards greift auf deine Fotos zu, damit du Bilder zu Image-Occlusion-Karten hinzufügen kannst." ITSAppUsesNonExemptEncryption: false entitlements: path: Sources/Resources/CardsNative.entitlements @@ -78,6 +83,37 @@ targets: ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor ENABLE_PREVIEWS: "YES" + CardsShareExtension: + type: app-extension + supportedDestinations: [iOS] + sources: + - path: ShareExtension + excludes: + - "Resources/Info.plist" + - "Resources/CardsShareExtension.entitlements" + - path: Sources/Core/Sync/PendingShareStore.swift + info: + path: ShareExtension/Resources/Info.plist + properties: + CFBundleDisplayName: Als Karte speichern + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController + NSExtensionAttributes: + NSExtensionActivationRule: + NSExtensionActivationSupportsText: true + NSExtensionActivationSupportsWebURLWithMaxCount: 1 + entitlements: + path: ShareExtension/Resources/CardsShareExtension.entitlements + properties: + com.apple.security.application-groups: + - group.ev.mana.cards + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards.share + CODE_SIGN_STYLE: Automatic + SKIP_INSTALL: "YES" + CardsWidgetExtension: type: app-extension supportedDestinations: [iOS] diff --git a/scripts/make-appicon.swift b/scripts/make-appicon.swift new file mode 100644 index 0000000..9c2afc1 --- /dev/null +++ b/scripts/make-appicon.swift @@ -0,0 +1,82 @@ +#!/usr/bin/env swift +// Generiert ein 1024×1024-AppIcon-PNG als Platzhalter. +// forest-green Hintergrund + großes weißes "C" mit Karten-Stack-Andeutung. +// +// Aufruf: +// swift scripts/make-appicon.swift +// +// Schreibt nach: Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +// +// Dies ist ein Platzhalter — vor App-Store-Submission durch ein +// professionelles Icon ersetzen (siehe docs/RELEASE_CHECKLIST.md). + +import AppKit +import CoreGraphics + +let size = 1024 +let scale: CGFloat = 1.0 +let cs = CGColorSpaceCreateDeviceRGB() +guard let ctx = CGContext( + data: nil, + width: size, height: size, + bitsPerComponent: 8, bytesPerRow: 0, + space: cs, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue +) else { + print("CGContext creation failed") + exit(1) +} + +// HSL(142, 76%, 28%) — forest primary light. Hand-konvertiert zu sRGB. +let background = CGColor(red: 0.043, green: 0.494, blue: 0.227, alpha: 1) +ctx.setFillColor(background) +ctx.fill(CGRect(x: 0, y: 0, width: size, height: size)) + +// Subtile Karten-Stack-Schatten hinter dem C +let shadow1 = CGColor(red: 1, green: 1, blue: 1, alpha: 0.08) +let shadow2 = CGColor(red: 1, green: 1, blue: 1, alpha: 0.05) +let cardW = CGFloat(size) * 0.62 +let cardH = CGFloat(size) * 0.82 +let cardX = (CGFloat(size) - cardW) / 2 +let cardY = (CGFloat(size) - cardH) / 2 + +ctx.setFillColor(shadow2) +ctx.beginPath() +ctx.addPath(CGPath(roundedRect: CGRect(x: cardX + 30, y: cardY - 30, width: cardW, height: cardH), + cornerWidth: 48, cornerHeight: 48, transform: nil)) +ctx.fillPath() + +ctx.setFillColor(shadow1) +ctx.beginPath() +ctx.addPath(CGPath(roundedRect: CGRect(x: cardX + 15, y: cardY - 15, width: cardW, height: cardH), + cornerWidth: 48, cornerHeight: 48, transform: nil)) +ctx.fillPath() + +// Großer weißer "C"-Buchstabe — Helvetica-Neue-Bold +let attrs: [NSAttributedString.Key: Any] = [ + .font: NSFont(name: "HelveticaNeue-Bold", size: 720) ?? NSFont.boldSystemFont(ofSize: 720), + .foregroundColor: NSColor.white, +] +let str = NSAttributedString(string: "C", attributes: attrs) +let line = CTLineCreateWithAttributedString(str) +let bounds = CTLineGetImageBounds(line, ctx) +let tx = (CGFloat(size) - bounds.width) / 2 - bounds.origin.x +let ty = (CGFloat(size) - bounds.height) / 2 - bounds.origin.y +ctx.textPosition = CGPoint(x: tx, y: ty) +CTLineDraw(line, ctx) + +// PNG schreiben +guard let cgImage = ctx.makeImage() else { + print("makeImage failed") + exit(1) +} +let bitmap = NSBitmapImageRep(cgImage: cgImage) +guard let data = bitmap.representation(using: .png, properties: [:]) else { + print("PNG encoding failed") + exit(1) +} + +let target = URL(fileURLWithPath: "Sources/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png") +try data.write(to: target) +print("Wrote \(target.path) (\(data.count) bytes)") +_ = scale // suppress unused warning