feat(library): add "Progress Read" sort option (#4427) (#4893)

Sort the library by reading progress (current/total pages). Books that
have never been opened read 0% and sort to the unread end; groups sort
by their most-progressed book. Direction reuses the existing
ascending/descending toggle, so "most read first" is Descending.

Adds the "Progress Read" entry to the Sort by menu and translates it
across all locales.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-02 23:52:17 +09:00
committed by GitHub
parent df34de1c38
commit 4d645befde
38 changed files with 132 additions and 33 deletions
@@ -195,6 +195,21 @@ describe('getBookSortValue', () => {
const book = createMockBook({ updatedAt: 99999 });
expect(getBookSortValue(book, 'unknown' as LibrarySortByType)).toBe(99999);
});
it('should return read ratio for Progress sort', () => {
const book = createMockBook({ progress: [50, 100] });
expect(getBookSortValue(book, LibrarySortByType.Progress)).toBe(0.5);
});
it('should return 0 for Progress sort when book has no progress', () => {
const book = createMockBook({});
expect(getBookSortValue(book, LibrarySortByType.Progress)).toBe(0);
});
it('should return 0 for Progress sort when total pages is 0', () => {
const book = createMockBook({ progress: [0, 0] });
expect(getBookSortValue(book, LibrarySortByType.Progress)).toBe(0);
});
});
describe('compareSortValues', () => {
@@ -310,4 +325,21 @@ describe('createBookSorter - additional cases', () => {
expect(sorted[0]!.title).toBe('Older');
expect(sorted[1]!.title).toBe('Newer');
});
it('should sort by reading progress ascending (unread books first)', () => {
const books = [
createMockBook({ title: 'Almost done', progress: [90, 100] }),
createMockBook({ title: 'Not started' }),
createMockBook({ title: 'Just begun', progress: [10, 100] }),
createMockBook({ title: 'Halfway', progress: [1, 2] }),
];
const sorter = createBookSorter(LibrarySortByType.Progress, 'en');
const sorted = [...books].sort(sorter);
expect(sorted.map((b) => b.title)).toEqual([
'Not started',
'Just begun',
'Halfway',
'Almost done',
]);
});
});
@@ -544,6 +544,18 @@ describe('getGroupSortValue', () => {
expect(getGroupSortValue(group, LibrarySortByType.Series)).toBe('My Series');
});
it('should return max read ratio for progress sort', () => {
const group = createMockGroup({
books: [
createMockBook({ progress: [10, 100] }),
createMockBook({ progress: [80, 100] }),
createMockBook({}),
],
});
expect(getGroupSortValue(group, LibrarySortByType.Progress)).toBe(0.8);
});
it('should handle empty groups gracefully', () => {
const group = createMockGroup({ books: [] });