feat(authors): rebuild the page as an index and a gallery on one view toggle

The two layouts are the viewMode the app already persists, so search, filters
and sort carry across them. Only 19% of a library's authors have a portrait,
so the gallery leans on monograms and the index treats the name as the content;
a sort name now prints only when it says something the display name does not.
Jump buckets, a cover fallback and two filters come from the server so the A-Z
rail and the chips cover the whole result set rather than the loaded pages.

Reconciles authors.has_photo against the image store on boot: the list resolves
portraits from disk while the filter queried the column, so "no portrait" was
over-reporting by 64.
This commit is contained in:
neon
2026-08-27 04:04:18 +00:00
parent 95239ba701
commit b41675c608
51 changed files with 2131 additions and 1108 deletions
+12
View File
@@ -9,6 +9,18 @@ export type AuthorSummary = {
imageUrl?: string | null;
bookCount: number;
lastAddedAt: string | null;
/**
* A book of theirs that has cover art, most recently added first. Only 19% of a
* typical library's authors have a portrait, so the grid falls back to this
* before it falls back to a monogram. Null when none of their books has a cover.
*/
coverBookId?: number | null;
};
/** One bucket of the authors A-Z rail. `letter` is "#" for anything non-alphabetic. */
export type AuthorLetterCount = {
letter: string;
count: number;
};
export type AuthorDetail = AuthorSummary & {
@@ -50,6 +50,9 @@ export interface DisplayPreferences {
smartScopeFilterExpanded: boolean;
authorCoverSize: number;
authorCoverShape: AuthorCoverShape;
authorRowDensity: TableDensity;
/** Stand a book's cover in for a missing author portrait. Off: monogram instead. */
authorCoverFallback: boolean;
tableZebraStriping: boolean;
tableDensity: TableDensity;
bookSpineOverlay: BookSpineOverlay;