Zwei Sachen, die am laufenden Player auffielen. **Zwei Sprechblasen nebeneinander.** Eine war Apples eingebautes Untertitelmenü, eine unseres — und welche was tut, sah man erst nach dem Drücken. Schlimmer als die Doppelung war, was dahinterlag: Apples Menü verstellt die **Systemeinstellung** für Untertitel, während unsere Engine `AVTextStyleRule`s aus `SubtitleAppearance` setzt. Zwei Stilregler, von denen nur einer wirkt. Apples Knopf ist jetzt abgeschaltet — `allowedSubtitleOptionLanguages` auf eine leere Liste, der einzige dokumentierte Hebel dafür; die Auswahl von Hand bleibt unberührt. Unser Menü trägt die Spuren oben und darunter Schriftgröße und Darstellung als Untermenüs. Tiefer geht es nicht: die Transportleiste zeigt genau eine Ebene. **Und das Tonmenü verschwindet, wenn Apple es besser kann.** Wird die Datei unverändert gespielt und stimmt die Spurzahl, liegen alle Spuren im Stream — dann zeigt Apple sie selbst, und zwei Knöpfe wären dasselbe zweimal. Nur beim Umrechnen liegt im Stream genau eine, und dann führt allein unseres weiter. **„S7 E2" heisst jetzt „Staffel 7 · Episode 2"**, wo Platz ist: in der Titelzeile des Players und im Aufmacher am Fernseher. Auf einer Kachel bleibt die kurze Form — dort ist eine Zeile breit, und ausgeschrieben verdrängte sie den Titel. Mitgewandert in die geteilte Schicht sind die Namen der Stilstufen: sie standen bei der Einstellungsansicht und wurden damit nur vom Telefon übersetzt. Zwei Listen gleich zu halten ist genau die Art Fehler, die erst auffällt, wenn jemand „Gelb auf Schwarz" wählt und „Weiss auf Schwarz" bekommt. Der Stil wirkt sofort und ohne Neuaufbau, und die Haken im Menü wandern mit: dafür steht er in der Momentaufnahme, obwohl der Player selbst ihn nicht braucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.7 KiB
Swift
89 lines
3.7 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import FlixyPlayback
|
|
import MediaKit
|
|
|
|
/// Der Player auf dem Fernseher.
|
|
///
|
|
/// **Das Modell lebt hier und stirbt hier.** Auf dem Telefon hält `RootView`
|
|
/// es fest, damit die Wiedergabe den Wechsel zwischen Kachel und Vollbild
|
|
/// überlebt. Ohne Mini-Player gibt es diesen Wechsel nicht, und ein
|
|
/// hochgehaltenes Modell hielte nur eine tote Engine, einen laufenden
|
|
/// Transcoder und eine offene Jellyfin-Sitzung am Leben, nachdem der Player
|
|
/// längst weg ist.
|
|
struct TVPlayerScreen: View {
|
|
let request: PlaybackRequest
|
|
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(PlaybackCoordinator.self) private var playback
|
|
@Environment(PlaybackSettings.self) private var settings
|
|
@Environment(LanguagePreferences.self) private var languages
|
|
@Environment(SubtitleAppearance.self) private var subtitleAppearance
|
|
@Environment(NetworkConditions.self) private var network
|
|
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
|
|
|
@State private var model: PlayerModel?
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.black.ignoresSafeArea()
|
|
|
|
if let model {
|
|
// **Die Momentaufnahme entsteht hier, im Rumpf einer View.**
|
|
// Nur so entsteht die Beobachtungsbeziehung zu `@Observable`
|
|
// — ein Repräsentant, der bloss das Modell hält, bekäme nie
|
|
// ein `updateUIViewController`.
|
|
TVPlayerHost(
|
|
model: model,
|
|
snapshot: TVPlayerSnapshot(
|
|
model,
|
|
appearance: subtitleAppearance,
|
|
offLabel: String(localized: "player.subtitlesOff")
|
|
),
|
|
appearance: subtitleAppearance,
|
|
onDismiss: { playback.close() }
|
|
)
|
|
.ignoresSafeArea()
|
|
} else {
|
|
ProgressView()
|
|
}
|
|
}
|
|
// **Der Stil wirkt sofort, ohne Neuaufbau.**
|
|
//
|
|
// Auf `style` und nicht auf die Klasse: ein `@Observable` meldet jede
|
|
// Berührung, und dann liefe das hier auch, wenn sich nichts geändert
|
|
// hat. `SubtitleStyle` ist vergleichbar und feuert nur bei einer
|
|
// echten Änderung.
|
|
//
|
|
// Dass es hier steht und nicht im Coordinator, hat denselben Grund
|
|
// wie die Momentaufnahme: nur im Rumpf einer View entsteht die
|
|
// Beobachtungsbeziehung.
|
|
.onChange(of: subtitleAppearance.style) { _, neu in
|
|
model?.engine?.applySubtitleStyle(neu)
|
|
}
|
|
.task {
|
|
guard model == nil else { return }
|
|
let frisch = PlayerModel(
|
|
request: request,
|
|
backend: session.backend,
|
|
liveTV: nil, // Kein Live-TV in Fassung eins.
|
|
downloads: nil, // Nie auf dem Fernseher.
|
|
diagnostics: diagnostics,
|
|
policy: settings.policy(for: network),
|
|
settings: settings,
|
|
languages: languages,
|
|
subtitles: subtitleAppearance,
|
|
isLocalConnection: session.isLocalConnection,
|
|
connectionDescription: session.connectionDescription,
|
|
unavailableMessage: session.playbackUnavailableMessage
|
|
)
|
|
model = frisch
|
|
await frisch.start()
|
|
}
|
|
// Beim Verschwinden wirklich Schluss: Engine abräumen, Fortschritt
|
|
// melden, die Jellyfin-Sitzung schliessen. Ohne das liefe der
|
|
// Transcoder auf dem Server weiter, obwohl niemand mehr zusieht.
|
|
.onDisappear { [model] in Task { await model?.stop() } }
|
|
}
|
|
}
|