Vier Dinge, die zusammengehören: Die Spiegelung ist weg. `backgroundExtensionEffect` füllt die Fläche unter der Leiste, indem er den Bildrand gespiegelt weiterzeichnet — und je größer die zu füllende Fläche wurde, desto mehr übernahm der Spiegel das Bild. Auf dem iPhone legte er sich ohne jede Seitenleiste über den Statusbalken. Jetzt läuft das Bild selbst weiter, über einen negativen Rand. Es endet dabei an der **Kante der Leiste**, nicht am Fensterrand. Der Unterschied sind zwölf Punkte, und die waren deutlich zu sehen: neben der schwebenden Leiste stand ein heller Streifen Bild, den nichts abdeckte. Das Seitenverhältnis im Karussell musste weichen. Es legte den Aufmacher auf die Breite des Inhaltsbereichs fest; weil er nach links unter die Leiste greift, blieb rechts genau so viel schwarz übrig, wie er links gewann. Und die Maße, in denen das Hintergrundbild angefordert wird, richten sich jetzt nach dem Rahmen, in dem es landet. Vorher wurden 900×1200 geholt — hochkant — und in einem 16:9-Rahmen gezeigt: zu sehen war ein Streifen aus der Mitte, oben und unten fehlte je ein Drittel. Dazu: auf schmalen Geräten ist der Aufmacher hochkant. In 16:9 blieben auf einem iPhone keine zweihundertzwanzig Punkte Höhe, und Schriftzug, Zeile, Beschreibung und zwei Knöpfe lagen übereinander. Und ein Schleier unter dem Glas der Leiste. Reines Glas zeigt alles, was dahinter vorbeizieht, und beim Scrollen zieht die ganze Startseite vorbei — aus den ruhigen Beschriftungen wurde ein unruhiger Brei. Der Schleier nimmt der Bewegung die Schärfe, ohne die Fläche zuzumauern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
3.0 KiB
Swift
79 lines
3.0 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import MediaKit
|
|
|
|
/// Der Aufmacher oben auf der Startseite.
|
|
struct HeroSection: View {
|
|
let item: MediaItem
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(PlaybackCoordinator.self) private var playback
|
|
@Environment(\.flixyAccent) private var accent
|
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
|
|
var body: some View {
|
|
HeroBanner(
|
|
title: item.rowTitle,
|
|
metadata: metadata,
|
|
summary: item.summary,
|
|
artworkURL: session.backend.flatMap {
|
|
HeroArtwork.backdropURL(for: item, backend: $0, compact: sizeClass == .compact)
|
|
},
|
|
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)
|
|
) {
|
|
Button {
|
|
playback.play(item)
|
|
} label: {
|
|
Label(playLabel, systemImage: "play.fill")
|
|
.font(.flixyCardTitle)
|
|
.padding(.horizontal, FlixySpacing.loose)
|
|
.padding(.vertical, FlixySpacing.snug)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(accent)
|
|
|
|
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
|
|
Label("hero.details", systemImage: "info.circle")
|
|
.font(.flixyCardTitle)
|
|
.padding(.horizontal, FlixySpacing.loose)
|
|
.padding(.vertical, FlixySpacing.snug)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.tint(Color.flixyTextSecondary)
|
|
}
|
|
}
|
|
|
|
/// „Weiterschauen" statt „Abspielen", wenn schon etwas gesehen wurde —
|
|
/// sonst wäre nicht erkennbar, dass der Film an der alten Stelle weitergeht.
|
|
private var playLabel: LocalizedStringKey {
|
|
item.watchProgress == nil ? "hero.play" : "hero.resume"
|
|
}
|
|
|
|
private var metadata: String? {
|
|
var pieces: [String] = []
|
|
if item.kind == .episode, let season = item.parentIndex, let episode = item.index {
|
|
pieces.append(String(localized: "episode.short \(season) \(episode)"))
|
|
}
|
|
if let year = item.year { pieces.append(String(year)) }
|
|
if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) }
|
|
if let rating = item.contentRating { pieces.append(rating) }
|
|
return pieces.isEmpty ? nil : pieces.joined(separator: " · ")
|
|
}
|
|
}
|
|
|
|
extension Duration {
|
|
/// Laufzeit als „2 Std 46 Min" bzw. „46 Min".
|
|
var runtimeLabel: String {
|
|
let totalMinutes = Int(components.seconds / 60)
|
|
let hours = totalMinutes / 60
|
|
let minutes = totalMinutes % 60
|
|
if hours > 0 {
|
|
return String(localized: "runtime.hoursMinutes \(hours) \(minutes)")
|
|
}
|
|
return String(localized: "runtime.minutes \(minutes)")
|
|
}
|
|
}
|