import Foundation import MediaKit import TVServices /// Das Regal über der App im tvOS-Menü. /// /// **Was hier nicht passiert.** Kein Netzabruf, keine Anmeldung, kein /// Blättern. tvOS ruft diese Erweiterung auf, während jemand im Menü über der /// App steht, und bricht sie ab, wenn sie zu lange braucht. Alles, was sie /// zeigt, hat die App beim letzten Öffnen abgelegt — eine Datei, ein /// `JSONDecoder`, fertig. /// /// **Und was sie nicht weiss.** Sie kennt weder Server noch Token. Die /// Bildadressen im Abzug tragen den Zugangsschlüssel bereits, weil Jellyfin /// ihn für Bilder verlangt; damit muss diese Erweiterung nie etwas erfragen. final class TopShelfProvider: TVTopShelfContentProvider { /// **Mit Rückruf statt `async`.** Die asynchrone Fassung verlangt einen /// `Sendable`-Rückgabewert, und `TVTopShelfContent` ist keiner — die /// Objekte gehören UIKit. Der Rückruf hat diese Anforderung nicht, und /// hier gibt es ohnehin nichts zu warten: eine Datei lesen und /// entschlüsseln. override func loadTopShelfContent( completionHandler: @escaping ((any TVTopShelfContent)?) -> Void ) { completionHandler(inhalt()) } private func inhalt() -> (any TVTopShelfContent)? { guard let abzug = TopShelfStore.read(group: TopShelfIdentity.group), !abzug.sections.isEmpty else { // Nichts abgelegt — die App war noch nie offen oder der Server // hat nie geantwortet. Dann bleibt es beim Icon; das ist // ehrlicher als eine leere Reihe mit Überschrift. return nil } let abschnitte = abzug.sections.map { abschnitt in let inhalte = abschnitt.entries.map(item(from:)) let reihe = TVTopShelfItemCollection(items: inhalte) reihe.title = abschnitt.title return reihe } return TVTopShelfSectionedContent(sections: abschnitte) } private func item(from eintrag: TopShelfSnapshot.Entry) -> TVTopShelfSectionedItem { let element = TVTopShelfSectionedItem(identifier: eintrag.id) element.title = eintrag.title // **Hochkant.** Dieselbe Form wie die Plakate in der App; ein // 16:9-Bild in einem Hochkantrahmen zeigt einen zufälligen // Ausschnitt und ist zum Wiedererkennen wertlos. element.imageShape = .poster if let adresse = eintrag.imageURL { element.setImageURL(adresse, for: .screenScale1x) element.setImageURL(adresse, for: .screenScale2x) } if let fortschritt = eintrag.progress { element.playbackProgress = fortschritt } // **Abspielen, nicht nur öffnen.** Wer im Menü über einem angefangenen // Film steht und drückt, will weiterschauen — nicht erst eine // Detailseite sehen. Die App entscheidet dann, was „abspielen" hier // heisst: bei einer Serie die nächste ungesehene Folge. element.playAction = TVTopShelfAction(url: TopShelfIdentity.playURL(for: eintrag.id)) element.displayAction = TVTopShelfAction(url: TopShelfIdentity.detailURL(for: eintrag.id)) return element } }