diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 46c328b..925e076 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.123.0" +version = "0.124.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b26d63f..50aea91 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.123.0" +version = "0.124.0" edition = "2021" default-run = "mangalord" diff --git a/frontend/e2e/reader-page-deep-link.spec.ts b/frontend/e2e/reader-page-deep-link.spec.ts index 458d3c5..8450080 100644 --- a/frontend/e2e/reader-page-deep-link.spec.ts +++ b/frontend/e2e/reader-page-deep-link.spec.ts @@ -170,13 +170,10 @@ test.describe('reader ?page=N deep link', () => { await expect(page.getByTestId('reader-continuous')).toBeVisible(); - // Pages 1..=4 (1-indexed in the testid, 0-indexed in the - // template; initialIndex = 3 means we eager-load at least 0..=3, - // i.e. testids 1..4). Without this guard, page 2+ would be lazy - // and their 0×0 placeholders would let the scroll target - // appear far above its final position. The rolling preload window - // (PRELOAD_AHEAD) extends the eager set a few pages past the - // target, so on this 6-page chapter every page is eager. + // The whole chapter is eager, so the scroll target (page 4) and + // every page before it have settled heights before the + // scroll-to-`?page=N` effect runs — the target can't be pushed + // past the viewport by later pages loading in. for (const n of [1, 2, 3, 4, 5, 6]) { await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute( 'loading', @@ -185,28 +182,21 @@ test.describe('reader ?page=N deep link', () => { } }); - test('continuous mode: no ?page= eager-loads a leading preload window, not the whole chapter', async ({ + test('continuous mode: eager-loads the whole chapter up front', async ({ page }) => { await mockReader(page, 'continuous'); await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); await expect(page.getByTestId('reader-continuous')).toBeVisible(); - // initialIndex = 0; the rolling window (PRELOAD_AHEAD = 3) eager-loads - // the first few pages ahead of the reader so they don't pop in on - // scroll ... - for (const n of [1, 2, 3]) { + // Every page is eager — the whole current chapter is preloaded so + // pages don't pop in / reflow as the reader scrolls onto them. + for (const n of [1, 2, 3, 4, 5, 6]) { await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute( 'loading', 'eager' ); } - // ... while the tail stays lazy so a long chapter doesn't fetch every - // page at once on open. - await expect(page.getByTestId('reader-page-6')).toHaveAttribute( - 'loading', - 'lazy' - ); }); test('single mode: ?page=N opens at the requested page', async ({ page }) => { diff --git a/frontend/e2e/reader-preload-window.spec.ts b/frontend/e2e/reader-preload-window.spec.ts index 68fe8bf..787e938 100644 --- a/frontend/e2e/reader-preload-window.spec.ts +++ b/frontend/e2e/reader-preload-window.spec.ts @@ -55,20 +55,22 @@ async function goContinuous(page: Page, path: string) { await expect(page.getByTestId('reader-continuous')).toBeVisible(); } -test('opening at page 1 eager-loads the leading window but leaves far pages lazy', async ({ page }) => { +test('eager-loads the whole current chapter up front, not just the visible pages', async ({ page }) => { await mockReader(page); await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}`); - // The first page is always eager (it's on screen). - await expect(page.getByTestId('reader-page-1')).toHaveAttribute('loading', 'eager'); - // A page far past the leading window is not fetched up front. - await expect(page.getByTestId('reader-page-10')).toHaveAttribute('loading', 'lazy'); -}); + // Every page — including ones far below the fold — is eager, so the + // browser fetches the whole chapter up front instead of on scroll. + for (let n = 1; n <= PAGE_COUNT; n++) { + await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute('loading', 'eager'); + } -test('opening deep in the chapter eager-loads the pages around that position', async ({ page }) => { - await mockReader(page); - // Deep-link to page 8: the window now covers the tail, so page 10 is eager. - await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}?page=8`); - - await expect(page.getByTestId('reader-page-10')).toHaveAttribute('loading', 'eager'); + // ... and a page well below the fold actually finishes loading without + // ever scrolling to it — the observable payoff of preloading. + const lastPage = page.getByTestId(`reader-page-${PAGE_COUNT}`); + await expect(async () => { + const loaded = await lastPage.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0); + expect(loaded).toBe(true); + }).toPass(); + expect(await page.evaluate(() => window.scrollY)).toBe(0); }); diff --git a/frontend/package.json b/frontend/package.json index a362776..48e3967 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.123.0", + "version": "0.124.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte index 9f02fc0..b2fbd05 100644 --- a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte +++ b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.svelte @@ -971,61 +971,6 @@ const furthestPage = $derived(mode === 'single' ? index + 1 : progressPage); - // ---- Current-chapter rolling preload window (continuous mode) ---- - // Eager-load pages up to this index so they're fetched (and sized) a few - // pages ahead of the reading position — they arrive while still below the - // fold instead of popping in / reflowing as the user scrolls onto them. - // Pages past the window stay lazy so opening a long chapter doesn't fire - // every request at once. - // - // The window anchors on `initialIndex` (deterministic at open, and keeps - // the scroll-settle guarantee that pages `0..=initialIndex` are eager) and - // rolls forward by `scrollLeadIdx` — the page at the top of the viewport, - // measured from live geometry below. It deliberately does NOT use the - // read-progress high-water (`furthestPage`): before images load they're - // 0-height and pile into the viewport, which latches that mark to the last - // page and would eager-load the whole chapter on open. - const PRELOAD_AHEAD = 3; - let scrollLeadIdx = $state(0); - const eagerThrough = $derived( - Math.max(1, initialIndex, scrollLeadIdx) + PRELOAD_AHEAD - ); - - // Track the top-of-viewport page from live rects so the window rolls with - // actual scrolling. Guarded by `scrollY` so the pre-load 0-height pile-up - // (all rects collapsed near the top) can't inflate the lead at open. - $effect(() => { - if (!browser || mode !== 'continuous') return; - const measure = () => { - if (window.scrollY < 4) { - scrollLeadIdx = 0; - return; - } - const els = continuousPageEls; - for (let i = 0; i < els.length; i++) { - if (els[i] && els[i].getBoundingClientRect().bottom > 1) { - scrollLeadIdx = i; - return; - } - } - scrollLeadIdx = Math.max(0, els.length - 1); - }; - measure(); - let raf = 0; - const onScroll = () => { - if (raf) return; - raf = requestAnimationFrame(() => { - raf = 0; - measure(); - }); - }; - window.addEventListener('scroll', onScroll, { passive: true }); - return () => { - window.removeEventListener('scroll', onScroll); - if (raf) cancelAnimationFrame(raf); - }; - }); - $effect(() => { const nc = nextChapter; // Drop a previous chapter's warmed images once the next chapter @@ -1358,20 +1303,19 @@ {:else}
{#each pages as p, i (p.id)} - + {`${manga.title} onPageContextMenu(e, p.id)} onpointerdown={(e) => onPagePointerDown(e, p.id)} onpointermove={onPagePointerMove}