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:
co-authored by
Claude Opus 5
parent
343b0cdcfb
commit
f10fb4c1bc
@@ -101,6 +101,12 @@ export type Angefangen = {
|
||||
state: Stand;
|
||||
/** Zu welcher Serie die Folge gehört — dorthin führt die Kachel. */
|
||||
seriesId: string | null;
|
||||
/** Die Datei, die abgespielt würde. Ohne sie gibt es nichts zu starten. */
|
||||
fileId?: string | null;
|
||||
/** Ihre Länge, für die Zeitleiste des Spielers. */
|
||||
durationSeconds?: number | null;
|
||||
/** Ab wann abspielbar. In der Zukunft heisst: noch nicht. */
|
||||
publishAt?: string | null;
|
||||
};
|
||||
export type Bibliothek = {
|
||||
id: string; name: string;
|
||||
|
||||
+98
-7
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
} from '../api';
|
||||
import { leer, fehlerzeile, meldung } from '../util';
|
||||
import { kachel, alsKachel, angefangenAlsKachel, reihe } from '../card';
|
||||
import { hero, alsHero, type Heroeintrag } from '../hero';
|
||||
import { hero, alsHero, angefangenAlsHero, type Heroeintrag } from '../hero';
|
||||
import { spieler } from '../spieler';
|
||||
|
||||
type Art = 'filme' | 'serien';
|
||||
|
||||
@@ -24,7 +25,12 @@ export function startseite(
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
void (async () => {
|
||||
// **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
|
||||
@@ -56,16 +62,28 @@ export function startseite(
|
||||
heroTitel.push(e);
|
||||
};
|
||||
|
||||
weiter.items.forEach(e => dazu(alsHero(e as never, 'serien' in e ? 'serien' : 'filme')));
|
||||
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 as never, 'filme')));
|
||||
vorschlag.items.forEach(e => dazu(alsHero(e, 'filme')));
|
||||
|
||||
const aufmacher = hero(heroTitel,
|
||||
(art, id) => oeffnen(art, id),
|
||||
(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(),
|
||||
});
|
||||
});
|
||||
if (aufmacher) wurzel.appendChild(aufmacher);
|
||||
|
||||
if (weiter.items.length) {
|
||||
@@ -106,7 +124,7 @@ export function startseite(
|
||||
wurzel.innerHTML = '';
|
||||
wurzel.appendChild(fehlerzeile(meldung(fehler)));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user