Files
Flaro/web/app/src/views/home.ts
T
ScarriffleandClaude Opus 5 fb8ad1c831 Der Stand lag daneben und wurde trotzdem weggeworfen
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>
2026-10-12 19:13:09 +02:00

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;
}