Die Datenschicht: - plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund - Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor Vermittlung), mit Abgleich der Server-Kennung gegen veraltete plex.direct-Adressen - PlexServer als einzige Stelle, an der die App Endpunkte kennt: Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten, Staffeln/Folgen, Suche Die Oberfläche: - Startseite mit Aufmacher und den Reihen vom Server - Mediathek als Raster oder Liste, mit Filter- und Sortierblatt - Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung, Fassungsauswahl und aufklappbaren technischen Angaben - Suche mit gemerkten Anfragen - Vollbildplayer mit eigenem Chrome Der Player: - AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll - VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test` in FlixyKit nicht das 920-MB-Binärpaket mitzieht Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf, dass `strong=true` einen dreißigstelligen Code liefert, der sich bei plex.tv/link nicht eintippen lässt — jetzt der kurze. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
2.7 KiB
Swift
74 lines
2.7 KiB
Swift
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<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 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
|
|
}
|