From 37951c2ebfbe14d84cbdddef70f96c3fdfbc4c9a Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 8 Apr 2025 21:37:18 +0800 Subject: [PATCH] feat: add book metadata search in the bookshelf, closes #100 (#838) --- .../public/locales/ar/translation.json | 3 +- .../public/locales/de/translation.json | 3 +- .../public/locales/el/translation.json | 3 +- .../public/locales/es/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/pl/translation.json | 3 +- .../public/locales/pt/translation.json | 3 +- .../public/locales/ru/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 | 26 +++++++++-- .../app/library/components/BookshelfItem.tsx | 7 ++- .../app/library/components/LibraryHeader.tsx | 43 +++++++++++++++++-- 21 files changed, 104 insertions(+), 26 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 17cd4731..070d6039 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} دقائق", "{{value}} hour": "{{value}} ساعة", "{{value}} hours": "{{value}} ساعات", - "CJK Font": "خط CJK" + "CJK Font": "خط CJK", + "Clear Search": "مسح البحث" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 1e34dee3..b2eca669 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} Minuten", "{{value}} hour": "{{value}} Stunde", "{{value}} hours": "{{value}} Stunden", - "CJK Font": "CJK-Schriftart" + "CJK Font": "CJK-Schriftart", + "Clear Search": "Suche löschen" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index acdb6c6a..e66f57d2 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} λεπτά", "{{value}} hour": "{{value}} ώρα", "{{value}} hours": "{{value}} ώρες", - "CJK Font": "Γραμματοσειρά CJK" + "CJK Font": "Γραμματοσειρά CJK", + "Clear Search": "Καθαρισμός αναζήτησης" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index cc7e03dd..92fbbc95 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} minutos", "{{value}} hour": "{{value}} hora", "{{value}} hours": "{{value}} horas", - "CJK Font": "Fuente CJK" + "CJK Font": "Fuente CJK", + "Clear Search": "Limpiar búsqueda" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index df197e64..8c86c7f7 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} minutes", "{{value}} hour": "{{value}} heure", "{{value}} hours": "{{value}} heures", - "CJK Font": "Police CJK" + "CJK Font": "Police CJK", + "Clear Search": "Effacer la recherche" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index d2d3a5bd..219108ea 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} मिनट", "{{value}} hour": "{{value}} घंटा", "{{value}} hours": "{{value}} घंटे", - "CJK Font": "CJK फ़ॉन्ट" + "CJK Font": "CJK फ़ॉन्ट", + "Clear Search": "खोज साफ करें" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index d23a767a..f143cf59 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} menit", "{{value}} hour": "{{value}} jam", "{{value}} hours": "{{value}} jam", - "CJK Font": "Font CJK" + "CJK Font": "Font CJK", + "Clear Search": "Hapus Pencarian" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index e1d1ee22..2348c490 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} minuti", "{{value}} hour": "{{value}} ora", "{{value}} hours": "{{value}} ore", - "CJK Font": "Font CJK" + "CJK Font": "Font CJK", + "Clear Search": "Cancella ricerca" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index f3ea7c68..1c5187c9 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}}分", "{{value}} hour": "{{value}}時間", "{{value}} hours": "{{value}}時間", - "CJK Font": "CJKフォント" + "CJK Font": "CJKフォント", + "Clear Search": "検索をクリア" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 53ccc994..954a683a 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} 분", "{{value}} hour": "{{value}} 시간", "{{value}} hours": "{{value}} 시간", - "CJK Font": "CJK 글꼴" + "CJK Font": "CJK 글꼴", + "Clear Search": "검색 지우기" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index e87e007d..bbbc684c 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} minut", "{{value}} hour": "{{value}} godzina", "{{value}} hours": "{{value}} godziny", - "CJK Font": "Czcionka CJK" + "CJK Font": "Czcionka CJK", + "Clear Search": "Wyczyść wyszukiwanie" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 99885c9e..5431b140 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} minutos", "{{value}} hour": "{{value}} hora", "{{value}} hours": "{{value}} horas", - "CJK Font": "Fonte CJK" + "CJK Font": "Fonte CJK", + "Clear Search": "Limpar Pesquisa" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 74dce2d5..e453b44d 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} минуты", "{{value}} hour": "{{value}} час", "{{value}} hours": "{{value}} часа", - "CJK Font": "CJK Шрифт" + "CJK Font": "CJK Шрифт", + "Clear Search": "Очистить поиск" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 3a286494..6a64035e 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} dakika", "{{value}} hour": "{{value}} saat", "{{value}} hours": "{{value}} saat", - "CJK Font": "CJK Yazı Tipi" + "CJK Font": "CJK Yazı Tipi", + "Clear Search": "Aramayı Temizle" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index b2d4372f..70d7df02 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} хвилин", "{{value}} hour": "{{value}} година", "{{value}} hours": "{{value}} години", - "CJK Font": "CJK шрифт" + "CJK Font": "CJK шрифт", + "Clear Search": "Очистити пошук" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index ac75dccb..06d469be 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}} phút", "{{value}} hour": "{{value}} giờ", "{{value}} hours": "{{value}} giờ", - "CJK Font": "Phông chữ CJK" + "CJK Font": "Phông chữ CJK", + "Clear Search": "Xóa tìm kiếm" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 2b0d0155..11fe7b7d 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}}分钟", "{{value}} hour": "{{value}}小时", "{{value}} hours": "{{value}}小时", - "CJK Font": "中文字体" + "CJK Font": "中文字体", + "Clear Search": "清除搜索" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index b6f8a2d1..7cf421ea 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -272,5 +272,6 @@ "{{value}} minutes": "{{value}}分鐘", "{{value}} hour": "{{value}}小時", "{{value}} hours": "{{value}}小時", - "CJK Font": "中文字體" + "CJK Font": "中文字體", + "Clear Search": "清除搜索" } diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index dc3cb152..32940798 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -10,6 +10,7 @@ import { useEnv } from '@/context/EnvContext'; import { useLibraryStore } from '@/store/libraryStore'; import { useTranslation } from '@/hooks/useTranslation'; import { navigateToLibrary, navigateToReader } from '@/utils/nav'; +import { formatAuthors, formatTitle } from '@/utils/book'; import { isMd5 } from '@/utils/md5'; import Alert from '@/components/Alert'; @@ -49,6 +50,7 @@ const Bookshelf: React.FC = ({ const [showSelectModeActions, setShowSelectModeActions] = useState(false); 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 isImportingBook = useRef(false); @@ -86,6 +88,12 @@ const Bookshelf: React.FC = ({ useEffect(() => { const group = searchParams?.get('group') || ''; + const query = searchParams?.get('q') || ''; + if (query) { + setQueryTerm(query); + } else { + setQueryTerm(null); + } if (group) { const booksGroup = allBookshelfItems.find( (item) => 'name' in item && item.id === group, @@ -93,11 +101,11 @@ const Bookshelf: React.FC = ({ if (booksGroup) { setNavBooksGroup(booksGroup); } else { - navigateToLibrary(router); + navigateToLibrary(router, query ? `q=${query}` : undefined); } } else { setNavBooksGroup(null); - navigateToLibrary(router); + navigateToLibrary(router, query ? `q=${query}` : undefined); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams, libraryBooks, showGroupingModal]); @@ -137,6 +145,18 @@ const Bookshelf: React.FC = ({ }; const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems; + const bookFilter = (item: Book, queryTerm: string) => { + if (item.deletedAt) return false; + const searchTerm = new RegExp(queryTerm, 'i'); + const title = formatTitle(item.title); + const authors = formatAuthors(item.author); + return searchTerm.test(title) || searchTerm.test(authors); + }; + const filteredBookshelfItems = currentBookshelfItems.filter((item) => { + if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || '')); + else if (queryTerm) return bookFilter(item, queryTerm); + return true; + }); return (
@@ -146,7 +166,7 @@ const Bookshelf: React.FC = ({ 'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12', )} > - {currentBookshelfItems.map((item, index) => ( + {filteredBookshelfItems.map((item, index) => ( = ({ }) => { const _ = useTranslation(); const router = useRouter(); + const searchParams = useSearchParams(); const { envConfig, appService } = useEnv(); const { settings } = useSettingsStore(); const { updateBook } = useLibraryStore(); @@ -133,7 +134,9 @@ const BookshelfItem: React.FC = ({ if (isSelectMode) { toggleSelection(group.id); } else { - navigateToLibrary(router, `group=${group.id}`); + const params = new URLSearchParams(searchParams?.toString()); + params.set('group', group.id); + navigateToLibrary(router, `${params.toString()}`); } }; diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index 2a0679e6..a796315e 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -1,17 +1,18 @@ import clsx from 'clsx'; -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { FaSearch } from 'react-icons/fa'; import { PiPlus } from 'react-icons/pi'; import { PiSelectionAllDuotone } from 'react-icons/pi'; -import { MdOutlineMenu } from 'react-icons/md'; -import { MdArrowBackIosNew } from 'react-icons/md'; +import { MdOutlineMenu, MdArrowBackIosNew } from 'react-icons/md'; +import { IoMdCloseCircle } from 'react-icons/io'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { useTrafficLightStore } from '@/store/trafficLightStore'; import { navigateToLibrary } from '@/utils/nav'; +import { throttle } from '@/utils/throttle'; import WindowButtons from '@/components/WindowButtons'; import Dropdown from '@/components/Dropdown'; import SettingsMenu from './SettingsMenu'; @@ -40,6 +41,7 @@ const LibraryHeader: React.FC = ({ setTrafficLightVisibility, cleanupTrafficLightListeners, } = useTrafficLightStore(); + const [searchQuery, setSearchQuery] = useState(searchParams?.get('q') ?? ''); const headerRef = useRef(null); const iconSize16 = useResponsiveSize(16); @@ -49,6 +51,26 @@ const LibraryHeader: React.FC = ({ onToggleSelectMode, }); + // eslint-disable-next-line react-hooks/exhaustive-deps + const throttledUpdateQueryParam = useCallback( + throttle((value: string) => { + const params = new URLSearchParams(searchParams?.toString()); + if (value) { + params.set('q', value); + } else { + params.delete('q'); + } + router.push(`?${params.toString()}`); + }, 1000), + [searchParams], + ); + + const handleSearchChange = (e: React.ChangeEvent) => { + const newQuery = e.target.value; + setSearchQuery(newQuery); + throttledUpdateQueryParam(newQuery); + }; + useEffect(() => { if (!appService?.hasTrafficLight) return; @@ -93,7 +115,9 @@ const LibraryHeader: React.FC = ({ = ({ />
+ {searchQuery && ( + + )}