diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index ad0046fd..7599f034 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -872,5 +872,7 @@ "Next Result": "النتيجة التالية", "Bookmarks": "الإشارات المرجعية", "Annotations": "التعليقات التوضيحية", - "Show Results": "عرض النتائج" + "Show Results": "عرض النتائج", + "Clear search": "مسح البحث", + "Clear search history": "مسح سجل البحث" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 12376737..c299d227 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -832,5 +832,7 @@ "Next Result": "পরবর্তী ফলাফল", "Bookmarks": "বুকমার্ক", "Annotations": "টীকা", - "Show Results": "ফলাফল দেখুন" + "Show Results": "ফলাফল দেখুন", + "Clear search": "অনুসন্ধান সাফ করুন", + "Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index a3a947f0..26fc83ec 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -822,5 +822,7 @@ "Next Result": "རྗེས་མའི་འབྲས་བུ།", "Bookmarks": "དཔེ་རྟགས།", "Annotations": "མཆན།", - "Show Results": "འབྲས་བུ་སྟོན།" + "Show Results": "འབྲས་བུ་སྟོན།", + "Clear search": "འཚོལ་བཤེར་གཙང་སེལ།", + "Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 75d0ab8f..5a8c3a66 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -832,5 +832,7 @@ "Next Result": "Nächstes Ergebnis", "Bookmarks": "Lesezeichen", "Annotations": "Anmerkungen", - "Show Results": "Ergebnisse anzeigen" + "Show Results": "Ergebnisse anzeigen", + "Clear search": "Suche löschen", + "Clear search history": "Suchverlauf löschen" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 3c88b8c6..d8df8e59 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -832,5 +832,7 @@ "Next Result": "Επόμενο αποτέλεσμα", "Bookmarks": "Σελιδοδείκτες", "Annotations": "Σχολιασμοί", - "Show Results": "Εμφάνιση αποτελεσμάτων" + "Show Results": "Εμφάνιση αποτελεσμάτων", + "Clear search": "Εκκαθάριση αναζήτησης", + "Clear search history": "Εκκαθάριση ιστορικού αναζήτησης" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 783eed66..a0729204 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -842,5 +842,7 @@ "Next Result": "Resultado siguiente", "Bookmarks": "Marcadores", "Annotations": "Anotaciones", - "Show Results": "Mostrar resultados" + "Show Results": "Mostrar resultados", + "Clear search": "Borrar búsqueda", + "Clear search history": "Borrar historial de búsqueda" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 13a722b9..35808068 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -832,5 +832,7 @@ "Next Result": "نتیجه بعدی", "Bookmarks": "نشانک‌ها", "Annotations": "یادداشت‌ها", - "Show Results": "نمایش نتایج" + "Show Results": "نمایش نتایج", + "Clear search": "پاک کردن جستجو", + "Clear search history": "پاک کردن تاریخچه جستجو" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index b558c9d6..247c78ca 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -842,5 +842,7 @@ "Next Result": "Résultat suivant", "Bookmarks": "Signets", "Annotations": "Annotations", - "Show Results": "Afficher les résultats" + "Show Results": "Afficher les résultats", + "Clear search": "Effacer la recherche", + "Clear search history": "Effacer l'historique de recherche" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index d6ef1790..389a5972 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -832,5 +832,7 @@ "Next Result": "अगला परिणाम", "Bookmarks": "बुकमार्क", "Annotations": "टिप्पणियाँ", - "Show Results": "परिणाम दिखाएं" + "Show Results": "परिणाम दिखाएं", + "Clear search": "खोज साफ़ करें", + "Clear search history": "खोज इतिहास साफ़ करें" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 39bbf86d..855fdf2b 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -822,5 +822,7 @@ "Next Result": "Hasil berikutnya", "Bookmarks": "Penanda", "Annotations": "Anotasi", - "Show Results": "Tampilkan Hasil" + "Show Results": "Tampilkan Hasil", + "Clear search": "Hapus pencarian", + "Clear search history": "Hapus riwayat pencarian" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 3b242080..ebc624d9 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -842,5 +842,7 @@ "Next Result": "Risultato successivo", "Bookmarks": "Segnalibri", "Annotations": "Annotazioni", - "Show Results": "Mostra risultati" + "Show Results": "Mostra risultati", + "Clear search": "Cancella ricerca", + "Clear search history": "Cancella cronologia ricerche" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 8ccbf337..46eb3a4d 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -822,5 +822,7 @@ "Next Result": "次の結果", "Bookmarks": "ブックマーク", "Annotations": "注釈", - "Show Results": "結果を表示" + "Show Results": "結果を表示", + "Clear search": "検索をクリア", + "Clear search history": "検索履歴をクリア" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 564a3c00..de5f5e96 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -822,5 +822,7 @@ "Next Result": "다음 결과", "Bookmarks": "북마크", "Annotations": "주석", - "Show Results": "결과 표시" + "Show Results": "결과 표시", + "Clear search": "검색 지우기", + "Clear search history": "검색 기록 지우기" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 1ceb6527..bf16e31b 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -822,5 +822,7 @@ "Next Result": "Hasil seterusnya", "Bookmarks": "Penanda buku", "Annotations": "Anotasi", - "Show Results": "Tunjukkan Hasil" + "Show Results": "Tunjukkan Hasil", + "Clear search": "Kosongkan carian", + "Clear search history": "Kosongkan sejarah carian" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 61d0e237..d6b1e5c0 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -832,5 +832,7 @@ "Next Result": "Volgend resultaat", "Bookmarks": "Bladwijzers", "Annotations": "Aantekeningen", - "Show Results": "Resultaten tonen" + "Show Results": "Resultaten tonen", + "Clear search": "Zoekopdracht wissen", + "Clear search history": "Zoekgeschiedenis wissen" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 1d1c774a..184bb48a 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -852,5 +852,7 @@ "Next Result": "Następny wynik", "Bookmarks": "Zakładki", "Annotations": "Adnotacje", - "Show Results": "Pokaż wyniki" + "Show Results": "Pokaż wyniki", + "Clear search": "Wyczyść wyszukiwanie", + "Clear search history": "Wyczyść historię wyszukiwania" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 5b63fb60..0ab0f90d 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -842,5 +842,7 @@ "Next Result": "Próximo resultado", "Bookmarks": "Marcadores", "Annotations": "Anotações", - "Show Results": "Mostrar resultados" + "Show Results": "Mostrar resultados", + "Clear search": "Limpar pesquisa", + "Clear search history": "Limpar histórico de pesquisa" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 062e2f33..6824a924 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -852,5 +852,7 @@ "Next Result": "Следующий результат", "Bookmarks": "Закладки", "Annotations": "Аннотации", - "Show Results": "Показать результаты" + "Show Results": "Показать результаты", + "Clear search": "Очистить поиск", + "Clear search history": "Очистить историю поиска" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 77c452a0..225836c8 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -832,5 +832,7 @@ "Next Result": "ඊළඟ ප්‍රතිඵලය", "Bookmarks": "පොත් සලකුණු", "Annotations": "අනුසටහන්", - "Show Results": "ප්‍රතිඵල පෙන්වන්න" + "Show Results": "ප්‍රතිඵල පෙන්වන්න", + "Clear search": "සෙවුම හිස් කරන්න", + "Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 71806e13..b5f913b6 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -832,5 +832,7 @@ "Next Result": "Nästa resultat", "Bookmarks": "Bokmärken", "Annotations": "Anteckningar", - "Show Results": "Visa resultat" + "Show Results": "Visa resultat", + "Clear search": "Rensa sökning", + "Clear search history": "Rensa sökhistorik" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 01b35325..3f45eabf 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -832,5 +832,7 @@ "Next Result": "அடுத்த முடிவு", "Bookmarks": "புக்மார்க்குகள்", "Annotations": "சிறுகுறிப்புகள்", - "Show Results": "முடிவுகளைக் காட்டு" + "Show Results": "முடிவுகளைக் காட்டு", + "Clear search": "தேடலை அழி", + "Clear search history": "தேடல் வரலாற்றை அழி" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index f54a1309..076eeb75 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -822,5 +822,7 @@ "Next Result": "ผลลัพธ์ถัดไป", "Bookmarks": "บุ๊กมาร์ก", "Annotations": "คำอธิบายประกอบ", - "Show Results": "แสดงผลลัพธ์" + "Show Results": "แสดงผลลัพธ์", + "Clear search": "ล้างการค้นหา", + "Clear search history": "ล้างประวัติการค้นหา" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index eea11a46..9ace0f94 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -832,5 +832,7 @@ "Next Result": "Sonraki sonuç", "Bookmarks": "Yer İşaretleri", "Annotations": "Açıklamalar", - "Show Results": "Sonuçları Göster" + "Show Results": "Sonuçları Göster", + "Clear search": "Aramayı temizle", + "Clear search history": "Arama geçmişini temizle" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 88744a8b..626b71a8 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -852,5 +852,7 @@ "Next Result": "Наступний результат", "Bookmarks": "Закладки", "Annotations": "Анотації", - "Show Results": "Показати результати" + "Show Results": "Показати результати", + "Clear search": "Очистити пошук", + "Clear search history": "Очистити історію пошуку" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 18ffce35..91a6efc2 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -822,5 +822,7 @@ "Next Result": "Kết quả tiếp theo", "Bookmarks": "Dấu trang", "Annotations": "Chú thích", - "Show Results": "Hiển thị kết quả" + "Show Results": "Hiển thị kết quả", + "Clear search": "Xóa tìm kiếm", + "Clear search history": "Xóa lịch sử 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 adff23b7..82b26839 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -822,5 +822,7 @@ "Next Result": "下一个结果", "Bookmarks": "书签", "Annotations": "注释", - "Show Results": "显示结果" + "Show Results": "显示结果", + "Clear search": "清除搜索", + "Clear search history": "清除搜索历史" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index c077f4ab..cd45bafe 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -822,5 +822,7 @@ "Next Result": "下一個結果", "Bookmarks": "書籤", "Annotations": "註解", - "Show Results": "顯示結果" + "Show Results": "顯示結果", + "Clear search": "清除搜尋", + "Clear search history": "清除搜尋歷史" } diff --git a/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx b/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx index 16af0ef2..18f92612 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx @@ -51,6 +51,7 @@ const BooknotesNav: React.FC = ({ bookKey, gridInsets, toc }) section={currentSection} hasPrevious={hasPreviousPage} hasNext={hasNextPage} + progress={1} previousTitle={_('Previous')} nextTitle={_('Next')} showResultsTitle={getShowResultsTitle()} diff --git a/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx index a40baece..4cf34382 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx @@ -14,6 +14,7 @@ interface ContentNavBarProps { gridInsets: Insets; title: string; section?: string; + progress?: number; // 0 to 1, where 1 means complete showListButton?: boolean; hasPrevious: boolean; hasNext: boolean; @@ -34,6 +35,7 @@ const ContentNavBar: React.FC = ({ gridInsets, title, section, + progress, showListButton = true, hasPrevious, hasNext, @@ -69,20 +71,27 @@ const ContentNavBar: React.FC = ({ }} > {/* Top bar: Info */} -
+
+ {progress !== undefined && progress < 1 && ( +
+ )} + {progress === 1 &&
} {showListButton && onShowResults ? ( ) : ( -
+
)} -
+
{title} {section && showSection && ( {section} @@ -92,7 +101,7 @@ const ContentNavBar: React.FC = ({ diff --git a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx index eaf2759c..f7ac890d 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx @@ -4,13 +4,14 @@ import { FaSearch, FaChevronDown } from 'react-icons/fa'; import { IoMdCloseCircle } from 'react-icons/io'; import { MdDeleteOutline } from 'react-icons/md'; +import { md5 } from 'js-md5'; import { useEnv } from '@/context/EnvContext'; import { useSettingsStore } from '@/store/settingsStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; -import { BookSearchConfig, BookSearchResult } from '@/types/book'; +import { BookSearchConfig, BookSearchMatch, BookSearchResult } from '@/types/book'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { debounce } from '@/utils/debounce'; import { isCJKStr } from '@/utils/lang'; @@ -21,6 +22,7 @@ import SearchOptions from './SearchOptions'; const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2; const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1; const SEARCH_HISTORY_KEY = 'search-history'; +const SEARCH_CACHE_DIR = 'search'; const MAX_SEARCH_HISTORY = 10; interface SearchBarProps { @@ -31,12 +33,13 @@ interface SearchBarProps { const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchBar }) => { const _ = useTranslation(); - const { envConfig } = useEnv(); + const { envConfig, appService } = useEnv(); const { settings } = useSettingsStore(); const { getBookData } = useBookDataStore(); - const { getConfig, saveConfig } = useBookDataStore(); + const { getConfig, setConfig, saveConfig } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); - const { getSearchNavState, setSearchTerm, setSearchResults } = useSidebarStore(); + const { getSearchNavState, setSearchTerm, setSearchResults, setSearchProgress } = + useSidebarStore(); const searchNavState = getSearchNavState(bookKey); const { searchTerm } = searchNavState; @@ -83,17 +86,79 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB inputRef.current?.focus(); }; - const handleClearHistory = () => { + const handleClearHistory = async () => { setSearchHistory([]); localStorage.removeItem(historyStorageKey); + await clearSearchCache(); }; + const getSearchCacheKey = useCallback((term: string, config: BookSearchConfig) => { + const configStr = JSON.stringify({ + scope: config.scope, + matchCase: config.matchCase, + matchWholeWords: config.matchWholeWords, + matchDiacritics: config.matchDiacritics, + }); + return md5(`${term}-${configStr}`); + }, []); + + const getSearchCache = useCallback( + async ( + term: string, + config: BookSearchConfig, + ): Promise => { + const cacheKey = getSearchCacheKey(term, config); + const cachePath = `${SEARCH_CACHE_DIR}/${bookHash}/${cacheKey}.json`; + try { + if (await appService?.exists(cachePath, 'Cache')) { + const content = await appService?.readFile(cachePath, 'Cache', 'text'); + if (content) return JSON.parse(content as string); + } + } catch (error) { + console.error('Failed to read search cache:', error); + } + return null; + }, + [bookHash, appService, getSearchCacheKey], + ); + + const saveSearchCache = useCallback( + async ( + term: string, + config: BookSearchConfig, + results: BookSearchResult[] | BookSearchMatch[], + ) => { + const cacheKey = getSearchCacheKey(term, config); + const cacheDir = `${SEARCH_CACHE_DIR}/${bookHash}`; + const cachePath = `${cacheDir}/${cacheKey}.json`; + try { + if (!(await appService?.exists(cacheDir, 'Cache'))) { + await appService?.createDir(cacheDir, 'Cache', true); + } + await appService?.writeFile(cachePath, 'Cache', JSON.stringify(results)); + } catch (error) { + console.error('Failed to save search cache:', error); + } + }, + [bookHash, appService, getSearchCacheKey], + ); + + const clearSearchCache = useCallback(async () => { + const cacheDir = `${SEARCH_CACHE_DIR}/${bookHash}`; + try { + if (await appService?.exists(cacheDir, 'Cache')) { + await appService?.deleteDir(cacheDir, 'Cache', true); + } + } catch (error) { + console.error('Failed to clear search cache:', error); + } + }, [bookHash, appService]); + const view = getView(bookKey)!; const config = getConfig(bookKey)!; const bookData = getBookData(bookKey)!; const progress = getProgress(bookKey)!; const primaryLang = bookData.book?.primaryLanguage || 'en'; - const searchConfig = config.searchConfig! as BookSearchConfig; const iconSize12 = useResponsiveSize(12); const iconSize16 = useResponsiveSize(16); @@ -142,7 +207,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB }; const handleSearchConfigChange = (searchConfig: BookSearchConfig) => { - config.searchConfig = searchConfig; + setConfig(bookKey, { searchConfig: { ...searchConfig } }); saveConfig(envConfig, bookKey, config, settings); handleSearchTermChange(searchTerm); }; @@ -158,6 +223,20 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const handleSearch = useCallback( async (term: string) => { console.log('searching for:', term); + + const searchConfig = config.searchConfig as BookSearchConfig; + const cachedResults = await getSearchCache(term, searchConfig); + if (cachedResults) { + setSearchResults(bookKey, cachedResults); + setSearchProgress(bookKey, 1); + if (cachedResults.length > 0) { + addToHistory(term); + } + } + + // Reset progress at start of search + setSearchProgress(bookKey, 0); + const { section } = progress; const index = searchConfig.scope === 'section' ? section.current : undefined; const generator = await view.search({ @@ -167,6 +246,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB acceptNode: createRejectFilter({ tags: primaryLang.startsWith('ja') ? ['rt'] : [], }), + results: cachedResults, }); const results: BookSearchResult[] = []; let lastProgressLogTime = 0; @@ -176,13 +256,16 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB if (typeof result === 'string') { if (result === 'done') { setSearchResults(bookKey, [...results]); + setSearchProgress(bookKey, 1); if (results.length > 0) { addToHistory(term); + await saveSearchCache(term, searchConfig, results); } console.log('search done'); } } else { if (result.progress) { + setSearchProgress(bookKey, result.progress); const now = Date.now(); if (now - lastProgressLogTime >= 1000) { console.log('search progress:', result.progress); @@ -206,7 +289,15 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB processResults(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [progress, searchConfig, setSearchResults, addToHistory], + [ + progress, + config.searchConfig, + setSearchResults, + setSearchProgress, + addToHistory, + getSearchCache, + saveSearchCache, + ], ); const resetSearch = useCallback(() => { @@ -266,7 +357,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB toggleButton={} > @@ -287,9 +378,9 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB ))}
@@ -303,8 +394,8 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB 'text-base-content/50 hover:text-base-content/80 bg-base-200 flex-shrink-0 items-center', 'flex h-6 min-h-6 w-8 min-w-8 items-center justify-center p-0', )} - title={_('Clear history')} - aria-label={_('Clear history')} + title={_('Clear search history')} + aria-label={_('Clear search history')} > diff --git a/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx b/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx index 9b015484..b52eb8cb 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx @@ -14,6 +14,7 @@ interface SearchResultsNavProps { const SearchResultsNav: React.FC = ({ bookKey, gridInsets }) => { const { searchTerm, + searchProgress, currentSection, showSearchNav, hasPreviousPage, @@ -35,6 +36,7 @@ const SearchResultsNav: React.FC = ({ bookKey, gridInsets gridInsets={gridInsets} title={_("Search results for '{{term}}'", { term: searchTerm })} section={currentSection} + progress={searchProgress} hasPrevious={hasPreviousPage} hasNext={hasNextPage} previousTitle={_('Previous Result')} diff --git a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts index 0893f5cb..7d484ab8 100644 --- a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts +++ b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts @@ -6,15 +6,11 @@ import { flattenSearchResults } from '../components/sidebar/SearchResultsNav'; export function useSearchNav(bookKey: string) { const { getView, getProgress } = useReaderStore(); - const { - setSideBarVisible, - getSearchNavState, - setSearchResultIndex, - clearSearch, - } = useSidebarStore(); + const { setSideBarVisible, getSearchNavState, setSearchResultIndex, clearSearch } = + useSidebarStore(); const searchNavState = getSearchNavState(bookKey); - const { searchTerm, searchResults, searchResultIndex } = searchNavState; + const { searchTerm, searchResults, searchResultIndex, searchProgress } = searchNavState; const progress = getProgress(bookKey); @@ -119,6 +115,7 @@ export function useSearchNav(bookKey: string) { return { searchTerm, + searchProgress, currentSection, searchResultIndex, totalResults, diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index b77c8018..3b4a7470 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -135,6 +135,10 @@ export abstract class BaseAppService implements AppService { return await this.fs.copyFile(srcPath, dstPath, base); } + async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') { + return await this.fs.readFile(path, base, mode); + } + async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) { return await this.fs.writeFile(path, base, content); } diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts index 552bdf88..f686e822 100644 --- a/apps/readest-app/src/services/webAppService.ts +++ b/apps/readest-app/src/services/webAppService.ts @@ -168,11 +168,30 @@ const indexedDBFileSystem: FileSystem = { transaction.onerror = () => reject(transaction.error); }); }, - async createDir() { - // Directories are virtual in IndexedDB; no-op + async createDir(path: string, base: BaseDir) { + return await this.writeFile(path, base, ''); }, - async removeDir() { - // Directories are virtual in IndexedDB; no-op + async removeDir(path: string, base: BaseDir) { + const { fp } = this.resolvePath(path, base); + const db = await openIndexedDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction('files', 'readwrite'); + const store = transaction.objectStore('files'); + const request = store.getAll(); + + request.onsuccess = () => { + const files = request.result as { path: string }[]; + files.forEach((file) => { + if (file.path.startsWith(fp)) { + store.delete(file.path); + } + }); + }; + + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + }); }, async readDir(path: string, base: BaseDir) { const { fp } = this.resolvePath(path, base); diff --git a/apps/readest-app/src/store/sidebarStore.ts b/apps/readest-app/src/store/sidebarStore.ts index dd91b307..e8d480af 100644 --- a/apps/readest-app/src/store/sidebarStore.ts +++ b/apps/readest-app/src/store/sidebarStore.ts @@ -1,11 +1,12 @@ import { create } from 'zustand'; -import { BookNote, BookNoteType, BookSearchResult } from '@/types/book'; +import { BookNote, BookNoteType, BookSearchMatch, BookSearchResult } from '@/types/book'; // Per-book search navigation state interface SearchNavState { searchTerm: string; - searchResults: BookSearchResult[] | null; + searchResults: BookSearchResult[] | BookSearchMatch[] | null; searchResultIndex: number; + searchProgress: number; // 0 to 1, where 1 means search complete } // Per-book booknotes navigation state @@ -34,8 +35,12 @@ interface SidebarState { // Search actions (per bookKey) getSearchNavState: (bookKey: string) => SearchNavState; setSearchTerm: (bookKey: string, term: string) => void; - setSearchResults: (bookKey: string, results: BookSearchResult[] | null) => void; + setSearchResults: ( + bookKey: string, + results: BookSearchResult[] | BookSearchMatch[] | null, + ) => void; setSearchResultIndex: (bookKey: string, index: number) => void; + setSearchProgress: (bookKey: string, progress: number) => void; clearSearch: (bookKey: string) => void; // Booknotes navigation actions (per bookKey) getBooknotesNavState: (bookKey: string) => BooknotesNavState; @@ -49,6 +54,7 @@ const defaultSearchNavState: SearchNavState = { searchTerm: '', searchResults: null, searchResultIndex: 0, + searchProgress: 1, }; const defaultBooknotesNavState: BooknotesNavState = { @@ -87,7 +93,7 @@ export const useSidebarStore = create((set, get) => ({ }, }, })), - setSearchResults: (bookKey: string, results: BookSearchResult[] | null) => + setSearchResults: (bookKey: string, results: BookSearchResult[] | BookSearchMatch[] | null) => set((state) => ({ searchNavStates: { ...state.searchNavStates, @@ -107,6 +113,16 @@ export const useSidebarStore = create((set, get) => ({ }, }, })), + setSearchProgress: (bookKey: string, progress: number) => + set((state) => ({ + searchNavStates: { + ...state.searchNavStates, + [bookKey]: { + ...(state.searchNavStates[bookKey] || defaultSearchNavState), + searchProgress: progress, + }, + }, + })), clearSearch: (bookKey: string) => set((state) => ({ searchNavStates: { diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index 349ee3ea..6f4ff277 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -268,6 +268,9 @@ export interface BookSearchConfig { index?: number; query?: string; acceptNode?: (node: Node) => number; + + // pre-cached search results + results?: BookSearchResult[] | BookSearchMatch[] | null; } export interface SearchExcerpt { @@ -282,6 +285,7 @@ export interface BookSearchMatch { } export interface BookSearchResult { + index?: number; label: string; subitems: BookSearchMatch[]; progress?: number; diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index 2ed9c047..6d19c311 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -92,6 +92,7 @@ export interface AppService { init(): Promise; openFile(path: string, base: BaseDir): Promise; copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise; + readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise; writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File): Promise; createDir(path: string, base: BaseDir, recursive?: boolean): Promise; deleteFile(path: string, base: BaseDir): Promise; diff --git a/packages/foliate-js b/packages/foliate-js index bab0bccb..4eb5ca42 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit bab0bccbe999877c0c14770387ca033ae247ad42 +Subproject commit 4eb5ca42a4c4ea36a8d43d0bd8349646d4974d65