Die Tonspur wird aus der Datei gewählt, nicht aus dem Stream
Im Spurenblatt stand eine einzige Zeile: „Unbekannt". Das ist der Name, den AVFoundation einer namenlosen Spur gibt — und bei einem umgerechneten Stream liefert der Server genau eine. Der Player hatte nichts zur Auswahl, weil die Wahl längst gefallen war, bevor er gefragt wurde. Gewählt wird jetzt aus den Spuren, die **in der Datei** stehen, und zwar bevor der Stream ausgehandelt wird. Die Nummer geht als `audioStreamIndex` mit hinaus; wer mitten im Film umstellt, bekommt den Stream neu — ausser bei unveränderter Wiedergabe, wo der Player ohne Unterbrechung umschaltet. Die Abwägung selbst macht `AudioTrackChooser`: gemerkte Spur, dann Wunschsprache, dann Rangliste, dann Standardspur — und Audiodeskription mit deutlichem Abzug statt Ausschluss, damit eine Sprache, die es nur so gibt, immer noch besser ist als Stille. Drei Ebenen in `LanguagePreferences`: Serie, Mediathek, allgemein. Gefragt wird von unten nach oben. Die Mediathek wird nur nachgeschlagen, wenn überhaupt jemand eine Sprache je Mediathek gesetzt hat — sonst kostete jede Wiedergabe eine Anfrage für eine Ebene, die niemand benutzt. Wer von Hand umstellt, dessen Wahl wird für die Serie gemerkt, ohne Nachfrage. Der Dialog „für diese Serie merken?" ist genau die Art, die man nach dem dritten Mal wegklickt. Sichtbar ist es trotzdem: das Spurenblatt zeigt es und lässt es löschen. Dazu die Rangliste in den Einstellungen, ziehbar, und der Schalter „Audiodeskription meiden". Und die Player-Oberfläche blendet endlich als Ganzes über. Die Kopfzeile hatte gar keine Überblendung — sie sprang herein, während die Bedienung darunter verblasste. Jetzt steht die Animation an einer Stelle statt an sechs, und sie läuft über Deckkraft statt über Einfügen und Entfernen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2dbe260901
commit
596a143545
@@ -126,9 +126,22 @@ struct PlayerView: View {
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
if showsControls {
|
||||
controls(for: model).transition(.opacity)
|
||||
}
|
||||
// **Deckkraft statt Einfügen und Entfernen.**
|
||||
//
|
||||
// Eine Übergangsanimation beim Einfügen hängt daran,
|
||||
// dass SwiftUI die Ansicht als dieselbe wiedererkennt
|
||||
// und der Einfügevorgang in einer animierten
|
||||
// Transaktion steckt. Bleibt eine der beiden
|
||||
// Bedingungen aus, erscheint die Bedienung schlagartig
|
||||
// — und genau so sah es aus. Deckkraft hat diese
|
||||
// Bedingungen nicht: sie ist ein Wert, und Werte
|
||||
// lassen sich immer überblenden.
|
||||
controls(for: model)
|
||||
.opacity(showsControls ? 1 : 0)
|
||||
// Unsichtbar heisst auch unantastbar. Sonst läge
|
||||
// der Scrubber weiterhin über dem Bild und
|
||||
// schluckte jeden Klick.
|
||||
.allowsHitTesting(showsControls)
|
||||
// Überspringen und „Nächste Folge" liegen außerhalb
|
||||
// der Bedienelemente: sie sollen auch dann erscheinen,
|
||||
// wenn die ausgeblendet sind — sonst müsste man erst
|
||||
@@ -140,11 +153,18 @@ struct PlayerView: View {
|
||||
// Die Kopfzeile liegt über allem und erscheint auch während
|
||||
// des Ladens. Ohne sie sitzt man bei langsamer Verbindung
|
||||
// fest: kein Zurück, kein Abbrechen, nur ein Ladekreis.
|
||||
if showsControls || !isPlayingPhase(model) {
|
||||
// Sie hatte bisher gar keine Überblendung: sie sprang
|
||||
// herein, während die Bedienung darunter verblasste. Zwei
|
||||
// Hälften derselben Oberfläche, die sich verschieden
|
||||
// verhalten, lesen sich als „es fadet nicht".
|
||||
do {
|
||||
let zeigt = showsControls || !isPlayingPhase(model)
|
||||
VStack {
|
||||
topBar(for: model)
|
||||
Spacer()
|
||||
}
|
||||
.opacity(zeigt ? 1 : 0)
|
||||
.allowsHitTesting(zeigt)
|
||||
.padding(FlixySpacing.loose)
|
||||
// **Unter der Fensterleiste beginnen.**
|
||||
//
|
||||
@@ -165,6 +185,17 @@ struct PlayerView: View {
|
||||
// Die Uhrzeit über einem laufenden Film hat dort nichts zu suchen —
|
||||
// und im Querformat erst recht nicht.
|
||||
.animation(.easeInOut(duration: 0.2), value: model.isBuffering)
|
||||
// **Herein sanft, hinaus zügig.** Das Einblenden begleitet eine
|
||||
// Bewegung und soll der Hand entgegenkommen; das Ausblenden ist ein
|
||||
// Aufräumen, und Aufräumen darf man nicht zusehen müssen.
|
||||
//
|
||||
// Hier, nicht an jeder einzelnen Umschaltstelle: die gab es sechsmal,
|
||||
// mit vier verschiedenen Dauern, und eine davon stand an einer Stelle,
|
||||
// die gar nicht animiert wurde.
|
||||
.animation(
|
||||
showsControls ? .easeOut(duration: 0.45) : .easeIn(duration: 0.28),
|
||||
value: showsControls
|
||||
)
|
||||
#if os(iOS)
|
||||
// Die Statusleiste gehört nicht über einen Film. Auf dem Mac gibt es
|
||||
// keine.
|
||||
@@ -239,7 +270,7 @@ struct PlayerView: View {
|
||||
// Ausblendfrist neu starten. Ohne das verschwänden sie sofort,
|
||||
// weil die alte Frist während des Dialogs abgelaufen ist.
|
||||
guard !isShown else { return }
|
||||
withAnimation(.easeInOut(duration: 0.2)) { showsControls = true }
|
||||
showsControls = true
|
||||
scheduleControlsHide()
|
||||
}
|
||||
.task { scheduleControlsHide() }
|
||||
@@ -787,7 +818,7 @@ struct PlayerView: View {
|
||||
|
||||
private func toggleControls() {
|
||||
if showsControls {
|
||||
withAnimation(.easeInOut(duration: 0.2)) { showsControls = false }
|
||||
showsControls = false
|
||||
} else {
|
||||
revealControls()
|
||||
}
|
||||
@@ -796,12 +827,8 @@ struct PlayerView: View {
|
||||
/// Zeigt die Bedienelemente und setzt die Ausblendfrist neu.
|
||||
private func revealControls() {
|
||||
if !showsControls {
|
||||
// **Herein sanft, hinaus zügig.** Das Einblenden begleitet eine
|
||||
// Bewegung des Zeigers und soll sich anfühlen, als käme die
|
||||
// Bedienung der Hand entgegen — zwei Zehntel wirken dabei wie ein
|
||||
// Aufblitzen. Das Ausblenden dagegen ist ein Aufräumen, und
|
||||
// Aufräumen darf man nicht zusehen müssen.
|
||||
withAnimation(.easeOut(duration: 0.45)) { showsControls = true }
|
||||
// Wie überblendet wird, steht am Player selbst — siehe dort.
|
||||
showsControls = true
|
||||
// Beim Aufdecken nachsehen, was inzwischen läuft — genau dann
|
||||
// will man es wissen.
|
||||
if model.isLive {
|
||||
@@ -832,7 +859,7 @@ struct PlayerView: View {
|
||||
hideTask = Task {
|
||||
try? await Task.sleep(for: .seconds(3.5))
|
||||
guard !Task.isCancelled, model.isPlaying else { return }
|
||||
withAnimation(.easeInOut(duration: 0.3)) { showsControls = false }
|
||||
showsControls = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user