Download-Regeln - Automatisches Löschen nach dem Ansehen, mit Frist statt sofort: wer die letzte Szene noch einmal sucht, soll sie nicht schon verloren haben. „Nie" ist die Voreinstellung. - „Halte N ungesehene Folgen offline": wartende und laufende Downloads zählen mit, sonst würde bei jedem Durchlauf nachgelegt, bis die ganze Serie auf dem Gerät liegt. - Beides als reine Rechnung ohne Zugriff auf Datenbank oder Dateien — bei einer Regel, die Filme löscht, ist Prüfbarkeit keine Formsache. 13 Tests. Qualitätswahl beim Download - transcodedDownloadURL über den Universal-Transcoder mit protocol=http statt über die umständliche Sync-API. NOCH NICHT AM SERVER GEPRÜFT: offen ist, ob Plex eine Content-Length mitschickt (sonst kein Fortschritt und kein Fortsetzen) und ob die Datei durchsuchbar ist. Steht so im Quelltext; Original bleibt Voreinstellung. Erscheinungsbild - Akzentfarbe: zehn kuratierte Kacheln plus freie Hex-Eingabe. Bei einer eigenen Farbe greift die Kontrastkorrektur und sagt es auch — sonst wirkt sie wie ein Fehler. - Vorschaufläche mit Knopf, Symbol und Fortschrittsbalken. Serveraktionen - Gesehen-Status umschalten und Sterne setzen über ein Menü auf der Detailseite. - Suchlauf der Mediathek aus der Werkzeugleiste. 115 Tests. 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 PlexKit
|
|
|
|
/// 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<PlexItem> = .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.server else { return }
|
|
state = await LoadState.load {
|
|
try await server.metadata(ratingKey: route.ratingKey)
|
|
}
|
|
}
|
|
}
|