// **Die ganze Datei nur für den Fernseher.** // // Sie liegt im geteilten Ordner, weil `Flixy/Session/` von beiden Zielen // übersetzt wird — und `TVServices` gibt es auf iOS nicht. Ein `#if` um den // einen Import genügte dabei nicht: der Abhängigkeits-Sucher von Clang prüft // ihn trotzdem und bricht mit „Unable to resolve module dependency: // TVServices" ab, ohne den Fernseher zu erwähnen. Dieselbe Falle wie bei // siehe `PlaybackSettings`. #if os(tvOS) import Foundation import MediaKit import TVServices /// Legt für das Regal über der App hin, was es zeigen soll. /// /// **Beim Laden der Startseite, nicht auf einem Zeitgeber.** Das Regal wird /// gezeichnet, wenn jemand im Menü über der App steht — und dann ist die App /// nicht gestartet. Was dort steht, ist also immer das, was beim letzten /// Öffnen zu sehen war. Ein Zeitgeber änderte daran nichts und hielte nur die /// App wach. @MainActor enum TopShelfWriter { /// Die App-Gruppe, die App und Erweiterung teilen. static let group = "group.com.scarriffleservices.flixy" /// Die Kante, in der das Regal Plakate zeigt. /// /// **Apples Mass für die doppelte Auflösung**, und für beide Stufen /// dasselbe Bild. Ein Apple TV zeichnet das Regal ohnehin doppelt /// aufgelöst; zwei Adressen abzulegen hiesse, dem Server zwei Grössen /// desselben Plakats abzuverlangen, damit eine davon nie gebraucht wird. private static let posterWidth = 666 private static let posterHeight = 1000 static func update( rows: [MediaRowGroup], backend: (any MediaBackend)? ) { guard let backend else { return } let abschnitte = rows .prefix(TopShelfSnapshot.sectionLimit) .map { reihe in TopShelfSnapshot.Section( id: reihe.id, title: reihe.title, entries: reihe.items .prefix(TopShelfSnapshot.entryLimit) .map { item in TopShelfSnapshot.Entry( id: item.id, title: item.rowTitle, subtitle: item.cardSubtitle, // **Das Hochkant-Plakat, nicht `thumb`.** Bei // einer Folge ist `thumb` das Szenenbild; im // Regal stünde davon ein zufälliger // Ausschnitt. Dieselbe Rückfallkette wie auf // den Kacheln. imageURL: backend.artworkURL( path: item.posterArtworkPath, width: posterWidth, height: posterHeight ), progress: item.watchProgress ) } ) } .filter { !$0.entries.isEmpty } TopShelfStore.write( TopShelfSnapshot(sections: Array(abschnitte)), group: group ) // **Und dem System sagen, dass es neu fragen soll.** // // Ohne diese Zeile bleibt die Erweiterung ungefragt, und im Header // steht, was dann geschieht: „If the completion handler is not called // in a timely manner or is called with empty content the system will // display the static image instead." Genau das war zu sehen — das // Plakat aus dem Asset-Katalog, gross und unverändert, während die // Datei daneben längst drei Reihen enthielt. // // Die Erweiterung selbst kann das nicht auslösen: sie läuft nur, // wenn sie gefragt wird. TVTopShelfContentProvider.topShelfContentDidChange() } /// Was zuletzt abgelegt wurde — in einem Satz, zum Hinsehen. /// /// **Weil man dem Regal sonst nichts ansieht.** Bleibt es leer, gibt es /// genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, /// oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen /// alle drei gleich aus — nämlich nach dem grossen Plakat aus dem /// Asset-Katalog. Diese Zeile unterscheidet sie. static var status: String { guard let ort = TopShelfStore.location(group: group) else { return String(localized: "topShelf.noGroup") } guard let abzug = TopShelfStore.read(group: group) else { let daIst = FileManager.default.fileExists(atPath: ort.path) return daIst ? String(localized: "topShelf.unreadable") : String(localized: "topShelf.noFile") } let einträge = abzug.sections.reduce(0) { $0 + $1.entries.count } let zeit = abzug.savedAt.formatted(date: .omitted, time: .shortened) return String(localized: "topShelf.written \(abzug.sections.count) \(einträge) \(zeit)") } } #endif