Du hast drei Filme vermisst, und die Zahlen sagten schon, wo sie sind: 448 Filme plus 13 zweite Fassungen sind genau die 461 Dateien. Es fehlte keine Datei; drei Filme steckten als Fassung in einem anderen. Gemessen, warum. Der Dateinamenleser ist unschuldig — „Teil 2" bleibt im Titel. TMDB aber liefert für „Heiligtümer des Todes Teil 1" wie für „Teil 2" denselben Treffer zuerst, sobald ohne Jahr gesucht werden muss. Und weil zwei Einträge dieselbe Kennung nicht teilen dürfen, wurde der zweite geschluckt. Zwei Dateien, deren Namen verschiedene Jahre nennen, sind jetzt verschiedene Filme. Der erste Anlauf hatte ein Jahr Abstand als Toleranz erlaubt — für Dezember-Erstaufführungen — und verschluckte damit genau den Fall, um den es geht: Teil 1 ist von 2010, Teil 2 von 2011. Dazu sieht man endlich, was nicht zugeordnet werden konnte. „Nicht zugeordnet" listete bisher nur Dateien ganz ohne Titel, und die gibt es bei Filmen nie — deshalb standen dort nur Serien. Daneben eine Liste der Titel mit mehreren Dateien: zwei Fassungen sind die Regel, zwei verschiedene Filme der Fehler, und an den Dateinamen sieht man es sofort. Der Bearbeiten-Dialog hat jetzt Reiter statt Knöpfe, die weitere Fenster öffnen — ein Fenster über einem Fenster, und man wusste nicht mehr, was man zumacht. „Titel ändern" und „Bilder bearbeiten" sind aus dem Kontextmenü raus; über „Bearbeiten" kommt man an beides. Beim Prüfen gefunden: ein Dialog blieb nach einem Seitenwechsel über der neuen Seite stehen. Er hängt an body, nicht am Inhaltsbereich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
240 lines
8.5 KiB
TypeScript
240 lines
8.5 KiB
TypeScript
import { bildAdresse, bildauswahl, bildSetzen, bildHochladen, type Bildkandidat } from './api';
|
|
import { knopf, fehlerzeile, meldung } from './util';
|
|
import { icons } from './icons';
|
|
import { beimSeitenwechselSchliessen } from './dialog';
|
|
|
|
type Bildart = 'poster' | 'backdrop' | 'logo';
|
|
|
|
const ARTEN: { art: Bildart; titel: string; verhaeltnis: string }[] = [
|
|
{ art: 'poster', titel: 'Plakat', verhaeltnis: '2 / 3' },
|
|
{ art: 'backdrop', titel: 'Hintergrund', verhaeltnis: '16 / 9' },
|
|
{ art: 'logo', titel: 'Schriftzug', verhaeltnis: '16 / 9' },
|
|
];
|
|
|
|
/** Plakat, Hintergrund und Schriftzug von Hand wählen.
|
|
*
|
|
* **Erreichbar aus der Übersicht, nicht erst von der Detailseite.** Bei
|
|
* zwanzig Titeln mit schlechtem Plakat waren das vierzig Seitenwechsel.
|
|
*
|
|
* **Die Kandidaten kosten nichts.** TMDB liefert sie beim Einzelabruf ohnehin
|
|
* mit; bisher wurde alles ausser dem Schriftzug weggeworfen.
|
|
*/
|
|
/** Bilder eines Titels austauschen.
|
|
*
|
|
* **`fertig` wird gerufen, wenn sich wirklich etwas geändert hat** — und erst
|
|
* beim Schliessen, nicht nach jeder einzelnen Wahl. Wer drei Bilder tauscht,
|
|
* löst nicht dreimal einen Neuaufbau der Kachel aus. Wer nur hineinschaut und
|
|
* wieder zumacht, gar keinen.
|
|
*
|
|
* **Warum es den Rückruf überhaupt gibt.** Ohne ihn zeigte die Kachel das
|
|
* alte Plakat weiter, bis man die ganze Oberfläche neu lud — und verlor dabei
|
|
* Bildlauf, Filter und offene Reiter.
|
|
*/
|
|
export function bilddialog(
|
|
art: 'movies' | 'series', id: string, titel: string, fertig?: () => void,
|
|
): void {
|
|
let geaendert = false;
|
|
const d = document.createElement('dialog');
|
|
d.className = 'dlg dlg-breit';
|
|
|
|
const kopf = document.createElement('div');
|
|
kopf.className = 'dlg-kopf';
|
|
const h = document.createElement('h2');
|
|
h.textContent = `Bilder — ${titel}`;
|
|
const zu = document.createElement('button');
|
|
zu.className = 'dlg-zu';
|
|
zu.innerHTML = icons.schliessen;
|
|
zu.setAttribute('aria-label', 'Schliessen');
|
|
zu.onclick = () => d.close();
|
|
kopf.append(h, zu);
|
|
|
|
const koerper = document.createElement('div');
|
|
koerper.className = 'dlg-koerper';
|
|
|
|
d.append(kopf, koerper);
|
|
d.addEventListener('close', () => { d.remove(); if (geaendert) fertig?.(); });
|
|
d.addEventListener('click', e => { if (e.target === d) d.close(); });
|
|
document.body.appendChild(d);
|
|
d.showModal();
|
|
beimSeitenwechselSchliessen(d);
|
|
|
|
bilderflaeche(koerper, art, id, () => { geaendert = true; });
|
|
}
|
|
|
|
/** Dieselbe Bildauswahl, aber in eine beliebige Fläche gezeichnet.
|
|
*
|
|
* **Damit sie als Reiter in „Bearbeiten" stehen kann.**
|
|
*
|
|
* Vorher ging von dort ein zweiter Dialog über dem ersten auf — ein Fenster
|
|
* über einem Fenster, und man wusste nicht mehr, was man eigentlich zumacht.
|
|
* Der eigene Dialog bleibt für alle, die nur die Bilder wollen; er ist jetzt
|
|
* nur noch ein Rahmen um diese Fläche.
|
|
*/
|
|
export function bilderflaeche(
|
|
koerper: HTMLElement,
|
|
art: 'movies' | 'series',
|
|
id: string,
|
|
beiAenderung?: () => void,
|
|
): void {
|
|
koerper.innerHTML = '<div class="spinner"></div>';
|
|
|
|
/** Welcher Reiter offen ist — überlebt das Neuzeichnen nach jeder Wahl. */
|
|
let offen: Bildart = 'poster';
|
|
|
|
void laden();
|
|
|
|
async function laden(): Promise<void> {
|
|
let auswahl;
|
|
try {
|
|
auswahl = await bildauswahl(art, id);
|
|
} catch (fehler) {
|
|
koerper.innerHTML = '';
|
|
koerper.appendChild(fehlerzeile(meldung(fehler)));
|
|
return;
|
|
}
|
|
|
|
koerper.innerHTML = '';
|
|
|
|
if (auswahl.candidates.length === 0) {
|
|
const hinweis = document.createElement('p');
|
|
hinweis.className = 'hinweis';
|
|
// Ohne Zuordnung gibt es nichts zu wählen — dann ist die Zuordnung das
|
|
// Problem, nicht das Bild, und das soll hier auch so dastehen.
|
|
hinweis.textContent = 'Keine Bilder gefunden. Ist der Titel richtig zugeordnet?';
|
|
koerper.appendChild(hinweis);
|
|
}
|
|
|
|
// **Ein Reiter je Bildtyp statt drei Blöcke untereinander.**
|
|
//
|
|
// Vorher scrollte der Dialogkörper, und jedes der drei Raster darin
|
|
// scrollte noch einmal. Zwei Ebenen übereinander heisst: das Rad trifft
|
|
// mal die eine, mal die andere, je nachdem wo der Zeiger steht — und man
|
|
// landet nie dort, wo man hinwollte. Jetzt ist genau ein Typ sichtbar und
|
|
// genau eine Fläche scrollt.
|
|
const reiter = document.createElement('div');
|
|
reiter.className = 'bildreiter';
|
|
reiter.setAttribute('role', 'tablist');
|
|
|
|
const buehne = document.createElement('div');
|
|
buehne.className = 'bildbuehne';
|
|
|
|
koerper.append(reiter, buehne);
|
|
|
|
const bloecke = new Map<Bildart, HTMLElement>();
|
|
|
|
ARTEN.forEach(({ art: bildart, titel: name, verhaeltnis }) => {
|
|
const aktuell = bildart === 'poster' ? auswahl.current.poster
|
|
: bildart === 'backdrop' ? auswahl.current.backdrop
|
|
: auswahl.current.logo;
|
|
|
|
const passende = auswahl.candidates.filter(k => k.kind === bildart);
|
|
|
|
const knopfReiter = document.createElement('button');
|
|
knopfReiter.type = 'button';
|
|
knopfReiter.className = 'bildreiter-knopf' + (bildart === offen ? ' an' : '');
|
|
knopfReiter.setAttribute('role', 'tab');
|
|
knopfReiter.setAttribute('aria-selected', String(bildart === offen));
|
|
// Die Zahl dazu: so sieht man vor dem Klicken, ob es dort etwas gibt.
|
|
knopfReiter.innerHTML = `<span>${name}</span>`
|
|
+ `<span class="winzig">${passende.length}</span>`;
|
|
knopfReiter.onclick = () => { offen = bildart; void laden(); };
|
|
reiter.appendChild(knopfReiter);
|
|
|
|
if (bildart !== offen) return;
|
|
|
|
const abschnitt = document.createElement('section');
|
|
abschnitt.className = 'bildblock';
|
|
|
|
const kopfzeile = document.createElement('div');
|
|
kopfzeile.className = 'knopfreihe';
|
|
const u = document.createElement('h3');
|
|
u.textContent = aktuell ? `${name} — eines ist gesetzt` : name;
|
|
kopfzeile.appendChild(u);
|
|
|
|
// Eigenes Bild — ein verstecktes Dateifeld hinter einem Knopf, weil
|
|
// das native Dateifeld in jedem Browser anders aussieht.
|
|
const feld = document.createElement('input');
|
|
feld.type = 'file';
|
|
feld.accept = 'image/*';
|
|
feld.className = 'hidden';
|
|
const eigenes = knopf('Eigenes Bild…', 'btn');
|
|
eigenes.type = 'button';
|
|
eigenes.onclick = () => feld.click();
|
|
feld.onchange = async () => {
|
|
const datei = feld.files?.[0];
|
|
if (!datei) return;
|
|
eigenes.disabled = true;
|
|
try { await bildHochladen(art, id, bildart, datei); await laden(); }
|
|
catch (fehler) {
|
|
abschnitt.appendChild(fehlerzeile(meldung(fehler)));
|
|
eigenes.disabled = false;
|
|
}
|
|
};
|
|
kopfzeile.append(eigenes, feld);
|
|
abschnitt.appendChild(kopfzeile);
|
|
|
|
if (passende.length === 0 && !aktuell) {
|
|
const leer = document.createElement('p');
|
|
leer.className = 'winzig';
|
|
leer.textContent = 'Nichts zur Auswahl.';
|
|
abschnitt.append(leer);
|
|
buehne.appendChild(abschnitt);
|
|
bloecke.set(bildart, abschnitt);
|
|
return;
|
|
}
|
|
|
|
const gitter = document.createElement('div');
|
|
gitter.className = 'bildgitter';
|
|
gitter.style.setProperty('--verhaeltnis', verhaeltnis);
|
|
|
|
// Das gesetzte Bild zuerst, damit sichtbar ist, wovon man weggeht.
|
|
if (aktuell) gitter.appendChild(kachelFuer(null, bildAdresse(aktuell)!, true, bildart));
|
|
passende.forEach(k => gitter.appendChild(kachelFuer(k, k.thumbUrl, false, bildart)));
|
|
|
|
abschnitt.appendChild(gitter);
|
|
buehne.appendChild(abschnitt);
|
|
bloecke.set(bildart, abschnitt);
|
|
});
|
|
}
|
|
|
|
function kachelFuer(
|
|
kandidat: Bildkandidat | null, adresse: string, gesetzt: boolean, bildart: Bildart,
|
|
): HTMLElement {
|
|
const b = document.createElement('button');
|
|
b.type = 'button';
|
|
b.className = 'bildwahl' + (gesetzt ? ' an' : '');
|
|
b.title = gesetzt ? 'Aktuell gesetzt'
|
|
: [kandidat?.language?.toUpperCase(), kandidat?.vote ? `★ ${kandidat.vote.toFixed(1)}` : null]
|
|
.filter(Boolean).join(' · ') || 'Auswählen';
|
|
|
|
const bild = document.createElement('img');
|
|
bild.src = adresse;
|
|
bild.alt = '';
|
|
bild.loading = 'lazy';
|
|
b.appendChild(bild);
|
|
|
|
if (gesetzt) {
|
|
const marke = document.createElement('span');
|
|
marke.className = 'bildwahl-marke';
|
|
marke.innerHTML = icons.haken;
|
|
b.appendChild(marke);
|
|
b.disabled = true;
|
|
return b;
|
|
}
|
|
|
|
b.onclick = async () => {
|
|
b.disabled = true;
|
|
try {
|
|
await bildSetzen(art, id, bildart, kandidat!.url);
|
|
beiAenderung?.();
|
|
await laden();
|
|
}
|
|
catch (fehler) {
|
|
b.after(fehlerzeile(meldung(fehler)));
|
|
b.disabled = false;
|
|
}
|
|
};
|
|
return b;
|
|
}
|
|
}
|