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>
90 lines
3.7 KiB
Swift
90 lines
3.7 KiB
Swift
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")
|
|
}
|
|
}
|
|
}
|