From 90caf863fa49ad4d5618a498386d5994f62d99c4 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 28 Feb 2025 14:27:10 +0100 Subject: [PATCH] feat: doublick zoom and pan position if zoomed --- src/components/reader/BookReader.tsx | 4 + .../reader/components/ReaderContent.tsx | 9 +++ .../reader/components/SinglePage.tsx | 14 +++- .../reader/hooks/usePageNavigation.ts | 75 +++++++++++++++---- 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/components/reader/BookReader.tsx b/src/components/reader/BookReader.tsx index 00146ce..70eaa1b 100644 --- a/src/components/reader/BookReader.tsx +++ b/src/components/reader/BookReader.tsx @@ -33,6 +33,8 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) { handleNextPage, shouldShowDoublePage, zoomLevel, + panPosition, + handleDoubleClick, } = usePageNavigation({ book, pages, @@ -118,6 +120,8 @@ export function BookReader({ book, pages, onClose }: BookReaderProps) { isRTL={isRTL} onThumbnailLoad={handleThumbnailLoad} zoomLevel={zoomLevel} + panPosition={panPosition} + onDoubleClick={handleDoubleClick} /> void; zoomLevel: number; + panPosition: { x: number; y: number }; + onDoubleClick: () => void; } export const ReaderContent = ({ @@ -24,6 +26,8 @@ export const ReaderContent = ({ isRTL, onThumbnailLoad, zoomLevel, + panPosition, + onDoubleClick, }: ReaderContentProps) => { return (
@@ -37,6 +41,8 @@ export const ReaderContent = ({ isRTL={isRTL} order="first" zoomLevel={zoomLevel} + panPosition={panPosition} + onDoubleClick={onDoubleClick} /> {isDoublePage && shouldShowDoublePage(currentPage) && ( @@ -48,6 +54,9 @@ export const ReaderContent = ({ isDoublePage={true} isRTL={isRTL} order="second" + zoomLevel={zoomLevel} + panPosition={panPosition} + onDoubleClick={onDoubleClick} /> )}
diff --git a/src/components/reader/components/SinglePage.tsx b/src/components/reader/components/SinglePage.tsx index 2729bcb..142f9f6 100644 --- a/src/components/reader/components/SinglePage.tsx +++ b/src/components/reader/components/SinglePage.tsx @@ -10,6 +10,8 @@ interface SinglePageProps { isRTL?: boolean; order?: "first" | "second"; zoomLevel?: number; + panPosition?: { x: number; y: number }; + onDoubleClick?: () => void; } export const SinglePage = ({ @@ -20,7 +22,9 @@ export const SinglePage = ({ isDoublePage = false, isRTL = false, order = "first", - zoomLevel, + zoomLevel = 1, + panPosition = { x: 0, y: 0 }, + onDoubleClick, }: SinglePageProps) => { return (
onLoad(pageNumber)} + onDoubleClick={onDoubleClick} /> )}
diff --git a/src/components/reader/hooks/usePageNavigation.ts b/src/components/reader/hooks/usePageNavigation.ts index e9d923c..c853fb5 100644 --- a/src/components/reader/hooks/usePageNavigation.ts +++ b/src/components/reader/hooks/usePageNavigation.ts @@ -19,13 +19,16 @@ export const usePageNavigation = ({ const [currentPage, setCurrentPage] = useState(book.readProgress?.page || 1); const [isLoading, setIsLoading] = useState(true); const [secondPageLoading, setSecondPageLoading] = useState(true); + const [zoomLevel, setZoomLevel] = useState(1); + const [panPosition, setPanPosition] = useState({ x: 0, y: 0 }); const timeoutRef = useRef(null); const touchStartXRef = useRef(null); const touchStartYRef = useRef(null); + const lastPanPositionRef = useRef({ x: 0, y: 0 }); const currentPageRef = useRef(currentPage); const isRTL = direction === "rtl"; - const [zoomLevel, setZoomLevel] = useState(1); const initialDistanceRef = useRef(null); + const DEFAULT_ZOOM_LEVEL = 2; useEffect(() => { currentPageRef.current = currentPage; @@ -113,18 +116,33 @@ export const usePageNavigation = ({ return Math.sqrt(dx * dx + dy * dy); }; - const handleTouchMove = useCallback((event: TouchEvent) => { - if (event.touches.length === 2) { - const distance = calculateDistance(event.touches[0], event.touches[1]); - if (initialDistanceRef.current !== null) { - const scale = distance / initialDistanceRef.current; - const zoomFactor = 0.3; - setZoomLevel((prevZoomLevel) => - Math.min(3, Math.max(1, prevZoomLevel + (scale - 1) * zoomFactor)) - ); + const handleTouchMove = useCallback( + (event: TouchEvent) => { + if (event.touches.length === 2) { + const distance = calculateDistance(event.touches[0], event.touches[1]); + if (initialDistanceRef.current !== null) { + const scale = distance / initialDistanceRef.current; + const zoomFactor = 0.3; + setZoomLevel((prevZoomLevel) => + Math.min(3, Math.max(1, prevZoomLevel + (scale - 1) * zoomFactor)) + ); + } + } else if (event.touches.length === 1 && zoomLevel > 1) { + // Gestion du pan uniquement quand on est zoomé + if (touchStartXRef.current !== null && touchStartYRef.current !== null) { + const deltaX = event.touches[0].clientX - touchStartXRef.current; + const deltaY = event.touches[0].clientY - touchStartYRef.current; + + setPanPosition({ + x: lastPanPositionRef.current.x + deltaX, + y: lastPanPositionRef.current.y + deltaY, + }); + } + event.preventDefault(); // Empêcher le scroll de la page } - } - }, []); + }, + [zoomLevel] + ); const handleTouchStart = useCallback( (event: TouchEvent) => { @@ -133,10 +151,11 @@ export const usePageNavigation = ({ } else { touchStartXRef.current = event.touches[0].clientX; touchStartYRef.current = event.touches[0].clientY; + lastPanPositionRef.current = panPosition; currentPageRef.current = currentPage; } }, - [currentPage] + [currentPage, panPosition] ); const handleTouchEnd = useCallback( @@ -151,6 +170,15 @@ export const usePageNavigation = ({ const deltaX = touchEndX - touchStartXRef.current; const deltaY = touchEndY - touchStartYRef.current; + // Si on est zoomé, on met à jour la position finale du pan + if (zoomLevel > 1) { + lastPanPositionRef.current = { + x: lastPanPositionRef.current.x + deltaX, + y: lastPanPositionRef.current.y + deltaY, + }; + return; + } + // Si le déplacement vertical est plus important que le déplacement horizontal, // on ne fait rien (pour éviter de confondre avec un scroll) if (Math.abs(deltaY) > Math.abs(deltaX)) return; @@ -177,9 +205,17 @@ export const usePageNavigation = ({ touchStartXRef.current = null; touchStartYRef.current = null; }, - [handleNextPage, handlePreviousPage, isRTL] + [handleNextPage, handlePreviousPage, isRTL, zoomLevel] ); + // Reset du pan quand on change de page ou dezoom + useEffect(() => { + if (zoomLevel === 1) { + setPanPosition({ x: 0, y: 0 }); + lastPanPositionRef.current = { x: 0, y: 0 }; + } + }, [zoomLevel, currentPage]); + useEffect(() => { setIsLoading(true); setSecondPageLoading(true); @@ -237,6 +273,15 @@ export const usePageNavigation = ({ }; }, [syncReadProgress]); + const handleDoubleClick = useCallback(() => { + setZoomLevel((prevZoom) => { + if (prevZoom === 1) { + return DEFAULT_ZOOM_LEVEL; + } + return 1; + }); + }, []); + return { currentPage, navigateToPage, @@ -248,5 +293,7 @@ export const usePageNavigation = ({ handleNextPage, shouldShowDoublePage, zoomLevel, + panPosition, + handleDoubleClick, }; };