feat(annotator): add a loupe when adjusting text selection range, closes #3002 (#3261)

This commit is contained in:
Huang Xin
2026-02-11 17:13:22 +08:00
committed by GitHub
parent 920032155b
commit fd3533dba1
4 changed files with 93 additions and 12 deletions
@@ -3,23 +3,28 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { BookNote, HighlightColor } from '@/types/book';
import { Point, TextSelection } from '@/utils/sel';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
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 MagnifierLoupe from './MagnifierLoupe';
interface HandleProps {
hidden?: boolean;
position: Point;
isVertical: boolean;
type: 'start' | 'end';
color: string;
onDragStart: () => void;
onDragStart: (pointerType: string) => void;
onDrag: (point: Point) => void;
onDragEnd: () => void;
}
const Handle: React.FC<HandleProps> = ({
hidden,
position,
isVertical,
type,
@@ -38,7 +43,7 @@ const Handle: React.FC<HandleProps> = ({
e.preventDefault();
e.stopPropagation();
isDragging.current = true;
onDragStart();
onDragStart(e.pointerType);
(e.target as HTMLElement).setPointerCapture(e.pointerId);
},
[onDragStart],
@@ -68,7 +73,10 @@ const Handle: React.FC<HandleProps> = ({
return (
<div
className='pointer-events-auto absolute z-50 cursor-grab touch-none active:cursor-grabbing'
className={clsx(
'pointer-events-auto absolute z-50 cursor-grab touch-none active:cursor-grabbing',
hidden && 'hidden',
)}
style={{
left: isVertical
? type === 'start'
@@ -143,8 +151,11 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
setSelection,
onStartEdit,
}) => {
const { appService } = useEnv();
const { settings } = useSettingsStore();
const { isDarkMode } = useThemeStore();
const { getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey);
const isEink = settings.globalViewSettings.isEink;
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
const { handlePositions, getHandlePositionsFromRange, handleAnnotationRangeChange } =
@@ -153,10 +164,13 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
const initializedRef = useRef(false);
const handleColorHex = getHighlightColorHex(settings, handleColor) ?? '#FFFF00';
const draggingRef = useRef<'start' | 'end' | null>(null);
const dragPointerTypeRef = useRef<string>('');
const startRef = useRef<Point>({ x: 0, y: 0 });
const endRef = useRef<Point>({ x: 0, y: 0 });
const [draggingHandle, setDraggingHandle] = useState<'start' | 'end' | null>(null);
const [currentStart, setCurrentStart] = useState<Point>({ x: 0, y: 0 });
const [currentEnd, setCurrentEnd] = useState<Point>({ x: 0, y: 0 });
const [loupePoint, setLoupePoint] = useState<Point | null>(null);
useEffect(() => {
if (initializedRef.current) return;
@@ -184,19 +198,32 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
endRef.current = handlePositions.end;
}, [handlePositions]);
const handleStartDragStart = useCallback(() => {
draggingRef.current = 'start';
onStartEdit();
}, [onStartEdit]);
const handleStartDragStart = useCallback(
(pointerType: string) => {
draggingRef.current = 'start';
dragPointerTypeRef.current = pointerType;
setDraggingHandle('start');
setLoupePoint({ ...startRef.current });
onStartEdit();
},
[onStartEdit],
);
const handleEndDragStart = useCallback(() => {
draggingRef.current = 'end';
onStartEdit();
}, [onStartEdit]);
const handleEndDragStart = useCallback(
(pointerType: string) => {
draggingRef.current = 'end';
dragPointerTypeRef.current = pointerType;
setDraggingHandle('end');
setLoupePoint({ ...endRef.current });
onStartEdit();
},
[onStartEdit],
);
const handleStartDrag = useCallback(
(point: Point) => {
setCurrentStart(point);
setLoupePoint(point);
startRef.current = point;
handleAnnotationRangeChange(point, endRef.current, isVertical, true);
},
@@ -206,6 +233,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
const handleEndDrag = useCallback(
(point: Point) => {
setCurrentEnd(point);
setLoupePoint(point);
endRef.current = point;
handleAnnotationRangeChange(startRef.current, point, isVertical, true);
},
@@ -214,6 +242,8 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
const handleDragEnd = useCallback(() => {
draggingRef.current = null;
setDraggingHandle(null);
setLoupePoint(null);
handleAnnotationRangeChange(startRef.current, endRef.current, isVertical, false);
}, [isVertical, handleAnnotationRangeChange]);
@@ -221,9 +251,12 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
return null;
}
const showLoupe = loupePoint !== null && appService?.isMobile && !viewSettings?.vertical;
return (
<div className='pointer-events-none fixed inset-0 z-50'>
<Handle
hidden={draggingHandle === 'end'}
position={currentStart}
isVertical={isVertical}
type='start'
@@ -233,6 +266,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
onDragEnd={handleDragEnd}
/>
<Handle
hidden={draggingHandle === 'start'}
position={currentEnd}
isVertical={isVertical}
type='end'
@@ -241,6 +275,14 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
onDrag={handleEndDrag}
onDragEnd={handleDragEnd}
/>
{showLoupe && (
<MagnifierLoupe
bookKey={bookKey}
dragPoint={loupePoint}
isVertical={isVertical}
color={handleColorHex}
/>
)}
</div>
);
};
@@ -0,0 +1,33 @@
import { useEffect } from 'react';
import { Point } from '@/utils/sel';
import { useReaderStore } from '@/store/readerStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface MagnifierLoupeProps {
bookKey: string;
dragPoint: Point;
isVertical: boolean;
color: string;
}
const MagnifierLoupe: React.FC<MagnifierLoupeProps> = ({
bookKey,
dragPoint,
isVertical,
color,
}) => {
const { getView } = useReaderStore();
const radius = useResponsiveSize(52);
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]);
return null;
};
export default MagnifierLoupe;
+6
View File
@@ -79,6 +79,12 @@ export interface FoliateView extends HTMLElement {
option?: AddEventListenerOptions,
) => void;
removeEventListener: (type: string, listener: EventListener) => void;
showLoupe?: (
x: number,
y: number,
options: { isVertical: boolean; color: string; radius: number },
) => void;
hideLoupe?: () => void;
};
}