import Foundation import MediaKit /// Der Ladezustand eines Bildschirms. /// /// Ein gemeinsamer Typ statt dreier Einzelfelder (`isLoading`, `items`, /// `errorMessage`): die lassen sich widersprüchlich besetzen — lädt noch und /// hat gleichzeitig einen Fehler — und jede View müsste selbst entscheiden, /// was dann gilt. enum LoadState: Sendable { case loading case loaded(Value) case failed(message: String) var value: Value? { if case .loaded(let value) = self { return value } return nil } } extension LoadState { /// Führt eine Ladeoperation aus und übersetzt Fehler in Text. /// /// `keepingPrevious` ist für „Nach unten ziehen zum Aktualisieren": dabei /// soll die Liste stehen bleiben, statt für die Dauer der Anfrage durch /// einen Ladekreis ersetzt zu werden. /// /// Auf dem Hauptakteur, weil sie ausschließlich aus Views heraus gerufen /// wird. Ohne diese Festlegung müsste der übergebene Ausdruck `Sendable` /// sein — was er nie ist, weil er den Server aus der Umgebung mitnimmt. @MainActor static func load( keepingPrevious previous: Self? = nil, _ operation: () async throws -> Value ) async -> Self { do { return .loaded(try await operation()) } catch { // Der Bildschirm wurde verlassen, während geladen wurde. Ein // Fehlertext wäre hier falsch — es ist niemand mehr da, den er // interessiert. // // Gefragt wird die Aufgabe, nicht der Fehler: eine abgebrochene // Netzanfrage kommt als Übertragungsfehler zurück und nicht als // `CancellationError`. Danach zu fangen sah richtig aus und griff // nie. if Task.isCancelled { return previous ?? .loading } return .failed(message: ErrorText.describe(error)) } } } /// Übersetzt Fehler in Sätze, die man einem Menschen zeigen kann. /// /// **Eine** Stelle. Es gab davon zwei — hier und in `FlixySession` —, und sie /// waren bereits auseinandergelaufen: derselbe Fehler bekam je nach /// Bildschirm eine andere Erklärung. enum ErrorText { static func describe(_ error: any Error) -> String { // Die neutrale Form zuerst: die Ansichten sehen nur noch sie, und // jeder Server übersetzt seine eigenen Fehler schon an der Grenze // seines Moduls dorthin. if let backendError = error as? MediaBackendError { return describe(backendError) } return error.localizedDescription } static func describe(_ error: MediaBackendError) -> String { switch error { case .unauthorized: String(localized: "error.unauthorized") case .notFound: String(localized: "error.notFound") case .noReachableConnection: String(localized: "error.serverUnreachable") case .server(let status): String(localized: "error.server \(status)") case .transport(let description): description case .decoding: String(localized: "error.unexpectedResponse") // Die Worte des Servers, nicht unsere: er weiß besser, warum er // ablehnt, als wir es raten könnten. case .refused(let reason): reason } } } /// Wohin ein Tipp auf eine Kachel führt. /// /// Nur der Schlüssel, nicht der ganze Titel: die Einträge aus den Reihen der /// Startseite sind unvollständig — ohne Dateiliste, ohne Besetzung. Die /// Detailseite muss den Titel ohnehin frisch holen. struct MediaRoute: Hashable { let ratingKey: String /// Für die Titelzeile, solange noch geladen wird. let title: String } /// Der Weg zu einer Mediathek. /// /// Ein Wert statt einer Ansicht, und das ist kein Geschmack: in einem Stapel /// dürfen nicht beide Arten von Verknüpfung nebeneinander vorkommen. Sobald /// eine Ansicht über eine **Ansichts**-Verknüpfung geschoben wurde, fanden die /// **Wert**-Verknüpfungen darin ihr Ziel nicht mehr — jedes Antippen einer /// Kachel führte ins Leere, ohne Fehlermeldung. struct LibraryRoute: Hashable { let id: String } /// Der Weg zum Programmführer. struct LiveTVRoute: Hashable {}