Navigation: das baut JellyTV nicht selbst — das ist Apples Bauteil
Auf deinem vierten Bild steht „Ziehe Objekte zur Seiten- oder Tableiste, um sie anzupassen". Das ist kein Text, den jemand geschrieben hat, sondern einer von Apple. JellyTV benutzt dort `TabView` mit `sidebarAdaptable` — und damit ist die Frage, wie die das hinbekommen, beantwortet: gar nicht, das System macht es. Meine drei selbstgebauten Formen fliegen dafür raus: das ausfahrbare Blatt, die geteilte Ansicht und die Reihe oben, die ich vor einer Stunde noch selbst gebaut habe. Zusammen konnten sie weniger als das eine Bauteil und mussten dreimal gepflegt werden. Was jetzt vom System kommt: - die Seitenleiste aus Glas, mit dem Inhalt **dahinter** statt daneben - der Knopf in ihrer Kopfzeile, der zwischen Seitenleiste und Leiste **oben** umschaltet — genau der, den du auf deinen Bildern siehst - „Bearbeiten" mit Ziehen, Ein- und Ausblenden, für beide Formen gleichzeitig - auf dem iPhone die gewohnte Leiste unten statt eines Hamburger-Menüs Die Einstellung „seitlich oder oben" entfällt damit: sie säße neben einem Systemknopf, der dasselbe besser kann. Was du umstellst, merkt sich das System selbst; wir geben ihm nur einen Ort zum Aufbewahren. Die Mediatheken stehen in einem eigenen Abschnitt, der sich in der Seitenleiste zuklappen lässt. Die Suche bekommt ihre eigene Rolle und wird deshalb getrennt von den übrigen Einträgen dargestellt. Angesehen auf iPad (Seitenleiste, Leiste oben, Bearbeiten-Dialog) und iPhone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5cebfed36b
commit
4a8500fc37
@@ -1,15 +1,15 @@
|
||||
import SwiftUI
|
||||
import FlixyDesign
|
||||
import MediaKit
|
||||
import FlixyDownloads
|
||||
import FlixyPlayback
|
||||
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`.
|
||||
/// Entscheidet anhand des Sitzungszustands, ob überhaupt etwas zu zeigen ist.
|
||||
/// Welche Form die Navigation danach annimmt — Seitenleiste, Leiste oben,
|
||||
/// Leiste unten —, entscheidet das System und der Nutzer, nicht diese Datei.
|
||||
struct RootView: View {
|
||||
@Environment(FlixySession.self) private var session
|
||||
@Environment(FlixyTheme.self) private var theme
|
||||
@@ -18,7 +18,6 @@ struct RootView: View {
|
||||
@Environment(NetworkConditions.self) private var network
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
||||
@Environment(NavigationSettings.self) private var navigation
|
||||
|
||||
/// Der laufende Player — hier gehalten, weil diese Ansicht immer da ist.
|
||||
///
|
||||
@@ -29,7 +28,13 @@ struct RootView: View {
|
||||
@Environment(\.horizontalSizeClass) private var sizeClass
|
||||
|
||||
@State private var selection: SidebarDestination = .home
|
||||
@State private var isSidebarOpen = false
|
||||
/// Was der Nutzer an der Navigation umgestellt hat.
|
||||
///
|
||||
/// Reihenfolge, Ein- und Ausblenden, und ob ein Eintrag in der Leiste oben
|
||||
/// liegt: das System schreibt es hier hinein, wir geben ihm nur einen Ort
|
||||
/// zum Aufbewahren.
|
||||
@AppStorage("flixy.navigation.customization")
|
||||
private var customization = TabViewCustomization()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -141,114 +146,86 @@ struct RootView: View {
|
||||
await fresh.start()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var library: some View {
|
||||
switch navigation.placement {
|
||||
case .top:
|
||||
topLayout
|
||||
case .side:
|
||||
if sizeClass == .regular {
|
||||
splitLayout
|
||||
} else {
|
||||
drawerLayout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Die Navigation als Reihe über dem Inhalt.
|
||||
/// Die Navigation.
|
||||
///
|
||||
/// Ohne Seitenleiste und ohne Blatt: die Einträge stehen alle gleichzeitig
|
||||
/// da. Auf einem Fernseher und auf einem breiten Fenster ist das die
|
||||
/// natürlichere Form — und auf dem Handy für den, der lieber tippt als
|
||||
/// wischt.
|
||||
private var topLayout: some View {
|
||||
NavigationStack {
|
||||
destination
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
TopNavigationBar(items: allDestinations, selection: $selection)
|
||||
/// Sie kommt vom System, nicht von uns. `sidebarAdaptable` ist genau die
|
||||
/// Form, die man aus JellyTV kennt: auf dem iPad eine Seitenleiste aus
|
||||
/// Glas, über deren Kopfzeile sich mit einem Knopf auf eine Leiste **oben**
|
||||
/// umschalten lässt, auf dem iPhone die gewohnte Leiste unten. Dazu gibt
|
||||
/// es „Bearbeiten" mit Ziehen, Ein- und Ausblenden — alles vom System,
|
||||
/// übersetzt, barrierefrei und mit der Gestaltung, die Apple in diesem
|
||||
/// Jahr vorgibt.
|
||||
///
|
||||
/// Vorher standen hier drei selbstgebaute Formen nebeneinander: ein
|
||||
/// ausfahrbares Blatt, eine geteilte Ansicht und eine Reihe oben. Sie
|
||||
/// konnten zusammen weniger als das eine Bauteil und mussten dreimal
|
||||
/// gepflegt werden.
|
||||
private var library: some View {
|
||||
TabView(selection: $selection) {
|
||||
Tab("sidebar.home", systemImage: "house", value: SidebarDestination.home) {
|
||||
NavigationStack { HomeView() }
|
||||
}
|
||||
.customizationID("flixy.tab.home")
|
||||
|
||||
TabSection {
|
||||
ForEach(session.libraries) { library in
|
||||
Tab(
|
||||
library.title,
|
||||
systemImage: symbol(for: library.kind),
|
||||
value: SidebarDestination.library(key: library.id, title: library.title)
|
||||
) {
|
||||
NavigationStack { LibraryView(library: library) }
|
||||
}
|
||||
.customizationID("flixy.tab.library.\(library.id)")
|
||||
}
|
||||
} header: {
|
||||
Text("sidebar.libraries")
|
||||
}
|
||||
.customizationID("flixy.tab.libraries")
|
||||
|
||||
if session.supportsLiveTV {
|
||||
Tab(
|
||||
"sidebar.liveTV",
|
||||
systemImage: "dot.radiowaves.left.and.right",
|
||||
value: SidebarDestination.liveTV
|
||||
) {
|
||||
NavigationStack { LiveTVView() }
|
||||
}
|
||||
.customizationID("flixy.tab.liveTV")
|
||||
}
|
||||
|
||||
Tab(
|
||||
"sidebar.downloads",
|
||||
systemImage: "arrow.down.circle",
|
||||
value: SidebarDestination.downloads
|
||||
) {
|
||||
NavigationStack { DownloadsView() }
|
||||
}
|
||||
.customizationID("flixy.tab.downloads")
|
||||
|
||||
Tab("sidebar.settings", systemImage: "gearshape", value: SidebarDestination.settings) {
|
||||
NavigationStack { SettingsView() }
|
||||
}
|
||||
.customizationID("flixy.tab.settings")
|
||||
|
||||
// Die Suche bekommt ihre eigene Rolle: das System stellt sie
|
||||
// getrennt von den übrigen Einträgen dar, so wie man es aus jeder
|
||||
// anderen App kennt.
|
||||
Tab(value: SidebarDestination.search, role: .search) {
|
||||
NavigationStack { SearchView() }
|
||||
}
|
||||
.customizationID("flixy.tab.search")
|
||||
}
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
.tabViewCustomization($customization)
|
||||
.miniPlayer(player, isVisible: playback.presentation == .mini)
|
||||
}
|
||||
|
||||
/// Alle Einträge in einer Reihe — für die Navigation oben, die keine
|
||||
/// Abschnitte kennt.
|
||||
private var allDestinations: [SidebarDestination] {
|
||||
var items: [SidebarDestination] = [.home]
|
||||
items.append(contentsOf: sidebarDestinations)
|
||||
if session.supportsLiveTV { items.append(.liveTV) }
|
||||
items.append(contentsOf: [.downloads, .settings])
|
||||
return items
|
||||
}
|
||||
|
||||
private var sidebarDestinations: [SidebarDestination] {
|
||||
session.libraries.map { .library(key: $0.id, title: $0.title) }
|
||||
}
|
||||
|
||||
/// iPad: die Seitenleiste ist Teil des Fensters.
|
||||
private var splitLayout: some View {
|
||||
NavigationSplitView {
|
||||
SidebarView(
|
||||
presentation: .column,
|
||||
libraries: sidebarDestinations,
|
||||
showsLiveTV: session.supportsLiveTV,
|
||||
selection: $selection,
|
||||
showsHeader: false
|
||||
) {}
|
||||
.navigationTitle(Branding.appName)
|
||||
} detail: {
|
||||
NavigationStack { destination }
|
||||
.miniPlayer(player, isVisible: playback.presentation == .mini)
|
||||
}
|
||||
}
|
||||
|
||||
/// iPhone: die Seitenleiste fährt über den Inhalt.
|
||||
private var drawerLayout: some View {
|
||||
DrawerContainer(isOpen: $isSidebarOpen) {
|
||||
SidebarView(
|
||||
libraries: sidebarDestinations,
|
||||
showsLiveTV: session.supportsLiveTV,
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Innerhalb des Inhalts, nicht darüber: fährt die Seitenleiste
|
||||
// aus, soll sie die Kachel verdecken und nicht umgekehrt.
|
||||
.miniPlayer(player, isVisible: playback.presentation == .mini)
|
||||
}
|
||||
}
|
||||
|
||||
@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.id == key }) {
|
||||
LibraryView(library: library)
|
||||
} else {
|
||||
PlaceholderScreen(destination: .library(key: key, title: title))
|
||||
}
|
||||
case .liveTV:
|
||||
LiveTVView()
|
||||
case .downloads:
|
||||
DownloadsView()
|
||||
case .settings:
|
||||
SettingsView()
|
||||
private func symbol(for kind: MediaLibrary.Kind) -> String {
|
||||
switch kind {
|
||||
case .movie: "film"
|
||||
case .show: "tv"
|
||||
default: "rectangle.stack"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user