feat(reader): prefetch the next chapter's first pages near the end
When the reader reaches within two pages of the end of a chapter (tracked in both modes off the furthest page reached), warm the next chapter's first few page images via hidden imgs so flipping over renders instantly. Fetched once per next-chapter id, guarded against mid-flight chapter changes, and retried on failure. e2e asserts the prefetch is absent at the chapter start and present (with the next chapter's URLs) near the end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import { GAP_PX, type ReaderPageGap } from '$lib/api/preferences';
|
||||
import { preferences } from '$lib/preferences.svelte';
|
||||
import { updateReadProgress } from '$lib/api/read_progress';
|
||||
import { chapterLabel } from '$lib/api/chapters';
|
||||
import { chapterLabel, getChapterPages } from '$lib/api/chapters';
|
||||
import { resyncChapter, analyzePage } from '$lib/api/admin';
|
||||
import { readerFullscreen } from '$lib/reader-fullscreen.svelte';
|
||||
import { session } from '$lib/session.svelte';
|
||||
@@ -959,6 +959,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Next-chapter image preloading ----
|
||||
// When the reader nears the end of the current chapter, warm the next
|
||||
// chapter's first few page images so flipping over is instant. Works in
|
||||
// both modes off the furthest page reached (single: `index`; continuous:
|
||||
// the IntersectionObserver high-water `progressPage`).
|
||||
const PRELOAD_WITHIN_PAGES = 2;
|
||||
const NEXT_PRELOAD_COUNT = 3;
|
||||
let nextPreloadUrls = $state<string[]>([]);
|
||||
let preloadedForChapterId: string | null = null;
|
||||
|
||||
const furthestPage = $derived(mode === 'single' ? index + 1 : progressPage);
|
||||
|
||||
$effect(() => {
|
||||
const nc = nextChapter;
|
||||
if (!nc || pages.length === 0) return;
|
||||
if (pages.length - furthestPage > PRELOAD_WITHIN_PAGES) return;
|
||||
if (preloadedForChapterId === nc.id) return;
|
||||
// Guard set before the await so overlapping effect runs don't
|
||||
// double-fetch; cleared on failure so a later pass can retry.
|
||||
preloadedForChapterId = nc.id;
|
||||
getChapterPages(manga.id, nc.id)
|
||||
.then((ps) => {
|
||||
if (preloadedForChapterId !== nc.id) return; // chapter changed mid-flight
|
||||
nextPreloadUrls = ps
|
||||
.slice(0, NEXT_PRELOAD_COUNT)
|
||||
.map((p) => fileUrl(p.storage_key));
|
||||
})
|
||||
.catch(() => {
|
||||
if (preloadedForChapterId === nc.id) preloadedForChapterId = null;
|
||||
});
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('pagehide', flushFinalProgress);
|
||||
});
|
||||
@@ -1369,6 +1401,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Warm the next chapter's first images once the reader nears the end of
|
||||
this one. Hidden (display:none still fetches) — a pure cache primer so
|
||||
flipping to the next chapter shows pages immediately. -->
|
||||
{#each nextPreloadUrls as u (u)}
|
||||
<img src={u} alt="" aria-hidden="true" class="preload" loading="eager" data-testid="reader-next-preload" />
|
||||
{/each}
|
||||
|
||||
<!-- Tap zones — mobile + single mode only. Continuous mode owns native
|
||||
scroll so left/right would steal panning. Tap left/right advances
|
||||
within the chapter (falling through to adjacent chapters at the
|
||||
|
||||
Reference in New Issue
Block a user