import SwiftUI import FlixyDesign import MediaKit /// Die Startseite auf dem Fernseher. /// /// **Dasselbe Modell wie am Telefon.** `HomeFeed` lädt, ordnet und lost den /// Aufmacher aus; hier steht nur, wie das aussieht. Der Unterschied ist die /// Bedienart, nicht der Inhalt. struct TVHomeView: View { @Environment(FlixySession.self) private var session @Environment(HomeLayoutSettings.self) private var layout @Environment(PlaybackCoordinator.self) private var playback @Environment(QuickPlay.self) private var quickPlay @State private var feed = HomeFeed() @State private var pfad = NavigationPath() /// Welcher Titel gerade oben gross steht. /// /// **Der Fokus bestimmt ihn, nicht eine Uhr.** Am Telefon wechselt der /// Aufmacher alle sieben Sekunden von selbst — dort blättert man ihn auch /// mit dem Daumen. Vor einem Fernseher wäre ein Bild, das sich von allein /// austauscht, während man es ansieht, eine Zumutung: man verliert den /// Titel, den man gerade lesen wollte. @State private var aufmacher: MediaItem? var body: some View { NavigationStack(path: $pfad) { inhalt .navigationDestination(for: MediaRoute.self) { route in TVDetailView(route: route) } .navigationDestination(for: LibraryRoute.self) { route in TVLibraryView(libraryID: route.id) } } .task(id: session.serverName) { await feed.load(from: session.backend) } } @ViewBuilder private var inhalt: some View { switch feed.state { case .loading: ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity) case .failed(let meldung): VStack(spacing: FlixySpacing.regular) { Text(meldung).font(.flixyBody).foregroundStyle(.white) Button("action.retry") { Task { await feed.load(from: session.backend) } } } case .loaded: let reihen = feed.rows(for: layout) if reihen.isEmpty && session.libraries.isEmpty { Text("home.empty.title").font(.flixyRowHeader).foregroundStyle(.white) } else { seite(reihen: reihen) } } } private func seite(reihen: [MediaRowGroup]) -> some View { ScrollView { LazyVStack(alignment: .leading, spacing: FlixySpacing.section) { if let gezeigt = aufmacher ?? feed.heroItems(for: layout).first { TVHeroSection( item: gezeigt, onPlay: { spiele(gezeigt) }, onDetails: { zeige(gezeigt) } ) .frame(height: 720) } ForEach(reihen) { reihe in // **Der Fokus setzt den Aufmacher.** Wer durch „Zuletzt // hinzugefügt" wandert, sieht oben, was er gerade // anschaut — ohne irgendwo hindrücken zu müssen. TVMediaRow( row: reihe, onSelect: { spiele($0) }, onFocus: { aufmacher = $0 } ) } if !session.libraries.isEmpty { mediatheken } } .padding(.bottom, FlixySpacing.screenMargin) } .scrollIndicators(.never) // **Bildfüllend, und der Rand steckt in `screenMargin`.** Der // Aufmacher muss bis an die Bildschirmkante reichen; ein Bild, das // achtzig Punkte davor aufhört, sieht nach Fehler aus. .ignoresSafeArea() } /// Die Mediatheken als eigene Reihe. /// /// Unten und nicht oben: wer den Fernseher einschaltet, will meistens /// weiterschauen, nicht stöbern. Und als Reihe statt als Registerkarten, /// weil eine Reihe am Fokus hängt und Registerkarten eine zweite Bedienart /// wären. private var mediatheken: some View { VStack(alignment: .leading, spacing: FlixySpacing.regular) { Text("library.title") .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) .padding(.horizontal, FlixySpacing.screenMargin) ScrollView(.horizontal) { LazyHStack(spacing: FlixySpacing.regular) { ForEach(session.libraries, id: \.id) { mediathek in Button { pfad.append(LibraryRoute(id: mediathek.id)) } label: { Text(mediathek.title) .font(.flixyRowHeader) .frame(width: 360, height: 180) } .buttonStyle(.card) } } .padding(.horizontal, FlixySpacing.screenMargin) .padding(.vertical, FlixySpacing.regular) } .scrollClipDisabled() .scrollIndicators(.never) } } // MARK: - Was ein Druck auslöst /// **Über QuickPlay, nicht unmittelbar.** Steht in der Reihe eine Serie — /// und Jellyfins „Zuletzt hinzugefügt" liefert neue Folgen als ihre Serie /// —, schickte der direkte Weg deren eigene Kennung an den Player, und der /// scheiterte mit „keine abspielbare Datei". Eine Serie hat keine /// Fassungen. private func spiele(_ item: MediaItem) { quickPlay.start(item, backend: session.backend, coordinator: playback) } private func zeige(_ item: MediaItem) { pfad.append(MediaRoute(ratingKey: item.id, title: item.title)) } }