Die Verwaltung zeichnete sich endlos neu
Gemessen am laufenden Server: nach dem Scan lief eine Schleife mit zwei Abrufen je Runde, sichtbar als vier dauernd drehende Spinner. Der Weg dahin: verfolgen() lief bei jedem Aufbau der Mediathekenzeile, sah den Zustand finished, meldete das über geaendert(), und geaendert() war mediathekenNeuLaden — also neu zeichnen. Der Neuaufbau startete verfolgen() erneut, das wieder finished sah. Dass der Lauf längst vorbei war, machte keinen Unterschied; der Zustand bleibt im Speicher stehen. Zwei Dinge dagegen. verfolgen() meldet nur noch, wenn es selbst einen Lauf angestossen hat — beim Aufbau zeigt es den Stand an und schweigt. Und die Verwaltung frischt über zaehlerAuffrischen() nur noch die Seitenleiste auf statt die ganze Seite neu zu zeichnen. Dazu zwei Befunde aus dem Plan, die sich beim Nachsehen als falsch erwiesen: die Takte werden sehr wohl abbestellt, dafür gibt es seit je einen MutationObserver, und das Dashboard tauscht seinen Inhalt erst nach dem Abruf. Geblieben ist der Spinner in den Listen, der bei jedem Auffrischen erschien — der kommt jetzt nur noch beim ersten Aufbau. Dazu der Zwischenspeicher für Ansichten. Der Router hängt sie aus statt sie zu zerstören; beim Zurückkommen stehen sie sofort wieder da, mitsamt Bildlauf, offenem Reiter und gesetzten Filtern, und laden leise nach. Gemessen: null Abrufe und 4 von 4 Kacheln beim Zurückkommen aus der Verwaltung. Höchstens fünf Ansichten, Verfall nach fünf Minuten, weggeworfen bei allem, was den Bestand ändert. Detailseite und Verwaltung sind ausgenommen — beide zeigen Laufendes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b67a7ec88
commit
637e95fbb1
+34
-13
@@ -20,10 +20,13 @@ import { icon } from '../icons';
|
||||
*
|
||||
* `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 {
|
||||
export function verwaltung(
|
||||
geaendert: () => void = () => {},
|
||||
signal: AbortSignal = new AbortController().signal,
|
||||
): HTMLElement {
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.append(dashboard(), vermisstenbereich(geaendert), bibliotheksbereich(geaendert),
|
||||
benutzerbereich(), serverbereich(geaendert));
|
||||
wurzel.append(dashboard(signal), vermisstenbereich(geaendert),
|
||||
bibliotheksbereich(geaendert, signal), benutzerbereich(), serverbereich(geaendert));
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
@@ -51,7 +54,7 @@ function abschnitt(titel: string, knoepfe: HTMLElement[] = []): HTMLElement {
|
||||
* **Fragt alle fünf Sekunden nach.** Ein dauerhaft offener Kanal wäre
|
||||
* sparsamer, aber für eine Handvoll Zeilen auf einer Seite, die man selten
|
||||
* offen hat, ist er der grössere Aufwand — auf beiden Seiten. */
|
||||
function dashboard(): HTMLElement {
|
||||
function dashboard(signal: AbortSignal): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Wird abgespielt'));
|
||||
|
||||
@@ -94,6 +97,10 @@ function dashboard(): HTMLElement {
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
return; // Nicht weiter nachfragen, wenn es scheitert.
|
||||
}
|
||||
// **Nur weitermachen, solange die Ansicht hängt.** Ohne diese Zeile
|
||||
// legte jeder Besuch der Verwaltung einen weiteren Fünf-Sekunden-Takt
|
||||
// an, und nach zehn Wechseln liefen zehn davon nebeneinander.
|
||||
if (signal.aborted) return;
|
||||
timer = window.setTimeout(() => void laden(), 5000);
|
||||
}
|
||||
|
||||
@@ -229,7 +236,7 @@ function vermisstenbereich(geaendert: () => void): HTMLElement {
|
||||
|
||||
/* ---------- Bibliotheken ---------- */
|
||||
|
||||
function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
|
||||
const anlegen = knopf('Mediathek hinzufügen', 'btn btn-primary');
|
||||
@@ -264,7 +271,10 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
void laden();
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
liste.innerHTML = '<div class="spinner"></div>';
|
||||
// **Der Spinner gehört an den ersten Aufbau, nicht an jedes Auffrischen.**
|
||||
// Sonst leert sich die Liste sichtbar, bevor dasselbe wieder erscheint —
|
||||
// und bei einem Takt im Hintergrund blinkt sie dauernd.
|
||||
if (liste.childElementCount === 0) liste.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const alle = await bibliotheken();
|
||||
liste.innerHTML = '';
|
||||
@@ -302,7 +312,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
const los = knopf('Scannen', 'btn');
|
||||
los.onclick = async () => {
|
||||
los.disabled = true;
|
||||
try { await scanStarten(b.id); await verfolgen(); }
|
||||
try { await scanStarten(b.id); await verfolgen(true); }
|
||||
catch (fehler) { stand.textContent = (fehler as Error).message; }
|
||||
los.disabled = false;
|
||||
};
|
||||
@@ -316,7 +326,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
+ 'nicht über eine neue Suche. Zuordnungen von Hand bleiben damit stehen. '
|
||||
+ 'Bei vielen Titeln dauert es.',
|
||||
bestaetigen: 'Auffrischen',
|
||||
tun: async () => { await metadatenAuffrischen(b.id); void verfolgen(); },
|
||||
tun: async () => { await metadatenAuffrischen(b.id); void verfolgen(true); },
|
||||
});
|
||||
|
||||
const umbenennen = knopf('Umbenennen', 'btn');
|
||||
@@ -341,8 +351,16 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
});
|
||||
|
||||
/** Fragt nach, bis der Lauf durch ist — er läuft im Hintergrund. */
|
||||
async function verfolgen(): Promise<void> {
|
||||
/// Verfolgt einen Lauf bis zum Ende.
|
||||
///
|
||||
/// **`melden` entscheidet, ob am Schluss `geaendert()` gerufen wird.**
|
||||
/// Beim Aufbau der Seite ist es `false`: ein längst beendeter Lauf meldete
|
||||
/// sonst „fertig", liess neu zeichnen, und der Neuaufbau verfolgte wieder
|
||||
/// denselben beendeten Lauf — eine Endlosschleife, die sich als vier
|
||||
/// dauernd drehende Spinner zeigte.
|
||||
async function verfolgen(melden: boolean): Promise<void> {
|
||||
for (let i = 0; i < 900; i++) {
|
||||
if (signal.aborted) return;
|
||||
const s = await scanstand(b.id);
|
||||
if (s.state === 'running') stand.textContent = 'Wird durchsucht…';
|
||||
else if (s.state === 'enriching') stand.textContent = 'Metadaten werden geholt…';
|
||||
@@ -353,9 +371,9 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
if (s.metadata?.skipped) teile.push('Metadaten übersprungen');
|
||||
else if (s.metadata) teile.push(`${s.metadata.moviesMatched + s.metadata.seriesMatched} zugeordnet`);
|
||||
if (s.metadata?.error) teile.push(s.metadata.error);
|
||||
if (s.missing) teile.push(`${s.missing} nicht mehr gefunden`);
|
||||
stand.textContent = teile.join(' · ');
|
||||
await laden();
|
||||
geaendert();
|
||||
if (melden) { await laden(); geaendert(); }
|
||||
return;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
@@ -366,7 +384,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
|
||||
knoepfe.className = 'knopfreihe';
|
||||
knoepfe.append(los, auffrischen, umbenennen, entfernen);
|
||||
d.append(knoepfe, stand);
|
||||
void verfolgen();
|
||||
void verfolgen(false);
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -404,7 +422,10 @@ function benutzerbereich(): HTMLElement {
|
||||
void laden();
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
liste.innerHTML = '<div class="spinner"></div>';
|
||||
// **Der Spinner gehört an den ersten Aufbau, nicht an jedes Auffrischen.**
|
||||
// Sonst leert sich die Liste sichtbar, bevor dasselbe wieder erscheint —
|
||||
// und bei einem Takt im Hintergrund blinkt sie dauernd.
|
||||
if (liste.childElementCount === 0) liste.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const alle = await benutzer();
|
||||
liste.innerHTML = '';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { inhalt, genresVon, empfehlungen as holeEmpfehlungen, type Bibliothek, t
|
||||
import { leer, fehlerzeile } from '../util';
|
||||
import { icons } from '../icons';
|
||||
import { kachel, alsKachel, reihe } from '../card';
|
||||
import { beiRueckkehr } from '../ansichten';
|
||||
|
||||
type Art = 'filme' | 'serien';
|
||||
type Reiter = 'empfehlungen' | 'mediathek' | 'kategorien';
|
||||
@@ -181,8 +182,14 @@ export function mediathek(
|
||||
|
||||
void laden();
|
||||
|
||||
// Beim Zurückkommen aus einer anderen Ansicht leise nachladen — die alte
|
||||
// Liste bleibt stehen, bis die neue da ist.
|
||||
beiRueckkehr(wurzel, () => void laden());
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
gitter.innerHTML = '<div class="spinner"></div>';
|
||||
// Der Spinner nur beim ersten Mal; beim Nachladen bleibt stehen, was da
|
||||
// ist, bis es ersetzt wird.
|
||||
if (gitter.childElementCount === 0) gitter.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const seite = await inhalt(bibliothek, { search: suche, sort: ordnung, genre, take: 200 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user