Die Oberfläche zeigt jetzt, was der Server seit Stufe 4 weiss. Auf der Startseite zwei neue Reihen, ganz oben: **Weiterschauen** und **Nächste Folge**. Sie überschneiden sich nicht — was angefangen ist, steht links, was ansteht, rechts. Eine leere Reihe wird gar nicht erst gezeigt. Auf jeder Kachel ein Balken, und zwar **ab der ersten Sekunde**. Bei einem Zweieinhalbstünder sind zwei Minuten 1,3 % — anderthalb Bildpunkte. Der Balken hat deshalb eine Mindestbreite, sonst wäre das Versprechen „wir merken uns jede Sekunde" ausgerechnet dort unsichtbar, wo es zählt. Serien tragen die Zahl offener Folgen als Plakette in der Ecke. Auf der Detailseite stehen **beide Knöpfe nebeneinander**: „Weiterschauen ab 0:42" und „Von vorn". Auch bei einem gesehenen Film — der Server wirft die Stelle nicht weg, wenn das Häkchen kommt, und sie hier zu verschweigen hiesse, genau den Unterschied wegzuwerfen, um dessentwillen er sie behält. Dazu ein Schalter für gesehen/ungesehen. Der Spieler meldet alle zehn Sekunden, beim Pausieren, am Ende und beim Schliessen. Nicht bei jedem `timeupdate`: das feuert viermal je Sekunde, und daraus Anfragen zu machen hiesse, den Server mit Meldungen zu bewerfen, die sich um 250 ms unterscheiden. Eine verlorene Meldung kostet ein paar Sekunden Fortschritt — dafür mitten im Film einen Fehler einzublenden wäre die schlechtere Hälfte des Tauschs. **Nebenbei ein Fehler beseitigt, der nur mit grosser Mediathek aufgefallen wäre:** die Detailseite holte 200 Titel und suchte darin nach der Kennung. Bei 201 Filmen hätte sie vom letzten behauptet, es gebe ihn nicht mehr. Jetzt gibt es den Einzelabruf, und sie benutzt ihn. Im Container durchgespielt: „Der Untergang" von vorn, die MKV wird umgepackt, läuft durch — danach steht „Gesehen" unter dem Titel, der Balken ist voll, und der Knopf bietet beides an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import {
|
|
filme, serien, weiterschauen, naechsteFolgen,
|
|
type Film, type Serie, type Angefangen,
|
|
} from '../api';
|
|
import { esc, plakat, leer, fehlerzeile, anteil, verbleibend } from '../util';
|
|
|
|
type Art = 'filme' | 'serien';
|
|
|
|
/** Die Startseite: waagerechte Reihen, wie bei Plex.
|
|
*
|
|
* Oben, was angefangen ist — darum geht man schliesslich hin. Darunter die
|
|
* Mediathek. Eine Reihe, die nichts enthält, wird gar nicht erst gezeigt. */
|
|
export function startseite(oeffnen: (art: Art, id: string) => void): HTMLElement {
|
|
const wurzel = document.createElement('div');
|
|
wurzel.innerHTML = '<div class="spinner"></div>';
|
|
|
|
void (async () => {
|
|
try {
|
|
const [weiter, naechste, f, s] = await Promise.all([
|
|
weiterschauen(), naechsteFolgen(), filme({ take: 24 }), serien({ take: 24 }),
|
|
]);
|
|
wurzel.innerHTML = '';
|
|
|
|
if (f.total === 0 && s.total === 0) {
|
|
wurzel.appendChild(leer('Noch nichts da. Lege unter Verwaltung eine Bibliothek an '
|
|
+ 'und starte einen Scan.'));
|
|
return;
|
|
}
|
|
|
|
if (weiter.items.length) wurzel.appendChild(angefangenReihe('Weiterschauen', weiter));
|
|
if (naechste.items.length) wurzel.appendChild(angefangenReihe('Nächste Folge', naechste));
|
|
if (f.total > 0) wurzel.appendChild(reihe(`Filme · ${f.total}`, f.items, 'filme'));
|
|
if (s.total > 0) wurzel.appendChild(reihe(`Serien · ${s.total}`, s.items, 'serien'));
|
|
} catch (fehler) {
|
|
wurzel.innerHTML = '';
|
|
wurzel.appendChild(fehlerzeile((fehler as Error).message));
|
|
}
|
|
})();
|
|
|
|
/** Eine Reihe aus Einträgen mit Stand — Filme und Folgen gemischt. */
|
|
function angefangenReihe(titel: string, seite: { items: Angefangen[] }): HTMLElement {
|
|
const bereich = document.createElement('section');
|
|
bereich.className = 'reihe';
|
|
|
|
const h = document.createElement('h2');
|
|
h.textContent = titel;
|
|
|
|
const streifen = document.createElement('div');
|
|
streifen.className = 'strip';
|
|
|
|
seite.items.forEach(eintrag => {
|
|
const k = document.createElement('button');
|
|
k.className = 'card';
|
|
k.appendChild(plakat(eintrag.posterImage, eintrag.title, anteil(eintrag.state)));
|
|
|
|
const unten = document.createElement('div');
|
|
unten.className = 'rowline';
|
|
// Bei einer Folge oben die Serie, darunter wo man ist — eine Folge
|
|
// ohne ihre Serie zu nennen hilft niemandem beim Wiedererkennen.
|
|
const zweite = eintrag.kind === 'episode'
|
|
? `S${String(eintrag.season).padStart(2, '0')}E${String(eintrag.episode).padStart(2, '0')}`
|
|
+ (eintrag.subTitle ? ` · ${eintrag.subTitle}` : '')
|
|
: verbleibend(eintrag.state);
|
|
unten.innerHTML = `<strong>${esc(eintrag.title)}</strong>`
|
|
+ `<span class="tiny">${esc(zweite)}</span>`;
|
|
k.appendChild(unten);
|
|
|
|
// Angesprungen wird der Titel, nicht die Folge: von dort kommt man zur
|
|
// Folge, umgekehrt fehlte der Zusammenhang.
|
|
k.onclick = () => oeffnen(eintrag.kind === 'movie' ? 'filme' : 'serien', eintrag.id);
|
|
streifen.appendChild(k);
|
|
});
|
|
|
|
bereich.append(h, streifen);
|
|
return bereich;
|
|
}
|
|
|
|
function reihe(titel: string, eintraege: (Film | Serie)[], art: Art): HTMLElement {
|
|
const bereich = document.createElement('section');
|
|
bereich.className = 'reihe';
|
|
|
|
const h = document.createElement('h2');
|
|
h.textContent = titel;
|
|
|
|
const streifen = document.createElement('div');
|
|
streifen.className = 'strip';
|
|
|
|
eintraege.forEach(eintrag => {
|
|
const k = document.createElement('button');
|
|
k.className = 'card';
|
|
|
|
const bild = plakat(
|
|
eintrag.posterImage, eintrag.title,
|
|
'state' in eintrag ? anteil(eintrag.state) : null);
|
|
if ('unwatchedCount' in eintrag && eintrag.unwatchedCount > 0) {
|
|
const plakette = document.createElement('span');
|
|
plakette.className = 'offen';
|
|
plakette.textContent = String(eintrag.unwatchedCount);
|
|
bild.appendChild(plakette);
|
|
}
|
|
k.appendChild(bild);
|
|
|
|
const unten = document.createElement('div');
|
|
unten.className = 'rowline';
|
|
unten.innerHTML = `<strong>${esc(eintrag.title)}</strong>`
|
|
+ `<span class="tiny">${esc(eintrag.year ?? '')}</span>`;
|
|
k.appendChild(unten);
|
|
k.onclick = () => oeffnen(art, eintrag.id);
|
|
streifen.appendChild(k);
|
|
});
|
|
|
|
bereich.append(h, streifen);
|
|
return bereich;
|
|
}
|
|
|
|
return wurzel;
|
|
}
|