diff --git a/web/app/src/icons.ts b/web/app/src/icons.ts index 898c140..14f21f6 100644 --- a/web/app/src/icons.ts +++ b/web/app/src/icons.ts @@ -96,8 +96,6 @@ export const icons = { * löst keinen Alarm aus. */ fahne: rahmen(''), - /** Tonversatz — zwei Pfeile, die auseinandergehen. */ - versatz: rahmen(''), } as const; export type IconName = keyof typeof icons; diff --git a/web/app/src/spieler.ts b/web/app/src/spieler.ts index 2750b60..129dafb 100644 --- a/web/app/src/spieler.ts +++ b/web/app/src/spieler.ts @@ -85,13 +85,12 @@ export function geraeteprofil(): { /** Wie oft der Stand zum Server geht. */ const MELDEABSTAND = 10_000; -/** Die Stufen des Tonversatzes in Millisekunden. - * - * **Nicht jeder Hunderter von −5000 bis +5000.** Das wären einhunderteins - * Einträge für etwas, das man im Jahr dreimal braucht. Diese elf decken ab, - * was vorkommt — und wer mehr braucht, hat keine Einstellung zu wenig, - * sondern eine kaputte Datei. */ -const VERSAETZE = [-1000, -500, -300, -200, -100, 0, 100, 200, 300, 500, 1000]; +/** In welchen Schritten der Regler einrastet. */ +const VERSATZSCHRITT = 100; + +/** Wie weit in beide Richtungen. Darüber hinaus ist es kein Versatz mehr, + * sondern eine kaputte Datei. */ +const VERSATZGRENZE = 3000; /** Wo gemerkte Tonversätze liegen — je Datei, in diesem Browser. * @@ -372,10 +371,58 @@ export function spieler(anfrage: Spieleranfrage): void { * acht Strahlen sah aus wie eine Sonne — niemand vermutete die Bildqualität * dahinter. Jetzt: Wellenform für den Ton, CC-Kasten für die Untertitel, * Schieberegler für die Qualität. */ - const tonmenue = menue('tonspur', 'Tonspur'); + // **Der Lautsprecher, nicht die Wellenform.** Hier stand einmal ein eigenes + // Zeichen, damit er nicht zweimal in der Leiste auftaucht — links für + // Stummschalten, rechts für die Tonspur. Die Wellenform erkannte aber + // niemand als „Ton"; zwei Lautsprecher sind das kleinere Übel, zumal der + // linke beim Stummschalten durchgestrichen wird und damit anders aussieht. + const tonmenue = menue('ton', 'Ton'); const textmenue = menue('untertitel', 'Untertitel'); const stufenmenue = menue('regler', 'Qualität'); - const versatzmenue = menue('versatz', 'Tonversatz'); + + /* ---------- Der Tonversatz, abgetrennt im Tonmenü ---------- */ + + // **Ein Regler, keine Liste.** Feste Stufen hiessen: für jeden Wert ein + // Eintrag, und die standen zwischen den Sprachen wie weitere Sprachen. + // Hier wird gezogen, und zwar erst beim Loslassen — jede Änderung baut den + // Strom neu auf, und das tut man nicht zwanzigmal beim Hinschieben. + const versatzkopf = document.createElement('div'); + versatzkopf.className = 'pl-fusstitel'; + + const versatzregler = document.createElement('input'); + versatzregler.type = 'range'; + versatzregler.className = 'pl-laut pl-versatz'; + versatzregler.min = String(-VERSATZGRENZE); + versatzregler.max = String(VERSATZGRENZE); + versatzregler.step = String(VERSATZSCHRITT); + versatzregler.setAttribute('aria-label', 'Tonversatz in Millisekunden'); + + const versatzzurueck = knopf('Zurücksetzen', 'pl-punkt'); + + function versatzstand(): void { + versatzkopf.textContent = `Tonversatz — ${versatzWort(tonversatz)}`; + versatzregler.value = String(tonversatz); + versatzregler.setAttribute('aria-valuetext', versatzWort(tonversatz)); + versatzzurueck.classList.toggle('hidden', tonversatz === 0); + } + + function versatzSetzen(ms: number): void { + tonversatz = Math.max(-VERSATZGRENZE, Math.min(VERSATZGRENZE, Math.round(ms / VERSATZSCHRITT) * VERSATZSCHRITT)); + versatzMerken(anfrage.dateiId, tonversatz); + versatzstand(); + void stromAufbauen(filmzeit()); + } + + // Beim Ziehen nur die Zahl mitlaufen lassen, beim Loslassen rechnen. + versatzregler.oninput = () => { + versatzkopf.textContent = `Tonversatz — ${versatzWort(Number(versatzregler.value))}`; + }; + versatzregler.onchange = () => versatzSetzen(Number(versatzregler.value)); + versatzregler.onclick = e => e.stopPropagation(); + versatzzurueck.onclick = e => { e.stopPropagation(); versatzSetzen(0); }; + + tonmenue.fuss.append(versatzkopf, versatzregler, versatzzurueck); + const links = document.createElement('div'); links.className = 'pl-gruppe'; @@ -383,8 +430,7 @@ export function spieler(anfrage: Spieleranfrage): void { const rechts = document.createElement('div'); rechts.className = 'pl-gruppe'; - rechts.append( - rest, tonmenue.knopf, versatzmenue.knopf, textmenue.knopf, stufenmenue.knopf, vollbild); + rechts.append(rest, tonmenue.knopf, textmenue.knopf, stufenmenue.knopf, vollbild); const unten = document.createElement('div'); unten.className = 'pl-unten'; @@ -653,23 +699,9 @@ export function spieler(anfrage: Spieleranfrage): void { tonmenue.setzen(spuren); - // **Der Versatz hat ein eigenes Menü.** Er stand vorher unter den - // Tonspuren, und dort las er sich wie eine: „Später (+100 ms)" sah aus - // wie der Name einer Spur. Eine Einstellung gehört nicht in eine Liste - // von Inhalten. - // - // Feste Stufen statt zweier Tasten, wie bei der Qualität: so zeigt der - // Haken, wo man steht, und man trifft einen Wert, statt sich - // hinzutasten — jeder Schritt baut den Strom neu auf. - versatzmenue.setzen(VERSAETZE.map(ms => ({ - text: versatzWort(ms), - gewaehlt: ms === tonversatz, - tun: () => { - tonversatz = ms; - versatzMerken(anfrage.dateiId, ms); - void stromAufbauen(filmzeit()); - }, - }))); + // Ohne Tonspur gibt es nichts zu verschieben. + tonmenue.fuss.classList.toggle('hidden', toene.length === 0); + versatzstand(); const texte = auskunft?.subtitleStreams ?? []; textmenue.setzen( @@ -914,7 +946,7 @@ export function spieler(anfrage: Spieleranfrage): void { /** Ein kleines Menü, das über der Leiste aufgeht. */ function menue(bild: keyof typeof icons, titel: string): { - knopf: HTMLElement; setzen: (punkte: Wahl[]) => void; + knopf: HTMLElement; fuss: HTMLElement; setzen: (punkte: Wahl[]) => void; } { const huelle = document.createElement('div'); huelle.className = 'pl-menue'; @@ -934,10 +966,20 @@ export function spieler(anfrage: Spieleranfrage): void { const liste = document.createElement('div'); liste.className = 'pl-liste'; + + // **Ein abgetrennter Fuss für das, was keine Auswahl ist.** + // Eine Einstellung zwischen Inhalten sieht aus wie ein Inhalt — der + // Tonversatz stand als „Später (+100 ms)" zwischen den Sprachen und las + // sich wie eine. Hier ist er durch einen Strich und eine Überschrift + // sichtbar etwas anderes. + const fuss = document.createElement('div'); + fuss.className = 'pl-fuss hidden'; huelle.append(ausloeser, liste); + liste.appendChild(fuss); return { knopf: huelle, + fuss, setzen: punkte => { liste.innerHTML = ''; punkte.forEach(p => { @@ -950,6 +992,8 @@ export function spieler(anfrage: Spieleranfrage): void { }; liste.appendChild(b); }); + // Der Fuss gehört nicht zur Auswahl und wird deshalb wieder angehängt. + liste.appendChild(fuss); }, }; } diff --git a/web/app/src/style.css b/web/app/src/style.css index 7adafcf..2907cef 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -897,6 +897,29 @@ textarea.feld { } .pl-menue.auf .pl-liste { display: flex; } +/* **Der Fuss ist sichtbar etwas anderes als die Liste darüber.** + Eine Einstellung zwischen Inhalten sieht aus wie ein Inhalt — der + Tonversatz stand als Eintrag zwischen den Sprachen und las sich wie eine. + Ein Strich, eine Überschrift, ein Regler: drei Mittel, und jedes einzelne + würde es schon sagen. */ +.pl-fuss { + margin-top: 6px; padding: 8px 8px 6px; + border-top: 1px solid var(--line); + display: flex; flex-direction: column; gap: 6px; +} +.pl-fusstitel { + font-size: 11px; font-weight: 600; letter-spacing: .04em; + text-transform: uppercase; color: var(--text-muted); +} +/* Breiter als der Lautstärkeregler — hier wird genau gezielt, nicht gewischt. */ +.pl-versatz { width: 100%; } +/* Die Mitte markieren: ohne sie trifft man die Null nur durch Zufall. */ +.pl-versatz::-webkit-slider-runnable-track { + background: + linear-gradient(rgba(255, 255, 255, .55), rgba(255, 255, 255, .55)) 50% / 2px 10px no-repeat, + rgba(255, 255, 255, .28); +} + .pl-punkt { display: flex; align-items: center; gap: 8px; background: none; border: 0; color: var(--text);