Lift von Hybrid (WKWebView für Lesen/Erkunden) auf fully-native ist beschlossen. Diese Phase entfernt die WebShell-Infrastruktur; das volle native Read-Surface folgt in η-2..η-5 nach docs/NATIVE_LIFT_PLAN.md. - ManaWebShell-Dep raus aus project.yml - Sources/Core/WebShell/CookieBridge.swift gelöscht - RootView auf vier native Tabs (Lesen + Erkunden = Platzhalter, Submit + Konto unverändert nativ) - DocComments in DeepLinkRouter / AppConfig / Account / Settings von WebView-Verweisen befreit - CLAUDE.md Invarianten von Hybrid auf η umgestellt (13 Invarianten, pure SwiftUI + Offline-first + SafariView-Ausnahme für Legal) - PLAN.md auf η-0 + Phasenübersicht η-0..η-10 - AppConfigTests.test_keychainService_matchesSharedGroup auf ManaSharedKeychainGroup aktualisiert (war drift seit Cross-App-SSO) Verifikation: - xcodebuild iOS-Simulator iPhone 16e: BUILD SUCCEEDED - nm ZitareNative | grep WKWebView: 0 Referenzen - otool -L: kein WebKit-Framework-Link - 20/20 Tests grün Cross-Repo-Follow-up (η-1 Blocker): - zitare/apps/zitare/ muss index-full.json + 7 Stammdaten-JSONs liefern - zitare/apps/api/ Volltext-Search-Endpoint bestätigen/ergänzen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
5.7 KiB
Swift
157 lines
5.7 KiB
Swift
import ManaAuthUI
|
|
import ManaCore
|
|
import SwiftUI
|
|
|
|
/// Top-Level-View: TabView mit vier nativen Tabs.
|
|
///
|
|
/// **η-0 (de-Hybrid 2026-05-22):** `WebShellView` ist raus. Lesen +
|
|
/// Erkunden zeigen jetzt Platzhalter-Views; das volle native Read-
|
|
/// Surface kommt in η-2 (Heute / Quote-Detail / Author-Detail) bzw.
|
|
/// η-4 (Explore + Filter). Universal-Links werden hier vorerst nur
|
|
/// geloggt und in den Lesen-Tab gepinnt, bis `NavigationStack`-Routing
|
|
/// in η-2 steht. Submit + Konto sind schon nativ.
|
|
struct RootView: View {
|
|
@Environment(AuthClient.self) private var auth
|
|
@Environment(ManaAuthGate.self) private var authGate
|
|
@State private var selectedTab: AppTab = .read
|
|
@State private var pendingDeepLink: URL?
|
|
@State private var healthStatus: HealthStatus = .unknown
|
|
|
|
var body: some View {
|
|
TabView(selection: $selectedTab) {
|
|
ReadPlaceholderView(pendingDeepLink: pendingDeepLink)
|
|
.tabItem { Label("Lesen", systemImage: "book") }
|
|
.tag(AppTab.read)
|
|
|
|
ExplorePlaceholderView()
|
|
.tabItem { Label("Erkunden", systemImage: "sparkle.magnifyingglass") }
|
|
.tag(AppTab.explore)
|
|
|
|
SubmitQuoteView()
|
|
.tabItem { Label("Einreichen", systemImage: "square.and.pencil") }
|
|
.tag(AppTab.submit)
|
|
|
|
AccountView(healthStatus: healthStatus)
|
|
.tabItem { Label("Konto", systemImage: "person.circle") }
|
|
.tag(AppTab.account)
|
|
}
|
|
.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)"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Deep-Link- + Universal-Link-Routing.
|
|
///
|
|
/// η-0: Routing-Logik (URL-Normalisierung + Tab-Auswahl) bleibt
|
|
/// erhalten, das eigentliche Ansteuern einer Quote/Author/Source-
|
|
/// Detail-View kommt in η-2 mit `NavigationPath`. Solange parken
|
|
/// wir die URL in `pendingDeepLink`, die Placeholder-View zeigt
|
|
/// sie zur Diagnose an.
|
|
private func handle(url: URL) {
|
|
Log.app.info("Deep-Link empfangen: \(url.absoluteString, privacy: .public)")
|
|
let routed = DeepLinkRouter.route(url, base: AppConfig.webBaseURL)
|
|
pendingDeepLink = routed.url
|
|
selectedTab = routed.isExplore ? .explore : .read
|
|
}
|
|
}
|
|
|
|
enum AppTab: Hashable {
|
|
case read
|
|
case explore
|
|
case submit
|
|
case account
|
|
}
|
|
|
|
enum HealthStatus {
|
|
case unknown
|
|
case ok
|
|
case down
|
|
}
|
|
|
|
/// η-0 Platzhalter — wird in η-2 durch HeuteView + QuoteFeedView ersetzt.
|
|
private struct ReadPlaceholderView: View {
|
|
let pendingDeepLink: URL?
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "book")
|
|
.font(.system(size: 56))
|
|
.foregroundStyle(ZitareTheme.mutedForeground)
|
|
Text("Lesen")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
Text("Native Read-Surface kommt in η-2 (Heute, Quote-Detail, Author-Detail).")
|
|
.font(.callout)
|
|
.foregroundStyle(ZitareTheme.mutedForeground)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
if let url = pendingDeepLink {
|
|
Text("Deep-Link wartet auf η-2:\n\(url.absoluteString)")
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(ZitareTheme.mutedForeground)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
.padding(.top, 8)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(ZitareTheme.background)
|
|
}
|
|
}
|
|
|
|
/// η-0 Platzhalter — wird in η-4 durch ExploreView (Facet-Filter) ersetzt.
|
|
private struct ExplorePlaceholderView: View {
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "sparkle.magnifyingglass")
|
|
.font(.system(size: 56))
|
|
.foregroundStyle(ZitareTheme.mutedForeground)
|
|
Text("Erkunden")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
Text("Filter (Sprache, Länge, Thema, Region, Epoche, Rolle) + lokale Suche kommen in η-4 und η-5.")
|
|
.font(.callout)
|
|
.foregroundStyle(ZitareTheme.mutedForeground)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(ZitareTheme.background)
|
|
}
|
|
}
|