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);