diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 429fa01f..817b22e9 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -294,5 +294,7 @@ "Format:": "التنسيق:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "الوصف:", - "No description available": "لا يوجد وصف متاح" + "No description available": "لا يوجد وصف متاح", + "List": "قائمة", + "Grid": "شبكة" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 30e61b24..1d28d4ca 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index e16eb999..ceb94e12 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -294,5 +294,7 @@ "Format:": "Μορφή:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "Περιγραφή:", - "No description available": "Δεν είναι διαθέσιμη καμία περιγραφή" + "No description available": "Δεν είναι διαθέσιμη καμία περιγραφή", + "List": "Λίστα", + "Grid": "Πλέγμα" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index ab008253..9eedb5ab 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 697a6752..6f4e82fc 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 9a786c38..99c45f64 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -294,5 +294,7 @@ "Format:": "फॉर्मेट:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "विवरण:", - "No description available": "कोई विवरण उपलब्ध नहीं है" + "No description available": "कोई विवरण उपलब्ध नहीं है", + "List": "सूची", + "Grid": "ग्रिड" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index acceb7a2..22a95c3f 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index dd6668f3..cb087028 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index c208fe98..5d552997 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -294,5 +294,7 @@ "Format:": "形式:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "説明:", - "No description available": "説明はありません" + "No description available": "説明はありません", + "List": "リスト", + "Grid": "グリッド" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 77c85d62..ce50b17b 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -294,5 +294,7 @@ "Format:": "형식:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "설명:", - "No description available": "설명 없음" + "No description available": "설명 없음", + "List": "목록", + "Grid": "격자" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 80e7d85c..08709c17 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 03eb3c1f..29f3ee7a 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 6faec388..10ed33bf 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -294,5 +294,7 @@ "Format:": "Формат:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "Описание:", - "No description available": "Нет доступного описания" + "No description available": "Нет доступного описания", + "List": "Список", + "Grid": "Сетка" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index eb3303f9..b23dcd19 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 82f60926..dbb88b8f 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -294,5 +294,7 @@ "Format:": "Формат:", "GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T", "Description:": "Опис:", - "No description available": "Опис недоступний" + "No description available": "Опис недоступний", + "List": "Список", + "Grid": "Сітка" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 6aeefab6..2e91fd17 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 932dcfb0..ab3bf3f3 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -294,5 +294,7 @@ "Format:": "格式", "GuanKiapTsingKhai-T": "原俠正楷-T", "Description:": "简介", - "No description available": "暂无简介" + "No description available": "暂无简介", + "List": "列表", + "Grid": "网格" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 2c26f5cb..e0e56e6e 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -294,5 +294,7 @@ "Format:": "格式", "GuanKiapTsingKhai-T": "原俠正楷-T", "Description:": "簡介", - "No description available": "暫無簡介" + "No description available": "暫無簡介", + "List": "列表", + "Grid": "網格" } diff --git a/apps/readest-app/src/app/library/components/BookCover.tsx b/apps/readest-app/src/app/library/components/BookCover.tsx index 5e7e7196..d4eb46d5 100644 --- a/apps/readest-app/src/app/library/components/BookCover.tsx +++ b/apps/readest-app/src/app/library/components/BookCover.tsx @@ -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 = ({ book, isPreview }) => { +const BookCover: React.FC = ({ mode = 'grid', book, isPreview }) => { return (
= ({ book, isPreview }) => {
{formatTitle(book.title)} @@ -43,7 +45,7 @@ const BookCover: React.FC = ({ book, isPreview }) => { {formatAuthors(book.author)} diff --git a/apps/readest-app/src/app/library/components/BookItem.tsx b/apps/readest-app/src/app/library/components/BookItem.tsx index c8eb3185..047b33e6 100644 --- a/apps/readest-app/src/app/library/components/BookItem.tsx +++ b/apps/readest-app/src/app/library/components/BookItem.tsx @@ -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 = ({ + mode, book, isSelectMode, selectedBooks, @@ -39,12 +43,19 @@ const BookItem: React.FC = ({ return (
-
- +
+ {selectedBooks.includes(book.hash) && (
)} @@ -58,11 +69,28 @@ const BookItem: React.FC = ({
)}
-
-
-

+
+
+

{book.title}

+ {mode === 'list' && ( +

+ {formatAuthors(book.author, book.primaryLanguage) || ''} +

+ )}
= ({ const [queryTerm, setQueryTerm] = useState(null); const [navBooksGroup, setNavBooksGroup] = useState(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 = ({ 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 = ({ } 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 = ({
{filteredBookshelfItems.map((item, index) => ( = ({ } /> ))} - {!navBooksGroup && allBookshelfItems.length > 0 && ( + {viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
{ +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 = ({ + mode, item, isSelectMode, selectedBooks, @@ -248,30 +255,35 @@ const BookshelfItem: React.FC = ({ }); return ( -
-
- {'format' in item ? ( - - ) : ( - +
+
+
+ {'format' in item ? ( + + ) : ( + + )} +
); diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx index dc4a7428..e197b6d7 100644 --- a/apps/readest-app/src/app/library/components/SettingsMenu.tsx +++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx @@ -132,7 +132,7 @@ const SettingsMenu: React.FC = ({ setIsDropdownOpen }) => { return (
{user ? ( = ({ setIsDropdownOpen }) => { : _('Logged in') } labelClass='!max-w-40' - icon={ + Icon={ avatarUrl ? ( = ({ setIsDropdownOpen }) => { height={20} /> ) : ( - + PiUserCircleCheck ) } >
    - +
) : ( - } onClick={handleUserLogin}> + )} : undefined} + Icon={isAutoUpload ? MdCheck : undefined} onClick={toggleAutoUploadBooks} /> {isTauriAppPlatform() && !appService?.isMobile && ( : undefined} + Icon={isAutoImportBooksOnOpen ? MdCheck : undefined} onClick={toggleAutoImportBooksOnOpen} /> )} {appService?.hasUpdater && ( : undefined} + Icon={isAutoCheckUpdates ? MdCheck : undefined} onClick={toggleAutoCheckUpdates} /> )} @@ -189,13 +189,13 @@ const SettingsMenu: React.FC = ({ setIsDropdownOpen }) => { {appService?.hasWindow && ( : undefined} + Icon={isAlwaysOnTop ? MdCheck : undefined} onClick={toggleAlwaysOnTop} /> )} : undefined} + Icon={isScreenWakeLock ? MdCheck : undefined} onClick={toggleScreenWakeLock} /> diff --git a/apps/readest-app/src/app/library/components/SortMenu.tsx b/apps/readest-app/src/app/library/components/SortMenu.tsx index 671bce58..8eb44c0e 100644 --- a/apps/readest-app/src/app/library/components/SortMenu.tsx +++ b/apps/readest-app/src/app/library/components/SortMenu.tsx @@ -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 = ({ 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 = ({ 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 = ({ setIsDropdownOpen }) => { tabIndex={0} className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl' > + {viewOptions.map((option) => ( + handleSetViewMode(option.value as LibraryViewModeType)} + /> + ))} +
= ({ setIsDropdownOpen }) => { key={option.value} label={option.label} buttonClass='h-8' - icon={sortBy === option.value ? : undefined} + Icon={sortBy === option.value ? MdCheck : undefined} onClick={() => handleSetSortBy(option.value as LibrarySortByType)} /> ))} @@ -83,9 +110,7 @@ const SortMenu: React.FC = ({ setIsDropdownOpen }) => { key={option.value.toString()} label={option.label} buttonClass='h-8' - icon={ - isAscending === option.value ? : undefined - } + Icon={isAscending === option.value ? MdCheck : undefined} onClick={() => handleSetSortAscending(option.value)} /> ))} diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index ef22d700..47b41331 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -537,7 +537,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
= ({ : undefined} + Icon={isScrolledMode ? MdCheck : undefined} onClick={toggleScrolledMode} /> @@ -132,7 +132,7 @@ const ViewMenu: React.FC = ({ ? _('Light Mode') : _('Auto Mode') } - icon={themeMode === 'dark' ? : themeMode === 'light' ? : } + Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon} onClick={cycleThemeMode} />
diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx index 967d3cb7..ddccdbcd 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx @@ -62,13 +62,13 @@ const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) .map((book) => ( { (e.target as HTMLImageElement).style.display = 'none'; }} diff --git a/apps/readest-app/src/components/Alert.tsx b/apps/readest-app/src/components/Alert.tsx index eaf60986..9c89626c 100644 --- a/apps/readest-app/src/components/Alert.tsx +++ b/apps/readest-app/src/components/Alert.tsx @@ -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', )} > -
+
-

{title}

+

{title}

{message}
-
-
diff --git a/apps/readest-app/src/components/MenuItem.tsx b/apps/readest-app/src/components/MenuItem.tsx index 1f644868..0887ead4 100644 --- a/apps/readest-app/src/components/MenuItem.tsx +++ b/apps/readest-app/src/components/MenuItem.tsx @@ -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 = ({ shortcut, disabled, noIcon = false, - icon, + Icon, children, onClick, }) => { - const iconSize = useDefaultIconSize(); + const iconSize = useResponsiveSize(16); const menuButton = (