Vier geratene Adressen, vier 404. Statt ein fünftes Mal zu raten, klopft die App jetzt in einem Durchgang und gleichzeitig ein Dutzend Adressen ab: was /media/providers an Endpunkten angibt, welche Unterverzeichnisse Programmanbieter und Empfänger in ihrer eigenen Antwort führen, die bekannten Formen anderer Serverfassungen, die Senderliste des Tuners selbst — und zuletzt das Programmraster. Dabei zwei Fehlerquellen mitbehoben, die jede einzeln wie „keine Sender" ausgesehen hätten: der Sendername steht je nach Tunertyp in title, name, channelTitle oder callSign, und eine Antwort mit dreißig namenlosen Einträgen sieht aus wie eine Senderliste, ist aber keine. Bleibt es leer, steht jetzt jede abgeklopfte Adresse mit ihrer Antwort in der Ansicht. Das beantwortet „liegt es am Server oder an der App" ohne eine weitere Runde Rückfragen. Dazu drei Sachen bei Serien: - Groß steht, worum es geht — bei einer Folge ihr Titel, bei einer Staffel der Name der Serie. „Staffel 10" als Überschrift sagte nicht, welche Serie gemeint ist. - Die Einordnung steht darunter: Serienname bei Folgen, Staffelnummer bei Staffeln. - Von der Folge führt ein Weg zur Staffel und zur Serie. Wer über „Weiterschauen" bei einer Folge landete, saß dort bisher fest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.3 KiB
Swift
121 lines
4.3 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import PlexKit
|
|
|
|
/// Eine waagerechte Reihe von Titeln.
|
|
struct MediaRow: View {
|
|
let hub: PlexHub
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
|
|
/// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt,
|
|
/// dass es weitergeht, ohne einen Pfeil dafür zu brauchen.
|
|
private let landscapeWidth: CGFloat = 220
|
|
/// Schmaler, weil hochkant: bei gleicher Höhe passen so mehr Plakate
|
|
/// nebeneinander, und mehr Auswahl auf einen Blick ist genau der Zweck.
|
|
private let posterWidth: CGFloat = 116
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: FlixySpacing.regular) {
|
|
Text(hub.title)
|
|
.font(.flixyRowHeader)
|
|
.foregroundStyle(Color.flixyTextPrimary)
|
|
.padding(.horizontal, FlixySpacing.screenMargin)
|
|
|
|
ScrollView(.horizontal) {
|
|
LazyHStack(spacing: FlixySpacing.regular) {
|
|
ForEach(hub.items) { item in
|
|
NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) {
|
|
card(for: item)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, FlixySpacing.screenMargin)
|
|
// Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe
|
|
// bleibt an beliebiger Stelle stehen.
|
|
.scrollTargetLayout()
|
|
}
|
|
.scrollTargetBehavior(.viewAligned)
|
|
.scrollIndicators(.hidden)
|
|
}
|
|
}
|
|
|
|
/// 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: PlexItem) -> some View {
|
|
if hub.isContinueWatching {
|
|
LandscapeCard(
|
|
title: item.rowTitle,
|
|
subtitle: item.cardSubtitle,
|
|
artworkURL: session.server?.artworkURL(
|
|
path: item.rowArtworkPath,
|
|
width: 440,
|
|
height: 248
|
|
),
|
|
progress: item.watchProgress
|
|
)
|
|
.frame(width: landscapeWidth)
|
|
} else {
|
|
PosterCard(
|
|
title: item.rowTitle,
|
|
subtitle: item.cardSubtitle,
|
|
artworkURL: session.server?.artworkURL(
|
|
path: item.posterArtworkPath,
|
|
width: 300,
|
|
height: 450
|
|
),
|
|
progress: item.watchProgress
|
|
)
|
|
.frame(width: posterWidth)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PlexItem {
|
|
/// Der Titel auf einer Kachel in einer Reihe.
|
|
///
|
|
/// Groß steht, worum es geht: bei einer Folge ihr eigener Titel, bei
|
|
/// einer Staffel der Name der Serie. Die Einordnung — Serie, Staffel —
|
|
/// gehört darunter. „Staffel 10" allein als Überschrift sagt nichts
|
|
/// darüber, welche Serie gemeint ist.
|
|
var rowTitle: String {
|
|
switch kind {
|
|
case .episode: title
|
|
// Die Serie steht je nach Antwort mal unter `parentTitle`, mal unter
|
|
// `grandparentTitle`. Fehlt beides, bleibt „Staffel 2" — besser als
|
|
// gar keine Beschriftung.
|
|
case .season: parentTitle ?? grandparentTitle ?? title
|
|
default: title
|
|
}
|
|
}
|
|
|
|
/// Welches Bild auf die Querformat-Kachel gehört.
|
|
///
|
|
/// Bei Folgen ist `thumb` das Szenenbild der Folge — genau richtig. Bei
|
|
/// Filmen ist `thumb` das Hochkant-Plakat, das im Querformat beschnitten
|
|
/// würde; dort ist `art` (das Hintergrundbild) die bessere Wahl.
|
|
var rowArtworkPath: String? {
|
|
switch kind {
|
|
case .episode: thumb ?? grandparentThumb
|
|
default: art ?? thumb
|
|
}
|
|
}
|
|
|
|
/// Welches Bild auf die Hochkant-Kachel gehört.
|
|
///
|
|
/// Bei Folgen das Plakat der Serie statt des Szenenbilds: ein 16:9-Bild
|
|
/// hochkant beschnitten zeigt einen zufälligen Ausschnitt und ist zum
|
|
/// Wiedererkennen wertlos.
|
|
var posterArtworkPath: String? {
|
|
switch kind {
|
|
case .episode: grandparentThumb ?? parentThumb ?? thumb
|
|
default: thumb
|
|
}
|
|
}
|
|
}
|