perf(search): use cache for search results (#2861)

This commit is contained in:
Huang Xin
2026-01-04 12:56:40 +01:00
committed by GitHub
parent 483d536ca4
commit 604ef65719
38 changed files with 259 additions and 61 deletions
@@ -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);
}
+23 -4
View File
@@ -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);
+20 -4
View File
@@ -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: {
+4
View File
@@ -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;
+1
View File
@@ -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>;