feat: add zoom functionality to BookReader and related components, enabling zoom state management and touch gesture handling

This commit is contained in:
Julien Froidefond
2025-10-16 14:38:31 +02:00
parent 0716f38175
commit 4209557768
4 changed files with 28 additions and 12 deletions

View File

@@ -19,6 +19,7 @@ export function BookReader({ book, pages, onClose, nextBook }: BookReaderProps)
const [isDoublePage, setIsDoublePage] = useState(false); const [isDoublePage, setIsDoublePage] = useState(false);
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const [showThumbnails, setShowThumbnails] = useState(false); const [showThumbnails, setShowThumbnails] = useState(false);
const [isZoomed, setIsZoomed] = useState(false);
const readerRef = useRef<HTMLDivElement>(null); const readerRef = useRef<HTMLDivElement>(null);
const isLandscape = useOrientation(); const isLandscape = useOrientation();
const { direction, toggleDirection, isRTL } = useReadingDirection(); const { direction, toggleDirection, isRTL } = useReadingDirection();
@@ -43,6 +44,7 @@ export function BookReader({ book, pages, onClose, nextBook }: BookReaderProps)
onClose, onClose,
direction, direction,
nextBook, nextBook,
isZoomed,
}); });
const { preloadPage, getPageUrl, cleanCache } = usePageCache({ const { preloadPage, getPageUrl, cleanCache } = usePageCache({
@@ -138,6 +140,7 @@ export function BookReader({ book, pages, onClose, nextBook }: BookReaderProps)
shouldShowDoublePage={shouldShowDoublePage} shouldShowDoublePage={shouldShowDoublePage}
isRTL={isRTL} isRTL={isRTL}
onThumbnailLoad={handleThumbnailLoad} onThumbnailLoad={handleThumbnailLoad}
onZoomChange={setIsZoomed}
/> />
<NavigationBar <NavigationBar

View File

@@ -10,6 +10,7 @@ interface ReaderContentProps {
shouldShowDoublePage: (page: number) => boolean; shouldShowDoublePage: (page: number) => boolean;
isRTL: boolean; isRTL: boolean;
onThumbnailLoad: (pageNumber: number) => void; onThumbnailLoad: (pageNumber: number) => void;
onZoomChange?: (isZoomed: boolean) => void;
} }
export const ReaderContent = ({ export const ReaderContent = ({
@@ -22,6 +23,7 @@ export const ReaderContent = ({
shouldShowDoublePage, shouldShowDoublePage,
isRTL, isRTL,
onThumbnailLoad, onThumbnailLoad,
onZoomChange,
}: ReaderContentProps) => { }: ReaderContentProps) => {
return ( return (
<div className="relative flex-1 flex items-center justify-center overflow-hidden p-1"> <div className="relative flex-1 flex items-center justify-center overflow-hidden p-1">
@@ -34,6 +36,7 @@ export const ReaderContent = ({
isDoublePage={isDoublePage} isDoublePage={isDoublePage}
isRTL={isRTL} isRTL={isRTL}
order="first" order="first"
onZoomChange={onZoomChange}
/> />
{isDoublePage && shouldShowDoublePage(currentPage) && ( {isDoublePage && shouldShowDoublePage(currentPage) && (
@@ -45,6 +48,7 @@ export const ReaderContent = ({
isDoublePage={true} isDoublePage={true}
isRTL={isRTL} isRTL={isRTL}
order="second" order="second"
onZoomChange={onZoomChange}
/> />
)} )}
</div> </div>

View File

@@ -1,5 +1,6 @@
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useState } from "react";
interface ZoomablePageProps { interface ZoomablePageProps {
pageUrl: string | null; pageUrl: string | null;
@@ -9,6 +10,7 @@ interface ZoomablePageProps {
isDoublePage?: boolean; isDoublePage?: boolean;
isRTL?: boolean; isRTL?: boolean;
order?: "first" | "second"; order?: "first" | "second";
onZoomChange?: (isZoomed: boolean) => void;
} }
export const ZoomablePage = ({ export const ZoomablePage = ({
@@ -19,7 +21,14 @@ export const ZoomablePage = ({
isDoublePage = false, isDoublePage = false,
isRTL = false, isRTL = false,
order = "first", order = "first",
onZoomChange,
}: ZoomablePageProps) => { }: ZoomablePageProps) => {
const [currentScale, setCurrentScale] = useState(1);
const handleTransform = (ref: any, state: { scale: number; positionX: number; positionY: number }) => {
setCurrentScale(state.scale);
onZoomChange?.(state.scale > 1.1);
};
return ( return (
<div <div
className={cn( className={cn(
@@ -55,6 +64,7 @@ export const ZoomablePage = ({
panning={{ disabled: false }} panning={{ disabled: false }}
limitToBounds={true} limitToBounds={true}
centerZoomedOut={false} centerZoomedOut={false}
onTransformed={handleTransform}
> >
<TransformComponent <TransformComponent
wrapperClass="w-full h-full flex items-center justify-center" wrapperClass="w-full h-full flex items-center justify-center"

View File

@@ -10,6 +10,7 @@ interface UsePageNavigationProps {
onClose?: (currentPage: number) => void; onClose?: (currentPage: number) => void;
direction: "ltr" | "rtl"; direction: "ltr" | "rtl";
nextBook?: KomgaBook | null; nextBook?: KomgaBook | null;
isZoomed?: boolean;
} }
export const usePageNavigation = ({ export const usePageNavigation = ({
@@ -19,6 +20,7 @@ export const usePageNavigation = ({
onClose, onClose,
direction, direction,
nextBook, nextBook,
isZoomed = false,
}: UsePageNavigationProps) => { }: UsePageNavigationProps) => {
const router = useRouter(); const router = useRouter();
const cPage = ClientOfflineBookService.getCurrentPage(book); const cPage = ClientOfflineBookService.getCurrentPage(book);
@@ -133,25 +135,22 @@ export const usePageNavigation = ({
const handleTouchStart = useCallback( const handleTouchStart = useCallback(
(event: TouchEvent) => { (event: TouchEvent) => {
// Ne gérer que les gestes à un seul doigt // Si on est zoomé, on ne gère pas la navigation
if (event.touches.length === 1) { if (isZoomed) {
touchStartXRef.current = event.touches[0].clientX;
touchStartYRef.current = event.touches[0].clientY;
currentPageRef.current = currentPage;
} else {
// Reset les références pour les gestes multi-touch (pinch)
touchStartXRef.current = null; touchStartXRef.current = null;
touchStartYRef.current = null; touchStartYRef.current = null;
return;
} }
touchStartXRef.current = event.touches[0].clientX;
touchStartYRef.current = event.touches[0].clientY;
currentPageRef.current = currentPage;
}, },
[currentPage] [currentPage, isZoomed]
); );
const handleTouchEnd = useCallback( const handleTouchEnd = useCallback(
(event: TouchEvent) => { (event: TouchEvent) => {
// Ne traiter que les gestes à un seul doigt
if (event.touches.length > 1) return;
if (touchStartXRef.current === null || touchStartYRef.current === null) return; if (touchStartXRef.current === null || touchStartYRef.current === null) return;
const touchEndX = event.changedTouches[0].clientX; const touchEndX = event.changedTouches[0].clientX;
@@ -163,7 +162,7 @@ export const usePageNavigation = ({
// on ne fait rien (pour éviter de confondre avec un scroll) // on ne fait rien (pour éviter de confondre avec un scroll)
if (Math.abs(deltaY) > Math.abs(deltaX)) return; if (Math.abs(deltaY) > Math.abs(deltaX)) return;
// Augmenter le seuil pour éviter les changements de page accidentels // Seuil pour éviter les changements de page accidentels
if (Math.abs(deltaX) > 100) { if (Math.abs(deltaX) > 100) {
if (deltaX > 0) { if (deltaX > 0) {
// Swipe vers la droite // Swipe vers la droite