Die Ansichten sprechen nicht mehr mit Plex

Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.

Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.

`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.

Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.

Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".

Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-13 12:46:52 +02:00
co-authored by Claude Opus 5
parent effdcd4532
commit fe2d730c00
30 changed files with 432 additions and 318 deletions
+6 -6
View File
@@ -1,10 +1,10 @@
import SwiftUI
import FlixyDesign
import PlexKit
import MediaKit
/// Der Aufmacher oben auf der Startseite.
struct HeroSection: View {
let item: PlexItem
let item: MediaItem
@Environment(FlixySession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback
@@ -15,12 +15,12 @@ struct HeroSection: View {
title: item.rowTitle,
metadata: metadata,
summary: item.summary,
artworkURL: session.server?.artworkURL(
path: item.art ?? item.grandparentArt ?? item.thumb,
artworkURL: session.backend?.artworkURL(
path: item.art ?? item.seriesArt ?? item.thumb,
width: 900,
height: 1200
),
logoURL: session.server?.rawImageURL(path: item.clearLogoPath)
logoURL: session.backend?.rawImageURL(path: item.clearLogoPath)
) {
Button {
playback.play(item)
@@ -33,7 +33,7 @@ struct HeroSection: View {
.buttonStyle(.borderedProminent)
.tint(accent)
NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) {
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
Label("hero.details", systemImage: "info.circle")
.font(.flixyCardTitle)
.padding(.horizontal, FlixySpacing.loose)
+10 -10
View File
@@ -1,12 +1,12 @@
import SwiftUI
import FlixyDesign
import PlexKit
import MediaKit
/// Die Startseite: Aufmacher oben, darunter die Reihen vom Server.
struct HomeView: View {
@Environment(FlixySession.self) private var session
@Environment(HomeLayoutSettings.self) private var layout
@State private var state: LoadState<[PlexHub]> = .loading
@State private var state: LoadState<[MediaRowGroup]> = .loading
var body: some View {
Group {
@@ -23,8 +23,8 @@ struct HomeView: View {
Button("action.retry") { Task { await load() } }
}
case .loaded(let allHubs):
let hubs = layout.apply(to: allHubs)
case .loaded(let allRows):
let hubs = layout.apply(to: allRows)
if hubs.isEmpty {
ContentUnavailableView {
Label("home.empty.title", systemImage: "film.stack")
@@ -55,7 +55,7 @@ struct HomeView: View {
.refreshable { await load() }
}
private func content(hubs: [PlexHub]) -> some View {
private func content(hubs: [MediaRowGroup]) -> some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: FlixySpacing.section) {
if let featured = heroItem(in: hubs) {
@@ -63,7 +63,7 @@ struct HomeView: View {
}
ForEach(hubs) { hub in
MediaRow(hub: hub)
MediaRow(row: hub)
}
}
.padding(.bottom, FlixySpacing.section)
@@ -76,15 +76,15 @@ struct HomeView: View {
/// Erste Wahl ist etwas Angefangenes — wer die App öffnet, will meistens
/// weiterschauen. Erst wenn es nichts Angefangenes gibt, tritt der
/// Neuzugang an die Stelle.
private func heroItem(in hubs: [PlexHub]) -> PlexItem? {
let continuing = hubs.first { $0.hubIdentifier.contains("continue") }
private func heroItem(in hubs: [MediaRowGroup]) -> MediaItem? {
let continuing = hubs.first(where: \.isContinueWatching)
return continuing?.items.first ?? hubs.first?.items.first
}
private func load() async {
guard let server = session.server else { return }
guard let backend = session.backend else { return }
state = await LoadState.load(keepingPrevious: state) {
try await server.homeHubs()
try await backend.homeRows()
}
}
}
+14 -14
View File
@@ -1,10 +1,10 @@
import SwiftUI
import FlixyDesign
import PlexKit
import MediaKit
/// Eine waagerechte Reihe von Titeln.
struct MediaRow: View {
let hub: PlexHub
let row: MediaRowGroup
@Environment(FlixySession.self) private var session
@@ -17,15 +17,15 @@ struct MediaRow: View {
var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) {
Text(hub.title)
Text(row.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)) {
ForEach(row.items) { item in
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
card(for: item)
}
.buttonStyle(.plain)
@@ -47,12 +47,12 @@ struct MediaRow: View {
/// 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 {
private func card(for item: MediaItem) -> some View {
if row.isContinueWatching {
LandscapeCard(
title: item.rowTitle,
subtitle: item.cardSubtitle,
artworkURL: session.server?.artworkURL(
artworkURL: session.backend?.artworkURL(
path: item.rowArtworkPath,
width: 440,
height: 248
@@ -64,7 +64,7 @@ struct MediaRow: View {
PosterCard(
title: item.rowTitle,
subtitle: item.cardSubtitle,
artworkURL: session.server?.artworkURL(
artworkURL: session.backend?.artworkURL(
path: item.posterArtworkPath,
width: 300,
height: 450
@@ -76,7 +76,7 @@ struct MediaRow: View {
}
}
extension PlexItem {
extension MediaItem {
/// Der Titel auf einer Kachel in einer Reihe.
///
/// Groß steht, worum es geht: bei einer Folge ihr eigener Titel, bei
@@ -87,9 +87,9 @@ extension PlexItem {
switch kind {
case .episode: title
// Die Serie steht je nach Antwort mal unter `parentTitle`, mal unter
// `grandparentTitle`. Fehlt beides, bleibt „Staffel 2" — besser als
// `seriesTitle`. Fehlt beides, bleibt „Staffel 2" — besser als
// gar keine Beschriftung.
case .season: parentTitle ?? grandparentTitle ?? title
case .season: parentTitle ?? seriesTitle ?? title
default: title
}
}
@@ -101,7 +101,7 @@ extension PlexItem {
/// würde; dort ist `art` (das Hintergrundbild) die bessere Wahl.
var rowArtworkPath: String? {
switch kind {
case .episode: thumb ?? grandparentThumb
case .episode: thumb ?? seriesThumb
default: art ?? thumb
}
}
@@ -113,7 +113,7 @@ extension PlexItem {
/// Wiedererkennen wertlos.
var posterArtworkPath: String? {
switch kind {
case .episode: grandparentThumb ?? parentThumb ?? thumb
case .episode: seriesThumb ?? parentThumb ?? thumb
default: thumb
}
}