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); };