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
+44
-47
@@ -1,51 +1,48 @@
|
||||
/* Die Symbole der Oberfläche.
|
||||
----------------------------------------------------------------------------
|
||||
Als Vektor, nicht als Schriftzeichen. Ein Wiedergabedreieck oder ein
|
||||
Lautsprecher-Emoji wird auf jeder Plattform anders gezeichnet, trägt fremde
|
||||
Farben, sitzt schief auf der Grundlinie und lässt sich weder in der
|
||||
Strichstärke noch in der Farbe anpassen — es sieht nach Platzhalter aus,
|
||||
weil es einer ist.
|
||||
/* Icons als SVG, nicht als Zeichen aus einer Schriftart.
|
||||
*
|
||||
* **Keine Emojis.** Ein ▶ oder ✓ aus Unicode sieht auf jedem System anders
|
||||
* aus, lässt sich nicht einfärben, nicht in der Strichstärke anpassen und
|
||||
* springt in der Grundlinie. Diese hier sind auf 24×24 gezeichnet, erben die
|
||||
* Textfarbe und skalieren mit der Schriftgrösse.
|
||||
*
|
||||
* Der Strichstil ist durchgehend derselbe: 1.8 Einheiten, runde Enden. */
|
||||
|
||||
`currentColor` erbt Farbe und Zustand vom Knopf darum. */
|
||||
const rahmen = (inhalt: string, gefuellt = false): string =>
|
||||
`<svg viewBox="0 0 24 24" aria-hidden="true" fill="${gefuellt ? 'currentColor' : 'none'}"`
|
||||
+ ` stroke="${gefuellt ? 'none' : 'currentColor'}" stroke-width="1.8"`
|
||||
+ ` stroke-linecap="round" stroke-linejoin="round">${inhalt}</svg>`;
|
||||
|
||||
type Zeichner = string | ((n: number) => string);
|
||||
export const icons = {
|
||||
start: rahmen('<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/>'),
|
||||
/** Filmstreifen — für eine Filmmediathek. */
|
||||
filme: rahmen('<rect x="3" y="4" width="18" height="16" rx="2"/>'
|
||||
+ '<path d="M7 4v16M17 4v16M3 9h4M3 15h4M17 9h4M17 15h4"/>'),
|
||||
/** Bildschirm — für eine Serienmediathek. */
|
||||
serien: rahmen('<rect x="2.5" y="4.5" width="19" height="12" rx="2"/><path d="M8 20h8"/>'),
|
||||
verwaltung: rahmen('<circle cx="12" cy="12" r="3.2"/>'
|
||||
+ '<path d="M19.4 14.5a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9a1.6 1.6 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.5 1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8V9a1.6 1.6 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1z"/>'),
|
||||
suche: rahmen('<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>'),
|
||||
abspielen: rahmen('<path d="M8 5.5v13l11-6.5z"/>', true),
|
||||
haken: rahmen('<path d="m5 12.5 4.5 4.5L19 7"/>'),
|
||||
hakenKreis: rahmen('<circle cx="12" cy="12" r="9"/><path d="m8 12.3 2.8 2.7L16 9.6"/>'),
|
||||
stift: rahmen('<path d="M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3z"/>'),
|
||||
download: rahmen('<path d="M12 3v12"/><path d="m7.5 11 4.5 4.5L16.5 11"/><path d="M4 20h16"/>'),
|
||||
links: rahmen('<path d="M15 5 8 12l7 7"/>'),
|
||||
rechts: rahmen('<path d="m9 5 7 7-7 7"/>'),
|
||||
mehr: rahmen('<circle cx="12" cy="5.5" r="1.3" fill="currentColor" stroke="none"/>'
|
||||
+ '<circle cx="12" cy="12" r="1.3" fill="currentColor" stroke="none"/>'
|
||||
+ '<circle cx="12" cy="18.5" r="1.3" fill="currentColor" stroke="none"/>'),
|
||||
abmelden: rahmen('<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3"/>'
|
||||
+ '<path d="M10 16.5 14.5 12 10 7.5"/><path d="M14.5 12H4"/>'),
|
||||
stern: rahmen('<path d="m12 3.6 2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8L3.5 9.8l5.9-.9z"/>'),
|
||||
} as const;
|
||||
|
||||
const ICONS: Record<string, Zeichner> = {
|
||||
play: '<path d="M8 5.5v13l11-6.5z" fill="currentColor" stroke="none"/>',
|
||||
waveform: '<path d="M3 12h2M7 8v8M11 4.5v15M15 8v8M19 11h2"/>',
|
||||
speed: '<path d="M12 20a8 8 0 100-16 8 8 0 000 16z"/><path d="M12 12l4-3"/>',
|
||||
// Kreispfeile mit der Sekundenzahl darin — dasselbe Bild wie
|
||||
// `gobackward.10` und `goforward.30` auf dem iPad.
|
||||
goBack: (n) => `<path d="M11.5 4.5L7 8l4.5 3.5" /><path d="M7 8h4.5a7.5 7.5 0 11-7.2 9.6"/>`
|
||||
+ `<text x="12" y="17.6" text-anchor="middle" font-size="7.5" font-weight="700"`
|
||||
+ ` fill="currentColor" stroke="none" font-family="inherit">${n}</text>`,
|
||||
goFwd: (n) => `<path d="M12.5 4.5L17 8l-4.5 3.5"/><path d="M17 8h-4.5a7.5 7.5 0 107.2 9.6"/>`
|
||||
+ `<text x="12" y="17.6" text-anchor="middle" font-size="7.5" font-weight="700"`
|
||||
+ ` fill="currentColor" stroke="none" font-family="inherit">${n}</text>`,
|
||||
captions: '<rect x="3" y="5.5" width="18" height="13" rx="3"/><path d="M9.5 10.5a2.2 2.2 0 100 3M16 10.5a2.2 2.2 0 100 3"/>',
|
||||
captionsOn: '<rect x="3" y="5.5" width="18" height="13" rx="3" fill="currentColor" stroke="none"/><path d="M9.5 10.5a2.2 2.2 0 100 3M16 10.5a2.2 2.2 0 100 3" stroke="#0B0B0D"/>',
|
||||
check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
|
||||
// Rechteck mit Balken oben bzw. Spalte links — die Leiste zieht dorthin.
|
||||
barTop: '<rect x="2" y="3.5" width="20" height="17" rx="3"/><path d="M2 9h20"/>'
|
||||
+ '<rect x="2" y="3.5" width="20" height="5.5" rx="3" fill="currentColor" stroke="none" opacity=".9"/>',
|
||||
barSide: '<rect x="2" y="3.5" width="20" height="17" rx="3"/><path d="M8.5 3.5v17"/>'
|
||||
+ '<rect x="2" y="3.5" width="6.5" height="17" rx="3" fill="currentColor" stroke="none" opacity=".9"/>',
|
||||
pause: '<path d="M9 5.5v13M15 5.5v13"/>',
|
||||
close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',
|
||||
sound: '<path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5H4z" fill="currentColor"/><path d="M16 9a4 4 0 010 6"/><path d="M18.5 6.5a7.5 7.5 0 010 11"/>',
|
||||
muted: '<path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5H4z" fill="currentColor"/><path d="M16 9.5l5 5M21 9.5l-5 5"/>',
|
||||
expand: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>',
|
||||
left: '<path d="M14.5 5.5L8 12l6.5 6.5"/>',
|
||||
right: '<path d="M9.5 5.5L16 12l-6.5 6.5"/>',
|
||||
up: '<path d="M12 18.5v-13M6.5 11L12 5.5 17.5 11"/>',
|
||||
down: '<path d="M12 5.5v13M6.5 13L12 18.5 17.5 13"/>',
|
||||
}
|
||||
|
||||
/** Ein Symbol als Vektor. `size` nur, wo es von der Vorgabe abweicht. */
|
||||
export function icon(name: keyof typeof ICONS, size?: number, arg?: number): string {
|
||||
const eintrag = ICONS[name];
|
||||
const inhalt = typeof eintrag === 'function' ? eintrag(arg ?? 0) : eintrag;
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"
|
||||
${size ? `style="width:${size}px;height:${size}px"` : ''}>${inhalt}</svg>`;
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
/** Ein Icon als Element — für alles, was nicht aus einer Zeichenkette gebaut wird. */
|
||||
export function icon(name: IconName, klasse = 'icon'): HTMLElement {
|
||||
const h = document.createElement('span');
|
||||
h.className = klasse;
|
||||
h.innerHTML = icons[name];
|
||||
return h;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user