From 85ba8e4261d62ec6f7f2f4fe2b3f31ea8775085b Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 1 Jul 2026 11:17:08 +0330 Subject: [PATCH] magnifier --- src/index.css | 5 + src/pages/viewer/components/BookPage.tsx | 13 +- src/pages/viewer/components/BookViewer.tsx | 86 +++++++-- .../components/Magnifier/Magnifier.module.css | 48 +++++ .../viewer/components/Magnifier/Magnifier.tsx | 113 ++++++++++++ .../viewer/components/Magnifier/constants.ts | 2 + .../components/Magnifier/magnifierUtils.ts | 121 +++++++++++++ .../components/Magnifier/useMagnifier.ts | 168 ++++++++++++++++++ .../viewer/components/Toolbar/ZoomButton.tsx | 33 ++++ src/pages/viewer/utils/pageBackground.ts | 18 ++ 10 files changed, 585 insertions(+), 22 deletions(-) create mode 100644 src/pages/viewer/components/Magnifier/Magnifier.module.css create mode 100644 src/pages/viewer/components/Magnifier/Magnifier.tsx create mode 100644 src/pages/viewer/components/Magnifier/constants.ts create mode 100644 src/pages/viewer/components/Magnifier/magnifierUtils.ts create mode 100644 src/pages/viewer/components/Magnifier/useMagnifier.ts create mode 100644 src/pages/viewer/components/Toolbar/ZoomButton.tsx create mode 100644 src/pages/viewer/utils/pageBackground.ts diff --git a/src/index.css b/src/index.css index fa55163..a1cc7e1 100644 --- a/src/index.css +++ b/src/index.css @@ -271,3 +271,8 @@ html { pointer-events: auto; z-index: 100; } + +/* حالت ذره‌بین: همهٔ صفحات visible در اسپرد قابل hit-test باشند */ +.flipbook-container.flipbook-magnifier-active .stf__item { + pointer-events: auto; +} diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index cc58033..6deea19 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -62,6 +62,12 @@ type BookPageProps = { showPaperShadow?: boolean; /** ورق سخت به‌جای clip-path برای iOS قدیمی */ useHardPage?: boolean; + /** + * پسوند یکتا برای idهای SVG (گرادیان و غیره). + * وقتی همان صفحه بیش از یک‌بار هم‌زمان رندر می‌شود (مثلاً در Magnifier) + * باید idها متفاوت باشند تا با نسخهٔ اصلی تداخل نکنند. + */ + idSuffix?: string; }; /** @@ -72,7 +78,7 @@ type BookPageProps = { * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند */ const BookPage = forwardRef( - ({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, showPaperShadow = true, useHardPage = false }, ref) => { + ({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, entrancePhase = 'idle', disableEntranceAnimations = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => { const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase; // تابع برای تبدیل opacity به رنگ (مثل editor) // در editor، getColorWithOpacity انتظار opacity 0-100 دارد @@ -448,7 +454,7 @@ const BookPage = forwardRef( const strokeWidth = hasStroke && obj.stroke ? actualStrokeWidth * scale : 0; const fillColor = obj.fill || '#000000'; const strokeColor = obj.stroke || 'transparent'; - const gradientId = `triangle-grad-${obj.id}-${index}`; + const gradientId = `triangle-grad-${obj.id}-${index}${idSuffix}`; return ( ( const strokeWidth = hasStroke && obj.stroke ? actualStrokeWidth * scale : 0; const fillColor = obj.fill || '#3b82f6'; const strokeColor = obj.stroke || 'transparent'; - const gradientId = `custom-shape-grad-${obj.id}-${index}`; + const gradientId = `custom-shape-grad-${obj.id}-${index}${idSuffix}`; const gradientEndpoints = obj.fillType === 'gradient' && obj.gradient ? getSvgGradientEndpoints(obj.gradient, width, height, 'rect') @@ -894,6 +900,7 @@ const BookPage = forwardRef( ref={ref} dir="rtl" className="page" + data-page-id={page.id} data-density={useHardPage ? 'hard' : 'soft'} style={{ position: 'relative', diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx index 097ba4f..912fed7 100644 --- a/src/pages/viewer/components/BookViewer.tsx +++ b/src/pages/viewer/components/BookViewer.tsx @@ -5,12 +5,15 @@ import { useBookEntranceController } from "@/pages/viewer/hooks/useBookEntranceC import { useIsBrowserFullscreen } from "@/pages/viewer/hooks/useIsBrowserFullscreen"; import { useViewerViewport } from "@/pages/viewer/hooks/useViewerViewport"; import { isLegacyIOSSafari } from "@/pages/viewer/utils/isLegacyIOSSafari"; +import { resolvePageBackground } from "@/pages/viewer/utils/pageBackground"; import { toggleViewerFullscreen } from "@/pages/viewer/utils/viewerFullscreen"; import { ArrowLeft2, ArrowRight2, Maximize4, Pause, Play, RowVertical } from "iconsax-react"; import { type FC, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import HTMLFlipBook from "react-pageflip"; import { type PageData } from "../types"; import BookPage from "./BookPage"; +import Magnifier from "./Magnifier/Magnifier"; +import ZoomButton from "./Toolbar/ZoomButton"; /** فاصله پیش‌فرض بین هر ورق در پخش خودکار (میلی‌ثانیه) */ const AUTO_PLAY_INTERVAL_MS = 3000; @@ -118,10 +121,13 @@ function triggerFlipPrev(api: PageFlipWithFlipController | undefined) { */ const BookViewer: FC = ({ pages, catalogSize, documentSettings }) => { const bookRef = useRef(null); + const bookAreaRef = useRef(null); + const flipbookWrapperRef = useRef(null); const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {}); const [currentPage, setCurrentPage] = useState(0); const [isAutoPlayActive, setIsAutoPlayActive] = useState(false); const [startPage, setStartPage] = useState(0); + const [magnifierEnabled, setMagnifierEnabled] = useState(false); const isDesktop = useIsDesktop(); const legacyIOS = useLegacyIOSSafari(); const isBrowserFullscreen = useIsBrowserFullscreen(); @@ -139,6 +145,10 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff"; const backgroundGradient = documentSettings?.backgroundGradient; const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? ""; + const pageBackgroundDefaults = useMemo( + () => ({ backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl }), + [backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl], + ); useEffect(() => { setIsAutoPlayActive(autoPlay); @@ -359,6 +369,21 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings setIsAutoPlayActive((prev) => !prev); }, [pages.length]); + const toggleMagnifier = useCallback(() => { + setMagnifierEnabled((prev) => !prev); + }, []); + + useEffect(() => { + if (!magnifierEnabled) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setMagnifierEnabled(false); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [magnifierEnabled]); + const navigateToPageIndex = useCallback( (targetIndex: number) => { const pageFlipAPI = bookRef.current?.pageFlip(); @@ -441,7 +466,7 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings return (
-
+
{/* Border و Shadow عمودی در وسط (فقط حالت دوصفحه‌ای دسکتاپ) */} {!usePortrait && (
= ({ pages, catalogSize, documentSettings ایندکس/onFlip با دکمه‌ها می‌شود. محتوای فارسی داخل BookPage با dir=rtl است. */}
= ({ pages, catalogSize, documentSettings showCover={hasMultiplePages} mobileScrollSupport={!usePortrait} clickEventForward={true} - useMouseEvents={hasMultiplePages} + useMouseEvents={hasMultiplePages && !magnifierEnabled} swipeDistance={hasMultiplePages ? (usePortrait ? 48 : 30) : 9999} startZIndex={1} showPageCorners={hasMultiplePages} disableFlipByClick={true} - className={clx("flipbook-container mx-auto max-w-full", legacyIOS && "flipbook-legacy-ios")} + className={clx( + "flipbook-container mx-auto max-w-full", + legacyIOS && "flipbook-legacy-ios", + magnifierEnabled && "flipbook-magnifier-active", + )} style={{}} onFlip={onFlipForwarded} onInit={handleBookInit} onChangeState={handleChangeState} > - {displayPages.map((page) => ( - - ))} + {displayPages.map((page) => { + const background = resolvePageBackground(page, pageBackgroundDefaults); + return ( + + ); + })}
+
@@ -570,6 +616,8 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings {hasMultiplePages && autoPlay && } + + +); + +export default ZoomButton; diff --git a/src/pages/viewer/utils/pageBackground.ts b/src/pages/viewer/utils/pageBackground.ts new file mode 100644 index 0000000..de2710d --- /dev/null +++ b/src/pages/viewer/utils/pageBackground.ts @@ -0,0 +1,18 @@ +import type { PageData } from "@/pages/viewer/types"; + +export type PageBackgroundDefaults = { + backgroundType?: "color" | "gradient" | "image"; + backgroundColor?: string; + backgroundGradient?: { from: string; to: string; angle: number }; + backgroundImageUrl?: string; +}; + +/** ترکیب پس‌زمینهٔ اختصاصی صفحه با مقادیر پیش‌فرض سند (همان قاعدهٔ استفاده‌شده در BookViewer) */ +export function resolvePageBackground(page: PageData, defaults: PageBackgroundDefaults): Required { + return { + backgroundType: page.backgroundType ?? defaults.backgroundType ?? "color", + backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff", + backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 }, + backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "", + }; +}