Files
Flaro/Flixy/Views/LoadState.swift
T
ScarriffleandClaude Opus 5 95723588c2 Nachgesehen — und dabei vier Fehler gefunden, zwei davon meine neuen
Der Simulator läuft wieder. Ich habe alles durchgeklickt, was ich dir in den
letzten beiden Runden ungeprüft gegeben habe. Es war nicht in Ordnung.

**Zwei Navigationsleisten übereinander.** Auf dem iPhone hat die Leiste unten
fünf Plätze; alles darüber schiebt das System in ein „Mehr"-Verzeichnis — und
dessen Einträge bekamen zwei Zurück-Pfeile, einen vom System und einen von
uns. Statt das zu flicken, gibt es jetzt die **Bibliothek**, die du ohnehin
wolltest: auf dem iPhone ein Eintrag, hinter dem alle Mediatheken stehen, auf
dem iPad weiter jede einzeln in der Seitenleiste. Damit bleibt die Leiste bei
fünf Einträgen, und das „Mehr" entfällt.

**Kacheln in der Mediathek öffneten nichts.** Kein Fehler, keine Meldung —
Antippen führte einfach ins Leere. Der Grund: in einem Stapel dürfen nicht
beide Arten von Verknüpfung nebeneinander vorkommen. Sobald ein Bildschirm
über eine **Ansichts**-Verknüpfung geschoben wurde, fanden die
**Wert**-Verknüpfungen darin ihr Ziel nicht mehr. Jetzt ist alles wertbasiert,
und das Ziel für Detailseiten steht **einmal je Stapel** an dessen Wurzel statt
in jedem Bildschirm einzeln.

**Der Lupenknopf in der Kopfzeile zeigte auf ein Ziel, das es nie gab.** Kein
Bildschirm hat je ein `SearchRoute`-Ziel angemeldet — der Knopf tat seit jeher
nichts. Er ist weg; die Suche hat ihren eigenen Platz in der Leiste.

**Der Download-Dialog zeigte Schlüssel statt Namen** — „quality.hd" statt
„720p". Die drei Übersetzungen fehlten schlicht.

**Die Kontokarte wuchs aus dem Bild heraus.** Die Leiste unten in der
Seitenleiste hat eine feste Höhe; meine aufklappende Karte schob die Zeile mit
dem Namen nach unten hinaus. Jetzt hängt sie als Popover darüber — die Zeile
bleibt stehen, wie bei JellyTV.

Verkleinerte Downloads bei Jellyfin laufen: Stufen erscheinen, die Adresse
stimmt, die Datei kommt an. Dass die Attrappe nur 28 kB liefert und die App
das als „keine Videodatei" zurückweist, ist richtig so — die Prüfung tut, was
sie soll.

Zwei Gruben in der Attrappe nebenbei gestopft: sie kannte `/stream.mp4` nicht
und meldete ihr Testbild als 360p, womit es gar keine kleineren Stufen geben
konnte.

263 Tests grün. Diesmal alles angesehen: iPhone und iPad, Einstellungen,
Bibliothek, Detailseite, Download-Dialog, Kontokarte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:22:18 +02:00

112 lines
4.3 KiB
Swift

import Foundation
import MediaKit
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<Value: Sendable>: 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, weil die Ansichten nur noch sie sehen.
// Plex' eigener Fehler kommt nur noch dort an, wo unmittelbar mit ihm
// gesprochen wird — Player, Downloads, Live-TV.
if let backendError = error as? MediaBackendError {
return describe(backendError)
}
if let plexError = error as? PlexError {
return describe(plexError.asMediaBackendError)
}
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 {}