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%))`;
}