- 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) <noreply@anthropic.com>
96 lines
3.3 KiB
Swift
96 lines
3.3 KiB
Swift
import ManaCore
|
|
import SwiftUI
|
|
|
|
/// Top-Level-View: TabView mit drei Tabs.
|
|
///
|
|
/// **Phase ζ-1:** Lesen + Erkunden laden `zitare.com` via `WebShellView`.
|
|
/// Universal-Links auf `zitare.com/q/<slug>` / `/a/<slug>` etc. öffnen
|
|
/// die App und routen in den passenden Tab.
|
|
struct RootView: View {
|
|
@Environment(AuthClient.self) private var auth
|
|
@State private var selectedTab: AppTab = .read
|
|
@State private var readTarget = WebTarget(url: AppConfig.webBaseURL)
|
|
@State private var exploreTarget = WebTarget(
|
|
url: AppConfig.webBaseURL.appendingPathComponent("explore")
|
|
)
|
|
@State private var reloadCounter: Int = 0
|
|
@State private var healthStatus: HealthStatus = .unknown
|
|
|
|
var body: some View {
|
|
TabView(selection: $selectedTab) {
|
|
WebShellView(target: readTarget)
|
|
.tabItem { Label("Lesen", systemImage: "book") }
|
|
.tag(AppTab.read)
|
|
|
|
WebShellView(target: exploreTarget)
|
|
.tabItem { Label("Erkunden", systemImage: "sparkle.magnifyingglass") }
|
|
.tag(AppTab.explore)
|
|
|
|
AccountView(healthStatus: healthStatus)
|
|
.tabItem { Label("Konto", systemImage: "person.circle") }
|
|
.tag(AppTab.account)
|
|
}
|
|
.task {
|
|
await probeHealth()
|
|
}
|
|
.onOpenURL { url in
|
|
handle(url: url)
|
|
}
|
|
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
|
if let url = activity.webpageURL { handle(url: url) }
|
|
}
|
|
}
|
|
|
|
private func probeHealth() async {
|
|
let api = ZitareAPI(auth: auth)
|
|
do {
|
|
let ok = try await api.healthCheck()
|
|
healthStatus = ok ? .ok : .down
|
|
Log.api.info("Healthz: \(ok ? "OK" : "DOWN")")
|
|
} catch {
|
|
healthStatus = .down
|
|
Log.api.warning(
|
|
"Healthz fehlgeschlagen: \(String(describing: error), privacy: .public)"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Universal-Link- und Custom-URL-Routing. Wird sowohl von
|
|
/// `onOpenURL` (Custom-Scheme `zitare://...`) als auch von
|
|
/// `onContinueUserActivity` (Universal-Links auf `zitare.com/...`)
|
|
/// aufgerufen.
|
|
///
|
|
/// Routing-Regeln (gespiegelt zu `app-manifest.json#link_patterns`):
|
|
/// - `/q/<slug>`, `/a/<slug>`, `/c/<slug>` → Lesen-Tab
|
|
/// - `/heute`, `/random`, `/feed.rss` → Lesen-Tab
|
|
/// - `/explore`, `/region/...`, `/thema/...`, `/rolle/...`,
|
|
/// `/epoche/...`, `/sprache/...`, `/search`, `/t/...` → Erkunden-Tab
|
|
/// - alles andere unter `zitare.com` → Lesen-Tab, Root-Pfad
|
|
///
|
|
/// Custom-Scheme `zitare://quote/<slug>` wird auf
|
|
/// `https://zitare.com/q/<slug>` umgemappt.
|
|
private func handle(url: URL) {
|
|
Log.app.info("Deep-Link empfangen: \(url.absoluteString, privacy: .public)")
|
|
let routed = DeepLinkRouter.route(url, base: AppConfig.webBaseURL)
|
|
reloadCounter += 1
|
|
if routed.isExplore {
|
|
exploreTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
|
selectedTab = .explore
|
|
} else {
|
|
readTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
|
selectedTab = .read
|
|
}
|
|
}
|
|
}
|
|
|
|
enum AppTab: Hashable {
|
|
case read
|
|
case explore
|
|
case account
|
|
}
|
|
|
|
enum HealthStatus {
|
|
case unknown
|
|
case ok
|
|
case down
|
|
}
|