From 2c51f639bed61fe31571df628337d20d130b0650 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 10 Oct 2026 21:33:35 +0200 Subject: [PATCH] =?UTF-8?q?Untertitel=20vorher=20w=C3=A4hlen,=20wie=20den?= =?UTF-8?q?=20Ton=20auch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf der Detailseite stand „Ton: Deutsch" und liess sich vor dem Start umstellen — aus gutem Grund: im Spieler umzuschalten heisst, den Strom neu aufzubauen. Für Untertitel gab es das nicht, obwohl dort dieselbe Überlegung gilt und die Spuren ohnehin schon dastehen. Jetzt steht „Untertitel: Keine" daneben. Auch bei nur einer Spur: anders als beim Ton gibt es hier immer mindestens zwei Möglichkeiten — diese eine, oder gar keine. Der Wunsch wird gemerkt wie beim Ton, mit einem dritten Zustand. „Keine" ist eine Antwort und kein Fehlen einer: wer sie abgewählt hat, will sie beim nächsten Film auch nicht, während „nie etwas gesagt" offen lässt, ob eine erzwungene Spur erscheinen darf. Co-Authored-By: Claude Opus 5 --- web/app/src/spieler.ts | 26 +++++++++++++- web/app/src/views/detail.ts | 68 ++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 0de8bdf..0643afe 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -138,6 +138,26 @@ export function tonspracheMerken(sprache: string): void { try { localStorage.setItem(SPRACHSCHLUESSEL, sprache); } catch { /* Privates Fenster. */ } } +/** Dasselbe für den Untertitel — mit einem dritten Zustand. + * + * **„Keine" ist eine Antwort, nicht das Fehlen einer.** Wer Untertitel + * abgewählt hat, will sie beim nächsten Film auch nicht; ein leerer Eintrag + * hiesse dagegen „noch nie etwas gesagt", und dann darf eine erzwungene + * Spur durchaus erscheinen. */ +const UNTERTITELSCHLUESSEL = 'flaro.untertitelsprache'; + +/** Der gemerkte Wunsch: eine Sprache, `'keine'` oder `null` für „nie gesagt". */ +export function untertitelsprache(): string | null { + try { return localStorage.getItem(UNTERTITELSCHLUESSEL); } catch { return null; } +} + +export function untertitelspracheMerken(wunsch: string): void { + try { localStorage.setItem(UNTERTITELSCHLUESSEL, wunsch); } catch { /* Privates Fenster. */ } +} + +/** Was „keine Untertitel" im Speicher heisst. */ +export const OHNE_UNTERTITEL = 'keine'; + /** „Ohne Versatz", „Ton 200 ms später", „Ton 1 s früher". */ function versatzWort(ms: number): string { if (ms === 0) return 'Ohne Versatz'; @@ -166,6 +186,8 @@ export type Spieleranfrage = { laenge: number | null; /** Mit welcher Tonspur gestartet wird — gewählt auf der Detailseite. */ tonspur?: number | null; + /** Mit welchem Untertitel gestartet wird. `null` heisst: keiner. */ + untertitelspur?: number | null; danach: () => void; }; @@ -213,7 +235,9 @@ export function spieler(anfrage: Spieleranfrage): void { /** Die gewählte Stufe und Spuren — sie überleben einen Neustart des Stroms. */ let stufe: number | null = null; let tonspur: number | null = anfrage.tonspur ?? null; - let untertitel: number | null = null; + // **Die Wahl von der Detailseite gilt ab dem ersten Bild.** Sie erst im + // Spieler zu treffen hiesse, die ersten Sätze ohne Untertitel zu sehen. + let untertitel: number | null = anfrage.untertitelspur ?? null; /** Ob der Server umkodieren soll, egal was das Profil sagt. * diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts index 5135620..de5915c 100644 --- a/web/app/src/views/detail.ts +++ b/web/app/src/views/detail.ts @@ -10,7 +10,10 @@ import { import { icons, icon } from '../icons'; import { kachel, alsKachel, reihe } from '../card'; import { vergessen } from '../ansichten'; -import { spieler, tonsprache, tonspracheMerken } from '../spieler'; +import { + spieler, tonsprache, tonspracheMerken, + untertitelsprache, untertitelspracheMerken, OHNE_UNTERTITEL, +} from '../spieler'; import { menueAnZiel } from '../menue'; import { meldedialog } from '../melden'; @@ -409,6 +412,9 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem const tonwahl = tonspurwahl(datei); if (tonwahl) reihe.appendChild(tonwahl); + const textwahl = untertitelwahl(datei); + if (textwahl) reihe.appendChild(textwahl); + // **Kein „Herunterladen" im Browser.** // // Hier stand ein Knopf, der die Datei als MP4 herausgab. Das ist etwas @@ -518,6 +524,65 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem return k; } + /** Welcher Untertitel beim Start laufen soll — `null` heisst: keiner. */ + let gewaehlterText: number | null = null; + /** Ob oben schon eine Wahl getroffen wurde. Sonst gilt der gemerkte Wunsch. */ + let textGewaehlt = false; + + /** Dasselbe für den Untertitel — mit „Keine" als erster Wahl. + * + * **Auch bei nur einer Spur.** Anders als beim Ton gibt es hier immer + * mindestens zwei Möglichkeiten: diese eine, oder gar keine. Ein Knopf, + * der nur „Deutsch" bestätigt, wäre beim Ton überflüssig — hier ist er + * der einzige Weg, die Untertitel vorher abzuschalten. */ + function untertitelwahl(datei: Datei): HTMLElement | null { + const spuren = datei.streams.filter(s => s.kind === 'Subtitle'); + if (spuren.length === 0) return null; + + if (!textGewaehlt) { + const wunsch = untertitelsprache(); + gewaehlterText = wunsch === null || wunsch === OHNE_UNTERTITEL + ? null + : spuren.find(s => s.language === wunsch)?.index ?? null; + textGewaehlt = true; + } + + const name = (s: Spur): string => + s.languageName ?? s.display?.split(' · ')[0] ?? s.language ?? `Spur ${s.index}`; + + const k = knopf('', 'btn'); + const zeichnen = (): void => { + const jetzt = spuren.find(s => s.index === gewaehlterText); + k.textContent = `Untertitel: ${jetzt ? name(jetzt) : 'Keine'}`; + k.prepend(icon('untertitel')); + }; + zeichnen(); + + k.onclick = () => menueAnZiel(k, [ + { + text: 'Keine', + icon: gewaehlterText === null ? ('haken' as const) : undefined, + tun: () => { + gewaehlterText = null; + untertitelspracheMerken(OHNE_UNTERTITEL); + zeichnen(); + }, + }, + ...spuren.map(s => ({ + text: name(s), + icon: s.index === gewaehlterText ? ('haken' as const) : undefined, + tun: () => { + gewaehlterText = s.index; + // Wer hier wechselt, meint es auch beim nächsten Film so. + if (s.language) untertitelspracheMerken(s.language); + zeichnen(); + }, + })), + ]); + + return k; + } + function rundKnopf( bild: keyof typeof icons, an: boolean, titel: string, tun: () => Promise, ): HTMLButtonElement { @@ -874,6 +939,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem ab, laenge: datei.durationSeconds, tonspur: gewaehlteSpur, + untertitelspur: gewaehlterText, danach: laden, }); }