forked from akai/readest
perf: eliminate redundant re-renders of book cover components (#1685)
This commit is contained in:
@@ -22,7 +22,7 @@ interface BookItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
coverFit: LibraryCoverFitType;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
bookSelected: boolean;
|
||||
transferProgress: number | null;
|
||||
handleBookUpload: (book: Book) => void;
|
||||
handleBookDownload: (book: Book) => void;
|
||||
@@ -34,7 +34,7 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
mode,
|
||||
coverFit,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
bookSelected,
|
||||
transferProgress,
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
@@ -67,12 +67,12 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
)}
|
||||
>
|
||||
<BookCover mode={mode} book={book} coverFit={coverFit} />
|
||||
{selectedBooks.includes(book.hash) && (
|
||||
{bookSelected && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
{isSelectMode && (
|
||||
<div className='absolute bottom-1 right-1'>
|
||||
{selectedBooks.includes(book.hash) ? (
|
||||
{bookSelected ? (
|
||||
<MdCheckCircle className='fill-blue-500' />
|
||||
) : (
|
||||
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
@@ -54,7 +54,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
|
||||
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
|
||||
const [showGroupingModal, setShowGroupingModal] = useState(false);
|
||||
@@ -70,6 +69,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const isImportingBook = useRef(false);
|
||||
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
|
||||
const allBookshelfItems =
|
||||
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
|
||||
|
||||
@@ -162,24 +162,24 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, libraryBooks, showGroupingModal]);
|
||||
|
||||
const toggleSelection = (id: string) => {
|
||||
setSelectedBooks((prev) =>
|
||||
prev.includes(id) ? prev.filter((selectedId) => selectedId !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
toggleSelectedBook(id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const openSelectedBooks = () => {
|
||||
handleSetSelectMode(false);
|
||||
if (appService?.hasWindow && settings.openBookInNewWindow) {
|
||||
showReaderWindow(appService, selectedBooks);
|
||||
showReaderWindow(appService, getSelectedBooks());
|
||||
} else {
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
navigateToReader(router, selectedBooks);
|
||||
navigateToReader(router, getSelectedBooks());
|
||||
}
|
||||
};
|
||||
|
||||
const openBookDetails = () => {
|
||||
handleSetSelectMode(false);
|
||||
const selectedBooks = getSelectedBooks();
|
||||
const book = libraryBooks.find((book) => book.hash === selectedBooks[0]);
|
||||
if (book) {
|
||||
handleShowDetailsBook(book);
|
||||
@@ -188,6 +188,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
|
||||
const getBooksToDelete = () => {
|
||||
const booksToDelete: Book[] = [];
|
||||
const selectedBooks = getSelectedBooks();
|
||||
selectedBooks.forEach((id) => {
|
||||
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
|
||||
if (book && !book.deletedAt) {
|
||||
@@ -293,6 +294,8 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSelectMode, isSelectAll, isSelectNone]);
|
||||
|
||||
const selectedBooks = getSelectedBooks();
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
<div
|
||||
@@ -310,7 +313,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
mode={viewMode as LibraryViewModeType}
|
||||
coverFit={coverFit as LibraryCoverFitType}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
itemSelected={
|
||||
'hash' in item ? selectedBooks.includes(item.hash) : selectedBooks.includes(item.id)
|
||||
}
|
||||
setLoading={setLoading}
|
||||
toggleSelection={toggleSelection}
|
||||
handleBookUpload={handleBookUpload}
|
||||
|
||||
@@ -77,7 +77,7 @@ interface BookshelfItemProps {
|
||||
item: BookshelfItem;
|
||||
coverFit: LibraryCoverFitType;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
itemSelected: boolean;
|
||||
transferProgress: number | null;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
toggleSelection: (hash: string) => void;
|
||||
@@ -93,7 +93,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
item,
|
||||
coverFit,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
itemSelected,
|
||||
transferProgress,
|
||||
setLoading,
|
||||
toggleSelection,
|
||||
@@ -110,11 +110,12 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const { settings } = useSettingsStore();
|
||||
const { updateBook } = useLibraryStore();
|
||||
|
||||
const showBookDetailsModal = async (book: Book) => {
|
||||
const showBookDetailsModal = useCallback(async (book: Book) => {
|
||||
if (await makeBookAvailable(book)) {
|
||||
handleShowDetailsBook(book);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const makeBookAvailable = async (book: Book) => {
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
@@ -122,7 +123,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
if (!book.downloadedAt || !book.coverDownloadedAt) {
|
||||
book.downloadedAt = Date.now();
|
||||
book.coverDownloadedAt = Date.now();
|
||||
updateBook(envConfig, book);
|
||||
await updateBook(envConfig, book);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -130,7 +131,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 200);
|
||||
try {
|
||||
available = await handleBookDownload(book);
|
||||
updateBook(envConfig, book);
|
||||
await updateBook(envConfig, book);
|
||||
} finally {
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
@@ -145,11 +146,14 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
if (isSelectMode) {
|
||||
toggleSelection(book.hash);
|
||||
} else {
|
||||
if (!(await makeBookAvailable(book))) return;
|
||||
const available = await makeBookAvailable(book);
|
||||
if (!available) return;
|
||||
if (appService?.hasWindow && settings.openBookInNewWindow) {
|
||||
showReaderWindow(appService, [book.hash]);
|
||||
} else {
|
||||
navigateToReader(router, [book.hash]);
|
||||
setTimeout(() => {
|
||||
navigateToReader(router, [book.hash]);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -164,7 +168,9 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
} else {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('group', group.id);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
setTimeout(() => {
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -177,7 +183,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const fileRevealLabel =
|
||||
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
|
||||
const selectBookMenuItem = await MenuItem.new({
|
||||
text: selectedBooks.includes(book.hash) ? _('Deselect Book') : _('Select Book'),
|
||||
text: itemSelected ? _('Deselect Book') : _('Select Book'),
|
||||
action: async () => {
|
||||
if (!isSelectMode) handleSetSelectMode(true);
|
||||
toggleSelection(book.hash);
|
||||
@@ -231,7 +237,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const groupContextMenuHandler = async (group: BooksGroup) => {
|
||||
if (!appService?.hasContextMenu) return;
|
||||
const selectGroupMenuItem = await MenuItem.new({
|
||||
text: selectedBooks.includes(group.id) ? _('Deselect Group') : _('Select Group'),
|
||||
text: itemSelected ? _('Deselect Group') : _('Select Group'),
|
||||
action: async () => {
|
||||
if (!isSelectMode) handleSetSelectMode(true);
|
||||
toggleSelection(group.id);
|
||||
@@ -291,7 +297,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
groupContextMenuHandler(item as BooksGroup);
|
||||
}
|
||||
}, 100),
|
||||
[],
|
||||
[itemSelected],
|
||||
);
|
||||
|
||||
const { pressing, handlers } = useLongPress(
|
||||
@@ -335,14 +341,14 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
book={item}
|
||||
coverFit={coverFit}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
bookSelected={itemSelected}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} groupSelected={itemSelected} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,10 @@ import BookCover from '@/components/BookCover';
|
||||
interface GroupItemProps {
|
||||
group: BooksGroup;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
groupSelected: boolean;
|
||||
}
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBooks }) => {
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelected }) => {
|
||||
const { appService } = useEnv();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
@@ -30,12 +30,12 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBook
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedBooks.includes(group.id) && (
|
||||
{groupSelected && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
{isSelectMode && (
|
||||
<div className='absolute bottom-1 right-1'>
|
||||
{selectedBooks.includes(group.id) ? (
|
||||
{groupSelected ? (
|
||||
<MdCheckCircle className='fill-blue-500' />
|
||||
) : (
|
||||
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
|
||||
|
||||
@@ -470,70 +470,78 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}));
|
||||
}, 500);
|
||||
|
||||
const handleBookUpload = async (book: Book) => {
|
||||
try {
|
||||
await appService?.uploadBook(book, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
pushLibrary();
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
message: _('Book uploaded: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('Not authenticated') && settings.keepLogin) {
|
||||
settings.keepLogin = false;
|
||||
setSettings(settings);
|
||||
navigateToLogin(router);
|
||||
return false;
|
||||
} else if (err.message.includes('Insufficient storage quota')) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Insufficient storage quota'),
|
||||
});
|
||||
return false;
|
||||
const handleBookUpload = useCallback(
|
||||
async (book: Book) => {
|
||||
try {
|
||||
await appService?.uploadBook(book, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
pushLibrary();
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
message: _('Book uploaded: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('Not authenticated') && settings.keepLogin) {
|
||||
settings.keepLogin = false;
|
||||
setSettings(settings);
|
||||
navigateToLogin(router);
|
||||
return false;
|
||||
} else if (err.message.includes('Insufficient storage quota')) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Insufficient storage quota'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Failed to upload book: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Failed to upload book: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[appService],
|
||||
);
|
||||
|
||||
const handleBookDownload = async (book: Book, redownload = false) => {
|
||||
try {
|
||||
await appService?.downloadBook(book, false, redownload, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
message: _('Book downloaded: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Failed to download book: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const handleBookDownload = useCallback(
|
||||
async (book: Book, redownload = false) => {
|
||||
try {
|
||||
await appService?.downloadBook(book, false, redownload, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
message: _('Book downloaded: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Failed to download book: {{title}}', {
|
||||
title: book.title,
|
||||
}),
|
||||
type: 'error',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[appService],
|
||||
);
|
||||
|
||||
const handleBookDelete = async (book: Book) => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { Book } from '@/types/book';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { formatAuthors, formatTitle } from '@/utils/book';
|
||||
@@ -14,102 +14,110 @@ interface BookCoverProps {
|
||||
isPreview?: boolean;
|
||||
}
|
||||
|
||||
const BookCover: React.FC<BookCoverProps> = ({
|
||||
book,
|
||||
mode = 'grid',
|
||||
coverFit = 'crop',
|
||||
className,
|
||||
imageClassName,
|
||||
isPreview,
|
||||
}) => {
|
||||
const coverRef = useRef<HTMLDivElement>(null);
|
||||
const BookCover: React.FC<BookCoverProps> = memo<BookCoverProps>(
|
||||
({ book, mode = 'grid', coverFit = 'crop', className, imageClassName, isPreview }) => {
|
||||
const coverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleImageVisibility = (showImage: boolean) => {
|
||||
if (coverRef.current) {
|
||||
const coverImage = coverRef.current.querySelector('.cover-image');
|
||||
const fallbackCover = coverRef.current.querySelector('.fallback-cover');
|
||||
if (coverImage) {
|
||||
coverImage.classList.toggle('invisible', !showImage);
|
||||
const toggleImageVisibility = (showImage: boolean) => {
|
||||
if (coverRef.current) {
|
||||
const coverImage = coverRef.current.querySelector('.cover-image');
|
||||
const fallbackCover = coverRef.current.querySelector('.fallback-cover');
|
||||
if (coverImage) {
|
||||
coverImage.classList.toggle('invisible', !showImage);
|
||||
}
|
||||
if (fallbackCover) {
|
||||
fallbackCover.classList.toggle('invisible', showImage);
|
||||
}
|
||||
}
|
||||
if (fallbackCover) {
|
||||
fallbackCover.classList.toggle('invisible', showImage);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
toggleImageVisibility(false);
|
||||
};
|
||||
const handleImageError = () => {
|
||||
toggleImageVisibility(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
toggleImageVisibility(true);
|
||||
}, [book.metadata?.coverImageUrl, book.coverImageUrl]);
|
||||
useEffect(() => {
|
||||
toggleImageVisibility(true);
|
||||
}, [book.metadata?.coverImageUrl, book.coverImageUrl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={coverRef}
|
||||
className={clsx('book-cover-container relative flex h-full w-full', className)}
|
||||
>
|
||||
{coverFit === 'crop' ? (
|
||||
<Image
|
||||
src={book.metadata?.coverImageUrl || book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
fill={true}
|
||||
className={clsx('cover-image crop-cover-img object-cover', imageClassName)}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full w-full justify-center',
|
||||
mode === 'grid' ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
ref={coverRef}
|
||||
className={clsx('book-cover-container relative flex h-full w-full', className)}
|
||||
>
|
||||
{coverFit === 'crop' ? (
|
||||
<Image
|
||||
src={book.metadata?.coverImageUrl || book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes='100vw'
|
||||
className={clsx(
|
||||
'cover-image fit-cover-img h-auto max-h-full w-auto max-w-full shadow-md',
|
||||
imageClassName,
|
||||
)}
|
||||
fill={true}
|
||||
className={clsx('cover-image crop-cover-img object-cover', imageClassName)}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'fallback-cover invisible absolute inset-0 rounded-none p-2',
|
||||
'text-neutral-content text-center font-serif font-medium',
|
||||
isPreview ? 'bg-base-200/50' : 'bg-base-100',
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full w-full justify-center',
|
||||
mode === 'grid' ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={book.metadata?.coverImageUrl || book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes='100vw'
|
||||
className={clsx(
|
||||
'cover-image fit-cover-img h-auto max-h-full w-auto max-w-full shadow-md',
|
||||
imageClassName,
|
||||
)}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className='flex h-1/2 items-center justify-center'>
|
||||
<span
|
||||
className={clsx(
|
||||
isPreview ? 'line-clamp-2' : mode === 'grid' ? 'line-clamp-3' : 'line-clamp-2',
|
||||
isPreview ? 'text-[0.5em]' : mode === 'grid' ? 'text-lg' : 'text-sm',
|
||||
)}
|
||||
>
|
||||
{formatTitle(book.title)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='h-1/6'></div>
|
||||
<div className='flex h-1/3 items-center justify-center'>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-neutral-content/50 line-clamp-1',
|
||||
isPreview ? 'text-[0.4em]' : mode === 'grid' ? 'text-base' : 'text-xs',
|
||||
)}
|
||||
>
|
||||
{formatAuthors(book.author)}
|
||||
</span>
|
||||
<div
|
||||
className={clsx(
|
||||
'fallback-cover invisible absolute inset-0 rounded-none p-2',
|
||||
'text-neutral-content text-center font-serif font-medium',
|
||||
isPreview ? 'bg-base-200/50' : 'bg-base-100',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-1/2 items-center justify-center'>
|
||||
<span
|
||||
className={clsx(
|
||||
isPreview ? 'line-clamp-2' : mode === 'grid' ? 'line-clamp-3' : 'line-clamp-2',
|
||||
isPreview ? 'text-[0.5em]' : mode === 'grid' ? 'text-lg' : 'text-sm',
|
||||
)}
|
||||
>
|
||||
{formatTitle(book.title)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='h-1/6'></div>
|
||||
<div className='flex h-1/3 items-center justify-center'>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-neutral-content/50 line-clamp-1',
|
||||
isPreview ? 'text-[0.4em]' : mode === 'grid' ? 'text-base' : 'text-xs',
|
||||
)}
|
||||
>
|
||||
{formatAuthors(book.author)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.book.coverImageUrl === nextProps.book.coverImageUrl &&
|
||||
prevProps.book.metadata?.coverImageUrl === nextProps.book.metadata?.coverImageUrl &&
|
||||
prevProps.mode === nextProps.mode &&
|
||||
prevProps.coverFit === nextProps.coverFit &&
|
||||
prevProps.isPreview === nextProps.isPreview &&
|
||||
prevProps.className === nextProps.className &&
|
||||
prevProps.imageClassName === nextProps.imageClassName
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BookCover.displayName = 'BookCover';
|
||||
|
||||
export default BookCover;
|
||||
|
||||
@@ -72,6 +72,7 @@ export const useBookDataStore = create<BookDataState>((set, get) => ({
|
||||
const book = library.splice(bookIndex, 1)[0]!;
|
||||
book.progress = config.progress;
|
||||
book.updatedAt = Date.now();
|
||||
book.downloadedAt = book.downloadedAt || Date.now();
|
||||
library.unshift(book);
|
||||
setLibrary(library);
|
||||
config.updatedAt = Date.now();
|
||||
|
||||
@@ -7,6 +7,10 @@ interface LibraryState {
|
||||
checkOpenWithBooks: boolean;
|
||||
checkLastOpenBooks: boolean;
|
||||
currentBookshelf: (Book | BooksGroup)[];
|
||||
selectedBooks: Set<string>; // hashes for books, ids for groups
|
||||
setSelectedBooks: (ids: string[]) => void;
|
||||
getSelectedBooks: () => string[];
|
||||
toggleSelectedBook: (id: string) => void;
|
||||
getVisibleLibrary: () => Book[];
|
||||
setCheckOpenWithBooks: (check: boolean) => void;
|
||||
setCheckLastOpenBooks: (check: boolean) => void;
|
||||
@@ -18,6 +22,7 @@ interface LibraryState {
|
||||
export const useLibraryStore = create<LibraryState>((set, get) => ({
|
||||
library: [],
|
||||
currentBookshelf: [],
|
||||
selectedBooks: new Set(),
|
||||
checkOpenWithBooks: isTauriAppPlatform(),
|
||||
checkLastOpenBooks: isTauriAppPlatform(),
|
||||
getVisibleLibrary: () => get().library.filter((book) => !book.deletedAt),
|
||||
@@ -35,6 +40,23 @@ export const useLibraryStore = create<LibraryState>((set, get) => ({
|
||||
library[bookIndex] = book;
|
||||
}
|
||||
set({ library: [...library] });
|
||||
appService.saveLibraryBooks(library);
|
||||
await appService.saveLibraryBooks(library);
|
||||
},
|
||||
setSelectedBooks: (ids: string[]) => {
|
||||
set({ selectedBooks: new Set(ids) });
|
||||
},
|
||||
getSelectedBooks: () => {
|
||||
return Array.from(get().selectedBooks);
|
||||
},
|
||||
toggleSelectedBook: (id: string) => {
|
||||
set((state) => {
|
||||
const newSelection = new Set(state.selectedBooks);
|
||||
if (newSelection.has(id)) {
|
||||
newSelection.delete(id);
|
||||
} else {
|
||||
newSelection.add(id);
|
||||
}
|
||||
return { selectedBooks: newSelection };
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user