diff --git a/apps/readest-app/src/app/reader/components/FootnotePopup.tsx b/apps/readest-app/src/app/reader/components/FootnotePopup.tsx index 20833306..5ccb3cda 100644 --- a/apps/readest-app/src/app/reader/components/FootnotePopup.tsx +++ b/apps/readest-app/src/app/reader/components/FootnotePopup.tsx @@ -37,6 +37,7 @@ const FootnotePopup: React.FC = ({ bookKey, bookDoc }) => { const view = getView(bookKey); const viewSettings = getViewSettings(bookKey)!; const footnoteHandler = new FootnoteHandler(); + const containerRef = useRef(null); const [gridRect, setGridRect] = useState(null); const [responsiveWidth, setResponsiveWidth] = useState(popupWidth); @@ -115,6 +116,12 @@ const FootnotePopup: React.FC = ({ bookKey, bookDoc }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [view]); + useEffect(() => { + if (showPopup) { + containerRef.current?.focus(); + } + }, [showPopup]); + useEffect(() => { if (viewSettings.vertical) { setResponsiveWidth(popupHeight); @@ -231,14 +238,16 @@ const FootnotePopup: React.FC = ({ bookKey, bookDoc }) => { }, [footnoteRef]); return ( -
+
{showPopup && }
void; + onDismiss?: () => void; } const OPTIONS_HEIGHT_PIX = 28; @@ -41,6 +42,7 @@ const AnnotationPopup: React.FC = ({ popupWidth, popupHeight, onHighlight, + onDismiss, }) => { const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX); const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX); @@ -53,6 +55,7 @@ const AnnotationPopup: React.FC = ({ trianglePosition={trianglePosition} className='selection-popup bg-gray-600 text-white' triangleClassName='text-gray-600' + onDismiss={onDismiss} >
= ({ bookKey }) => { const view = getView(bookKey); const viewSettings = getViewSettings(bookKey)!; + const containerRef = React.useRef(null); + const [selection, setSelection] = useState(null); const [showAnnotPopup, setShowAnnotPopup] = useState(false); const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false); @@ -373,6 +375,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { }, [progress]); const handleShowAnnotPopup = () => { + if (!appService?.isMobile) { + containerRef.current?.focus(); + } setShowAnnotPopup(true); setShowDeepLPopup(false); setShowWiktionaryPopup(false); @@ -421,13 +426,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { } }; - const handleHighlight = (update = false) => { + const handleHighlight = (update = false, highlightStyle?: HighlightStyle) => { if (!selection || !selection.text) return; setHighlightOptionsVisible(true); const { booknotes: annotations = [] } = config; const cfi = view?.getCFI(selection.index, selection.range); if (!cfi) return; - const style = settings.globalReadSettings.highlightStyle; + const style = highlightStyle || settings.globalReadSettings.highlightStyle; const color = settings.globalReadSettings.highlightStyles[style]; const annotation: BookNote = { id: uniqueId(), @@ -479,7 +484,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const handleSearch = () => { if (!selection || !selection.text) return; - setShowAnnotPopup(false); + handleDismissPopupAndSelection(); eventDispatcher.dispatch('search', { term: selection.text }); }; @@ -510,20 +515,32 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { // Keyboard shortcuts: trigger actions only if there's an active selection and popup hidden useShortcuts( { + onHighlightSelection: () => { + handleHighlight(false, 'highlight'); + }, + onUnderlineSelection: () => { + handleHighlight(false, 'underline'); + }, + onAnnotateSelection: () => { + handleAnnotate(); + }, + onSearchSelection: () => { + handleSearch(); + }, + onCopySelection: () => { + handleCopy(); + }, onTranslateSelection: () => { - if (selection?.text) { - handleTranslation(); - } + handleTranslation(); }, onDictionarySelection: () => { - if (selection?.text) { - handleDictionary(); - } + handleDictionary(); }, onWikipediaSelection: () => { - if (selection?.text) { - handleWikipedia(); - } + handleWikipedia(); + }, + onReadAloudSelection: () => { + handleSpeakText(); }, }, [selection?.text], @@ -647,7 +664,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { ]; return ( -
+
{showWiktionaryPopup && trianglePosition && dictPopupPosition && ( = ({ bookKey }) => { trianglePosition={trianglePosition} popupWidth={dictPopupWidth} popupHeight={dictPopupHeight} + onDismiss={handleDismissPopupAndSelection} /> )} {showWikipediaPopup && trianglePosition && dictPopupPosition && ( @@ -666,6 +684,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { trianglePosition={trianglePosition} popupWidth={dictPopupWidth} popupHeight={dictPopupHeight} + onDismiss={handleDismissPopupAndSelection} /> )} {showDeepLPopup && trianglePosition && translatorPopupPosition && ( @@ -675,6 +694,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { trianglePosition={trianglePosition} popupWidth={transPopupWidth} popupHeight={transPopupHeight} + onDismiss={handleDismissPopupAndSelection} /> )} {showAnnotPopup && trianglePosition && annotPopupPosition && ( @@ -690,6 +710,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { popupWidth={annotPopupWidth} popupHeight={annotPopupHeight} onHighlight={handleHighlight} + onDismiss={handleDismissPopupAndSelection} /> )}
diff --git a/apps/readest-app/src/app/reader/components/annotator/TranslatorPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/TranslatorPopup.tsx index 6e65a9fa..9508e7cf 100644 --- a/apps/readest-app/src/app/reader/components/annotator/TranslatorPopup.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/TranslatorPopup.tsx @@ -25,6 +25,7 @@ interface TranslatorPopupProps { trianglePosition: Position; popupWidth: number; popupHeight: number; + onDismiss?: () => void; } interface TranslatorType { @@ -38,6 +39,7 @@ const TranslatorPopup: React.FC = ({ trianglePosition, popupWidth, popupHeight, + onDismiss, }) => { const _ = useTranslation(); const { token } = useAuth(); @@ -141,6 +143,7 @@ const TranslatorPopup: React.FC = ({ position={position} className='grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600 text-white' triangleClassName='text-gray-600' + onDismiss={onDismiss} >
diff --git a/apps/readest-app/src/app/reader/components/annotator/WikipediaPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/WikipediaPopup.tsx index 1fa33f91..d9afd4ee 100644 --- a/apps/readest-app/src/app/reader/components/annotator/WikipediaPopup.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/WikipediaPopup.tsx @@ -9,6 +9,7 @@ interface WikipediaPopupProps { trianglePosition: Position; popupWidth: number; popupHeight: number; + onDismiss?: () => void; } const WikipediaPopup: React.FC = ({ @@ -18,6 +19,7 @@ const WikipediaPopup: React.FC = ({ trianglePosition, popupWidth, popupHeight, + onDismiss, }) => { const isLoading = useRef(false); @@ -109,6 +111,7 @@ const WikipediaPopup: React.FC = ({ position={position} trianglePosition={trianglePosition} className='select-text' + onDismiss={onDismiss} >
diff --git a/apps/readest-app/src/app/reader/components/annotator/WiktionaryPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/WiktionaryPopup.tsx index bba752b6..e8f6753b 100644 --- a/apps/readest-app/src/app/reader/components/annotator/WiktionaryPopup.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/WiktionaryPopup.tsx @@ -20,6 +20,7 @@ interface WiktionaryPopupProps { trianglePosition: Position; popupWidth: number; popupHeight: number; + onDismiss?: () => void; } const WiktionaryPopup: React.FC = ({ @@ -29,6 +30,7 @@ const WiktionaryPopup: React.FC = ({ trianglePosition, popupWidth, popupHeight, + onDismiss, }) => { const [lookupWord, setLookupWord] = useState(word); const isLookingUp = useRef(false); @@ -164,6 +166,7 @@ const WiktionaryPopup: React.FC = ({ height={popupHeight} position={position} className='select-text' + onDismiss={onDismiss} >
diff --git a/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx b/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx index f45e9798..7a0b114a 100644 --- a/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx +++ b/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSettingsStore } from '@/store/settingsStore'; import { useBookDataStore } from '@/store/bookDataStore'; @@ -16,6 +16,7 @@ import { uniqueId } from '@/utils/misc'; import { eventDispatcher } from '@/utils/event'; import { getBookDirFromLanguage } from '@/utils/book'; import { Overlay } from '@/components/Overlay'; +import useShortcuts from '@/hooks/useShortcuts'; import BooknoteItem from '../sidebar/BooknoteItem'; import NotebookHeader from './Header'; import NoteEditor from './NoteEditor'; @@ -50,6 +51,15 @@ const Notebook: React.FC = ({}) => { } }; + const handleHideNotebook = useCallback(() => { + if (!isNotebookPinned) { + setNotebookVisible(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isNotebookPinned]); + + useShortcuts({ onEscape: handleHideNotebook }, [handleHideNotebook]); + useEffect(() => { if (isNotebookVisible) { updateAppTheme('base-200'); diff --git a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx index 37a987f2..5d1c43d0 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx @@ -40,6 +40,7 @@ const SideBar: React.FC<{ const [isSearchBarVisible, setIsSearchBarVisible] = useState(false); const [searchResults, setSearchResults] = useState(null); const [searchTerm, setSearchTerm] = useState(''); + const searchTermRef = useRef(searchTerm); const sidebarHeight = useRef(1.0); const isMobile = window.innerWidth < 640; const { @@ -59,7 +60,9 @@ const SideBar: React.FC<{ const { term } = event.detail; setSideBarVisible(true); setIsSearchBarVisible(true); - setSearchTerm(term); + if (term !== undefined && term !== null) { + setSearchTerm(term); + } }; const onNavigateEvent = async () => { @@ -79,6 +82,10 @@ const SideBar: React.FC<{ // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSideBarVisible]); + useEffect(() => { + searchTermRef.current = searchTerm; + }, [searchTerm]); + useEffect(() => { eventDispatcher.on('search', onSearchEvent); eventDispatcher.on('navigate', onNavigateEvent); @@ -176,16 +183,35 @@ const SideBar: React.FC<{ }); }; + const handleShowSearchBar = useCallback(() => { + setTimeout(() => { + setSideBarVisible(true); + setIsSearchBarVisible(true); + }, 100); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleHideSearchBar = useCallback(() => { setIsSearchBarVisible(false); setSearchResults(null); - setSearchTerm(''); + setTimeout(() => { + setSearchTerm(''); + }, 100); getView(sideBarBookKey)?.clearSearch(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sideBarBookKey]); - useShortcuts({ onToggleSearchBar: handleToggleSearchBar, onEscape: handleHideSearchBar }, [ - sideBarBookKey, + const handleHideSideBar = useCallback(() => { + if (searchTermRef.current) { + handleHideSearchBar(); + } else if (!isSideBarPinned) { + setSideBarVisible(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sideBarBookKey, isSideBarPinned]); + + useShortcuts({ onShowSearchBar: handleShowSearchBar, onEscape: handleHideSideBar }, [ + handleHideSideBar, ]); const handleSearchResultClick = (cfi: string) => { diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 56d49a4a..a7dca4cd 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -665,6 +665,7 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => { position={panelPosition} trianglePosition={trianglePosition} className='bg-base-200 flex shadow-lg' + onDismiss={handleDismissPopup} > { - eventDispatcher.dispatch('search', { term: '' }); + setTimeout(() => { + eventDispatcher.dispatch('search', { term: null }); + }, 100); }; const applyZoomLevel = (zoomLevel: number) => { @@ -241,7 +243,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) = onToggleScrollMode: toggleScrollMode, onToggleBookmark: toggleBookmark, onOpenFontLayoutSettings: () => setSettingsDialogOpen(true), - onToggleSearchBar: showSearchBar, + onShowSearchBar: showSearchBar, onToggleFullscreen: toggleFullscreen, onToggleTTS: toggleTTS, onReloadPage: reloadPage, diff --git a/apps/readest-app/src/components/Popup.tsx b/apps/readest-app/src/components/Popup.tsx index 9036a699..fe67f697 100644 --- a/apps/readest-app/src/components/Popup.tsx +++ b/apps/readest-app/src/components/Popup.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx'; import { Position } from '@/utils/sel'; import { useEffect, useRef, useState } from 'react'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; const Popup = ({ width, @@ -14,7 +15,10 @@ const Popup = ({ className = '', triangleClassName = '', additionalStyle = {}, + isOpen = true, + onDismiss, }: { + isOpen?: boolean; width: number; height?: number; minHeight?: number; @@ -25,11 +29,14 @@ const Popup = ({ className?: string; triangleClassName?: string; additionalStyle?: React.CSSProperties; + onDismiss?: () => void; }) => { const containerRef = useRef(null); const [adjustedPosition, setAdjustedPosition] = useState(position); const [childrenHeight, setChildrenHeight] = useState(height || minHeight || 0); + useKeyDownActions({ onCancel: onDismiss, elementRef: containerRef, enabled: isOpen }); + const popupPadding = useResponsiveSize(10); let availableHeight = window.innerHeight - 2 * popupPadding; if (trianglePosition?.dir === 'up') { diff --git a/apps/readest-app/src/helpers/shortcuts.ts b/apps/readest-app/src/helpers/shortcuts.ts index 0fc65450..eb2b89bc 100644 --- a/apps/readest-app/src/helpers/shortcuts.ts +++ b/apps/readest-app/src/helpers/shortcuts.ts @@ -2,15 +2,20 @@ const DEFAULT_SHORTCUTS = { onSwitchSideBar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'], onToggleSideBar: ['s', 'F9'], onToggleNotebook: ['n'], - onToggleSearchBar: ['ctrl+f', 'cmd+f'], + onShowSearchBar: ['ctrl+f', 'cmd+f'], onToggleScrollMode: ['shift+j'], onToggleSelectMode: ['shift+s'], onToggleBookmark: ['ctrl+d', 'cmd+d'], onToggleTTS: ['t'], - // Selection actions (lowercase key without modifiers when selection active) - onTranslateSelection: ['ctrl+z', 'ctrl+shift+t'], - onDictionarySelection: ['ctrl+x', 'ctrl+shift+d'], - onWikipediaSelection: ['ctrl+w', 'ctrl+shift+w'], + onHighlightSelection: ['ctrl+h', 'cmd+h'], + onUnderlineSelection: ['ctrl+u', 'cmd+u'], + onAnnotateSelection: ['ctrl+n', 'cmd+n'], + onSearchSelection: ['ctrl+f', 'cmd+f'], + onCopySelection: ['ctrl+c', 'cmd+c'], + onTranslateSelection: ['ctrl+t', 'cmd+t'], + onDictionarySelection: ['ctrl+d', 'cmd+d'], + onWikipediaSelection: ['ctrl+w', 'cmd+w'], + onReadAloudSelection: ['ctrl+r', 'cmd+r'], onOpenFontLayoutSettings: ['shift+f', 'ctrl+,', 'cmd+,'], onOpenBooks: ['ctrl+o', 'cmd+o'], onReloadPage: ['shift+r'], diff --git a/apps/readest-app/src/hooks/useShortcuts.ts b/apps/readest-app/src/hooks/useShortcuts.ts index a2e0828d..00c943d2 100644 --- a/apps/readest-app/src/hooks/useShortcuts.ts +++ b/apps/readest-app/src/hooks/useShortcuts.ts @@ -58,16 +58,20 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency if (key === 'backspace') return true; for (const [actionName, actionHandler] of Object.entries(actions)) { const shortcutKey = actionName as keyof ShortcutConfig; - const handler = actionHandler as ((event?: KeyboardEvent | MessageEvent) => void) | undefined; + const handler = actionHandler as + | ((event?: KeyboardEvent | MessageEvent) => void | boolean) + | undefined; const shortcutList = shortcuts[shortcutKey as keyof ShortcutConfig]; + // console.log('Checking action:', shortcutKey); if ( handler && shortcutList?.some((shortcut) => isShortcutMatch(shortcut, key, ctrlKey, altKey, metaKey, shiftKey), ) ) { - handler(event); - return true; + if (handler(event)) { + return true; + } } } return false; @@ -96,6 +100,7 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency } const handled = processKeyEvent(key.toLowerCase(), ctrlKey, altKey, metaKey, shiftKey, event); + // console.log('Key event handled:', key, handled); if (handled) event.preventDefault(); } else if ( event instanceof MessageEvent &&