Files
Flaro/Flixy/Views/Detail/ItemActionsMenu.swift
T
ScarriffleandClaude Opus 5 3f680c7426 Gesehen und Favorit direkt an der Kachel — und die Pinch-Geste greift wieder
Drei Dinge aus dem Gebrauch.

**Das Kontextmenü einer Kachel trug genau einen Eintrag.** „Abspielen",
sonst nichts; der Kommentar daneben nannte es seit Längerem den
natürlichen Ort für „Als gesehen markieren". Jetzt steht es dort —
zusammen mit Favorit und Sternen, soweit der Server sie kennt. Es sind
dieselben Einträge wie im Menü der Detailseite: `ItemActionEntries` wird
von beiden benutzt, statt sie zweimal zu beschreiben.

Zwei Dinge daran sind nicht offensichtlich. Der Inhalt eines
Kontextmenüs lebt nur, solange das Menü offen ist — ein `@State` darin
könnte nichts anzeigen, was nach dem Tippen passiert. Ausgeführt wird
deshalb von der Kachel, die länger lebt, und dort hängt auch die
Fehlermeldung. Und danach muss die Seite neu laden: eine abgehakte Folge
gehört nicht mehr in „Weiterschauen", und der Fortschrittsbalken stimmte
sonst nicht mehr. Dafür gibt es `flixyItemChanged` in der Umgebung —
sonst müsste der Rückweg durch jede Zwischenebene gereicht werden.

**Die Pinch-Geste griff fast nie.** Sie hing an der Bildfläche, und über
der liegen die Sprungflächen — und sobald man einmal getippt hat, die
ganze Bedienung. Eine Zwei-Finger-Geste bildet sich aber nur, wenn
**beide** Finger auf der Ansicht landen, die sie trägt; in dem schmalen
Streifen dazwischen gelang das kaum. Sie hängt jetzt als
`simultaneousGesture` am ganzen Player: sie erkennt mit, statt
dazwischenzugehen, und Tippen, Doppeltippen und Scrubber bleiben
unberührt.

Gemessen im Simulator, Querformat auf einem Pro Max, mit einer echten
Zwei-Finger-Geste:

    auseinander   956 × 440 pt, Rand 0,0 / 0,0
    zusammen      782 × 440 pt, Rand 87,0 / 87,0

**Der Drehknopf zeigte ein Telefon.** Das las sich als Zustandsanzeige —
„so liegt es" — statt als Knopf. Zwei Pfeile im Kreis sagen, was
passiert.

iOS und tvOS bauen, 463 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 08:19:19 +02:00

72 lines
2.5 KiB
Swift

import SwiftUI
import FlixyDesign
import MediaKit
/// 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.
///
/// Welche Einträge erscheinen, entscheidet der Server: Plex kennt Sterne, aber
/// keine Favoriten, Jellyfin Favoriten, aber keine Sterne. Ein Eintrag, der
/// nichts bewirkt, ist schlimmer als keiner.
struct ItemActionsMenu: View {
let item: MediaItem
/// 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?
private var capabilities: Set<BackendCapability> {
session.backend?.capabilities ?? []
}
var body: some View {
Menu {
// Dieselben Einträge wie im Kontextmenü einer Kachel — siehe
// `ItemActionEntries`. Hier mit Kreisel und Fehlermeldung, weil
// diese Ansicht lange genug lebt, um beides zu zeigen.
ItemActionEntries(item: item, capabilities: capabilities, run: run)
} 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 (any MediaBackend) async throws -> Void) {
guard let backend = session.backend else { return }
isWorking = true
Task {
defer { isWorking = false }
do {
try await action(backend)
await onChange()
} catch {
errorMessage = ErrorText.describe(error)
}
}
}
}