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
+19 -19
View File
@@ -22,7 +22,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
h.textContent = 'Bibliotheken';
const liste = document.createElement('div');
liste.className = 'settings';
liste.className = 'einstellungen';
bereich.append(h, liste, anlegeform());
void laden();
@@ -45,10 +45,10 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
function block(b: Bibliothek): HTMLElement {
const d = document.createElement('div');
d.className = 'setting';
d.className = 'einstellung';
const kopf = document.createElement('div');
kopf.className = 'rowline';
kopf.className = 'zeile';
// „1 Filme" ist der Satz, an dem man sieht, dass niemand hingeschaut hat.
const serie = b.kind === 'Shows';
const anzahl = b.itemCount == null ? ''
@@ -56,11 +56,11 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
? (serie ? 'Serie' : 'Film')
: (serie ? 'Serien' : 'Filme'));
kopf.innerHTML = `<strong>${esc(b.name)}</strong>`
+ `<span class="tiny">${esc(b.path)}${esc(anzahl)}</span>`;
+ `<span class="winzig">${esc(b.path)}${esc(anzahl)}</span>`;
d.appendChild(kopf);
const stand = document.createElement('div');
stand.className = 'hint';
stand.className = 'hinweis';
const los = knopf('Scannen', 'btn');
const umbenennen = knopf('Umbenennen', 'btn');
const entfernen = knopf('Entfernen', 'btn');
@@ -71,7 +71,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
eingabe.value = b.name;
const fertig = knopf('Speichern', 'btn btn-primary');
const reihe = document.createElement('div');
reihe.className = 'btn-row';
reihe.className = 'knopfreihe';
reihe.append(eingabe, fertig);
d.appendChild(reihe);
eingabe.focus();
@@ -140,7 +140,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
}
const knoepfe = document.createElement('div');
knoepfe.className = 'btn-row';
knoepfe.className = 'knopfreihe';
knoepfe.append(los, umbenennen, entfernen);
d.append(knoepfe, stand);
void verfolgen();
@@ -149,7 +149,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
function anlegeform(): HTMLElement {
const form = document.createElement('div');
form.className = 'setting';
form.className = 'einstellung';
const name = feld('Name, etwa Filme');
const pfad = feld('Pfad im Container, etwa /media/Filme');
@@ -159,7 +159,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
const los = knopf('Anlegen', 'btn btn-primary');
const reihe = document.createElement('div');
reihe.className = 'btn-row';
reihe.className = 'knopfreihe';
reihe.append(name, art, pfad, los);
form.append(reihe);
@@ -187,21 +187,21 @@ function benutzerbereich(): HTMLElement {
const h = document.createElement('h2');
h.textContent = 'Benutzer';
const liste = document.createElement('div');
liste.className = 'settings';
liste.className = 'einstellungen';
const name = feld('Benutzername');
const passwort = feld('Passwort, mindestens 8 Zeichen', 'password');
const admin = document.createElement('label');
admin.className = 'hint';
admin.className = 'hinweis';
const haken = document.createElement('input');
haken.type = 'checkbox';
admin.append(haken, document.createTextNode(' Administrator'));
const los = knopf('Anlegen', 'btn');
const form = document.createElement('div');
form.className = 'setting';
form.className = 'einstellung';
const reihe = document.createElement('div');
reihe.className = 'btn-row';
reihe.className = 'knopfreihe';
reihe.append(name, passwort, admin, los);
form.append(reihe);
@@ -215,9 +215,9 @@ function benutzerbereich(): HTMLElement {
liste.innerHTML = '';
alle.forEach(b => {
const d = document.createElement('div');
d.className = 'setting';
d.innerHTML = `<div class="rowline"><strong>${esc(b.userName)}</strong>`
+ `<span class="tiny">${b.isAdministrator ? 'Administrator' : 'Benutzer'}</span></div>`;
d.className = 'einstellung';
d.innerHTML = `<div class="zeile"><strong>${esc(b.userName)}</strong>`
+ `<span class="winzig">${b.isAdministrator ? 'Administrator' : 'Benutzer'}</span></div>`;
liste.appendChild(d);
});
} catch (fehler) {
@@ -247,7 +247,7 @@ function metadatenbereich(): HTMLElement {
const h = document.createElement('h2');
h.textContent = 'Metadaten';
const block = document.createElement('div');
block.className = 'setting';
block.className = 'einstellung';
bereich.append(h, block);
void (async () => {
@@ -255,7 +255,7 @@ function metadatenbereich(): HTMLElement {
const stand = await metadatenEinstellung();
const auskunft = document.createElement('div');
auskunft.className = 'hint';
auskunft.className = 'hinweis';
auskunft.textContent = stand.usingProjectKey
? 'Es wird der mitgelieferte TMDB-Zugang benutzt. Ein eigener löst ihn ab.'
: stand.hasApiKey ? 'Ein eigener TMDB-Zugang ist hinterlegt.'
@@ -267,7 +267,7 @@ function metadatenbereich(): HTMLElement {
const los = knopf('Speichern', 'btn');
const reihe = document.createElement('div');
reihe.className = 'btn-row';
reihe.className = 'knopfreihe';
reihe.append(schluessel, sprache, los);
los.onclick = async () => {