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
@@ -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/<Folgenkennung></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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user