fix(bookshelf): merge groups and ungrouped books then sort them together (#3169)

This commit is contained in:
Huang Xin
2026-02-04 19:32:29 +08:00
committed by GitHub
parent 8dfab2c963
commit 92116e7455
2 changed files with 89 additions and 7 deletions
@@ -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<BookshelfProps> = ({
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<BookshelfProps> = ({
// 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(() => {
@@ -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.
*/