A "Continue reading" shelf listing series the reader already finished (read to the last page, nothing new) is odd. Expose the last-read chapter's page_count on the read-progress list, and filter the shelf to drop entries that are caught up (on the last page of the latest chapter with no new chapters). Mid-chapter and has-new-chapters entries stay; the full history view is unaffected. Finished detection is a pure, unit-tested helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { ApiError, request, type Page } from './client';
|
|
|
|
export type ReadProgress = {
|
|
user_id: string;
|
|
manga_id: string;
|
|
chapter_id: string | null;
|
|
page: number;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type ReadProgressSummary = {
|
|
manga_id: string;
|
|
manga_title: string;
|
|
manga_cover_image_path: string | null;
|
|
chapter_id: string | null;
|
|
/** `null` if the chapter was deleted after the progress was written. */
|
|
chapter_number: number | null;
|
|
/** Page count of the last-read chapter (`null` when unknown / deleted).
|
|
* Lets the shelf tell a finished series from one still in progress. */
|
|
chapter_page_count: number | null;
|
|
page: number;
|
|
updated_at: string;
|
|
/** Chapters past the reader's last-read chapter — a personal "new
|
|
* since you last read" count. `0` when caught up or when the position
|
|
* is unknown (manga-level progress / deleted chapter). */
|
|
new_chapters_count: number;
|
|
};
|
|
|
|
export type ReadProgressPage = {
|
|
items: ReadProgressSummary[];
|
|
page: Page;
|
|
};
|
|
|
|
export type UpsertReadProgress = {
|
|
manga_id: string;
|
|
chapter_id?: string | null;
|
|
page?: number | null;
|
|
};
|
|
|
|
export async function updateReadProgress(
|
|
input: UpsertReadProgress
|
|
): Promise<ReadProgress> {
|
|
return request<ReadProgress>('/v1/me/read-progress', {
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export async function listMyReadProgress(
|
|
opts: { limit?: number; offset?: number } = {}
|
|
): Promise<ReadProgressPage> {
|
|
const params = new URLSearchParams();
|
|
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
const qs = params.toString();
|
|
return request<ReadProgressPage>(
|
|
`/v1/me/read-progress${qs ? `?${qs}` : ''}`
|
|
);
|
|
}
|
|
|
|
export async function listMyReadProgressOrEmpty(): Promise<ReadProgressPage> {
|
|
try {
|
|
return await listMyReadProgress();
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 401) {
|
|
return { items: [], page: { limit: 50, offset: 0, total: null } };
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Single-manga response shape returned by GET /me/read-progress/:id.
|
|
* Includes `chapter_number` so the "Continue reading" CTA can render
|
|
* without resolving the chapter id against a paged chapters list.
|
|
*/
|
|
export type ReadProgressForManga = {
|
|
manga_id: string;
|
|
chapter_id: string | null;
|
|
/** `null` if the chapter was deleted after the progress was written. */
|
|
chapter_number: number | null;
|
|
page: number;
|
|
updated_at: string;
|
|
/** Distinct chapter numbers past the last-read chapter — the
|
|
* authoritative "new since last read" count (over all chapters, not the
|
|
* detail page's paginated list). `0` when caught up / position unknown. */
|
|
new_chapters_count: number;
|
|
};
|
|
|
|
/**
|
|
* Returns the user's progress for a specific manga, or `null` when
|
|
* they've never opened it (or aren't signed in). Used by the manga
|
|
* detail page's "Continue from Ch. N" CTA and by the reader to seed
|
|
* its session-local high-water mark from the persisted value.
|
|
*/
|
|
export async function getMyReadProgressForManga(
|
|
mangaId: string
|
|
): Promise<ReadProgressForManga | null> {
|
|
try {
|
|
return await request<ReadProgressForManga>(
|
|
`/v1/me/read-progress/${encodeURIComponent(mangaId)}`
|
|
);
|
|
} catch (e) {
|
|
if (e instanceof ApiError && (e.status === 404 || e.status === 401)) {
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export async function clearReadProgress(mangaId: string): Promise<void> {
|
|
await request<void>(
|
|
`/v1/me/read-progress/${encodeURIComponent(mangaId)}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
}
|