diff --git a/web/frontend/index.html b/web/frontend/index.html index 6c1fa3a..b298a54 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -255,15 +255,23 @@ img { display: block; } /* Zwei Verläufe statt einer Fläche: die Mitte bleibt frei, und genau dort ist das Bild. */ +/* Eine gleichmäßige Abdunklung über allem, wie in der App: „genug, damit + weiße Schrift auf hellen Szenen lesbar bleibt, wenig genug, dass das Bild + nicht verfälscht wirkt." Zwei Verläufe an den Rändern taten das nicht — + in der Mitte stehen die Transportknöpfe und hatten nichts hinter sich. */ +.pl-chrome { background: rgba(0,0,0,.35); } + .pl-top, .pl-bottom { padding: var(--s-loose) var(--s-section); } .pl-top { - display: flex; align-items: center; gap: var(--s-loose); - background: linear-gradient(to bottom, rgba(0,0,0,.75), transparent); + display: flex; align-items: center; gap: var(--s-regular); padding-top: calc(var(--s-loose) + env(safe-area-inset-top)); } -.pl-bottom { - background: linear-gradient(to top, rgba(0,0,0,.85), transparent); - padding-bottom: calc(var(--s-loose) + env(safe-area-inset-bottom)); +.pl-bottom { padding-bottom: calc(var(--s-loose) + env(safe-area-inset-bottom)); } + +/* Die Stufe als Text, deshalb kein rundes Feld. */ +.pl-quality { + width: auto; padding: 0 var(--s-regular); border-radius: var(--r-control); + font-size: 13px; font-weight: 600; white-space: nowrap; } .pl-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; } @@ -286,7 +294,10 @@ img { display: block; } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; } .tiny, .pl-icon, .pl-round, .hero-nav { display: inline-flex; align-items: center; justify-content: center; } -.pl-center { display: flex; align-items: center; justify-content: center; gap: var(--s-section); } +.pl-center { + display: flex; align-items: center; justify-content: center; + gap: calc(var(--s-section) * 2); +} /* Dunkel statt hell durchscheinend: über einer sonnigen Szene verschwanden helle Knöpfe vollständig. Ein Schatten hebt sie zusätzlich ab. */ .pl-round { @@ -298,7 +309,8 @@ img { display: block; } transition: background .15s ease, transform .15s ease; } .pl-round:hover { background: rgba(40,40,46,.8); transform: scale(1.06); } -.pl-round.pl-play { width: 76px; height: 76px; font-size: 27px; } +.pl-round.pl-play { width: 84px; height: 84px; } +.pl-round.pl-play svg { width: 34px; height: 34px; } /* Die Sekundenzahl als Text, nicht als Ziffer im Pfeil: übereinander war beides unlesbar, und die Zahl ist die eigentliche Auskunft. */ #pl-back, #pl-fwd { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; } @@ -366,8 +378,8 @@ img { display: block; } Ein Blatt würde das Bild verdecken, und man stellt hier etwas ein, das man am Bild beurteilen will. */ .pl-menu { - position: absolute; right: var(--s-section); bottom: 96px; z-index: 3; - min-width: 232px; max-height: 60vh; overflow-y: auto; + position: fixed; z-index: 4; + min-width: 232px; max-width: 320px; max-height: 60vh; overflow-y: auto; background: rgba(20,20,23,.92); border: 1px solid var(--separator); border-radius: var(--r-sheet); padding: var(--s-snug); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); @@ -1584,6 +1596,10 @@ function openPlayer(item) { ${esc(item.SeriesName || item.Name)} ${item.SeriesName ? `${esc(episodeLabel(item))}` : ''} + + + +
@@ -1603,7 +1619,6 @@ function openPlayer(item) { -
@@ -1767,70 +1782,75 @@ function openPlayer(item) { startStream(item, v, fail, lauf); }; + /* **Drei Knöpfe, drei Listen** — wie auf dem iPad, und aus demselben Grund: + die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im + Film, die Qualität, wenn es hakt. Das sind drei verschiedene Fragen, und + eine gemeinsame Liste hieße jedes Mal an den beiden anderen vorbeizusuchen. */ const menu = el2('pl-menu'); const closeMenu = () => { menu.hidden = true; }; - el2('pl-more').onclick = () => (menu.hidden ? openMenu() : closeMenu()); - layer.addEventListener('click', (e) => { - if (!menu.hidden && !menu.contains(e.target) && e.target.closest('#pl-more') === null) closeMenu(); - }); - function openMenu() { - const spuren = (art) => lauf.streams.filter((x) => x.Type === art); - const eintrag = (text, gewaehlt, tun) => { + const spuren = (art) => lauf.streams.filter((x) => x.Type === art); + const beschriftung = (t) => t.DisplayTitle || t.Language || `Spur ${t.Index}`; + + function zeigeListe(knopf, titel, eintraege) { + if (!menu.hidden && menu.dataset.von === knopf.id) return closeMenu(); + menu.dataset.von = knopf.id; + menu.innerHTML = `
${esc(titel)}
`; + + eintraege.forEach(({ text, gewaehlt, waehlen }) => { const b = document.createElement('button'); b.className = 'pl-item' + (gewaehlt ? ' on' : ''); b.innerHTML = `${esc(text)}${gewaehlt ? icon('check', 16) : ''}`; - b.onclick = tun; - return b; - }; - - menu.innerHTML = ''; - const block = (titel) => { - const h = document.createElement('div'); - h.className = 'pl-group'; h.textContent = titel; - menu.appendChild(h); - }; - - block('Qualität'); - QUALITIES.forEach(([wert, text]) => { - menu.appendChild(eintrag(text, lauf.quality === wert, () => { - if (lauf.quality === wert) return closeMenu(); - lauf.quality = wert; neuLaden(); - })); + b.onclick = () => (gewaehlt ? closeMenu() : waehlen()); + menu.appendChild(b); }); - const ton = spuren('Audio'); - if (ton.length > 1) { - block('Ton'); - ton.forEach((t) => { - menu.appendChild(eintrag(t.DisplayTitle || t.Language || `Spur ${t.Index}`, - lauf.audio === t.Index, () => { - if (lauf.audio === t.Index) return closeMenu(); - lauf.audio = t.Index; neuLaden(); - })); - }); - } - - const untertitel = spuren('Subtitle'); - if (untertitel.length) { - block('Untertitel'); - menu.appendChild(eintrag('Aus', lauf.subtitle === -1 || lauf.subtitle === null, () => { - if (lauf.subtitle === -1) return closeMenu(); - lauf.subtitle = -1; neuLaden(); - })); - untertitel.forEach((t) => { - menu.appendChild(eintrag(t.DisplayTitle || t.Language || `Spur ${t.Index}`, - lauf.subtitle === t.Index, () => { - if (lauf.subtitle === t.Index) return closeMenu(); - lauf.subtitle = t.Index; neuLaden(); - })); - }); - } - + // Unter seinem Knopf, nicht in der Mitte: so ist zu sehen, wozu die Liste + // gehört, wenn drei davon nebeneinander sitzen. + const r = knopf.getBoundingClientRect(); + menu.style.right = Math.max(12, innerWidth - r.right) + 'px'; + menu.style.top = (r.bottom + 8) + 'px'; menu.hidden = false; show(); } + el2('pl-audio').onclick = () => zeigeListe(el2('pl-audio'), 'Tonspur', + spuren('Audio').map((t) => ({ + text: beschriftung(t), gewaehlt: lauf.audio === t.Index, + waehlen: () => { lauf.audio = t.Index; neuLaden(); }, + }))); + + el2('pl-subs').onclick = () => zeigeListe(el2('pl-subs'), 'Untertitel', [ + { text: 'Aus', gewaehlt: lauf.subtitle < 0, waehlen: () => { lauf.subtitle = -1; neuLaden(); } }, + ...spuren('Subtitle').map((t) => ({ + text: beschriftung(t), gewaehlt: lauf.subtitle === t.Index, + waehlen: () => { lauf.subtitle = t.Index; neuLaden(); }, + })), + ]); + + el2('pl-qual').onclick = () => zeigeListe(el2('pl-qual'), 'Qualität', + QUALITIES.map(([wert, text]) => ({ + text, gewaehlt: lauf.quality === wert, + waehlen: () => { lauf.quality = wert; neuLaden(); }, + }))); + + layer.addEventListener('click', (e) => { + if (menu.hidden || menu.contains(e.target) || e.target.closest('.pl-icon')) return; + closeMenu(); + }); + + /* Was die Kopfzeile zeigt, sobald der Server geantwortet hat. */ + layer.__spurenBekannt = () => { + el2('pl-audio').hidden = spuren('Audio').length === 0; + el2('pl-subs').hidden = spuren('Subtitle').length === 0; + // Gefülltes Symbol, solange Untertitel laufen — wie auf dem iPad. + el2('pl-subs').innerHTML = icon(lauf.subtitle >= 0 ? 'captionsOn' : 'captions'); + // Die Stufe als Text: drei Schieberegler sagen nicht, was eingestellt ist, + // „8 Mbit/s" schon — und genau das ist die Frage, wegen der man hintippt. + el2('pl-qual').textContent = + (QUALITIES.find(([w]) => w === lauf.quality)?.[1] || 'Original').split(' — ').pop(); + }; + show(); layer.classList.add('loading'); el2('pl-busy').classList.add('on'); @@ -1889,6 +1909,7 @@ async function startStream(item, v, fail, lauf) { ?? lauf.streams.find((x) => x.Type === 'Audio')?.Index ?? null; } if (lauf.subtitle == null) lauf.subtitle = source.DefaultSubtitleStreamIndex ?? -1; + v.closest('#player')?.__spurenBekannt?.(); // **Der Rückfall muss Umrechnung erzwingen, nicht sie umgehen.** // @@ -2046,7 +2067,9 @@ function streamURL(path, query) { überall gleich, damit die Symbole zueinander passen. */ const ICONS = { play: '', - sliders: '', + waveform: '', + captions: '', + captionsOn: '', check: '', pause: '', close: '',