Files
Flaro/Flixy/Views/Detail/MediaDetailView.swift
T
GuidoandClaude Opus 5 af23372997 Phase 4 und Teile von Phase 5
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>
2026-09-11 17:40:10 +02:00

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)
}
}
}