Files
Flaro/Flixy/Views/Navigation/RootView.swift
T
GuidoandClaude Opus 5 7cfc6e0667 Akzentfarbe zieht sich durch
Picker-Werte leuchteten weiter in der Standardfarbe, während Schalter
und Symbole daneben schon die gewählte trugen. Zwei Ursachen, beide
behoben — welche davon im Einzelfall zog, ließ sich nicht nachstellen,
weil der Bildschirm hinter der Anmeldung liegt.

Erstens stand `.tint` im Scene-Body. Scene-Bodies werten Änderungen an
beobachteten Objekten nicht zuverlässig neu aus; nach einem Farbwechsel
blieb ein Teil der Oberfläche auf dem alten Wert stehen. Jetzt setzt
RootView Tönung und Akzent — eine View wertet zuverlässig neu aus.

Zweitens nahm der Vorgabewert der Umgebung blind die Standardfarbe.
Listen und Formulare zeichnen ihre Zeilen teilweise in einem eigenen
Darstellungskontext, in dem eigene Umgebungswerte verloren gehen; dort
erschien dann Mint statt der gewählten Farbe. Der Vorgabewert liest
jetzt die gespeicherte Auswahl.

Damit das nicht wieder auseinanderläuft, liegt die Speicherung an einer
Stelle: AccentOption.stored/store. Vorher hatte FlixyTheme eigene
Schlüssel — zwei Kopien derselben Namen driften auseinander, und zwar
unbemerkt.

Dazu setzen die Formulare die Tönung ausdrücklich, statt sich auf
Vererbung zu verlassen.

154 Tests, fünf davon zur Speicherung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 09:14:50 +02:00

145 lines
4.9 KiB
Swift

import SwiftUI
import FlixyDesign
import PlexKit
/// Die Wurzelansicht.
///
/// Entscheidet zuerst anhand des Sitzungszustands, ob überhaupt etwas zu
/// zeigen ist — und erst danach, in welcher Form die Seitenleiste erscheint:
/// auf dem iPhone als ausfahrbares Blatt, auf dem iPad dauerhaft neben dem
/// Inhalt. Beide Wege nutzen dieselbe `SidebarView`.
struct RootView: View {
@Environment(FlixySession.self) private var session
@Environment(FlixyTheme.self) private var theme
@Environment(PlaybackCoordinator.self) private var playback
@Environment(\.horizontalSizeClass) private var sizeClass
@State private var selection: SidebarDestination = .home
@State private var isSidebarOpen = false
var body: some View {
Group {
switch session.phase {
case .launching:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas)
case .signedOut, .authenticating:
SignInView(
phase: session.phase,
manualCode: session.showsManualCode ? session.pendingCode : nil,
onSignIn: { Task { await session.beginSignIn() } },
onCancel: { session.cancelSignIn() }
)
case .choosingServer(let servers):
NavigationStack {
ServerPickerView(servers: servers) { resource in
Task { await session.connect(to: resource) }
}
}
case .connecting(let name):
ConnectingView(serverName: name)
case .failed(let message):
ConnectionFailureView(
message: message,
onRetry: { Task { await session.retry() } },
onSignOut: { session.signOut() }
)
case .ready:
library
}
}
.background(Color.flixyCanvas)
// Hier statt in der Scene: eine View wertet Beobachtungsänderungen
// zuverlässig neu aus, ein Scene-Body nicht.
.flixyAccent(theme)
.tint(theme.accentColor)
// Einmal an der Wurzel statt in jedem Bildschirm, von dem aus sich
// etwas starten lässt — sonst gäbe es vier Kopien desselben Übergangs.
.fullScreenCover(item: Bindable(playback).request) { request in
PlayerView(request: request)
}
.task { await session.restore() }
.onChange(of: session.phase) { _, newPhase in
// Nach einer Neuanmeldung darf nicht eine Mediathek ausgewählt
// bleiben, die es auf dem neuen Server gar nicht gibt.
if newPhase == .ready, case .library = selection { selection = .home }
}
}
@ViewBuilder
private var library: some View {
if sizeClass == .regular {
splitLayout
} else {
drawerLayout
}
}
private var sidebarDestinations: [SidebarDestination] {
session.libraries.map { .library(key: $0.key, title: $0.title) }
}
/// iPad: die Seitenleiste ist Teil des Fensters.
private var splitLayout: some View {
NavigationSplitView {
SidebarView(
libraries: sidebarDestinations,
selection: $selection,
showsHeader: false
) {}
.navigationTitle(Branding.appName)
} detail: {
NavigationStack { destination }
}
}
/// iPhone: die Seitenleiste fährt über den Inhalt.
private var drawerLayout: some View {
DrawerContainer(isOpen: $isSidebarOpen) {
SidebarView(libraries: sidebarDestinations, selection: $selection) {
isSidebarOpen = false
}
} content: {
NavigationStack {
destination
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
withAnimation(.snappy(duration: 0.28)) { isSidebarOpen = true }
} label: {
Image(systemName: "line.3.horizontal")
}
.accessibilityLabel(Text("sidebar.open"))
}
}
}
}
}
@ViewBuilder
private var destination: some View {
switch selection {
case .home:
HomeView()
case .library(let key, let title):
if let library = session.libraries.first(where: { $0.key == key }) {
LibraryView(library: library)
} else {
PlaceholderScreen(destination: .library(key: key, title: title))
}
case .liveTV:
LiveTVView()
case .downloads:
DownloadsView()
case .settings:
SettingsView()
}
}
}