From 24e40f7403b4b9f09fe20aa76c3456789668f04e Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 17:53:17 +0200 Subject: [PATCH] Der Web-Player folgt jetzt dem Bauplan des iPad-Players MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mein Menü machte genau den Fehler, den der iPad-Code im Kommentar ausschließt: alles in einer Liste. Dort steht, warum das falsch ist — "die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der anderen Liste vorbei." Jetzt drei Knöpfe in der Kopfzeile, jeder mit seiner eigenen Liste darunter, und jede Liste hängt sichtbar an ihrem Knopf. Die Tonspur erscheint auch dann, wenn es nur eine gibt — welche läuft, will man wissen, nicht nur ob man wechseln kann. Die Untertitel-Taste ist gefüllt, solange welche laufen. Die Qualität steht als Text im Knopf, nicht als Symbol. Auch das aus dem iPad-Player übernommen: "Drei Schieberegler sagen nicht, was eingestellt ist; '1080p' schon — und genau das ist die Frage, wegen der man hintippt." Dazu eine gleichmäßige Abdunklung statt zweier Verläufe an den Rändern. Die Verläufe ließen die Mitte frei — wo die Transportknöpfe stehen, die damit auf hellen Szenen nichts hinter sich hatten. Co-Authored-By: Claude Opus 5 --- web/frontend/index.html | 149 +++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 63 deletions(-) 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: '',