From eed84a059a50cdb81cfca91fb310a5aebc7e1fc3 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 4 Jan 2026 17:10:49 +0100 Subject: [PATCH] fix(search): should be able to terminate search when sidebar is invisible (#2863) --- .../app/library/components/SettingsMenu.tsx | 10 +-- .../reader/components/sidebar/SearchBar.tsx | 28 +++++--- .../src/app/reader/hooks/useSearchNav.ts | 4 +- .../src/app/reader/hooks/useTextSelector.ts | 70 ++++++++----------- apps/readest-app/src/store/sidebarStore.ts | 20 ++++++ 5 files changed, 74 insertions(+), 58 deletions(-) diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx index c5402d80..5d3d7f16 100644 --- a/apps/readest-app/src/app/library/components/SettingsMenu.tsx +++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx @@ -264,11 +264,6 @@ const SettingsMenu: React.FC = ({ setIsDropdownOpen }) => { ) : ( )} - {user && ( = ({ setIsDropdownOpen }) => { onClick={openTransferQueue} /> )} + {isTauriAppPlatform() && !appService?.isMobile && ( = ({ isVisible, bookKey, onHideSearchB const { getBookData } = useBookDataStore(); const { getConfig, setConfig, saveConfig } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); - const { getSearchNavState, setSearchTerm, setSearchResults, setSearchProgress } = - useSidebarStore(); + const { setSearchTerm, setSearchResults, setSearchProgress } = useSidebarStore(); + const { getSearchNavState, getSearchStatus, setSearchStatus } = useSidebarStore(); const searchNavState = getSearchNavState(bookKey); const { searchTerm } = searchNavState; @@ -65,14 +65,12 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB 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; - }); + const filtered = searchHistory.filter((t) => t !== term); + const updated = [term, ...filtered].slice(0, MAX_SEARCH_HISTORY); + localStorage.setItem(historyStorageKey, JSON.stringify(updated)); + setSearchHistory(updated); }, - [historyStorageKey], + [historyStorageKey, searchHistory], ); const handleHistoryClick = (term: string) => { @@ -176,13 +174,15 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB inputRef.current.onfocus = () => { inputFocusedRef.current = true; }; - inputRef.current.focus(); + if (!appService?.isMobile) { + inputRef.current.focus(); + } } if (isVisible && searchTerm) { handleSearchTermChange(searchTerm); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible]); + }, [appService, isVisible]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -236,6 +236,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB // Reset progress at start of search setSearchProgress(bookKey, 0); + setSearchStatus(bookKey, 'searching'); const { section } = progress; const index = searchConfig.scope === 'section' ? section.current : undefined; @@ -253,8 +254,13 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const processResults = async () => { for await (const result of generator) { + if (getSearchStatus(bookKey) === 'terminated') { + console.log('search terminated'); + return; + } if (typeof result === 'string') { if (result === 'done') { + setSearchStatus(bookKey, 'completed'); setSearchResults(bookKey, [...results]); setSearchProgress(bookKey, 1); if (results.length > 0) { diff --git a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts index 7d484ab8..a8e8ab38 100644 --- a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts +++ b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts @@ -6,8 +6,8 @@ import { flattenSearchResults } from '../components/sidebar/SearchResultsNav'; export function useSearchNav(bookKey: string) { const { getView, getProgress } = useReaderStore(); - const { setSideBarVisible, getSearchNavState, setSearchResultIndex, clearSearch } = - useSidebarStore(); + const { setSideBarVisible } = useSidebarStore(); + const { getSearchNavState, setSearchResultIndex, clearSearch } = useSidebarStore(); const searchNavState = getSearchNavState(bookKey); const { searchTerm, searchResults, searchResultIndex, searchProgress } = searchNavState; diff --git a/apps/readest-app/src/app/reader/hooks/useTextSelector.ts b/apps/readest-app/src/app/reader/hooks/useTextSelector.ts index 557ea1e4..fd4ffafe 100644 --- a/apps/readest-app/src/app/reader/hooks/useTextSelector.ts +++ b/apps/readest-app/src/app/reader/hooks/useTextSelector.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useBookDataStore } from '@/store/bookDataStore'; @@ -27,7 +27,6 @@ export const useTextSelector = ( const isTouchStarted = useRef(false); const selectionPosition = useRef(null); const lastPointerType = useRef('mouse'); - const [textSelected, setTextSelected] = useState(false); const isValidSelection = (sel: Selection) => { return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0; @@ -47,7 +46,7 @@ export const useTextSelector = ( }; const makeSelection = async (sel: Selection, index: number, rebuildRange = false) => { isTextSelected.current = true; - setTextSelected(true); + isUpToPopup.current = true; const range = sel.getRangeAt(0); if (rebuildRange) { sel.removeAllRanges(); @@ -64,7 +63,7 @@ export const useTextSelector = ( // FIXME: extremely hacky way to dismiss system selection tools on iOS const makeSelectionOnIOS = async (sel: Selection, index: number) => { isTextSelected.current = true; - setTextSelected(true); + isUpToPopup.current = true; const range = sel.getRangeAt(0); setTimeout(() => { sel.removeAllRanges(); @@ -81,25 +80,6 @@ export const useTextSelector = ( }, 30); }, 30); }; - const handleSelectionchange = (doc: Document) => { - // Available on iOS, Android and Desktop, fired when the selection is changed - // Ideally the popup only shows when the selection is done, - const sel = doc.getSelection() as Selection; - if (osPlatform === 'ios' || appService?.isIOSApp) return; - if (!isValidSelection(sel)) { - if (!isUpToPopup.current) { - handleDismissPopup(); - isTextSelected.current = false; - setTextSelected(false); - } - if (isPopuped.current) { - isUpToPopup.current = false; - } - return; - } - - isUpToPopup.current = true; - }; const isPointerInsideSelection = (selection: Selection, ev: PointerEvent) => { if (selection.rangeCount === 0) return false; const range = selection.getRangeAt(0); @@ -143,18 +123,38 @@ export const useTextSelector = ( const handleTouchEnd = () => { isTouchStarted.current = false; }; + const handleSelectionchange = (doc: Document) => { + // Available on iOS, Android and Desktop, fired when the selection is changed + if (osPlatform !== 'android' || !appService?.isAndroidApp) return; + + const sel = doc.getSelection() as Selection; + if (isValidSelection(sel)) { + isTextSelected.current = true; + isUpToPopup.current = true; + if (!selectionPosition.current) { + selectionPosition.current = view?.renderer?.start || null; + } + } else { + if (!isUpToPopup.current) { + handleDismissPopup(); + isTextSelected.current = false; + } + if (isPopuped.current) { + isUpToPopup.current = false; + } + selectionPosition.current = null; + } + }; const handleScroll = () => { // Prevent the container from scrolling when text is selected in paginated mode // FIXME: this is a workaround for issue #873 // TODO: support text selection across pages + if (osPlatform !== 'android' || !appService?.isAndroidApp) return; + const viewSettings = getViewSettings(bookKey); - if ( - appService?.isAndroidApp && - isTextSelected.current && - !viewSettings?.scrolled && - view?.renderer?.containerPosition && - selectionPosition.current - ) { + if (viewSettings?.scrolled) return; + + if (isTextSelected.current && view?.renderer?.containerPosition && selectionPosition.current) { console.warn('Keep container position', selectionPosition.current); view.renderer.containerPosition = selectionPosition.current; } @@ -189,15 +189,6 @@ export const useTextSelector = ( return; }; - useEffect(() => { - if (isTextSelected.current && !selectionPosition.current) { - selectionPosition.current = view?.renderer?.start || null; - } else if (!isTextSelected.current) { - selectionPosition.current = null; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textSelected]); - useEffect(() => { const handleSingleClick = (): boolean => { if (isUpToPopup.current) { @@ -207,7 +198,6 @@ export const useTextSelector = ( if (isTextSelected.current) { handleDismissPopup(); isTextSelected.current = false; - setTextSelected(false); view?.deselect(); return true; } diff --git a/apps/readest-app/src/store/sidebarStore.ts b/apps/readest-app/src/store/sidebarStore.ts index e8d480af..a52d0b19 100644 --- a/apps/readest-app/src/store/sidebarStore.ts +++ b/apps/readest-app/src/store/sidebarStore.ts @@ -1,6 +1,8 @@ import { create } from 'zustand'; import { BookNote, BookNoteType, BookSearchMatch, BookSearchResult } from '@/types/book'; +type SearchStatus = 'searching' | 'completed' | 'terminated'; + // Per-book search navigation state interface SearchNavState { searchTerm: string; @@ -24,6 +26,7 @@ interface SidebarState { // Per-book navigation states searchNavStates: Record; booknotesNavStates: Record; + searchStatuses: Record; getIsSideBarVisible: () => boolean; getSideBarWidth: () => string; setSideBarBookKey: (key: string) => void; @@ -35,6 +38,8 @@ interface SidebarState { // Search actions (per bookKey) getSearchNavState: (bookKey: string) => SearchNavState; setSearchTerm: (bookKey: string, term: string) => void; + setSearchStatus: (bookKey: string, status: SearchStatus) => void; + getSearchStatus: (bookKey: string) => SearchStatus | null; setSearchResults: ( bookKey: string, results: BookSearchResult[] | BookSearchMatch[] | null, @@ -71,6 +76,7 @@ export const useSidebarStore = create((set, get) => ({ // Per-book navigation states searchNavStates: {}, booknotesNavStates: {}, + searchStatuses: {}, getIsSideBarVisible: () => get().isSideBarVisible, getSideBarWidth: () => get().sideBarWidth, setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }), @@ -80,6 +86,9 @@ export const useSidebarStore = create((set, get) => ({ setSideBarVisible: (visible: boolean) => set({ isSideBarVisible: visible }), setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }), // Search actions + getSearchStatus: (bookKey: string) => { + return get().searchStatuses[bookKey] || null; + }, getSearchNavState: (bookKey: string) => { return get().searchNavStates[bookKey] || defaultSearchNavState; }, @@ -129,6 +138,17 @@ export const useSidebarStore = create((set, get) => ({ ...state.searchNavStates, [bookKey]: { ...defaultSearchNavState }, }, + searchStatuses: { + ...state.searchStatuses, + [bookKey]: 'terminated', + }, + })), + setSearchStatus: (bookKey: string, status: SearchStatus) => + set((state) => ({ + searchStatuses: { + ...state.searchStatuses, + [bookKey]: status, + }, })), // Booknotes navigation actions getBooknotesNavState: (bookKey: string) => {