Files
Flaro/Flixy/Session/HeroArtwork.swift
T
ScarriffleandClaude Opus 5 87f23afe90 Die Startseite am Fernseher — und die Ladeschicht nur einmal
Aufmacher, Reihen, Mediatheken, Raster. Der sichtbare Teil ist neu; der
Teil darunter ist derselbe wie am Telefon, und das war die eigentliche
Arbeit.

**Herausgelöst statt kopiert.** `HomeFeed` und `LibraryFeed` tragen jetzt,
was vorher in `HomeView` und `LibraryView` stand: laden, ordnen, den
Aufmacher auslosen, den Zwischenspeicher abziehen, entdoppeln, nach einem
Fehlschlag sperren. Das zu kopieren wäre der schlimmste Ausgang dieses
Vorhabens gewesen — an mindestens drei dieser Stellen klebte schon je ein
Fehler, der sich als „es sind nicht alle Filme drin" äusserte und sonst
nichts. Einer davon, das Los des Aufmachers an der Position statt am
Titel, wäre in einer Kopie ein zweites Mal zu finden gewesen.

Mitgewandert sind `rowTitle`, `rowArtworkPath`, `posterArtworkPath`,
`runtimeLabel` und `HeroArtwork`: reine Ableitungen, die in einer
Ansichtsdatei lagen und deshalb nur das Telefon übersetzte.

**Nachgeladen wird am Fokus, nicht am Erscheinen.** Die alte Regel hiess
„die letzten fünf" und hing daran, dass eine Kachel sichtbar wird. Beim
ersten Zeichnen erscheinen aber alle sechzig, auch die letzten fünf — das
Nachladen lief sofort los statt beim Blättern. Auf einem Raster aus sechs
Spalten sind fünf Einträge ausserdem nicht einmal eine Zeile. `PagingWindow`
rechnet deshalb in Zeilen; sechs Tests halten es fest, darunter der Fall,
der die alte Regel entlarvt.

**Der Fokus setzt den Aufmacher.** Wer durch „Zuletzt hinzugefügt"
wandert, sieht oben, was er gerade anschaut. Kein Wechsel im Sieben-
sekundentakt wie am Telefon: vor einem Fernseher wäre ein Bild, das sich
von allein austauscht, während man es liest, eine Zumutung.

**Drei Dinge, die erst der Simulator zeigte.** Die Seiten müssen
bildfüllend laufen, weil ein Aufmacher, der achtzig Punkte vor der
Bildschirmkante aufhört, nach Fehler aussieht — also trägt `screenMargin`
auf tvOS den Überscan mit. Die Karten brauchen einen Fokus-Rückruf, weil
`\.isFocused` den nächsten fokussierbaren Vorfahren meint und das in einer
Reihe aus Karten nicht verlässlich die Karte ist. Und der Bau scheiterte
an „Unable to resolve module dependency: VLCKit" — der Abhängigkeits-
Sucher prüft `canImport(FlixyVLC)` auch für ein Ziel, das es nie verlinkt.
Ein `&& !os(tvOS)` genügt; das Paket selbst bleibt unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 19:36:49 +02:00

87 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import FlixyDesign
import MediaKit
/// Woher der Aufmacher seine Bilder nimmt.
///
/// **Liegt bei den Modellen, obwohl der Name nach Ansicht klingt.** Hier steht
/// keine Zeile SwiftUI, nur die Frage, welche Adresse in welcher Grösse
/// angefordert wird — und die beantwortet der Fernseher genauso wie das
/// Telefon.
///
/// An **einer** Stelle, weil zwei Stellen es schon zweimal falsch gemacht
/// haben:
///
/// Erstens wurde das Hintergrundbild in 900×1200 angefordert — hochkant — und
/// dann in einem 16:9-Rahmen gezeigt. Zu sehen war davon ein schmaler Streifen
/// aus der Mitte; vom Bild fehlte oben und unten je ein Drittel. Die Maße
/// richten sich jetzt nach dem Rahmen, in dem das Bild landet.
///
/// Zweitens holt das Karussell das nächste Bild vor, solange das aktuelle noch
/// steht. Weicht dessen Adresse auch nur in einer Zahl ab, ist es eine andere
/// Adresse, der Zwischenspeicher trifft daneben und der Aufmacher blendet auf
/// eine graue Fläche über.
enum HeroArtwork {
/// Die Maße, in denen das Bild angefordert wird.
///
/// Im Seitenverhältnis des Rahmens, in dem es landet — sonst schneidet der
/// Server zu, und man sieht einen Streifen aus der Mitte.
static func size(compact: Bool) -> (width: Int, height: Int) {
compact ? (600, 900) : (1920, 1080)
}
static func aspectRatio(compact: Bool) -> CGFloat {
compact ? FlixyAspectRatio.poster : FlixyAspectRatio.hero
}
/// Auf einem schmalen Bildschirm das **Plakat**, sonst das Hintergrundbild.
///
/// Ein Hintergrundbild ist 16:9. Hochkant gezeigt fehlen ihm links und
/// rechts je ein Viertel, und zu sehen ist ein Ausschnitt aus der Mitte —
/// vom Bild bleibt wenig übrig. Ein Plakat ist von vornherein hochkant;
/// es passt in den Rahmen, ohne dass etwas weggeschnitten werden muss.
static func backdropURL(for item: MediaItem, backend: any MediaBackend, compact: Bool) -> URL? {
backdropURLs(for: item, backend: backend, compact: compact).first
}
/// Alle Kandidaten, vom besten zum schlechtesten.
///
/// **Warum mehrere und nicht einer.** Eine Folge hat selten ein eigenes
/// Querformatbild, die Staffel manchmal, die Serie fast immer. Bisher
/// wurde die erste Adresse genommen, die sich **zusammensetzen** liess —
/// und ob es das Bild dahinter gibt, stellt sich erst beim Abruf heraus.
/// Gab es keines, blieb die Fläche grau, obwohl eine Ebene höher eines
/// gelegen hätte. Genau so sahen Folgen im Aufmacher aus.
///
/// Die Reihenfolge steht hier, ausprobiert wird sie beim Laden.
static func backdropURLs(
for item: MediaItem,
backend: any MediaBackend,
compact: Bool
) -> [URL] {
let size = size(compact: compact)
let pfade: [String?] = compact
// Hochkant: das Plakat der Folge, dann das der Staffel, dann das
// der Serie. Ein Hintergrundbild kommt hier zuletzt — es ist
// quer und verliert hochkant die Hälfte.
? [item.posterArtworkPath, item.thumb, item.parentThumb, item.seriesThumb, item.art]
// Quer: das eigene Hintergrundbild, dann das der Staffel, dann
// das der Serie — und erst danach das Vorschaubild der Folge.
: [item.art, item.parentThumb, item.seriesArt, item.seriesThumb, item.thumb]
var gesehen = Set<String>()
return pfade
.compactMap(\.self)
.filter { gesehen.insert($0).inserted }
.compactMap { backend.artworkURL(path: $0, width: size.width, height: size.height) }
}
/// Der freigestellte Schriftzug — ohne Bild-Transcoder.
///
/// Er ist ein PNG mit durchsichtigem Grund, und der Transcoder liefert
/// JPEG. Aus dem Schriftzug würde sonst ein schwarzer Kasten.
static func logoURL(for item: MediaItem, backend: any MediaBackend) -> URL? {
backend.rawImageURL(path: item.clearLogoPath)
}
}