forked from akai/readest
fix(search): should be able to terminate search when sidebar is invisible (#2863)
This commit is contained in:
@@ -264,11 +264,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
|||||||
) : (
|
) : (
|
||||||
<MenuItem label={_('Sign In')} Icon={PiUserCircle} onClick={handleUserLogin}></MenuItem>
|
<MenuItem label={_('Sign In')} Icon={PiUserCircle} onClick={handleUserLogin}></MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
|
||||||
label={_('Auto Upload Books to Cloud')}
|
|
||||||
toggled={isAutoUpload}
|
|
||||||
onClick={toggleAutoUploadBooks}
|
|
||||||
/>
|
|
||||||
{user && (
|
{user && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label={_('Cloud File Transfers')}
|
label={_('Cloud File Transfers')}
|
||||||
@@ -286,6 +281,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
|||||||
onClick={openTransferQueue}
|
onClick={openTransferQueue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<MenuItem
|
||||||
|
label={_('Auto Upload Books to Cloud')}
|
||||||
|
toggled={isAutoUpload}
|
||||||
|
onClick={toggleAutoUploadBooks}
|
||||||
|
/>
|
||||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label={_('Auto Import on File Open')}
|
label={_('Auto Import on File Open')}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
|||||||
const { getBookData } = useBookDataStore();
|
const { getBookData } = useBookDataStore();
|
||||||
const { getConfig, setConfig, saveConfig } = useBookDataStore();
|
const { getConfig, setConfig, saveConfig } = useBookDataStore();
|
||||||
const { getView, getProgress } = useReaderStore();
|
const { getView, getProgress } = useReaderStore();
|
||||||
const { getSearchNavState, setSearchTerm, setSearchResults, setSearchProgress } =
|
const { setSearchTerm, setSearchResults, setSearchProgress } = useSidebarStore();
|
||||||
useSidebarStore();
|
const { getSearchNavState, getSearchStatus, setSearchStatus } = useSidebarStore();
|
||||||
const searchNavState = getSearchNavState(bookKey);
|
const searchNavState = getSearchNavState(bookKey);
|
||||||
|
|
||||||
const { searchTerm } = searchNavState;
|
const { searchTerm } = searchNavState;
|
||||||
@@ -65,14 +65,12 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
|||||||
|
|
||||||
const addToHistory = useCallback(
|
const addToHistory = useCallback(
|
||||||
(term: string) => {
|
(term: string) => {
|
||||||
setSearchHistory((prev) => {
|
const filtered = searchHistory.filter((t) => t !== term);
|
||||||
const filtered = prev.filter((t) => t !== term);
|
const updated = [term, ...filtered].slice(0, MAX_SEARCH_HISTORY);
|
||||||
const updated = [term, ...filtered].slice(0, MAX_SEARCH_HISTORY);
|
localStorage.setItem(historyStorageKey, JSON.stringify(updated));
|
||||||
localStorage.setItem(historyStorageKey, JSON.stringify(updated));
|
setSearchHistory(updated);
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[historyStorageKey],
|
[historyStorageKey, searchHistory],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHistoryClick = (term: string) => {
|
const handleHistoryClick = (term: string) => {
|
||||||
@@ -176,13 +174,15 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
|||||||
inputRef.current.onfocus = () => {
|
inputRef.current.onfocus = () => {
|
||||||
inputFocusedRef.current = true;
|
inputFocusedRef.current = true;
|
||||||
};
|
};
|
||||||
inputRef.current.focus();
|
if (!appService?.isMobile) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isVisible && searchTerm) {
|
if (isVisible && searchTerm) {
|
||||||
handleSearchTermChange(searchTerm);
|
handleSearchTermChange(searchTerm);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isVisible]);
|
}, [appService, isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -236,6 +236,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
|||||||
|
|
||||||
// Reset progress at start of search
|
// Reset progress at start of search
|
||||||
setSearchProgress(bookKey, 0);
|
setSearchProgress(bookKey, 0);
|
||||||
|
setSearchStatus(bookKey, 'searching');
|
||||||
|
|
||||||
const { section } = progress;
|
const { section } = progress;
|
||||||
const index = searchConfig.scope === 'section' ? section.current : undefined;
|
const index = searchConfig.scope === 'section' ? section.current : undefined;
|
||||||
@@ -253,8 +254,13 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
|||||||
|
|
||||||
const processResults = async () => {
|
const processResults = async () => {
|
||||||
for await (const result of generator) {
|
for await (const result of generator) {
|
||||||
|
if (getSearchStatus(bookKey) === 'terminated') {
|
||||||
|
console.log('search terminated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
if (result === 'done') {
|
if (result === 'done') {
|
||||||
|
setSearchStatus(bookKey, 'completed');
|
||||||
setSearchResults(bookKey, [...results]);
|
setSearchResults(bookKey, [...results]);
|
||||||
setSearchProgress(bookKey, 1);
|
setSearchProgress(bookKey, 1);
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { flattenSearchResults } from '../components/sidebar/SearchResultsNav';
|
|||||||
|
|
||||||
export function useSearchNav(bookKey: string) {
|
export function useSearchNav(bookKey: string) {
|
||||||
const { getView, getProgress } = useReaderStore();
|
const { getView, getProgress } = useReaderStore();
|
||||||
const { setSideBarVisible, getSearchNavState, setSearchResultIndex, clearSearch } =
|
const { setSideBarVisible } = useSidebarStore();
|
||||||
useSidebarStore();
|
const { getSearchNavState, setSearchResultIndex, clearSearch } = useSidebarStore();
|
||||||
|
|
||||||
const searchNavState = getSearchNavState(bookKey);
|
const searchNavState = getSearchNavState(bookKey);
|
||||||
const { searchTerm, searchResults, searchResultIndex, searchProgress } = searchNavState;
|
const { searchTerm, searchResults, searchResultIndex, searchProgress } = searchNavState;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useEnv } from '@/context/EnvContext';
|
import { useEnv } from '@/context/EnvContext';
|
||||||
import { useReaderStore } from '@/store/readerStore';
|
import { useReaderStore } from '@/store/readerStore';
|
||||||
import { useBookDataStore } from '@/store/bookDataStore';
|
import { useBookDataStore } from '@/store/bookDataStore';
|
||||||
@@ -27,7 +27,6 @@ export const useTextSelector = (
|
|||||||
const isTouchStarted = useRef(false);
|
const isTouchStarted = useRef(false);
|
||||||
const selectionPosition = useRef<number | null>(null);
|
const selectionPosition = useRef<number | null>(null);
|
||||||
const lastPointerType = useRef<string>('mouse');
|
const lastPointerType = useRef<string>('mouse');
|
||||||
const [textSelected, setTextSelected] = useState(false);
|
|
||||||
|
|
||||||
const isValidSelection = (sel: Selection) => {
|
const isValidSelection = (sel: Selection) => {
|
||||||
return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0;
|
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) => {
|
const makeSelection = async (sel: Selection, index: number, rebuildRange = false) => {
|
||||||
isTextSelected.current = true;
|
isTextSelected.current = true;
|
||||||
setTextSelected(true);
|
isUpToPopup.current = true;
|
||||||
const range = sel.getRangeAt(0);
|
const range = sel.getRangeAt(0);
|
||||||
if (rebuildRange) {
|
if (rebuildRange) {
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
@@ -64,7 +63,7 @@ export const useTextSelector = (
|
|||||||
// FIXME: extremely hacky way to dismiss system selection tools on iOS
|
// FIXME: extremely hacky way to dismiss system selection tools on iOS
|
||||||
const makeSelectionOnIOS = async (sel: Selection, index: number) => {
|
const makeSelectionOnIOS = async (sel: Selection, index: number) => {
|
||||||
isTextSelected.current = true;
|
isTextSelected.current = true;
|
||||||
setTextSelected(true);
|
isUpToPopup.current = true;
|
||||||
const range = sel.getRangeAt(0);
|
const range = sel.getRangeAt(0);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
@@ -81,25 +80,6 @@ export const useTextSelector = (
|
|||||||
}, 30);
|
}, 30);
|
||||||
}, 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) => {
|
const isPointerInsideSelection = (selection: Selection, ev: PointerEvent) => {
|
||||||
if (selection.rangeCount === 0) return false;
|
if (selection.rangeCount === 0) return false;
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
@@ -143,18 +123,38 @@ export const useTextSelector = (
|
|||||||
const handleTouchEnd = () => {
|
const handleTouchEnd = () => {
|
||||||
isTouchStarted.current = false;
|
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 = () => {
|
const handleScroll = () => {
|
||||||
// Prevent the container from scrolling when text is selected in paginated mode
|
// Prevent the container from scrolling when text is selected in paginated mode
|
||||||
// FIXME: this is a workaround for issue #873
|
// FIXME: this is a workaround for issue #873
|
||||||
// TODO: support text selection across pages
|
// TODO: support text selection across pages
|
||||||
|
if (osPlatform !== 'android' || !appService?.isAndroidApp) return;
|
||||||
|
|
||||||
const viewSettings = getViewSettings(bookKey);
|
const viewSettings = getViewSettings(bookKey);
|
||||||
if (
|
if (viewSettings?.scrolled) return;
|
||||||
appService?.isAndroidApp &&
|
|
||||||
isTextSelected.current &&
|
if (isTextSelected.current && view?.renderer?.containerPosition && selectionPosition.current) {
|
||||||
!viewSettings?.scrolled &&
|
|
||||||
view?.renderer?.containerPosition &&
|
|
||||||
selectionPosition.current
|
|
||||||
) {
|
|
||||||
console.warn('Keep container position', selectionPosition.current);
|
console.warn('Keep container position', selectionPosition.current);
|
||||||
view.renderer.containerPosition = selectionPosition.current;
|
view.renderer.containerPosition = selectionPosition.current;
|
||||||
}
|
}
|
||||||
@@ -189,15 +189,6 @@ export const useTextSelector = (
|
|||||||
return;
|
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(() => {
|
useEffect(() => {
|
||||||
const handleSingleClick = (): boolean => {
|
const handleSingleClick = (): boolean => {
|
||||||
if (isUpToPopup.current) {
|
if (isUpToPopup.current) {
|
||||||
@@ -207,7 +198,6 @@ export const useTextSelector = (
|
|||||||
if (isTextSelected.current) {
|
if (isTextSelected.current) {
|
||||||
handleDismissPopup();
|
handleDismissPopup();
|
||||||
isTextSelected.current = false;
|
isTextSelected.current = false;
|
||||||
setTextSelected(false);
|
|
||||||
view?.deselect();
|
view?.deselect();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { BookNote, BookNoteType, BookSearchMatch, BookSearchResult } from '@/types/book';
|
import { BookNote, BookNoteType, BookSearchMatch, BookSearchResult } from '@/types/book';
|
||||||
|
|
||||||
|
type SearchStatus = 'searching' | 'completed' | 'terminated';
|
||||||
|
|
||||||
// Per-book search navigation state
|
// Per-book search navigation state
|
||||||
interface SearchNavState {
|
interface SearchNavState {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
@@ -24,6 +26,7 @@ interface SidebarState {
|
|||||||
// Per-book navigation states
|
// Per-book navigation states
|
||||||
searchNavStates: Record<string, SearchNavState>;
|
searchNavStates: Record<string, SearchNavState>;
|
||||||
booknotesNavStates: Record<string, BooknotesNavState>;
|
booknotesNavStates: Record<string, BooknotesNavState>;
|
||||||
|
searchStatuses: Record<string, SearchStatus>;
|
||||||
getIsSideBarVisible: () => boolean;
|
getIsSideBarVisible: () => boolean;
|
||||||
getSideBarWidth: () => string;
|
getSideBarWidth: () => string;
|
||||||
setSideBarBookKey: (key: string) => void;
|
setSideBarBookKey: (key: string) => void;
|
||||||
@@ -35,6 +38,8 @@ interface SidebarState {
|
|||||||
// Search actions (per bookKey)
|
// Search actions (per bookKey)
|
||||||
getSearchNavState: (bookKey: string) => SearchNavState;
|
getSearchNavState: (bookKey: string) => SearchNavState;
|
||||||
setSearchTerm: (bookKey: string, term: string) => void;
|
setSearchTerm: (bookKey: string, term: string) => void;
|
||||||
|
setSearchStatus: (bookKey: string, status: SearchStatus) => void;
|
||||||
|
getSearchStatus: (bookKey: string) => SearchStatus | null;
|
||||||
setSearchResults: (
|
setSearchResults: (
|
||||||
bookKey: string,
|
bookKey: string,
|
||||||
results: BookSearchResult[] | BookSearchMatch[] | null,
|
results: BookSearchResult[] | BookSearchMatch[] | null,
|
||||||
@@ -71,6 +76,7 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
|
|||||||
// Per-book navigation states
|
// Per-book navigation states
|
||||||
searchNavStates: {},
|
searchNavStates: {},
|
||||||
booknotesNavStates: {},
|
booknotesNavStates: {},
|
||||||
|
searchStatuses: {},
|
||||||
getIsSideBarVisible: () => get().isSideBarVisible,
|
getIsSideBarVisible: () => get().isSideBarVisible,
|
||||||
getSideBarWidth: () => get().sideBarWidth,
|
getSideBarWidth: () => get().sideBarWidth,
|
||||||
setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }),
|
setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }),
|
||||||
@@ -80,6 +86,9 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
|
|||||||
setSideBarVisible: (visible: boolean) => set({ isSideBarVisible: visible }),
|
setSideBarVisible: (visible: boolean) => set({ isSideBarVisible: visible }),
|
||||||
setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }),
|
setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }),
|
||||||
// Search actions
|
// Search actions
|
||||||
|
getSearchStatus: (bookKey: string) => {
|
||||||
|
return get().searchStatuses[bookKey] || null;
|
||||||
|
},
|
||||||
getSearchNavState: (bookKey: string) => {
|
getSearchNavState: (bookKey: string) => {
|
||||||
return get().searchNavStates[bookKey] || defaultSearchNavState;
|
return get().searchNavStates[bookKey] || defaultSearchNavState;
|
||||||
},
|
},
|
||||||
@@ -129,6 +138,17 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
|
|||||||
...state.searchNavStates,
|
...state.searchNavStates,
|
||||||
[bookKey]: { ...defaultSearchNavState },
|
[bookKey]: { ...defaultSearchNavState },
|
||||||
},
|
},
|
||||||
|
searchStatuses: {
|
||||||
|
...state.searchStatuses,
|
||||||
|
[bookKey]: 'terminated',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
setSearchStatus: (bookKey: string, status: SearchStatus) =>
|
||||||
|
set((state) => ({
|
||||||
|
searchStatuses: {
|
||||||
|
...state.searchStatuses,
|
||||||
|
[bookKey]: status,
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
// Booknotes navigation actions
|
// Booknotes navigation actions
|
||||||
getBooknotesNavState: (bookKey: string) => {
|
getBooknotesNavState: (bookKey: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user