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>
102 lines
3.6 KiB
Swift
102 lines
3.6 KiB
Swift
import SwiftUI
|
|
import HaloDesign
|
|
|
|
/// Was auf dem Fernseher zu sehen ist.
|
|
///
|
|
/// **Nur der Zustandswechsel, keine Navigation.** Das ist alles, was von
|
|
/// `RootView` auf tvOS übrig bleibt: dort sind es 556 Zeilen Seitenleiste,
|
|
/// Tab-Anpassung, gemessene Statusleistenhöhe und Mini-Player-Übergang, und
|
|
/// nichts davon gibt es hier. Deshalb eine eigene Datei und kein `#if` in der
|
|
/// bestehenden — ein `#if` wäre kein Zweig gewesen, sondern ein zweiter Rumpf
|
|
/// in derselben Datei.
|
|
struct TVRootView: View {
|
|
@Environment(HaloSession.self) private var session
|
|
@Environment(PlaybackCoordinator.self) private var playback
|
|
@Environment(QuickPlay.self) private var quickPlay
|
|
|
|
/// Wohin ein Link aus dem Regal führt, sobald der Titel geladen ist.
|
|
@State private var ziel: MediaRoute?
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.haloCanvas.ignoresSafeArea()
|
|
|
|
switch session.phase {
|
|
case .launching:
|
|
ProgressView()
|
|
|
|
case .signedOut:
|
|
TVSignInView()
|
|
|
|
case .connecting(let name):
|
|
verbinden(mit: name)
|
|
|
|
case .failed(let meldung):
|
|
fehlgeschlagen(meldung)
|
|
|
|
case .ready:
|
|
TVShell(deepLinkTarget: $ziel)
|
|
}
|
|
}
|
|
.task { await session.restore() }
|
|
// **Hier und nicht in der Startseite.** Von einer Kachel, aus dem
|
|
// Aufmacher und aus der Detailseite wird dasselbe gestartet; läge der
|
|
// Übergang weiter innen, gäbe es ihn dreimal.
|
|
//
|
|
// `fullScreenCover(item:)` gibt es auf tvOS seit 14, und
|
|
// `PlaybackRequest` ist bereits `Identifiable`.
|
|
.fullScreenCover(
|
|
item: Binding(
|
|
get: { playback.request },
|
|
set: { if $0 == nil { playback.close() } }
|
|
)
|
|
) { request in
|
|
TVPlayerScreen(request: request)
|
|
}
|
|
// Der Weg zurück aus dem Regal über der App.
|
|
.onOpenURL { adresse in
|
|
guard let link = TVDeepLink(adresse) else { return }
|
|
Task { await öffne(link) }
|
|
}
|
|
}
|
|
|
|
/// Holt den Titel und macht damit, was der Link sagt.
|
|
///
|
|
/// **Nachgeschlagen, nicht geraten.** Im Regal steht nur eine Kennung;
|
|
/// was dahintersteckt — Film, Serie, Folge — weiss erst der Server. Und
|
|
/// bei einer Serie ist „abspielen" die nächste ungesehene Folge, was
|
|
/// wiederum `QuickPlay` entscheidet.
|
|
private func öffne(_ link: TVDeepLink) async {
|
|
guard let backend = session.backend else { return }
|
|
let kennung = switch link {
|
|
case .play(let id), .detail(let id): id
|
|
}
|
|
guard let item = try? await backend.item(id: kennung) else { return }
|
|
|
|
switch link {
|
|
case .play:
|
|
quickPlay.start(item, backend: backend, coordinator: playback)
|
|
case .detail:
|
|
ziel = MediaRoute(ratingKey: item.id, title: item.title)
|
|
}
|
|
}
|
|
|
|
private func verbinden(mit name: String) -> some View {
|
|
VStack(spacing: HaloSpacing.regular) {
|
|
ProgressView()
|
|
Text(name).font(.haloRowHeader).foregroundStyle(.white)
|
|
}
|
|
}
|
|
|
|
private func fehlgeschlagen(_ meldung: String) -> some View {
|
|
VStack(spacing: HaloSpacing.regular) {
|
|
Text(meldung).font(.haloBody).foregroundStyle(.white)
|
|
HStack(spacing: HaloSpacing.regular) {
|
|
Button("action.retry") { Task { await session.retry() } }
|
|
Button("action.signOut") { session.signOut() }
|
|
}
|
|
}
|
|
.frame(maxWidth: 900)
|
|
}
|
|
}
|