forked from akai/readest
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<BookItemProps> = ({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const seriesText = formatSeries(book.metadata?.series, book.metadata?.seriesIndex);
|
||||
|
||||
return (
|
||||
<div
|
||||
role='none'
|
||||
@@ -112,15 +114,15 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-2')}>
|
||||
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-1')}>
|
||||
<h4
|
||||
className={clsx(
|
||||
'overflow-hidden text-ellipsis font-semibold',
|
||||
mode === 'grid' && 'block whitespace-nowrap text-[0.6em] text-xs',
|
||||
mode === 'list' && 'line-clamp-2 text-base',
|
||||
mode === 'list' && 'line-clamp-1 text-base',
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
@@ -131,6 +133,9 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{mode === 'list' && seriesText && (
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>{seriesText}</p>
|
||||
)}
|
||||
{mode === 'list' && (
|
||||
<h4 className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatDescription(book.metadata?.description)}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user