From 873b8a44059f0c9bab652210c538130e7538b64e Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 13 Sep 2026 20:12:42 +0200 Subject: [PATCH] =?UTF-8?q?Volle=20H=C3=B6he,=20eine=20Kante=20statt=20ein?= =?UTF-8?q?es=20Streifens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Aufmacher nimmt jetzt achtundachtzig Prozent der sichtbaren Höhe, statt sich seine Höhe aus 16:9 auszurechnen. Ein Aufmacher, der ein Drittel hoch ist, sieht aus wie ein breites Vorschaubild; die zwölf Prozent Rest sind Absicht, dort lugt die erste Reihe herein und sagt, dass es weitergeht. Auf schmalen Geräten bleibt es beim Hochformat. `FlixyArtwork` kann dafür den Rahmen ausfüllen, den es bekommt. Das Seitenverhältnis bleibt trotzdem wichtig — danach werden die Bilder beim Server angefordert. Und `glassEffect` ist raus. Er zeichnet an seiner Kante einen Lichtsaum, und über einem hellen Aufmacher wurde daraus ein heller Balken von mehreren Punkten Breite neben der Leiste — kein Rand, sondern ein Streifen. Von einer Kante war gleichzeitig nichts zu erkennen. Von Hand sind es drei Lagen: ein trübes Material, ein Schleier gegen die durchziehende Startseite, und ein Haarstrich, der oben links aufhellt und unten rechts nachlässt. Dazu ein Schatten — was schwebt, wirft einen. Co-Authored-By: Claude Opus 5 --- Flixy/Views/Home/HeroCarousel.swift | 28 +++++++++ Flixy/Views/Home/HeroSection.swift | 7 ++- .../Views/Navigation/FlixyNavigationBar.swift | 57 +++++++++++++------ .../FlixyDesign/Components/FlixyArtwork.swift | 22 ++++++- .../FlixyDesign/Components/HeroBanner.swift | 6 +- 5 files changed, 98 insertions(+), 22 deletions(-) diff --git a/Flixy/Views/Home/HeroCarousel.swift b/Flixy/Views/Home/HeroCarousel.swift index df0b0a5..361001e 100644 --- a/Flixy/Views/Home/HeroCarousel.swift +++ b/Flixy/Views/Home/HeroCarousel.swift @@ -85,6 +85,16 @@ struct HeroCarousel: View { .offset(x: (1 - cover) * (isMovingForward ? Self.slide : -Self.slide)) } } + // Die Höhe: fast das ganze Fenster. + // + // Ein Aufmacher, der nur ein Drittel hoch ist, sieht aus wie ein + // breites Vorschaubild. Die zwölf Prozent, die übrig bleiben, + // sind Absicht — dort lugt die erste Reihe herein und sagt, dass + // es weitergeht. + // + // Auf einem schmalen Gerät nicht: dort ist der Aufmacher hochkant + // und rechnet sich seine Höhe aus der Breite aus. + .modifier(HeroHeight(fraction: sizeClass == .compact ? nil : 0.88)) // **Kein** Seitenverhältnis hier: der Aufmacher bringt sein // eigenes mit, und eines darüber legte ihn auf die Breite des // Inhaltsbereichs fest. Weil er nach links unter die Seitenleiste @@ -184,3 +194,21 @@ struct HeroCarousel: View { } } } + +/// Gibt dem Aufmacher einen Anteil an der sichtbaren Höhe — oder gar nichts. +/// +/// Eigener Baustein, weil `containerRelativeFrame` sich nicht abschalten lässt: +/// es wirkt, sobald es dasteht. Ein `if` in der Ansicht brächte den beiden +/// Zweigen außerdem verschiedene Identitäten ein, und der Aufmacher würde beim +/// Drehen neu geladen statt mitzuwandern. +private struct HeroHeight: ViewModifier { + let fraction: CGFloat? + + func body(content: Content) -> some View { + if let fraction { + content.containerRelativeFrame(.vertical) { height, _ in height * fraction } + } else { + content + } + } +} diff --git a/Flixy/Views/Home/HeroSection.swift b/Flixy/Views/Home/HeroSection.swift index 101f6b3..ed301e4 100644 --- a/Flixy/Views/Home/HeroSection.swift +++ b/Flixy/Views/Home/HeroSection.swift @@ -22,7 +22,12 @@ struct HeroSection: View { 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. - aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact) + 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 + // vorschreibt, und darüber und darunter bliebe der halbe + // Bildschirm leer. + fillsAvailableSpace: sizeClass != .compact ) { Button { playback.play(item) diff --git a/Flixy/Views/Navigation/FlixyNavigationBar.swift b/Flixy/Views/Navigation/FlixyNavigationBar.swift index adf016d..5e8842d 100644 --- a/Flixy/Views/Navigation/FlixyNavigationBar.swift +++ b/Flixy/Views/Navigation/FlixyNavigationBar.swift @@ -54,6 +54,40 @@ struct FlixySidebar: View { /// kommt eine Mediathek dazu, soll nicht plötzlich die falsche zuklappen. @State private var collapsed: Set = [] + /// Die Fläche, auf der die Leiste liegt. + /// + /// Von Hand gebaut und nicht mit `glassEffect`. Der zeichnet an seiner + /// Kante einen Lichtsaum, und über einem hellen Aufmacher wurde daraus ein + /// heller Balken von mehreren Punkten Breite, der neben der Leiste stand — + /// kein Rand, sondern ein Streifen. Gleichzeitig war von einer Kante nichts + /// zu erkennen. + /// + /// Drei Lagen: ein trübes Material, damit die Leiste eine eigene Fläche + /// ist; ein Schleier, damit die durchziehende Startseite nicht mitgelesen + /// wird; und ein Haarstrich, der oben links aufhellt und unten rechts + /// nachlässt — so fängt Glas das Licht. Dazu ein Schatten, denn was + /// schwebt, wirft einen. + @ViewBuilder + static func surface(in shape: some InsettableShape) -> some View { + shape + .fill(.ultraThinMaterial) + .overlay { shape.fill(Color.flixyCanvas.opacity(0.55)) } + .overlay { + shape.strokeBorder( + LinearGradient( + stops: [ + .init(color: .white.opacity(0.55), location: 0), + .init(color: .white.opacity(0.10), location: 0.5), + .init(color: .white.opacity(0.30), location: 1), + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + } + } + static let width: CGFloat = 235 /// Der Abstand zum Fensterrand. Er ist der eigentliche Punkt der Sache. static let margin: CGFloat = 12 @@ -81,21 +115,9 @@ struct FlixySidebar: View { .padding(.vertical, FlixySpacing.snug) } .frame(width: Self.width) - // Ein Schleier unter dem Glas. - // - // Reines Glas zeigt alles, was dahinter vorbeizieht — und beim Scrollen - // zieht die ganze Startseite vorbei. Der Aufmacher, die Plakatreihen, - // die Überschriften: alles wanderte durch die Leiste hindurch, und - // neben den ruhigen Beschriftungen wurde daraus ein unruhiger Brei. - // - // Der Schleier nimmt der Bewegung die Schärfe, ohne die Fläche - // zuzumauern: man sieht noch, dass dort etwas ist, aber man liest es - // nicht mehr mit. - .background( - Color.flixyCanvas.opacity(0.72), - in: .rect(cornerRadius: FlixyRadius.panel, style: .continuous) - ) - .glassEffect(.regular, in: .rect(cornerRadius: FlixyRadius.panel)) + .background { Self.surface(in: .rect(cornerRadius: FlixyRadius.panel, style: .continuous)) } + .clipShape(.rect(cornerRadius: FlixyRadius.panel, style: .continuous)) + .shadow(color: .black.opacity(0.45), radius: 22, y: 8) .padding(Self.margin) } @@ -223,8 +245,9 @@ struct FlixyTopBar: View { } .padding(.horizontal, FlixySpacing.snug) .padding(.vertical, FlixySpacing.tight) - .background(Color.flixyCanvas.opacity(0.72), in: .capsule) - .glassEffect(.regular, in: .capsule) + .background { FlixySidebar.surface(in: .capsule) } + .clipShape(.capsule) + .shadow(color: .black.opacity(0.45), radius: 18, y: 6) .padding(.top, FlixySpacing.snug) } diff --git a/Packages/FlixyKit/Sources/FlixyDesign/Components/FlixyArtwork.swift b/Packages/FlixyKit/Sources/FlixyDesign/Components/FlixyArtwork.swift index b392c8a..7393c0c 100644 --- a/Packages/FlixyKit/Sources/FlixyDesign/Components/FlixyArtwork.swift +++ b/Packages/FlixyKit/Sources/FlixyDesign/Components/FlixyArtwork.swift @@ -10,6 +10,13 @@ import SwiftUI public struct FlixyArtwork: View { private let url: URL? private let aspectRatio: CGFloat + /// Ob das Bild den Rahmen einnimmt, den es bekommt, statt sich seine Höhe + /// aus dem Seitenverhältnis auszurechnen. + /// + /// Für den Aufmacher: der soll so hoch sein wie das Fenster und nicht so + /// hoch, wie 16:9 es vorgibt. Das Seitenverhältnis bleibt trotzdem + /// wichtig — danach werden die Bilder beim Server angefordert. + private let fillsFrame: Bool private let fallbackSymbol: String private let contentMode: ContentMode @@ -22,11 +29,13 @@ public struct FlixyArtwork: View { public init( url: URL?, aspectRatio: CGFloat, + fillsFrame: Bool = false, fallbackSymbol: String = "film", contentMode: ContentMode = .fill ) { self.url = url self.aspectRatio = aspectRatio + self.fillsFrame = fillsFrame self.fallbackSymbol = fallbackSymbol self.contentMode = contentMode } @@ -35,9 +44,7 @@ public struct FlixyArtwork: View { @State private var hasFailed = false public var body: some View { - Rectangle() - .fill(Color.flixyPlaceholder) - .aspectRatio(aspectRatio, contentMode: .fit) + placeholder .overlay { content } @@ -45,6 +52,15 @@ public struct FlixyArtwork: View { .task(id: url) { await load() } } + @ViewBuilder + private var placeholder: some View { + if fillsFrame { + Rectangle().fill(Color.flixyPlaceholder) + } else { + Rectangle().fill(Color.flixyPlaceholder).aspectRatio(aspectRatio, contentMode: .fit) + } + } + /// Das Bild, das jetzt gezeigt werden kann. /// /// Der Blick in den Speicher steht **hier**, nicht in `load()`: eine diff --git a/Packages/FlixyKit/Sources/FlixyDesign/Components/HeroBanner.swift b/Packages/FlixyKit/Sources/FlixyDesign/Components/HeroBanner.swift index e189767..e12f58d 100644 --- a/Packages/FlixyKit/Sources/FlixyDesign/Components/HeroBanner.swift +++ b/Packages/FlixyKit/Sources/FlixyDesign/Components/HeroBanner.swift @@ -15,6 +15,8 @@ public struct HeroBanner: View { /// eigenen Schrift ist wiedererkennbar, eine Systemschrift nicht. private let logoURL: URL? private let aspectRatio: CGFloat + /// Ob der Aufmacher den Platz einnimmt, den er bekommt. + private let fillsAvailableSpace: Bool private let actions: Actions @State private var logo: PlatformImage? @@ -38,6 +40,7 @@ public struct HeroBanner: View { // kräftig beschnitten, und vom Aufmacher war nur ein Ausschnitt zu // sehen. aspectRatio: CGFloat = FlixyAspectRatio.hero, + fillsAvailableSpace: Bool = false, @ViewBuilder actions: () -> Actions ) { self.title = title @@ -46,6 +49,7 @@ public struct HeroBanner: View { self.artworkURL = artworkURL self.logoURL = logoURL self.aspectRatio = aspectRatio + self.fillsAvailableSpace = fillsAvailableSpace self.actions = actions() } @@ -121,7 +125,7 @@ public struct HeroBanner: View { } public var body: some View { - FlixyArtwork(url: artworkURL, aspectRatio: aspectRatio) + FlixyArtwork(url: artworkURL, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace) // Bis unter die Leiste und an den Fensterrand. // // Hier stand `backgroundExtensionEffect()`. Der spiegelt den