From 483d536ca4fecaa4b77d682dbf716c457d6afd5d Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 4 Jan 2026 07:47:43 +0100 Subject: [PATCH] feat(search): support search terms history, closes #2836 (#2859) --- .../reader/components/sidebar/SearchBar.tsx | 112 +++++++++++++++++- apps/readest-app/src/styles/globals.css | 2 +- 2 files changed, 107 insertions(+), 7 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 ffd6f409..eaf2759c 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx @@ -1,6 +1,8 @@ import clsx from 'clsx'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FaSearch, FaChevronDown } from 'react-icons/fa'; +import { IoMdCloseCircle } from 'react-icons/io'; +import { MdDeleteOutline } from 'react-icons/md'; import { useEnv } from '@/context/EnvContext'; import { useSettingsStore } from '@/store/settingsStore'; @@ -18,6 +20,8 @@ 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 MAX_SEARCH_HISTORY = 10; interface SearchBarProps { isVisible: boolean; @@ -34,11 +38,56 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const { getView, getProgress } = useReaderStore(); const { getSearchNavState, setSearchTerm, setSearchResults } = useSidebarStore(); const searchNavState = getSearchNavState(bookKey); + const { searchTerm } = searchNavState; const queuedSearchTerm = useRef(''); const inputRef = useRef(null); const inputFocusedRef = useRef(false); + const bookHash = useMemo(() => bookKey.split('-')[0]!, [bookKey]); + const historyStorageKey = useMemo(() => `${SEARCH_HISTORY_KEY}-${bookHash}`, [bookHash]); + + const [searchHistory, setSearchHistory] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem(historyStorageKey); + return saved ? JSON.parse(saved) : []; + } + return []; + }); + + useEffect(() => { + const saved = localStorage.getItem(historyStorageKey); + setSearchHistory(saved ? JSON.parse(saved) : []); + }, [historyStorageKey]); + + const addToHistory = useCallback( + (term: string) => { + setSearchHistory((prev) => { + const filtered = prev.filter((t) => t !== term); + const updated = [term, ...filtered].slice(0, MAX_SEARCH_HISTORY); + localStorage.setItem(historyStorageKey, JSON.stringify(updated)); + return updated; + }); + }, + [historyStorageKey], + ); + + const handleHistoryClick = (term: string) => { + setSearchTerm(bookKey, term); + handleSearchTermChange(term); + }; + + const handleClearInput = () => { + setSearchTerm(bookKey, ''); + resetSearch(); + inputRef.current?.focus(); + }; + + const handleClearHistory = () => { + setSearchHistory([]); + localStorage.removeItem(historyStorageKey); + }; + const view = getView(bookKey)!; const config = getConfig(bookKey)!; const bookData = getBookData(bookKey)!; @@ -90,7 +139,6 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const value = e.target.value; setSearchTerm(bookKey, value); handleSearchTermChange(value); - queuedSearchTerm.current = value; }; const handleSearchConfigChange = (searchConfig: BookSearchConfig) => { @@ -128,6 +176,9 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB if (typeof result === 'string') { if (result === 'done') { setSearchResults(bookKey, [...results]); + if (results.length > 0) { + addToHistory(term); + } console.log('search done'); } } else { @@ -137,7 +188,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB console.log('search progress:', result.progress); lastProgressLogTime = now; } - if (queuedSearchTerm.current && queuedSearchTerm.current !== term) { + if (queuedSearchTerm.current !== term) { console.log('search term changed, resetting search'); resetSearch(); return; @@ -155,7 +206,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB processResults(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [progress, searchConfig, setSearchResults], + [progress, searchConfig, setSearchResults, addToHistory], ); const resetSearch = useCallback(() => { @@ -166,6 +217,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB // eslint-disable-next-line react-hooks/exhaustive-deps const handleSearchTermChange = useCallback( debounce((term: string) => { + queuedSearchTerm.current = term; if (exceedMinSearchTermLength(term)) { handleSearch(term); } else { @@ -176,7 +228,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB ); return ( -
+
@@ -189,9 +241,19 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB spellCheck={false} onChange={handleInputChange} placeholder={_('Search...')} - className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none' + className='w-full bg-transparent p-2 pr-0 font-sans text-sm font-light focus:outline-none' /> + {searchTerm && ( + + )} +
= ({ isVisible, bookKey, onHideSearchB
+ + {searchHistory.length > 0 && !searchTerm && ( +
+ ); }; diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css index 81c30cf2..a0951aaa 100644 --- a/apps/readest-app/src/styles/globals.css +++ b/apps/readest-app/src/styles/globals.css @@ -268,7 +268,7 @@ foliate-fxl { } .search-bar-visible { - max-height: 48px; + max-height: 100%; visibility: visible; }