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/` / `/a/` 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/`, `/a/`, `/c/` → 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/` wird auf /// `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 reloadCounter += 1 if isExplorePath(path) { exploreTarget = WebTarget(url: resolved, reloadToken: reloadCounter) selectedTab = .explore } else { readTarget = WebTarget(url: resolved, 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 { case read case explore case account } enum HealthStatus { case unknown case ok case down }