Die Oberfläche nach Plex' Vorbild — Hintergrund, Schriftzug, Besetzung
Nach den Bildern deiner Plex-Installation umgebaut. Übernommen ist der Aufbau, nicht die Farbe: Plex ist bernsteingelb, Flaro bleibt Orchidee. **Kopfleiste oben** mit Marke, Suche und Konto. Die Suche gehört dorthin und nicht in jede Mediathek: wer einen Titel sucht, weiss oft gar nicht, in welcher er liegt. Sie geht quer durch alles. **Seitenleiste mit Icons** — echte SVG, keine Zeichen aus einer Schriftart. Ein ▶ aus Unicode sieht auf jedem System anders aus, lässt sich nicht einfärben und springt in der Grundlinie. Die aktive Zeile bekommt einen schmalen Balken links statt eines leuchtenden Blocks; das liest sich in einer langen Liste ruhiger. **Die Kachel steht jetzt an einer Stelle** statt an fünfen. Sie trägt den Haken für Gesehenes in der Ecke — wie bei Plex, ein Blick über das Raster genügt —, die Zahl offener Folgen an derselben Stelle, den Fortschrittsbalken unten und beim Überfahren einen Abspielknopf. Folgen sind dreizeilig: Serie, Folgentitel, „S14 · F22". **Reihen haben Pfeile.** Zwanzig Kacheln passen nicht in den Bildschirm, und quer zu scrollen geht mit dem Rad nur mit Umschalttaste. Die Überschrift nennt die Mediathek beim Namen: „Zuletzt in Dokumentationen hinzugefügt". **Mediatheken haben drei Reiter** — Empfehlungen, Mediathek, Kategorien. Darunter eine Filterzeile mit Genre, Sortierung und der Anzahl. Die Kategorien sind farbige Kacheln; die Farbe kommt aus dem Namen, nicht aus einer festen Liste, die jedes Genre kennen müsste, das TMDB je erfindet. **Die Detailseite** hat jetzt den unscharfen Hintergrund über die ganze Seite, den **Schriftzug des Films** statt der Systemschrift, eine Zeile mit Jahr, Genres, Laufzeit, Altersfreigabe und Bewertung, runde Knöpfe für gesehen/Favorit/weglegen, die Medienangaben als Tabelle — Datei, Video, Ton, Untertitel — und zuletzt Besetzung & Stab mit runden Porträts. **Dazu am Server:** ein Name. Ohne ihn zeigten die Clients den Hostnamen, und die App schrieb „localhost" über die Anmeldung. Vorgabe ist der Rechnername des Containers. --- **Der Fehler, den das erste Bild zeigte:** die Lupe in der Suchleiste war dreihundert Bildpunkte gross und lag über der halben Seite. Ein rohes `<svg>` ohne Breite füllt seinen Container; die Regel galt nur für `.icon`. Jetzt für jedes. 368 Tests grün. Am Container nachgesehen: „Der Untergang" mit eigenem Schriftzug, FSK 12, Bewertung 7,9, Genres Drama/Historie/Kriegsfilm, beide Tonspuren in der Tabelle, 22 Porträts darunter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ede4069cd4
commit
8c395ac051
+37
-111
@@ -1,18 +1,18 @@
|
||||
import {
|
||||
inhalt, weiterschauen, naechsteFolgen,
|
||||
type Bibliothek, type Film, type Serie, type Angefangen,
|
||||
inhalt, weiterschauen, naechsteFolgen, empfehlungen,
|
||||
type Bibliothek,
|
||||
} from '../api';
|
||||
import { esc, plakat, leer, fehlerzeile, anteil, verbleibend } from '../util';
|
||||
import { leer, fehlerzeile } from '../util';
|
||||
import { kachel, alsKachel, angefangenAlsKachel, reihe } from '../card';
|
||||
|
||||
type Art = 'filme' | 'serien';
|
||||
|
||||
/** Die Startseite: waagerechte Reihen, wie bei Plex.
|
||||
/** Die Startseite — waagerechte Reihen, wie bei Plex.
|
||||
*
|
||||
* Oben, was angefangen ist — darum geht man schliesslich hin. Darunter **je
|
||||
* Mediathek eine Reihe** mit dem Zuletzt-Hinzugefügten. Nicht eine Reihe für
|
||||
* alle Filme: wer „Filme" und „Dokumentationen" getrennt angelegt hat, hat
|
||||
* sie getrennt gemeint, und eine Startseite, die sie wieder zusammenwirft,
|
||||
* nimmt die Entscheidung zurück.
|
||||
* Oben, was angefangen ist. Darunter **je Mediathek eine Reihe** mit dem
|
||||
* zuletzt Hinzugefügten: wer „Filme" und „Dokumentationen" getrennt angelegt
|
||||
* hat, hat sie getrennt gemeint, und eine Startseite, die sie wieder
|
||||
* zusammenwirft, nimmt die Entscheidung zurück.
|
||||
*
|
||||
* Eine Reihe ohne Inhalt wird gar nicht erst gezeigt. */
|
||||
export function startseite(
|
||||
@@ -25,13 +25,13 @@ export function startseite(
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
// **Alles auf einmal anfragen, nicht eine Reihe nach der anderen.** Bei
|
||||
// fünf Mediatheken wären das sonst sieben Anfragen nacheinander, und
|
||||
// die Startseite baut sich vor den Augen des Benutzers Zeile für Zeile
|
||||
// auf.
|
||||
const [weiter, naechste, ...bestaende] = await Promise.all([
|
||||
// **Alles auf einmal anfragen.** Bei fünf Mediatheken wären es sonst
|
||||
// acht Anfragen nacheinander, und die Startseite baut sich vor den
|
||||
// Augen des Benutzers Zeile für Zeile auf.
|
||||
const [weiter, naechste, vorschlag, ...bestaende] = await Promise.all([
|
||||
weiterschauen(),
|
||||
naechsteFolgen(),
|
||||
empfehlungen(24).catch(() => ({ items: [], total: 0 })),
|
||||
...bibliotheken.map(b => inhalt(b, { sort: 'added', take: 24 })),
|
||||
]);
|
||||
|
||||
@@ -43,17 +43,37 @@ export function startseite(
|
||||
return;
|
||||
}
|
||||
|
||||
if (weiter.items.length) wurzel.appendChild(angefangenReihe('Weiterschauen', weiter.items));
|
||||
if (naechste.items.length) wurzel.appendChild(angefangenReihe('Nächste Folge', naechste.items));
|
||||
if (weiter.items.length) {
|
||||
wurzel.appendChild(reihe('Fortsetzen',
|
||||
weiter.items.map(e => kachel(angefangenAlsKachel(e), oeffnen))));
|
||||
}
|
||||
if (naechste.items.length) {
|
||||
wurzel.appendChild(reihe('Nächste Folge',
|
||||
naechste.items.map(e => kachel(angefangenAlsKachel(e), oeffnen))));
|
||||
}
|
||||
|
||||
let etwasDa = false;
|
||||
bibliotheken.forEach((b, i) => {
|
||||
const seite = bestaende[i];
|
||||
if (!seite?.items.length) return;
|
||||
etwasDa = true;
|
||||
wurzel.appendChild(reihe(b, seite.items, seite.total));
|
||||
wurzel.appendChild(reihe(
|
||||
// Plex schreibt den Namen der Mediathek in die Überschrift —
|
||||
// „Zuletzt in Filme hinzugefügt". Bei mehreren Mediatheken ist das
|
||||
// der Unterschied zwischen einer Reihe und sieben gleich
|
||||
// aussehenden.
|
||||
`Zuletzt in ${b.name} hinzugefügt`,
|
||||
seite.items.map(e => kachel(alsKachel(e), oeffnen)),
|
||||
seite.total > seite.items.length
|
||||
? { text: 'Alle zeigen', tun: () => alleZeigen(b) }
|
||||
: undefined));
|
||||
});
|
||||
|
||||
if (vorschlag.items.length) {
|
||||
wurzel.appendChild(reihe('Weil du Ähnliches gesehen hast',
|
||||
vorschlag.items.map(e => kachel(alsKachel(e), oeffnen))));
|
||||
}
|
||||
|
||||
if (!etwasDa && !weiter.items.length && !naechste.items.length) {
|
||||
wurzel.appendChild(leer('Noch nichts da. Starte unter Verwaltung einen Scan.'));
|
||||
}
|
||||
@@ -63,99 +83,5 @@ export function startseite(
|
||||
}
|
||||
})();
|
||||
|
||||
/** Eine Reihe aus Einträgen mit Stand — Filme und Folgen gemischt. */
|
||||
function angefangenReihe(titel: string, eintraege: Angefangen[]): HTMLElement {
|
||||
const bereich = kopfzeile(titel);
|
||||
const streifen = document.createElement('div');
|
||||
streifen.className = 'strip';
|
||||
|
||||
eintraege.forEach(eintrag => {
|
||||
const k = document.createElement('button');
|
||||
k.className = 'card';
|
||||
k.appendChild(plakat(eintrag.posterImage, eintrag.title, anteil(eintrag.state)));
|
||||
|
||||
const unten = document.createElement('div');
|
||||
unten.className = 'rowline';
|
||||
// Bei einer Folge oben die Serie, darunter wo man ist — eine Folge
|
||||
// ohne ihre Serie zu nennen hilft niemandem beim Wiedererkennen.
|
||||
const zweite = eintrag.kind === 'episode'
|
||||
? `S${String(eintrag.season).padStart(2, '0')}E${String(eintrag.episode).padStart(2, '0')}`
|
||||
+ (eintrag.subTitle ? ` · ${eintrag.subTitle}` : '')
|
||||
: verbleibend(eintrag.state);
|
||||
unten.innerHTML = `<strong>${esc(eintrag.title)}</strong>`
|
||||
+ `<span class="tiny">${esc(zweite)}</span>`;
|
||||
k.appendChild(unten);
|
||||
|
||||
// Angesprungen wird der Titel, nicht die Folge: von dort kommt man zur
|
||||
// Folge, umgekehrt fehlte der Zusammenhang.
|
||||
k.onclick = () => oeffnen(eintrag.kind === 'movie' ? 'filme' : 'serien', eintrag.id);
|
||||
streifen.appendChild(k);
|
||||
});
|
||||
|
||||
bereich.appendChild(streifen);
|
||||
return bereich;
|
||||
}
|
||||
|
||||
function reihe(b: Bibliothek, eintraege: (Film | Serie)[], gesamt: number): HTMLElement {
|
||||
const art: Art = b.kind === 'Shows' ? 'serien' : 'filme';
|
||||
// Steht mehr drin, als die Reihe zeigt, führt ein Weg dorthin. Ohne ihn
|
||||
// wäre die Reihe eine Sackgasse mit vierundzwanzig Kacheln.
|
||||
const bereich = kopfzeile(
|
||||
b.name, gesamt > eintraege.length ? () => alleZeigen(b) : undefined, gesamt);
|
||||
|
||||
const streifen = document.createElement('div');
|
||||
streifen.className = 'strip';
|
||||
|
||||
eintraege.forEach(eintrag => {
|
||||
const k = document.createElement('button');
|
||||
k.className = 'card';
|
||||
|
||||
const bild = plakat(
|
||||
eintrag.posterImage, eintrag.title,
|
||||
'state' in eintrag ? anteil(eintrag.state) : null);
|
||||
if ('unwatchedCount' in eintrag && eintrag.unwatchedCount > 0) {
|
||||
const plakette = document.createElement('span');
|
||||
plakette.className = 'offen';
|
||||
plakette.textContent = String(eintrag.unwatchedCount);
|
||||
bild.appendChild(plakette);
|
||||
}
|
||||
k.appendChild(bild);
|
||||
|
||||
const unten = document.createElement('div');
|
||||
unten.className = 'rowline';
|
||||
unten.innerHTML = `<strong>${esc(eintrag.title)}</strong>`
|
||||
+ `<span class="tiny">${esc(eintrag.year ?? '')}</span>`;
|
||||
k.appendChild(unten);
|
||||
k.onclick = () => oeffnen(art, eintrag.id);
|
||||
streifen.appendChild(k);
|
||||
});
|
||||
|
||||
bereich.appendChild(streifen);
|
||||
return bereich;
|
||||
}
|
||||
|
||||
function kopfzeile(titel: string, mehr?: () => void, gesamt?: number): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.className = 'reihe';
|
||||
|
||||
const zeile = document.createElement('div');
|
||||
zeile.className = 'reihe-kopf';
|
||||
|
||||
const h = document.createElement('h2');
|
||||
h.textContent = gesamt != null ? `${titel} · ${gesamt}` : titel;
|
||||
zeile.appendChild(h);
|
||||
|
||||
if (mehr) {
|
||||
const knopf = document.createElement('button');
|
||||
knopf.className = 'mehr';
|
||||
knopf.textContent = 'Alle zeigen';
|
||||
knopf.onclick = mehr;
|
||||
zeile.appendChild(knopf);
|
||||
}
|
||||
|
||||
bereich.appendChild(zeile);
|
||||
return bereich;
|
||||
}
|
||||
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user