import { inhalt, genresVon, empfehlungen as holeEmpfehlungen, type Bibliothek, type Ordnung } from '../api'; import { leer, fehlerzeile } from '../util'; import { kachel, alsKachel, reihe } from '../card'; type Art = 'filme' | 'serien'; type Reiter = 'empfehlungen' | 'mediathek' | 'kategorien'; /** Eine Mediathek — mit Reitern, wie bei Plex. * * **Drei Sichten auf denselben Bestand.** „Empfehlungen" ist das, womit man * ankommt; „Mediathek" ist das vollständige Raster für alle, die etwas * Bestimmtes suchen; „Kategorien" ist der Weg hinein, wenn man noch nicht * weiss, wonach. Alles in eine Ansicht zu legen hiesse, zwei davon unter ein * Ausklappmenü zu schieben. */ export function mediathek( bibliothek: Bibliothek, servername: string, reiter: string, oeffnen: (art: Art, id: string) => void, ): HTMLElement { const wurzel = document.createElement('div'); const welcher: Reiter = reiter === 'empfehlungen' || reiter === 'kategorien' ? reiter : 'mediathek'; wurzel.appendChild(kopf()); const buehne = document.createElement('div'); wurzel.appendChild(buehne); if (welcher === 'empfehlungen') empfehlungen(buehne); else if (welcher === 'kategorien') kategorien(buehne); else raster(buehne); /* ---------- Kopf mit Reitern ---------- */ function kopf(): HTMLElement { const k = document.createElement('div'); k.className = 'seitenkopf'; const wer = document.createElement('div'); wer.className = 'wer'; const h = document.createElement('h1'); h.textContent = bibliothek.name; const quelle = document.createElement('div'); quelle.className = 'quelle'; quelle.textContent = servername; wer.append(h, quelle); const reiterleiste = document.createElement('nav'); reiterleiste.className = 'reiter'; ([ ['empfehlungen', 'Empfehlungen'], ['mediathek', 'Mediathek'], ['kategorien', 'Kategorien'], ] as [Reiter, string][]).forEach(([wert, text]) => { const b = document.createElement('button'); b.textContent = text; b.classList.toggle('check', wert === welcher); b.onclick = () => { location.hash = `#/m/${bibliothek.id}/${wert}`; }; reiterleiste.appendChild(b); }); k.append(wer, reiterleiste); return k; } /* ---------- Reiter „Mediathek" ---------- */ function raster(ziel: HTMLElement): void { let suche = ''; let ordnung: Ordnung = 'title'; let genre = ''; let tippt: number | undefined; const zeile = document.createElement('div'); zeile.className = 'filterzeile'; const anzahl = document.createElement('span'); anzahl.className = 'anzahl'; anzahl.textContent = '…'; const genreWahl = document.createElement('select'); genreWahl.innerHTML = ''; genreWahl.onchange = () => { genre = genreWahl.value; void laden(); }; const sortierung = document.createElement('select'); ([ ['title', 'Nach Titel'], ['added', 'Zuletzt hinzugefügt'], ['year', 'Nach Jahr'], ] as [Ordnung, string][]).forEach(([wert, text]) => { const o = document.createElement('option'); o.value = wert; o.textContent = text; sortierung.appendChild(o); }); sortierung.onchange = () => { ordnung = sortierung.value as Ordnung; void laden(); }; const suchfeld = document.createElement('input'); suchfeld.className = 'suche'; suchfeld.type = 'search'; suchfeld.placeholder = `In ${bibliothek.name} suchen`; suchfeld.oninput = () => { // Nicht bei jedem Tastendruck fragen: das wären bei „Untergang" neun // Anfragen, von denen acht niemanden interessieren. window.clearTimeout(tippt); tippt = window.setTimeout(() => { suche = suchfeld.value.trim(); void laden(); }, 250); }; zeile.append(genreWahl, sortierung, anzahl, suchfeld); const gitter = document.createElement('div'); gitter.className = 'raster'; ziel.append(zeile, gitter); // 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 => { namen.forEach(n => { const o = document.createElement('option'); o.value = n; o.textContent = n; genreWahl.appendChild(o); }); }).catch(() => {}); void laden(); async function laden(): Promise { gitter.innerHTML = '
'; try { const seite = await inhalt(bibliothek, { search: suche, sort: ordnung, genre, take: 200 }); anzahl.textContent = String(seite.total); gitter.innerHTML = ''; if (seite.items.length === 0) { gitter.appendChild(leer(suche || genre ? 'Nichts gefunden.' : `„${bibliothek.name}" ist leer. Starte unter Verwaltung einen Scan.`)); return; } seite.items.forEach(e => gitter.appendChild(kachel(alsKachel(e), oeffnen))); } catch (fehler) { gitter.innerHTML = ''; gitter.appendChild(fehlerzeile((fehler as Error).message)); } } } /* ---------- Reiter „Empfehlungen" ---------- */ function empfehlungen(ziel: HTMLElement): void { ziel.innerHTML = '
'; void (async () => { try { const [neu, beliebt, vorschlag] = await Promise.all([ inhalt(bibliothek, { sort: 'added', take: 24 }), inhalt(bibliothek, { sort: 'year', take: 24 }), holeEmpfehlungen(24).catch(() => ({ items: [], total: 0 })), ]); ziel.innerHTML = ''; if (neu.items.length === 0) { ziel.appendChild(leer(`„${bibliothek.name}" ist leer.`)); return; } ziel.appendChild(reihe( `Zuletzt in ${bibliothek.name} hinzugefügt`, neu.items.map(e => kachel(alsKachel(e), oeffnen)))); if (vorschlag.items.length) { ziel.appendChild(reihe( 'Weil du Ähnliches gesehen hast', vorschlag.items.map(e => kachel(alsKachel(e), oeffnen)))); } ziel.appendChild(reihe( 'Nach Jahr', beliebt.items.map(e => kachel(alsKachel(e), oeffnen)))); } catch (fehler) { ziel.innerHTML = ''; ziel.appendChild(fehlerzeile((fehler as Error).message)); } })(); } /* ---------- Reiter „Kategorien" ---------- */ function kategorien(ziel: HTMLElement): void { ziel.innerHTML = '
'; void (async () => { try { const namen = await genresVon(bibliothek.id); ziel.innerHTML = ''; if (namen.length === 0) { ziel.appendChild(leer('Für diese Mediathek sind noch keine Genres bekannt. ' + 'Frisch unter Verwaltung die Metadaten auf.')); return; } const gitter = document.createElement('div'); gitter.className = 'kategorien'; namen.forEach(name => { const k = document.createElement('button'); k.className = 'kategorie'; k.textContent = name; // **Die Farbe aus dem Namen, nicht aus einer Liste.** Eine feste // Zuordnung Genre→Farbe müsste jedes Genre kennen, das TMDB je // erfindet. So bekommt jedes eine eigene und immer dieselbe. k.style.background = farbeAus(name); k.onclick = () => { location.hash = `#/m/${bibliothek.id}/mediathek`; // Der Filter wird über die Adresse gesetzt, sobald das Raster // steht — sonst schriebe man in ein Element, das es nicht gibt. window.setTimeout(() => { const wahl = document.querySelector('.filterzeile select'); if (wahl) { wahl.value = name; wahl.dispatchEvent(new Event('change')); } }, 0); }; gitter.appendChild(k); }); ziel.appendChild(gitter); } catch (fehler) { ziel.innerHTML = ''; ziel.appendChild(fehlerzeile((fehler as Error).message)); } })(); } return wurzel; } /** Eine immer gleiche Farbe aus einem Namen. */ function farbeAus(name: string): string { let summe = 0; 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%))`; }