import Foundation import PlexKit /// 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 is CancellationError { // Der Bildschirm wurde verlassen, während geladen wurde. Ein // Fehlertext wäre hier falsch — es ist niemand mehr da, den er // interessiert. return previous ?? .loading } catch { return .failed(message: PlexErrorText.describe(error)) } } } /// Übersetzt Fehler in Sätze, die man einem Menschen zeigen kann. enum PlexErrorText { static func describe(_ error: any Error) -> String { guard let plexError = error as? PlexError else { return error.localizedDescription } return switch plexError { 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") } } } /// 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 }