forked from akai/readest
feat(library): import-failure modal + group sort + Android callout fix (#4345)
* fix(library): suppress Android image callout on book covers Long-pressing a cover on Android could trigger the WebView's native image callout at the same time as the bookshelf's own 500ms long-press handler for multi-select, causing apparent freezes. `-webkit-touch- callout: none` doesn't inherit, so the existing `.no-context-menu` rule on the item container never reached the cover `<img>`. Apply the callout suppression to descendant images/anchors and disable native drag on the cover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(library): show modal for multi-file import failures When a batch import yields more than one failure, the previous toast crammed every filename onto a single line that often overflowed and truncated. Add a dialog that lists each failed filename with its error reason, dedupes the message into a header banner when every file failed for the same reason, and falls back to the existing toast for single-file failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(library): sort manage-group modal by most recent activity The Group Books modal listed groups in store-insertion order, which made recently-active groups hard to find in libraries with many groups. Sort each level desc by the newest `updatedAt` across the group's books, propagating up the path so a recently-touched book in `Literature/Fiction` keeps `Literature` fresh too. Extract the index as `buildGroupNameUpdatedAt` in libraryUtils for reuse and unit testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(library): tighten select-mode action bar and header polish - SelectModeActions: switch the narrow-viewport grid from 3 columns to 4 (with the delete action explicitly placed in column 2) so the icon set stops wrapping awkwardly on phones below ~500px. - LibraryHeader: keep the "Select All" / "Deselect" label on a single line so it doesn't wrap and shove the underlying button taller. - SetStatusAlert: drop the hover bg on the small-screen cancel button and rely on text-color contrast so it stops flashing a tinted disc on mobile taps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
findGroupById,
|
||||
getGroupDisplayName,
|
||||
expandBookshelfSelection,
|
||||
buildGroupNameUpdatedAt,
|
||||
} from '../../app/library/utils/libraryUtils';
|
||||
import { Book, BooksGroup } from '../../types/book';
|
||||
import { LibraryGroupByType, LibrarySortByType } from '../../types/settings';
|
||||
@@ -1065,3 +1066,51 @@ describe('expandBookshelfSelection', () => {
|
||||
expect(expandBookshelfSelection(['ghost-hash'], [])).toEqual(['ghost-hash']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildGroupNameUpdatedAt', () => {
|
||||
it('takes the max updatedAt across books in each direct group', () => {
|
||||
const books = [
|
||||
createMockBook({ groupName: 'Fiction', updatedAt: 100 }),
|
||||
createMockBook({ groupName: 'Fiction', updatedAt: 300 }),
|
||||
createMockBook({ groupName: 'Fiction', updatedAt: 200 }),
|
||||
createMockBook({ groupName: 'History', updatedAt: 50 }),
|
||||
];
|
||||
const map = buildGroupNameUpdatedAt(books);
|
||||
expect(map.get('Fiction')).toBe(300);
|
||||
expect(map.get('History')).toBe(50);
|
||||
});
|
||||
|
||||
it('propagates a descendant book up to all ancestor groups', () => {
|
||||
const books = [
|
||||
createMockBook({ groupName: 'Literature/Fiction/Sci-Fi', updatedAt: 500 }),
|
||||
createMockBook({ groupName: 'Literature', updatedAt: 100 }),
|
||||
];
|
||||
const map = buildGroupNameUpdatedAt(books);
|
||||
expect(map.get('Literature/Fiction/Sci-Fi')).toBe(500);
|
||||
expect(map.get('Literature/Fiction')).toBe(500);
|
||||
expect(map.get('Literature')).toBe(500);
|
||||
});
|
||||
|
||||
it('ignores books without groupName or updatedAt', () => {
|
||||
const books = [
|
||||
createMockBook({ groupName: undefined, updatedAt: 999 }),
|
||||
createMockBook({ groupName: 'A', updatedAt: 0 }),
|
||||
createMockBook({ groupName: 'A', updatedAt: 42 }),
|
||||
];
|
||||
const map = buildGroupNameUpdatedAt(books);
|
||||
expect(map.get('A')).toBe(42);
|
||||
expect(map.size).toBe(1);
|
||||
});
|
||||
|
||||
it('produces a desc-sort by group freshness when used as the sort key', () => {
|
||||
const books = [
|
||||
createMockBook({ groupName: 'Old', updatedAt: 1 }),
|
||||
createMockBook({ groupName: 'Newer', updatedAt: 10 }),
|
||||
createMockBook({ groupName: 'Newest', updatedAt: 100 }),
|
||||
];
|
||||
const map = buildGroupNameUpdatedAt(books);
|
||||
const groups = [{ name: 'Old' }, { name: 'Newest' }, { name: 'Newer' }];
|
||||
groups.sort((a, b) => (map.get(b.name) ?? 0) - (map.get(a.name) ?? 0));
|
||||
expect(groups.map((g) => g.name)).toEqual(['Newest', 'Newer', 'Old']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user