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
+14 -14
View File
@@ -1,24 +1,24 @@
import SwiftUI
import FlixyDesign
import PlexKit
import MediaKit
/// Filter und Sortierung einer Mediathek.
struct LibraryFilterSheet: View {
@Binding var query: LibraryQuery
let libraryKind: PlexLibrary.Kind?
@Binding var query: MediaQuery
let libraryKind: MediaLibrary.Kind?
@Environment(\.dismiss) private var dismiss
@Environment(\.flixyAccent) private var accent
@Environment(FlixySession.self) private var session
@State private var genres: [PlexFilterOption] = []
@State private var genres: [MediaFilterOption] = []
/// Ein Arbeitsstand, der erst beim Bestätigen übernommen wird.
///
/// Sonst löste jeder Tipp im Blatt sofort eine neue Serverabfrage aus —
/// bei drei Änderungen hintereinander drei verworfene Ladevorgänge.
@State private var draft: LibraryQuery
@State private var draft: MediaQuery
init(query: Binding<LibraryQuery>, libraryKind: PlexLibrary.Kind?) {
init(query: Binding<MediaQuery>, libraryKind: MediaLibrary.Kind?) {
self._query = query
self.libraryKind = libraryKind
self._draft = State(initialValue: query.wrappedValue)
@@ -29,7 +29,7 @@ struct LibraryFilterSheet: View {
Form {
Section("filter.sort") {
Picker("filter.sort", selection: $draft.sort) {
ForEach(LibraryQuery.Sort.allCases, id: \.self) { sort in
ForEach(MediaQuery.Sort.allCases, id: \.self) { sort in
Text(sort.label).tag(sort)
}
}
@@ -43,7 +43,7 @@ struct LibraryFilterSheet: View {
Picker("filter.genre", selection: $draft.genre) {
Text("filter.any").tag(String?.none)
ForEach(genres) { genre in
Text(genre.title).tag(String?.some(genre.key))
Text(genre.title).tag(String?.some(genre.id))
}
}
@@ -58,7 +58,7 @@ struct LibraryFilterSheet: View {
Section {
Button("filter.reset", role: .destructive) {
draft = LibraryQuery(sort: draft.sort)
draft = MediaQuery(sort: draft.sort)
}
.disabled(!draft.isFiltering)
}
@@ -91,14 +91,14 @@ struct LibraryFilterSheet: View {
}
private func loadGenres() async {
guard let server = session.server,
let library = session.libraries.first(where: { $0.kind == libraryKind })
guard let server = session.backend,
let library = session.mediaLibraries.first(where: { $0.kind == libraryKind })
else { return }
genres = (try? await server.filterOptions(inLibrary: library, field: "genre")) ?? []
genres = (try? await server.filterOptions(inLibrary: library, field: .genre)) ?? []
}
}
extension LibraryQuery.Sort {
extension MediaQuery.Sort {
var label: LocalizedStringKey {
switch self {
case .titleAscending: "sort.titleAscending"
@@ -106,7 +106,7 @@ extension LibraryQuery.Sort {
case .recentlyAdded: "sort.recentlyAdded"
case .releaseDate: "sort.releaseDate"
case .rating: "sort.rating"
case .lastViewed: "sort.lastViewed"
case .lastPlayed: "sort.lastPlayed"
}
}
}
+23 -22
View File
@@ -1,18 +1,18 @@
import SwiftUI
import FlixyDesign
import PlexKit
import MediaKit
/// Eine Mediathek zum Durchblättern.
struct LibraryView: View {
let library: PlexLibrary
let library: MediaLibrary
@Environment(FlixySession.self) private var session
@AppStorage("flixy.library.layout") private var layout: LibraryLayout = .grid
@State private var items: [PlexItem] = []
@State private var items: [MediaItem] = []
@State private var state: LoadState<Void> = .loading
@State private var query = LibraryQuery()
@State private var page: PlexPage<PlexItem>?
@State private var query = MediaQuery()
@State private var page: MediaPage?
@State private var isLoadingMore = false
@State private var showsFilters = false
@State private var isScanning = false
@@ -41,7 +41,7 @@ struct LibraryView: View {
} actions: {
if query.isFiltering {
Button("library.clearFilters") {
query = LibraryQuery(sort: query.sort)
query = MediaQuery(sort: query.sort)
}
}
}
@@ -58,7 +58,7 @@ struct LibraryView: View {
LibraryFilterSheet(query: $query, libraryKind: library.kind)
}
.navigationDestination(for: MediaRoute.self) { MediaDetailView(route: $0) }
.task(id: library.key) { await reload() }
.task(id: library.id) { await reload() }
// Jede Änderung an Filter oder Sortierung verwirft die bisherige
// Liste — Nachladen mit altem Versatz würde sonst Einträge aus zwei
// verschiedenen Abfragen vermischen.
@@ -84,11 +84,11 @@ struct LibraryView: View {
spacing: FlixySpacing.loose
) {
ForEach(items) { item in
NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) {
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
PosterCard(
title: item.title,
subtitle: item.cardSubtitle,
artworkURL: session.server?.artworkURL(path: item.thumb, width: 300, height: 450),
artworkURL: session.backend?.artworkURL(path: item.thumb, width: 300, height: 450),
progress: item.watchProgress
)
}
@@ -106,7 +106,7 @@ struct LibraryView: View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(items) { item in
NavigationLink(value: MediaRoute(ratingKey: item.ratingKey, title: item.title)) {
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
LibraryListRow(item: item)
}
.buttonStyle(.plain)
@@ -164,7 +164,7 @@ struct LibraryView: View {
/// nach kurzer Wartezeit neu geholt. Wer gerade Dateien abgelegt hat,
/// sieht sie damit meistens schon.
private func scan() async {
guard let server = session.server, !isScanning else { return }
guard let server = session.backend, !isScanning else { return }
isScanning = true
defer { isScanning = false }
@@ -176,30 +176,30 @@ struct LibraryView: View {
// MARK: - Laden
private func reload() async {
guard let server = session.server else { return }
guard let server = session.backend else { return }
state = .loading
do {
let first = try await server.items(inLibrary: library, query: query)
let first = try await server.items(inLibrary: library, query: query, offset: 0, limit: 60)
items = first.items
page = first
state = .loaded(())
} catch is CancellationError {
return
} catch {
state = .failed(message: PlexErrorText.describe(error))
state = .failed(message: ErrorText.describe(error))
}
}
/// Lädt nach, sobald die Ansicht sich dem Ende der Liste nähert.
private func loadMoreIfNeeded(reaching item: PlexItem) async {
guard let server = session.server,
private func loadMoreIfNeeded(reaching item: MediaItem) async {
guard let server = session.backend,
let page, page.hasMore,
!isLoadingMore,
// Fünf Einträge vor Schluss: früh genug, dass der Nachschub da
// ist, bevor jemand am Ende ankommt, und spät genug, dass nicht
// jedes Scrollen eine Anfrage auslöst.
items.suffix(5).contains(where: { $0.ratingKey == item.ratingKey })
items.suffix(5).contains(where: { $0.id == item.id })
else { return }
isLoadingMore = true
@@ -209,13 +209,14 @@ struct LibraryView: View {
let next = try await server.items(
inLibrary: library,
query: query,
offset: page.nextOffset
offset: page.nextOffset,
limit: 60
)
// Plex kann bei gleichzeitigem Serverscan denselben Titel auf zwei
// Seiten liefern. Ohne diese Prüfung stünde er doppelt im Raster —
// und SwiftUI beschwert sich über doppelte Kennungen.
let known = Set(items.map(\.ratingKey))
items.append(contentsOf: next.items.filter { !known.contains($0.ratingKey) })
let known = Set(items.map(\.id))
items.append(contentsOf: next.items.filter { !known.contains($0.id) })
self.page = next
} catch {
// Stillschweigend: die Liste steht bereits, ein Fehlerblatt mitten
@@ -231,13 +232,13 @@ enum LibraryLayout: String, CaseIterable {
/// Eine Zeile in der Listenansicht.
private struct LibraryListRow: View {
let item: PlexItem
let item: MediaItem
@Environment(FlixySession.self) private var session
var body: some View {
HStack(spacing: FlixySpacing.regular) {
FlixyArtwork(
url: session.server?.artworkURL(path: item.thumb, width: 120, height: 180),
url: session.backend?.artworkURL(path: item.thumb, width: 120, height: 180),
aspectRatio: PosterCard.aspectRatio
)
.frame(width: 44)