feat(reader): continue a comic series from the last page

Finishing an issue offered no way onward, so the next one had to be opened from the library.
Resolve the next readable book server-side with one keyset query, offer it on the last page, and
let an opt-in setting cross over on a further page turn. The reader route now remounts on a param
change, without which it would keep rendering the book it was mounted with.

Also cast the compared value in compareSeriesIndexSql: as a bare parameter Postgres could not type
it inside the null test and rejected the statement.

Closes #998
This commit is contained in:
neon
2026-08-25 04:26:31 +00:00
parent 6702d5b729
commit 63d4dc3bb4
28 changed files with 982 additions and 12 deletions
+10
View File
@@ -54,6 +54,13 @@ export function getFormatGroup(format: string): ReaderFormatGroup {
return FORMAT_TO_GROUP[format.toLowerCase()] ?? "epub";
}
/** Formats a given reader can open, so a caller can ask for "another file this same reader handles". */
export function getOpenableFormatsForGroup(group: ReaderFormatGroup): string[] {
return Object.entries(FORMAT_TO_GROUP)
.filter(([format, formatGroup]) => formatGroup === group && READER_OPENABLE_FORMATS.has(format))
.map(([format]) => format);
}
export interface EpubReaderSettings {
themeName: string; // matches one of the reader's built-in theme names
isDark: boolean;
@@ -98,6 +105,8 @@ export interface CbxReaderSettings {
forceTwoPage: boolean;
widePageSingletonMode: "auto" | "disable";
bgColor: "black" | "gray" | "white";
// Turning past the last page opens the next book in the series instead of stopping.
autoAdvance: boolean;
}
export interface AudioReaderSettings {
@@ -154,6 +163,7 @@ export const CBX_READER_DEFAULTS: CbxReaderSettings = {
forceTwoPage: false,
widePageSingletonMode: "auto",
bgColor: "black",
autoAdvance: false,
};
export const AUDIO_READER_DEFAULTS: AudioReaderSettings = {
+14
View File
@@ -1,4 +1,5 @@
import type { BooksPage } from "./book";
import type { SeriesIndex } from "./series-index";
export type SeriesSummary = {
id: number;
@@ -31,3 +32,16 @@ export type SeriesDetail = {
export type SeriesBooksPage = BooksPage & {
seriesInfo: SeriesDetail;
};
/** A series book the reader can open next, already resolved down to the file it should open. */
export type SeriesNextBook = {
bookId: number;
fileId: number;
format: string;
title: string | null;
seriesIndex: SeriesIndex | null;
};
export type SeriesNextBookResponse = {
next: SeriesNextBook | null;
};