import { vorschlaege, zuordnen, herkunft, type Zuordnungsstand } from './api'; import { esc, knopf, feld, fehlerzeile, leer, meldung } from './util'; import { icons } from './icons'; import { beimSeitenwechselSchliessen } from './dialog'; /** Einen Titel einem anderen Eintrag der Metadatenquelle zuordnen. * * **Als eigener Dialog und nicht nur auf der Detailseite.** Wer in der * Mediathek zwanzig falsch erkannte Titel vor sich hat, soll sie dort * geradeziehen können — vierzig Seitenwechsel sind keine Bedienung. * * `fertig` wird gerufen, wenn etwas zugeordnet wurde; der Aufrufer frischt * damit auf, was er zeigt. */ export function zuordnungsdialog( art: 'movies' | 'series', id: string, titel: string, jahr: number | null, /** Wird nach einer übernommenen Zuordnung gerufen — mit dem neuen Stand. * * **Der Stand sagt auch, ob zusammengelegt wurde.** Dann gibt es den * korrigierten Eintrag nicht mehr, und wer weiter auf ihn zeigt, bekommt * beim nächsten Handgriff eine 404. */ fertig?: (stand: Zuordnungsstand) => void, ): void { 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 = 'Zuordnung korrigieren'; 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()); d.addEventListener('click', e => { if (e.target === d) d.close(); }); document.body.appendChild(d); d.showModal(); beimSeitenwechselSchliessen(d); zuordnungsflaeche(koerper, art, id, titel, jahr, stand => { d.close(); fertig?.(stand); }); } /** Dieselbe Zuordnungssuche, aber in eine beliebige Fläche gezeichnet. * * **Damit sie als Reiter in „Bearbeiten" stehen kann** — statt als zweiter * Dialog über dem ersten. Der eigene Dialog bleibt für alle, die nur die * Zuordnung wollen; er ist jetzt nur noch ein Rahmen um diese Fläche. * * `fertig` wird nach einer übernommenen Zuordnung gerufen. Ob daraufhin etwas * geschlossen wird, entscheidet der Aufrufer — in einem Reiter wäre das * falsch, in einem eigenen Dialog richtig. */ export function zuordnungsflaeche( koerper: HTMLElement, art: 'movies' | 'series', id: string, titel: string, jahr: number | null, fertig?: (stand: Zuordnungsstand) => void, ): void { koerper.innerHTML = ''; // **Woher der Titel stammt, über allem anderen.** // // Liegt die Zuordnung völlig daneben, sagt der angezeigte Titel nichts mehr // darüber, welche Datei hier eigentlich gemeint ist — „Dark Shadows" von // 1966 kann alles sein. Der Ordner sagt es in einer Zeile. const quelle = document.createElement('div'); quelle.className = 'quelle'; quelle.innerHTML = 'Herkunft wird gelesen…'; void herkunft(art, id).then(h => { quelle.innerHTML = ''; const ordner = document.createElement('div'); ordner.className = 'quelle-pfad'; ordner.textContent = h.ordner ?? 'Kein gemeinsamer Ordner'; ordner.title = h.ordner ?? ''; quelle.appendChild(ordner); const dazu = document.createElement('span'); dazu.className = 'winzig'; const namen = h.beispiele.join(' · '); dazu.textContent = h.dateien === 0 ? 'Keine Datei mehr zugeordnet.' : `${h.dateien} ${h.dateien === 1 ? 'Datei' : 'Dateien'}` + (namen ? ` — ${namen}${h.dateien > h.beispiele.length ? ' …' : ''}` : ''); quelle.appendChild(dazu); }).catch(() => { quelle.innerHTML = 'Herkunft nicht lesbar.'; }); const hinweis = document.createElement('p'); hinweis.className = 'hinweis'; hinweis.textContent = 'Der gewählte Eintrag ersetzt Titel, Beschreibung, Bilder ' + 'und Besetzung. Von Hand gewählte Bilder bleiben.'; const suche = feld('Anderer Suchbegriff'); suche.value = titel; // Leer heisst: der Server nimmt das Jahr aus dem Dateinamen. Hier etwas // vorzutragen, das aus der zu korrigierenden Zuordnung stammt, hiesse die // Suche auf genau den falschen Jahrgang einzugrenzen. // **Leer heisst: unbekannt.** Das Jahr aus dem Dateinamen steht schon // drin; wer es herausnimmt, sucht ohne Jahr. Der Server fiel hier // früher auf eben dieses Jahr zurück — das leere Feld hatte damit // keine Wirkung. const jahrfeld = feld('Jahr — leer: unbekannt', 'number'); if (jahr) jahrfeld.value = String(jahr); const suchen = knopf('Suchen', 'btn'); const reihe = document.createElement('div'); reihe.className = 'knopfreihe'; reihe.append(suche, jahrfeld, suchen); const treffer = document.createElement('div'); treffer.className = 'einstellungen'; koerper.append(quelle, hinweis, reihe, treffer); // Enter im Suchfeld sucht, statt den Dialog zu schliessen. suche.onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); void fragen(); } }; suchen.onclick = () => void fragen(); void fragen(); async function fragen(): Promise { treffer.innerHTML = '
'; try { const liste = await vorschlaege(art, id, suche.value.trim(), jahrfeld.value ? Number(jahrfeld.value) : undefined); treffer.innerHTML = ''; if (liste.length === 0) { treffer.appendChild(leer('Nichts gefunden.')); return; } const zeilen: HTMLButtonElement[] = []; liste.forEach(v => { const zeile = document.createElement('button'); zeile.className = 'vorschlag'; // **Mit Plakat.** „Dark" von 2017 und „Dark Shadows" von 1966 sind an // einer Textzeile kaum zu unterscheiden, am Plakat in einer // Zehntelsekunde. Der Server liefert die Adresse längst mit; sie wurde // bisher weggeworfen. const bild = document.createElement('div'); bild.className = 'vorschlag-bild'; if (v.posterUrl) { const img = document.createElement('img'); img.src = v.posterUrl; img.alt = ''; img.loading = 'lazy'; img.onload = () => bild.classList.add('geladen'); bild.appendChild(img); } const text = document.createElement('div'); text.className = 'vorschlag-text'; text.innerHTML = `
${esc(v.title)}` + `${esc(v.year ?? '')} · TMDB ${v.tmdbId}
` + `
${esc((v.overview ?? '').slice(0, 160))}
`; zeile.append(bild, text); zeile.onclick = async () => { // **Sichtbar machen, dass etwas passiert.** // // Ein Klick löste einen Abruf bei TMDB aus, der Bilder nachlädt und // mehrere Sekunden braucht — und die Oberfläche sah aus wie vorher. // Wer nichts sieht, klickt noch einmal, und dann laufen zwei. zeilen.forEach(z => { z.disabled = true; }); zeile.classList.add('laeuft'); const lauf = document.createElement('span'); lauf.className = 'vorschlag-lauf'; lauf.innerHTML = 'Wird zugeordnet…'; zeile.appendChild(lauf); try { const stand = await zuordnen(art, id, v.tmdbId); // Erst melden, dann schliessen: sonst blitzt nur der Dialog weg, // und es bleibt offen, ob es geklappt hat. lauf.innerHTML = stand.merged ? 'Übernommen und mit dem vorhandenen Eintrag zusammengelegt.' : 'Übernommen.'; fertig?.(stand); } catch (fehler) { lauf.remove(); zeile.classList.remove('laeuft'); zeilen.forEach(z => { z.disabled = false; }); treffer.appendChild(fehlerzeile(meldung(fehler))); } }; zeilen.push(zeile); treffer.appendChild(zeile); }); } catch (fehler) { treffer.innerHTML = ''; treffer.appendChild(fehlerzeile(meldung(fehler))); } } }