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 /// 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.watchProgress != nil { 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 flixyItemChanged: () async -> Void = {} }