This commit is contained in:
@@ -37,6 +37,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const footnoteHandler = new FootnoteHandler();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [gridRect, setGridRect] = useState<DOMRect | null>(null);
|
||||
const [responsiveWidth, setResponsiveWidth] = useState(popupWidth);
|
||||
@@ -115,6 +116,12 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ 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<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
}, [footnoteRef]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={containerRef} role='toolbar' tabIndex={-1}>
|
||||
{showPopup && <Overlay onDismiss={handleDismissPopup} />}
|
||||
<Popup
|
||||
isOpen={showPopup}
|
||||
width={responsiveWidth}
|
||||
height={responsiveHeight}
|
||||
position={showPopup ? popupPosition! : undefined}
|
||||
trianglePosition={showPopup ? trianglePosition! : undefined}
|
||||
className='select-text overflow-y-auto'
|
||||
onDismiss={handleDismissPopup}
|
||||
>
|
||||
<div
|
||||
className=''
|
||||
|
||||
@@ -24,6 +24,7 @@ interface AnnotationPopupProps {
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
onHighlight: (update?: boolean) => void;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const OPTIONS_HEIGHT_PIX = 28;
|
||||
@@ -41,6 +42,7 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
onHighlight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX);
|
||||
const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX);
|
||||
@@ -53,6 +55,7 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
trianglePosition={trianglePosition}
|
||||
className='selection-popup bg-gray-600 text-white'
|
||||
triangleClassName='text-gray-600'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -51,6 +51,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const [selection, setSelection] = useState<TextSelection | null>(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 (
|
||||
<div>
|
||||
<div ref={containerRef} role='toolbar' tabIndex={-1}>
|
||||
{showWiktionaryPopup && trianglePosition && dictPopupPosition && (
|
||||
<WiktionaryPopup
|
||||
word={selection?.text as string}
|
||||
@@ -656,6 +673,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ 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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ interface TranslatorPopupProps {
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
interface TranslatorType {
|
||||
@@ -38,6 +39,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { token } = useAuth();
|
||||
@@ -141,6 +143,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
position={position}
|
||||
className='grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600 text-white'
|
||||
triangleClassName='text-gray-600'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className='overflow-y-auto p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface WikipediaPopupProps {
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
@@ -18,6 +19,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const isLoading = useRef(false);
|
||||
|
||||
@@ -109,6 +111,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
position={position}
|
||||
trianglePosition={trianglePosition}
|
||||
className='select-text'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className='text-base-content flex h-full flex-col pt-2'>
|
||||
<main className='flex-grow overflow-y-auto px-2 font-sans'></main>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface WiktionaryPopupProps {
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
@@ -29,6 +30,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const [lookupWord, setLookupWord] = useState(word);
|
||||
const isLookingUp = useRef(false);
|
||||
@@ -164,6 +166,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
height={popupHeight}
|
||||
position={position}
|
||||
className='select-text'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className='flex h-full flex-col'>
|
||||
<main className='flex-grow overflow-y-auto p-4 font-sans' />
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -40,6 +40,7 @@ const SideBar: React.FC<{
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(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) => {
|
||||
|
||||
@@ -665,6 +665,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
position={panelPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
className='bg-base-200 flex shadow-lg'
|
||||
onDismiss={handleDismissPopup}
|
||||
>
|
||||
<TTSPanel
|
||||
bookKey={bookKey}
|
||||
|
||||
@@ -156,7 +156,9 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
};
|
||||
|
||||
const showSearchBar = () => {
|
||||
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,
|
||||
|
||||
@@ -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<HTMLDivElement>(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') {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user