import ManaCore import ManaWebShell import SwiftUI @MainActor private func makeWebShellConfig() -> WebShellConfig { WebShellConfig( allowedHosts: [ "zitare.com", "www.zitare.com", "*.mana.how", ], userAgent: AppConfig.userAgent, backgroundColor: ZitareTheme.background, progressTint: ZitareTheme.primary, errorBackgroundColor: ZitareTheme.muted, errorForegroundColor: ZitareTheme.foreground, errorIconColor: ZitareTheme.warning, userScripts: [ // Syncs System-Dark-Mode in den WebView; zitare-web liest // `localStorage['zitare-mode']` beim First Paint und toggelt // dann `.dark` auf . WebShellScripts.syncDarkMode(localStorageKey: "zitare-mode"), // Versteckt den zitare-web-Header (Brand-Logo + Nav), weil // die native TabBar bereits global navigiert. WebShellScripts.hideElements( selectors: [ "header[data-app-nav]", "body header:has(a.brand)", "body > header:first-of-type", "body > div > header:first-of-type", ], tagName: "hide-web-header" ), ] ) } /// 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 private var webShellConfig: WebShellConfig { makeWebShellConfig() } var body: some View { TabView(selection: $selectedTab) { WebShellView(target: readTarget, config: webShellConfig) .tabItem { Label("Lesen", systemImage: "book") } .tag(AppTab.read) WebShellView(target: exploreTarget, config: webShellConfig) .tabItem { Label("Erkunden", systemImage: "sparkle.magnifyingglass") } .tag(AppTab.explore) AccountView(healthStatus: healthStatus) .tabItem { Label("Konto", systemImage: "person.circle") } .tag(AppTab.account) } // Mac-Window-Hintergrund auf Paper-Theme setzen, damit der // TabBar-/Title-Bar-Bereich oben nicht mit dem System-Grau // gegen das Paper-Theme ausreißt. `windowToolbar`-Placement // ist macOS-only. .background(ZitareTheme.background.ignoresSafeArea()) #if os(macOS) .toolbarBackground(ZitareTheme.background, for: .windowToolbar) .toolbarBackground(.visible, for: .windowToolbar) #endif .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 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 }