Mehrere Mediatheken, jede für sich
Bisher gab es in der Seitenleiste „Filme" und „Serien" — zwei feste Zeilen, hinter denen alles zusammenfiel, was der Server kannte. Wer „Filme" und „Dokumentationen" getrennt angelegt hatte, sah sie wieder in einem Topf. Das nahm die Entscheidung zurück, sie zu trennen. Jetzt führt die Seitenleiste **jede Mediathek einzeln**, mit Namen und Zahl, wie bei Plex und Jellyfin. Die Startseite bekommt je Mediathek eine Reihe mit dem zuletzt Hinzugefügten; steht mehr drin, als die Reihe zeigt, führt „Alle zeigen" dorthin. Die Mediathekenseite trägt den Namen im Titel, sucht innerhalb der Mediathek und lässt sich sortieren. **Am Server dafür:** `?sort=` auf `/movies` und `/series` — `title`, `added`, `year`. Der Titel immer als letzter Vergleich, sonst hätten zwei Filme aus demselben Jahr keine feste Reihenfolge und Seite 2 zeigte denselben Film noch einmal oder gar nicht. Die Liste der Bibliotheken nennt je Zeile einen Zählwert. „Dokumentationen · 0" sagt mehr als ein Eintrag, hinter dem sich nach dem Klick nichts findet — da ist der Unterschied zwischen „noch nicht gescannt" und „falscher Pfad" wenigstens sichtbar. `null` heisst „nicht gezählt", nicht „leer". Umbenennen und Entfernen, beides nur für Administratoren. **Umbenennen, mehr nicht:** die Art zu ändern hiesse, Filme in Serien zu verwandeln; den Pfad zu ändern hiesse, einen Bestand gegen einen anderen zu tauschen, während die Einträge stehenbleiben. Beides ist kein Bearbeiten, sondern eine neue Bibliothek. **Beim Entfernen bleiben die Mediendateien liegen.** Weg sind der Eintrag, die Zuordnungen und der Sehfortschritt aller Benutzer dazu; die Oberfläche sagt das vorher, und eine Prüfung hält fest, dass die Datei danach noch da ist. Ein Server, der beim Aufräumen in der Oberfläche fremde Filme löscht, ist einer, den man nicht noch einmal benutzt. --- **Der Fehler, den der Browser zeigte, und er betrifft jeden Betrieb:** nach einem Server-Update lief weiter die alte Oberfläche. Die Dateien unter `assets/` tragen eine Prüfsumme im Namen — ändert sich der Inhalt, ändert sich der Name. `index.html` trägt keine; sie nennt nur deren Namen. Lag sie im Zwischenspeicher, bekam der Browser weiter die alte App: nicht kaputt, sondern veraltet, und das fällt niemandem auf. Nachgemessen: geladen war `index-C0K3V8eg.js`, ausgeliefert wurde längst `index-DomHN4_b.js`. Jetzt zwei Lebensdauern — `no-cache` für die Seite, ein Jahr und `immutable` für die Bausteine. Mit Prüfung, und am Container nachgemessen. Der Beispielbestand bekommt zwei weitere Mediatheken, „Dokumentationen" und „Konzerte". Mit nur Filme und Serien liesse sich gar nicht prüfen, ob mehrere wirklich getrennt bleiben — zwei Bestände **derselben Art** sind der Fall, in dem es auffällt. 358 Tests grün. Am Container nachgewiesen: vier Mediatheken in der Seitenleiste mit Zählwerten, je eine Reihe auf der Startseite, und „Dokumentationen" zeigt zwei Titel statt sieben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
172b38da06
commit
230beb9664
@@ -1,19 +1,22 @@
|
||||
import {
|
||||
bibliotheken, bibliothekAnlegen, scanStarten, scanstand,
|
||||
bibliotheken, bibliothekAnlegen, bibliothekUmbenennen, bibliothekLoeschen,
|
||||
scanStarten, scanstand,
|
||||
benutzer, benutzerAnlegen, metadatenEinstellung, metadatenSetzen,
|
||||
type Bibliothek,
|
||||
} from '../api';
|
||||
import { esc, knopf, feld, fehlerzeile, leer } from '../util';
|
||||
|
||||
export function verwaltung(): HTMLElement {
|
||||
/// `geaendert` wird gerufen, wenn sich die Mediathekenliste geändert hat —
|
||||
/// die Seitenleiste steht auf ihr und muss dann neu gezeichnet werden.
|
||||
export function verwaltung(geaendert: () => void = () => {}): HTMLElement {
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.append(bibliotheksbereich(), benutzerbereich(), metadatenbereich());
|
||||
wurzel.append(bibliotheksbereich(geaendert), benutzerbereich(), metadatenbereich());
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
/* ---------- Bibliotheken ---------- */
|
||||
|
||||
function bibliotheksbereich(): HTMLElement {
|
||||
function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
const h = document.createElement('h2');
|
||||
h.textContent = 'Bibliotheken';
|
||||
@@ -43,12 +46,69 @@ function bibliotheksbereich(): HTMLElement {
|
||||
function block(b: Bibliothek): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'setting';
|
||||
d.innerHTML = `<div class="rowline"><strong>${esc(b.name)}</strong>`
|
||||
+ `<span class="tiny">${esc(b.kind)} · ${esc(b.path)}</span></div>`;
|
||||
|
||||
const kopf = document.createElement('div');
|
||||
kopf.className = 'rowline';
|
||||
// „1 Filme" ist der Satz, an dem man sieht, dass niemand hingeschaut hat.
|
||||
const serie = b.kind === 'Shows';
|
||||
const anzahl = b.itemCount == null ? ''
|
||||
: ` · ${b.itemCount} ` + (b.itemCount === 1
|
||||
? (serie ? 'Serie' : 'Film')
|
||||
: (serie ? 'Serien' : 'Filme'));
|
||||
kopf.innerHTML = `<strong>${esc(b.name)}</strong>`
|
||||
+ `<span class="tiny">${esc(b.path)}${esc(anzahl)}</span>`;
|
||||
d.appendChild(kopf);
|
||||
|
||||
const stand = document.createElement('div');
|
||||
stand.className = 'hint';
|
||||
const los = knopf('Scannen', 'btn');
|
||||
const umbenennen = knopf('Umbenennen', 'btn');
|
||||
const entfernen = knopf('Entfernen', 'btn');
|
||||
|
||||
umbenennen.onclick = () => {
|
||||
umbenennen.remove();
|
||||
const eingabe = feld('Neuer Name');
|
||||
eingabe.value = b.name;
|
||||
const fertig = knopf('Speichern', 'btn btn-primary');
|
||||
const reihe = document.createElement('div');
|
||||
reihe.className = 'btn-row';
|
||||
reihe.append(eingabe, fertig);
|
||||
d.appendChild(reihe);
|
||||
eingabe.focus();
|
||||
|
||||
fertig.onclick = async () => {
|
||||
fertig.disabled = true;
|
||||
try {
|
||||
await bibliothekUmbenennen(b.id, eingabe.value.trim());
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile((fehler as Error).message));
|
||||
fertig.disabled = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
entfernen.onclick = async () => {
|
||||
// **Einmal nachfragen.** Was hier verschwindet, ist der Eintrag und
|
||||
// der Sehfortschritt aller Benutzer dazu — die Mediendateien bleiben
|
||||
// liegen, aber das weiss nicht jeder, der auf den Knopf sieht.
|
||||
const sicher = window.confirm(
|
||||
`„${b.name}" aus Flaro entfernen?\n\n`
|
||||
+ 'Die Mediendateien auf der Platte bleiben unangetastet. Weg sind die '
|
||||
+ 'Einträge, die Zuordnungen und der Sehfortschritt aller Benutzer dazu.');
|
||||
if (!sicher) return;
|
||||
|
||||
entfernen.disabled = true;
|
||||
try {
|
||||
await bibliothekLoeschen(b.id);
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile((fehler as Error).message));
|
||||
entfernen.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
los.onclick = async () => {
|
||||
los.disabled = true;
|
||||
@@ -79,7 +139,10 @@ function bibliotheksbereich(): HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
d.append(los, stand);
|
||||
const knoepfe = document.createElement('div');
|
||||
knoepfe.className = 'btn-row';
|
||||
knoepfe.append(los, umbenennen, entfernen);
|
||||
d.append(knoepfe, stand);
|
||||
void verfolgen();
|
||||
return d;
|
||||
}
|
||||
@@ -107,6 +170,7 @@ function bibliotheksbereich(): HTMLElement {
|
||||
await bibliothekAnlegen(name.value.trim(), art.value, pfad.value.trim());
|
||||
name.value = ''; pfad.value = '';
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) { form.appendChild(fehlerzeile((fehler as Error).message)); }
|
||||
los.disabled = false;
|
||||
};
|
||||
|
||||
+73
-29
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
filme, serien, weiterschauen, naechsteFolgen,
|
||||
type Film, type Serie, type Angefangen,
|
||||
inhalt, weiterschauen, naechsteFolgen,
|
||||
type Bibliothek, type Film, type Serie, type Angefangen,
|
||||
} from '../api';
|
||||
import { esc, plakat, leer, fehlerzeile, anteil, verbleibend } from '../util';
|
||||
|
||||
@@ -8,29 +8,55 @@ type Art = 'filme' | 'serien';
|
||||
|
||||
/** Die Startseite: waagerechte Reihen, wie bei Plex.
|
||||
*
|
||||
* Oben, was angefangen ist — darum geht man schliesslich hin. Darunter die
|
||||
* Mediathek. Eine Reihe, die nichts enthält, wird gar nicht erst gezeigt. */
|
||||
export function startseite(oeffnen: (art: Art, id: string) => void): HTMLElement {
|
||||
* 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.
|
||||
*
|
||||
* Eine Reihe ohne Inhalt wird gar nicht erst gezeigt. */
|
||||
export function startseite(
|
||||
bibliotheken: Bibliothek[],
|
||||
oeffnen: (art: Art, id: string) => void,
|
||||
alleZeigen: (b: Bibliothek) => void,
|
||||
): HTMLElement {
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [weiter, naechste, f, s] = await Promise.all([
|
||||
weiterschauen(), naechsteFolgen(), filme({ take: 24 }), serien({ take: 24 }),
|
||||
// **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([
|
||||
weiterschauen(),
|
||||
naechsteFolgen(),
|
||||
...bibliotheken.map(b => inhalt(b, { sort: 'added', take: 24 })),
|
||||
]);
|
||||
|
||||
wurzel.innerHTML = '';
|
||||
|
||||
if (f.total === 0 && s.total === 0) {
|
||||
wurzel.appendChild(leer('Noch nichts da. Lege unter Verwaltung eine Bibliothek an '
|
||||
if (bibliotheken.length === 0) {
|
||||
wurzel.appendChild(leer('Noch keine Mediathek. Lege unter Verwaltung eine an '
|
||||
+ 'und starte einen Scan.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (weiter.items.length) wurzel.appendChild(angefangenReihe('Weiterschauen', weiter));
|
||||
if (naechste.items.length) wurzel.appendChild(angefangenReihe('Nächste Folge', naechste));
|
||||
if (f.total > 0) wurzel.appendChild(reihe(`Filme · ${f.total}`, f.items, 'filme'));
|
||||
if (s.total > 0) wurzel.appendChild(reihe(`Serien · ${s.total}`, s.items, 'serien'));
|
||||
if (weiter.items.length) wurzel.appendChild(angefangenReihe('Weiterschauen', weiter.items));
|
||||
if (naechste.items.length) wurzel.appendChild(angefangenReihe('Nächste Folge', naechste.items));
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
if (!etwasDa && !weiter.items.length && !naechste.items.length) {
|
||||
wurzel.appendChild(leer('Noch nichts da. Starte unter Verwaltung einen Scan.'));
|
||||
}
|
||||
} catch (fehler) {
|
||||
wurzel.innerHTML = '';
|
||||
wurzel.appendChild(fehlerzeile((fehler as Error).message));
|
||||
@@ -38,17 +64,12 @@ export function startseite(oeffnen: (art: Art, id: string) => void): HTMLElement
|
||||
})();
|
||||
|
||||
/** Eine Reihe aus Einträgen mit Stand — Filme und Folgen gemischt. */
|
||||
function angefangenReihe(titel: string, seite: { items: Angefangen[] }): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.className = 'reihe';
|
||||
|
||||
const h = document.createElement('h2');
|
||||
h.textContent = titel;
|
||||
|
||||
function angefangenReihe(titel: string, eintraege: Angefangen[]): HTMLElement {
|
||||
const bereich = kopfzeile(titel);
|
||||
const streifen = document.createElement('div');
|
||||
streifen.className = 'strip';
|
||||
|
||||
seite.items.forEach(eintrag => {
|
||||
eintraege.forEach(eintrag => {
|
||||
const k = document.createElement('button');
|
||||
k.className = 'card';
|
||||
k.appendChild(plakat(eintrag.posterImage, eintrag.title, anteil(eintrag.state)));
|
||||
@@ -71,16 +92,16 @@ export function startseite(oeffnen: (art: Art, id: string) => void): HTMLElement
|
||||
streifen.appendChild(k);
|
||||
});
|
||||
|
||||
bereich.append(h, streifen);
|
||||
bereich.appendChild(streifen);
|
||||
return bereich;
|
||||
}
|
||||
|
||||
function reihe(titel: string, eintraege: (Film | Serie)[], art: Art): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.className = 'reihe';
|
||||
|
||||
const h = document.createElement('h2');
|
||||
h.textContent = titel;
|
||||
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';
|
||||
@@ -109,7 +130,30 @@ export function startseite(oeffnen: (art: Art, id: string) => void): HTMLElement
|
||||
streifen.appendChild(k);
|
||||
});
|
||||
|
||||
bereich.append(h, streifen);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { filme, serien, type Film, type Serie } from '../api';
|
||||
import { inhalt, type Bibliothek, type Film, type Serie, type Ordnung } from '../api';
|
||||
import { esc, plakat, leer, fehlerzeile, anteil } from '../util';
|
||||
|
||||
type Art = 'filme' | 'serien';
|
||||
|
||||
/** Eine Bibliothek als Raster.
|
||||
/** Eine Mediathek als Raster.
|
||||
*
|
||||
* Welche Art gezeigt wird, entscheidet die Seitenleiste — wie bei Plex, wo
|
||||
* jede Bibliothek ihr eigener Punkt in der Navigation ist. Eine zweite
|
||||
* Umschaltung innerhalb der Seite wäre dieselbe Entscheidung an zwei Orten.
|
||||
* **Eine Ansicht je Mediathek, nicht eine je Art.** Welche gezeigt wird,
|
||||
* entscheidet die Seitenleiste — wie bei Plex, wo jede Mediathek ihr eigener
|
||||
* Punkt in der Navigation ist. Wer „Filme" und „Dokumentationen" getrennt
|
||||
* angelegt hat, hat sie getrennt gemeint.
|
||||
*
|
||||
* Gesucht wird am Server, nicht im Browser: bei zehntausend Titeln wäre
|
||||
* alles zu laden und dann zu filtern der falsche Weg. */
|
||||
export function mediathek(art: Art, oeffnen: (art: Art, id: string) => void): HTMLElement {
|
||||
* Gesucht und sortiert wird am Server: bei zehntausend Titeln wäre alles zu
|
||||
* laden und dann zu sieben der falsche Weg. */
|
||||
export function mediathek(
|
||||
bibliothek: Bibliothek,
|
||||
oeffnen: (art: Art, id: string) => void,
|
||||
): HTMLElement {
|
||||
const wurzel = document.createElement('div');
|
||||
const art: Art = bibliothek.kind === 'Shows' ? 'serien' : 'filme';
|
||||
let suche = '';
|
||||
let ordnung: Ordnung = 'title';
|
||||
let tippt: number | undefined;
|
||||
|
||||
const kopf = document.createElement('div');
|
||||
@@ -21,12 +27,26 @@ export function mediathek(art: Art, oeffnen: (art: Art, id: string) => void): HT
|
||||
|
||||
const titel = document.createElement('h1');
|
||||
titel.className = 'seitentitel';
|
||||
titel.textContent = art === 'filme' ? 'Filme' : 'Serien';
|
||||
titel.textContent = bibliothek.name;
|
||||
|
||||
const sortierung = document.createElement('select');
|
||||
sortierung.className = 'field schmal';
|
||||
([
|
||||
['title', 'A–Z'],
|
||||
['added', 'Zuletzt hinzugefügt'],
|
||||
['year', 'Jahr'],
|
||||
] as [Ordnung, string][]).forEach(([wert, text]) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = wert;
|
||||
o.textContent = text;
|
||||
sortierung.appendChild(o);
|
||||
});
|
||||
sortierung.onchange = () => { ordnung = sortierung.value as Ordnung; void laden(); };
|
||||
|
||||
const suchfeld = document.createElement('input');
|
||||
suchfeld.className = 'field';
|
||||
suchfeld.type = 'search';
|
||||
suchfeld.placeholder = 'Suchen';
|
||||
suchfeld.placeholder = `In ${bibliothek.name} suchen`;
|
||||
suchfeld.oninput = () => {
|
||||
// Nicht bei jedem Tastendruck fragen: das wären bei „Untergang" neun
|
||||
// Anfragen, von denen acht niemanden interessieren.
|
||||
@@ -34,7 +54,7 @@ export function mediathek(art: Art, oeffnen: (art: Art, id: string) => void): HT
|
||||
tippt = window.setTimeout(() => { suche = suchfeld.value.trim(); void laden(); }, 250);
|
||||
};
|
||||
|
||||
kopf.append(titel, suchfeld);
|
||||
kopf.append(titel, sortierung, suchfeld);
|
||||
|
||||
const raster = document.createElement('div');
|
||||
raster.className = 'grid';
|
||||
@@ -45,15 +65,13 @@ export function mediathek(art: Art, oeffnen: (art: Art, id: string) => void): HT
|
||||
raster.innerHTML = '';
|
||||
raster.appendChild(ladeanzeige());
|
||||
try {
|
||||
const seite = art === 'filme'
|
||||
? await filme({ search: suche, take: 100 })
|
||||
: await serien({ search: suche, take: 100 });
|
||||
const seite = await inhalt(bibliothek, { search: suche, sort: ordnung, take: 100 });
|
||||
|
||||
raster.innerHTML = '';
|
||||
if (seite.items.length === 0) {
|
||||
raster.appendChild(leer(suche
|
||||
? `Nichts gefunden für „${suche}“.`
|
||||
: 'Noch nichts da. Lege unter Verwaltung eine Bibliothek an und starte einen Scan.'));
|
||||
: `„${bibliothek.name}“ ist leer. Starte unter Verwaltung einen Scan.`));
|
||||
return;
|
||||
}
|
||||
seite.items.forEach(eintrag => raster.appendChild(karte(eintrag)));
|
||||
|
||||
Reference in New Issue
Block a user