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:
Scarriffle
2026-10-04 01:04:27 +02:00
co-authored by Claude Opus 5
parent ede4069cd4
commit 8c395ac051
15 changed files with 1428 additions and 693 deletions
+59
View File
@@ -0,0 +1,59 @@
import { filme, serien } from '../api';
import { esc, leer, fehlerzeile } from '../util';
import { kachel, alsKachel } from '../card';
/** Suchergebnisse quer durch alle Mediatheken.
*
* **Nicht je Mediathek.** Wer einen Titel sucht, weiss oft gar nicht, in
* welcher er liegt — genau deshalb steht die Suche oben in der Kopfleiste
* und nicht in jeder Mediathek einzeln. */
export function suchseite(
text: string,
oeffnen: (art: 'filme' | 'serien', id: string) => void,
): HTMLElement {
const wurzel = document.createElement('div');
const kopf = document.createElement('div');
kopf.className = 'seitenkopf';
kopf.innerHTML = `<div class="wer"><h1>${esc(text)}</h1>`
+ '<div class="quelle">Suchergebnisse</div></div>';
wurzel.appendChild(kopf);
const buehne = document.createElement('div');
buehne.innerHTML = '<div class="spinner"></div>';
wurzel.appendChild(buehne);
void (async () => {
try {
const [f, s] = await Promise.all([
filme({ search: text, take: 100 }),
serien({ search: text, take: 100 }),
]);
buehne.innerHTML = '';
if (f.total === 0 && s.total === 0) {
buehne.appendChild(leer(`Nichts gefunden für „${text}".`));
return;
}
if (f.total > 0) buehne.appendChild(abschnitt(`Filme · ${f.total}`, f.items));
if (s.total > 0) buehne.appendChild(abschnitt(`Serien · ${s.total}`, s.items));
} catch (fehler) {
buehne.innerHTML = '';
buehne.appendChild(fehlerzeile((fehler as Error).message));
}
})();
function abschnitt(titel: string, eintraege: Parameters<typeof alsKachel>[0][]): HTMLElement {
const bereich = document.createElement('section');
const h = document.createElement('h2');
h.textContent = titel;
const gitter = document.createElement('div');
gitter.className = 'raster';
eintraege.forEach(e => gitter.appendChild(kachel(alsKachel(e), oeffnen)));
bereich.append(h, gitter);
return bereich;
}
return wurzel;
}