Die Schwelle aus den Einstellungen wirkt serverseitig, also sofort überall: `/home/continue` liest die gespeicherte Spalte. Eine Stelle blieb trotzdem widersprüchlich — die **Beschriftung**. Web und Apps entschieden „Weiterschauen" gegen „Abspielen" selbst, allein daran, ob eine Stelle über null steht. Nach einer Sekunde stand der Titel damit nicht mehr in der Reihe, hiess aber weiter „Weiterschauen", trug einen Fortschrittsbalken unter der Kachel und bot „Aus Weiterschauen entfernen" an. Der Server hatte diese Frage längst beantwortet. `ItemStateResponse` bringt sie jetzt mit; `MediaItem.isResumable` und `.resumeProgress` sind die einzigen Stellen, die sie noch stellen. Im Simulator nachgeprüft: nach einer Sekunde heisst der Knopf „Abspielen", „Von vorn" fehlt, und der Balken unter der Kachel ist weg. Kennt ein Server das Feld nicht — Jellyfin, oder ein älterer Flaro —, gilt wie früher die blosse Stelle. **Und das Profilbild steht in den Apps.** `FlaroAccount` führt es mit, `/auth/me` bringt es bei jedem Start frisch, und der Buchstabenkreis tritt zurück, sobald eines da ist. Jellyfin bleibt beim Buchstaben — dort gibt es kein Profilbild, und das ist keine Lücke, sondern der Stand der Dinge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.4 KiB
Swift
65 lines
2.4 KiB
Swift
import SwiftUI
|
|
import FlaroDesign
|
|
import MediaKit
|
|
|
|
/// Der Aufmacher oben auf der Startseite des Fernsehers.
|
|
///
|
|
/// Derselbe `HeroBanner` wie am Telefon — das Bauteil kennt keine Plattform,
|
|
/// es setzt Bild, Verlauf und Text. Anders sind nur die Knöpfe: hier sind es
|
|
/// Fokusziele, und sie müssen gross genug sein, dass man aus drei Metern
|
|
/// sieht, welcher gerade dran ist.
|
|
struct TVHeroSection: View {
|
|
let item: MediaItem
|
|
let onPlay: () -> Void
|
|
let onDetails: () -> Void
|
|
|
|
@Environment(FlaroSession.self) private var session
|
|
@Environment(\.flaroAccent) private var accent
|
|
|
|
var body: some View {
|
|
HeroBanner(
|
|
title: item.rowTitle,
|
|
metadata: metadaten,
|
|
summary: item.summary,
|
|
artworkURLs: session.backend.map {
|
|
HeroArtwork.backdropURLs(for: item, backend: $0, compact: false)
|
|
} ?? [],
|
|
logoURL: session.backend.flatMap { HeroArtwork.logoURL(for: item, backend: $0) },
|
|
aspectRatio: FlaroAspectRatio.hero,
|
|
// Der Aufmacher nimmt die Höhe, die das Karussell ihm gibt.
|
|
fillsAvailableSpace: true
|
|
) {
|
|
Button(action: onPlay) {
|
|
Label(abspielTitel, systemImage: "play.fill")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(accent)
|
|
|
|
Button(action: onDetails) {
|
|
Label("hero.details", systemImage: "info.circle")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
|
|
/// „Weiterschauen" statt „Abspielen", wenn schon etwas gesehen wurde.
|
|
private var abspielTitel: LocalizedStringKey {
|
|
item.isResumable ? "hero.resume" : "hero.play"
|
|
}
|
|
|
|
private var metadaten: String? {
|
|
var teile: [String] = []
|
|
if item.kind == .episode, let staffel = item.parentIndex, let folge = item.index {
|
|
// Ausgeschrieben: der Aufmacher am Fernseher ist 1760 Punkte
|
|
// breit, und gelesen wird er aus drei Metern.
|
|
teile.append(String(localized: "episode.long \(staffel) \(folge)"))
|
|
}
|
|
if let jahr = item.year { teile.append(String(jahr)) }
|
|
if let dauer = item.duration {
|
|
teile.append(Duration.milliseconds(dauer).runtimeLabel)
|
|
}
|
|
if let freigabe = item.contentRating { teile.append(freigabe) }
|
|
return teile.isEmpty ? nil : teile.joined(separator: " · ")
|
|
}
|
|
}
|