ManaWebShell aus mana-swift-ui v0.6.0 ersetzt den lokalen `Sources/Features/WebShell/`-Ordner. WebShellCoordinator, WebShell- View, WebShellScripts geloescht (~430 LOC). CookieBridge bleibt lokal (App-spezifischer Cookie-SSO-Pfad fuer .mana.how), wandert nach `Sources/Core/WebShell/CookieBridge.swift`. `RootView.makeWebShellConfig()` baut Config mit Host-Whitelist `zitare.com` + `www.zitare.com` + `*.mana.how`, ZitareTheme-Hints, `syncDarkMode(localStorageKey: "zitare-mode")` und `hideElements` fuer den zitare-web-Header. ZitareTheme forwarded auf ManaTheme.paper aus mana-swift-core v1.6.0 (~90 LOC weg, paper-Werte jetzt single-source in `mana/packages/themes/src/variants/paper.css`). AppConfig.userAgent als plattform-spezifischer Helper hinzu. 20/20 Unit-Tests gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.1 KiB
Swift
142 lines
5.1 KiB
Swift
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 <html>.
|
|
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/<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
|
|
|
|
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/<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
|
|
}
|