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:
co-authored by
Claude Opus 5
parent
effdcd4532
commit
fe2d730c00
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user