feat(annotator): show the loupe when selecting text in instant annotation, closes #3539 (#3565)

This commit is contained in:
Huang Xin
2026-03-19 22:10:33 +08:00
committed by GitHub
parent 7f62cdcab9
commit eab83c6d3f
10 changed files with 282 additions and 31 deletions
@@ -35,6 +35,7 @@ import {
} from '@/utils/style';
import { mountAdditionalFonts, mountCustomFont } from '@/styles/fonts';
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { getIndexFromCfi } from '@/utils/cfi';
import { useUICSS } from '@/hooks/useUICSS';
import {
handleKeydown,
@@ -253,9 +254,16 @@ const FoliateViewer: React.FC<{
}
setTimeout(() => {
const sectionIndex = detail.index;
const booknotes = config.booknotes || [];
booknotes
.filter((item) => !item.deletedAt && item.type === 'annotation' && item.style)
.filter(
(item) =>
!item.deletedAt &&
item.type === 'annotation' &&
item.style &&
getIndexFromCfi(item.cfi) === sectionIndex,
)
.forEach((annotation) => viewRef.current?.addAnnotation(annotation));
}, 100);
@@ -9,7 +9,11 @@ import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useAnnotationEditor } from '../../hooks/useAnnotationEditor';
import { getHighlightColorHex } from '../../utils/annotatorUtil';
import {
getEffectiveLoupePoint,
getExternalDragHandle,
getHighlightColorHex,
} from '../../utils/annotatorUtil';
import MagnifierLoupe from './MagnifierLoupe';
interface HandleProps {
@@ -136,6 +140,7 @@ interface AnnotationRangeEditorProps {
annotation: BookNote;
selection: TextSelection;
handleColor: HighlightColor;
externalDragPoint?: Point | null;
getAnnotationText: (range: Range) => Promise<string>;
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
onStartEdit: () => void;
@@ -147,6 +152,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
annotation,
selection,
handleColor,
externalDragPoint,
getAnnotationText,
setSelection,
onStartEdit,
@@ -247,12 +253,17 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
return null;
}
const showLoupe = loupePoint !== null && appService?.isMobile && !viewSettings?.vertical;
const effectiveLoupePoint = getEffectiveLoupePoint(loupePoint, externalDragPoint);
const activeHandle =
draggingHandle ??
(externalDragPoint ? getExternalDragHandle(externalDragPoint, currentStart, currentEnd) : null);
const showLoupe = effectiveLoupePoint !== null && appService?.isMobile && !viewSettings?.vertical;
return (
<div className='pointer-events-none fixed inset-0 z-50'>
<Handle
hidden={draggingHandle === 'end'}
hidden={activeHandle === 'end'}
position={currentStart}
isVertical={isVertical}
type='start'
@@ -262,7 +273,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
onDragEnd={handleDragEnd}
/>
<Handle
hidden={draggingHandle === 'start'}
hidden={activeHandle === 'start'}
position={currentEnd}
isVertical={isVertical}
type='end'
@@ -271,10 +282,10 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
onDrag={handleEndDrag}
onDragEnd={handleDragEnd}
/>
{showLoupe && (
{showLoupe && effectiveLoupePoint && (
<MagnifierLoupe
bookKey={bookKey}
dragPoint={loupePoint}
dragPoint={effectiveLoupePoint}
isVertical={isVertical}
color={handleColorHex}
/>
@@ -20,7 +20,7 @@ import { useFoliateEvents } from '../../hooks/useFoliateEvents';
import { useNotesSync } from '../../hooks/useNotesSync';
import { useReadwiseSync } from '../../hooks/useReadwiseSync';
import { useTextSelector } from '../../hooks/useTextSelector';
import { Position, TextSelection } from '@/utils/sel';
import { Point, Position, TextSelection } from '@/utils/sel';
import { getPopupPosition, getPosition, getTextFromRange } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
@@ -79,6 +79,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [showAnnotationNotes, setShowAnnotationNotes] = useState(false);
const [annotationNotes, setAnnotationNotes] = useState<BookNote[]>([]);
const [editingAnnotation, setEditingAnnotation] = useState<BookNote | null>(null);
const [externalDragPoint, setExternalDragPoint] = useState<Point | null>(null);
const [showExportDialog, setShowExportDialog] = useState(false);
const [exportData, setExportData] = useState<{
booknotes: BookNote[];
@@ -207,6 +208,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const {
isTextSelected,
isInstantAnnotating,
handleScroll,
handleTouchStart,
handleTouchMove,
@@ -219,7 +221,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
handleShowPopup,
handleUpToPopup,
handleContextmenu,
} = useTextSelector(bookKey, setSelection, getAnnotationText, handleDismissPopup);
} = useTextSelector(
bookKey,
setSelection,
setEditingAnnotation,
setExternalDragPoint,
getAnnotationText,
handleDismissPopup,
);
const handleDismissPopupAndSelection = () => {
handleDismissPopup();
@@ -235,7 +244,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// Available on iOS, on Android not fired
// To make the popup not follow the selection while dragging
setShowAnnotPopup(false);
setEditingAnnotation(null);
if (!isInstantAnnotating.current) {
setEditingAnnotation(null);
}
handleTouchMove(ev);
};
@@ -1003,6 +1014,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
annotation={editingAnnotation}
selection={selection}
handleColor={selectedColor}
externalDragPoint={externalDragPoint}
getAnnotationText={getAnnotationText}
setSelection={setSelection}
onStartEdit={handleStartEditAnnotation}
@@ -18,14 +18,31 @@ const MagnifierLoupe: React.FC<MagnifierLoupeProps> = ({
color,
}) => {
const { getView } = useReaderStore();
const radius = useResponsiveSize(52);
const gap = useResponsiveSize(22);
const margin = useResponsiveSize(8);
const radius = useResponsiveSize(28);
useEffect(() => {
return () => {
const view = getView(bookKey);
view?.renderer.hideLoupe?.();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKey]);
// Update loupe position on every drag move (fast path — no DOM recreation).
useEffect(() => {
const view = getView(bookKey);
if (!view) return;
view.renderer.showLoupe?.(dragPoint.x, dragPoint.y, { isVertical, color, radius });
return () => view.renderer.hideLoupe?.();
}, [bookKey, dragPoint, getView, isVertical, color, radius]);
view.renderer.showLoupe?.(dragPoint.x, dragPoint.y, {
isVertical,
color,
gap,
margin,
radius,
magnification: 1.1,
});
}, [bookKey, dragPoint, getView, isVertical, color, radius, gap, margin]);
return null;
};