This commit is contained in:
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import Popup from '@/components/Popup';
|
||||
import PopupButton from './PopupButton';
|
||||
import AnnotationToolButton from './AnnotationToolButton';
|
||||
import AnnotationNotes from './AnnotationNotes';
|
||||
import HighlightOptions from './HighlightOptions';
|
||||
|
||||
@@ -69,7 +69,7 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
{buttons.map((button, index) => {
|
||||
if (button.visible === false) return null;
|
||||
return (
|
||||
<PopupButton
|
||||
<AnnotationToolButton
|
||||
key={index}
|
||||
showTooltip={!highlightOptionsVisible}
|
||||
tooltipText={button.tooltipText}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface PopupButtonProps {
|
||||
interface AnnotationToolButtonProps {
|
||||
showTooltip: boolean;
|
||||
tooltipText: string;
|
||||
disabled?: boolean;
|
||||
@@ -9,7 +9,7 @@ interface PopupButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const PopupButton: React.FC<PopupButtonProps> = ({
|
||||
const AnnotationToolButton: React.FC<AnnotationToolButtonProps> = ({
|
||||
showTooltip,
|
||||
tooltipText,
|
||||
disabled,
|
||||
@@ -40,4 +40,4 @@ const PopupButton: React.FC<PopupButtonProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupButton;
|
||||
export default AnnotationToolButton;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { IconType } from 'react-icons';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { FiCopy } from 'react-icons/fi';
|
||||
import { PiHighlighterFill } from 'react-icons/pi';
|
||||
import { FaWikipediaW } from 'react-icons/fa';
|
||||
import { BsPencilSquare } from 'react-icons/bs';
|
||||
import { BsTranslate } from 'react-icons/bs';
|
||||
import { TbHexagonLetterD } from 'react-icons/tb';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
import { IoIosBuild } from 'react-icons/io';
|
||||
import { AnnotationToolType } from '@/types/annotator';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
type AnnotationToolButton = {
|
||||
type: AnnotationToolType;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
Icon: IconType;
|
||||
quickAction?: boolean;
|
||||
};
|
||||
|
||||
function createAnnotationToolButtons<T extends AnnotationToolType>(
|
||||
buttons: AnnotationToolType extends T
|
||||
? {
|
||||
[K in T]: {
|
||||
type: K;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
Icon: IconType;
|
||||
quickAction?: boolean;
|
||||
};
|
||||
}[T][]
|
||||
: never,
|
||||
): AnnotationToolButton[] {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
export const annotationToolButtons = createAnnotationToolButtons([
|
||||
{ type: 'copy', label: _('Copy'), tooltip: _('Copy text after selection'), Icon: FiCopy },
|
||||
{
|
||||
type: 'highlight',
|
||||
label: _('Highlight'),
|
||||
tooltip: _('Highlight text after selection'),
|
||||
Icon: PiHighlighterFill,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'annotate',
|
||||
label: _('Annotate'),
|
||||
tooltip: _('Annotate text after selection'),
|
||||
Icon: BsPencilSquare,
|
||||
},
|
||||
{ type: 'search', label: _('Search'), tooltip: _('Search text after selection'), Icon: FiSearch },
|
||||
{
|
||||
type: 'dictionary',
|
||||
label: _('Dictionary'),
|
||||
tooltip: _('Look up text in dictionary after selection'),
|
||||
Icon: TbHexagonLetterD,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'wikipedia',
|
||||
label: _('Wikipedia'),
|
||||
tooltip: _('Look up text in Wikipedia after selection'),
|
||||
Icon: FaWikipediaW,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'translate',
|
||||
label: _('Translate'),
|
||||
tooltip: _('Translate text after selection'),
|
||||
Icon: BsTranslate,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'tts',
|
||||
label: _('Speak'),
|
||||
tooltip: _('Read text aloud after selection'),
|
||||
Icon: FaHeadphones,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'proofread',
|
||||
label: _('Proofread'),
|
||||
tooltip: _('Proofread text after selection'),
|
||||
Icon: IoIosBuild,
|
||||
},
|
||||
]);
|
||||
|
||||
export const annotationToolQuickActions = annotationToolButtons.filter(
|
||||
(button) => button.quickAction,
|
||||
);
|
||||
@@ -1,20 +1,12 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { FiCopy } from 'react-icons/fi';
|
||||
import { PiHighlighterFill } from 'react-icons/pi';
|
||||
import { FaWikipediaW } from 'react-icons/fa';
|
||||
import { BsPencilSquare } from 'react-icons/bs';
|
||||
import { RiDeleteBinLine } from 'react-icons/ri';
|
||||
import { BsTranslate } from 'react-icons/bs';
|
||||
import { TbHexagonLetterD } from 'react-icons/tb';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
import { IoIosBuild } from 'react-icons/io';
|
||||
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote, BooknoteGroup, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
import { NativeTouchEventType } from '@/types/system';
|
||||
import { getOSPlatform, uniqueId } from '@/utils/misc';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -22,6 +14,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
import { useNotesSync } from '../../hooks/useNotesSync';
|
||||
import { useTextSelector } from '../../hooks/useTextSelector';
|
||||
@@ -30,8 +23,9 @@ import { eventDispatcher } from '@/utils/event';
|
||||
import { findTocItemBS } from '@/utils/toc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { runSimpleCC } from '@/utils/simplecc';
|
||||
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import { getWordCount } from '@/utils/word';
|
||||
import { getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
import { annotationToolButtons } from './AnnotationTools';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
import WikipediaPopup from './WikipediaPopup';
|
||||
@@ -46,6 +40,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
|
||||
const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore();
|
||||
const { setNotebookVisible, setNotebookNewAnnotation } = useNotebookStore();
|
||||
const { listenToNativeTouchEvents } = useDeviceControlStore();
|
||||
|
||||
useNotesSync(bookKey);
|
||||
|
||||
@@ -163,12 +158,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDismissPopupAndSelection = () => {
|
||||
handleDismissPopup();
|
||||
view?.deselect();
|
||||
};
|
||||
|
||||
const {
|
||||
isTextSelected,
|
||||
handleScroll,
|
||||
handleTouchStart,
|
||||
handleTouchEnd,
|
||||
@@ -180,6 +171,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
handleContextmenu,
|
||||
} = useTextSelector(bookKey, setSelection, handleDismissPopup);
|
||||
|
||||
const handleDismissPopupAndSelection = () => {
|
||||
handleDismissPopup();
|
||||
view?.deselect();
|
||||
isTextSelected.current = false;
|
||||
};
|
||||
|
||||
const onLoad = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { doc, index } = detail;
|
||||
@@ -190,6 +187,21 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setShowAnnotPopup(false);
|
||||
};
|
||||
|
||||
const handleNativeTouch = (event: CustomEvent) => {
|
||||
const ev = event.detail as NativeTouchEventType;
|
||||
if (ev.type === 'touchstart') {
|
||||
handleTouchStart();
|
||||
} else if (ev.type === 'touchend') {
|
||||
handleTouchEnd();
|
||||
handlePointerup(doc, index);
|
||||
}
|
||||
};
|
||||
|
||||
if (appService?.isAndroidApp) {
|
||||
listenToNativeTouchEvents();
|
||||
eventDispatcher.on('native-touch', handleNativeTouch);
|
||||
}
|
||||
|
||||
// Attach generic selection listeners for all formats, including PDF.
|
||||
// For PDF we only guarantee Copy & Translate; highlight/annotate may be limited by CFI support.
|
||||
view?.renderer?.addEventListener('scroll', handleScroll);
|
||||
@@ -204,7 +216,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
detail.doc?.addEventListener('pointerup', (ev: PointerEvent) =>
|
||||
handlePointerup(doc, index, ev),
|
||||
);
|
||||
detail.doc?.addEventListener('selectionchange', () => handleSelectionchange(doc, index));
|
||||
detail.doc?.addEventListener('selectionchange', () => handleSelectionchange(doc));
|
||||
|
||||
// For PDF selections, enable right-click context menu to directly open translator popup.
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
@@ -242,9 +254,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { draw, annotation, doc, range } = detail;
|
||||
const { style, color } = annotation as BookNote;
|
||||
const customColors = settings.globalReadSettings.customHighlightColors;
|
||||
const hexColor =
|
||||
color && customColors ? customColors[color] : color ? HIGHLIGHT_COLOR_HEX[color] : color;
|
||||
const hexColor = getHighlightColorHex(settings, color);
|
||||
if (annotation.note) {
|
||||
const { defaultView } = doc;
|
||||
const node = range.startContainer;
|
||||
@@ -361,6 +371,32 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleQuickAction = () => {
|
||||
const action = viewSettings.annotationQuickAction;
|
||||
switch (action) {
|
||||
case 'copy':
|
||||
handleCopy(false);
|
||||
handleDismissPopupAndSelection();
|
||||
break;
|
||||
case 'highlight':
|
||||
handleHighlight();
|
||||
handleDismissPopupAndSelection();
|
||||
break;
|
||||
case 'dictionary':
|
||||
handleDictionary();
|
||||
break;
|
||||
case 'wikipedia':
|
||||
handleWikipedia();
|
||||
break;
|
||||
case 'translate':
|
||||
handleTranslation();
|
||||
break;
|
||||
case 'tts':
|
||||
handleSpeakText(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setHighlightOptionsVisible(!!(selection && selection.annotated));
|
||||
if (selection && selection.text.trim().length > 0) {
|
||||
@@ -406,10 +442,16 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setTranslatorPopupPosition(transPopupPos);
|
||||
setProofreadPopupPosition(proofreadPopupPos);
|
||||
setTrianglePosition(triangPos);
|
||||
handleShowAnnotPopup();
|
||||
|
||||
const { enableAnnotationQuickActions, annotationQuickAction } = viewSettings;
|
||||
if (enableAnnotationQuickActions && annotationQuickAction && isTextSelected.current) {
|
||||
handleQuickAction();
|
||||
} else {
|
||||
handleShowAnnotPopup();
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection, bookKey]);
|
||||
}, [selection?.cfi, bookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!progress) return;
|
||||
@@ -465,7 +507,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setShowWikipediaPopup(false);
|
||||
};
|
||||
|
||||
const handleCopy = (copyToNotebook = true, dismissPopup = true) => {
|
||||
const handleCopy = (dismissPopup = true) => {
|
||||
if (!selection || !selection.text) return;
|
||||
setTimeout(() => {
|
||||
// Delay to ensure it won't be overridden by system clipboard actions
|
||||
@@ -475,7 +517,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
handleDismissPopupAndSelection();
|
||||
}
|
||||
|
||||
if (!copyToNotebook) return;
|
||||
if (!viewSettings?.copyToNotebook) return;
|
||||
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
@@ -604,13 +646,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setShowDeepLPopup(true);
|
||||
};
|
||||
|
||||
const handleSpeakText = async () => {
|
||||
const handleSpeakText = async (oneTime = false) => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range: selection.range });
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range: selection.range, oneTime });
|
||||
};
|
||||
|
||||
const handleShowProofreadOptions = () => {
|
||||
const handleProofread = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setShowProofreadPopup(true);
|
||||
@@ -641,7 +683,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
handleSearch();
|
||||
},
|
||||
onCopySelection: () => {
|
||||
handleCopy(false, false);
|
||||
handleCopy(false);
|
||||
},
|
||||
onTranslateSelection: () => {
|
||||
handleTranslation();
|
||||
@@ -655,6 +697,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
onReadAloudSelection: () => {
|
||||
handleSpeakText();
|
||||
},
|
||||
onProofreadSelection: () => {
|
||||
handleProofread();
|
||||
},
|
||||
},
|
||||
[selection?.text],
|
||||
);
|
||||
@@ -745,42 +790,55 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
};
|
||||
|
||||
const selectionAnnotated = selection?.annotated;
|
||||
const buttons = [
|
||||
{ tooltipText: _('Copy'), Icon: FiCopy, onClick: handleCopy },
|
||||
{
|
||||
tooltipText: selectionAnnotated ? _('Delete Highlight') : _('Highlight'),
|
||||
Icon: selectionAnnotated ? RiDeleteBinLine : PiHighlighterFill,
|
||||
onClick: handleHighlight,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
},
|
||||
{
|
||||
tooltipText: _('Annotate'),
|
||||
Icon: BsPencilSquare,
|
||||
onClick: handleAnnotate,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
},
|
||||
{
|
||||
tooltipText: _('Search'),
|
||||
Icon: FiSearch,
|
||||
onClick: handleSearch,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
},
|
||||
{ tooltipText: _('Dictionary'), Icon: TbHexagonLetterD, onClick: handleDictionary },
|
||||
{ tooltipText: _('Wikipedia'), Icon: FaWikipediaW, onClick: handleWikipedia },
|
||||
{ tooltipText: _('Translate'), Icon: BsTranslate, onClick: handleTranslation },
|
||||
{
|
||||
tooltipText: _('Speak'),
|
||||
Icon: FaHeadphones,
|
||||
onClick: handleSpeakText,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
},
|
||||
{
|
||||
tooltipText: _('Proofread'),
|
||||
Icon: IoIosBuild,
|
||||
onClick: handleShowProofreadOptions,
|
||||
disabled: bookData.book?.format !== 'EPUB',
|
||||
},
|
||||
];
|
||||
const toolButtons = annotationToolButtons.map(({ type, label, Icon }) => {
|
||||
switch (type) {
|
||||
case 'copy':
|
||||
return { tooltipText: _(label), Icon, onClick: handleCopy };
|
||||
case 'highlight':
|
||||
return {
|
||||
tooltipText: selectionAnnotated ? _('Delete Highlight') : _(label),
|
||||
Icon: selectionAnnotated ? RiDeleteBinLine : Icon,
|
||||
onClick: handleHighlight,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'annotate':
|
||||
return {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleAnnotate,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'search':
|
||||
return {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleSearch,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'dictionary':
|
||||
return { tooltipText: _(label), Icon, onClick: handleDictionary };
|
||||
case 'wikipedia':
|
||||
return { tooltipText: _(label), Icon, onClick: handleWikipedia };
|
||||
case 'translate':
|
||||
return { tooltipText: _(label), Icon, onClick: handleTranslation };
|
||||
case 'tts':
|
||||
return {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleSpeakText,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'proofread':
|
||||
return {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleProofread,
|
||||
disabled: bookData.book?.format !== 'EPUB',
|
||||
};
|
||||
default:
|
||||
return { tooltipText: '', Icon, onClick: () => {} };
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} role='toolbar' tabIndex={-1}>
|
||||
@@ -821,7 +879,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
bookKey={bookKey}
|
||||
dir={viewSettings.rtl ? 'rtl' : 'ltr'}
|
||||
isVertical={viewSettings.vertical}
|
||||
buttons={buttons}
|
||||
buttons={toolButtons}
|
||||
notes={annotationNotes}
|
||||
position={annotPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
|
||||
import { AnnotationToolType } from '@/types/annotator';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { annotationToolQuickActions } from './AnnotationTools';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Menu from '@/components/Menu';
|
||||
|
||||
interface QuickActionMenuProps {
|
||||
selectedAction?: AnnotationToolType | null;
|
||||
onActionSelect: (action: AnnotationToolType) => void;
|
||||
setIsDropdownOpen?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const QuickActionMenu: React.FC<QuickActionMenuProps> = ({
|
||||
selectedAction,
|
||||
onActionSelect,
|
||||
setIsDropdownOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const handleActionClick = (action: AnnotationToolType) => {
|
||||
onActionSelect(action);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={clsx(
|
||||
'annotation-quick-action-menu dropdown-content z-20 mt-1 border',
|
||||
'bgcolor-base-200 border-base-200 shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
maxWidth: `${window.innerWidth - 40}px`,
|
||||
}}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
{annotationToolQuickActions.map((button) => (
|
||||
<MenuItem
|
||||
key={button.type}
|
||||
label={_(button.label)}
|
||||
tooltip={_(button.tooltip)}
|
||||
Icon={selectedAction === button.type ? MdCheck : button.Icon}
|
||||
onClick={() => handleActionClick(button.type)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickActionMenu;
|
||||
Reference in New Issue
Block a user