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
+4
-40
@@ -1,4 +1,4 @@
|
||||
import { bildAdresse } from './api';
|
||||
|
||||
|
||||
export function esc(wert: unknown): string {
|
||||
return String(wert ?? '').replace(/[&<>"']/g, z =>
|
||||
@@ -20,42 +20,6 @@ export function groesse(bytes: number): string {
|
||||
return `${Math.round(bytes / 1024)} KB`;
|
||||
}
|
||||
|
||||
/** Ein Plakat mit Platzhalter dahinter.
|
||||
*
|
||||
* Der Platzhalter hält die Fläche frei, bis das Bild da ist — ohne ihn
|
||||
* springt das Raster beim Nachladen jedes Mal um. */
|
||||
export function plakat(
|
||||
schluessel: string | null, beschriftung: string, fortschritt?: number | null,
|
||||
): HTMLElement {
|
||||
const rahmen = document.createElement('div');
|
||||
rahmen.className = 'poster';
|
||||
rahmen.setAttribute('role', 'img');
|
||||
rahmen.setAttribute('aria-label', beschriftung);
|
||||
|
||||
const adresse = bildAdresse(schluessel);
|
||||
if (adresse) {
|
||||
const img = document.createElement('img');
|
||||
img.src = adresse;
|
||||
img.alt = '';
|
||||
img.loading = 'lazy';
|
||||
// Erst einblenden, wenn es da ist — sonst blitzt der Platzhalter durch.
|
||||
img.onload = () => rahmen.classList.add('geladen');
|
||||
rahmen.appendChild(img);
|
||||
}
|
||||
|
||||
// Der Balken unten, wie bei Plex. Er erscheint, sobald überhaupt etwas
|
||||
// gesehen wurde — es gibt keinen Anteil, der dafür zu klein wäre.
|
||||
if (fortschritt != null && fortschritt > 0) {
|
||||
const balken = document.createElement('div');
|
||||
balken.className = 'fortschritt';
|
||||
const gefuellt = document.createElement('div');
|
||||
gefuellt.style.width = `${Math.min(100, fortschritt * 100)}%`;
|
||||
balken.appendChild(gefuellt);
|
||||
rahmen.appendChild(balken);
|
||||
}
|
||||
return rahmen;
|
||||
}
|
||||
|
||||
/** Wie weit ein Stand gediehen ist, als Anteil — oder nichts. */
|
||||
export function anteil(stand: { positionSeconds: number; runtimeSeconds: number | null } | null | undefined)
|
||||
: number | null {
|
||||
@@ -79,7 +43,7 @@ export function knopf(text: string, klasse = 'btn'): HTMLButtonElement {
|
||||
|
||||
export function feld(platzhalter: string, art = 'text'): HTMLInputElement {
|
||||
const i = document.createElement('input');
|
||||
i.className = 'field';
|
||||
i.className = 'feld';
|
||||
i.type = art;
|
||||
i.placeholder = platzhalter;
|
||||
return i;
|
||||
@@ -88,14 +52,14 @@ export function feld(platzhalter: string, art = 'text'): HTMLInputElement {
|
||||
/** Zeigt einen Fehler, ohne die Seite zu verlassen. */
|
||||
export function fehlerzeile(text: string): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'error';
|
||||
d.className = 'fehler';
|
||||
d.textContent = text;
|
||||
return d;
|
||||
}
|
||||
|
||||
export function leer(text: string): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'empty';
|
||||
d.className = 'leer';
|
||||
d.textContent = text;
|
||||
return d;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user