Plakate hochkant, Weiterschauen im Querformat

Alle Reihen zeigten Querformat-Kacheln. Für „Weiterschauen" ist das
richtig — man erkennt die Szene wieder, an der man aufgehört hat. Beim
Aussuchen sucht man dagegen nach Plakaten, und die sind hochkant.

Die Unterscheidung hängt an der Hub-Kennung, nicht am Titel: der kommt
in der Sprache des Servers, und die Startseite sähe sonst auf Englisch
anders aus als auf Deutsch, ohne dass jemand auf die Ursache käme.
`ondeck` zählt mit — ältere Serverfassungen nennen es so.

Bei Folgen in Hochkant-Reihen wird das Serienplakat genommen, nicht das
Szenenbild: ein 16:9-Bild hochkant beschnitten zeigt einen zufälligen
Ausschnitt und ist zum Wiedererkennen wertlos.

Plakate sind schmaler als Szenenbilder, damit bei gleicher Höhe mehr
nebeneinander passen — mehr Auswahl auf einen Blick ist der Zweck.

Vier Tests, darunter einer, der festhält, dass der übersetzte Titel die
Form nicht bestimmen darf.

149 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 22:44:13 +02:00
co-authored by Claude Opus 5
parent eea81aad2f
commit 53dacc9f1d
3 changed files with 114 additions and 12 deletions
+51 -12
View File
@@ -10,7 +10,10 @@ struct MediaRow: View {
/// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt, /// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt,
/// dass es weitergeht, ohne einen Pfeil dafür zu brauchen. /// dass es weitergeht, ohne einen Pfeil dafür zu brauchen.
private let cardWidth: CGFloat = 220 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 { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: FlixySpacing.regular) {
@@ -23,17 +26,7 @@ struct MediaRow: View {
LazyHStack(spacing: FlixySpacing.regular) { LazyHStack(spacing: FlixySpacing.regular) {
ForEach(hub.items) { item in ForEach(hub.items) { item in
NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) { NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) {
LandscapeCard( card(for: item)
title: item.rowTitle,
subtitle: item.cardSubtitle,
artworkURL: session.server?.artworkURL(
path: item.rowArtworkPath,
width: 440,
height: 248
),
progress: item.watchProgress
)
.frame(width: cardWidth)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@@ -47,6 +40,40 @@ struct MediaRow: View {
.scrollIndicators(.hidden) .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 { extension PlexItem {
@@ -70,4 +97,16 @@ extension PlexItem {
default: art ?? thumb 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
}
}
} }
@@ -67,6 +67,22 @@ public struct PlexHub: Sendable, Equatable, Decodable, Identifiable {
public var id: String { hubIdentifier } public var id: String { hubIdentifier }
/// Ob diese Reihe Angefangenes zeigt.
///
/// Entscheidet über die Form der Kacheln: Weiterschauen lebt vom
/// Szenenbild — man erkennt die Stelle wieder, an der man aufgehört hat.
/// Überall sonst ist das Plakat richtig, denn danach sucht man einen Film
/// aus.
///
/// Geprüft wird die Kennung, nicht der Titel: der ist übersetzt und hieße
/// auf Deutsch „Weiterschauen", auf Englisch „Continue Watching".
public var isContinueWatching: Bool {
let identifier = hubIdentifier.lowercased()
// `ondeck` ist die ältere Bezeichnung; Server unterschiedlicher
// Fassungen liefern die eine oder die andere.
return identifier.contains("continue") || identifier.contains("ondeck")
}
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case hubIdentifier, title, type, key, size, more case hubIdentifier, title, type, key, size, more
case items = "Metadata" case items = "Metadata"
@@ -0,0 +1,47 @@
import Foundation
import Testing
@testable import PlexKit
/// Welche Reihe welche Kachelform bekommt.
///
/// Weiterschauen lebt vom Szenenbild — man erkennt die Stelle wieder, an der
/// man aufgehört hat. Überall sonst ist das Plakat richtig, denn danach sucht
/// man einen Film aus.
@Suite("Form der Reihen")
struct HubShapeTests {
@Test(
"Angefangenes wird als Szenenbild gezeigt",
arguments: ["home.continue", "continueWatching", "tv.ondeck", "home.ondeck"]
)
func continueWatchingUsesLandscape(identifier: String) {
#expect(PlexHub(hubIdentifier: identifier, title: "Weiterschauen").isContinueWatching)
}
@Test(
"Alles andere bekommt Plakate",
arguments: ["home.movies.recent", "movie.recentlyadded", "home.watchlist",
"movie.collection", "tv.recentlyaired"]
)
func everythingElseUsesPosters(identifier: String) {
#expect(!PlexHub(hubIdentifier: identifier, title: "Irgendeine Reihe").isContinueWatching)
}
@Test("Die Entscheidung hängt nicht am übersetzten Titel")
func titleDoesNotDecide() {
// Der Titel kommt in der Sprache des Servers. Würde er die Form
// bestimmen, sähe die Startseite auf Englisch anders aus als auf
// Deutsch — und niemand käme auf die Ursache.
let english = PlexHub(hubIdentifier: "home.continue", title: "Continue Watching")
let german = PlexHub(hubIdentifier: "home.continue", title: "Weiterschauen")
#expect(english.isContinueWatching == german.isContinueWatching)
let misleading = PlexHub(hubIdentifier: "home.movies.recent", title: "Weiterschauen")
#expect(!misleading.isContinueWatching)
}
@Test("Groß- und Kleinschreibung spielt keine Rolle")
func caseInsensitive() {
#expect(PlexHub(hubIdentifier: "Home.ContinueWatching", title: "x").isContinueWatching)
}
}