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:
Scarriffle
2026-09-28 16:31:38 +02:00
co-authored by Claude Opus 5
parent 2dbe260901
commit 596a143545
12 changed files with 755 additions and 120 deletions
+81 -14
View File
@@ -8,18 +8,12 @@ import FlixyDesign
/// sie stand in den Voreinstellungen und blieb dort für immer leer. Eine
/// Vorliebe, die niemand äußern kann, ist keine.
struct LanguagesView: View {
@Environment(PlaybackSettings.self) private var settings
@Environment(LanguagePreferences.self) private var settings
@Environment(\.flixyAccent) private var accent
/// Die Sprachen, die in Mediatheken tatsächlich vorkommen.
///
/// Keine vollständige Liste aller Sprachen der Welt: ein Auswahlrad mit
/// zweihundert Einträgen, um „Deutsch" zu finden, ist keine Hilfe. Was
/// fehlt, lässt sich später ergänzen — dann mit einem Suchfeld.
private static let codes = [
"de", "en", "fr", "es", "it", "nl", "pl", "pt", "ru", "sv", "da", "tr",
"ja", "ko", "zh",
]
@State private var isAdding = false
private var codes: [String] { LanguagePreferences.offeredCodes }
var body: some View {
@Bindable var settings = settings
@@ -33,9 +27,9 @@ struct LanguagesView: View {
)
Section {
Picker("languages.audio", selection: $settings.preferredAudioLanguage) {
Picker("languages.audio", selection: $settings.audioLanguage) {
Text("languages.serverDefault").tag("")
ForEach(Self.codes, id: \.self) { code in
ForEach(codes, id: \.self) { code in
Text(name(of: code)).tag(code)
}
}
@@ -45,10 +39,18 @@ struct LanguagesView: View {
Text("languages.audioExplanation")
}
fallbackSection
Section {
Picker("languages.subtitles", selection: $settings.preferredSubtitleLanguage) {
Toggle("languages.avoidAudioDescription", isOn: $settings.avoidsAudioDescription)
} footer: {
Text("languages.avoidAudioDescriptionExplanation")
}
Section {
Picker("languages.subtitles", selection: $settings.subtitleLanguage) {
Text("languages.subtitlesOff").tag("")
ForEach(Self.codes, id: \.self) { code in
ForEach(codes, id: \.self) { code in
Text(name(of: code)).tag(code)
}
}
@@ -64,6 +66,71 @@ struct LanguagesView: View {
.background(Color.flixyCanvas)
.navigationTitle(Text("languages.title"))
.flixyInlineTitle()
// Eine Rangliste ohne Umsortieren wäre keine. Auf dem iPhone braucht
// das Ziehen den Bearbeitungsmodus nicht — `onMove` in einer `Form`
// genügt, sobald die Liste bearbeitbar ist.
.environment(\.editMode, .constant(.active))
}
/// Die Rangliste.
///
/// **Was sie ist:** der Boden unter der Wunschsprache. Sie greift nur,
/// wenn ein Titel die gewünschte Sprache gar nicht hat.
///
/// **Was sie nicht ist:** eine zweite Wunschsprache. Steht die
/// Wunschsprache zur Verfügung, wird die Liste nicht einmal angesehen.
@ViewBuilder
private var fallbackSection: some View {
@Bindable var settings = settings
Section {
if settings.fallbacks.isEmpty {
Text("languages.fallbackEmpty")
.foregroundStyle(Color.flixyTextSecondary)
} else {
ForEach(Array(settings.fallbacks.enumerated()), id: \.element) { position, code in
HStack(spacing: FlixySpacing.regular) {
// Die Nummer, weil die Reihenfolge der ganze Zweck
// ist — eine Liste ohne sichtbaren Rang liest sich
// wie eine Menge.
Text("\(position + 1)")
.font(.flixyCaption.monospacedDigit())
.foregroundStyle(Color.flixyTextSecondary)
.frame(minWidth: 18, alignment: .trailing)
Text(name(of: code))
.foregroundStyle(Color.flixyTextPrimary)
}
}
.onMove { von, nach in
settings.fallbacks.move(fromOffsets: von, toOffset: nach)
}
.onDelete { settings.fallbacks.remove(atOffsets: $0) }
}
Button {
isAdding = true
} label: {
Label("languages.fallbackAdd", systemImage: "plus")
}
.disabled(remaining.isEmpty)
} header: {
Text("languages.fallbackHeader")
} footer: {
Text("languages.fallbackExplanation")
}
.confirmationDialog("languages.fallbackAdd", isPresented: $isAdding, titleVisibility: .visible) {
ForEach(remaining, id: \.self) { code in
Button(name(of: code)) { settings.fallbacks.append(code) }
}
}
}
/// Was noch nicht in der Liste steht.
///
/// Eine Sprache zweimal in einer Rangliste hat keine Bedeutung — und die
/// Liste benutzt den Code als Kennung, was bei Dubletten bräche.
private var remaining: [String] {
codes.filter { !settings.fallbacks.contains($0) }
}
/// Der Name der Sprache in der Sprache des Geräts.