Abspielen spielt ab

Im Aufmacher der Startseite stand „Abspielen", und wer draufdrückte,
landete auf der Detailseite. Gemessen am laufenden Server: beide Knöpfe
riefen dieselbe Funktion — `oeffnen` —, der eine hiess nur anders.

Drei Dinge waren falsch, alle an derselben Stelle:

**Der Knopf log.** Jetzt startet er den Spieler, mit Fortsetzen ab der
Stelle, an der man war. Ohne Datei — eine ganze Serie — heisst er
„Ansehen" und führt dorthin, wo man wählen kann.

**Eine angefangene Folge war kein Film.** Sie lief durch `alsHero` mit
`art: 'filme'` und ihrer eigenen Kennung; der Klick ging auf
`#/filme/<Folgenkennung>` und endete in „Diesen Film gibt es nicht
mehr". Nachgemessen im Browser, dann `angefangenAlsHero` dafür gebaut:
die Detailseite gehört der Serie, der Fortschritt der Folge.

**Ein geplanter Titel sah aus wie jeder andere.** Jetzt steht statt des
Knopfes „Bald ist es soweit" mit dem Termin, wie auf der Detailseite.

Dafür nennt `/home/continue` und `/home/next` nun Datei, Länge und
Termin, und die Vorschläge bringen ihre Dateien mit — ohne die konnte
der Aufmacher gar nicht abspielen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 15:42:42 +02:00
co-authored by Claude Opus 5
parent 343b0cdcfb
commit f10fb4c1bc
6 changed files with 248 additions and 21 deletions
+98 -7
View File
@@ -1,9 +1,10 @@
import { bildAdresse, type Film, type Serie } from './api';
import { bildAdresse, type Angefangen, type Film, type Serie } from './api';
import { icons } from './icons';
import { knopf } from './util';
import { dauer, knopf } from './util';
/** Was im Hero stehen kann — Film oder Serie, mehr braucht es nicht. */
/** Was im Hero stehen kann — Film, Serie oder eine angefangene Folge. */
export type Heroeintrag = {
/** Wohin „Mehr" führt. Bei einer Folge zur Serie, nicht zur Folge. */
art: 'filme' | 'serien';
id: string;
titel: string;
@@ -14,6 +15,18 @@ export type Heroeintrag = {
freigabe: string | null;
bewertung: number | null;
beschreibung: string | null;
/** Ab wann es den Titel gibt. In der Zukunft heisst: noch nicht. */
verfuegbarAb: string | null;
/** Die Datei, die abgespielt würde. Ohne sie gibt es nichts zu starten. */
dateiId: string | null;
/** Ihre Länge — aus der Mediathek, nicht aus dem Strom. */
laenge: number | null;
/** Wo man stehengeblieben ist. */
stelle: number;
/** Wem der Fortschritt gehört: bei einer Folge der Folge, nicht der Serie. */
standId: string;
};
const WECHSEL_MS = 9000;
@@ -40,7 +53,7 @@ const HOECHSTENS = 7;
export function hero(
eintraege: Heroeintrag[],
oeffnen: (art: 'filme' | 'serien', id: string) => void,
abspielen: (art: 'filme' | 'serien', id: string) => void,
abspielen: (eintrag: Heroeintrag) => void,
signal?: AbortSignal,
): HTMLElement | null {
const mitBild = eintraege.filter(e => e.hintergrund).slice(0, HOECHSTENS);
@@ -76,11 +89,25 @@ export function hero(
text.className = 'hero-text';
const spielen = knopf('Abspielen', 'btn btn-primary');
spielen.prepend(iconElement(icons.abspielen));
const spielIcon = iconElement(icons.abspielen);
spielen.prepend(spielIcon);
const mehr = knopf('Mehr', 'btn');
// **Statt des Knopfes, wenn es den Titel noch nicht gibt.** Ein Knopf, der
// „Abspielen" sagt und eine Absage bringt, ist schlimmer als keiner.
const bald = document.createElement('div');
bald.className = 'verfuegbar-ab';
const baldText = document.createElement('div');
baldText.className = 'verfuegbar-text';
const baldOben = document.createElement('strong');
baldOben.textContent = 'Bald ist es soweit';
const baldUnten = document.createElement('span');
baldText.append(baldOben, baldUnten);
bald.append(iconElement(icons.uhr), baldText);
const knoepfe = document.createElement('div');
knoepfe.className = 'hero-knoepfe';
knoepfe.append(spielen, mehr);
knoepfe.append(bald, spielen, mehr);
inhalt.append(schriftzug, titel, meta, text, knoepfe);
@@ -134,8 +161,36 @@ export function hero(
].filter(Boolean).join(' · ');
text.textContent = e.beschreibung ?? '';
spielen.onclick = () => abspielen(e.art, e.id);
mehr.onclick = () => oeffnen(e.art, e.id);
// **Drei Fälle, und jeder sagt die Wahrheit.**
//
// Geplant: kein Knopf, sondern der Termin. Mit Datei: abspielen, und
// zwar wirklich — vorher öffnete „Abspielen" die Detailseite, was
// niemand erwartet. Ohne Datei (eine ganze Serie): „Ansehen", denn es
// gibt keine eine Datei, die gemeint wäre.
const geplantAb = e.verfuegbarAb ? new Date(e.verfuegbarAb) : null;
const nochNicht = geplantAb !== null && geplantAb > new Date();
bald.classList.toggle('hidden', !nochNicht);
spielen.classList.toggle('hidden', nochNicht);
if (nochNicht) {
baldUnten.textContent = `Ab ${geplantAb!.toLocaleString('de-CH',
{ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
hour: '2-digit', minute: '2-digit' })} Uhr`;
return;
}
const beschriftung = e.dateiId === null ? 'Ansehen'
: e.stelle > 0 ? `Fortsetzen ab ${dauer(e.stelle)}`
: 'Abspielen';
spielen.textContent = beschriftung;
spielen.prepend(spielIcon);
spielIcon.classList.toggle('hidden', e.dateiId === null);
spielen.onclick = e.dateiId === null
? () => oeffnen(e.art, e.id)
: () => abspielen(e);
}
function weiter(): void { zeige(jetzt + 1); }
@@ -179,6 +234,7 @@ function iconElement(svg: string): HTMLElement {
/** Baut Heroeinträge aus dem, was die Startseite ohnehin geholt hat. */
export function alsHero(eintrag: Film | Serie, art: 'filme' | 'serien'): Heroeintrag {
const datei = 'files' in eintrag ? eintrag.files[0] ?? null : null;
return {
art,
id: eintrag.id,
@@ -190,5 +246,40 @@ export function alsHero(eintrag: Film | Serie, art: 'filme' | 'serien'): Heroein
freigabe: eintrag.contentRating,
bewertung: eintrag.rating,
beschreibung: eintrag.overview,
verfuegbarAb: eintrag.publishAt,
dateiId: datei?.id ?? null,
laenge: datei?.durationSeconds ?? null,
stelle: eintrag.state?.positionSeconds ?? 0,
standId: eintrag.id,
};
}
/** Dasselbe für eine angefangene Zeile.
*
* **Eine Folge ist kein Film.** Vorher lief auch sie durch `alsHero` — mit
* `art: 'filme'` und der Kennung der Folge. Wer den Aufmacher anklickte,
* landete auf `#/filme/<Folgenkennung>` und las „Diesen Film gibt es nicht
* mehr". Die Detailseite gehört der Serie; der Fortschritt der Folge.
*/
export function angefangenAlsHero(eintrag: Angefangen): Heroeintrag {
const folge = eintrag.kind === 'episode';
return {
art: folge ? 'serien' : 'filme',
id: folge ? eintrag.seriesId ?? eintrag.id : eintrag.id,
titel: eintrag.title,
schriftzug: null,
hintergrund: eintrag.backdropImage ?? '',
jahr: eintrag.year,
laufzeit: null,
freigabe: null,
bewertung: null,
beschreibung: folge && eintrag.season !== null
? `S${eintrag.season} · F${eintrag.episode}${eintrag.subTitle ? ` — ${eintrag.subTitle}` : ''}`
: null,
verfuegbarAb: eintrag.publishAt ?? null,
dateiId: eintrag.fileId ?? null,
laenge: eintrag.durationSeconds ?? eintrag.state.runtimeSeconds,
stelle: eintrag.state.positionSeconds,
standId: eintrag.id,
};
}