fix: reader loads the full chapter list for prev/next and the dropdown
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.127.0",
|
"version": "0.127.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import {
|
import {
|
||||||
listChapters,
|
listChapters,
|
||||||
|
listAllChapters,
|
||||||
getChapter,
|
getChapter,
|
||||||
getChapterPages,
|
getChapterPages,
|
||||||
createChapter,
|
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 () => {
|
it('getChapterPages unwraps the {pages} envelope into the array', async () => {
|
||||||
fetchSpy.mockResolvedValueOnce(
|
fetchSpy.mockResolvedValueOnce(
|
||||||
ok({
|
ok({
|
||||||
|
|||||||
@@ -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<Chapter[]> {
|
||||||
|
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<Chapter> {
|
export async function getChapter(mangaId: string, chapterId: string): Promise<Chapter> {
|
||||||
return request<Chapter>(
|
return request<Chapter>(
|
||||||
`/v1/mangas/${encodeURIComponent(mangaId)}/chapters/${encodeURIComponent(chapterId)}`
|
`/v1/mangas/${encodeURIComponent(mangaId)}/chapters/${encodeURIComponent(chapterId)}`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getManga } from '$lib/api/mangas';
|
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 { getMyReadProgressForManga } from '$lib/api/read_progress';
|
||||||
import type { PageLoad } from './$types';
|
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
|
// `null` for guests or first-time openers — the reader uses
|
||||||
// this to seed its session-local high-water mark.
|
// this to seed its session-local high-water mark.
|
||||||
getMyReadProgressForManga(params.id),
|
getMyReadProgressForManga(params.id),
|
||||||
// Loaded so the reader can compute prev/next chapter for the
|
// The FULL chapter list (paged through the API's 200-row cap) so the
|
||||||
// chevron-driven chapter navigation. limit=200 covers every
|
// prev/next chevrons and the chapter dropdown work even for a deep
|
||||||
// realistic series; mangas with more chapters will lose some
|
// chapter — a single 200-row window dead-ended chapter #250 with null
|
||||||
// chapter-jump precision at the tail edge but the in-page
|
// neighbours.
|
||||||
// navigation still works fine.
|
listAllChapters(params.id)
|
||||||
listChapters(params.id, { limit: 200 })
|
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
manga,
|
manga,
|
||||||
chapter,
|
chapter,
|
||||||
pages,
|
pages,
|
||||||
readProgress,
|
readProgress,
|
||||||
chapters: chapterList.items,
|
chapters: chapterList,
|
||||||
// `?page=N` lets the prev-chapter chevron land directly on the
|
// `?page=N` lets the prev-chapter chevron land directly on the
|
||||||
// last page of the chapter it just navigated to. `last` is a
|
// last page of the chapter it just navigated to. `last` is a
|
||||||
// convenience sentinel for "however many pages this chapter
|
// convenience sentinel for "however many pages this chapter
|
||||||
|
|||||||
Reference in New Issue
Block a user