zitare-native/Sources/App/RootView.swift
Till JS 7ba8684074 fix(account): Anmelden-Button + ManaAuthGate-Wiring
AccountView hatte zwar einen Status-Text aber keinen Login-Button,
und der ManaAuthGate war überhaupt nicht im App-Tree eingebaut —
Guest-Mode-User konnten sich nirgends anmelden.

- ZitareNativeApp: ManaAuthGate(auth:) instantiiert + via environment
  durchgereicht
- RootView: .manaBrand(ZitareBrand.manaBrand) +
  .manaAuthGate(authGate) { ManaLoginView(…) } für globales
  Sign-In-Sheet
- AccountView: authActionCard mit "Mit mana-Konto anmelden" /
  "Abmelden" (keepGuestMode: true)
- ZitareBrand neu (paper-Theme-Brücke zu ManaBrandConfig)
- project.yml: platformFilter: iOS für Widget+Share-Extensions
  (macOS-Build war pre-existing kaputt mit "embedded iOS content")

iOS + macOS BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:28:08 +02:00

155 lines
5.5 KiB
Swift

import ManaAuthUI
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
@Environment(ManaAuthGate.self) private var authGate
@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
.manaBrand(ZitareBrand.manaBrand)
.manaAuthGate(authGate) {
NavigationStack {
ManaLoginView(
auth: auth,
onSignUpTapped: {},
onForgotTapped: {}
)
.manaBrand(ZitareBrand.manaBrand)
}
}
.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
}