Drei Sachen am Karussell. Die Vorschlaege wechselten bei jedem Start, obwohl es dafuer laengst ein Zeitfenster von sechs Stunden gab. Der Grund: das Los hing an der Position in der Liste, nicht am Titel. Kam einer dazu, verschwand einer, oder aenderte sich ein Fortschritt, bekamen alle dahinter ein anderes Los — und „Weiterschauen\" waechst und schrumpft staendig. Jetzt haengt das Los an der Kennung; wer dazukommt, stoert niemanden. Als eigener Typ mit elf Tests, weil „es wechselt zu oft\" kein Befund ist, den man debuggen kann. Folgen zeigten kein Bild. Die Kette Folge → Serie gab es zwar, aber sie hoerte beim ersten Glied auf, das sich **zusammensetzen** liess — und ob es das Bild dahinter gibt, stellt sich erst beim Abruf heraus. Die Reihenfolge wandert deshalb ins Laden: Folge, Staffel, Serie, der Reihe nach, bis eine Adresse antwortet. Und auf dem Mac gibt es jetzt Pfeile links und rechts, sobald der Zeiger ueber dem Aufmacher liegt, sowie Punkte, die unter dem Zeiger wachsen und auf die man springen kann. Nur dort: auf dem iPad und dem iPhone wischt man ohnehin, und zwei Knoepfe staenden nur im Bild. 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,
|
|
artworkURLs: session.backend.map {
|
|
HeroArtwork.backdropURLs(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)")
|
|
}
|
|
}
|