Ein Kontextmenü auf jeder Kachel — mitsamt Bildauswahl

Gesehen, Favorit und Weglegen gab es ausschliesslich auf der Detailseite.
In einer Übersicht mit zwanzig Titeln hiess "die hab ich alle gesehen"
vierzig Seitenwechsel. Im ganzen Quelltext stand kein einziger
contextmenu-Zuhörer.

Drei Wege hinein, weil es drei Arten gibt zu bedienen: Rechtsklick für die
Maus, Shift+F10 und die Kontextmenü-Taste für die Tastatur, langer Druck
für den Finger. Nur den Rechtsklick anzubieten hiesse, das Menü auf dem
Telefon zu verstecken. Der lange Druck bricht bei jeder Bewegung ab, sonst
löst Scrollen ihn aus.

Die Aktionen wirken auf der Kachel selbst. Die Liste neu zu laden verlöre
Bildlauf und Filter — für einen Haken.

Dazu die Bildauswahl. Es gab genau einen Bild-Endpunkt, und der lieferte
nur aus: wählte TMDB ein schlechtes Plakat, blieb es. Jetzt stehen Plakat,
Hintergrund und Schriftzug zur Wahl, dazu eigene Bilder bis 10 MB. Die
Kandidaten kosten nichts — TMDB liefert sie beim Einzelabruf ohnehin mit,
und bisher wurde alles ausser dem Schriftzug weggeworfen.

Drei Merker je Titel, nicht einer: man kann ein eigenes Plakat wollen und
trotzdem automatische Hintergründe. Ohne sie überschriebe das nächste
Auffrischen jede Handauswahl, und wer einmal ein besseres Plakat gesucht
hat, suchte es jedes Mal neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 12:43:52 +02:00
co-authored by Claude Opus 5
parent e1399c808f
commit eb63271e92
16 changed files with 1759 additions and 11 deletions
+90 -1
View File
@@ -1,6 +1,11 @@
import { bildAdresse, type Film, type Serie, type Angefangen, type Stand } from './api';
import {
bildAdresse, gesehen, favorit, weglegen, istAdmin,
type Film, type Serie, type Angefangen, type Stand,
} from './api';
import { esc, anteil } from './util';
import { icons } from './icons';
import { kontextmenue, type Menuepunkt } from './menue';
import { bilddialog } from './bilder';
/** Was eine Kachel zeigt — unabhängig davon, woher sie kommt. */
export type Kachel = {
@@ -59,6 +64,11 @@ export function kachel(k: Kachel, oeffnen: (art: 'filme' | 'serien', id: string)
knopf.className = 'kachel';
knopf.onclick = () => oeffnen(k.art, k.id);
// **Das Kontextmenü an jeder Kachel, überall.** Gesehen, Favorit und
// Weglegen gab es nur auf der Detailseite — in einer Übersicht mit zwanzig
// Titeln hiess „die hab ich alle gesehen" vierzig Seitenwechsel.
kontextmenue(knopf, () => kachelpunkte(k, knopf, oeffnen));
const rahmen = document.createElement('div');
rahmen.className = 'plakat';
rahmen.setAttribute('role', 'img');
@@ -172,3 +182,82 @@ export function reihe(
bereich.append(kopf, streifen);
return bereich;
}
/** Was im Kontextmenü einer Kachel steht.
*
* **Aktionen wirken sofort auf der Kachel**, ohne Seitenwechsel und ohne
* Neuaufbau der Liste. Den Zustand hier zu spiegeln statt neu zu laden ist
* nicht nur schneller — ein Neuaufbau verlöre Bildlauf und Filter.
*/
function kachelpunkte(
k: Kachel,
knopf: HTMLElement,
oeffnen: (art: 'filme' | 'serien', id: string) => void,
): Menuepunkt[] {
const istGesehen = k.stand?.watched === true;
const punkte: Menuepunkt[] = [
{
text: istGesehen ? 'Als ungesehen markieren' : 'Als gesehen markieren',
icon: 'hakenKreis',
tun: async () => {
await gesehen(k.id, !istGesehen);
// **Die Kachel spiegelt den neuen Zustand selbst.** Die Liste neu zu
// laden verlöre Bildlauf und Filter — für einen Haken.
if (k.stand) k.stand.watched = !istGesehen;
hakenZeigen(knopf, !istGesehen);
},
},
{
text: k.stand?.favourite ? 'Aus Favoriten entfernen' : 'Zu Favoriten',
icon: 'stern',
tun: async () => {
const neuerWert = !k.stand?.favourite;
await favorit(k.id, neuerWert);
if (k.stand) k.stand.favourite = neuerWert;
},
},
];
// Nur anbieten, wenn es dort auch steht — ein Knopf, der nichts tut, ist
// schlechter als keiner.
if (k.stand && !k.stand.watched && k.stand.positionSeconds > 0) {
punkte.push({
text: 'Aus Weiterschauen nehmen',
tun: async () => {
await weglegen(k.id);
if (k.stand) k.stand.dismissed = true;
knopf.querySelector('.fortschritt')?.remove();
},
});
}
punkte.push({ text: 'Details', icon: 'mehr', tun: () => oeffnen(k.art, k.id) });
if (istAdmin()) {
punkte.push({
text: 'Bilder bearbeiten',
icon: 'stift',
tun: () => bilddialog(k.art === 'serien' ? 'series' : 'movies', k.id, k.titel),
});
}
return punkte;
}
/** Setzt oder entfernt den Haken, ohne die Kachel neu zu bauen. */
function hakenZeigen(knopf: HTMLElement, an: boolean): void {
const plakat = knopf.querySelector('.plakat');
if (!plakat) return;
plakat.querySelector('.ecke.haken')?.remove();
knopf.querySelector('.fortschritt')?.remove();
if (!an) return;
const ecke = document.createElement('span');
ecke.className = 'ecke haken';
ecke.title = 'Gesehen';
ecke.innerHTML = icons.haken;
plakat.appendChild(ecke);
}