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
+14 -1
View File
@@ -618,7 +618,20 @@ public sealed record ContinueEntry(
int? Season, int? Episode, int? Year, int? Season, int? Episode, int? Year,
string? PosterImage, string? BackdropImage, string? PosterImage, string? BackdropImage,
ItemStateResponse State, ItemStateResponse State,
Guid? SeriesId = null); Guid? SeriesId = null,
/// <summary>Die Datei, die abgespielt würde.</summary>
///
/// <remarks>
/// <b>Damit „Abspielen" abspielt.</b> Der Aufmacher der Startseite hatte
/// keine Datei zur Hand und konnte deshalb nur die Detailseite öffnen —
/// ein Knopf, auf dem „Abspielen" steht und der etwas anderes tut.
/// </remarks>
Guid? FileId = null,
double? DurationSeconds = null,
/// <summary>Ab wann abspielbar. In der Zukunft heisst: noch nicht.</summary>
DateTimeOffset? PublishAt = null);
/// <summary>Eine laufende Wiedergabe — für das Dashboard.</summary> /// <summary>Eine laufende Wiedergabe — für das Dashboard.</summary>
public sealed record SessionResponse( public sealed record SessionResponse(
@@ -286,7 +286,7 @@ public static class PlaybackEndpoints
var folgen = await db.Episodes.AsNoTracking() var folgen = await db.Episodes.AsNoTracking()
.Where(f => serienIds.Contains(f.SeriesId)) .Where(f => serienIds.Contains(f.SeriesId))
.Select(f => new { f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber, f.Title, f.StillImage }) .Select(f => new { f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber, f.Title, f.StillImage, f.PublishAt })
.ToListAsync(); .ToListAsync();
var folgenIds = folgen.Select(f => f.Id).ToList(); var folgenIds = folgen.Select(f => f.Id).ToList();
@@ -319,9 +319,19 @@ public static class PlaybackEndpoints
naechste.SeasonNumber, naechste.EpisodeNumber, null, naechste.SeasonNumber, naechste.EpisodeNumber, null,
serie.PosterImage, naechste.StillImage ?? serie.PosterImage, serie.PosterImage, naechste.StillImage ?? serie.PosterImage,
zustand is null ? Leer() : Abbild(zustand), zustand is null ? Leer() : Abbild(zustand),
SeriesId: x.SeriesId)); SeriesId: x.SeriesId,
PublishAt: naechste.PublishAt));
} }
// Die Dateien erst jetzt, für die ausgewählten Folgen — nicht für
// alle Folgen aller angefassten Serien.
var erste = await Erstdateien(db, [], liste.Select(e => e.Id).ToList());
liste = liste.Select(e => e with
{
FileId = erste.GetValueOrDefault(e.Id).Id,
DurationSeconds = erste.GetValueOrDefault(e.Id).Sekunden,
}).ToList();
return Results.Ok(new Page<ContinueEntry>(liste, liste.Count)); return Results.Ok(new Page<ContinueEntry>(liste, liste.Count));
}); });
@@ -383,9 +393,24 @@ public static class PlaybackEndpoints
.Select(x => x.Film) .Select(x => x.Film)
.ToListAsync(); .ToListAsync();
// **Mit Datei und Termin.** Ein Vorschlag ohne Datei liesse den
// Aufmacher „Abspielen" anbieten und dann doch nur blättern; ein
// Vorschlag ohne Termin verschwiege, dass es ihn noch nicht gibt.
var vorschlagIds = treffer.Select(m => m.Id).ToList();
var vorschlagsdateien = await db.MediaFiles.AsNoTracking()
.Where(d => d.MovieId != null && vorschlagIds.Contains(d.MovieId!.Value))
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.MovieId, d.VersionLabel })
.ToListAsync();
var items = treffer.Select(m => new MovieResponse( var items = treffer.Select(m => new MovieResponse(
m.Id, m.Title, m.Year, m.Overview, m.PosterImage, m.BackdropImage, m.Id, m.Title, m.Year, m.Overview, m.PosterImage, m.BackdropImage,
[], null, m.LogoImage, m.RuntimeMinutes, m.Rating, null, m.ContentRating)).ToList(); vorschlagsdateien.Where(d => d.MovieId == m.Id)
.Select(d => new MediaFileResponse(
d.Id, Path.GetFileName(d.Path), d.SizeBytes, d.Container,
d.Duration?.TotalSeconds, [], d.VersionLabel))
.ToList(),
null, m.LogoImage, m.RuntimeMinutes, m.Rating, null, m.ContentRating,
PublishAt: m.PublishAt)).ToList();
return Results.Ok(new Page<MovieResponse>(items, items.Count)); return Results.Ok(new Page<MovieResponse>(items, items.Count));
}); });
@@ -412,33 +437,67 @@ public static class PlaybackEndpoints
var folgen = zustände.Where(z => z.EpisodeId != null).Select(z => z.EpisodeId!.Value).ToList(); var folgen = zustände.Where(z => z.EpisodeId != null).Select(z => z.EpisodeId!.Value).ToList();
var filmdaten = await db.Movies.AsNoTracking().Where(m => filme.Contains(m.Id)) var filmdaten = await db.Movies.AsNoTracking().Where(m => filme.Contains(m.Id))
.Select(m => new { m.Id, m.Title, m.Year, m.PosterImage, m.BackdropImage }) .Select(m => new { m.Id, m.Title, m.Year, m.PosterImage, m.BackdropImage, m.PublishAt })
.ToDictionaryAsync(m => m.Id); .ToDictionaryAsync(m => m.Id);
var folgendaten = await db.Episodes.AsNoTracking().Where(f => folgen.Contains(f.Id)) var folgendaten = await db.Episodes.AsNoTracking().Where(f => folgen.Contains(f.Id))
.Join(db.Series.AsNoTracking(), f => f.SeriesId, s => s.Id, (f, s) => new .Join(db.Series.AsNoTracking(), f => f.SeriesId, s => s.Id, (f, s) => new
{ {
f.Id, f.SeasonNumber, f.EpisodeNumber, FolgenTitel = f.Title, f.StillImage, f.Id, f.SeasonNumber, f.EpisodeNumber, FolgenTitel = f.Title, f.StillImage,
SerienId = s.Id, SerienTitel = s.Title, s.PosterImage, SerienId = s.Id, SerienTitel = s.Title, s.PosterImage, f.PublishAt,
}) })
.ToDictionaryAsync(f => f.Id); .ToDictionaryAsync(f => f.Id);
var dateien = await Erstdateien(db, filme, folgen);
var liste = new List<ContinueEntry>(zustände.Count); var liste = new List<ContinueEntry>(zustände.Count);
foreach (var z in zustände) foreach (var z in zustände)
{ {
if (z.MovieId is { } film && filmdaten.TryGetValue(film, out var m)) if (z.MovieId is { } film && filmdaten.TryGetValue(film, out var m))
liste.Add(new ContinueEntry( liste.Add(new ContinueEntry(
m.Id, "movie", m.Title, null, null, null, m.Year, m.Id, "movie", m.Title, null, null, null, m.Year,
m.PosterImage, m.BackdropImage ?? m.PosterImage, Abbild(z))); m.PosterImage, m.BackdropImage ?? m.PosterImage, Abbild(z),
FileId: dateien.GetValueOrDefault(m.Id).Id,
DurationSeconds: dateien.GetValueOrDefault(m.Id).Sekunden,
PublishAt: m.PublishAt));
else if (z.EpisodeId is { } folge && folgendaten.TryGetValue(folge, out var f)) else if (z.EpisodeId is { } folge && folgendaten.TryGetValue(folge, out var f))
liste.Add(new ContinueEntry( liste.Add(new ContinueEntry(
f.Id, "episode", f.SerienTitel, f.FolgenTitel, f.SeasonNumber, f.EpisodeNumber, null, f.Id, "episode", f.SerienTitel, f.FolgenTitel, f.SeasonNumber, f.EpisodeNumber, null,
f.PosterImage, f.StillImage ?? f.PosterImage, Abbild(z), f.PosterImage, f.StillImage ?? f.PosterImage, Abbild(z),
SeriesId: f.SerienId)); SeriesId: f.SerienId,
FileId: dateien.GetValueOrDefault(f.Id).Id,
DurationSeconds: dateien.GetValueOrDefault(f.Id).Sekunden,
PublishAt: f.PublishAt));
} }
return liste; return liste;
} }
/// <summary>Je Titel die erste Datei — die, die abgespielt würde.</summary>
///
/// <remarks>
/// <b>In einer Abfrage für alle.</b> Zwanzig Zeilen auf der Startseite
/// wären sonst zwanzig Abfragen, und SQLite führt die nacheinander aus.
/// </remarks>
private static async Task<Dictionary<Guid, (Guid? Id, double? Sekunden)>> Erstdateien(
FlaroContext db, List<Guid> filme, List<Guid> folgen)
{
if (filme.Count == 0 && folgen.Count == 0) return [];
var roh = await db.MediaFiles.AsNoTracking()
.Where(d => (d.MovieId != null && filme.Contains(d.MovieId!.Value))
|| (d.EpisodeId != null && folgen.Contains(d.EpisodeId!.Value)))
.Select(d => new { d.Id, d.MovieId, d.EpisodeId, d.Duration })
.ToListAsync();
return roh
.GroupBy(d => d.MovieId ?? d.EpisodeId!.Value)
.ToDictionary(g => g.Key, g =>
{
var erste = g.First();
return ((Guid?)erste.Id, erste.Duration?.TotalSeconds);
});
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Gemeinsames // Gemeinsames
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@@ -139,6 +139,46 @@ public class PlaybackTests
}); });
} }
/// <summary>„Weiterschauen" muss sagen, was abgespielt würde.</summary>
///
/// <remarks>
/// <b>Sonst kann der Aufmacher nur blättern.</b> Er bot „Abspielen" an
/// und öffnete die Detailseite — weil ihm die Datei fehlte. Und bei einer
/// Folge führte er auf <c>#/filme/&lt;Folgenkennung&gt;</c>, wo nichts
/// steht: die Detailseite gehört der Serie, der Fortschritt der Folge.
/// </remarks>
[Test]
public async Task Weiterschauen_nennt_Datei_und_Serie()
{
Datei("Dark (2017)/Season 01/Dark - S01E01 - Geheimnisse.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items.Single();
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
var folge = folgen!.Items.Single();
await Melden(client, folge.Id, 120, 1500);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
var zeile = reihe!.Items.Single();
Assert.Multiple(() =>
{
Assert.That(zeile.Kind, Is.EqualTo("episode"));
Assert.That(zeile.SeriesId, Is.EqualTo(serie.Id),
"ohne die Serie landet der Aufmacher auf einer Folge, die als Film nicht existiert");
Assert.That(zeile.FileId, Is.EqualTo(folge.Files.Single().Id),
"ohne die Datei kann „Abspielen\u201c nicht abspielen");
// Dieselbe Länge wie die Datei selbst. Im Prüfbestand liegen
// Attrappen ohne Spuren, also steht dort beidemal nichts — der
// Punkt ist, dass es derselbe Wert ist.
Assert.That(zeile.DurationSeconds, Is.EqualTo(folge.Files.Single().DurationSeconds));
});
}
[Test] [Test]
public async Task Durchgeschautes_verlaesst_Weiterschauen() public async Task Durchgeschautes_verlaesst_Weiterschauen()
{ {
+6
View File
@@ -101,6 +101,12 @@ export type Angefangen = {
state: Stand; state: Stand;
/** Zu welcher Serie die Folge gehört — dorthin führt die Kachel. */ /** Zu welcher Serie die Folge gehört — dorthin führt die Kachel. */
seriesId: string | null; 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 = { export type Bibliothek = {
id: string; name: string; id: string; name: string;
+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 { 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 = { export type Heroeintrag = {
/** Wohin „Mehr" führt. Bei einer Folge zur Serie, nicht zur Folge. */
art: 'filme' | 'serien'; art: 'filme' | 'serien';
id: string; id: string;
titel: string; titel: string;
@@ -14,6 +15,18 @@ export type Heroeintrag = {
freigabe: string | null; freigabe: string | null;
bewertung: number | null; bewertung: number | null;
beschreibung: string | 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; const WECHSEL_MS = 9000;
@@ -40,7 +53,7 @@ const HOECHSTENS = 7;
export function hero( export function hero(
eintraege: Heroeintrag[], eintraege: Heroeintrag[],
oeffnen: (art: 'filme' | 'serien', id: string) => void, oeffnen: (art: 'filme' | 'serien', id: string) => void,
abspielen: (art: 'filme' | 'serien', id: string) => void, abspielen: (eintrag: Heroeintrag) => void,
signal?: AbortSignal, signal?: AbortSignal,
): HTMLElement | null { ): HTMLElement | null {
const mitBild = eintraege.filter(e => e.hintergrund).slice(0, HOECHSTENS); const mitBild = eintraege.filter(e => e.hintergrund).slice(0, HOECHSTENS);
@@ -76,11 +89,25 @@ export function hero(
text.className = 'hero-text'; text.className = 'hero-text';
const spielen = knopf('Abspielen', 'btn btn-primary'); 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'); 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'); const knoepfe = document.createElement('div');
knoepfe.className = 'hero-knoepfe'; knoepfe.className = 'hero-knoepfe';
knoepfe.append(spielen, mehr); knoepfe.append(bald, spielen, mehr);
inhalt.append(schriftzug, titel, meta, text, knoepfe); inhalt.append(schriftzug, titel, meta, text, knoepfe);
@@ -134,8 +161,36 @@ export function hero(
].filter(Boolean).join(' · '); ].filter(Boolean).join(' · ');
text.textContent = e.beschreibung ?? ''; text.textContent = e.beschreibung ?? '';
spielen.onclick = () => abspielen(e.art, e.id);
mehr.onclick = () => oeffnen(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); } 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. */ /** Baut Heroeinträge aus dem, was die Startseite ohnehin geholt hat. */
export function alsHero(eintrag: Film | Serie, art: 'filme' | 'serien'): Heroeintrag { export function alsHero(eintrag: Film | Serie, art: 'filme' | 'serien'): Heroeintrag {
const datei = 'files' in eintrag ? eintrag.files[0] ?? null : null;
return { return {
art, art,
id: eintrag.id, id: eintrag.id,
@@ -190,5 +246,40 @@ export function alsHero(eintrag: Film | Serie, art: 'filme' | 'serien'): Heroein
freigabe: eintrag.contentRating, freigabe: eintrag.contentRating,
bewertung: eintrag.rating, bewertung: eintrag.rating,
beschreibung: eintrag.overview, 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,
}; };
} }
+24 -6
View File
@@ -4,7 +4,8 @@ import {
} from '../api'; } from '../api';
import { leer, fehlerzeile, meldung } from '../util'; import { leer, fehlerzeile, meldung } from '../util';
import { kachel, alsKachel, angefangenAlsKachel, reihe } from '../card'; 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'; type Art = 'filme' | 'serien';
@@ -24,7 +25,12 @@ export function startseite(
const wurzel = document.createElement('div'); const wurzel = document.createElement('div');
wurzel.innerHTML = '<div class="spinner"></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 { try {
// **Alles auf einmal anfragen.** Bei fünf Mediatheken wären es sonst // **Alles auf einmal anfragen.** Bei fünf Mediatheken wären es sonst
// acht Anfragen nacheinander, und die Startseite baut sich vor den // acht Anfragen nacheinander, und die Startseite baut sich vor den
@@ -56,16 +62,28 @@ export function startseite(
heroTitel.push(e); 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) => { bibliotheken.forEach((b, i) => {
const art: Art = b.kind === 'Shows' ? 'serien' : 'filme'; const art: Art = b.kind === 'Shows' ? 'serien' : 'filme';
bestaende[i]?.items.forEach(e => dazu(alsHero(e as never, art))); 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, const aufmacher = hero(heroTitel,
(art, id) => oeffnen(art, id), (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 (aufmacher) wurzel.appendChild(aufmacher);
if (weiter.items.length) { if (weiter.items.length) {
@@ -106,7 +124,7 @@ export function startseite(
wurzel.innerHTML = ''; wurzel.innerHTML = '';
wurzel.appendChild(fehlerzeile(meldung(fehler))); wurzel.appendChild(fehlerzeile(meldung(fehler)));
} }
})(); }
return wurzel; return wurzel;
} }