Der Web-Player folgt jetzt dem Bauplan des iPad-Players

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 17:53:17 +02:00
co-authored by Claude Opus 5
parent 9da446b295
commit 24e40f7403
+86 -63
View File
@@ -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) {
<strong>${esc(item.SeriesName || item.Name)}</strong>
${item.SeriesName ? `<span>${esc(episodeLabel(item))}</span>` : ''}
</div>
<span class="pl-spacer"></span>
<button class="pl-icon" id="pl-audio" aria-label="Tonspur" hidden>${icon('waveform')}</button>
<button class="pl-icon" id="pl-subs" aria-label="Untertitel" hidden>${icon('captions')}</button>
<button class="pl-icon pl-quality" id="pl-qual" aria-label="Qualität">Original</button>
</div>
<div class="pl-center">
@@ -1603,7 +1619,6 @@ function openPlayer(item) {
<input type="range" id="pl-vol" min="0" max="1" step="0.05" value="1" aria-label="Lautstärke">
<span class="pl-spacer"></span>
<label class="pl-rate">Tempo <select class="pick" id="pl-rate"></select></label>
<button class="pl-icon" id="pl-more" aria-label="Qualität und Spuren">${icon('sliders')}</button>
<button class="pl-icon" id="pl-full" aria-label="Vollbild">${icon('expand')}</button>
</div>
</div>
@@ -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 = `<div class="pl-group">${esc(titel)}</div>`;
eintraege.forEach(({ text, gewaehlt, waehlen }) => {
const b = document.createElement('button');
b.className = 'pl-item' + (gewaehlt ? ' on' : '');
b.innerHTML = `<span>${esc(text)}</span>${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: '<path d="M8 5.5v13l11-6.5z" fill="currentColor" stroke="none"/>',
sliders: '<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>',
waveform: '<path d="M3 12h2M7 8v8M11 4.5v15M15 8v8M19 11h2"/>',
captions: '<rect x="3" y="5.5" width="18" height="13" rx="3"/><path d="M9.5 10.5a2.2 2.2 0 100 3M16 10.5a2.2 2.2 0 100 3"/>',
captionsOn: '<rect x="3" y="5.5" width="18" height="13" rx="3" fill="currentColor" stroke="none"/><path d="M9.5 10.5a2.2 2.2 0 100 3M16 10.5a2.2 2.2 0 100 3" stroke="#0B0B0D"/>',
check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
pause: '<path d="M9 5.5v13M15 5.5v13"/>',
close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',