diff --git a/Flixy/Session/HeroArtwork.swift b/Flixy/Session/HeroArtwork.swift index 524e0de..63ab0c3 100644 --- a/Flixy/Session/HeroArtwork.swift +++ b/Flixy/Session/HeroArtwork.swift @@ -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. diff --git a/Flixy/Views/Detail/PlayableDetailView.swift b/Flixy/Views/Detail/PlayableDetailView.swift index 3b6e44f..97e17a6 100644 --- a/Flixy/Views/Detail/PlayableDetailView.swift +++ b/Flixy/Views/Detail/PlayableDetailView.swift @@ -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) diff --git a/Flixy/Views/Detail/SeriesDetailView.swift b/Flixy/Views/Detail/SeriesDetailView.swift index d89a65f..7cbdc85 100644 --- a/Flixy/Views/Detail/SeriesDetailView.swift +++ b/Flixy/Views/Detail/SeriesDetailView.swift @@ -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) } diff --git a/Flixy/Views/Home/HeroCarousel.swift b/Flixy/Views/Home/HeroCarousel.swift index b591626..cae76d5 100644 --- a/Flixy/Views/Home/HeroCarousel.swift +++ b/Flixy/Views/Home/HeroCarousel.swift @@ -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 diff --git a/Flixy/Views/Home/HeroSection.swift b/Flixy/Views/Home/HeroSection.swift index 59e2682..8de27dd 100644 --- a/Flixy/Views/Home/HeroSection.swift +++ b/Flixy/Views/Home/HeroSection.swift @@ -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 diff --git a/Packages/FlixyKit/Sources/FlixyDesign/FlixyFont.swift b/Packages/FlixyKit/Sources/FlixyDesign/FlixyFont.swift index 30dea19..fae9c56 100644 --- a/Packages/FlixyKit/Sources/FlixyDesign/FlixyFont.swift +++ b/Packages/FlixyKit/Sources/FlixyDesign/FlixyFont.swift @@ -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.