import { bildAdresse, type Angefangen, type Film, type Serie } from './api'; import { icons } from './icons'; import { dauer, knopf, folgenname } from './util'; /** 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; schriftzug: string | null; hintergrund: string; jahr: number | null; laufzeit: number | null; 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; /** Ob der Server den Titel als angefangen führt — die Schwelle ist einstellbar. */ angefangen: boolean; /** Wem der Fortschritt gehört: bei einer Folge der Folge, nicht der Serie. */ standId: string; /** Bei einer Folge ihre Teile — für die Zeile über dem Spieler. * * **`titel` reicht dafür nicht.** Dort steht bei einer Folge der Name der * *Serie*, weil der Aufmacher die Serie zeigt. Wer von hier aus startete, * las über dem Spieler nur „Biohackers" und nicht, welche Folge läuft. */ staffel: number | null; folge: number | null; folgentitel: string | null; }; const WECHSEL_MS = 9000; const HOECHSTENS = 7; /** Der Aufmacher der Startseite. * * **Warum überhaupt.** Die Startseite begann bei einer Reihe aus 150 px * breiten Plakaten — das grösste Element über dem Falz war 225 px hoch. Plex * und Netflix öffnen mit einem Bild über die volle Breite, und das ist der * grösste Einzelunterschied im Eindruck. Es ist ein Layout-, kein * Farbproblem. * * **Nur mit Hintergrundbild.** Ein Hero ohne Bild ist schlechter als keiner: * eine graue Fläche über den halben Bildschirm sagt nichts und nimmt den * Reihen den Platz. Gibt es keinen einzigen Titel mit Backdrop, entfällt er * ersatzlos. * * **Pause bei Mauszeiger und Fokus.** Wer gerade liest oder mit der Tastatur * unterwegs ist, soll nicht nach neun Sekunden woanders landen. Und wer * „Bewegung reduzieren" eingestellt hat, bekommt gar keinen selbsttätigen * Wechsel — nur die Punkte zum Weiterklicken. */ export function hero( eintraege: Heroeintrag[], oeffnen: (art: 'filme' | 'serien', id: string) => void, abspielen: (eintrag: Heroeintrag) => void, signal?: AbortSignal, ): HTMLElement | null { const mitBild = eintraege.filter(e => e.hintergrund).slice(0, HOECHSTENS); if (mitBild.length === 0) return null; const wurzel = document.createElement('section'); wurzel.className = 'hero'; wurzel.setAttribute('aria-roledescription', 'Karussell'); wurzel.setAttribute('aria-label', 'Vorgestellte Titel'); const buehne = document.createElement('div'); buehne.className = 'hero-buehne'; const folien = mitBild.map((_, i) => { const folie = document.createElement('div'); folie.className = 'hero-folie' + (i === 0 ? ' an' : ''); folie.setAttribute('aria-hidden', i === 0 ? 'false' : 'true'); buehne.appendChild(folie); return folie; }); /** Hängt einer Folie ihr Bild an — und nur dieser, und nur einmal. * * **Sieben Bilder auf einmal waren sechs zu viel.** Hier wurde jeder * Folie sofort ihr Hintergrundbild gesetzt; sichtbar ist aber immer nur * eine, und es sind `w1280`-Bilder. Beim ersten Aufbau der Startseite * gingen damit sieben grosse Abrufe gleichzeitig hinaus — über dieselbe * Leitung, auf der auch die Plakate und die Reihen kommen. */ function bildAnhaengen(i: number): void { const folie = folien[i]; const eintrag = mitBild[i]; if (!folie || !eintrag || folie.style.backgroundImage) return; folie.style.backgroundImage = `url("${bildAdresse(eintrag.hintergrund)}")`; } bildAnhaengen(0); // **Der Rest, sobald der Browser Luft hat.** Nicht erst beim // Weiterschalten: dann sähe man beim ersten Wechsel einen leeren Kasten. // `requestIdleCallback` kennt nicht jeder Browser — der Rückfall ist ein // Zeitgeber, und eine Sekunde reicht für den ersten Aufbau. const nachtragen = () => mitBild.forEach((_, i) => bildAnhaengen(i)); const leerlauf = (window as unknown as { requestIdleCallback?: (f: () => void, o?: { timeout: number }) => void; }).requestIdleCallback; if (leerlauf) leerlauf(nachtragen, { timeout: 3000 }); else window.setTimeout(nachtragen, 1000); const inhalt = document.createElement('div'); inhalt.className = 'hero-inhalt'; const schriftzug = document.createElement('img'); schriftzug.className = 'hero-schriftzug'; const titel = document.createElement('h1'); titel.className = 'hero-titel'; // Kommt der Schriftzug nicht an, tritt der Name an seine Stelle. schriftzug.onerror = () => { schriftzug.classList.add('hidden'); titel.classList.remove('hidden'); }; const meta = document.createElement('div'); meta.className = 'hero-meta'; const text = document.createElement('p'); text.className = 'hero-text'; const spielen = knopf('Abspielen', 'btn btn-primary'); 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. // // Hier einzeilig, anders als auf der Detailseite: zwei Zeilen wären höher // als ein Knopf, und dann rückte beim Umblättern die ganze Reihe auf und ab. const bald = document.createElement('div'); bald.className = 'verfuegbar-ab hero-bald'; const baldText = document.createElement('span'); bald.append(iconElement(icons.uhr), baldText); // **Ein Platz fester Breite für das, was vorne steht.** // // „Abspielen", „Fortsetzen ab 1:23:45" und der Termin sind verschieden // breit. Standen sie im selben Fluss, wanderte „Mehr" bei jedem Umblättern // mit — und das Karussell blättert von allein. Was sich ändert, soll den // Rest in Ruhe lassen. const haupt = document.createElement('div'); haupt.className = 'hero-haupt'; haupt.append(bald, spielen); const knoepfe = document.createElement('div'); knoepfe.className = 'hero-knoepfe'; knoepfe.append(haupt, mehr); inhalt.append(schriftzug, titel, meta, text, knoepfe); const punkte = document.createElement('div'); punkte.className = 'hero-punkte'; const punktknoepfe = mitBild.map((e, i) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'hero-punkt' + (i === 0 ? ' an' : ''); // **Echte Knöpfe mit Beschriftung**, keine `