Auf dem Telefon stand ein Plakat im Aufmacher — zweimal derselbe Name

Der Aufmacher auf dem iPhone hat nicht das Hintergrundbild geholt,
sondern das **Plakat**. Dafür gab es einen Grund, und er stimmte: ein
16:9-Bild in einem 2:3-Rahmen ist ein Streifen aus der Mitte.

Nur war der Rahmen das Problem, nicht das Bild. Ein Plakat trägt den
Titel selbst, und darüber legt der Aufmacher noch den freigestellten
Schriftzug — derselbe Name zweimal, einmal gesetzt und einmal gemalt.

**Jetzt ein Querbild in einem 4:3-Rahmen.** 16:9 ginge nicht: das sind
auf 440 Punkten Breite 248 Punkte Höhe, und darin sollen Schriftzug,
Zeile, ein Satz Beschreibung und zwei Knöpfe stehen. Sie standen schon
einmal übereinander, das ist der dokumentierte Grund für das Hochformat.
4:3 gibt 330 Punkte — gemessen passt der Text hinein; dafür fehlt dem
Bild links und rechts zusammen ein Viertel.

**Die Detailseite bleibt hochkant.** Dort hat man sich schon entschieden;
das Plakat ist das Bild dieses Films, und du wolltest den Kopf der Seite
ausdrücklich höher. Beide Seiten lasen bisher aus **derselben**
Funktion — eine Änderung für den Aufmacher hätte die Detailseite von 660
auf 330 Punkte gestutzt, ohne dass irgendwo ein Fehler aufgetaucht wäre.
`HeroArtwork` kennt deshalb jetzt den Verwendungszweck:

    HeroArtwork.aspectRatio(.detailHeader, compact: …)   // 2:3
    HeroArtwork.aspectRatio(compact: …)                  // 4:3 bzw. 16:9

Maße, Verhältnis und Reihenfolge der Bildpfade hängen alle an **einer**
privaten Abfrage — sie müssen zusammenpassen, sonst beschneidet der
Server anders als der Rahmen.

Die Maße schmal: 1200×900 statt 1920×1080. Der Rahmen ist dort höchstens
1320 Bildpunkte breit; das spart auf einer Mobilfunkverbindung ein
Drittel.

Gemessen im Simulator (iPhone 17 Pro Max): Aufmacher 4:3, Text passt,
Detailseite unverändert hochkant. iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-01 12:57:42 +02:00
co-authored by Claude Opus 5
parent 2ab05ee8f6
commit c37087fd34
6 changed files with 92 additions and 36 deletions
+50 -16
View File
@@ -22,26 +22,59 @@ import MediaKit
/// 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(compact: Bool) -> (width: Int, height: Int) {
compact ? (600, 900) : (1920, 1080)
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(compact: Bool) -> CGFloat {
compact ? FlixyAspectRatio.poster : FlixyAspectRatio.hero
static func aspectRatio(_ placement: Placement = .carousel, compact: Bool) -> CGFloat {
if isPortrait(placement, compact: compact) { return FlixyAspectRatio.poster }
return compact ? FlixyAspectRatio.heroCompact : 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
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.
@@ -57,13 +90,14 @@ enum HeroArtwork {
static func backdropURLs(
for item: MediaItem,
backend: any MediaBackend,
placement: Placement = .carousel,
compact: Bool
) -> [URL] {
let size = size(compact: compact)
let pfade: [String?] = compact
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.
// 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.
+12 -5
View File
@@ -78,17 +78,24 @@ struct PlayableDetailView: View {
// davon ein Streifen aus der Mitte. Derselbe Fehler war im
// Aufmacher schon einmal behoben worden, nur hier nicht.
artworkURLs: session.backend.map {
HeroArtwork.backdropURLs(for: item, backend: $0, compact: sizeClass == .compact)
HeroArtwork.backdropURLs(
for: item,
backend: $0,
placement: .detailHeader,
compact: sizeClass == .compact
)
} ?? [],
logoURL: session.backend?.rawImageURL(path: item.clearLogoPath),
// **Dasselbe Verhältnis, in dem das Bild bestellt wurde.** Hier
// stand keines, also galt die Vorgabe 16∶9 — während `HeroArtwork`
// auf einem schmalen Gerät ein **Plakat** anfordert, also hochkant.
// Zu sehen war davon ein Streifen aus der Mitte: das Bild wirkte
// angeschnitten und der Kopf der Seite viel zu flach. Der Aufmacher
// auf der Startseite macht es seit Langem richtig; die Detailseiten
// nicht.
aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact)
// angeschnitten und der Kopf der Seite viel zu flach.
//
// Beides kommt deshalb aus derselben Quelle und mit demselben
// Verwendungszweck: `detailHeader` bleibt hochkant, auch seit das
// Karussell auf dem Telefon quer ist.
aspectRatio: HeroArtwork.aspectRatio(.detailHeader, compact: sizeClass == .compact)
) {
Button {
playback.play(item, mediaIndex: selectedVersion)
+12 -5
View File
@@ -62,17 +62,24 @@ struct SeriesDetailView: View {
// davon ein Streifen aus der Mitte. Derselbe Fehler war im
// Aufmacher schon einmal behoben worden, nur hier nicht.
artworkURLs: session.backend.map {
HeroArtwork.backdropURLs(for: item, backend: $0, compact: sizeClass == .compact)
HeroArtwork.backdropURLs(
for: item,
backend: $0,
placement: .detailHeader,
compact: sizeClass == .compact
)
} ?? [],
logoURL: session.backend?.rawImageURL(path: item.clearLogoPath),
// **Dasselbe Verhältnis, in dem das Bild bestellt wurde.** Hier
// stand keines, also galt die Vorgabe 16∶9 — während `HeroArtwork`
// auf einem schmalen Gerät ein **Plakat** anfordert, also hochkant.
// Zu sehen war davon ein Streifen aus der Mitte: das Bild wirkte
// angeschnitten und der Kopf der Seite viel zu flach. Der Aufmacher
// auf der Startseite macht es seit Langem richtig; die Detailseiten
// nicht.
aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact)
// angeschnitten und der Kopf der Seite viel zu flach.
//
// Beides kommt deshalb aus derselben Quelle und mit demselben
// Verwendungszweck: `detailHeader` bleibt hochkant, auch seit das
// Karussell auf dem Telefon quer ist.
aspectRatio: HeroArtwork.aspectRatio(.detailHeader, compact: sizeClass == .compact)
) {
DownloadAllButton(container: item)
}
+2 -2
View File
@@ -128,8 +128,8 @@ struct HeroCarousel: View {
// ohne zu scrollen. Zweiundachtzig Prozent lassen die erste Reihe
// gerade hereinlugen, und damit weiß man, dass es weitergeht.
//
// Auf einem schmalen Gerät nicht: dort ist der Aufmacher hochkant
// und rechnet sich seine Höhe aus der Breite aus.
// Auf einem schmalen Gerät nicht: dort rechnet sich der Aufmacher
// seine Höhe aus der Breite aus — ein Viertel breiter als hoch.
.modifier(HeroHeight(fraction: sizeClass == .compact ? nil : layout.heroHeight, extra: overlap))
// **Kein** Seitenverhältnis hier: der Aufmacher bringt sein
// eigenes mit, und eines darüber legte ihn auf die Breite des
+3 -2
View File
@@ -37,8 +37,9 @@ struct HeroSection: View {
crossfade: crossfade,
incomingOffset: incomingOffset,
logoURL: session.backend.flatMap { HeroArtwork.logoURL(for: item, backend: $0) },
// Auf dem iPhone hochkant: 16:9 ließ dem Text so wenig Platz, dass
// Schriftzug und Beschreibung übereinanderlagen.
// Auf dem iPhone 4:3 statt 16:9: hochkant ließ 16:9 dem Text so
// wenig Platz, dass Schriftzug und Beschreibung übereinanderlagen.
// Beschnitten statt hochkant — das Bild bleibt ein Querbild.
aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact),
// Auf einem großen Bildschirm nimmt der Aufmacher die Höhe, die
// das Karussell ihm gibt — sonst wäre er so hoch, wie 16:9 es
@@ -165,12 +165,19 @@ public enum FlixyAspectRatio {
public static let hero: CGFloat = landscape
/// Der Aufmacher auf einem schmalen Bildschirm.
///
/// Nicht 16:9: auf einem iPhone sind das keine zweihundertzwanzig Punkte
/// Höhe, und darin sollen Titel oder Schriftzug, Zeile mit Jahr und Länge,
/// ein Satz Beschreibung und zwei Knöpfe Platz finden. Sie fanden ihn
/// nicht und lagen übereinander. Hochkant ist hier auch die ehrlichere
/// Form — ein Handy hält man so.
public static let heroCompact: CGFloat = 3.0 / 4.0
/// **Quer, aber beschnitten — und beides hat einen Grund.**
///
/// Quer, weil dort ein Hintergrundbild liegt. Vorher lag hier ein Plakat
/// (2:3), und das ist für einen Aufmacher das falsche Bild: ein Plakat
/// trägt den Titel schon selbst, und darüber liegt dann noch der
/// freigestellte Schriftzug — zweimal derselbe Name.
///
/// Beschnitten, weil 16:9 auf einem iPhone keine zweihundertfünfzig
/// Punkte Höhe sind, und darin sollen Schriftzug, Zeile mit Jahr und
/// Länge, ein Satz Beschreibung und zwei Knöpfe Platz finden. Sie fanden
/// ihn nicht und lagen übereinander. 4:3 gibt dem Bild rund ein Drittel
/// mehr Höhe; dafür fehlt ihm links und rechts zusammen ein Viertel.
public static let heroCompact: CGFloat = 4.0 / 3.0
/// Sendersymbole. Breiter als hoch, weil Senderlogos Schriftzüge sind —
/// quadratisch beschnitten fehlt der halbe Name.