Die Mediathek hörte bei I auf, und jede Folge stand doppelt
Zwei Befunde aus dem laufenden Betrieb. Die Liste holte genau eine Seite. Der Server gibt höchstens 200 Einträge je Zug — das ist richtig so —, nur fragte die Oberfläche nie nach der zweiten. Bei 455 Filmen endete die Mediathek mitten im Alphabet, ohne Hinweis, dass da noch etwas kommt. Jetzt wird beim Scrollen nachgeladen, und die Zahl sagt „200 von 455" statt einer Behauptung, die man beim Scrollen widerlegt. Dabei kam heraus, dass ein IntersectionObserver eine Seite braucht, die auch gezeichnet wird: im Fenster im Hintergrund löst er nicht aus, und beim Prüfen blieb die Liste genau deshalb bei 200 stehen. Daneben steht jetzt eine Prüfung am Scrollereignis, die überall greift. Der zweite Befund: jede Folge zeigte zwei gleich aussehende Knopfreihen. Es sind zwei Dateien — Plex' „Optimized for Mobile"-Kopien. Flaro kannte „optimized", aber nicht „Optimized for Mobile", und sah ohnehin nur den unmittelbaren Elternordner; Plex legt die Fassung aber als `Plex Versions/Optimized for Mobile/<Serie>/<Staffel>/…` ab. Jetzt wird drei Ebenen hoch gesucht, die Schublade `Plex Versions` benennt keine Serie mehr, und an der Folge steht „Mobil" statt einer zweiten, unerklärten Reihe. Nicht um Plex zu bedienen — die Dateien liegen nun einmal so auf der Platte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
960cca3dae
commit
8809b9f46f
@@ -210,9 +210,15 @@ public sealed record MediaStreamResponse(
|
||||
/// App spricht Dateien über ihre Kennung an. Zum Unterscheiden zweier
|
||||
/// Fassungen genügt der Name.
|
||||
/// </remarks>
|
||||
/// <param name="VersionLabel">
|
||||
/// <b>Wie diese Fassung heisst</b> — „Mobil", „4K", „Director's Cut" — oder
|
||||
/// <c>null</c>, wenn es nur eine gibt. Ohne diese Angabe standen zwei Dateien
|
||||
/// derselben Folge als zwei gleich aussehende Knopfreihen untereinander, und
|
||||
/// die Folge wirkte doppelt.
|
||||
/// </param>
|
||||
public sealed record MediaFileResponse(
|
||||
Guid Id, string FileName, long SizeBytes, string? Container, double? DurationSeconds,
|
||||
IReadOnlyList<MediaStreamResponse> Streams);
|
||||
IReadOnlyList<MediaStreamResponse> Streams, string? VersionLabel = null);
|
||||
|
||||
public sealed record MovieResponse(
|
||||
Guid Id, string Title, int? Year, string? Overview,
|
||||
|
||||
@@ -70,8 +70,13 @@ public static class LibraryContentEndpoints
|
||||
|
||||
private static MediaFileResponse Datei(
|
||||
Guid id, string pfad, long bytes, string? schachtel, TimeSpan? dauer,
|
||||
ILookup<Guid, MediaStreamResponse> spuren) =>
|
||||
new(id, System.IO.Path.GetFileName(pfad), bytes, schachtel, dauer?.TotalSeconds, spuren[id].ToList());
|
||||
ILookup<Guid, MediaStreamResponse> spuren, string? fassung = null) =>
|
||||
new(id, System.IO.Path.GetFileName(pfad), bytes, schachtel, dauer?.TotalSeconds,
|
||||
spuren[id].ToList(),
|
||||
// Steht sie nicht in der Datenbank, wird sie aus dem Pfad gelesen:
|
||||
// ein Bestand von vor dieser Änderung soll nicht erst neu gescannt
|
||||
// werden müssen, um „Mobil" zu zeigen.
|
||||
fassung ?? FileNames.Fassung(pfad));
|
||||
|
||||
/// <summary>Die Genres eines Titels, in der Reihenfolge der Quelle.</summary>
|
||||
private static async Task<IReadOnlyList<string>> GenresZu(FlaroContext db, Guid? film, Guid? serie) =>
|
||||
@@ -315,7 +320,7 @@ public static class LibraryContentEndpoints
|
||||
var ids = kennungen.Select(m => m.Id).ToList();
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(f => f.MovieId != null && ids.Contains(f.MovieId!.Value))
|
||||
.Select(f => new { f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, f.MovieId })
|
||||
.Select(f => new { f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, f.MovieId, f.VersionLabel })
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(f => f.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, filme: ids);
|
||||
@@ -323,7 +328,7 @@ public static class LibraryContentEndpoints
|
||||
var items = kennungen.Select(m => new MovieResponse(
|
||||
m.Id, m.Title, m.Year, m.Overview, m.PosterImage, m.BackdropImage,
|
||||
dateien.Where(f => f.MovieId == m.Id)
|
||||
.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren)).ToList(),
|
||||
.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren, f.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(m.Id),
|
||||
m.LogoImage, m.RuntimeMinutes, m.Rating, null, m.ContentRating,
|
||||
OriginalTitle: m.OriginalTitle)).ToList();
|
||||
@@ -477,7 +482,7 @@ public static class LibraryContentEndpoints
|
||||
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(f => f.MovieId == id)
|
||||
.Select(f => new { f.Id, f.Path, f.SizeBytes, f.Container, f.Duration })
|
||||
.Select(f => new { f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, f.VersionLabel })
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(f => f.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, filme: [id]);
|
||||
@@ -489,7 +494,7 @@ public static class LibraryContentEndpoints
|
||||
return Results.Ok(new MovieResponse(
|
||||
film.Id, uebersetzt?.Title ?? film.Title, film.Year,
|
||||
uebersetzt?.Overview ?? film.Overview, film.PosterImage, film.BackdropImage,
|
||||
dateien.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren)).ToList(),
|
||||
dateien.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren, f.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(id),
|
||||
film.LogoImage, film.RuntimeMinutes, film.Rating,
|
||||
uebersetzt?.Tagline ?? film.Tagline, film.ContentRating,
|
||||
@@ -553,7 +558,7 @@ public static class LibraryContentEndpoints
|
||||
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => d.EpisodeId == naechste.Id)
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration })
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.VersionLabel })
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, folgen: [naechste.Id]);
|
||||
@@ -569,7 +574,7 @@ public static class LibraryContentEndpoints
|
||||
return Results.Ok(new EpisodeResponse(
|
||||
naechste.Id, naechste.SeasonNumber, naechste.EpisodeNumber,
|
||||
naechste.Title, naechste.Overview, naechste.StillImage ?? serienBild,
|
||||
dateien.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren)).ToList(),
|
||||
dateien.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(naechste.Id)));
|
||||
}).RequireAuthorization();
|
||||
|
||||
@@ -589,7 +594,7 @@ public static class LibraryContentEndpoints
|
||||
var ids = folgen.Select(f => f.Id).ToList();
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => d.EpisodeId != null && ids.Contains(d.EpisodeId!.Value))
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.EpisodeId })
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.EpisodeId, d.VersionLabel })
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, folgen: ids);
|
||||
@@ -602,7 +607,7 @@ public static class LibraryContentEndpoints
|
||||
f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview,
|
||||
f.StillImage ?? serienBild,
|
||||
dateien.Where(d => d.EpisodeId == f.Id)
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren)).ToList(),
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(f.Id))).ToList();
|
||||
|
||||
return Results.Ok(new Page<EpisodeResponse>(items, items.Count));
|
||||
|
||||
@@ -140,12 +140,32 @@ public static partial class FileNames
|
||||
if (hinten.Length > 0 && IstFassungsname(hinten)) return Vereinheitlichen(hinten);
|
||||
}
|
||||
|
||||
var ordner = Path.GetFileName(Path.GetDirectoryName(pfad) ?? "");
|
||||
if (ordner.Length > 0 && IstFassungsname(ordner)) return Vereinheitlichen(Saeubern(ordner));
|
||||
// **Drei Ebenen hoch, nicht nur eine.** Plex legt seine zweite Fassung
|
||||
// als `Plex Versions/Optimized for Mobile/<Serie>/<Staffel>/…` ab —
|
||||
// der Ordner, der die Fassung benennt, steht dort also weit über der
|
||||
// Datei. Drei Ebenen decken das ab, ohne irgendwann die Mediathek
|
||||
// selbst für eine Fassung zu halten.
|
||||
var ordner = Path.GetDirectoryName(pfad);
|
||||
for (var ebene = 0; ebene < 3 && !string.IsNullOrEmpty(ordner); ebene++)
|
||||
{
|
||||
var name = Path.GetFileName(ordner);
|
||||
if (name.Length > 0 && IstFassungsname(name)) return Vereinheitlichen(Saeubern(name));
|
||||
ordner = Path.GetDirectoryName(ordner);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Ordner, die nur Fassungen einsammeln und keine Serie benennen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <c>Plex Versions</c> ist kein Titel, sondern eine Schublade. Ohne diese
|
||||
/// Prüfung hiesse die Serie so, und darunter läge der halbe Bestand ein
|
||||
/// zweites Mal.
|
||||
/// </remarks>
|
||||
private static bool IstFassungsordner(string name) =>
|
||||
IstFassungsname(name) || Schlank(name).Equals("plexversions", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Macht aus einer gemessenen Höhe einen Namen: 2160 wird „4K".</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
@@ -194,6 +214,13 @@ public static partial class FileNames
|
||||
{
|
||||
["mobil"] = "Mobil", ["mobile"] = "Mobil",
|
||||
["optimiert"] = "Mobil", ["optimized"] = "Mobil",
|
||||
// Wie Plex seine zweite Fassung nennt. Nicht um Plex zu bedienen,
|
||||
// sondern weil die Dateien nun einmal so auf der Platte liegen:
|
||||
// ohne diese Zeilen steht jede Folge zweimal untereinander, mit
|
||||
// zwei gleich aussehenden Knopfreihen und ohne Hinweis, warum.
|
||||
["optimizedformobile"] = "Mobil",
|
||||
["optimiertfürmobilgeräte"] = "Mobil",
|
||||
["optimiertfuermobilgeraete"] = "Mobil",
|
||||
["directorscut"] = "Director's Cut", ["directorcut"] = "Director's Cut",
|
||||
["extended"] = "Extended", ["extendedcut"] = "Extended",
|
||||
["theatrical"] = "Kinofassung", ["kinofassung"] = "Kinofassung",
|
||||
@@ -355,8 +382,17 @@ public static partial class FileNames
|
||||
// sie benennt — dann bleibt nur der Dateiname.
|
||||
if (unterhalb is null || !unterhalb.Contains('/')) return null;
|
||||
|
||||
var erster = unterhalb[..unterhalb.IndexOf('/')];
|
||||
return Saubermachen(erster);
|
||||
// **Schubladen überspringen.** Unter `Plex Versions/Optimized for
|
||||
// Mobile/` steht die Serie erst darunter; wer den ersten Ordner
|
||||
// nimmt, legt eine Serie namens „Plex Versions" an.
|
||||
var teile = unterhalb.Split('/');
|
||||
foreach (var teil in teile[..^1])
|
||||
{
|
||||
if (IstFassungsordner(teil) || IstStaffelordner(teil)) continue;
|
||||
return Saubermachen(teil);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var ordner = Path.GetDirectoryName(pfad);
|
||||
|
||||
@@ -352,3 +352,64 @@ public class EchteBenennungen
|
||||
Does.Contain("keine Folgennummer"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Was Plex als zweite Fassung ablegt, ist keine zweite Folge.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Aus dem echten Bestand.</b> In der Folgenliste stand jede Folge mit zwei
|
||||
/// gleich aussehenden Knopfreihen untereinander — zwei Dateien, kein Hinweis
|
||||
/// worauf. Es sind Plex' „Optimized for Mobile"-Kopien. Erkannt heissen sie
|
||||
/// „Mobil" und stehen als Fassung an derselben Folge; unerkannt sehen sie aus
|
||||
/// wie ein Fehler.
|
||||
/// </remarks>
|
||||
public class PlexFassungen
|
||||
{
|
||||
private const string Wurzel = "/media/Serien";
|
||||
|
||||
[Test]
|
||||
public void Optimiert_fuers_Telefon_heisst_Mobil()
|
||||
{
|
||||
Assert.That(
|
||||
FileNames.Fassung($"{Wurzel}/Plex Versions/Optimized for Mobile/Dark/S01/Dark - S01E01.mkv"),
|
||||
Is.EqualTo("Mobil"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Die_Schublade_ist_keine_Serie()
|
||||
{
|
||||
var name = FileNames.ParseEpisode(
|
||||
$"{Wurzel}/Plex Versions/Optimized for Mobile/Dark/S01/Dark - S01E01.mkv", Wurzel);
|
||||
|
||||
Assert.That(name, Is.Not.Null);
|
||||
Assert.That(name!.SeriesTitle, Is.EqualTo("Dark"),
|
||||
"Ohne das hiesse die Serie „Plex Versions“.");
|
||||
Assert.That(name.Season, Is.EqualTo(1));
|
||||
Assert.That(name.Episode, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>Beide Wege führen zur selben Folge — nur mit anderer Fassung.</summary>
|
||||
[Test]
|
||||
public void Original_und_Kopie_sind_dieselbe_Folge()
|
||||
{
|
||||
var original = FileNames.ParseEpisode($"{Wurzel}/Dark/S01/Dark - S01E01.mkv", Wurzel);
|
||||
var mobil = FileNames.ParseEpisode(
|
||||
$"{Wurzel}/Plex Versions/Optimized for Mobile/Dark/S01/Dark - S01E01.mkv", Wurzel);
|
||||
|
||||
Assert.That(mobil!.SeriesTitle, Is.EqualTo(original!.SeriesTitle));
|
||||
Assert.That(mobil.Season, Is.EqualTo(original.Season));
|
||||
Assert.That(mobil.Episode, Is.EqualTo(original.Episode));
|
||||
|
||||
Assert.That(FileNames.Fassung($"{Wurzel}/Dark/S01/Dark - S01E01.mkv"), Is.Null);
|
||||
Assert.That(FileNames.Fassung(
|
||||
$"{Wurzel}/Plex Versions/Optimized for Mobile/Dark/S01/Dark - S01E01.mkv"),
|
||||
Is.EqualTo("Mobil"));
|
||||
}
|
||||
|
||||
/// <summary>Ein Staffelordner zwischendrin ändert nichts.</summary>
|
||||
[Test]
|
||||
public void Staffelordner_benennt_weiterhin_keine_Serie()
|
||||
{
|
||||
var name = FileNames.ParseEpisode($"{Wurzel}/Dark/Staffel 2/Dark - S02E05.mkv", Wurzel);
|
||||
Assert.That(name!.SeriesTitle, Is.EqualTo("Dark"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export type Spur = {
|
||||
export type Datei = {
|
||||
id: string; fileName: string; sizeBytes: number;
|
||||
container: string | null; durationSeconds: number | null; streams: Spur[];
|
||||
/** „Mobil", „4K", „Director's Cut" — oder `null`, wenn es nur eine gibt. */
|
||||
versionLabel: string | null;
|
||||
};
|
||||
/** Was dieser Benutzer mit einem Titel zu tun hatte. */
|
||||
export type Stand = {
|
||||
|
||||
@@ -1045,6 +1045,20 @@ h2 { font-size: var(--t-abschnitt); font-weight: 700; letter-spacing: -.015em; m
|
||||
.grundblock summary strong { color: var(--accent); margin-right: 6px; }
|
||||
.grundblock .protokoll { max-height: 220px; margin-top: var(--s-snug); }
|
||||
|
||||
/* Der Dateiname über einer Aktionsreihe — nur wenn es mehrere gibt. */
|
||||
.dateiname.fassung {
|
||||
font-family: inherit; color: var(--text-dim); font-weight: 600;
|
||||
}
|
||||
.dateiname {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: var(--text-faint); margin-top: var(--s-snug);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Der Fühler, der das Nachladen auslöst — unsichtbar, aber nicht null hoch:
|
||||
* ein Element ohne Höhe kreuzt die Sichtgrenze nie zuverlässig. */
|
||||
.nachlader { height: 1px; }
|
||||
|
||||
.bildblock { display: flex; flex-direction: column; gap: var(--s-snug); }
|
||||
.bildblock h3 { margin: 0; font-size: var(--t-gross); font-weight: 600; flex: 1; }
|
||||
|
||||
|
||||
@@ -452,8 +452,27 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
block.appendChild(text);
|
||||
}
|
||||
|
||||
folge.files.forEach(datei => block.appendChild(
|
||||
aktionsreihe({ ...({} as Film), id: folge.id, files: folge.files } as Film, folge.state, datei)));
|
||||
// **Bei mehreren Dateien steht dabei, welche es ist.**
|
||||
//
|
||||
// Vorher standen zwei gleich aussehende Reihen untereinander, und die
|
||||
// Folge wirkte doppelt. Es sind zwei Dateien — eine zweite Fassung, ein
|
||||
// anderes Format, eine Kopie —, und genau das muss dastehen, damit man
|
||||
// entscheiden kann, ob eine davon wegkann.
|
||||
folge.files.forEach(datei => {
|
||||
if (folge.files.length > 1) {
|
||||
const welche = document.createElement('div');
|
||||
welche.className = 'winzig dateiname';
|
||||
// Der Fassungsname, wenn es einen gibt — „Mobil" sagt mehr als ein
|
||||
// Pfad. Sonst der Dateiname, damit überhaupt etwas dasteht.
|
||||
welche.textContent = (datei.versionLabel ?? datei.fileName)
|
||||
+ (datei.sizeBytes ? ` · ${groesse(datei.sizeBytes)}` : '');
|
||||
welche.title = datei.fileName;
|
||||
if (datei.versionLabel) welche.classList.add('fassung');
|
||||
block.appendChild(welche);
|
||||
}
|
||||
block.appendChild(aktionsreihe(
|
||||
{ ...({} as Film), id: folge.id, files: folge.files } as Film, folge.state, datei));
|
||||
});
|
||||
return block;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,18 +172,69 @@ export function mediathek(
|
||||
]);
|
||||
}).catch(() => {});
|
||||
|
||||
void laden();
|
||||
/** Wie viele Einträge der Server höchstens je Zug gibt. */
|
||||
const SEITE = 200;
|
||||
|
||||
// Beim Zurückkommen aus einer anderen Ansicht leise nachladen — die alte
|
||||
// Liste bleibt stehen, bis die neue da ist.
|
||||
beiRueckkehr(wurzel, () => void laden());
|
||||
let geladen = 0;
|
||||
let gesamt = 0;
|
||||
let amEnde = false;
|
||||
let holtGerade = false;
|
||||
|
||||
/** Der Fühler unter dem Raster: wird er sichtbar, kommt die nächste Seite. */
|
||||
const fuehler = document.createElement('div');
|
||||
fuehler.className = 'nachlader';
|
||||
ziel.appendChild(fuehler);
|
||||
|
||||
// **Nachladen beim Scrollen statt eines Knopfes.**
|
||||
//
|
||||
// Die Mediathek holte genau eine Seite und hörte danach auf — bei 455
|
||||
// Filmen endete die Liste mitten im Alphabet, ohne Hinweis, dass da noch
|
||||
// etwas kommt. Der Fühler steht 600 Bildpunkte vor dem Ende, damit die
|
||||
// nächste Seite da ist, bevor man sie braucht.
|
||||
const beobachter = new IntersectionObserver(
|
||||
eintraege => { if (eintraege.some(e => e.isIntersecting)) void laden(false); },
|
||||
{ rootMargin: '600px' });
|
||||
beobachter.observe(fuehler);
|
||||
// Kein eigenes Aufräumen: ein abgehängter Fühler löst nichts aus, und
|
||||
// wird die Ansicht aus dem Zwischenspeicher zurückgeholt, soll er wieder
|
||||
// greifen.
|
||||
|
||||
// **Und ein zweiter Weg über das Scrollen.**
|
||||
//
|
||||
// Ein IntersectionObserver braucht eine Seite, die auch gezeichnet wird;
|
||||
// in einem Fenster im Hintergrund löst er nicht aus, und beim Prüfen blieb
|
||||
// die Liste genau deshalb bei 200 stehen. Die Prüfung von Hand kostet
|
||||
// nichts — sie läuft nur, solange überhaupt noch etwas nachzuladen ist.
|
||||
const beimScrollen = () => {
|
||||
if (amEnde || holtGerade) return;
|
||||
if (!fuehler.isConnected) return;
|
||||
if (fuehler.getBoundingClientRect().top < window.innerHeight + 600) void laden(false);
|
||||
};
|
||||
window.addEventListener('scroll', beimScrollen, { passive: true, capture: true });
|
||||
window.addEventListener('resize', beimScrollen, { passive: true });
|
||||
|
||||
async function laden(vonVorn = true): Promise<void> {
|
||||
if (holtGerade) return;
|
||||
if (!vonVorn && amEnde) return;
|
||||
holtGerade = true;
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
// 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>';
|
||||
if (vonVorn && gitter.childElementCount === 0) {
|
||||
gitter.innerHTML = '<div class="spinner"></div>';
|
||||
}
|
||||
|
||||
try {
|
||||
const seite = await inhalt(bibliothek, { search: suche, sort: ordnung, genre, take: 200 });
|
||||
const seite = await inhalt(bibliothek, {
|
||||
search: suche, sort: ordnung, genre,
|
||||
skip: vonVorn ? 0 : geladen, take: SEITE,
|
||||
});
|
||||
|
||||
if (vonVorn) { gitter.innerHTML = ''; geladen = 0; amEnde = false; }
|
||||
|
||||
gesamt = seite.total;
|
||||
geladen += seite.items.length;
|
||||
if (seite.items.length < SEITE || geladen >= gesamt) amEnde = true;
|
||||
|
||||
// **Gesehen/offen siebt hier, nicht am Server.** Er kann es noch
|
||||
// nicht; es jetzt wegzulassen hiesse, einen Filter anzubieten, den
|
||||
@@ -196,23 +247,43 @@ export function mediathek(
|
||||
return gesehen === 'gesehen' ? ist : !ist;
|
||||
});
|
||||
|
||||
anzahl.textContent = gesehen === ''
|
||||
? String(seite.total)
|
||||
: `${uebrig.length} von ${seite.total}`;
|
||||
uebrig.forEach(e => gitter.appendChild(kachel(alsKachel(e), oeffnen)));
|
||||
|
||||
gitter.innerHTML = '';
|
||||
if (uebrig.length === 0) {
|
||||
// **Die Zahl sagt, wie viel schon da ist.** „455" über einer Liste mit
|
||||
// 200 Kacheln wäre eine Behauptung, die man beim Scrollen widerlegt.
|
||||
anzahl.textContent = amEnde && gesehen === ''
|
||||
? String(gesamt)
|
||||
: `${gitter.childElementCount} von ${gesamt}`;
|
||||
|
||||
if (gitter.childElementCount === 0 && amEnde) {
|
||||
gitter.appendChild(leer(suche || genre || gesehen
|
||||
? 'Nichts gefunden.'
|
||||
: `„${bibliothek.name}" ist leer. Starte unter Verwaltung einen Scan.`));
|
||||
return;
|
||||
}
|
||||
uebrig.forEach(e => gitter.appendChild(kachel(alsKachel(e), oeffnen)));
|
||||
} catch (fehler) {
|
||||
gitter.innerHTML = '';
|
||||
if (vonVorn) gitter.innerHTML = '';
|
||||
gitter.appendChild(fehlerzeile(meldung(fehler)));
|
||||
amEnde = true;
|
||||
} finally {
|
||||
holtGerade = false;
|
||||
}
|
||||
|
||||
// Füllt eine Seite den Bildschirm nicht, kommt der Fühler nie in Sicht
|
||||
// — dann muss von Hand nachgefasst werden.
|
||||
if (!amEnde && !vonVorn && fuehler.getBoundingClientRect().top < window.innerHeight) {
|
||||
void laden(false);
|
||||
}
|
||||
}
|
||||
|
||||
// **Erst hier starten.** `laden()` greift auf `holtGerade`, `geladen` und
|
||||
// den Fühler zu; stand der Aufruf darüber, lief er in die temporale
|
||||
// Totzone von `const`/`let` — „Cannot access 'holtGerade' before
|
||||
// initialization", und das Raster blieb leer.
|
||||
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());
|
||||
}
|
||||
|
||||
/* ---------- Reiter „Empfehlungen" ---------- */
|
||||
|
||||
Reference in New Issue
Block a user