Die Schwelle aus den Einstellungen wirkt serverseitig, also sofort überall: `/home/continue` liest die gespeicherte Spalte. Eine Stelle blieb trotzdem widersprüchlich — die **Beschriftung**. Web und Apps entschieden „Weiterschauen" gegen „Abspielen" selbst, allein daran, ob eine Stelle über null steht. Nach einer Sekunde stand der Titel damit nicht mehr in der Reihe, hiess aber weiter „Weiterschauen", trug einen Fortschrittsbalken unter der Kachel und bot „Aus Weiterschauen entfernen" an. Der Server hatte diese Frage längst beantwortet. `ItemStateResponse` bringt sie jetzt mit; `MediaItem.isResumable` und `.resumeProgress` sind die einzigen Stellen, die sie noch stellen. Im Simulator nachgeprüft: nach einer Sekunde heisst der Knopf „Abspielen", „Von vorn" fehlt, und der Balken unter der Kachel ist weg. Kennt ein Server das Feld nicht — Jellyfin, oder ein älterer Flaro —, gilt wie früher die blosse Stelle. **Und das Profilbild steht in den Apps.** `FlaroAccount` führt es mit, `/auth/me` bringt es bei jedem Start frisch, und der Buchstabenkreis tritt zurück, sobald eines da ist. Jellyfin bleibt beim Buchstaben — dort gibt es kein Profilbild, und das ist keine Lücke, sondern der Stand der Dinge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.2 KiB
Swift
104 lines
4.2 KiB
Swift
import SwiftUI
|
|
import MediaKit
|
|
|
|
/// Die Einträge, die am Server etwas ändern — ohne Hülle darum.
|
|
///
|
|
/// **Gesehen, Favorit, Sterne gehören nicht nur auf die Detailseite.** Wer in
|
|
/// einer Reihe steht und weiss, dass er die Folge schon kennt, will sie dort
|
|
/// abhaken und nicht erst eine Seite tiefer gehen. Dieselben Einträge, zwei
|
|
/// Orte — und deshalb an einer Stelle beschrieben.
|
|
///
|
|
/// **Warum sie keinen eigenen Zustand tragen.** Der Inhalt eines
|
|
/// Kontextmenüs lebt nur, solange das Menü offen ist: SwiftUI baut ihn beim
|
|
/// Öffnen und wirft ihn beim Schliessen weg. Ein `@State` darin könnte also
|
|
/// nichts anzeigen, was **nach** dem Tippen passiert — weder Kreisel noch
|
|
/// Fehlermeldung, denn da ist das Menü längst zu. Ausgeführt wird deshalb vom
|
|
/// Aufrufer, der länger lebt als das Menü.
|
|
///
|
|
/// Aus demselben Grund kommen `capabilities` als Wert herein, statt aus der
|
|
/// Umgebung gelesen zu werden: was in ein Kontextmenü durchgereicht wird, ist
|
|
/// von SwiftUI-Fassung zu SwiftUI-Fassung verschieden — ein Wert, den der
|
|
/// Aufrufer übergibt, ist es nicht.
|
|
struct ItemActionEntries: View {
|
|
let item: MediaItem
|
|
let capabilities: Set<BackendCapability>
|
|
/// Führt die Serveraktion aus und räumt hinterher auf.
|
|
let run: (@escaping (any MediaBackend) async throws -> Void) -> Void
|
|
|
|
var body: some View {
|
|
Button {
|
|
run { backend in
|
|
if item.isWatched {
|
|
try await backend.markUnwatched(id: item.id)
|
|
} else {
|
|
try await backend.markWatched(id: item.id)
|
|
}
|
|
}
|
|
} label: {
|
|
Label(
|
|
item.isWatched ? "action.markUnwatched" : "action.markWatched",
|
|
systemImage: item.isWatched ? "eye.slash" : "checkmark.circle"
|
|
)
|
|
}
|
|
|
|
// **Nur, wenn es etwas zu entfernen gibt.** Ohne gemerkte Stelle
|
|
// stünde hier ein Eintrag, der nichts tut — und der Nutzer suchte den
|
|
// Fehler bei sich.
|
|
if item.isResumable {
|
|
Button {
|
|
run { try await $0.removeFromContinueWatching(item) }
|
|
} label: {
|
|
Label("action.removeFromContinue", systemImage: "minus.circle")
|
|
}
|
|
}
|
|
|
|
if capabilities.contains(.favourites) {
|
|
Button {
|
|
run { try await $0.setFavourite(!item.isFavourite, id: item.id) }
|
|
} label: {
|
|
Label(
|
|
item.isFavourite ? "action.removeFavourite" : "action.addFavourite",
|
|
systemImage: item.isFavourite ? "heart.slash" : "heart"
|
|
)
|
|
}
|
|
}
|
|
|
|
if capabilities.contains(.ratings) {
|
|
Menu {
|
|
ForEach((1...5).reversed(), id: \.self) { stars in
|
|
Button {
|
|
// In Zehnteln: fünf Sterne sind eine 10.
|
|
run { try await $0.rate(id: item.id, rating: stars * 2) }
|
|
} label: {
|
|
Label(String(repeating: "★", count: stars), systemImage: "star")
|
|
}
|
|
}
|
|
if item.rating != nil {
|
|
Divider()
|
|
Button(role: .destructive) {
|
|
run { try await $0.rate(id: item.id, rating: 0) }
|
|
} label: {
|
|
Label("action.clearRating", systemImage: "star.slash")
|
|
}
|
|
}
|
|
} label: {
|
|
Label("action.rate", systemImage: "star")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
/// Was zu tun ist, wenn ein Titel am Server verändert wurde.
|
|
///
|
|
/// **Über die Umgebung, weil der Weg sonst lang ist.** Eine Kachel steckt
|
|
/// in einer Reihe, die in der Startseite steckt — und nur die Startseite
|
|
/// weiss, wie man ihre Reihen neu lädt. Diesen Rückweg durch jede
|
|
/// Zwischenebene zu reichen hiesse, drei Ansichten um ein Argument zu
|
|
/// erweitern, das sie nur weitergeben.
|
|
///
|
|
/// Die Voreinstellung tut nichts: wo niemand etwas hinterlegt, gibt es
|
|
/// auch nichts neu zu laden.
|
|
@Entry var flaroItemChanged: () async -> Void = {}
|
|
}
|