Zwei Dinge in derselben Ecke, und ein Modus, den niemand bestellt hat

Der Gesehen-Haken und die drei Punkte sassen beide oben rechts — auf dem
Plakat sah das aus wie ein Fehler. Jetzt hat jede Ecke eine Aufgabe: oben
links die Auswahl, oben rechts der Stand, unten links Abspielen, unten rechts
das Menü.

Der Knopf „Auswählen" ist weg. Gemessen, warum er stören musste: der längere
Text „Auswahl beenden" brach die Filterzeile um und schob das Raster um
zwanzig Bildpunkte nach unten. Und grundsätzlicher ist ein Modus ein Zustand,
den man kennen muss, bevor man etwas tun kann. Jetzt trägt jede Kachel oben
links ein Kästchen, das beim Überfahren erscheint — wie die drei Punkte auch.
Der erste Haken fängt die Auswahl an, der letzte beendet sie.

Der Spieler lässt sich verkleinern. Er rutscht in eine Leiste am unteren Rand
und läuft weiter, während man die Mediathek aufräumt; ein Klick darauf holt
ihn zurück. Dasselbe Videoelement, nur anders eingerahmt — ein zweiter
Spieler unten hiesse, das Video auszuhängen, und dabei hört es auf zu
spielen.

Das Zahnrad mit acht Strahlen sah aus wie eine Sonne. Jetzt: Wellenform für
den Ton, CC-Kasten für die Untertitel, Schieberegler für die Qualität. Alle
drei Menüs stehen immer da — vorher verschwanden sie bei nur einer Tonspur
oder ohne Untertitel, und wer sie suchte, fand nichts und wusste nicht warum.

Dabei gefunden und behoben: die Wiedergabe blieb nach zwanzig Sekunden
stehen, obwohl längst der ganze Rest auf der Platte lag. Results.File setzte
Last-Modified, der Browser legte die Playlist mit zwei Einträgen ab, und
hls.js bekam bei jedem Nachfragen dieselben zwei zurück. Eine wachsende Liste
ist das Gegenteil von zwischenspeicherbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-05 20:31:12 +02:00
co-authored by Claude Opus 5
parent eda9e67802
commit 7df9fa6c01
7 changed files with 471 additions and 120 deletions
+94 -60
View File
@@ -159,80 +159,109 @@ export function mediathek(
};
suchhuelle.appendChild(suchfeld);
// **Mehrere auf einmal.** „Die Staffel hab ich gesehen" hiess bisher
// zwanzigmal Kontextmenü. Der Knopf steht in der Filterzeile, weil er zu
// dem gehört, was man mit der Liste tut — nicht zu einer einzelnen Kachel.
const auswahlKnopf = document.createElement('button');
auswahlKnopf.className = 'zuruecksetzen';
auswahlKnopf.textContent = 'Auswählen';
auswahlKnopf.onclick = () => auswahlmodusSetzen(!auswahlmodus);
zeile.append(genrePille.huelle, standPille.huelle, sortierHuelle,
anzahl, zuruecksetzen, auswahlKnopf, suchhuelle);
anzahl, zuruecksetzen, suchhuelle);
const gitter = document.createElement('div');
gitter.className = 'raster';
ziel.append(zeile, gitter);
/* ---------- Auswahl ---------- */
/* ---------- Auswahl ----------
*
* **Kein Modus, den man erst einschalten muss.**
*
* Zuerst stand ein Knopf „Auswählen" in der Filterzeile. Zwei Dinge
* sprachen dagegen, und beide sind nachgemessen: der längere Text
* „Auswahl beenden" brach die Zeile um und schob das Raster um zwanzig
* Bildpunkte nach unten — ein Sprung mitten beim Anschauen. Und
* grundsätzlicher: ein Modus ist ein Zustand, den man kennen muss, bevor
* man etwas tun kann.
*
* Jetzt trägt jede Kachel oben links ein Kästchen, das beim Überfahren
* erscheint. Der erste Haken fängt die Auswahl an, der letzte beendet
* sie. Solange etwas gewählt ist, wählt auch ein Klick auf die Kachel
* selbst — sonst öffnet er wie immer die Detailseite.
*/
let auswahlmodus = false;
const gewaehlt = new Set<string>();
/** Was hinter jeder Kachel steht — nach Kennung, für Sammelaktionen. */
const bekannt = new Map<string, Kachel>();
const leiste = auswahlleiste();
ziel.appendChild(leiste.element);
/** Hängt einer Kachel die Wahlmarke an — oder nimmt sie wieder weg. */
/** Hängt einer Kachel das Kästchen an. Sichtbar wird es über die CSS. */
function markeAn(k: HTMLElement): void {
const plakat = k.querySelector('.plakat');
if (!plakat || plakat.querySelector('.wahlmarke')) return;
const m = document.createElement('span');
m.className = 'wahlmarke';
m.setAttribute('role', 'checkbox');
m.setAttribute('aria-checked', 'false');
m.setAttribute('aria-label', 'Auswählen');
m.setAttribute('tabindex', '0');
m.innerHTML = icons.haken;
plakat.appendChild(m);
}
function auswahlmodusSetzen(an: boolean): void {
auswahlmodus = an;
gitter.classList.toggle('auswahl', an);
auswahlKnopf.textContent = an ? 'Auswahl beenden' : 'Auswählen';
auswahlKnopf.classList.toggle('gesetzt', an);
if (an) {
gitter.querySelectorAll<HTMLElement>('.kachel').forEach(markeAn);
} else {
gewaehlt.clear();
gitter.querySelectorAll('.wahlmarke').forEach(m => m.remove());
gitter.querySelectorAll('.kachel.gewaehlt').forEach(k => {
k.classList.remove('gewaehlt');
k.removeAttribute('aria-pressed');
});
}
leiste.auffrischen(gewaehlt.size, an);
}
// **Im Auswahlmodus fängt das Raster den Klick ab**, bevor er die Kachel
// erreicht. Sonst öffnete jeder Haken zugleich die Detailseite — und der
// Griff mit den drei Punkten ein Kontextmenü obendrein.
gitter.addEventListener('click', e => {
if (!auswahlmodus) return;
const k = (e.target as HTMLElement).closest<HTMLElement>('.kachel');
if (!k || !gitter.contains(k)) return;
e.preventDefault();
e.stopPropagation();
function umschalten(k: HTMLElement): void {
const id = k.dataset.id;
if (!id) return;
const jetztGewaehlt = !gewaehlt.has(id);
if (jetztGewaehlt) gewaehlt.add(id); else gewaehlt.delete(id);
k.classList.toggle('gewaehlt', jetztGewaehlt);
k.setAttribute('aria-pressed', String(jetztGewaehlt));
leiste.auffrischen(gewaehlt.size, true);
const jetzt = !gewaehlt.has(id);
if (jetzt) gewaehlt.add(id); else gewaehlt.delete(id);
k.classList.toggle('gewaehlt', jetzt);
k.querySelector('.wahlmarke')?.setAttribute('aria-checked', String(jetzt));
nachAuswahl();
}
function nachAuswahl(): void {
// **Alle Kästchen bleiben stehen, solange etwas gewählt ist.** Sonst
// müsste man für jede weitere Kachel erst wieder das richtige Eck
// treffen.
gitter.classList.toggle('waehlt', gewaehlt.size > 0);
leiste.auffrischen(gewaehlt.size);
}
function abwaehlen(): void {
gewaehlt.clear();
gitter.querySelectorAll('.kachel.gewaehlt').forEach(k => {
k.classList.remove('gewaehlt');
k.querySelector('.wahlmarke')?.setAttribute('aria-checked', 'false');
});
nachAuswahl();
}
// **Das Raster fängt den Klick ab, bevor er die Kachel erreicht.**
// Sonst öffnete ein Haken zugleich die Detailseite.
gitter.addEventListener('click', e => {
const ziel = e.target as HTMLElement;
const k = ziel.closest<HTMLElement>('.kachel');
if (!k || !gitter.contains(k)) return;
// Das Kästchen wählt immer. Die Kachel selbst nur dann, wenn schon
// etwas gewählt ist — sonst bliebe die Mediathek unbenutzbar.
if (!ziel.closest('.wahlmarke') && gewaehlt.size === 0) return;
e.preventDefault();
e.stopPropagation();
umschalten(k);
}, true);
// Dieselbe Geste für die Tastatur.
gitter.addEventListener('keydown', e => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const marke = (e.target as HTMLElement).closest('.wahlmarke');
if (!marke) return;
const k = marke.closest<HTMLElement>('.kachel');
if (!k) return;
e.preventDefault();
e.stopPropagation();
umschalten(k);
}, true);
/** Die Leiste am unteren Rand — nur sichtbar, solange ausgewählt wird. */
function auswahlleiste(): { element: HTMLElement; auffrischen: (zahl: number, an: boolean) => void } {
function auswahlleiste(): { element: HTMLElement; auffrischen: (zahl: number) => void } {
const b = document.createElement('div');
b.className = 'auswahlleiste hidden';
@@ -249,7 +278,6 @@ export function mediathek(
* zurück, bevor die erste durch ist, und die Meldung wäre geraten. */
const aktion = (text: string, arbeit: (ids: string[]) => Promise<string>): HTMLButtonElement => {
const b = knopf(text, 'btn');
b.dataset.braucht = 'auswahl';
b.onclick = async () => {
const ids = [...gewaehlt];
if (ids.length === 0) return;
@@ -286,20 +314,20 @@ export function mediathek(
// **Abwählen bleibt immer anklickbar** — es ist der Weg heraus.
const wegKnopf = knopf('Abwählen', 'btn');
wegKnopf.onclick = () => auswahlmodusSetzen(false);
wegKnopf.onclick = abwaehlen;
knoepfe.appendChild(wegKnopf);
b.append(zahl, knoepfe);
return {
element: b,
auffrischen: (anzahl, an) => {
b.classList.toggle('hidden', !an);
zahl.textContent = anzahl === 0
? 'Nichts ausgewählt'
: `${anzahlwort(anzahl)} ausgewählt`;
knoepfe.querySelectorAll<HTMLButtonElement>('[data-braucht="auswahl"]')
.forEach(x => { x.disabled = anzahl === 0; });
auffrischen: anzahl => {
// **Die Leiste kommt mit dem ersten Haken und geht mit dem
// letzten.** Sie leer stehen zu lassen hiesse, Platz für nichts zu
// belegen — und „Nichts ausgewählt" ist keine Auskunft, die jemand
// braucht.
b.classList.toggle('hidden', anzahl === 0);
zahl.textContent = `${anzahlwort(anzahl)} ausgewählt`;
},
};
}
@@ -411,7 +439,15 @@ export function mediathek(
skip: vonVorn ? 0 : geladen, take: SEITE,
});
if (vonVorn) { gitter.innerHTML = ''; geladen = 0; amEnde = false; bekannt.clear(); }
if (vonVorn) {
gitter.innerHTML = ''; geladen = 0; amEnde = false;
bekannt.clear();
// Nach einem Filterwechsel steht die alte Auswahl nicht mehr auf dem
// Bildschirm. Sie stillschweigend weiterzuführen hiesse, Titel zu
// bearbeiten, die niemand mehr sieht.
gewaehlt.clear();
nachAuswahl();
}
gesamt = seite.total;
geladen += seite.items.length;
@@ -439,9 +475,7 @@ export function mediathek(
// muss die Kachel wissen, wie viele Folgen die Serie hat — sonst
// liesse sich die Plakette „noch 6 offen" nicht wiederherstellen.
bekannt.set(beschreibung.id, beschreibung);
// Nachgeladene Kacheln brauchen die Marke auch — sonst sind die
// ersten zweihundert auswählbar und der Rest nicht.
if (auswahlmodus) markeAn(k);
markeAn(k);
gitter.appendChild(k);
});