This commit is contained in:
@@ -113,6 +113,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
}, [isScrolledMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zoomLevel === viewSettings.zoomLevel) return;
|
||||
saveViewSettings(envConfig, bookKey, 'zoomLevel', zoomLevel, true, true);
|
||||
if (bookData.bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
getView(bookKey)?.renderer.setAttribute('scale-factor', zoomLevel);
|
||||
@@ -273,7 +274,6 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
shortcut='Shift+J'
|
||||
Icon={isScrolledMode ? MdCheck : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
disabled={bookData.isFixedLayout}
|
||||
/>
|
||||
|
||||
<hr aria-hidden='true' className='border-base-300 my-1' />
|
||||
|
||||
@@ -908,7 +908,6 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleSearch,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'dictionary':
|
||||
return { tooltipText: _(label), Icon, onClick: handleDictionary };
|
||||
|
||||
@@ -179,9 +179,10 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
const viewSettings = getViewSettings(sideBarBookKey)!;
|
||||
viewSettings!.zoomLevel = zoomLevel;
|
||||
setViewSettings(sideBarBookKey, viewSettings!);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!));
|
||||
if (bookData?.bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
if (bookData?.isFixedLayout) {
|
||||
view?.renderer.setAttribute('scale-factor', zoomLevel);
|
||||
} else {
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -246,14 +247,23 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
return true;
|
||||
};
|
||||
|
||||
const handlePinchZoom = (event: CustomEvent) => {
|
||||
const zoomLevel = event.detail?.zoomLevel;
|
||||
if (zoomLevel != null) {
|
||||
applyZoomLevel(zoomLevel);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('zoom-in', handleZoomIn);
|
||||
eventDispatcher.on('zoom-out', handleZoomOut);
|
||||
eventDispatcher.on('reset-zoom', resetZoom);
|
||||
eventDispatcher.on('pinch-zoom', handlePinchZoom);
|
||||
return () => {
|
||||
eventDispatcher.off('zoom-in', handleZoomIn);
|
||||
eventDispatcher.off('zoom-out', handleZoomOut);
|
||||
eventDispatcher.off('reset-zoom', resetZoom);
|
||||
eventDispatcher.off('pinch-zoom', handlePinchZoom);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { ScrollSource } from './usePagination';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '@/services/constants';
|
||||
|
||||
export const useMouseEvent = (
|
||||
bookKey: string,
|
||||
@@ -97,23 +98,61 @@ export const useTouchEvent = (
|
||||
handleContinuousScroll: (source: ScrollSource, delta: number, threshold: number) => void,
|
||||
) => {
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { hoveredBookKey, setHoveredBookKey, getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey, getViewSettings, getView } = useReaderStore();
|
||||
|
||||
const touchStartRef = useRef<IframeTouch | null>(null);
|
||||
const touchEndRef = useRef<IframeTouch | null>(null);
|
||||
const touchStartTimeRef = useRef<number | null>(null);
|
||||
const touchEndTimeRef = useRef<number | null>(null);
|
||||
const isPinchingRef = useRef(false);
|
||||
const initialPinchDistRef = useRef(0);
|
||||
const initialZoomRef = useRef(100);
|
||||
const lastPinchRatioRef = useRef(1);
|
||||
|
||||
const getTouchDistance = (t0: IframeTouch, t1: IframeTouch) => {
|
||||
// Use screenX/screenY instead of clientX/clientY because pinchZoom
|
||||
// applies a CSS transform to the iframe's parent, which changes the
|
||||
// iframe's coordinate space and causes clientX/clientY to oscillate
|
||||
const dx = t1.screenX - t0.screenX;
|
||||
const dy = t1.screenY - t0.screenY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
const onTouchStart = (e: IframeTouchEvent | React.TouchEvent<HTMLDivElement>) => {
|
||||
const touch = e.targetTouches[0];
|
||||
if (!touch) return;
|
||||
touchStartRef.current = touch;
|
||||
const t0 = e.targetTouches[0] as IframeTouch | undefined;
|
||||
const t1 = e.targetTouches[1] as IframeTouch | undefined;
|
||||
if (t0 && t1) {
|
||||
const bookData = getBookData(bookKey);
|
||||
if (bookData?.isFixedLayout) {
|
||||
isPinchingRef.current = true;
|
||||
initialPinchDistRef.current = getTouchDistance(t0, t1);
|
||||
initialZoomRef.current = getViewSettings(bookKey)?.zoomLevel ?? 100;
|
||||
lastPinchRatioRef.current = 1;
|
||||
touchStartRef.current = null;
|
||||
touchEndRef.current = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!t0) return;
|
||||
touchStartRef.current = t0;
|
||||
touchStartTimeRef.current = 'timeStamp' in e ? e.timeStamp : Date.now();
|
||||
};
|
||||
|
||||
const onTouchMove = (e: IframeTouchEvent | React.TouchEvent<HTMLDivElement>) => {
|
||||
const t0 = e.targetTouches[0] as IframeTouch | undefined;
|
||||
const t1 = e.targetTouches[1] as IframeTouch | undefined;
|
||||
if (isPinchingRef.current && t0 && t1) {
|
||||
const currentDist = getTouchDistance(t0, t1);
|
||||
if (initialPinchDistRef.current > 0) {
|
||||
const ratio = currentDist / initialPinchDistRef.current;
|
||||
lastPinchRatioRef.current = ratio;
|
||||
const renderer = getView(bookKey)?.renderer;
|
||||
renderer?.pinchZoom?.(ratio);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!touchStartRef.current) return;
|
||||
const touch = e.targetTouches[0];
|
||||
const touch = t0;
|
||||
if (touch) {
|
||||
touchEndRef.current = touch;
|
||||
touchEndTimeRef.current = 'timeStamp' in e ? e.timeStamp : Date.now();
|
||||
@@ -135,6 +174,22 @@ export const useTouchEvent = (
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: IframeTouchEvent | React.TouchEvent<HTMLDivElement>) => {
|
||||
if (isPinchingRef.current) {
|
||||
const t0 = e.targetTouches[0] as IframeTouch | undefined;
|
||||
const t1 = e.targetTouches[1] as IframeTouch | undefined;
|
||||
if (t0 && t1) return; // still pinching with 2+ fingers
|
||||
isPinchingRef.current = false;
|
||||
const renderer = getView(bookKey)?.renderer;
|
||||
if (renderer && initialPinchDistRef.current > 0) {
|
||||
renderer.pinchEnd?.();
|
||||
const newZoom = Math.round(initialZoomRef.current * lastPinchRatioRef.current);
|
||||
const clampedZoom = Math.max(MIN_ZOOM_LEVEL, Math.min(MAX_ZOOM_LEVEL, newZoom));
|
||||
eventDispatcher.dispatch('pinch-zoom', { zoomLevel: clampedZoom });
|
||||
}
|
||||
touchStartRef.current = null;
|
||||
touchEndRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (!touchStartRef.current) return;
|
||||
|
||||
const touch = e.targetTouches[0];
|
||||
|
||||
@@ -213,6 +213,7 @@ export const usePagination = (
|
||||
} else if (
|
||||
msg.type === 'touch-swipe' &&
|
||||
bookData.isFixedLayout &&
|
||||
!viewSettings?.scrolled &&
|
||||
!isPanningView(viewRef.current, viewSettings)
|
||||
) {
|
||||
const { deltaX, deltaY, deltaT } = msg.detail;
|
||||
@@ -245,8 +246,8 @@ export const usePagination = (
|
||||
const renderer = viewRef.current?.renderer;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
// Currently continuous scroll is not supported in pre-paginated layout
|
||||
if (bookData.bookDoc?.rendition?.layout === 'pre-paginated') return;
|
||||
// Continuous scroll is not supported in pre-paginated layout unless scrolled mode is active
|
||||
if (bookData.bookDoc?.rendition?.layout === 'pre-paginated' && !viewSettings.scrolled) return;
|
||||
|
||||
if (renderer && viewSettings.scrolled && viewSettings.continuousScroll) {
|
||||
const doScroll = () => {
|
||||
|
||||
@@ -234,15 +234,21 @@ export const handleClick = (
|
||||
};
|
||||
|
||||
const handleTouchEv = (bookKey: string, event: TouchEvent, type: string) => {
|
||||
const touch = event.targetTouches[0];
|
||||
// Use event.touches (all active touches) instead of event.targetTouches
|
||||
// so that multi-finger gestures work even when fingers land on different
|
||||
// elements within the iframe (e.g. canvas vs textLayer spans in PDF)
|
||||
const touchList = type === 'iframe-touchend' ? event.targetTouches : event.touches;
|
||||
const touches = [];
|
||||
if (touch) {
|
||||
touches.push({
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
});
|
||||
for (let i = 0; i < touchList.length; i++) {
|
||||
const touch = touchList[i];
|
||||
if (touch) {
|
||||
touches.push({
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
});
|
||||
}
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface FoliateView extends HTMLElement {
|
||||
options: { isVertical: boolean; color: string; radius: number },
|
||||
) => void;
|
||||
hideLoupe?: () => void;
|
||||
pinchZoom?: (ratio: number) => void;
|
||||
pinchEnd?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 345b246cba...81e37d2a84
Reference in New Issue
Block a user