pageta-native/Sources/App/RootView.swift
Till JS 8d6fc1a666 i18n: pageta-native voll über PagetaStrings (Phase 4)
Alle UI-Strings der Feature-/App-Schicht ziehen aus mana/i18n
(Listen/Detail/Marker/Feed/Rückblick/Zusammenfassen/Settings/Account
+ Archiv/Bulk/Note-Sheet/Palette); Interpolation als generierte Funktionen.
Verwaisten-Check grün (0 hartkodierte UI-Strings). Onboarding bleibt
seine eigene SOT (mana/content/onboarding).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:06:55 +02:00

249 lines
8.4 KiB
Swift

import ManaAuthUI
import ManaCore
import ManaThemeUI
import ManaTokens
import SwiftUI
/// Top-Level-Container. Platform-specific:
/// - **iOS**: TabView am unteren Rand (iPhone-Standard, gut für Daumen).
/// - **macOS**: NavigationSplitView mit Sidebar + Detail (Desktop-
/// Standard, nutzt die größere Fläche).
struct RootView: View {
#if os(iOS)
@Environment(AuthClient.self) private var auth
/// Konto-Sheet (hinter dem Avatar oben rechts) — ersetzt den
/// früheren Konto-Tab. Apple-Muster: Konto ist selten benutzt und
/// gehört nicht in die Tab-Bar. Siehe
/// `../mana/docs/playbooks/ACCOUNT_AVATAR_MIGRATION.md`.
///
/// macOS bleibt unangetastet: das Konto liegt dort weiterhin in der
/// Settings-Scene (⌘,), nicht hinter einem Avatar.
@State private var showAccount = false
/// Profil für den Avatar (treibt die Initialen). Lazy via
/// `getProfile()` geladen — `AuthClient.Status` trägt nur die E-Mail.
@State private var accountProfile: ProfileInfo?
// Theme + Modus auch hier, um sie am Konto-Sheet zu wiederholen: ein
// offenes Sheet erbt `.preferredColorScheme`/`.manaTheme` NICHT reaktiv vom
// App-Root, sonst griffe ein Hell/Dunkel-/Variant-Wechsel erst nach Reopen.
@Environment(\.manaTheme) private var theme
@AppStorage(ManaAppearanceMode.storageKey) private var appearanceRaw = ManaAppearanceMode.system
.rawValue
private var appearance: ManaAppearanceMode {
ManaAppearanceMode(rawValue: appearanceRaw) ?? .system
}
#endif
#if os(macOS)
@SceneStorage("pageta.sidebar.section") private var rawSection: String = SidebarSection.liste
.rawValue
/// Konto-Section ist auf macOS bewusst NICHT in der Sidebar —
/// Konto/Darstellung/Benachrichtigungen leben in der Settings-
/// Scene (⌘,), wie es macOS-Standard ist.
///
/// `.feed` ist seit 2026-06-01 NICHT mehr gelistet (siehe
/// SidebarSection.feed-Kommentar + ../pageta/docs/FEED.md).
private static let macSidebar: [SidebarSection] = [.liste, .markierungen, .rueckblick]
private var selection: Binding<SidebarSection> {
Binding(
get: {
let saved = SidebarSection(rawValue: rawSection) ?? .liste
return Self.macSidebar.contains(saved) ? saved : .liste
},
set: { rawSection = $0.rawValue }
)
}
var body: some View {
NavigationSplitView {
List(Self.macSidebar, id: \.self, selection: selection) { section in
Label(section.label, systemImage: section.icon)
.tag(section)
}
.listStyle(.sidebar)
.navigationTitle("Pageta")
.navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 320)
} detail: {
detailView(for: selection.wrappedValue)
}
.navigationSplitViewStyle(.balanced)
}
@ViewBuilder
private func detailView(for section: SidebarSection) -> some View {
switch section {
case .liste: LeseListeTabView()
case .markierungen: MarkierungenTabView()
case .rueckblick: RueckblickTabView()
case .feed: FeedTabView()
case .konto: LeseListeTabView() // Fallback — Konto ist über ⌘, erreichbar.
case .search: LeseListeTabView() // macOS-Suche liegt in der Lese-Liste-Toolbar, nicht in
// der Sidebar.
}
}
#else
@SceneStorage("pageta.tab.section") private var rawSection: String = SidebarSection.liste
.rawValue
/// iOS-Tabs ohne `.feed` (dormant) und ohne `.konto` (liegt seit der
/// Avatar-Migration hinter dem Avatar oben rechts → Sheet). Ein in
/// SceneStorage persistierter `.feed`- oder `.konto`-Zustand (User
/// war zuletzt im alten Feed-/Konto-Tab) fällt auf `.liste` zurück —
/// sonst landet die App auf einem nicht mehr existierenden Tab.
private static let iosTabs: [SidebarSection] = [.liste, .markierungen, .rueckblick]
private var selection: Binding<SidebarSection> {
Binding(
get: {
let saved = SidebarSection(rawValue: rawSection) ?? .liste
return Self.iosTabs.contains(saved) ? saved : .liste
},
set: { rawSection = $0.rawValue }
)
}
// Tab-Reihenfolge: Lese-Liste links (= default-Tab beim Cold-
// Launch), Markierungen, Rückblick. Pageta ist primär eine
// Reader-App — die persönliche Liste ist die wichtigste Surface,
// also der erste Tab.
//
// Der Feed-Tab ist seit 2026-06-01 raus (Werte-Spannung zum
// Engagement-Feed-Verbot + commodity-Charakter). FeedTabView +
// PagetaAPI.fetchFeed bleiben dormant im Repo — Begründung und
// Revival-Bedingung in ../pageta/docs/FEED.md.
//
// Der Konto-Tab ist seit der Avatar-Migration raus: das Konto liegt
// hinter `ManaAccountAvatarButton` oben rechts (in jedem Tab via
// `.manaAccountToolbar`) und öffnet das Konto als Sheet. Siehe
// ../mana/docs/playbooks/ACCOUNT_AVATAR_MIGRATION.md.
var body: some View {
TabView(selection: selection) {
Tab(PagetaStrings.tabArticles, systemImage: "books.vertical", value: SidebarSection.liste) {
LeseListeTabView(
accountProfile: accountProfile,
onAccountTap: { showAccount = true }
)
}
Tab(PagetaStrings.tabHighlights, systemImage: "highlighter", value: SidebarSection.markierungen) {
MarkierungenTabView(
accountProfile: accountProfile,
onAccountTap: { showAccount = true }
)
}
Tab(
PagetaStrings.tabReflection,
systemImage: "clock.arrow.circlepath",
value: SidebarSection.rueckblick
) {
RueckblickTabView(
accountProfile: accountProfile,
onAccountTap: { showAccount = true }
)
}
// iOS-26-Such-Tab: lokale Artikel-Suche statt der bisherigen
// Inline-`.searchable` in der Lese-Liste.
Tab(
PagetaStrings.actionSearch,
systemImage: "magnifyingglass",
value: SidebarSection.search,
role: .search
) {
PagetaSearchView(
accountProfile: accountProfile,
onAccountTap: { showAccount = true }
)
}
}
.sheet(isPresented: $showAccount) {
NavigationStack {
AccountTabView()
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(PagetaStrings.actionDone) { showAccount = false }
}
}
}
// Theme/Modus am Sheet-Inhalt wiederholen: ein offenes Sheet erbt
// `.preferredColorScheme`/`.manaTheme` NICHT reaktiv vom App-Root —
// sonst griffe ein Hell/Dunkel-/Variant-Wechsel erst nach Reopen.
.preferredColorScheme(appearance.colorScheme)
.manaTheme(theme)
.manaBrand(PagetaBrand.config(for: theme))
}
.task(id: signedInId) { await refreshAccountProfile() }
}
/// Identität für `task(id:)` — feuert `refreshAccountProfile` initial
/// und erneut bei jeder Sign-In/Sign-Out-Transition (der Avatar
/// braucht nach dem Login den Namen für die Initialen).
private var signedInId: String? {
if case let .signedIn(email) = auth.status { email } else { nil }
}
/// Lädt das Profil für den Avatar (Initialen), wenn signed-in; sonst
/// nil. Fehler werden geschluckt — der Avatar fällt dann auf den
/// Glyph zurück.
private func refreshAccountProfile() async {
if case .signedIn = auth.status {
accountProfile = try? await auth.getProfile()
} else {
accountProfile = nil
}
}
#endif
}
/// Top-Level-Surfaces der App. iOS rendert als TabView-Items,
/// macOS als Sidebar-Liste. `rawValue` ist persistent über
/// SceneStorage — User landet beim Re-Open im letzten Tab/Sidebar.
enum SidebarSection: String, CaseIterable, Identifiable, Hashable {
// Reihenfolge bestimmt sowohl macOS-Sidebar-Listing als auch
// impliziert die Tab-Reihenfolge auf iOS. `liste` ist bewusst
// zuerst — siehe Kommentar in RootView.body.
//
// `feed` ist seit 2026-06-01 dormant: der Case bleibt, damit
// FeedTabView weiter kompiliert und ein Wiedereinbau billig ist,
// wird aber weder von `iosTabs` noch von `macSidebar` gelistet.
// Siehe ../pageta/docs/FEED.md.
case liste
case markierungen
case rueckblick
case feed
case konto
/// iOS-26-Such-Tab (`Tab(role: .search)`) — lokale Artikel-Suche, auf
/// iOS 26 als unten schwebendes Liquid-Glass-Suchfeld. Nur iOS; macOS
/// hat die Suche in der Lese-Liste-Toolbar, nicht in der Sidebar.
case search
var id: String {
rawValue
}
var label: String {
switch self {
case .liste: PagetaStrings.tabArticles
case .markierungen: PagetaStrings.tabHighlights
case .rueckblick: PagetaStrings.tabReflection
case .feed: PagetaStrings.tabFeed
case .konto: PagetaStrings.tabAccount
case .search: PagetaStrings.actionSearch
}
}
var icon: String {
switch self {
case .liste: "books.vertical"
case .markierungen: "highlighter"
case .rueckblick: "clock.arrow.circlepath"
case .feed: "newspaper"
case .konto: "person.crop.circle"
case .search: "magnifyingglass"
}
}
}
#Preview {
RootView()
}