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>
155 lines
5.5 KiB
Swift
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
|
|
}
|