Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,107 @@
|
||||
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<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 `HaloSession` —, 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 {}
|
||||
Reference in New Issue
Block a user