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>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import SwiftUI
|
||||
import FlixyDesign
|
||||
import PlexKit
|
||||
|
||||
/// Das Aktionsmenü auf einer Detailseite.
|
||||
///
|
||||
/// Alles, was den Server verändert, liegt hier zusammen — statt verstreut als
|
||||
/// einzelne Knöpfe. Das hält die Kopfzeile frei für das, was man ständig
|
||||
/// braucht: Abspielen und Herunterladen.
|
||||
struct ItemActionsMenu: View {
|
||||
let item: PlexItem
|
||||
/// Wird gerufen, wenn sich am Server etwas geändert hat und die
|
||||
/// Detailseite neu geladen werden sollte.
|
||||
let onChange: () async -> Void
|
||||
|
||||
@Environment(FlixySession.self) private var session
|
||||
@State private var isWorking = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button {
|
||||
run { server in
|
||||
if item.isWatched {
|
||||
try await server.markUnwatched(ratingKey: item.ratingKey)
|
||||
} else {
|
||||
try await server.markWatched(ratingKey: item.ratingKey)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
item.isWatched ? "action.markUnwatched" : "action.markWatched",
|
||||
systemImage: item.isWatched ? "eye.slash" : "checkmark.circle"
|
||||
)
|
||||
}
|
||||
|
||||
Menu {
|
||||
ForEach((1...5).reversed(), id: \.self) { stars in
|
||||
Button {
|
||||
// Plex rechnet in Zehnteln: fünf Sterne sind eine 10.
|
||||
run { try await $0.rate(ratingKey: item.ratingKey, rating: stars * 2) }
|
||||
} label: {
|
||||
Label(String(repeating: "★", count: stars), systemImage: "star")
|
||||
}
|
||||
}
|
||||
if item.rating != nil {
|
||||
Divider()
|
||||
Button(role: .destructive) {
|
||||
run { try await $0.rate(ratingKey: item.ratingKey, rating: 0) }
|
||||
} label: {
|
||||
Label("action.clearRating", systemImage: "star.slash")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("action.rate", systemImage: "star")
|
||||
}
|
||||
} label: {
|
||||
if isWorking {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
}
|
||||
.disabled(isWorking)
|
||||
.alert(
|
||||
Text("error.title"),
|
||||
isPresented: Binding(get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })
|
||||
) {
|
||||
Button("action.close") { errorMessage = nil }
|
||||
} message: {
|
||||
if let errorMessage { Text(errorMessage) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt eine Serveraktion aus und lädt danach neu.
|
||||
///
|
||||
/// Bewusst ohne vorweggenommene Anzeige: den Haken sofort zu setzen und
|
||||
/// bei einem Fehler wieder zurückzunehmen sieht aus wie ein Wackler. Die
|
||||
/// Aktionen dauern Millisekunden.
|
||||
private func run(_ action: @escaping (PlexServer) async throws -> Void) {
|
||||
guard let server = session.server else { return }
|
||||
isWorking = true
|
||||
|
||||
Task {
|
||||
defer { isWorking = false }
|
||||
do {
|
||||
try await action(server)
|
||||
await onChange()
|
||||
} catch {
|
||||
errorMessage = PlexErrorText.describe(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user