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(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, .awaitingAuthorization: SignInView( phase: session.phase, clientIdentifier: session.clientIdentifier, 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) // 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() } } }