import SwiftUI import FlixyDesign import MediaKit /// Eine Mediathek als Raster auf dem Fernseher. /// /// **Nachgeladen wird am Fokus, nicht am Erscheinen.** Am Telefon hängt es /// daran, dass eine Kachel sichtbar wird — bei sechzig Kacheln auf einem /// breiten Raster erscheinen die aber alle beim ersten Zeichnen, und das /// Nachladen liefe sofort los statt beim Blättern. Der Fokus steht dagegen /// immer nur an einer Stelle, und genau die ist die Frage. struct TVLibraryView: View { let libraryID: String @Environment(FlixySession.self) private var session @Environment(PlaybackCoordinator.self) private var playback @Environment(QuickPlay.self) private var quickPlay @State private var feed: LibraryFeed? @FocusState private var fokus: String? /// Sechs Spalten auf 1760 Punkten Nutzbreite: rund 250 Punkte je Plakat, /// aus drei Metern noch erkennbar. private static let spalten = 6 private var raster: [GridItem] { Array(repeating: GridItem(.flexible(), spacing: FlixySpacing.section), count: Self.spalten) } var body: some View { Group { if let feed { inhalt(feed: feed) } else { ProgressView() } } .task { guard feed == nil, let mediathek = session.libraries.first(where: { $0.id == libraryID }) else { return } let frisch = LibraryFeed(library: mediathek) feed = frisch await frisch.reload(from: session.backend) } } @ViewBuilder private func inhalt(feed: LibraryFeed) -> some View { switch feed.state { case .loading where feed.items.isEmpty: ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity) case .failed(let meldung) where feed.items.isEmpty: VStack(spacing: FlixySpacing.regular) { Text(meldung).font(.flixyBody).foregroundStyle(.white) Button("action.retry") { Task { await feed.reload(from: session.backend) } } } default: raster(feed: feed) } } private func raster(feed: LibraryFeed) -> some View { ScrollView { VStack(alignment: .leading, spacing: FlixySpacing.section) { Text(feed.library.title) .font(.flixyHero) .foregroundStyle(.white) .padding(.horizontal, FlixySpacing.screenMargin) LazyVGrid(columns: raster, spacing: FlixySpacing.section) { ForEach(Array(feed.items.enumerated()), id: \.element.id) { stelle, item in TVPosterCard( title: item.rowTitle, subtitle: item.cardSubtitle, artworkURL: session.backend?.artworkURL( path: item.posterArtworkPath, width: 560, height: 840 ), progress: item.watchProgress ) { // **Über QuickPlay, nicht unmittelbar.** Steht im // Raster eine Serie, hat die keine Fassungen; der // direkte Weg schickte ihre eigene Kennung an den // Player und scheiterte mit „keine abspielbare // Datei". QuickPlay löst stattdessen die nächste // ungesehene Folge auf. // // Ein Kontextmenü wie am Telefon gibt es hier // nicht: langes Drücken ist auf der Fernbedienung // keine verlässliche Geste. quickPlay.start(item, backend: session.backend, coordinator: playback) } .focused($fokus, equals: item.id) .onChange(of: fokus) { _, neu in guard neu == item.id, feed.shouldLoadMore(at: stelle, columns: Self.spalten) else { return } Task { await feed.loadMore(from: session.backend) } } } // **Die Platzhalter bleiben.** Am Fernseher sind sie // wichtiger als am Telefon: hört das Raster einfach auf, // läuft der Fokus gegen eine unsichtbare Wand. Sie dürfen // ihn aber nicht annehmen, sonst landet er auf einem // grauen Kasten. ForEach(0..