From 757ed8066b9f59feb3cd1d8b6460d958d2982d27 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 16 Jun 2026 23:07:27 +0800 Subject: [PATCH] feat(library): show series and number in list view (#4593) (#4612) In the library list view, surface each book's series and series number on their own line, in addition to the description. Previously series info was only visible by grouping by series or opening a book's details. - Add `formatSeries(series, seriesIndex)` helper ("Series #N", trims the name, omits a zero/NaN/negative index) with unit tests. - In list mode, render a dedicated single-line "Series #N" line above the description when the book has series metadata. - Clamp every list line (incl. title) to one line and tighten the row gap to `gap-1` so the extra line fits the fixed-height row without clipping. Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/__tests__/utils/book.test.ts | 34 +++++++++++++++++++ .../src/app/library/components/BookItem.tsx | 13 ++++--- apps/readest-app/src/utils/book.ts | 8 +++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 apps/readest-app/src/__tests__/utils/book.test.ts diff --git a/apps/readest-app/src/__tests__/utils/book.test.ts b/apps/readest-app/src/__tests__/utils/book.test.ts new file mode 100644 index 00000000..04d41a80 --- /dev/null +++ b/apps/readest-app/src/__tests__/utils/book.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { formatSeries } from '@/utils/book'; + +describe('formatSeries', () => { + it('returns an empty string when there is no series name', () => { + expect(formatSeries(undefined)).toBe(''); + expect(formatSeries('')).toBe(''); + expect(formatSeries(' ')).toBe(''); + }); + + it('returns an empty string when only an index is present', () => { + expect(formatSeries(undefined, 3)).toBe(''); + }); + + it('returns the trimmed series name when no index is present', () => { + expect(formatSeries('Harry Potter')).toBe('Harry Potter'); + expect(formatSeries(' The Expanse ')).toBe('The Expanse'); + }); + + it('appends the series number when a positive index is present', () => { + expect(formatSeries('Harry Potter', 3)).toBe('Harry Potter #3'); + expect(formatSeries(' The Expanse ', 2)).toBe('The Expanse #2'); + }); + + it('supports fractional series indices', () => { + expect(formatSeries('The Expanse', 1.5)).toBe('The Expanse #1.5'); + }); + + it('omits the number when the index is zero or not a finite number', () => { + expect(formatSeries('Harry Potter', 0)).toBe('Harry Potter'); + expect(formatSeries('Harry Potter', Number.NaN)).toBe('Harry Potter'); + }); +}); diff --git a/apps/readest-app/src/app/library/components/BookItem.tsx b/apps/readest-app/src/app/library/components/BookItem.tsx index 6159b180..536a6557 100644 --- a/apps/readest-app/src/app/library/components/BookItem.tsx +++ b/apps/readest-app/src/app/library/components/BookItem.tsx @@ -16,7 +16,7 @@ import { useSettingsStore } from '@/store/settingsStore'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings'; import { navigateToLogin } from '@/utils/nav'; -import { formatAuthors, formatDescription } from '@/utils/book'; +import { formatAuthors, formatDescription, formatSeries } from '@/utils/book'; import ReadingProgress from './ReadingProgress'; import BookCover from '@/components/BookCover'; @@ -65,6 +65,8 @@ const BookItem: React.FC = ({ } : undefined; + const seriesText = formatSeries(book.metadata?.series, book.metadata?.seriesIndex); + return (
= ({ className={clsx( 'flex w-full flex-col p-0', mode === 'grid' && 'pt-2', - mode === 'list' && 'gap-2 py-0', + mode === 'list' && 'gap-1 py-0', )} > -
+

{book.title} @@ -131,6 +133,9 @@ const BookItem: React.FC = ({

)}

+ {mode === 'list' && seriesText && ( +

{seriesText}

+ )} {mode === 'list' && (

{formatDescription(book.metadata?.description)} diff --git a/apps/readest-app/src/utils/book.ts b/apps/readest-app/src/utils/book.ts index 10a9061f..90473640 100644 --- a/apps/readest-app/src/utils/book.ts +++ b/apps/readest-app/src/utils/book.ts @@ -158,6 +158,14 @@ export const formatDescription = (description?: string | LanguageMap) => { .trim(); }; +export const formatSeries = (series?: string, seriesIndex?: number) => { + const name = series?.trim(); + if (!name) return ''; + const hasIndex = + typeof seriesIndex === 'number' && Number.isFinite(seriesIndex) && seriesIndex > 0; + return hasIndex ? `${name} #${seriesIndex}` : name; +}; + export const formatPublisher = (publisher: string | LanguageMap) => { return typeof publisher === 'string' ? publisher : formatLanguageMap(publisher); };