import Foundation import HaloDesign 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 { /// Wofür das Bild gebraucht wird. /// /// **Auf einem breiten Bildschirm macht das keinen Unterschied** — dort /// ist beides ein 16:9-Hintergrundbild über die ganze Breite. Schmal /// gehen die beiden auseinander, und zwar aus einem inhaltlichen Grund: /// /// Im **Karussell** sucht man noch. Dort soll eine Szene stehen, kein /// Plakat: das Plakat trägt den Titel selbst, und darüber liegt noch der /// freigestellte Schriftzug — derselbe Name zweimal. /// /// Auf einer **Detailseite** hat man sich entschieden. Dort ist das /// Plakat das Bild dieses Films, und der Kopf der Seite darf die Höhe /// nehmen, die ein Plakat braucht. enum Placement { /// Der Aufmacher auf der Startseite. case carousel /// Der Kopf einer Detailseite. case detailHeader } /// Ob der Rahmen hochkant ist. /// /// Die eine Stelle, an der die Entscheidung fällt — Maße, Verhältnis und /// Reihenfolge der Bildpfade hängen alle daran, und sie müssen /// zusammenpassen. private static func isPortrait(_ placement: Placement, compact: Bool) -> Bool { compact && placement == .detailHeader } /// 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(_ placement: Placement = .carousel, compact: Bool) -> (width: Int, height: Int) { if isPortrait(placement, compact: compact) { return (600, 900) } // Schmal landet das Querbild in einem 4:3-Rahmen von höchstens 440 // Punkten Breite — bei dreifacher Auflösung rund 1320 Bildpunkte. // Zwölfhundert ist genug und spart gegenüber 1920 ein Drittel. return compact ? (1200, 900) : (1920, 1080) } static func aspectRatio(_ placement: Placement = .carousel, compact: Bool) -> CGFloat { if isPortrait(placement, compact: compact) { return HaloAspectRatio.poster } return compact ? HaloAspectRatio.heroCompact : HaloAspectRatio.hero } static func backdropURL( for item: MediaItem, backend: any MediaBackend, placement: Placement = .carousel, compact: Bool ) -> URL? { backdropURLs(for: item, backend: backend, placement: placement, 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, placement: Placement = .carousel, compact: Bool ) -> [URL] { let size = size(placement, compact: compact) let pfade: [String?] = isPortrait(placement, compact: 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() 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) } }