Der Fortschrittsbalken ist an **einer** Stelle gebaut und wird in jeder
Kachel verwendet — Web wie App. Fehlte er, dann nicht an der Kachel,
sondern weil der Server `State: null` schickte. Drei Listen taten das,
obwohl die Stände in derselben Abfrage danebenlagen:
/home/suggestions — „Weil du Ähnliches gesehen hast"
/home/seasonal — die saisonale Reihe
/movies/{id}/similar — „Ähnliche Titel"
Ausgerechnet die erste entsteht aus dem Sehverhalten: ein halb gesehener
Film sah dort aus wie ein unangetasteter, während dieselbe Kachel in der
Mediathek einen Balken trug. `ZuständeZu` gab es längst, es war nur
privat.
**Und „Als Nächstes" lässt sich jetzt räumen.** Das vorhandene Weglegen
reichte nicht dorthin: die Reihe liest `Watched` und `InProgress`, nie
`Dismissed` — und bei einer nie gestarteten Folge ist `InProgress` ohnehin
`false`. Weggelegt wird deshalb die **Serie**, nicht die Folge: beim
nächsten Laden rückte sonst die übernächste nach, und man legte
zwanzigmal weg. Den Zustand dafür gibt es schon, er wurde nur nie
gelesen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
import {
|
|
inhalt, weiterschauen, naechsteFolgen, empfehlungen, saisonal,
|
|
type Bibliothek,
|
|
} from '../api';
|
|
import { leer, fehlerzeile, meldung } from '../util';
|
|
import { kachel, alsKachel, angefangenAlsKachel, reihe } from '../card';
|
|
import { hero, alsHero, angefangenAlsHero, type Heroeintrag } from '../hero';
|
|
import { spieler } from '../spieler';
|
|
|
|
type Art = 'filme' | 'serien';
|
|
|
|
/** Die Startseite — waagerechte Reihen, wie bei Plex.
|
|
*
|
|
* Oben, was angefangen ist. Darunter **je Mediathek eine Reihe** mit dem
|
|
* zuletzt Hinzugefügten: wer „Filme" und „Dokumentationen" getrennt angelegt
|
|
* hat, hat sie getrennt gemeint, und eine Startseite, die sie wieder
|
|
* zusammenwirft, nimmt die Entscheidung zurück.
|
|
*
|
|
* Eine Reihe ohne Inhalt wird gar nicht erst gezeigt. */
|
|
export function startseite(
|
|
bibliotheken: Bibliothek[],
|
|
oeffnen: (art: Art, id: string) => void,
|
|
alleZeigen: (b: Bibliothek) => void,
|
|
/** **Wird gebraucht, nicht nur mitgeführt.** Die Startseite wird gemerkt;
|
|
* ohne dieses Signal lief der Takt des Aufmachers weiter, während man
|
|
* längst woanders war, und seine Zuhörer am Fenster blieben hängen. */
|
|
signal?: AbortSignal,
|
|
): HTMLElement {
|
|
const wurzel = document.createElement('div');
|
|
wurzel.innerHTML = '<div class="spinner"></div>';
|
|
|
|
// **Benannt, damit der Spieler sie danach neu bauen kann.** Wer aus dem
|
|
// Aufmacher heraus zu Ende schaut, soll die Zeile „Fortsetzen" nicht
|
|
// unverändert wiederfinden.
|
|
void aufbauen();
|
|
|
|
async function aufbauen(): Promise<void> {
|
|
try {
|
|
// **Alles auf einmal anfragen.** Bei fünf Mediatheken wären es sonst
|
|
// acht Anfragen nacheinander, und die Startseite baut sich vor den
|
|
// Augen des Benutzers Zeile für Zeile auf.
|
|
const [weiter, naechste, vorschlag, saison, ...bestaende] = await Promise.all([
|
|
weiterschauen(),
|
|
naechsteFolgen(),
|
|
empfehlungen(24).catch(() => ({ items: [], total: 0 })),
|
|
// **Scheitert sie, fehlt eine Reihe und nicht die Startseite.** Ein
|
|
// älterer Server kennt den Weg nicht; das darf nicht dazu führen,
|
|
// dass gar nichts erscheint.
|
|
saisonal(24).catch(() => ({ occasion: null, items: { items: [], total: 0 } })),
|
|
...bibliotheken.map(b => inhalt(b, { sort: 'added', take: 24 })),
|
|
]);
|
|
|
|
wurzel.innerHTML = '';
|
|
|
|
if (bibliotheken.length === 0) {
|
|
wurzel.appendChild(leer('Noch keine Mediathek. Lege unter Verwaltung eine an '
|
|
+ 'und starte einen Scan.'));
|
|
return;
|
|
}
|
|
|
|
// **Der Aufmacher, aus dem, was ohnehin schon geholt wurde.**
|
|
// Erst Angefangenes — wer mittendrin ist, will dort weiter —, dann
|
|
// Neuzugänge, dann Vorschläge. Titel ohne Hintergrundbild fallen im
|
|
// Hero selbst heraus; ohne einen einzigen entfällt er ganz.
|
|
const heroTitel: Heroeintrag[] = [];
|
|
const schonDa = new Set<string>();
|
|
const dazu = (e: Heroeintrag) => {
|
|
if (schonDa.has(e.id) || !e.hintergrund) return;
|
|
schonDa.add(e.id);
|
|
heroTitel.push(e);
|
|
};
|
|
|
|
weiter.items.forEach(e => dazu(angefangenAlsHero(e)));
|
|
bibliotheken.forEach((b, i) => {
|
|
const art: Art = b.kind === 'Shows' ? 'serien' : 'filme';
|
|
bestaende[i]?.items.forEach(e => dazu(alsHero(e as never, art)));
|
|
});
|
|
vorschlag.items.forEach(e => dazu(alsHero(e, 'filme')));
|
|
|
|
const aufmacher = hero(heroTitel,
|
|
(art, id) => oeffnen(art, id),
|
|
e => {
|
|
// Ohne Datei gibt es nichts zu starten — dann führt der Knopf
|
|
// dorthin, wo man wählen kann.
|
|
if (e.dateiId === null) { oeffnen(e.art, e.id); return; }
|
|
spieler({
|
|
dateiId: e.dateiId,
|
|
titelId: e.standId,
|
|
name: e.titel,
|
|
ab: e.stelle,
|
|
laenge: e.laenge,
|
|
danach: () => void aufbauen(),
|
|
});
|
|
},
|
|
signal);
|
|
if (aufmacher) wurzel.appendChild(aufmacher);
|
|
|
|
if (weiter.items.length) {
|
|
wurzel.appendChild(reihe('Fortsetzen',
|
|
weiter.items.map(e => kachel(angefangenAlsKachel(e), oeffnen))));
|
|
}
|
|
if (naechste.items.length) {
|
|
wurzel.appendChild(reihe('Nächste Folge',
|
|
naechste.items.map(e => kachel(angefangenAlsKachel(e, 'naechste'), oeffnen))));
|
|
}
|
|
|
|
// **Weit oben, weil sie nur jetzt gilt.** „Zu Halloween" unter den
|
|
// Neuzugängen wäre im November verschwunden, ohne dass jemand sie
|
|
// gesehen hat. Ohne Anlass oder ohne Treffer kommt `occasion` leer
|
|
// zurück — dann steht hier gar keine Reihe statt einer mit
|
|
// Überschrift und nichts darunter.
|
|
if (saison.occasion && saison.items.items.length) {
|
|
wurzel.appendChild(reihe(saison.occasion,
|
|
saison.items.items.map(e => kachel(alsKachel(e), oeffnen))));
|
|
}
|
|
|
|
let etwasDa = false;
|
|
bibliotheken.forEach((b, i) => {
|
|
const seite = bestaende[i];
|
|
if (!seite?.items.length) return;
|
|
etwasDa = true;
|
|
wurzel.appendChild(reihe(
|
|
// Plex schreibt den Namen der Mediathek in die Überschrift —
|
|
// „Zuletzt in Filme hinzugefügt". Bei mehreren Mediatheken ist das
|
|
// der Unterschied zwischen einer Reihe und sieben gleich
|
|
// aussehenden.
|
|
`Zuletzt in ${b.name} hinzugefügt`,
|
|
seite.items.map(e => kachel(alsKachel(e), oeffnen)),
|
|
seite.total > seite.items.length
|
|
? { text: 'Alle zeigen', tun: () => alleZeigen(b) }
|
|
: undefined));
|
|
});
|
|
|
|
if (vorschlag.items.length) {
|
|
wurzel.appendChild(reihe('Weil du Ähnliches gesehen hast',
|
|
vorschlag.items.map(e => kachel(alsKachel(e), oeffnen))));
|
|
}
|
|
|
|
if (!etwasDa && !weiter.items.length && !naechste.items.length) {
|
|
wurzel.appendChild(leer('Noch nichts da. Starte unter Verwaltung einen Scan.'));
|
|
}
|
|
} catch (fehler) {
|
|
wurzel.innerHTML = '';
|
|
wurzel.appendChild(fehlerzeile(meldung(fehler)));
|
|
}
|
|
}
|
|
|
|
return wurzel;
|
|
}
|