From c308eb3eaca1603f44bcd960dea9553b6342604e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 11 Jul 2026 15:02:44 +0200 Subject: [PATCH] fix: reader loads the full chapter list for prev/next and the dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader fetched only the first 200 chapters, so a deep chapter (#250) fell outside the window and its prev/next chevrons resolved to null — a dead end — and the chapter dropdown was truncated. Add listAllChapters, which pages through the API's 200-row cap, and use it in the reader loader. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/package.json | 2 +- frontend/src/lib/api/chapters.test.ts | 25 +++++++++++++++++++ frontend/src/lib/api/chapters.ts | 22 ++++++++++++++++ .../manga/[id]/chapter/[chapter_id]/+page.ts | 15 ++++++----- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 70affe3..b64131a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.127.0", + "version": "0.127.1", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/api/chapters.test.ts b/frontend/src/lib/api/chapters.test.ts index ec5faa3..b850969 100644 --- a/frontend/src/lib/api/chapters.test.ts +++ b/frontend/src/lib/api/chapters.test.ts @@ -9,6 +9,7 @@ import { } from 'vitest'; import { listChapters, + listAllChapters, getChapter, getChapterPages, createChapter, @@ -142,6 +143,30 @@ describe('chapters api client', () => { }); }); + it('listAllChapters pages through the 200-row cap and returns every chapter', async () => { + const mk = (n: number) => ({ ...chapterFixture, id: `c${n}`, number: n }); + const first = Array.from({ length: 200 }, (_, i) => mk(i + 1)); + const second = Array.from({ length: 50 }, (_, i) => mk(i + 201)); + fetchSpy + .mockResolvedValueOnce(ok({ items: first, page: { limit: 200, offset: 0, total: 250 } })) + .mockResolvedValueOnce(ok({ items: second, page: { limit: 200, offset: 200, total: 250 } })); + + const all = await listAllChapters('m1'); + expect(all).toHaveLength(250); + expect(all[249].number).toBe(250); + expect(fetchSpy.mock.calls[0][0]).toContain('offset=0'); + expect(fetchSpy.mock.calls[1][0]).toContain('offset=200'); + }); + + it('listAllChapters stops after a short final page (single request)', async () => { + fetchSpy.mockResolvedValueOnce( + ok({ items: [chapterFixture], page: { limit: 200, offset: 0, total: 1 } }) + ); + const all = await listAllChapters('m1'); + expect(all).toHaveLength(1); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + it('getChapterPages unwraps the {pages} envelope into the array', async () => { fetchSpy.mockResolvedValueOnce( ok({ diff --git a/frontend/src/lib/api/chapters.ts b/frontend/src/lib/api/chapters.ts index 3f7b2ce..ce089ac 100644 --- a/frontend/src/lib/api/chapters.ts +++ b/frontend/src/lib/api/chapters.ts @@ -40,6 +40,28 @@ export async function listChapters( ); } +/** + * Every chapter of a manga, in display order, paging through the API (which + * clamps `limit` to 200 per request). The reader needs the complete list so + * prev/next navigation and the chapter dropdown work for a deep chapter — a + * single 200-row window dead-ended chapter #250 with null neighbours. + */ +export async function listAllChapters(mangaId: string): Promise { + const PAGE = 200; + const all: Chapter[] = []; + let offset = 0; + // Hard iteration cap (200 * 100 = 20k chapters) so a bad `total` can never + // spin forever. + for (let i = 0; i < 100; i++) { + const { items, page } = await listChapters(mangaId, { limit: PAGE, offset }); + all.push(...items); + offset += items.length; + const total = page.total ?? all.length; + if (items.length < PAGE || all.length >= total) break; + } + return all; +} + export async function getChapter(mangaId: string, chapterId: string): Promise { return request( `/v1/mangas/${encodeURIComponent(mangaId)}/chapters/${encodeURIComponent(chapterId)}` diff --git a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.ts b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.ts index 0986c0f..3e7e3ef 100644 --- a/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.ts +++ b/frontend/src/routes/manga/[id]/chapter/[chapter_id]/+page.ts @@ -1,5 +1,5 @@ import { getManga } from '$lib/api/mangas'; -import { getChapter, getChapterPages, listChapters } from '$lib/api/chapters'; +import { getChapter, getChapterPages, listAllChapters } from '$lib/api/chapters'; import { getMyReadProgressForManga } from '$lib/api/read_progress'; import type { PageLoad } from './$types'; @@ -13,19 +13,18 @@ export const load: PageLoad = async ({ params, url }) => { // `null` for guests or first-time openers — the reader uses // this to seed its session-local high-water mark. getMyReadProgressForManga(params.id), - // Loaded so the reader can compute prev/next chapter for the - // chevron-driven chapter navigation. limit=200 covers every - // realistic series; mangas with more chapters will lose some - // chapter-jump precision at the tail edge but the in-page - // navigation still works fine. - listChapters(params.id, { limit: 200 }) + // The FULL chapter list (paged through the API's 200-row cap) so the + // prev/next chevrons and the chapter dropdown work even for a deep + // chapter — a single 200-row window dead-ended chapter #250 with null + // neighbours. + listAllChapters(params.id) ]); return { manga, chapter, pages, readProgress, - chapters: chapterList.items, + chapters: chapterList, // `?page=N` lets the prev-chapter chevron land directly on the // last page of the chapter it just navigated to. `last` is a // convenience sentinel for "however many pages this chapter