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 <noreply@anthropic.com>
84 lines
3.4 KiB
Swift
84 lines
3.4 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),
|
|
// 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)
|
|
} 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)")
|
|
}
|
|
}
|