From 6fde15704748952a4b61a2d6aa080309aac62d66 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 16 Nov 2025 18:29:37 +0530 Subject: [PATCH] feat(bookshelf): support nested groups in the bookshelf, closes #568 (#2449) --- .../public/locales/ar/translation.json | 3 +- .../public/locales/bn/translation.json | 3 +- .../public/locales/bo/translation.json | 3 +- .../public/locales/de/translation.json | 3 +- .../public/locales/el/translation.json | 3 +- .../public/locales/es/translation.json | 3 +- .../public/locales/fa/translation.json | 3 +- .../public/locales/fr/translation.json | 3 +- .../public/locales/hi/translation.json | 3 +- .../public/locales/id/translation.json | 3 +- .../public/locales/it/translation.json | 3 +- .../public/locales/ja/translation.json | 3 +- .../public/locales/ko/translation.json | 3 +- .../public/locales/nl/translation.json | 3 +- .../public/locales/pl/translation.json | 3 +- .../public/locales/pt/translation.json | 3 +- .../public/locales/ru/translation.json | 3 +- .../public/locales/si/translation.json | 3 +- .../public/locales/sv/translation.json | 3 +- .../public/locales/ta/translation.json | 3 +- .../public/locales/th/translation.json | 3 +- .../public/locales/tr/translation.json | 3 +- .../public/locales/uk/translation.json | 3 +- .../public/locales/vi/translation.json | 3 +- .../public/locales/zh-CN/translation.json | 3 +- .../public/locales/zh-TW/translation.json | 3 +- .../src/app/library/components/Bookshelf.tsx | 40 ++- .../app/library/components/BookshelfItem.tsx | 43 +++- .../src/app/library/components/GroupItem.tsx | 4 +- .../app/library/components/GroupingModal.tsx | 240 +++++++++++++----- .../app/library/components/LibraryHeader.tsx | 17 +- apps/readest-app/src/app/library/page.tsx | 62 ++++- .../src/app/library/utils/libraryUtils.ts | 9 + apps/readest-app/src/components/Toast.tsx | 2 +- apps/readest-app/src/store/libraryStore.ts | 56 +++- apps/readest-app/src/types/book.ts | 1 + 36 files changed, 409 insertions(+), 143 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 3c9d105f..fb846687 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -671,5 +671,6 @@ "Updating email ...": "جارٍ تحديث البريد الإلكتروني ...", "Update email": "تحديث البريد الإلكتروني", "Current email": "البريد الإلكتروني الحالي", - "Update Email": "تحديث البريد الإلكتروني" + "Update Email": "تحديث البريد الإلكتروني", + "All": "الكل" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index bd9a930c..fa330795 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "ইমেল আপডেট হচ্ছে ...", "Update email": "ইমেল আপডেট করুন", "Current email": "বর্তমান ইমেল", - "Update Email": "ইমেল আপডেট করুন" + "Update Email": "ইমেল আপডেট করুন", + "All": "সব" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 8170ca34..df7ead35 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "ཡིག་འཕྲིན་གསར་བསྒྱུར་བཞིན་པ། ...", "Update email": "ཡིག་འཕྲིན་གསར་བསྒྱུར།", "Current email": "མིག་སྔའི་ཡིག་འཕྲིན།", - "Update Email": "ཡིག་འཕྲིན་གསར་བསྒྱུར།" + "Update Email": "ཡིག་འཕྲིན་གསར་བསྒྱུར།", + "All": "ཆེན་མོ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 592a32f2..706583c5 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "E-Mail wird aktualisiert ...", "Update email": "E-Mail aktualisieren", "Current email": "Aktuelle E-Mail", - "Update Email": "E-Mail aktualisieren" + "Update Email": "E-Mail aktualisieren", + "All": "Alle" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 79e97c51..58d787b7 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "Ενημέρωση email ...", "Update email": "Ενημέρωση email", "Current email": "Τρέχον email", - "Update Email": "Ενημέρωση email" + "Update Email": "Ενημέρωση email", + "All": "Όλα" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 2329d047..dff6b097 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -659,5 +659,6 @@ "Updating email ...": "Actualizando correo electrónico ...", "Update email": "Actualizar correo electrónico", "Current email": "Correo electrónico actual", - "Update Email": "Actualizar correo electrónico" + "Update Email": "Actualizar correo electrónico", + "All": "Todos" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index c803a5b5..63971cec 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "در حال به‌روزرسانی ایمیل ...", "Update email": "به‌روزرسانی ایمیل", "Current email": "ایمیل فعلی", - "Update Email": "به‌روزرسانی ایمیل" + "Update Email": "به‌روزرسانی ایمیل", + "All": "همه" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 994d64d5..f8cdee10 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -659,5 +659,6 @@ "Updating email ...": "Mise à jour de l’e-mail ...", "Update email": "Mettre à jour l’e-mail", "Current email": "E-mail actuel", - "Update Email": "Mettre à jour l’e-mail" + "Update Email": "Mettre à jour l’e-mail", + "All": "Tous" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 479dc426..f926c49d 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "ईमेल अपडेट किया जा रहा है ...", "Update email": "ईमेल अपडेट करें", "Current email": "वर्तमान ईमेल", - "Update Email": "ईमेल अपडेट करें" + "Update Email": "ईमेल अपडेट करें", + "All": "सभी" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 24c074ec..8d531eb2 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "Memperbarui email ...", "Update email": "Perbarui email", "Current email": "Email saat ini", - "Update Email": "Perbarui email" + "Update Email": "Perbarui email", + "All": "Semua" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index f6dbdf80..75f1d316 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -659,5 +659,6 @@ "Updating email ...": "Aggiornamento dell'email ...", "Update email": "Aggiorna email", "Current email": "Email attuale", - "Update Email": "Aggiorna email" + "Update Email": "Aggiorna email", + "All": "Tutti" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 91e1de70..13067776 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "メールを更新しています ...", "Update email": "メールを更新", "Current email": "現在のメールアドレス", - "Update Email": "メールを更新" + "Update Email": "メールを更新", + "All": "すべて" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 5e100d6b..faedf44f 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "이메일 업데이트 중...", "Update email": "이메일 업데이트", "Current email": "현재 이메일", - "Update Email": "이메일 업데이트" + "Update Email": "이메일 업데이트", + "All": "전체" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 62d4b86d..6d48cf6a 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "E-mail wordt bijgewerkt...", "Update email": "E-mail bijwerken", "Current email": "Huidig e-mailadres", - "Update Email": "E-mail bijwerken" + "Update Email": "E-mail bijwerken", + "All": "Alles" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index ab498929..2a6d3c4a 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -663,5 +663,6 @@ "Updating email ...": "Aktualizowanie e-maila...", "Update email": "Zaktualizuj e-mail", "Current email": "Obecny e-mail", - "Update Email": "Zaktualizuj e-mail" + "Update Email": "Zaktualizuj e-mail", + "All": "Wszystkie" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 15817fb7..bc5f0984 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -659,5 +659,6 @@ "Updating email ...": "A atualizar o e-mail...", "Update email": "Atualizar e-mail", "Current email": "E-mail atual", - "Update Email": "Atualizar e-mail" + "Update Email": "Atualizar e-mail", + "All": "Todos" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 6fa9434d..e92acc39 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -663,5 +663,6 @@ "Updating email ...": "Обновление электронной почты ...", "Update email": "Обновить электронную почту", "Current email": "Текущий адрес электронной почты", - "Update Email": "Обновить электронную почту" + "Update Email": "Обновить электронную почту", + "All": "Все" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index ee8f88fb..28b2d12b 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "විද්‍යුත් තැපැල් යාවත්කාලීන කරමින් ...", "Update email": "විද්‍යුත් තැපැල් යාවත්කාලීන කරන්න", "Current email": "වත්මන් විද්‍යුත් තැපැල්", - "Update Email": "විද්‍යුත් තැපැල් යාවත්කාලීන කරන්න" + "Update Email": "විද්‍යුත් තැපැල් යාවත්කාලීන කරන්න", + "All": "සියල්ල" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index df5f6b9f..187eab25 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "Uppdaterar e-post ...", "Update email": "Uppdatera e-post", "Current email": "Nuvarande e-post", - "Update Email": "Uppdatera e-post" + "Update Email": "Uppdatera e-post", + "All": "Alla" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index dd385923..7c84e644 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "மின்னஞ்சல் புதுப்பிக்கப்படுகிறது ...", "Update email": "மின்னஞ்சல் புதுப்பிக்கவும்", "Current email": "தற்போதைய மின்னஞ்சல்", - "Update Email": "மின்னஞ்சல் புதுப்பிக்கவும்" + "Update Email": "மின்னஞ்சல் புதுப்பிக்கவும்", + "All": "அனைத்து" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 74ce97ab..f866edb8 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "กำลังอัปเดตอีเมล ...", "Update email": "อัปเดตอีเมล", "Current email": "อีเมลปัจจุบัน", - "Update Email": "อัปเดตอีเมล" + "Update Email": "อัปเดตอีเมล", + "All": "ทั้งหมด" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 6571b83c..c5d3c1c2 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -655,5 +655,6 @@ "Updating email ...": "E-posta güncelleniyor ...", "Update email": "E-postayı güncelle", "Current email": "Mevcut e-posta", - "Update Email": "E-postayı güncelle" + "Update Email": "E-postayı güncelle", + "All": "Tümü" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index a06732f3..070a3683 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -663,5 +663,6 @@ "Updating email ...": "Оновлення електронної пошти ...", "Update email": "Оновити електронну пошту", "Current email": "Поточна електронна пошта", - "Update Email": "Оновити електронну пошту" + "Update Email": "Оновити електронну пошту", + "All": "Всі" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index c8480e6e..04009712 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "Đang cập nhật email ...", "Update email": "Cập nhật email", "Current email": "Email hiện tại", - "Update Email": "Cập nhật email" + "Update Email": "Cập nhật email", + "All": "Tất cả" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index a95ae432..0f477d3b 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "正在更新邮箱 ...", "Update email": "更新邮箱", "Current email": "当前邮箱", - "Update Email": "更新邮箱" + "Update Email": "更新邮箱", + "All": "全部" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 43f40097..b9989c43 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -651,5 +651,6 @@ "Updating email ...": "正在更新電子郵件 ...", "Update email": "更新電子郵件", "Current email": "當前電子郵件", - "Update Email": "更新電子郵件" + "Update Email": "更新電子郵件", + "All": "全部" } diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index a0837879..4e664ee7 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, 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'; -import { Book, BooksGroup } from '@/types/book'; +import { Book } from '@/types/book'; import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings'; import { useEnv } from '@/context/EnvContext'; import { useThemeStore } from '@/store/themeStore'; @@ -66,8 +66,8 @@ const Bookshelf: React.FC = ({ const [showDeleteAlert, setShowDeleteAlert] = useState(false); const [showGroupingModal, setShowGroupingModal] = useState(false); const [queryTerm, setQueryTerm] = useState(null); - const [navBooksGroup, setNavBooksGroup] = useState(null); const [importBookUrl] = useState(searchParams?.get('url') || ''); + const [groupId, setGroupId] = useState(searchParams?.get('group') || ''); const [viewMode, setViewMode] = useState(searchParams?.get('view') || settings.libraryViewMode); const [sortBy, setSortBy] = useState(searchParams?.get('sort') || settings.librarySortBy); const [sortOrder, setSortOrder] = useState( @@ -78,6 +78,7 @@ const Bookshelf: React.FC = ({ const { setCurrentBookshelf, setLibrary } = useLibraryStore(); const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore(); + const { getGroupName } = useLibraryStore(); const bookFilter = useMemo(() => createBookFilter(queryTerm), [queryTerm]); const uiLanguage = localStorage?.getItem('i18nextLng') || ''; @@ -87,9 +88,14 @@ const Bookshelf: React.FC = ({ return queryTerm ? libraryBooks.filter((book) => bookFilter(book)) : libraryBooks; }, [libraryBooks, queryTerm, bookFilter]); - const allBookshelfItems = useMemo(() => { - return generateBookshelfItems(filteredBooks); - }, [filteredBooks]); + const currentBookshelfItems = useMemo(() => { + const groupName = getGroupName(groupId) || ''; + if (groupId && !groupName) { + return []; + } + const items = generateBookshelfItems(filteredBooks, groupName); + return items; + }, [filteredBooks, groupId, getGroupName]); const autofocusRef = useAutoFocus(); @@ -113,12 +119,8 @@ const Bookshelf: React.FC = ({ }, [importBookUrl, appService]); useEffect(() => { - if (navBooksGroup) { - setCurrentBookshelf(navBooksGroup.books); - } else { - setCurrentBookshelf(allBookshelfItems); - } - }, [allBookshelfItems, navBooksGroup, setCurrentBookshelf]); + setCurrentBookshelf(currentBookshelfItems); + }, [currentBookshelfItems, setCurrentBookshelf]); useEffect(() => { const group = searchParams?.get('group') || ''; @@ -162,23 +164,19 @@ const Bookshelf: React.FC = ({ params.delete('order'); params.delete('view'); } + setGroupId(group); if (group) { - const booksGroup = allBookshelfItems.find( - (item) => 'name' in item && item.id === group, - ) as BooksGroup; - if (booksGroup) { - setNavBooksGroup(booksGroup); + if (currentBookshelfItems.length > 0) { params.set('group', group); } else { params.delete('group'); navigateToLibrary(router, `${params.toString()}`); } } else { - setNavBooksGroup(null); params.delete('group'); navigateToLibrary(router, `${params.toString()}`); } - }, [router, settings, searchParams, allBookshelfItems, showGroupingModal]); + }, [router, settings, searchParams, currentBookshelfItems, showGroupingModal]); const toggleSelection = useCallback( (id: string) => { @@ -252,7 +250,6 @@ const Bookshelf: React.FC = ({ const sortedBookshelfItems = useMemo(() => { const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1; - const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems; return currentBookshelfItems.sort((a, b) => { if (sortBy === 'updated') { return ( @@ -268,7 +265,7 @@ const Bookshelf: React.FC = ({ return 0; } }); - }, [sortOrder, sortBy, uiLanguage, navBooksGroup, allBookshelfItems, bookSorter]); + }, [sortOrder, sortBy, uiLanguage, currentBookshelfItems, bookSorter]); useEffect(() => { if (isSelectMode) { @@ -330,7 +327,7 @@ const Bookshelf: React.FC = ({ } /> ))} - {viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && ( + {viewMode === 'grid' && currentBookshelfItems.length > 0 && (
+ + {/* Create group input */} {showInput && ( -
- setEditGroupName(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleConfirmCreateGroup(); - e.stopPropagation(); - }} - className='input input-ghost w-full border-0 px-2 text-base !outline-none sm:text-sm' - /> - +
+
+ setEditGroupName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirmCreateGroup(); + if (e.key === 'Escape') setShowInput(false); + e.stopPropagation(); + }} + className='input input-ghost w-full border-0 px-2 text-base !outline-none sm:text-sm' + /> + +
)} -
    - {allGroups.map((group, index) => ( + + {/* Breadcrumb navigation */} + {currentPath && ( +
    - ))} + + {getBreadcrumbs(currentPath).map((crumb, index, array) => { + const isLast = index === array.length - 1; + return ( + + + {isLast ? ( + {crumb.name} + ) : ( + + )} + + ); + })} +
    + )} + + {/* Groups list */} +
      + {currentGroupsList.map((group, index) => { + const displayName = getDisplayName(group.name); + const hasChildren = allGroups.some((g) => g.name.startsWith(group.name + '/')); + return ( +
      + + {hasChildren && ( + + )} +
      + ); + })}
    + + {/* Footer actions */}
    - )}
    diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index d974be9b..42aa377e 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx'; import * as React from 'react'; +import { MdChevronRight } from 'react-icons/md'; import { useState, useRef, useEffect, Suspense, useCallback } from 'react'; import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation'; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from 'overlayscrollbars-react'; @@ -9,7 +10,7 @@ import 'overlayscrollbars/overlayscrollbars.css'; import { Book } from '@/types/book'; import { AppService, DeleteAction } from '@/types/system'; -import { navigateToLogin, navigateToReader } from '@/utils/nav'; +import { navigateToLibrary, navigateToLogin, navigateToReader } from '@/utils/nav'; import { formatAuthors, formatTitle, getPrimaryLanguage, listFormater } from '@/utils/book'; import { eventDispatcher } from '@/utils/event'; import { ProgressPayload } from '@/utils/transfer'; @@ -27,6 +28,7 @@ import { useThemeStore } from '@/store/themeStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; +import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { usePullToRefresh } from '@/hooks/usePullToRefresh'; import { useTheme } from '@/hooks/useTheme'; import { useUICSS } from '@/hooks/useUICSS'; @@ -50,6 +52,7 @@ import { UpdaterWindow } from '@/components/UpdaterWindow'; import { MigrateDataWindow } from './components/MigrateDataWindow'; import { useDragDropImport } from './hooks/useDragDropImport'; import { Toast } from '@/components/Toast'; +import { getBreadcrumbs } from './utils/libraryUtils'; import Spinner from '@/components/Spinner'; import LibraryHeader from './components/LibraryHeader'; import Bookshelf from './components/Bookshelf'; @@ -72,6 +75,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP syncProgress, updateBook, setLibrary, + getGroupId, getGroupName, refreshGroups, checkOpenWithBooks, @@ -90,12 +94,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP const [isSelectAll, setIsSelectAll] = useState(false); const [isSelectNone, setIsSelectNone] = useState(false); const [showDetailsBook, setShowDetailsBook] = useState(null); + const [currentGroupPath, setCurrentGroupPath] = useState(undefined); const [booksTransferProgress, setBooksTransferProgress] = useState<{ [key: string]: number | null; }>({}); const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState(null); const isInitiating = useRef(false); + const iconSize = useResponsiveSize(18); const viewSettings = settings.globalViewSettings; const demoBooks = useDemoBooks(); const osRef = useRef(null); @@ -325,6 +331,12 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); + useEffect(() => { + const group = searchParams?.get('group') || ''; + const groupName = getGroupName(group); + setCurrentGroupPath(groupName); + }, [libraryBooks, searchParams, getGroupName]); + useEffect(() => { if (demoBooks.length > 0 && libraryLoaded) { const newLibrary = [...libraryBooks]; @@ -594,6 +606,20 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP setShowDetailsBook(book); }; + const handleNavigateToPath = (path: string | undefined) => { + const group = path ? getGroupId(path) : ''; + const params = new URLSearchParams(searchParams?.toString()); + if (group) { + params.set('group', group); + } else { + params.delete('group'); + } + navigateToLibrary(router, `${params.toString()}`); + setTimeout(() => { + setCurrentGroupPath(path); + }, 300); + }; + if (!appService || !insets || checkOpenWithBooks || checkLastOpenBooks) { return
    ; } @@ -630,6 +656,40 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
    )} + {currentGroupPath && ( +
    +
    + + {getBreadcrumbs(currentGroupPath).map((crumb, index, array) => { + const isLast = index === array.length - 1; + return ( + + + {isLast ? ( + {crumb.name} + ) : ( + + )} + + ); + })} +
    +
    + )} {showBookshelf && (libraryBooks.some((book) => !book.deletedAt) ? ( (a: Book return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); } }; + +export const getBreadcrumbs = (currentPath: string) => { + if (!currentPath) return []; + const segments = currentPath.split('/'); + return segments.map((segment, index) => ({ + name: segment, + path: segments.slice(0, index + 1).join('/'), + })); +}; diff --git a/apps/readest-app/src/components/Toast.tsx b/apps/readest-app/src/components/Toast.tsx index cb8e0431..4075e666 100644 --- a/apps/readest-app/src/components/Toast.tsx +++ b/apps/readest-app/src/components/Toast.tsx @@ -71,7 +71,7 @@ export const Toast = () => { 'max-h-[50vh] min-w-32 overflow-y-auto', 'text-center font-sans text-base font-normal sm:text-sm', toastType === 'info' - ? 'max-w-[80vw]' + ? 'max-w-[80vw] overflow-x-hidden' : 'min-w-[60vw] max-w-[80vw] whitespace-normal break-words sm:min-w-40 sm:max-w-80', messageClass, )} diff --git a/apps/readest-app/src/store/libraryStore.ts b/apps/readest-app/src/store/libraryStore.ts index 12ad698d..c302f639 100644 --- a/apps/readest-app/src/store/libraryStore.ts +++ b/apps/readest-app/src/store/libraryStore.ts @@ -24,11 +24,13 @@ interface LibraryState { setLibrary: (books: Book[]) => void; updateBook: (envConfig: EnvConfigType, book: Book) => void; setCurrentBookshelf: (bookshelf: (Book | BooksGroup)[]) => void; - refreshGroups: () => void; addGroup: (name: string) => BookGroupType; getGroups: () => BookGroupType[]; + getGroupId: (path: string) => string | undefined; getGroupName: (id: string) => string | undefined; + getParentPath: (path: string) => string | undefined; + getGroupsByParent: (parentPath?: string) => BookGroupType[]; } export const useLibraryStore = create((set, get) => ({ @@ -40,15 +42,22 @@ export const useLibraryStore = create((set, get) => ({ groups: {}, checkOpenWithBooks: isTauriAppPlatform(), checkLastOpenBooks: isTauriAppPlatform(), + setIsSyncing: (syncing: boolean) => set({ isSyncing: syncing }), setSyncProgress: (progress: number) => set({ syncProgress: progress }), getVisibleLibrary: () => get().library.filter((book) => !book.deletedAt), + setCurrentBookshelf: (bookshelf: (Book | BooksGroup)[]) => { set({ currentBookshelf: bookshelf }); }, + setCheckOpenWithBooks: (check) => set({ checkOpenWithBooks: check }), setCheckLastOpenBooks: (check) => set({ checkLastOpenBooks: check }), - setLibrary: (books) => set({ library: books }), + setLibrary: (books) => { + const { refreshGroups } = get(); + set({ library: books }); + refreshGroups(); + }, updateBook: async (envConfig: EnvConfigType, book: Book) => { const appService = await envConfig.getAppService(); const { library } = get(); @@ -59,12 +68,15 @@ export const useLibraryStore = create((set, get) => ({ set({ library: [...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); @@ -76,6 +88,7 @@ export const useLibraryStore = create((set, get) => ({ return { selectedBooks: newSelection }; }); }, + refreshGroups: () => { const { library } = get(); const groups: Record = {}; @@ -89,11 +102,19 @@ export const useLibraryStore = create((set, get) => ({ !book.deletedAt ) { groups[book.groupId] = book.groupName; + let nextSlashIndex = book.groupName.indexOf('/', 0); + while (nextSlashIndex > 0) { + const groupName = book.groupName.substring(0, nextSlashIndex); + const groupId = md5Fingerprint(groupName); + groups[groupId] = groupName; + nextSlashIndex = book.groupName.indexOf('/', nextSlashIndex + 1); + } } }); set({ groups }); }, + addGroup: (name: string) => { const trimmedName = name.trim(); if (!trimmedName) { @@ -107,13 +128,44 @@ export const useLibraryStore = create((set, get) => ({ return { id, name: trimmedName }; }, + getGroups: () => { const { groups } = get(); return Object.entries(groups) .map(([id, name]) => ({ id, name })) .sort((a, b) => a.name.localeCompare(b.name)); }, + + getGroupId: (path: string) => { + const { groups } = get(); + + const directId = Object.entries(groups).find(([_, name]) => name === path)?.[0]; + if (directId) { + return directId; + } + + return md5Fingerprint(path); + }, + getGroupName: (id: string) => { return get().groups[id]; }, + + getParentPath: (path: string) => { + const lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex === -1) return ''; + return path.slice(0, lastSlashIndex); + }, + + getGroupsByParent: (parentPath?: string) => { + const { groups } = get(); + const result: BookGroupType[] = []; + Object.entries(groups).forEach(([id, name]) => { + const groupParentPath = get().getParentPath(name); + if (groupParentPath === (parentPath || '')) { + result.push({ id, name }); + } + }); + return result; + }, })); diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index 39d194c1..b673608a 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -266,6 +266,7 @@ export interface BookDataRecord { export interface BooksGroup { id: string; name: string; + displayName: string; books: Book[]; updatedAt: number;