From 92116e74551a06b2d5967b5367c7be5151ce0db9 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 4 Feb 2026 19:32:29 +0800 Subject: [PATCH] fix(bookshelf): merge groups and ungrouped books then sort them together (#3169) --- .../src/app/library/components/Bookshelf.tsx | 43 ++++++++++++--- .../src/app/library/utils/libraryUtils.ts | 53 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index a1fc82cc..2ab8623b 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -26,6 +26,9 @@ import { createWithinGroupSorter, ensureLibraryGroupByType, ensureLibrarySortByType, + getBookSortValue, + getGroupSortValue, + compareSortValues, } from '../utils/libraryUtils'; import { eventDispatcher } from '@/utils/event'; @@ -180,10 +183,6 @@ const Bookshelf: React.FC = ({ const ungroupedBooks = currentBookshelfItems.filter((item): item is Book => 'format' in item); const groups = currentBookshelfItems.filter((item): item is BooksGroup => 'books' in item); - // Sort groups by aggregate value - const groupSorter = createGroupSorter(sortBy, uiLanguage); - groups.sort((a, b) => groupSorter(a, b) * sortOrderMultiplier); - // Sort books within each group const withinGroupSorter = createWithinGroupSorter(groupBy, sortBy, uiLanguage); groups.forEach((group) => { @@ -192,15 +191,45 @@ const Bookshelf: React.FC = ({ // Sort ungrouped books - use within-group sorter if we're inside a group // (for series, this ensures books are sorted by series index) + const bookSorter = createBookSorter(sortBy, uiLanguage); if (groupId && groupBy !== LibraryGroupByType.Manual && groupBy !== LibraryGroupByType.None) { ungroupedBooks.sort((a, b) => withinGroupSorter(a, b) * sortOrderMultiplier); } else { - const bookSorter = createBookSorter(sortBy, uiLanguage); ungroupedBooks.sort((a, b) => bookSorter(a, b) * sortOrderMultiplier); } - // Return groups first, then ungrouped books - return [...groups, ...ungroupedBooks]; + // Merge groups and ungrouped books, then sort them together + const allItems: (Book | BooksGroup)[] = [...groups, ...ungroupedBooks]; + const groupSorter = createGroupSorter(sortBy, uiLanguage); + + allItems.sort((a, b) => { + const isAGroup = 'books' in a; + const isBGroup = 'books' in b; + + // If both are groups, use group sorter + if (isAGroup && isBGroup) { + return groupSorter(a, b) * sortOrderMultiplier; + } + + // If both are books, use book sorter + if (!isAGroup && !isBGroup) { + return bookSorter(a, b) * sortOrderMultiplier; + } + + // One is a group, one is a book - compare their sort values + if (isAGroup && !isBGroup) { + const groupValue = getGroupSortValue(a, sortBy); + const bookValue = getBookSortValue(b, sortBy); + return compareSortValues(groupValue, bookValue, uiLanguage) * sortOrderMultiplier; + } else if (!isAGroup && isBGroup) { + const bookValue = getBookSortValue(a, sortBy); + const groupValue = getGroupSortValue(b, sortBy); + return compareSortValues(bookValue, groupValue, uiLanguage) * sortOrderMultiplier; + } + return 0; + }); + + return allItems; }, [sortOrder, sortBy, groupBy, groupId, uiLanguage, currentBookshelfItems]); useEffect(() => { diff --git a/apps/readest-app/src/app/library/utils/libraryUtils.ts b/apps/readest-app/src/app/library/utils/libraryUtils.ts index 5c9aa40a..7dfe7f1d 100644 --- a/apps/readest-app/src/app/library/utils/libraryUtils.ts +++ b/apps/readest-app/src/app/library/utils/libraryUtils.ts @@ -295,6 +295,38 @@ export const createWithinGroupSorter = return createBookSorter(sortBy, uiLanguage)(a, b); }; +/** + * Get the sort value from a book for comparison with groups. + */ +export const getBookSortValue = (book: Book, sortBy: LibrarySortByType): number | string => { + switch (sortBy) { + case LibrarySortByType.Title: + return formatTitle(book.title); + + case LibrarySortByType.Author: + return formatAuthors(book.author, book?.primaryLanguage || 'en', true); + + case LibrarySortByType.Updated: + return book.updatedAt; + + case LibrarySortByType.Created: + return book.createdAt; + + case LibrarySortByType.Format: + return book.format; + + case LibrarySortByType.Published: { + const published = book.metadata?.published; + if (!published) return 0; + const publishedTime = new Date(published).getTime(); + return isNaN(publishedTime) ? 0 : publishedTime; + } + + default: + return book.updatedAt; + } +}; + /** * Get the aggregate sort value from a group for sorting groups. */ @@ -344,6 +376,27 @@ export const getGroupSortValue = ( } }; +/** + * Compare two sort values (string or number) for sorting. + */ +export const compareSortValues = ( + aValue: number | string, + bValue: number | string, + uiLanguage: string, +): number => { + // String comparison for text-based sorts + if (typeof aValue === 'string' && typeof bValue === 'string') { + return aValue.localeCompare(bValue, uiLanguage || navigator.language); + } + + // Numeric comparison for date-based sorts + if (typeof aValue === 'number' && typeof bValue === 'number') { + return aValue - bValue; + } + + return 0; +}; + /** * Create a sorter for groups themselves based on sort criteria. */