diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index 3a99cdd3..715969af 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -19,7 +19,7 @@ import { isMd5 } from '@/utils/md5'; import Alert from '@/components/Alert'; import Spinner from '@/components/Spinner'; import ModalPortal from '@/components/ModalPortal'; -import BookshelfItem, { generateGridItems, generateListItems } from './BookshelfItem'; +import BookshelfItem, { generateBookshelfItems } from './BookshelfItem'; import GroupingModal from './GroupingModal'; interface BookshelfProps { @@ -72,8 +72,7 @@ const Bookshelf: React.FC = ({ const { setCurrentBookshelf, setLibrary } = useLibraryStore(); const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore(); - const allBookshelfItems = - viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks); + const allBookshelfItems = generateBookshelfItems(libraryBooks); useEffect(() => { if (isImportingBook.current) return; diff --git a/apps/readest-app/src/app/library/components/BookshelfItem.tsx b/apps/readest-app/src/app/library/components/BookshelfItem.tsx index 6d391063..e89fac69 100644 --- a/apps/readest-app/src/app/library/components/BookshelfItem.tsx +++ b/apps/readest-app/src/app/library/components/BookshelfItem.tsx @@ -20,7 +20,7 @@ import GroupItem from './GroupItem'; export type BookshelfItem = Book | BooksGroup; -export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => { +export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => { const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => { if (book.deletedAt) return acc; book.groupId = book.groupId || BOOK_UNGROUPED_ID; @@ -49,10 +49,6 @@ export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => { return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt); }; -export const generateListItems = (books: Book[]): (Book | BooksGroup)[] => { - return books.filter((book) => !book.deletedAt).sort((a, b) => b.updatedAt - a.updatedAt); -}; - export const generateGroupsList = (items: Book[]): BookGroupType[] => { return items .sort((a, b) => b.updatedAt - a.updatedAt) @@ -326,7 +322,7 @@ const BookshelfItem: React.FC = ({ mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4', mode === 'list' && 'border-base-300 flex flex-col border-b py-2', appService?.isMobileApp && 'no-context-menu', - pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100', + pressing && mode === 'grid' ? 'scale-95' : 'scale-100', )} style={{ transition: 'transform 0.2s', @@ -347,7 +343,12 @@ const BookshelfItem: React.FC = ({ showBookDetailsModal={showBookDetailsModal} /> ) : ( - + )} diff --git a/apps/readest-app/src/app/library/components/GroupItem.tsx b/apps/readest-app/src/app/library/components/GroupItem.tsx index 7cb2065f..5ce36958 100644 --- a/apps/readest-app/src/app/library/components/GroupItem.tsx +++ b/apps/readest-app/src/app/library/components/GroupItem.tsx @@ -1,19 +1,99 @@ import clsx from 'clsx'; -import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md'; +import { useEffect, useRef, useState } from 'react'; +import { MdCheckCircle, MdCheckCircleOutline, MdChevronRight, MdChevronLeft } from 'react-icons/md'; import { useEnv } from '@/context/EnvContext'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { BooksGroup } from '@/types/book'; +import { LibraryViewModeType } from '@/types/settings'; import BookCover from '@/components/BookCover'; interface GroupItemProps { + mode: LibraryViewModeType; group: BooksGroup; isSelectMode: boolean; groupSelected: boolean; } -const GroupItem: React.FC = ({ group, isSelectMode, groupSelected }) => { +const GroupItem: React.FC = ({ mode, group, isSelectMode, groupSelected }) => { const { appService } = useEnv(); const iconSize15 = useResponsiveSize(15); + const scrollContainerRef = useRef(null); + const [showLeftArrow, setShowLeftArrow] = useState(false); + const [showRightArrow, setShowRightArrow] = useState(false); + + const checkScrollArrows = () => { + if (mode === 'list' && scrollContainerRef.current) { + const container = scrollContainerRef.current; + const hasOverflow = container.scrollWidth > container.clientWidth; + + if (hasOverflow) { + const isAtStart = container.scrollLeft <= 5; + const isAtEnd = container.scrollLeft >= container.scrollWidth - container.clientWidth - 5; + setShowLeftArrow(!isAtStart); + setShowRightArrow(!isAtEnd); + } else { + setShowLeftArrow(false); + setShowRightArrow(false); + } + } else { + setShowLeftArrow(false); + setShowRightArrow(false); + } + }; + + useEffect(() => { + checkScrollArrows(); + if (mode === 'list' && scrollContainerRef.current) { + const container = scrollContainerRef.current; + setTimeout(() => { + container.style.transform = 'translateZ(0)'; + container.scrollLeft = 0; + }, 0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode, group.books.length, scrollContainerRef.current]); + + const handleScroll = () => { + checkScrollArrows(); + }; + + const scrollLeft = () => { + if (scrollContainerRef.current) { + const container = scrollContainerRef.current; + const scrollAmount = container.clientWidth * 0.5; + const currentScroll = container.scrollLeft; + const targetScroll = Math.max(0, currentScroll - scrollAmount); + container.scrollTo({ left: targetScroll, behavior: 'smooth' }); + } + }; + + const scrollRight = () => { + if (scrollContainerRef.current) { + const container = scrollContainerRef.current; + const scrollAmount = container.clientWidth * 0.5; + const currentScroll = container.scrollLeft; + const maxScroll = container.scrollWidth - container.clientWidth; + const targetScroll = Math.min(maxScroll, currentScroll + scrollAmount); + container.scrollTo({ left: targetScroll, behavior: 'smooth' }); + } + }; + + const stopEvent = (e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleLeftArrowClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + scrollLeft(); + }; + + const handleRightArrowClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + scrollRight(); + }; return (
= ({ group, isSelectMode, groupSelecte appService?.hasContextMenu ? 'cursor-pointer' : '', )} > -
-
- {group.books.slice(0, 4).map((book) => ( -
- +
+
+
+ {group.books.slice(0, mode === 'grid' ? 4 : undefined).map((book) => ( +
+ +
+ ))} +
+ {mode === 'list' && showLeftArrow && ( +
+
+
- ))} + )} + {mode === 'list' && showRightArrow && ( +
+
+ +
+ )}
+ {mode === 'list' && ( +
+ {group.name} +
+ )} {groupSelected && (
)} @@ -43,14 +208,16 @@ const GroupItem: React.FC = ({ group, isSelectMode, groupSelecte
)}
-
-
-

- {group.name} -

+ {mode === 'grid' && ( +
+
+

+ {group.name} +

+
+
-
-
+ )}
); };