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>
60 lines
1.9 KiB
Swift
60 lines
1.9 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import MediaKit
|
|
|
|
/// Die Detailseite eines Titels.
|
|
///
|
|
/// Holt die Metadaten frisch, statt den Eintrag aus der Liste weiterzureichen:
|
|
/// Einträge in Reihen und Rastern kommen ohne Dateiliste, ohne Besetzung und
|
|
/// ohne Marker. Für das Abspielen braucht es aber genau diese Angaben.
|
|
struct MediaDetailView: View {
|
|
let route: MediaRoute
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
@State private var state: LoadState<MediaItem> = .loading
|
|
|
|
var body: some View {
|
|
Group {
|
|
switch state {
|
|
case .loading:
|
|
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
case .failed(let message):
|
|
ContentUnavailableView {
|
|
Label("error.title", systemImage: "exclamationmark.triangle")
|
|
} description: {
|
|
Text(message)
|
|
} actions: {
|
|
Button("action.retry") { Task { await load() } }
|
|
}
|
|
|
|
case .loaded(let item):
|
|
switch item.kind {
|
|
case .show, .season:
|
|
SeriesDetailView(item: item)
|
|
default:
|
|
PlayableDetailView(item: item)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.flixyCanvas)
|
|
.navigationTitle(state.value?.title ?? route.title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
if let item = state.value {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
ItemActionsMenu(item: item) { await load() }
|
|
}
|
|
}
|
|
}
|
|
.task(id: route.ratingKey) { await load() }
|
|
}
|
|
|
|
private func load() async {
|
|
guard let server = session.backend else { return }
|
|
state = await LoadState.load {
|
|
try await server.item(id: route.ratingKey)
|
|
}
|
|
}
|
|
}
|