This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
getEffectiveLoupePoint,
|
||||
getExternalDragHandle,
|
||||
toParentViewportPoint,
|
||||
} from '@/app/reader/utils/annotatorUtil';
|
||||
import { Point } from '@/utils/sel';
|
||||
|
||||
describe('getEffectiveLoupePoint', () => {
|
||||
it('returns loupePoint when set (internal handle drag takes priority)', () => {
|
||||
const loupePoint: Point = { x: 150, y: 180 };
|
||||
const externalDragPoint: Point = { x: 280, y: 200 };
|
||||
const result = getEffectiveLoupePoint(loupePoint, externalDragPoint);
|
||||
expect(result).toBe(loupePoint);
|
||||
});
|
||||
|
||||
it('returns externalDragPoint when loupePoint is null (smooth pointer tracking)', () => {
|
||||
const externalDragPoint: Point = { x: 280, y: 200 };
|
||||
const result = getEffectiveLoupePoint(null, externalDragPoint);
|
||||
expect(result).toBe(externalDragPoint);
|
||||
});
|
||||
|
||||
it('returns null when both loupePoint and externalDragPoint are null', () => {
|
||||
const result = getEffectiveLoupePoint(null, null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when externalDragPoint is undefined', () => {
|
||||
const result = getEffectiveLoupePoint(null, undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExternalDragHandle', () => {
|
||||
const currentStart: Point = { x: 100, y: 200 };
|
||||
const currentEnd: Point = { x: 300, y: 200 };
|
||||
|
||||
it('forward drag — externalDragPoint closer to end → returns end', () => {
|
||||
const result = getExternalDragHandle({ x: 280, y: 200 }, currentStart, currentEnd);
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
|
||||
it('backward drag — externalDragPoint closer to start → returns start', () => {
|
||||
const result = getExternalDragHandle({ x: 120, y: 200 }, currentStart, currentEnd);
|
||||
expect(result).toBe('start');
|
||||
});
|
||||
|
||||
it('vertical text — works with vertical coordinates', () => {
|
||||
const vStart: Point = { x: 200, y: 100 };
|
||||
const vEnd: Point = { x: 200, y: 400 };
|
||||
const result = getExternalDragHandle({ x: 200, y: 350 }, vStart, vEnd);
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
|
||||
it('equal distance — returns end (deterministic tie-breaking)', () => {
|
||||
// Midpoint between start(100,200) and end(300,200) is (200,200)
|
||||
// distToStart === distToEnd, so !(distToStart < distToEnd) → returns 'end'
|
||||
const result = getExternalDragHandle({ x: 200, y: 200 }, currentStart, currentEnd);
|
||||
expect(result).toBe('end');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toParentViewportPoint', () => {
|
||||
it('adds frameRect offset to coordinates', () => {
|
||||
const mockFrameElement = {
|
||||
getBoundingClientRect: vi.fn(() => ({
|
||||
top: 50,
|
||||
left: 80,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: vi.fn(),
|
||||
})),
|
||||
};
|
||||
const doc = {
|
||||
defaultView: {
|
||||
frameElement: mockFrameElement,
|
||||
},
|
||||
} as unknown as Document;
|
||||
|
||||
const result = toParentViewportPoint(doc, 100, 200);
|
||||
expect(result).toEqual({ x: 180, y: 250 });
|
||||
});
|
||||
|
||||
it('defaults to {0,0} offset when no frameElement (detached doc)', () => {
|
||||
const doc = {
|
||||
defaultView: null,
|
||||
} as unknown as Document;
|
||||
|
||||
const result = toParentViewportPoint(doc, 100, 200);
|
||||
expect(result).toEqual({ x: 100, y: 200 });
|
||||
});
|
||||
|
||||
it('handles non-zero iframe offset (e.g., sidebar shifts iframe right)', () => {
|
||||
const mockFrameElement = {
|
||||
getBoundingClientRect: vi.fn(() => ({
|
||||
top: 0,
|
||||
left: 250,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: vi.fn(),
|
||||
})),
|
||||
};
|
||||
const doc = {
|
||||
defaultView: {
|
||||
frameElement: mockFrameElement,
|
||||
},
|
||||
} as unknown as Document;
|
||||
|
||||
const result = toParentViewportPoint(doc, 50, 100);
|
||||
expect(result).toEqual({ x: 300, y: 100 });
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -2,18 +2,27 @@ import { useCallback, useRef } from 'react';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { Point, TextSelection, snapRangeToWords } from '@/utils/sel';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { toParentViewportPoint } from '../utils/annotatorUtil';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
interface UseInstantAnnotationProps {
|
||||
bookKey: string;
|
||||
getAnnotationText: (range: Range) => Promise<string>;
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>;
|
||||
setEditingAnnotation: React.Dispatch<React.SetStateAction<BookNote | null>>;
|
||||
setExternalDragPoint: React.Dispatch<React.SetStateAction<Point | null>>;
|
||||
}
|
||||
|
||||
export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantAnnotationProps) => {
|
||||
export const useInstantAnnotation = ({
|
||||
bookKey,
|
||||
getAnnotationText,
|
||||
setSelection,
|
||||
setEditingAnnotation,
|
||||
setExternalDragPoint,
|
||||
}: UseInstantAnnotationProps) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
@@ -41,6 +50,13 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
}
|
||||
}, [bookKey, getViewsById]);
|
||||
|
||||
const clearInstantAnnotationState = useCallback(() => {
|
||||
clearPreviewAnnotation();
|
||||
setEditingAnnotation(null);
|
||||
setSelection(null);
|
||||
setExternalDragPoint(null);
|
||||
}, [clearPreviewAnnotation, setEditingAnnotation, setSelection, setExternalDragPoint]);
|
||||
|
||||
const findPositionAtPoint = useCallback((doc: Document, x: number, y: number) => {
|
||||
if (doc.caretPositionFromPoint) {
|
||||
const pos = doc.caretPositionFromPoint(x, y);
|
||||
@@ -200,6 +216,21 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
views.forEach((v) => v?.addAnnotation(annotation));
|
||||
previewAnnotationRef.current = annotation;
|
||||
|
||||
const progress = getProgress(bookKey);
|
||||
setEditingAnnotation(annotation);
|
||||
setSelection({
|
||||
key: bookKey,
|
||||
text: '',
|
||||
cfi,
|
||||
page: progress?.page || 0,
|
||||
range: newRange,
|
||||
index,
|
||||
annotated: true,
|
||||
});
|
||||
|
||||
// Convert iframe pointer coords to parent viewport coords for the loupe
|
||||
setExternalDragPoint(toParentViewportPoint(doc, ev.clientX, ev.clientY));
|
||||
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -211,9 +242,9 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
return true;
|
||||
}, [isInstantAnnotationEnabled, clearPreviewAnnotation]);
|
||||
}, [isInstantAnnotationEnabled, clearInstantAnnotationState]);
|
||||
|
||||
const handleInstantAnnotationPointerUp = useCallback(
|
||||
async (doc: Document, index: number, ev: PointerEvent) => {
|
||||
@@ -223,7 +254,7 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
if (!startPointRef.current || !view) {
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -237,13 +268,13 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2),
|
||||
);
|
||||
if (distance < 10) {
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
return false;
|
||||
}
|
||||
|
||||
const newRange = createRangeFromPoints(doc, startPoint, endPoint);
|
||||
if (!newRange) {
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -251,11 +282,11 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
const cfi = view.getCFI(index, newRange);
|
||||
|
||||
if (!text || !cfi || text.trim().length === 0) {
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPreviewAnnotation();
|
||||
clearInstantAnnotationState();
|
||||
const annotation = createAnnotation(cfi, text);
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((v) => v?.addAnnotation(annotation));
|
||||
@@ -286,14 +317,19 @@ export const useInstantAnnotation = ({ bookKey, getAnnotationText }: UseInstantA
|
||||
return true;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isInstantAnnotationEnabled, createRangeFromPoints, getAnnotationText, clearPreviewAnnotation],
|
||||
[
|
||||
isInstantAnnotationEnabled,
|
||||
createRangeFromPoints,
|
||||
getAnnotationText,
|
||||
clearInstantAnnotationState,
|
||||
],
|
||||
);
|
||||
|
||||
const cancelInstantAnnotation = useCallback(() => {
|
||||
startPointRef.current = null;
|
||||
startDocRef.current = null;
|
||||
clearPreviewAnnotation();
|
||||
}, [clearPreviewAnnotation]);
|
||||
clearInstantAnnotationState();
|
||||
}, [clearInstantAnnotationState]);
|
||||
|
||||
return {
|
||||
isInstantAnnotationEnabled,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isPointerInsideSelection, TextSelection } from '@/utils/sel';
|
||||
import { isPointerInsideSelection, Point, TextSelection } from '@/utils/sel';
|
||||
import { useInstantAnnotation } from './useInstantAnnotation';
|
||||
|
||||
export const useTextSelector = (
|
||||
bookKey: string,
|
||||
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>,
|
||||
setEditingAnnotation: React.Dispatch<React.SetStateAction<BookNote | null>>,
|
||||
setExternalDragPoint: React.Dispatch<React.SetStateAction<Point | null>>,
|
||||
getAnnotationText: (range: Range) => Promise<string>,
|
||||
handleDismissPopup: () => void,
|
||||
) => {
|
||||
@@ -35,7 +38,13 @@ export const useTextSelector = (
|
||||
handleInstantAnnotationPointerMove,
|
||||
handleInstantAnnotationPointerCancel,
|
||||
handleInstantAnnotationPointerUp,
|
||||
} = useInstantAnnotation({ bookKey, getAnnotationText, setSelection });
|
||||
} = useInstantAnnotation({
|
||||
bookKey,
|
||||
getAnnotationText,
|
||||
setSelection,
|
||||
setEditingAnnotation,
|
||||
setExternalDragPoint,
|
||||
});
|
||||
|
||||
const isValidSelection = (sel: Selection) => {
|
||||
return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0;
|
||||
@@ -125,7 +134,6 @@ export const useTextSelector = (
|
||||
stopInstantAnnotating(ev);
|
||||
const handled = await handleInstantAnnotationPointerUp(doc, index, ev);
|
||||
if (handled) {
|
||||
isTextSelected.current = true;
|
||||
return;
|
||||
} else {
|
||||
// If instant annotation was not created, we let the event propagate
|
||||
@@ -251,6 +259,7 @@ export const useTextSelector = (
|
||||
|
||||
return {
|
||||
isTextSelected,
|
||||
isInstantAnnotating,
|
||||
handleScroll,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import { HighlightColor } from '@/types/book';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { Point } from '@/utils/sel';
|
||||
|
||||
export const getHighlightColorHex = (
|
||||
settings: SystemSettings,
|
||||
@@ -11,3 +12,32 @@ export const getHighlightColorHex = (
|
||||
const customColors = settings.globalReadSettings.customHighlightColors;
|
||||
return customColors?.[color] ?? HIGHLIGHT_COLOR_HEX[color];
|
||||
};
|
||||
|
||||
export function getEffectiveLoupePoint(
|
||||
loupePoint: Point | null,
|
||||
externalDragPoint: Point | null | undefined,
|
||||
): Point | null {
|
||||
return loupePoint ?? externalDragPoint ?? null;
|
||||
}
|
||||
|
||||
export function getExternalDragHandle(
|
||||
externalDragPoint: Point,
|
||||
currentStart: Point,
|
||||
currentEnd: Point,
|
||||
): 'start' | 'end' {
|
||||
const distToStart = Math.hypot(
|
||||
externalDragPoint.x - currentStart.x,
|
||||
externalDragPoint.y - currentStart.y,
|
||||
);
|
||||
const distToEnd = Math.hypot(
|
||||
externalDragPoint.x - currentEnd.x,
|
||||
externalDragPoint.y - currentEnd.y,
|
||||
);
|
||||
return distToStart < distToEnd ? 'start' : 'end';
|
||||
}
|
||||
|
||||
export function toParentViewportPoint(doc: Document, x: number, y: number): Point {
|
||||
const frameElement = doc.defaultView?.frameElement;
|
||||
const frameRect = frameElement?.getBoundingClientRect() ?? { top: 0, left: 0 };
|
||||
return { x: x + frameRect.left, y: y + frameRect.top };
|
||||
}
|
||||
|
||||
@@ -94,9 +94,17 @@ export interface FoliateView extends HTMLElement {
|
||||
showLoupe?: (
|
||||
x: number,
|
||||
y: number,
|
||||
options: { isVertical: boolean; color: string; radius: number },
|
||||
options: {
|
||||
isVertical: boolean;
|
||||
color: string;
|
||||
gap: number;
|
||||
margin: number;
|
||||
radius: number;
|
||||
magnification: number;
|
||||
},
|
||||
) => void;
|
||||
hideLoupe?: () => void;
|
||||
destroyLoupe?: () => void;
|
||||
pinchZoom?: (ratio: number) => void;
|
||||
pinchEnd?: () => void;
|
||||
};
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 84ff50a800...b47e523678
Reference in New Issue
Block a user