forked from akai/readest
perf(search): use cache for search results (#2861)
This commit is contained in:
@@ -872,5 +872,7 @@
|
||||
"Next Result": "النتيجة التالية",
|
||||
"Bookmarks": "الإشارات المرجعية",
|
||||
"Annotations": "التعليقات التوضيحية",
|
||||
"Show Results": "عرض النتائج"
|
||||
"Show Results": "عرض النتائج",
|
||||
"Clear search": "مسح البحث",
|
||||
"Clear search history": "مسح سجل البحث"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "পরবর্তী ফলাফল",
|
||||
"Bookmarks": "বুকমার্ক",
|
||||
"Annotations": "টীকা",
|
||||
"Show Results": "ফলাফল দেখুন"
|
||||
"Show Results": "ফলাফল দেখুন",
|
||||
"Clear search": "অনুসন্ধান সাফ করুন",
|
||||
"Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "རྗེས་མའི་འབྲས་བུ།",
|
||||
"Bookmarks": "དཔེ་རྟགས།",
|
||||
"Annotations": "མཆན།",
|
||||
"Show Results": "འབྲས་བུ་སྟོན།"
|
||||
"Show Results": "འབྲས་བུ་སྟོན།",
|
||||
"Clear search": "འཚོལ་བཤེར་གཙང་སེལ།",
|
||||
"Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "Επόμενο αποτέλεσμα",
|
||||
"Bookmarks": "Σελιδοδείκτες",
|
||||
"Annotations": "Σχολιασμοί",
|
||||
"Show Results": "Εμφάνιση αποτελεσμάτων"
|
||||
"Show Results": "Εμφάνιση αποτελεσμάτων",
|
||||
"Clear search": "Εκκαθάριση αναζήτησης",
|
||||
"Clear search history": "Εκκαθάριση ιστορικού αναζήτησης"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "نتیجه بعدی",
|
||||
"Bookmarks": "نشانکها",
|
||||
"Annotations": "یادداشتها",
|
||||
"Show Results": "نمایش نتایج"
|
||||
"Show Results": "نمایش نتایج",
|
||||
"Clear search": "پاک کردن جستجو",
|
||||
"Clear search history": "پاک کردن تاریخچه جستجو"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "अगला परिणाम",
|
||||
"Bookmarks": "बुकमार्क",
|
||||
"Annotations": "टिप्पणियाँ",
|
||||
"Show Results": "परिणाम दिखाएं"
|
||||
"Show Results": "परिणाम दिखाएं",
|
||||
"Clear search": "खोज साफ़ करें",
|
||||
"Clear search history": "खोज इतिहास साफ़ करें"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "次の結果",
|
||||
"Bookmarks": "ブックマーク",
|
||||
"Annotations": "注釈",
|
||||
"Show Results": "結果を表示"
|
||||
"Show Results": "結果を表示",
|
||||
"Clear search": "検索をクリア",
|
||||
"Clear search history": "検索履歴をクリア"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "다음 결과",
|
||||
"Bookmarks": "북마크",
|
||||
"Annotations": "주석",
|
||||
"Show Results": "결과 표시"
|
||||
"Show Results": "결과 표시",
|
||||
"Clear search": "검색 지우기",
|
||||
"Clear search history": "검색 기록 지우기"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -852,5 +852,7 @@
|
||||
"Next Result": "Следующий результат",
|
||||
"Bookmarks": "Закладки",
|
||||
"Annotations": "Аннотации",
|
||||
"Show Results": "Показать результаты"
|
||||
"Show Results": "Показать результаты",
|
||||
"Clear search": "Очистить поиск",
|
||||
"Clear search history": "Очистить историю поиска"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "ඊළඟ ප්රතිඵලය",
|
||||
"Bookmarks": "පොත් සලකුණු",
|
||||
"Annotations": "අනුසටහන්",
|
||||
"Show Results": "ප්රතිඵල පෙන්වන්න"
|
||||
"Show Results": "ප්රතිඵල පෙන්වන්න",
|
||||
"Clear search": "සෙවුම හිස් කරන්න",
|
||||
"Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -832,5 +832,7 @@
|
||||
"Next Result": "அடுத்த முடிவு",
|
||||
"Bookmarks": "புக்மார்க்குகள்",
|
||||
"Annotations": "சிறுகுறிப்புகள்",
|
||||
"Show Results": "முடிவுகளைக் காட்டு"
|
||||
"Show Results": "முடிவுகளைக் காட்டு",
|
||||
"Clear search": "தேடலை அழி",
|
||||
"Clear search history": "தேடல் வரலாற்றை அழி"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "ผลลัพธ์ถัดไป",
|
||||
"Bookmarks": "บุ๊กมาร์ก",
|
||||
"Annotations": "คำอธิบายประกอบ",
|
||||
"Show Results": "แสดงผลลัพธ์"
|
||||
"Show Results": "แสดงผลลัพธ์",
|
||||
"Clear search": "ล้างการค้นหา",
|
||||
"Clear search history": "ล้างประวัติการค้นหา"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -852,5 +852,7 @@
|
||||
"Next Result": "Наступний результат",
|
||||
"Bookmarks": "Закладки",
|
||||
"Annotations": "Анотації",
|
||||
"Show Results": "Показати результати"
|
||||
"Show Results": "Показати результати",
|
||||
"Clear search": "Очистити пошук",
|
||||
"Clear search history": "Очистити історію пошуку"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "下一个结果",
|
||||
"Bookmarks": "书签",
|
||||
"Annotations": "注释",
|
||||
"Show Results": "显示结果"
|
||||
"Show Results": "显示结果",
|
||||
"Clear search": "清除搜索",
|
||||
"Clear search history": "清除搜索历史"
|
||||
}
|
||||
|
||||
@@ -822,5 +822,7 @@
|
||||
"Next Result": "下一個結果",
|
||||
"Bookmarks": "書籤",
|
||||
"Annotations": "註解",
|
||||
"Show Results": "顯示結果"
|
||||
"Show Results": "顯示結果",
|
||||
"Clear search": "清除搜尋",
|
||||
"Clear search history": "清除搜尋歷史"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const BooknotesNav: React.FC<BooknotesNavProps> = ({ bookKey, gridInsets, toc })
|
||||
section={currentSection}
|
||||
hasPrevious={hasPreviousPage}
|
||||
hasNext={hasNextPage}
|
||||
progress={1}
|
||||
previousTitle={_('Previous')}
|
||||
nextTitle={_('Next')}
|
||||
showResultsTitle={getShowResultsTitle()}
|
||||
|
||||
@@ -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<ContentNavBarProps> = ({
|
||||
gridInsets,
|
||||
title,
|
||||
section,
|
||||
progress,
|
||||
showListButton = true,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
@@ -69,20 +71,27 @@ const ContentNavBar: React.FC<ContentNavBarProps> = ({
|
||||
}}
|
||||
>
|
||||
{/* Top bar: Info */}
|
||||
<div className='bg-base-200 pointer-events-auto flex items-center justify-between rounded-xl px-4 py-1 shadow-lg sm:gap-6'>
|
||||
<div className='bg-base-100 pointer-events-auto relative flex items-center justify-between overflow-hidden rounded-xl px-2 py-1 shadow-lg sm:gap-6'>
|
||||
{progress !== undefined && progress < 1 && (
|
||||
<div
|
||||
className='bg-base-200 absolute inset-y-0 left-0 transition-all duration-300'
|
||||
style={{ width: `${progress * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
{progress === 1 && <div className='bg-base-200 absolute inset-0' />}
|
||||
{showListButton && onShowResults ? (
|
||||
<button
|
||||
title={showResultsTitle || _('Show Results')}
|
||||
onClick={onShowResults}
|
||||
className='btn btn-ghost h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
className='btn btn-ghost relative z-10 h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
>
|
||||
<IoIosList size={iconSize20} className='text-base-content' />
|
||||
</button>
|
||||
) : (
|
||||
<div className='w-8' />
|
||||
<div className='relative z-10 w-8' />
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col items-center px-2'>
|
||||
<div className='relative z-10 flex flex-1 flex-col items-center px-2'>
|
||||
<span className='line-clamp-1 text-sm font-medium'>{title}</span>
|
||||
{section && showSection && (
|
||||
<span className='text-base-content/70 line-clamp-1 text-xs'>{section}</span>
|
||||
@@ -92,7 +101,7 @@ const ContentNavBar: React.FC<ContentNavBarProps> = ({
|
||||
<button
|
||||
title={closeTitle || _('Close')}
|
||||
onClick={onClose}
|
||||
className='btn btn-ghost h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
className='btn btn-ghost relative z-10 h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
>
|
||||
<IoMdCloseCircle size={iconSize16} />
|
||||
</button>
|
||||
|
||||
@@ -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<SearchBarProps> = ({ 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<SearchBarProps> = ({ 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<BookSearchResult[] | BookSearchMatch[] | null> => {
|
||||
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<SearchBarProps> = ({ 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<SearchBarProps> = ({ 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<SearchBarProps> = ({ 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<SearchBarProps> = ({ 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<SearchBarProps> = ({ 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<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
||||
toggleButton={<FaChevronDown size={iconSize12} className='text-base-content/50' />}
|
||||
>
|
||||
<SearchOptions
|
||||
searchConfig={searchConfig}
|
||||
searchConfig={config.searchConfig as BookSearchConfig}
|
||||
onSearchConfigChanged={handleSearchConfigChange}
|
||||
/>
|
||||
</Dropdown>
|
||||
@@ -287,9 +378,9 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleHistoryClick(term)}
|
||||
className='hover:bg-base-content/20 text-base-content/70 bg-base-100 flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
|
||||
className='hover:bg-base-content/20 text-base-content/70 bg-base-100 max-w-[60%] flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
|
||||
>
|
||||
{term}
|
||||
<p className='truncate'>{term}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -303,8 +394,8 @@ const SearchBar: React.FC<SearchBarProps> = ({ 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')}
|
||||
>
|
||||
<MdDeleteOutline size={iconSize16} />
|
||||
</button>
|
||||
|
||||
@@ -14,6 +14,7 @@ interface SearchResultsNavProps {
|
||||
const SearchResultsNav: React.FC<SearchResultsNavProps> = ({ bookKey, gridInsets }) => {
|
||||
const {
|
||||
searchTerm,
|
||||
searchProgress,
|
||||
currentSection,
|
||||
showSearchNav,
|
||||
hasPreviousPage,
|
||||
@@ -35,6 +36,7 @@ const SearchResultsNav: React.FC<SearchResultsNavProps> = ({ bookKey, gridInsets
|
||||
gridInsets={gridInsets}
|
||||
title={_("Search results for '{{term}}'", { term: searchTerm })}
|
||||
section={currentSection}
|
||||
progress={searchProgress}
|
||||
hasPrevious={hasPreviousPage}
|
||||
hasNext={hasNextPage}
|
||||
previousTitle={_('Previous Result')}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<void>((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);
|
||||
|
||||
@@ -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<SidebarState>((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<SidebarState>((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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface AppService {
|
||||
init(): Promise<void>;
|
||||
openFile(path: string, base: BaseDir): Promise<File>;
|
||||
copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void>;
|
||||
readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise<string | ArrayBuffer>;
|
||||
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File): Promise<void>;
|
||||
createDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
|
||||
deleteFile(path: string, base: BaseDir): Promise<void>;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: bab0bccbe9...4eb5ca42a4
Reference in New Issue
Block a user