**In „Fortsetzen" startet ein Tipp.** Dort steht ein Titel, *weil* man ihn angefangen hat, und die Stelle steht daneben — der Umweg über die Detailseite stellte eine Frage, die schon beantwortet war. Überall sonst bleibt es beim Weg auf die Seite: wer nachsehen will, wie viele Staffeln eine Serie hat, soll nicht den Film bekommen. **Ein Haken nimmt die Kachel mit.** In „Fortsetzen" und „Als Nächstes" steht, was offen ist. Eine abgehakte Folge ist nicht mehr offen — sie mit einem Haken stehen zu lassen sah aus, als hätte das Abhaken nicht gewirkt. Dasselbe für „Aus Fortsetzen entfernen": in der Reihe selbst heisst entfernen genau das. **Und ein getauschtes Bild erreicht den Aufmacher.** Wer auf der Startseite ein Plakat wechselt, sieht denselben Titel gleich darüber in voller Breite — dort stand bis zum nächsten Neuladen das alte. Die Kachel frischt sich selbst auf; der Aufmacher steht ganz woanders und erfuhr nichts davon. Jetzt meldet der Bilderdialog es der ganzen Seite, und wer es angeht, hört zu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
506 lines
19 KiB
TypeScript
506 lines
19 KiB
TypeScript
import {
|
|
bildAdresse, gesehen, favorit, weglegen, istAdmin, film, serie,
|
|
type Film, type Serie, type Angefangen, type Stand,
|
|
} from './api';
|
|
import { esc, anteil, hinweisZeigen } from './util';
|
|
import { icons } from './icons';
|
|
import { kontextmenue, menueAnZiel, type Menuepunkt } from './menue';
|
|
import { zuordnungsdialog } from './zuordnung';
|
|
import { bearbeiten } from './bearbeiten';
|
|
|
|
/** Was eine Kachel zeigt — unabhängig davon, woher sie kommt. */
|
|
export type Kachel = {
|
|
/** Wohin der Klick führt — bei einer Folge die Serie, zu der sie gehört. */
|
|
id: string;
|
|
/** Welche Kennung den Sehstand trägt. Fehlt sie, ist es `id`.
|
|
*
|
|
* **Nur dort verschieden, wo die Kachel woandershin führt als sie misst:**
|
|
* eine Folge aus „Fortsetzen" öffnet die Serie, abgehakt wird aber die
|
|
* Folge. */
|
|
standId?: string;
|
|
/** Aus welcher Reihe die Kachel stammt — nur dort, wo es etwas ändert.
|
|
*
|
|
* **Bisher nur für „Als Nächstes".** Dort heisst Weglegen etwas anderes
|
|
* als in „Fortsetzen": nicht diese Folge, sondern diese Serie. */
|
|
reihe?: 'naechste' | 'fortsetzen';
|
|
/** Was ein Tipp auslöst, wenn es nicht die Detailseite sein soll.
|
|
*
|
|
* **Nur in „Fortsetzen".** Dort ist die Absicht eindeutig: der Titel steht
|
|
* in der Reihe, *weil* man ihn angefangen hat, und die Stelle steht
|
|
* daneben. Überall sonst führt der Tipp auf die Seite — wer nur nachsehen
|
|
* will, wie viele Staffeln eine Serie hat, soll nicht den Film bekommen. */
|
|
starten?: () => void;
|
|
art: 'filme' | 'serien';
|
|
/** Die obere, fette Zeile. Bei einer Folge die Serie. */
|
|
titel: string;
|
|
/** Die zweite Zeile. Bei einer Folge der Folgentitel, sonst das Jahr. */
|
|
zweite?: string | null;
|
|
/** Die dritte Zeile — „S14 · F22". Nur bei Folgen. */
|
|
dritte?: string | null;
|
|
bild: string | null;
|
|
stand?: Stand | null;
|
|
/** Die Zahl offener Folgen, als Plakette oben rechts. */
|
|
offen?: number | null;
|
|
/** Wie viele Folgen es überhaupt sind — um die Plakette wiederherzustellen. */
|
|
gesamt?: number | null;
|
|
/** Wonach einsortiert wird. **Damit die Kachel weiss, wohin sie gehört**,
|
|
* wenn sich der Titel ändert — sonst stand „WALL·E" nach der Korrektur
|
|
* weiter unter E, bis man die Seite neu lud. */
|
|
sortTitel?: string | null;
|
|
/** `null` heisst: wartet auf Freigabe. Ein Zeitpunkt in der Zukunft: geplant. */
|
|
freigabe?: string | null;
|
|
};
|
|
|
|
/* ---------- Umwandlungen ---------- */
|
|
|
|
export const alsKachel = (e: Film | Serie): Kachel => ({
|
|
id: e.id,
|
|
art: 'episodeCount' in e ? 'serien' : 'filme',
|
|
titel: e.title,
|
|
zweite: e.year ? String(e.year) : null,
|
|
bild: e.posterImage,
|
|
stand: e.state,
|
|
offen: 'unwatchedCount' in e ? e.unwatchedCount : null,
|
|
gesamt: 'episodeCount' in e ? e.episodeCount : null,
|
|
sortTitel: e.sortTitle,
|
|
freigabe: e.publishAt,
|
|
});
|
|
|
|
/** Eine Kachel aus „Fortsetzen" oder „Nächste Folge" — bei Folgen
|
|
* dreizeilig, wie bei Plex.
|
|
*
|
|
* **Die Kachel führt zur Serie, der Sehstand bleibt bei der Folge.**
|
|
*
|
|
* Hier stand die Folgenkennung, und die Kachel öffnete damit eine Serienseite
|
|
* — die es unter dieser Kennung nie gibt. Heraus kam „Diese Serie gibt es
|
|
* nicht mehr", ausgerechnet unter „Nächste Folge", wo man gerade
|
|
* weiterschauen wollte. Eine eigene Seite je Folge gibt es nicht; die Staffeln
|
|
* stehen auf der Serienseite, und dorthin gehört der Klick.
|
|
*
|
|
* **Beides auf dieselbe Kennung zu legen wäre der nächste Fehler gewesen:**
|
|
* „Als gesehen markieren" hätte dann die ganze Serie abgehakt statt der
|
|
* einen Folge, die man gerade zur Hälfte gesehen hat.
|
|
*/
|
|
export const angefangenAlsKachel = (e: Angefangen, reihe?: Kachel['reihe']): Kachel =>
|
|
e.kind === 'episode'
|
|
? {
|
|
id: e.seriesId ?? e.id, art: 'serien',
|
|
standId: e.id,
|
|
titel: e.title,
|
|
zweite: e.subTitle,
|
|
dritte: `S${e.season} · F${e.episode}`,
|
|
bild: e.posterImage, stand: e.state, reihe,
|
|
}
|
|
: {
|
|
id: e.id, art: 'filme',
|
|
titel: e.title,
|
|
zweite: e.year ? String(e.year) : null,
|
|
bild: e.posterImage, stand: e.state, reihe,
|
|
};
|
|
|
|
/* ---------- Die Kachel selbst ---------- */
|
|
|
|
/** Baut eine Plakatkachel.
|
|
*
|
|
* **Alles auf einer Kachel steht an einer Stelle**, weil sie an fünf Stellen
|
|
* vorkommt — Startseite, Mediathek, Vorschläge, Suche, Staffeln. Jede davon
|
|
* eigen zu zeichnen hiesse: der Haken für Gesehenes erscheint an vier
|
|
* Stellen und an der fünften nicht, und niemand weiss, warum. */
|
|
export function kachel(k: Kachel, oeffnen: (art: 'filme' | 'serien', id: string) => void): HTMLElement {
|
|
const knopf = document.createElement('button');
|
|
knopf.className = 'kachel';
|
|
// **Die Kennung steht am Knoten.** So kann ein Auswahlmodus über dem Raster
|
|
// arbeiten, ohne dass jede der fünf Stellen, die Kacheln baut, davon wissen
|
|
// muss — sie liest sie vom angeklickten Element ab.
|
|
knopf.dataset.id = k.id;
|
|
knopf.dataset.art = k.art;
|
|
knopf.onclick = () => (k.starten ? k.starten() : 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));
|
|
|
|
// **Ein sichtbarer Griff zum selben Menü.**
|
|
//
|
|
// Rechtsklick findet nur, wer ihn vermutet, und auf einem Telefon gibt es
|
|
// ihn gar nicht. Die drei Punkte erscheinen beim Überfahren und bei
|
|
// Tastaturfokus — sie sind ein eigener Knopf, nicht Teil der Kachel, sonst
|
|
// öffnete jeder Klick darauf auch die Detailseite.
|
|
const mehr = document.createElement('span');
|
|
mehr.className = 'kachel-mehr';
|
|
mehr.setAttribute('role', 'button');
|
|
mehr.setAttribute('tabindex', '0');
|
|
mehr.setAttribute('aria-label', `Mehr zu „${k.titel}“`);
|
|
mehr.innerHTML = icons.punkte;
|
|
mehr.onclick = e => {
|
|
e.stopPropagation();
|
|
menueAnZiel(mehr, kachelpunkte(k, knopf, oeffnen));
|
|
};
|
|
mehr.onkeydown = e => {
|
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
menueAnZiel(mehr, kachelpunkte(k, knopf, oeffnen));
|
|
};
|
|
|
|
const rahmen = document.createElement('div');
|
|
rahmen.className = 'plakat';
|
|
rahmen.setAttribute('role', 'img');
|
|
rahmen.setAttribute('aria-label', k.titel);
|
|
|
|
const adresse = bildAdresse(k.bild);
|
|
if (adresse) {
|
|
const bild = document.createElement('img');
|
|
bild.src = adresse;
|
|
bild.alt = '';
|
|
bild.loading = 'lazy';
|
|
// **Kein Ziehen des Bildes.** Browser machen aus jedem Bild von sich aus
|
|
// ein Ziehgut; wer über die Kacheln streicht, um mehrere auszuwählen,
|
|
// schleppte sonst ein halbdurchsichtiges Plakat hinter sich her.
|
|
bild.draggable = false;
|
|
// Erst einblenden, wenn es da ist — sonst blitzt der Platzhalter durch.
|
|
bild.onload = () => rahmen.classList.add('geladen');
|
|
rahmen.appendChild(bild);
|
|
}
|
|
|
|
// **Was noch wartet, trägt ein Band.**
|
|
//
|
|
// Für einen Administrator stehen wartende Titel mitten zwischen den
|
|
// freigegebenen, ohne dass etwas sie unterscheidet — und wer einen davon
|
|
// sucht, findet ihn nur über die Prüfliste. Für alle anderen ist er gar
|
|
// nicht da, dann erscheint das Band auch nie.
|
|
if (k.freigabe === null || (k.freigabe && new Date(k.freigabe) > new Date())) {
|
|
const band = document.createElement('span');
|
|
band.className = 'wartet';
|
|
band.textContent = k.freigabe === null ? 'Wartet' : 'Geplant';
|
|
band.title = k.freigabe === null
|
|
? 'Wartet auf Freigabe — nur für Administratoren sichtbar'
|
|
: `Erscheint am ${new Date(k.freigabe).toLocaleString('de-CH',
|
|
{ dateStyle: 'medium', timeStyle: 'short' })}`;
|
|
rahmen.appendChild(band);
|
|
}
|
|
|
|
// **Der Haken sitzt in der Ecke, nicht in der Beschriftung.** So wie bei
|
|
// Plex: ein Blick über das Raster zeigt, was noch offen ist, ohne eine
|
|
// einzige Zeile zu lesen.
|
|
if (k.stand?.watched) {
|
|
const haken = document.createElement('span');
|
|
haken.className = 'ecke haken';
|
|
haken.innerHTML = icons.haken;
|
|
haken.title = 'Gesehen';
|
|
rahmen.appendChild(haken);
|
|
} else if (k.offen != null && k.offen > 0) {
|
|
const plakette = document.createElement('span');
|
|
plakette.className = 'ecke zahl';
|
|
plakette.textContent = String(k.offen);
|
|
plakette.title = `${k.offen} Folgen offen`;
|
|
rahmen.appendChild(plakette);
|
|
}
|
|
|
|
// Der Balken unten — ab der ersten Sekunde, mit Mindestbreite, sonst wären
|
|
// zwei Minuten eines Zweistünders anderthalb Bildpunkte.
|
|
const wie = anteil(k.stand);
|
|
if (wie != null && wie > 0 && !k.stand?.watched) {
|
|
const balken = document.createElement('div');
|
|
balken.className = 'fortschritt';
|
|
const gefuellt = document.createElement('div');
|
|
gefuellt.style.width = `${Math.min(100, wie * 100)}%`;
|
|
balken.appendChild(gefuellt);
|
|
rahmen.appendChild(balken);
|
|
}
|
|
|
|
// Beim Überfahren ein Abspielknopf in der Mitte — auch das von Plex.
|
|
const schweben = document.createElement('span');
|
|
schweben.className = 'schweber';
|
|
schweben.innerHTML = icons.abspielen;
|
|
rahmen.appendChild(schweben);
|
|
rahmen.appendChild(mehr);
|
|
|
|
knopf.appendChild(rahmen);
|
|
|
|
const text = document.createElement('div');
|
|
text.className = 'kacheltext';
|
|
text.innerHTML =
|
|
`<strong>${esc(k.titel)}</strong>`
|
|
+ (k.zweite ? `<span>${esc(k.zweite)}</span>` : '')
|
|
+ (k.dritte ? `<span class="dritte">${esc(k.dritte)}</span>` : '');
|
|
knopf.appendChild(text);
|
|
|
|
return knopf;
|
|
}
|
|
|
|
/** Eine waagerechte Reihe mit Überschrift und Pfeilen. */
|
|
export function reihe(
|
|
titel: string,
|
|
kacheln: HTMLElement[],
|
|
mehr?: { text: string; tun: () => void },
|
|
): HTMLElement {
|
|
const bereich = document.createElement('section');
|
|
bereich.className = 'reihe';
|
|
|
|
const kopf = document.createElement('div');
|
|
kopf.className = 'reihe-kopf';
|
|
|
|
const h = document.createElement('h2');
|
|
h.textContent = titel;
|
|
kopf.appendChild(h);
|
|
|
|
const streifen = document.createElement('div');
|
|
streifen.className = 'strip';
|
|
kacheln.forEach(k => streifen.appendChild(k));
|
|
|
|
// **Pfeile, weil eine Reihe mit zwanzig Kacheln nicht in den Bildschirm
|
|
// passt.** Scrollen mit dem Rad geht quer nur mit Umschalttaste, und ein
|
|
// Trackpad hat nicht jeder.
|
|
const steuerung = document.createElement('div');
|
|
steuerung.className = 'reihe-pfeile';
|
|
(['links', 'rechts'] as const).forEach(richtung => {
|
|
const p = document.createElement('button');
|
|
p.className = 'pfeil';
|
|
p.innerHTML = icons[richtung];
|
|
p.setAttribute('aria-label', richtung === 'links' ? 'Zurück' : 'Weiter');
|
|
p.onclick = () => streifen.scrollBy({
|
|
left: (richtung === 'links' ? -1 : 1) * Math.max(240, streifen.clientWidth * 0.8),
|
|
behavior: 'smooth',
|
|
});
|
|
steuerung.appendChild(p);
|
|
});
|
|
|
|
if (mehr) {
|
|
const m = document.createElement('button');
|
|
m.className = 'mehr';
|
|
m.textContent = mehr.text;
|
|
m.onclick = mehr.tun;
|
|
kopf.appendChild(m);
|
|
}
|
|
kopf.appendChild(steuerung);
|
|
|
|
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 () => {
|
|
// **Die Antwort des Servers gilt, nicht die Annahme hier.** Bei einer
|
|
// Serie zieht das Häkchen alle Folgen mit; was dabei herauskommt,
|
|
// weiss nur der Server.
|
|
k.stand = await gesehen(k.standId ?? k.id, !istGesehen);
|
|
|
|
// **In „Fortsetzen" und „Als Nächstes" ist ein Haken ein Abschied.**
|
|
//
|
|
// Beide Reihen zeigen, was offen ist. Eine abgehakte Folge ist nicht
|
|
// mehr offen — sie mit einem Haken stehen zu lassen sah aus, als
|
|
// hätte das Abhaken nicht gewirkt. Der Server lieferte beim nächsten
|
|
// Laden längst das Richtige; nur sah man das nie, weil die Reihe
|
|
// nicht neu geladen wurde.
|
|
if (k.reihe && !istGesehen) {
|
|
knopf.remove();
|
|
return;
|
|
}
|
|
|
|
// **Sonst spiegelt die Kachel den neuen Zustand selbst.** Die Liste
|
|
// neu zu laden verlöre Bildlauf und Filter — für einen Haken.
|
|
k.offen = istGesehen ? (k.gesamt ?? k.offen) : 0;
|
|
hakenZeigen(knopf, k, !istGesehen);
|
|
},
|
|
},
|
|
{
|
|
text: k.stand?.favourite ? 'Aus Favoriten entfernen' : 'Zu Favoriten',
|
|
icon: 'stern',
|
|
tun: async () => {
|
|
const neuerWert = !k.stand?.favourite;
|
|
k.stand = await favorit(k.standId ?? k.id, 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({
|
|
// **Dieselbe Reihe, derselbe Name.** Oben stand „Fortsetzen", hier
|
|
// „Weiterschauen" — zwei Wörter für eine Sache, und der Knopf sah
|
|
// danach aus, als meinte er etwas anderes.
|
|
text: 'Aus Fortsetzen entfernen',
|
|
tun: async () => {
|
|
await weglegen(k.standId ?? k.id);
|
|
// In der Reihe selbst heisst „entfernen" genau das.
|
|
if (k.reihe === 'fortsetzen') { knopf.remove(); return; }
|
|
if (k.stand) k.stand.dismissed = true;
|
|
knopf.querySelector('.fortschritt')?.remove();
|
|
},
|
|
});
|
|
}
|
|
|
|
// **In „Als Nächstes" wird die Serie weggelegt, nicht die Folge.**
|
|
//
|
|
// Eine einzelne Folge dort wegzulegen nützt nichts: beim nächsten Laden
|
|
// rückt die übernächste nach. Gemeint ist „diese Serie nicht mehr
|
|
// vorschlagen" — und `k.id` ist bei einer Folgenkachel ohnehin die Serie.
|
|
if (k.reihe === 'naechste') {
|
|
punkte.push({
|
|
text: 'Serie aus Als Nächstes entfernen',
|
|
tun: async () => {
|
|
await weglegen(k.id);
|
|
knopf.remove();
|
|
},
|
|
});
|
|
}
|
|
|
|
punkte.push({ text: 'Details', icon: 'mehr', tun: () => oeffnen(k.art, k.id) });
|
|
|
|
if (istAdmin()) {
|
|
const art = k.art === 'serien' ? 'series' : 'movies';
|
|
|
|
// **Ein Ort für alles, und die Abkürzungen bleiben.** Titel, Sortiertitel,
|
|
// Bilder und Zuordnung gehören zusammen: wer einen Film geraderückt, macht
|
|
// selten nur eines davon. Wer weiss, was er will, nimmt weiter den
|
|
// einzelnen Punkt darunter.
|
|
punkte.push({
|
|
text: 'Bearbeiten',
|
|
icon: 'stift',
|
|
tun: () => bearbeiten(art, k.id, ergebnis => {
|
|
if (ergebnis.verschmolzen) {
|
|
knopf.remove();
|
|
hinweisZeigen(`„${ergebnis.titel}" wurde mit einem vorhandenen Eintrag zusammengelegt.`);
|
|
return;
|
|
}
|
|
// Auch ein getauschtes Plakat kommt damit mit — `kachelAuffrischen`
|
|
// holt den Eintrag ohnehin frisch.
|
|
void kachelAuffrischen(k, knopf);
|
|
}),
|
|
});
|
|
|
|
// **Nur noch diese eine Abkürzung.** Titel und Bilder standen zusätzlich
|
|
// im Menü, seit es „Bearbeiten" gibt aber doppelt — und ein Menü mit zwei
|
|
// Wegen zur selben Sache ist eines zu viel. Die Zuordnung bleibt, weil
|
|
// sie der häufigste Grund ist, überhaupt etwas anzufassen.
|
|
punkte.push({
|
|
text: 'Zuordnung korrigieren',
|
|
icon: 'suche',
|
|
// **Hier und nicht nur auf der Detailseite.** Wer zwanzig falsch
|
|
// erkannte Titel vor sich hat, soll sie hier geradeziehen können.
|
|
// **Ohne Jahr.** Das angezeigte stammt aus der Zuordnung, die gerade
|
|
// korrigiert werden soll — bei einem falschen Treffer grenzt es die
|
|
// Suche auf genau das falsche Jahr ein. Ohne Angabe nimmt der Server
|
|
// das aus dem Dateinamen.
|
|
tun: () => zuordnungsdialog(art, k.id, k.titel, null, stand => {
|
|
// **Zusammengelegt heisst: diesen Eintrag gibt es nicht mehr.**
|
|
//
|
|
// Vorher blieb die Kachel stehen und zeigte auf einen gelöschten
|
|
// Eintrag; wer sie dann bearbeiten wollte, bekam „Not Found" — eine
|
|
// englische Meldung für etwas, das gar kein Fehler war. Der
|
|
// überlebende Eintrag steht ohnehin schon in der Liste.
|
|
if (stand.merged) {
|
|
knopf.remove();
|
|
hinweisZeigen(`„${stand.title}" wurde mit dem vorhandenen Eintrag zusammengelegt.`);
|
|
return;
|
|
}
|
|
|
|
// **Nur die Kachel, nicht die Seite.** `location.reload()` warf
|
|
// Bildlauf, Filter und offene Reiter weg — für einen Titel, der sich
|
|
// geändert hat. Bei zwanzig Korrekturen hintereinander ist das
|
|
// zwanzigmal von vorn anfangen.
|
|
void kachelAuffrischen(k, knopf);
|
|
}),
|
|
});
|
|
}
|
|
|
|
return punkte;
|
|
}
|
|
|
|
/** Holt einen Titel neu und schreibt Beschriftung und Plakat fort.
|
|
*
|
|
* **Warum nicht die ganze Liste.** Nach einer Korrektur ändert sich genau ein
|
|
* Eintrag. Alles neu zu laden kostet einen Netzabruf über mehrere hundert
|
|
* Titel und setzt die Ansicht an den Anfang zurück.
|
|
*/
|
|
export async function kachelAuffrischen(k: Kachel, knopf: HTMLElement): Promise<void> {
|
|
let frisch: Film | Serie;
|
|
try { frisch = k.art === 'serien' ? await serie(k.id) : await film(k.id); }
|
|
catch { return; } // Still bleiben: die alte Kachel ist besser als eine Meldung.
|
|
|
|
const vorher = k.sortTitel;
|
|
|
|
k.titel = frisch.title;
|
|
k.zweite = frisch.year ? String(frisch.year) : null;
|
|
k.bild = frisch.posterImage;
|
|
k.sortTitel = frisch.sortTitle;
|
|
|
|
// **Die Liste erfährt, dass die Kachel woandershin gehört.** Sie allein
|
|
// weiss, ob der neue Platz überhaupt geladen ist.
|
|
if (vorher !== k.sortTitel) {
|
|
knopf.dispatchEvent(new CustomEvent('kachel-umsortiert', {
|
|
bubbles: true,
|
|
detail: { id: k.id, sortTitel: k.sortTitel, titel: k.titel },
|
|
}));
|
|
}
|
|
|
|
const text = knopf.querySelector('.kacheltext');
|
|
if (text) {
|
|
text.innerHTML = `<strong>${esc(k.titel)}</strong>`
|
|
+ (k.zweite ? `<span>${esc(k.zweite)}</span>` : '');
|
|
}
|
|
|
|
const plakat = knopf.querySelector('.plakat');
|
|
const adresse = bildAdresse(k.bild);
|
|
plakat?.querySelector('img')?.remove();
|
|
plakat?.classList.remove('geladen');
|
|
if (plakat && adresse) {
|
|
const bild = document.createElement('img');
|
|
bild.src = adresse;
|
|
bild.alt = '';
|
|
bild.draggable = false;
|
|
bild.onload = () => plakat.classList.add('geladen');
|
|
plakat.prepend(bild);
|
|
}
|
|
}
|
|
|
|
/** Setzt oder entfernt den Haken, ohne die Kachel neu zu bauen.
|
|
*
|
|
* **Die Ecke zeigt eines von beidem.** Haken oder Zahl offener Folgen — beides
|
|
* nebeneinander wäre ein Widerspruch. Wird eine Serie abgehakt, geht die Zahl;
|
|
* wird das Häkchen genommen, kommt sie zurück.
|
|
*/
|
|
function hakenZeigen(knopf: HTMLElement, k: Kachel, an: boolean): void {
|
|
const plakat = knopf.querySelector('.plakat');
|
|
if (!plakat) return;
|
|
|
|
plakat.querySelector('.ecke')?.remove();
|
|
knopf.querySelector('.fortschritt')?.remove();
|
|
|
|
if (an) {
|
|
const ecke = document.createElement('span');
|
|
ecke.className = 'ecke haken';
|
|
ecke.title = 'Gesehen';
|
|
ecke.innerHTML = icons.haken;
|
|
plakat.appendChild(ecke);
|
|
return;
|
|
}
|
|
|
|
if (k.offen == null || k.offen <= 0) return;
|
|
const plakette = document.createElement('span');
|
|
plakette.className = 'ecke zahl';
|
|
plakette.textContent = String(k.offen);
|
|
plakette.title = `${k.offen} Folgen offen`;
|
|
plakat.appendChild(plakette);
|
|
}
|