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:
co-authored by
Claude Opus 5
parent
9da446b295
commit
24e40f7403
+86
-63
@@ -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"/>',
|
||||
|
||||
Reference in New Issue
Block a user