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>
82 lines
3.0 KiB
Swift
82 lines
3.0 KiB
Swift
import SwiftUI
|
|
import FlaroDesign
|
|
import MediaKit
|
|
|
|
/// Eine waagerechte Reihe von Titeln.
|
|
struct MediaRow: View {
|
|
let row: MediaRowGroup
|
|
/// Wird durchgereicht — siehe `QuickPlayTile`.
|
|
var opensDetails: Bool = false
|
|
|
|
@Environment(FlaroSession.self) private var session
|
|
|
|
@Environment(HomeLayoutSettings.self) private var layout
|
|
|
|
/// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt,
|
|
/// dass es weitergeht, ohne einen Pfeil dafür zu brauchen.
|
|
private var landscapeWidth: CGFloat { 220 * layout.cardScale }
|
|
/// Schmaler, weil hochkant: bei gleicher Höhe passen so mehr Plakate
|
|
/// nebeneinander, und mehr Auswahl auf einen Blick ist genau der Zweck.
|
|
///
|
|
/// Beide mit demselben Faktor: ihr Verhältnis zueinander soll stimmen,
|
|
/// egal wie groß es insgesamt eingestellt ist.
|
|
private var posterWidth: CGFloat { 116 * layout.cardScale }
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
|
Text(row.title)
|
|
.font(.flaroRowHeader)
|
|
.foregroundStyle(Color.flaroTextPrimary)
|
|
.padding(.horizontal, FlaroSpacing.screenMargin)
|
|
|
|
ScrollView(.horizontal) {
|
|
LazyHStack(spacing: FlaroSpacing.regular) {
|
|
ForEach(row.items) { item in
|
|
QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) }
|
|
}
|
|
}
|
|
.padding(.horizontal, FlaroSpacing.screenMargin)
|
|
// Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe
|
|
// bleibt an beliebiger Stelle stehen.
|
|
.scrollTargetLayout()
|
|
}
|
|
.scrollTargetBehavior(.viewAligned)
|
|
.scrollIndicators(.never)
|
|
}
|
|
}
|
|
|
|
/// Szenenbild beim Weiterschauen, sonst das Plakat.
|
|
///
|
|
/// Angefangenes erkennt man an der Szene, an der man aufgehört hat —
|
|
/// dafür ist das Querformat richtig. Wer dagegen aussucht, was er als
|
|
/// Nächstes sehen will, sucht nach Plakaten.
|
|
@ViewBuilder
|
|
private func card(for item: MediaItem) -> some View {
|
|
if row.isContinueWatching {
|
|
LandscapeCard(
|
|
title: item.rowTitle,
|
|
subtitle: item.cardSubtitle,
|
|
artworkURL: session.backend?.artworkURL(
|
|
path: item.rowArtworkPath,
|
|
width: 440,
|
|
height: 248
|
|
),
|
|
progress: item.resumeProgress
|
|
)
|
|
.frame(width: landscapeWidth)
|
|
} else {
|
|
PosterCard(
|
|
title: item.rowTitle,
|
|
subtitle: item.cardSubtitle,
|
|
artworkURL: session.backend?.artworkURL(
|
|
path: item.posterArtworkPath,
|
|
width: 300,
|
|
height: 450
|
|
),
|
|
progress: item.resumeProgress
|
|
)
|
|
.frame(width: posterWidth)
|
|
}
|
|
}
|
|
}
|