pageta-native/Sources/App/RootView.swift
Till JS 7cb9399b4c feat: Lese-Rückblick (Tag/Woche/Gesamt) als nativer Tab
Pendant zur Web-Route /rueckblick. Synthetisiert, was man WIRKLICH
gelesen hat (status=finished oder readingProgress>=0.5 im Fenster).

- Core/Domain/Digest.swift: reine, getestete Logik — Swift-Spiegel von
  pageta-domain/digest.ts (Auswahl nach Zeitfenster + Lese-Signal,
  Theme-Gruppierung nach Tags, Prompt-Builder). 8 XCTest-Fälle, grün.
- Features/RueckblickTabView.swift: deterministisches Gerüst steht IMMER
  (gratis, offline) — Kennzahlen, Themencluster, eigene Markierungen
  wörtlich. Darüber optionale Synthese via ManaLLM.generate (lokal-first;
  liefert sie nichts, bleibt das Gerüst nutzbar). Tag/Woche/Gesamt-Picker,
  Markdown-ShareLink, Tap → ArticleDetailView.
- RootView: neuer Tab „Rückblick" (iOS TabView + macOS Sidebar), Icon
  clock.arrow.circlepath, nach Markierungen.
- Motivations-Leitplanke (Wert 8): ruhiger Spiegel, keine Streaks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:26:43 +02:00

147 lines
4.7 KiB
Swift

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(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.
}
}
#else
@SceneStorage("pageta.tab.section") private var rawSection: String = SidebarSection.liste.rawValue
// iOS-Tabs ohne `.feed` (dormant). Ein in SceneStorage
// persistierter `.feed`-Zustand (User war zuletzt im alten
// Feed-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, .konto]
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, Konto rechts. 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.
var body: some View {
TabView(selection: selection) {
LeseListeTabView()
.tag(SidebarSection.liste)
.tabItem { Label("Stapel", systemImage: "books.vertical") }
MarkierungenTabView()
.tag(SidebarSection.markierungen)
.tabItem { Label("Marker", systemImage: "highlighter") }
RueckblickTabView()
.tag(SidebarSection.rueckblick)
.tabItem { Label("Rückblick", systemImage: "clock.arrow.circlepath") }
AccountTabView()
.tag(SidebarSection.konto)
.tabItem { Label("Konto", systemImage: "person.crop.circle") }
}
}
#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
var id: String { rawValue }
var label: String {
switch self {
case .liste: "Stapel"
case .markierungen: "Marker"
case .rueckblick: "Rückblick"
case .feed: "Feed"
case .konto: "Konto"
}
}
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"
}
}
}
#Preview {
RootView()
}