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 = '
'; /** Welcher Reiter offen ist — überlebt das Neuzeichnen nach jeder Wahl. */ let offen: Bildart = 'poster'; void laden(); async function laden(): Promise { 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(); 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 = `${name}` + `${passende.length}`; 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; } }