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:
Scarriffle
2026-10-03 11:12:32 +02:00
co-authored by Claude Opus 5
parent 2fcf6821c8
commit 4b9a1e7b67
280 changed files with 1456 additions and 1456 deletions
+89
View File
@@ -0,0 +1,89 @@
import AVKit
import SwiftUI
import HaloPlayback
import MediaKit
/// Hängt Apples Player in den SwiftUI-Baum.
///
/// **Wem was gehört.** Der `AVPlayerViewController` gehört dem Coordinator,
/// nicht der SwiftUI-Struktur — die wird bei jedem Neuzeichnen neu gebaut —
/// und nicht dem `PlayerModel`, das frei von UIKit bleiben soll. Das Modell
/// gibt nur seinen `AVPlayer` heraus, und das ist AVFoundation.
@MainActor
struct TVPlayerHost: UIViewControllerRepresentable {
let model: PlayerModel
/// Eine Momentaufnahme dessen, was den Controller betrifft.
///
/// **Als Wert, nicht als Verweis.** SwiftUI ruft `updateUIViewController`
/// nur, wenn sich am Repräsentanten etwas geändert hat. Stünde hier allein
/// das Modell, änderte sich nie etwas: es ist eine Klasse, und ihre
/// Kennung bleibt über jeden Spurwechsel hinweg dieselbe. Der Wert wird im
/// Rumpf der umgebenden Ansicht gebildet — dort und nur dort entsteht die
/// Beobachtungsbeziehung zu `@Observable`.
let snapshot: TVPlayerSnapshot
/// Der Untertitelstil — er steht im selben Menü wie die Spurwahl.
let appearance: SubtitleAppearance
let onDismiss: () -> Void
func makeUIViewController(context: Context) -> AVPlayerViewController {
context.coordinator.controller
}
func updateUIViewController(_ controller: AVPlayerViewController, context: Context) {
context.coordinator.apply(snapshot, model: model)
}
func makeCoordinator() -> TVPlayerCoordinator {
TVPlayerCoordinator(appearance: appearance, onDismiss: onDismiss)
}
}
/// Was der Controller von der Wiedergabe wissen muss — und mehr nicht.
struct TVPlayerSnapshot: Equatable {
/// Wechselt bei jedem Neuaufbau. Daran hängt der Austausch des Players.
let playerIdentity: ObjectIdentifier?
let audio: [TrackMenu.Entry]
let subtitles: [TrackMenu.Entry]
/// Läuft gerade ein Neuaufbau? Dann ist das Bild schwarz und das Menü
/// gesperrt.
let isRebuilding: Bool
let title: String
let subtitle: String?
/// Der Zeitbereich einer Marke — für „Intro überspringen".
let markerLabel: String?
/// Der eingestellte Untertitelstil.
///
/// **Gehört in die Momentaufnahme, obwohl der Player ihn nicht braucht.**
/// Er steht im selben Menü wie die Spurwahl, und ohne ihn hier merkte
/// SwiftUI nicht, dass sich etwas geändert hat — die Haken blieben beim
/// alten Wert stehen, während die Untertitel längst anders aussähen.
let subtitleStyle: SubtitleStyle
/// `@MainActor`, weil das Modell es ist: gelesen wird hier zwölfmal aus
/// ihm, und zwar im Rumpf einer View — also ohnehin auf dem Hauptakteur.
@MainActor
init(_ model: PlayerModel, appearance: SubtitleAppearance, offLabel: String) {
subtitleStyle = appearance.style
playerIdentity = model.systemPlayer.map(ObjectIdentifier.init)
let direkt: Bool = if case .directPlay = model.decision?.method { true } else { false }
audio = TrackMenu.audio(
from: model.audioStreams,
selected: model.selectedAudioStream,
isDirectPlay: direkt,
playerTrackCount: model.audioTracks.count
)
subtitles = TrackMenu.subtitles(
from: model.subtitleStreams,
selected: model.selectedSubtitleStream,
isDirectPlay: direkt,
playerTrackLanguages: model.subtitleTracks.map(\.languageCode),
offLabel: offLabel
)
isRebuilding = model.isChangingQuality
title = model.title
subtitle = model.subtitle
markerLabel = model.activeMarker.map {
String(localized: $0.isIntro ? "player.skipIntro" : "player.skipCredits")
}
}
}