Ein Klick, der nichts tat, und niemand sagte warum
Eine Serienkachel kannte der Server als Titel nicht: „als gesehen markieren" lief in eine 404, und weil das Kontextmenü die Ablehnung verschluckte, geschah sichtbar gar nichts. Dasselbe beim Favoriten. UserItemState bekommt eine Serienkennung, das Häkchen zieht alle Folgen mit, und was wirft, erscheint als Meldung statt in der Browserkonsole. Dazu eine Mehrfachauswahl in der Mediathek — gesehen, ungesehen, Metadaten neu holen für das Gewählte, in einer Anfrage statt in zweihundert. Ein neues Plakat steht sofort auf der Kachel, und ein Titel, den es nicht mehr gibt, endet nicht mehr in einer Sackgasse. Gemessen: ein POST auf /refresh ohne Rumpf antwortete nach der Erweiterung mit 404 — also genau der Aufruf „alles auffrischen", den es länger gibt als die Auswahl. Der Rumpf wird jetzt von Hand gelesen, und beide Formen stehen in einer Prüfung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c854c4a09a
commit
557708b769
@@ -340,7 +340,7 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
||||
{ text: 'Titel ändern', icon: 'stift',
|
||||
tun: () => titeldialog(art, e.id, e.title, () => void laden()) },
|
||||
{ text: 'Bilder bearbeiten', icon: 'stift',
|
||||
tun: () => bilddialog(art, e.id, e.title) },
|
||||
tun: () => bilddialog(art, e.id, e.title, () => void laden()) },
|
||||
{ text: 'Zuordnung korrigieren', icon: 'suche',
|
||||
// Ohne Jahr — siehe card.ts: das der falschen Zuordnung grenzt falsch ein.
|
||||
tun: () => zuordnungsdialog(art, e.id, e.title, null, () => void laden()) },
|
||||
|
||||
+65
-17
@@ -4,9 +4,12 @@ import { ApiFehler,
|
||||
type Film, type Serie, type Folge, type Datei, type Lieferung, type Stand,
|
||||
type Mitwirkender, aehnliche,
|
||||
} from '../api';
|
||||
import { esc, dauer, groesse, knopf, feld, fehlerzeile, leer, anteil, meldung } from '../util';
|
||||
import {
|
||||
esc, dauer, groesse, knopf, feld, fehlerzeile, leer, anteil, meldung, hinweisZeigen,
|
||||
} from '../util';
|
||||
import { icons, icon } from '../icons';
|
||||
import { kachel, alsKachel, reihe } from '../card';
|
||||
import { vergessen } from '../ansichten';
|
||||
|
||||
type Art = 'filme' | 'serien';
|
||||
|
||||
@@ -41,7 +44,9 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
wurzel.append(
|
||||
hintergrund(eintrag.backdropImage),
|
||||
zurueckKnopf(),
|
||||
kopfbereich(eintrag, null, null),
|
||||
// **Mit Zustand.** Hier stand `null`, und darum wusste die
|
||||
// Knopfreihe einer Serie nie, ob sie abgehakt ist.
|
||||
kopfbereich(eintrag, eintrag.state, null),
|
||||
await staffeln(eintrag),
|
||||
besetzung(eintrag.credits),
|
||||
zuordnungsbereich(eintrag.title, eintrag.year));
|
||||
@@ -49,12 +54,45 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
} catch (fehler) {
|
||||
wurzel.innerHTML = '';
|
||||
// Am Status, nicht am Text: der Text ist jetzt ein deutscher Satz.
|
||||
wurzel.appendChild(fehler instanceof ApiFehler && fehler.status === 404
|
||||
? leer(art === 'filme' ? 'Diesen Film gibt es nicht mehr.' : 'Diese Serie gibt es nicht mehr.')
|
||||
: fehlerzeile(meldung(fehler)));
|
||||
if (fehler instanceof ApiFehler && fehler.status === 404) wurzel.appendChild(verschwunden());
|
||||
else wurzel.appendChild(fehlerzeile(meldung(fehler)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Was dasteht, wenn es den Titel nicht mehr gibt.
|
||||
*
|
||||
* **Vorher war das eine Sackgasse**: ein Satz, sonst nichts. Dabei ist der
|
||||
* häufigste Grund kein Verlust, sondern ein Zusammenlegen — zwei Einträge
|
||||
* derselben Serie wurden beim Scan zu einem, und die Liste, aus der man
|
||||
* gerade kam, zeigte noch die alte Kennung. Der Weg heraus ist, die
|
||||
* gemerkten Listen wegzuwerfen und zurückzugehen; dann steht der Titel
|
||||
* unter seiner neuen Kennung wieder da.
|
||||
*/
|
||||
function verschwunden(): HTMLElement {
|
||||
vergessen();
|
||||
|
||||
const kasten = document.createElement('div');
|
||||
kasten.className = 'leer';
|
||||
|
||||
const satz = document.createElement('p');
|
||||
satz.textContent = art === 'filme'
|
||||
? 'Diesen Film gibt es nicht mehr.'
|
||||
: 'Diese Serie gibt es nicht mehr.';
|
||||
|
||||
const warum = document.createElement('p');
|
||||
warum.className = 'hinweis';
|
||||
warum.textContent = 'Entweder wurde der Eintrag beim letzten Scan mit einem '
|
||||
+ 'anderen zusammengelegt, oder die Dateien dazu sind weg. Die Liste '
|
||||
+ 'wurde aufgefrischt — unter dem neuen Eintrag ist er vermutlich da.';
|
||||
|
||||
const raus = knopf('Zurück zur Liste', 'btn btn-primary');
|
||||
raus.prepend(icon('links'));
|
||||
raus.onclick = zurueck;
|
||||
|
||||
kasten.append(satz, warum, raus);
|
||||
return kasten;
|
||||
}
|
||||
|
||||
function zurueckKnopf(): HTMLElement {
|
||||
const b = knopf('Zurück', 'btn');
|
||||
b.prepend(icon('links'));
|
||||
@@ -216,19 +254,22 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
|
||||
// **Runde Knöpfe ohne Beschriftung, wie bei Plex.** Gesehen, Favorit,
|
||||
// weglegen — drei Dinge, die man oft tut und nie liest.
|
||||
if ('files' in eintrag) {
|
||||
reihe.appendChild(rundKnopf('hakenKreis', stand?.watched ?? false,
|
||||
stand?.watched ? 'Als ungesehen markieren' : 'Als gesehen markieren',
|
||||
async () => { await gesehen(eintrag.id, !stand?.watched); await laden(); }));
|
||||
//
|
||||
// **Auch bei einer Serie.** Vorher stand hier `'files' in eintrag`, und
|
||||
// weil eine Serie keine Dateien hat, fehlte auf ihrer Seite jeder dieser
|
||||
// Knöpfe. Was eine Serie nicht hat, ist eine Stelle — abhaken und zu den
|
||||
// Favoriten legen lässt sie sich sehr wohl.
|
||||
reihe.appendChild(rundKnopf('hakenKreis', stand?.watched ?? false,
|
||||
stand?.watched ? 'Als ungesehen markieren' : 'Als gesehen markieren',
|
||||
async () => { await gesehen(eintrag.id, !stand?.watched); await laden(); }));
|
||||
|
||||
reihe.appendChild(rundKnopf('stern', stand?.favourite ?? false,
|
||||
stand?.favourite ? 'Aus Favoriten nehmen' : 'Zu Favoriten',
|
||||
async () => { await favorit(eintrag.id, !stand?.favourite); await laden(); }));
|
||||
reihe.appendChild(rundKnopf('stern', stand?.favourite ?? false,
|
||||
stand?.favourite ? 'Aus Favoriten nehmen' : 'Zu Favoriten',
|
||||
async () => { await favorit(eintrag.id, !stand?.favourite); await laden(); }));
|
||||
|
||||
if ((stand?.positionSeconds ?? 0) > 0 && !stand?.dismissed) {
|
||||
reihe.appendChild(rundKnopf('mehr', false, 'Aus „Weiterschauen" nehmen',
|
||||
async () => { await weglegen(eintrag.id); await laden(); }));
|
||||
}
|
||||
if ((stand?.positionSeconds ?? 0) > 0 && !stand?.dismissed) {
|
||||
reihe.appendChild(rundKnopf('mehr', false, 'Aus „Weiterschauen" nehmen',
|
||||
async () => { await weglegen(eintrag.id); await laden(); }));
|
||||
}
|
||||
|
||||
const huelle = document.createElement('div');
|
||||
@@ -244,7 +285,14 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
b.innerHTML = icons[bild];
|
||||
b.title = titel;
|
||||
b.setAttribute('aria-label', titel);
|
||||
b.onclick = async () => { b.disabled = true; try { await tun(); } catch { b.disabled = false; } };
|
||||
// **Ein leeres `catch` war hier zu wenig.** Scheiterte die Anfrage, ging
|
||||
// der Knopf wieder an und sonst geschah nichts — ununterscheidbar davon,
|
||||
// dass man danebengeklickt hat.
|
||||
b.onclick = async () => {
|
||||
b.disabled = true;
|
||||
try { await tun(); }
|
||||
catch (fehler) { b.disabled = false; hinweisZeigen(meldung(fehler), 'fehler'); }
|
||||
};
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { inhalt, genresVon, empfehlungen as holeEmpfehlungen, type Bibliothek, type Ordnung } from '../api';
|
||||
import { leer, fehlerzeile, meldung } from '../util';
|
||||
import {
|
||||
inhalt, genresVon, empfehlungen as holeEmpfehlungen, gesehenSammel, metadatenAuffrischen,
|
||||
istAdmin, type Bibliothek, type Ordnung, type Sammelergebnis,
|
||||
} from '../api';
|
||||
import { leer, fehlerzeile, meldung, knopf, hinweisZeigen } from '../util';
|
||||
import { icons } from '../icons';
|
||||
import { kachel, alsKachel, reihe } from '../card';
|
||||
import { kachel, alsKachel, reihe, type Kachel } from '../card';
|
||||
import { beiRueckkehr } from '../ansichten';
|
||||
import { auswahl, type Auswahlfeld } from '../auswahl';
|
||||
|
||||
@@ -156,13 +159,191 @@ 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, suchhuelle);
|
||||
anzahl, zuruecksetzen, auswahlKnopf, suchhuelle);
|
||||
|
||||
const gitter = document.createElement('div');
|
||||
gitter.className = 'raster';
|
||||
ziel.append(zeile, gitter);
|
||||
|
||||
/* ---------- Auswahl ---------- */
|
||||
|
||||
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. */
|
||||
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.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();
|
||||
|
||||
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);
|
||||
}, true);
|
||||
|
||||
/** Die Leiste am unteren Rand — nur sichtbar, solange ausgewählt wird. */
|
||||
function auswahlleiste(): { element: HTMLElement; auffrischen: (zahl: number, an: boolean) => void } {
|
||||
const b = document.createElement('div');
|
||||
b.className = 'auswahlleiste hidden';
|
||||
|
||||
const zahl = document.createElement('span');
|
||||
zahl.className = 'auswahlzahl';
|
||||
|
||||
const knoepfe = document.createElement('div');
|
||||
knoepfe.className = 'knopfreihe';
|
||||
|
||||
/** Baut einen Knopf, der die Auswahl bearbeitet und dabei alle sperrt.
|
||||
*
|
||||
* **Gesperrt, solange es läuft.** Zweimal „Als gesehen" hintereinander
|
||||
* wären zwei Sammelanfragen auf denselben Titeln; die zweite käme
|
||||
* 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;
|
||||
const alle = [...knoepfe.querySelectorAll('button')];
|
||||
alle.forEach(x => { x.disabled = true; });
|
||||
try { hinweisZeigen(await arbeit(ids)); }
|
||||
catch (fehler) { hinweisZeigen(meldung(fehler), 'fehler'); }
|
||||
finally { alle.forEach(x => { x.disabled = false; }); }
|
||||
};
|
||||
return b;
|
||||
};
|
||||
|
||||
knoepfe.append(
|
||||
aktion('Als gesehen', async ids => {
|
||||
const ergebnis = await gesehenSammel(ids, true);
|
||||
nachMarkierung(ids, true);
|
||||
return satzZurMarkierung(ergebnis, true);
|
||||
}),
|
||||
aktion('Als ungesehen', async ids => {
|
||||
const ergebnis = await gesehenSammel(ids, false);
|
||||
nachMarkierung(ids, false);
|
||||
return satzZurMarkierung(ergebnis, false);
|
||||
}));
|
||||
|
||||
// Metadaten holen darf nur, wer die Mediathek verwaltet — der Server
|
||||
// lehnt es sonst ab, und ein Knopf, der nur ablehnt, ist keiner.
|
||||
if (istAdmin()) {
|
||||
knoepfe.appendChild(aktion('Metadaten holen', async ids => {
|
||||
await metadatenAuffrischen(bibliothek.id, ids);
|
||||
return `Metadaten werden für ${anzahlwort(ids.length)} geholt. `
|
||||
+ 'Der Fortschritt steht in der Verwaltung.';
|
||||
}));
|
||||
}
|
||||
|
||||
// **Abwählen bleibt immer anklickbar** — es ist der Weg heraus.
|
||||
const wegKnopf = knopf('Abwählen', 'btn');
|
||||
wegKnopf.onclick = () => auswahlmodusSetzen(false);
|
||||
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; });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Schreibt die Kacheln fort, ohne die Liste neu zu holen.
|
||||
*
|
||||
* **Haken oder Zahl, nie beides.** Wird eine Serie abgehakt, verschwindet
|
||||
* „noch 6 offen"; wird das Häkchen genommen, kommt die Zahl zurück — und
|
||||
* zwar die volle Folgenzahl, denn genau das ist jetzt offen.
|
||||
*/
|
||||
function nachMarkierung(ids: string[], wert: boolean): void {
|
||||
ids.forEach(id => {
|
||||
const k = gitter.querySelector<HTMLElement>(`.kachel[data-id="${CSS.escape(id)}"]`);
|
||||
const plakat = k?.querySelector('.plakat');
|
||||
if (!k || !plakat) return;
|
||||
|
||||
const beschreibung = bekannt.get(id);
|
||||
if (beschreibung) {
|
||||
if (beschreibung.stand) beschreibung.stand.watched = wert;
|
||||
beschreibung.offen = wert ? 0 : (beschreibung.gesamt ?? beschreibung.offen);
|
||||
}
|
||||
|
||||
plakat.querySelector('.ecke')?.remove();
|
||||
k.querySelector('.fortschritt')?.remove();
|
||||
|
||||
if (wert) {
|
||||
const ecke = document.createElement('span');
|
||||
ecke.className = 'ecke haken';
|
||||
ecke.title = 'Gesehen';
|
||||
ecke.innerHTML = icons.haken;
|
||||
plakat.appendChild(ecke);
|
||||
return;
|
||||
}
|
||||
|
||||
const offen = beschreibung?.offen ?? 0;
|
||||
if (offen <= 0) return;
|
||||
const plakette = document.createElement('span');
|
||||
plakette.className = 'ecke zahl';
|
||||
plakette.textContent = String(offen);
|
||||
plakette.title = `${offen} Folgen offen`;
|
||||
plakat.appendChild(plakette);
|
||||
});
|
||||
}
|
||||
|
||||
// Die Genres der Mediathek nachladen — die Liste steht erst, wenn der
|
||||
// Server sie genannt hat, und solange bleibt „Alle Genres" allein.
|
||||
void genresVon(bibliothek.id).then(namen => {
|
||||
@@ -230,7 +411,7 @@ export function mediathek(
|
||||
skip: vonVorn ? 0 : geladen, take: SEITE,
|
||||
});
|
||||
|
||||
if (vonVorn) { gitter.innerHTML = ''; geladen = 0; amEnde = false; }
|
||||
if (vonVorn) { gitter.innerHTML = ''; geladen = 0; amEnde = false; bekannt.clear(); }
|
||||
|
||||
gesamt = seite.total;
|
||||
geladen += seite.items.length;
|
||||
@@ -242,12 +423,27 @@ export function mediathek(
|
||||
// was übrig bleibt, nicht was der Server gefunden hat.
|
||||
const uebrig = gesehen === '' ? seite.items
|
||||
: seite.items.filter(e => {
|
||||
const ist = 'state' in e ? e.state?.watched === true
|
||||
: 'unwatchedCount' in e ? e.unwatchedCount === 0 : false;
|
||||
// **Eine Serie gilt als gesehen, wenn keine Folge mehr offen
|
||||
// ist** — oder wenn der Benutzer sie von Hand abgehakt hat und
|
||||
// die Liste seitdem nicht neu geladen wurde. Eine Serie ganz
|
||||
// ohne Folgen ist nicht gesehen, sondern leer.
|
||||
const ist = e.state?.watched === true
|
||||
|| ('unwatchedCount' in e && e.episodeCount > 0 && e.unwatchedCount === 0);
|
||||
return gesehen === 'gesehen' ? ist : !ist;
|
||||
});
|
||||
|
||||
uebrig.forEach(e => gitter.appendChild(kachel(alsKachel(e), oeffnen)));
|
||||
uebrig.forEach(e => {
|
||||
const beschreibung = alsKachel(e);
|
||||
const k = kachel(beschreibung, oeffnen);
|
||||
// **Die Beschreibung bleibt greifbar.** Nach einer Sammelmarkierung
|
||||
// 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);
|
||||
gitter.appendChild(k);
|
||||
});
|
||||
|
||||
// **Die Zahl sagt, wie viel schon da ist.** „455" über einer Liste mit
|
||||
// 200 Kacheln wäre eine Behauptung, die man beim Scrollen widerlegt.
|
||||
@@ -378,3 +574,24 @@ function farbeAus(name: string): string {
|
||||
for (const z of name) summe = (summe * 31 + z.charCodeAt(0)) % 360;
|
||||
return `linear-gradient(135deg, hsl(${summe} 42% 28%), hsl(${(summe + 40) % 360} 38% 20%))`;
|
||||
}
|
||||
|
||||
/** „1 Titel" / „14 Titel" — die Eins ohne Ziffer liest sich wie ein Satz. */
|
||||
function anzahlwort(n: number): string {
|
||||
return n === 1 ? 'ein Titel' : `${n} Titel`;
|
||||
}
|
||||
|
||||
/** Was nach einer Sammelmarkierung dasteht.
|
||||
*
|
||||
* **Auch das, was nicht ging.** Der Server nennt die Kennungen, zu denen er
|
||||
* nichts fand; sie zu verschweigen hiesse, einen Teilerfolg als vollen
|
||||
* auszugeben.
|
||||
*/
|
||||
function satzZurMarkierung(ergebnis: Sammelergebnis, wert: boolean): string {
|
||||
const was = wert ? 'als gesehen' : 'als ungesehen';
|
||||
const kern = ergebnis.changed === 0
|
||||
? `Nichts geändert — alles war schon ${was} markiert.`
|
||||
: `${anzahlwort(ergebnis.changed)} ${was} markiert.`;
|
||||
return ergebnis.unknown.length === 0
|
||||
? kern
|
||||
: `${kern} ${anzahlwort(ergebnis.unknown.length)} gibt es nicht mehr.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user