Phase 1: Anmeldung, Bibliothek, Detailseiten, Hybrid-Player
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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user