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) } }