diff --git a/Flixy/Views/Home/MediaRow.swift b/Flixy/Views/Home/MediaRow.swift index b3d66da..2d5de55 100644 --- a/Flixy/Views/Home/MediaRow.swift +++ b/Flixy/Views/Home/MediaRow.swift @@ -10,7 +10,10 @@ struct MediaRow: View { /// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt, /// 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 { VStack(alignment: .leading, spacing: FlixySpacing.regular) { @@ -23,17 +26,7 @@ struct MediaRow: View { LazyHStack(spacing: FlixySpacing.regular) { ForEach(hub.items) { item in NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) { - LandscapeCard( - title: item.rowTitle, - subtitle: item.cardSubtitle, - artworkURL: session.server?.artworkURL( - path: item.rowArtworkPath, - width: 440, - height: 248 - ), - progress: item.watchProgress - ) - .frame(width: cardWidth) + card(for: item) } .buttonStyle(.plain) } @@ -47,6 +40,40 @@ struct MediaRow: View { .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 { @@ -70,4 +97,16 @@ extension PlexItem { 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 + } + } } diff --git a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLibrary.swift b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLibrary.swift index 53f6d1a..6b5965e 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLibrary.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLibrary.swift @@ -67,6 +67,22 @@ public struct PlexHub: Sendable, Equatable, Decodable, Identifiable { 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 { case hubIdentifier, title, type, key, size, more case items = "Metadata" diff --git a/Packages/FlixyKit/Tests/PlexKitTests/HubShapeTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/HubShapeTests.swift new file mode 100644 index 0000000..939409d --- /dev/null +++ b/Packages/FlixyKit/Tests/PlexKitTests/HubShapeTests.swift @@ -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) + } +}