From dd10f85ccadfe12363dabb9cdcb25ebf6499d45f Mon Sep 17 00:00:00 2001 From: Till Date: Thu, 14 May 2026 13:06:37 +0200 Subject: [PATCH] =?UTF-8?q?=CE=B6-1=20abgeschlossen:=20DeepLinkRouter=20+?= =?UTF-8?q?=20Web-Header-Hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeepLinkRouter als pure-Logic-Enum aus RootView extrahiert (resolveToWebURL, isExplorePath, route) - 11 DeepLinkRouterTests grün: custom-scheme, https passthrough, Erkunden-vs-Lesen-Routing, Substring-Guard - WebShellScripts.hideWebHeader: WKUserScript injiziert at document.start CSS, das den zitare-Web-Header (body header:has(a.brand)) ausblendet. Native TabBar übernimmt globale Navigation, Content bleibt sichtbar. - Simulator-Verifikation: Quote rendert ohne doppelte Nav-Leiste, 17 (UI + Unit) Tests grün Co-Authored-By: Claude Opus 4.7 (1M context) --- PLAN.md | 4 +- Sources/App/DeepLinkRouter.swift | 38 ++++++++ Sources/App/RootView.swift | 38 +------- .../Features/WebShell/WebShellScripts.swift | 30 +++++++ Sources/Features/WebShell/WebShellView.swift | 2 + Tests/UnitTests/DeepLinkRouterTests.swift | 90 +++++++++++++++++++ 6 files changed, 166 insertions(+), 36 deletions(-) create mode 100644 Sources/App/DeepLinkRouter.swift create mode 100644 Sources/Features/WebShell/WebShellScripts.swift create mode 100644 Tests/UnitTests/DeepLinkRouterTests.swift diff --git a/PLAN.md b/PLAN.md index eeb283e..30bdc8f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -64,8 +64,8 @@ in [`../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`](../mana/docs/playbooks | Phase | Ziel | Erfolg | Status | |---|---|---|---| -| ζ-0 | Setup, leerer Build, Login | iOS-Build ✅ + Tests ✅, Mac + Login + Healthz Live offen | 🚧 (90%) | -| ζ-1 | WebShellView + Universal-Links | UL öffnet App auf Quote-Detail | ⏳ | +| ζ-0 | Setup, leerer Build, Login | iOS-Build ✅, Tests ✅, Healthz Live ✅ | ✅ (Mac + Git-Push offen) | +| ζ-1 | WebShellView + Universal-Links | zitare.mana.how rendert im WebView, UL-Routing implementiert | 🚧 (90%) | | ζ-2 | Snapshot-Sync + DailyQuoteWidget | Widget auf realem Gerät zeigt Zitat | ⏳ | | ζ-3 | Submit-View nativ | Founder submittet Quote, Draft in /admin/queue | ⏳ | | ζ-4 | Spotlight + ShareExt + App Intents | Spotlight findet, ShareExt POSTet | ⏳ | diff --git a/Sources/App/DeepLinkRouter.swift b/Sources/App/DeepLinkRouter.swift new file mode 100644 index 0000000..200e321 --- /dev/null +++ b/Sources/App/DeepLinkRouter.swift @@ -0,0 +1,38 @@ +import Foundation + +/// Routet sowohl Custom-Scheme- (`zitare://`) als auch Universal-Link-URLs +/// (`zitare.com/...`) auf eine konkrete `WebTarget` + Ziel-Tab. +/// +/// Pure-Logic, kein State — easy testbar. +enum DeepLinkRouter { + /// Mapt eine externe URL auf eine WebShell-URL. + /// `zitare://quote/x` → `https://zitare.com/q/x`, + /// `zitare://author/x` → `https://zitare.com/a/x`, + /// `zitare://collection/x` → `https://zitare.com/c/x`. + /// `https://*` bleibt unverändert. + static func resolveToWebURL(_ url: URL, base: URL) -> URL { + if url.scheme == "zitare" { + let host = url.host ?? "" + let path = url.path + switch host { + case "quote": return base.appendingPathComponent("q\(path)") + case "author": return base.appendingPathComponent("a\(path)") + case "collection": return base.appendingPathComponent("c\(path)") + default: return base + } + } + return url + } + + /// `true` wenn der Pfad in den Erkunden-Tab gehört. Sonst Lesen-Tab. + static func isExplorePath(_ path: String) -> Bool { + let prefixes = ["/explore", "/region", "/thema", "/rolle", "/epoche", "/sprache", "/search", "/t/"] + return prefixes.contains { path == $0 || path.hasPrefix($0 + "/") } + } + + /// One-Shot-Resolution: URL + Base → (resolvedURL, isExploreTab). + static func route(_ url: URL, base: URL) -> (url: URL, isExplore: Bool) { + let resolved = resolveToWebURL(url, base: base) + return (resolved, isExplorePath(resolved.path)) + } +} diff --git a/Sources/App/RootView.swift b/Sources/App/RootView.swift index 5e94371..c8aeced 100644 --- a/Sources/App/RootView.swift +++ b/Sources/App/RootView.swift @@ -71,46 +71,16 @@ struct RootView: View { /// `https://zitare.com/q/` umgemappt. private func handle(url: URL) { Log.app.info("Deep-Link empfangen: \(url.absoluteString, privacy: .public)") - - let resolved = resolveToWebURL(url) - let path = resolved.path + let routed = DeepLinkRouter.route(url, base: AppConfig.webBaseURL) reloadCounter += 1 - - if isExplorePath(path) { - exploreTarget = WebTarget(url: resolved, reloadToken: reloadCounter) + if routed.isExplore { + exploreTarget = WebTarget(url: routed.url, reloadToken: reloadCounter) selectedTab = .explore } else { - readTarget = WebTarget(url: resolved, reloadToken: reloadCounter) + readTarget = WebTarget(url: routed.url, reloadToken: reloadCounter) selectedTab = .read } } - - /// `zitare://quote/spitteler-...` → `https://zitare.com/q/spitteler-...`. - /// `zitare://author/x` → `https://zitare.com/a/x`. - /// `zitare://collection/x` → `https://zitare.com/c/x`. - /// `https://zitare.com/` bleibt wie es ist. - private func resolveToWebURL(_ url: URL) -> URL { - if url.scheme == "zitare" { - let host = url.host ?? "" - let path = url.path - switch host { - case "quote": - return AppConfig.webBaseURL.appendingPathComponent("q\(path)") - case "author": - return AppConfig.webBaseURL.appendingPathComponent("a\(path)") - case "collection": - return AppConfig.webBaseURL.appendingPathComponent("c\(path)") - default: - return AppConfig.webBaseURL - } - } - return url - } - - private func isExplorePath(_ path: String) -> Bool { - let prefixes = ["/explore", "/region", "/thema", "/rolle", "/epoche", "/sprache", "/search", "/t/"] - return prefixes.contains { path == $0 || path.hasPrefix($0 + "/") } - } } enum AppTab: Hashable { diff --git a/Sources/Features/WebShell/WebShellScripts.swift b/Sources/Features/WebShell/WebShellScripts.swift new file mode 100644 index 0000000..f6182bd --- /dev/null +++ b/Sources/Features/WebShell/WebShellScripts.swift @@ -0,0 +1,30 @@ +import Foundation +import WebKit + +/// User-Scripts, die in jeden `WKWebView` injiziert werden. +/// +/// `WKUserScript` ist MainActor-isolated; deshalb sind die Factory- +/// Methoden hier ebenfalls MainActor. Aufrufer leben sowieso auf Main +/// (SwiftUI `makeUIView`/`makeNSView` sind MainActor). +@MainActor +enum WebShellScripts { + /// Versteckt den zitare-Web-Header (`
` mit ``), + /// weil die native TabBar bereits global navigiert. Footer und + /// Hauptinhalt bleiben sichtbar. + /// + /// CSS wird at document.start als `