feat: add list view for the bookshelf, closes #542 (#955)

This commit is contained in:
Huang Xin
2025-04-25 01:53:09 +08:00
committed by GitHub
parent efd415ece2
commit ea8ab91ed3
33 changed files with 226 additions and 92 deletions
@@ -294,5 +294,7 @@
"Format:": "التنسيق:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "الوصف:",
"No description available": "لا يوجد وصف متاح"
"No description available": "لا يوجد وصف متاح",
"List": "قائمة",
"Grid": "شبكة"
}
@@ -294,5 +294,7 @@
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Beschreibung:",
"No description available": "Keine Beschreibung verfügbar"
"No description available": "Keine Beschreibung verfügbar",
"List": "Liste",
"Grid": "Raster"
}
@@ -294,5 +294,7 @@
"Format:": "Μορφή:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Περιγραφή:",
"No description available": "Δεν είναι διαθέσιμη καμία περιγραφή"
"No description available": "Δεν είναι διαθέσιμη καμία περιγραφή",
"List": "Λίστα",
"Grid": "Πλέγμα"
}
@@ -294,5 +294,7 @@
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descripción:",
"No description available": "No hay descripción disponible"
"No description available": "No hay descripción disponible",
"List": "Lista",
"Grid": "Cuadrícula"
}
@@ -294,5 +294,7 @@
"Format:": "Format :",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Description :",
"No description available": "Aucune description disponible"
"No description available": "Aucune description disponible",
"List": "Liste",
"Grid": "Grille"
}
@@ -294,5 +294,7 @@
"Format:": "फॉर्मेट:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "विवरण:",
"No description available": "कोई विवरण उपलब्ध नहीं है"
"No description available": "कोई विवरण उपलब्ध नहीं है",
"List": "सूची",
"Grid": "ग्रिड"
}
@@ -294,5 +294,7 @@
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Deskripsi:",
"No description available": "Tidak ada deskripsi yang tersedia"
"No description available": "Tidak ada deskripsi yang tersedia",
"List": "Daftar",
"Grid": "Jala"
}
@@ -294,5 +294,7 @@
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descrizione:",
"No description available": "Nessuna descrizione disponibile"
"No description available": "Nessuna descrizione disponibile",
"List": "Elenco",
"Grid": "Griglia"
}
@@ -294,5 +294,7 @@
"Format:": "形式:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "説明:",
"No description available": "説明はありません"
"No description available": "説明はありません",
"List": "リスト",
"Grid": "グリッド"
}
@@ -294,5 +294,7 @@
"Format:": "형식:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "설명:",
"No description available": "설명 없음"
"No description available": "설명 없음",
"List": "목록",
"Grid": "격자"
}
@@ -294,5 +294,7 @@
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Opis:",
"No description available": "Brak opisu"
"No description available": "Brak opisu",
"List": "Lista",
"Grid": "Siatka"
}
@@ -294,5 +294,7 @@
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descrição:",
"No description available": "Nenhuma descrição disponível"
"No description available": "Nenhuma descrição disponível",
"List": "Lista",
"Grid": "Grade"
}
@@ -294,5 +294,7 @@
"Format:": "Формат:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Описание:",
"No description available": "Нет доступного описания"
"No description available": "Нет доступного описания",
"List": "Список",
"Grid": "Сетка"
}
@@ -294,5 +294,7 @@
"Format:": "Biçim:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Tanım:",
"No description available": "Tanım mevcut değil"
"No description available": "Tanım mevcut değil",
"List": "Liste",
"Grid": "Izgara"
}
@@ -294,5 +294,7 @@
"Format:": "Формат:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Опис:",
"No description available": "Опис недоступний"
"No description available": "Опис недоступний",
"List": "Список",
"Grid": "Сітка"
}
@@ -294,5 +294,7 @@
"Format:": "Định dạng:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Miêu tả:",
"No description available": "Không có mô tả nào"
"No description available": "Không có mô tả nào",
"List": "Danh sách",
"Grid": "Lưới"
}
@@ -294,5 +294,7 @@
"Format:": "格式",
"GuanKiapTsingKhai-T": "原俠正楷-T",
"Description:": "简介",
"No description available": "暂无简介"
"No description available": "暂无简介",
"List": "列表",
"Grid": "网格"
}
@@ -294,5 +294,7 @@
"Format:": "格式",
"GuanKiapTsingKhai-T": "原俠正楷-T",
"Description:": "簡介",
"No description available": "暫無簡介"
"No description available": "暫無簡介",
"List": "列表",
"Grid": "網格"
}
@@ -1,14 +1,16 @@
import clsx from 'clsx';
import Image from 'next/image';
import { Book } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { formatAuthors, formatTitle } from '@/utils/book';
interface BookCoverProps {
book: Book;
isPreview?: boolean;
mode?: LibraryViewModeType;
}
const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview }) => {
return (
<div className='relative flex h-full w-full'>
<Image
@@ -31,8 +33,8 @@ const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
<div className='flex h-1/2 items-center justify-center'>
<span
className={clsx(
isPreview ? 'line-clamp-2' : 'line-clamp-3',
isPreview ? 'text-[0.5em]' : 'text-lg',
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)}
@@ -43,7 +45,7 @@ const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
<span
className={clsx(
'text-neutral-content/50 line-clamp-1',
isPreview ? 'text-[0.4em]' : 'text-base',
isPreview ? 'text-[0.4em]' : mode === 'grid' ? 'text-base' : 'text-xs',
)}
>
{formatAuthors(book.author)}
@@ -6,10 +6,13 @@ import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/li
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { LibraryViewModeType } from '@/types/settings';
import { formatAuthors } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
import BookCover from './BookCover';
interface BookItemProps {
mode: LibraryViewModeType;
book: Book;
isSelectMode: boolean;
selectedBooks: string[];
@@ -20,6 +23,7 @@ interface BookItemProps {
}
const BookItem: React.FC<BookItemProps> = ({
mode,
book,
isSelectMode,
selectedBooks,
@@ -39,12 +43,19 @@ const BookItem: React.FC<BookItemProps> = ({
return (
<div
className={clsx(
'book-item flex h-full flex-col',
'book-item flex',
mode === 'grid' && 'h-full flex-col',
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md'>
<BookCover book={book} />
<div
className={clsx(
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
mode === 'list' && 'min-w-20',
)}
>
<BookCover mode={mode} book={book} />
{selectedBooks.includes(book.hash) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
@@ -58,11 +69,28 @@ const BookItem: React.FC<BookItemProps> = ({
</div>
)}
</div>
<div className={clsx('flex w-full flex-col p-0 pt-2')}>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
<div
className={clsx(
'flex w-full flex-col p-0',
mode === 'grid' && 'pt-2',
mode === 'list' && 'py-2',
)}
>
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-2')}>
<h4
className={clsx(
'overflow-hidden text-ellipsis font-semibold',
mode === 'grid' && 'block whitespace-nowrap text-[0.6em] text-xs',
mode === 'list' && 'line-clamp-2 text-base',
)}
>
{book.title}
</h4>
{mode === 'list' && (
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatAuthors(book.author, book.primaryLanguage) || ''}
</p>
)}
</div>
<div
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
@@ -6,6 +6,7 @@ import { MdDelete, MdOpenInNew, MdOutlineCancel } from 'react-icons/md';
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
@@ -16,7 +17,7 @@ import { isMd5 } from '@/utils/md5';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
import BookshelfItem, { generateGridItems, generateListItems } from './BookshelfItem';
import GroupingModal from './GroupingModal';
interface BookshelfProps {
@@ -55,12 +56,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [queryTerm, setQueryTerm] = useState<string | null>(null);
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || 'updated');
const [sortOrder, setSortOrder] = useState(searchParams?.get('order') || 'desc');
const [viewMode, setViewMode] = useState(searchParams?.get('view') || settings.libraryViewMode);
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || settings.librarySortBy);
const [sortOrder, setSortOrder] = useState(
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
);
const isImportingBook = useRef(false);
const { setLibrary } = useLibraryStore();
const allBookshelfItems = generateBookshelfItems(libraryBooks);
const allBookshelfItems =
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
useEffect(() => {
if (isSelectMode) {
@@ -93,6 +98,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
useEffect(() => {
const group = searchParams?.get('group') || '';
const query = searchParams?.get('q') || '';
const view = searchParams?.get('view') || settings.libraryViewMode;
const sort = searchParams?.get('sort') || settings.librarySortBy;
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
const params = new URLSearchParams(searchParams?.toString());
@@ -115,9 +121,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
} else {
params.delete('order');
}
if (sort === 'updated' && order === 'desc') {
if (view) {
params.set('view', view);
setViewMode(view);
} else {
params.delete('view');
}
if (sort === 'updated' && order === 'desc' && view === 'grid') {
params.delete('sort');
params.delete('order');
params.delete('view');
}
if (group) {
const booksGroup = allBookshelfItems.find(
@@ -229,13 +242,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<div className='bookshelf'>
<div
className={clsx(
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
'transform-wrapper',
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
viewMode === 'list' && 'flex flex-col',
)}
>
{filteredBookshelfItems.map((item, index) => (
<BookshelfItem
key={`library-item-${index}`}
mode={viewMode as LibraryViewModeType}
item={item}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
@@ -251,7 +267,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
}
/>
))}
{!navBooksGroup && allBookshelfItems.length > 0 && (
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
<div
className={clsx(
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
@@ -10,6 +10,7 @@ import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { getOSPlatform } from '@/utils/misc';
import { getLocalBookFilename } from '@/utils/book';
import { LibraryViewModeType } from '@/types/settings';
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { Book, BookGroupType, BooksGroup } from '@/types/book';
@@ -18,7 +19,7 @@ import GroupItem from './GroupItem';
export type BookshelfItem = Book | BooksGroup;
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
export const generateGridItems = (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;
@@ -47,6 +48,10 @@ export const generateBookshelfItems = (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)
@@ -66,6 +71,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
};
interface BookshelfItemProps {
mode: LibraryViewModeType;
item: BookshelfItem;
isSelectMode: boolean;
selectedBooks: string[];
@@ -80,6 +86,7 @@ interface BookshelfItemProps {
}
const BookshelfItem: React.FC<BookshelfItemProps> = ({
mode,
item,
isSelectMode,
selectedBooks,
@@ -248,30 +255,35 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
});
return (
<div
className={clsx(
'sm:hover:bg-base-300/50 group flex h-full flex-col px-0 py-4 sm:px-4',
pressing ? 'scale-95' : 'scale-100',
)}
style={{
transition: 'transform 0.2s',
}}
{...handlers}
>
<div className='flex-grow'>
{'format' in item ? (
<BookItem
book={item}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
transferProgress={transferProgress}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
showBookDetailsModal={showBookDetailsModal}
/>
) : (
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
<div
className={clsx(
'group',
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',
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
)}
style={{
transition: 'transform 0.2s',
}}
{...handlers}
>
<div className='flex-grow'>
{'format' in item ? (
<BookItem
mode={mode}
book={item}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
transferProgress={transferProgress}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
showBookDetailsModal={showBookDetailsModal}
/>
) : (
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
)}
</div>
</div>
</div>
);
@@ -132,7 +132,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 w-72 shadow-2xl'
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
>
{user ? (
<MenuItem
@@ -142,7 +142,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
: _('Logged in')
}
labelClass='!max-w-40'
icon={
Icon={
avatarUrl ? (
<Image
src={avatarUrl}
@@ -153,34 +153,34 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
height={20}
/>
) : (
<PiUserCircleCheck />
PiUserCircleCheck
)
}
>
<ul>
<Quota quotas={quotas} className='h-10 pl-4 pr-2' />
<Quota quotas={quotas} className='h-10 pl-3 pr-2' />
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
</ul>
</MenuItem>
) : (
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
<MenuItem label={_('Sign In')} Icon={PiUserCircle} onClick={handleUserLogin}></MenuItem>
)}
<MenuItem
label={_('Auto Upload Books to Cloud')}
icon={isAutoUpload ? <MdCheck className='text-base-content' /> : undefined}
Icon={isAutoUpload ? MdCheck : undefined}
onClick={toggleAutoUploadBooks}
/>
{isTauriAppPlatform() && !appService?.isMobile && (
<MenuItem
label={_('Auto Import on File Open')}
icon={isAutoImportBooksOnOpen ? <MdCheck className='text-base-content' /> : undefined}
Icon={isAutoImportBooksOnOpen ? MdCheck : undefined}
onClick={toggleAutoImportBooksOnOpen}
/>
)}
{appService?.hasUpdater && (
<MenuItem
label={_('Check Updates on Start')}
icon={isAutoCheckUpdates ? <MdCheck className='text-base-content' /> : undefined}
Icon={isAutoCheckUpdates ? MdCheck : undefined}
onClick={toggleAutoCheckUpdates}
/>
)}
@@ -189,13 +189,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
{appService?.hasWindow && (
<MenuItem
label={_('Always on Top')}
icon={isAlwaysOnTop ? <MdCheck className='text-base-content' /> : undefined}
Icon={isAlwaysOnTop ? MdCheck : undefined}
onClick={toggleAlwaysOnTop}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
Icon={isScreenWakeLock ? MdCheck : undefined}
onClick={toggleScreenWakeLock}
/>
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
@@ -4,7 +4,7 @@ import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibrarySortByType } from '@/types/settings';
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
@@ -19,9 +19,15 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const { envConfig } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const viewMode = settings.libraryViewMode;
const sortBy = settings.librarySortBy;
const isAscending = settings.librarySortAscending;
const viewOptions = [
{ label: _('List'), value: 'list' },
{ label: _('Grid'), value: 'grid' },
];
const sortByOptions = [
{ label: _('Title'), value: 'title' },
{ label: _('Author'), value: 'author' },
@@ -35,6 +41,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
{ label: _('Descending'), value: false },
];
const handleSetViewMode = (value: LibraryViewModeType) => {
settings.libraryViewMode = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('view', value);
navigateToLibrary(router, `${params.toString()}`);
};
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
@@ -62,6 +79,16 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
>
{viewOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={viewMode === option.value ? MdCheck : undefined}
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
@@ -73,7 +100,7 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
key={option.value}
label={option.label}
buttonClass='h-8'
icon={sortBy === option.value ? <MdCheck className='text-base-content' /> : undefined}
Icon={sortBy === option.value ? MdCheck : undefined}
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
/>
))}
@@ -83,9 +110,7 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
key={option.value.toString()}
label={option.label}
buttonClass='h-8'
icon={
isAscending === option.value ? <MdCheck className='text-base-content' /> : undefined
}
Icon={isAscending === option.value ? MdCheck : undefined}
onClick={() => handleSetSortAscending(option.value)}
/>
))}
+1 -1
View File
@@ -537,7 +537,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<div
ref={containerRef}
className={clsx(
'scroll-container drop-zone flex-grow overflow-y-auto px-4 sm:px-2',
'scroll-container drop-zone flex-grow overflow-y-auto',
appService?.hasSafeAreaInset && 'pt-[52px]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
@@ -117,7 +117,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
<MenuItem
label={_('Scrolled Mode')}
shortcut='Shift+J'
icon={isScrolledMode ? <MdCheck /> : undefined}
Icon={isScrolledMode ? MdCheck : undefined}
onClick={toggleScrolledMode}
/>
@@ -132,7 +132,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
? _('Light Mode')
: _('Auto Mode')
}
icon={themeMode === 'dark' ? <BiMoon /> : themeMode === 'light' ? <BiSun /> : <TbSunMoon />}
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
onClick={cycleThemeMode}
/>
</div>
@@ -62,13 +62,13 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
.map((book) => (
<MenuItem
key={book.hash}
icon={
Icon={
<Image
src={book.coverImageUrl!}
alt={book.title}
width={56}
height={80}
className='aspect-auto max-h-8 max-w-6 rounded-sm shadow-md'
className='aspect-auto max-h-8 max-w-4 rounded-sm shadow-md'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
+5 -4
View File
@@ -17,9 +17,10 @@ const Alert: React.FC<{
'alert flex items-center justify-between',
'bg-base-300 rounded-lg border-none p-4 shadow-2xl',
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
'min-w-[70vw] flex-col sm:min-w-[40vw] sm:flex-row',
)}
>
<div className='flex items-center space-x-2'>
<div className='flex items-center space-x-2 self-start'>
<svg
xmlns='http://www.w3.org/2000/svg'
fill='none'
@@ -34,12 +35,12 @@ const Alert: React.FC<{
></path>
</svg>
<div className=''>
<h3 className='font-sm text-base'>{title}</h3>
<h3 className='font-sm text-start text-base sm:text-center'>{title}</h3>
<div className='text-xs'>{message}</div>
</div>
</div>
<div className='flex max-w-[20vw] flex-wrap items-center justify-center gap-2'>
<button className='btn btn-sm' onClick={onCancel}>
<div className='flex flex-wrap items-center gap-2 self-end sm:max-w-[20vw]'>
<button className='btn btn-sm btn-neutral' onClick={onCancel}>
{_('Cancel')}
</button>
<button className='btn btn-sm btn-warning' onClick={onConfirm}>
@@ -142,7 +142,7 @@ const BookDetailModal = ({
{formatTitle(book.title) || _('Untitled')}
</p>
<p className='text-neutral-content line-clamp-1'>
{formatAuthors(book.author, bookMeta.language) || _('Unknown')}
{formatAuthors(book.author, book.primaryLanguage) || _('Unknown')}
</p>
</div>
<div className='flex flex-wrap items-center gap-x-4'>
+15 -6
View File
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import React from 'react';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { IconType } from 'react-icons';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface MenuItemProps {
label: string;
@@ -9,7 +10,7 @@ interface MenuItemProps {
shortcut?: string;
disabled?: boolean;
noIcon?: boolean;
icon?: React.ReactNode;
Icon?: React.ReactNode | IconType;
children?: React.ReactNode;
onClick?: () => void;
}
@@ -21,15 +22,15 @@ const MenuItem: React.FC<MenuItemProps> = ({
shortcut,
disabled,
noIcon = false,
icon,
Icon,
children,
onClick,
}) => {
const iconSize = useDefaultIconSize();
const iconSize = useResponsiveSize(16);
const menuButton = (
<button
className={clsx(
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-1',
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
@@ -37,7 +38,15 @@ const MenuItem: React.FC<MenuItemProps> = ({
disabled={disabled}
>
<div className='flex min-w-0 items-center'>
{!noIcon && <span style={{ minWidth: `${iconSize}px` }}>{icon}</span>}
{!noIcon && (
<span style={{ minWidth: `${iconSize}px` }}>
{typeof Icon === 'function' ? (
<Icon className='text-base-content' size={iconSize} />
) : (
Icon
)}
</span>
)}
<span
className={clsx('mx-2 flex-1 truncate text-base sm:text-sm', labelClass)}
style={{ minWidth: 0 }}
+3 -1
View File
@@ -40,7 +40,9 @@ const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className }) => {
></div>
)}
<div className={clsx('relative flex items-center justify-between p-2', className)}>
<div
className={clsx('relative flex items-center justify-between gap-4 p-2', className)}
>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={quota.tooltip}>
<span className='truncate'>{quota.name}</span>
</div>
@@ -38,6 +38,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
autoCheckUpdates: true,
screenWakeLock: true,
autoImportBooksOnOpen: false,
libraryViewMode: 'grid',
librarySortBy: 'updated',
librarySortAscending: false,
+2
View File
@@ -2,6 +2,7 @@ import { CustomTheme } from '@/styles/themes';
import { HighlightColor, HighlightStyle, ViewSettings } from './book';
export type ThemeType = 'light' | 'dark' | 'auto';
export type LibraryViewModeType = 'grid' | 'list';
export type LibrarySortByType = 'title' | 'author' | 'updated' | 'created' | 'size' | 'format';
export interface ReadSettings {
@@ -27,6 +28,7 @@ export interface SystemSettings {
autoCheckUpdates: boolean;
screenWakeLock: boolean;
autoImportBooksOnOpen: boolean;
libraryViewMode: LibraryViewModeType;
librarySortBy: LibrarySortByType;
librarySortAscending: boolean;