feat(authors): collapse series on author pages (#1271)

Reduce large author listings to one card per series while preserving book counts, scoped visibility, and row-based pagination. Compute both totals in the collapsed query so deep pages remain consistent without a second author-wide scan.

Closes #301

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
This commit is contained in:
SergeiAK
2026-09-10 21:39:14 +03:00
committed by GitHub
parent b7e1e21d07
commit 73ac0bb722
23 changed files with 953 additions and 59 deletions
+9 -1
View File
@@ -43,7 +43,15 @@ export type AuthorsPage = {
size: number;
};
export type AuthorBooksPage = BooksPage;
/**
* `total` counts rows in the page's own unit, so it is the number of cards once
* `collapseSeries` folds a series into one card - that is what paginating and "is there more"
* have to be driven by. `bookTotal` stays the number of books behind those rows, for the
* places that count books rather than cards. The two are equal while the list is flat.
*/
export type AuthorBooksPage = BooksPage & {
bookTotal: number;
};
export type MergeAuthorsResult = {
target: AuthorDetail;
+11 -1
View File
@@ -3,6 +3,13 @@ export type SeriesCollapsePreferences = {
libraries: Record<string, boolean>;
collections: Record<string, boolean>;
smartScopes?: Record<string, boolean>;
/**
* Author pages share one flag instead of a per-author bucket: the bucket would grow with every
* author browsed, and "collapsed for King but not for Pratchett" is not a distinction anyone
* asks for. Absent means off, so an upgrade leaves author pages flat even for a user whose
* {@link SeriesCollapsePreferences.global} is on.
*/
authorPages?: boolean;
};
export type CollapsedSeriesInfo = {
@@ -18,9 +25,12 @@ export type CollapsedSeriesInfo = {
export function resolveCollapsePreference(
prefs: SeriesCollapsePreferences | undefined,
ctx: { libraryId?: number; collectionId?: number; smartScopeId?: number },
ctx: { libraryId?: number; collectionId?: number; smartScopeId?: number; authorPages?: boolean },
): boolean {
if (!prefs) return false;
// Author pages resolve to their own flag and stop there - they deliberately do not inherit the
// global default, and the library filter on an author page is a filter, not a scope to override.
if (ctx.authorPages) return prefs.authorPages ?? false;
if (ctx.smartScopeId !== undefined) {
const override = prefs.smartScopes?.[String(ctx.smartScopeId)];
if (override !== undefined) return override;