From 38869ce438fe7a4ed2624db5bca537cc71947a4f Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 9 Jul 2025 18:36:22 +0800 Subject: [PATCH] search: more responsive full-text search, closes #1558 (#1562) --- .../reader/components/sidebar/SearchBar.tsx | 133 +++++++++--------- .../src/components/UpdaterWindow.tsx | 1 + 2 files changed, 69 insertions(+), 65 deletions(-) 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 f576b84f..4cc6a391 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FaSearch, FaChevronDown } from 'react-icons/fa'; import { useEnv } from '@/context/EnvContext'; @@ -9,6 +9,7 @@ import { useReaderStore } from '@/store/readerStore'; import { useTranslation } from '@/hooks/useTranslation'; import { BookSearchConfig, BookSearchResult } from '@/types/book'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; +import { debounce } from '@/utils/debounce'; import { isCJKStr } from '@/utils/lang'; import Dropdown from '@/components/Dropdown'; import SearchOptions from './SearchOptions'; @@ -38,6 +39,7 @@ const SearchBar: React.FC = ({ const { getConfig, saveConfig } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); const [searchTerm, setSearchTerm] = useState(term); + const queuedSearchTerm = useRef(''); const inputRef = useRef(null); const inputFocusedRef = useRef(false); @@ -48,10 +50,6 @@ const SearchBar: React.FC = ({ const primaryLang = bookData.book?.primaryLanguage || 'en'; const searchConfig = config.searchConfig! as BookSearchConfig; - const queuedSearchTerm = useRef(''); - const isSearchPending = useRef(false); - const searchTimeout = useRef | null>(null); - const iconSize12 = useResponsiveSize(12); const iconSize16 = useResponsiveSize(16); @@ -95,27 +93,14 @@ const SearchBar: React.FC = ({ window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); - if (searchTimeout.current) { - clearTimeout(searchTimeout.current); - } }; }, [onHideSearchBar]); const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setSearchTerm(value); - - if (searchTimeout.current) { - clearTimeout(searchTimeout.current); - } - - searchTimeout.current = setTimeout(() => { - if (!isSearchPending.current) { - handleSearchTermChange(value); - } else { - queuedSearchTerm.current = value; - } - }, 500); + handleSearchTermChange(value); + queuedSearchTerm.current = value; }; const handleSearchConfigChange = (searchConfig: BookSearchConfig) => { @@ -132,14 +117,6 @@ const SearchBar: React.FC = ({ return searchTerm.length >= minLength; }; - const handleSearchTermChange = (term: string) => { - if (exceedMinSearchTermLength(term)) { - handleSearch(term); - } else { - resetSearch(); - } - }; - const createAcceptNode = ({ withRT = true } = {}) => { return (node: Node): number => { if (node.nodeType === Node.ELEMENT_NODE) { @@ -153,46 +130,72 @@ const SearchBar: React.FC = ({ }; }; - const handleSearch = async (term: string) => { - console.log('searching for:', term); - isSearchPending.current = true; - const { section } = progress; - const index = searchConfig.scope === 'section' ? section.current : undefined; - const generator = await view.search({ - ...searchConfig, - index, - query: term, - acceptNode: createAcceptNode({ withRT: !primaryLang.startsWith('ja') }), - }); - const results: BookSearchResult[] = []; - for await (const result of generator) { - if (typeof result === 'string') { - if (result === 'done') { - onSearchResultChange([...results]); - isSearchPending.current = false; - console.log('search done'); - if ( - queuedSearchTerm.current !== term && - exceedMinSearchTermLength(queuedSearchTerm.current) - ) { - handleSearch(queuedSearchTerm.current); - } - } - } else { - if (result.progress) { - //console.log('search progress:', result.progress); - } else { - results.push(result); - onSearchResultChange([...results]); - } - } - } - }; + const handleSearch = useCallback( + async (term: string) => { + console.log('searching for:', term); + const { section } = progress; + const index = searchConfig.scope === 'section' ? section.current : undefined; + const generator = await view.search({ + ...searchConfig, + index, + query: term, + acceptNode: createAcceptNode({ withRT: !primaryLang.startsWith('ja') }), + }); + const results: BookSearchResult[] = []; + let lastProgressLogTime = 0; - const resetSearch = () => { + const processResults = async () => { + for await (const result of generator) { + if (typeof result === 'string') { + if (result === 'done') { + onSearchResultChange([...results]); + console.log('search done'); + } + } else { + if (result.progress) { + const now = Date.now(); + if (now - lastProgressLogTime >= 1000) { + console.log('search progress:', result.progress); + lastProgressLogTime = now; + } + if (queuedSearchTerm.current !== term) { + console.log('search term changed, resetting search'); + resetSearch(); + return; + } + } else { + results.push(result); + onSearchResultChange([...results]); + } + } + + await new Promise((resolve) => setTimeout(resolve, 0)); + } + }; + + processResults(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [progress, searchConfig], + ); + + const resetSearch = useCallback(() => { onSearchResultChange([]); view?.clearSearch(); - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [view]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const handleSearchTermChange = useCallback( + debounce((term: string) => { + if (exceedMinSearchTermLength(term)) { + handleSearch(term); + } else { + resetSearch(); + } + }, 500), + [handleSearch, resetSearch], + ); return (
diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx index 9d2e59ae..c3a57925 100644 --- a/apps/readest-app/src/components/UpdaterWindow.tsx +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -193,6 +193,7 @@ export const UpdaterContent = ({ body: '', } as GenericUpdate); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [latestVersion, lastVersion, checkUpdate]); useEffect(() => {