From 4af718138465ac84210c9ecbaf0138020758a230 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 1 Jul 2026 16:17:03 +0330 Subject: [PATCH] fix animation --- src/pages/viewer/components/BookViewer.tsx | 47 ++++++++++++++----- .../viewer/hooks/useBookEntranceController.ts | 29 ++++++------ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx index 6e0c9c8..7a075a0 100644 --- a/src/pages/viewer/components/BookViewer.tsx +++ b/src/pages/viewer/components/BookViewer.tsx @@ -6,6 +6,7 @@ import { useIsBrowserFullscreen } from "@/pages/viewer/hooks/useIsBrowserFullscr import { useViewerViewport } from "@/pages/viewer/hooks/useViewerViewport"; import { isLegacyIOSSafari } from "@/pages/viewer/utils/isLegacyIOSSafari"; import { resolvePageBackground } from "@/pages/viewer/utils/pageBackground"; +import { getVisiblePageIndices } from "@/pages/viewer/utils/visiblePageIndices"; 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"; @@ -222,22 +223,44 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}-${flipIndexCount}`; const { - scheduleEntranceForSpread, + scheduleEntranceForIndices, getEntrancePhase, reset: resetEntrance, - } = useBookEntranceController({ - pages, - portrait: usePortrait, - showCover: hasMultiplePages, - }); + } = useBookEntranceController({ pages }); + + /** + * ایندکس‌های منطقیِ صفحات قابل‌مشاهده در اسپرد جاری را برمی‌گرداند. + * جفت‌سازی (کدام دو صفحه با هم دیده می‌شوند) باید در فضای نمایشیِ کتاب + * (flip index) انجام شود، نه فضای منطقی: در کتاب RTL این دو فضا برعکس + * هم‌اند، پس مثلاً «ایندکس منطقی + ۱» لزوماً هم‌صفحه‌ی واقعی نیست. + */ + const getVisibleLogicalIndices = useCallback( + (flipIdx: number): number[] => { + const displayIndices = getVisiblePageIndices(flipIdx, flipIndexCount, { + portrait: usePortrait, + showCover: hasMultiplePages, + }); + const seen = new Set(); + const result: number[] = []; + for (const di of displayIndices) { + const logical = toLogicalIndex(di); + if (logical >= 0 && logical < pages.length && !seen.has(logical)) { + seen.add(logical); + result.push(logical); + } + } + return result; + }, + [flipIndexCount, usePortrait, hasMultiplePages, toLogicalIndex, pages.length], + ); const commitEntranceAtCurrentSpread = useCallback(() => { const api = bookRef.current?.pageFlip(); const idx = api?.getCurrentPageIndex?.(); if (typeof idx !== "number" || flipIndexCount <= 0) return; const flipIdx = Math.max(0, Math.min(idx, flipIndexCount - 1)); - scheduleEntranceForSpread(toLogicalIndex(flipIdx)); - }, [flipIndexCount, scheduleEntranceForSpread, toLogicalIndex]); + scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx)); + }, [flipIndexCount, scheduleEntranceForIndices, getVisibleLogicalIndices]); useEffect(() => { resetEntrance(); @@ -359,9 +382,9 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings setCurrentPage(logicalIdx); lastFlipPageRef.current = logicalIdx; // اولین اسپرد قبل از onChangeState('read') هم باید انیمیشن ورود بگیرد - scheduleEntranceForSpread(logicalIdx); + scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx)); }, - [flipIndexCount, scheduleEntranceForSpread, toLogicalIndex], + [flipIndexCount, scheduleEntranceForIndices, getVisibleLogicalIndices, toLogicalIndex], ); /** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را می‌گیریم و انیمیشن ورود را یک‌بار شروع می‌کنیم */ @@ -429,9 +452,9 @@ const BookViewer: FC = ({ pages, catalogSize, documentSettings const flipIdx = toFlipIndex(logicalIdx); pageFlipAPI.turnToPage(flipIdx); lastFlipPageRef.current = logicalIdx; - scheduleEntranceForSpread(logicalIdx); + scheduleEntranceForIndices(getVisibleLogicalIndices(flipIdx)); }, - [pages.length, scheduleEntranceForSpread, toFlipIndex], + [pages.length, scheduleEntranceForIndices, getVisibleLogicalIndices, toFlipIndex], ); const handleLinkClick = useCallback( diff --git a/src/pages/viewer/hooks/useBookEntranceController.ts b/src/pages/viewer/hooks/useBookEntranceController.ts index 744b333..afe9b32 100644 --- a/src/pages/viewer/hooks/useBookEntranceController.ts +++ b/src/pages/viewer/hooks/useBookEntranceController.ts @@ -3,7 +3,6 @@ import { ENTRANCE_ANIMATION_BASE_DELAY_MS, MAX_ENTRANCE_DELAY_MS, } from "@/shared/entranceAnimation"; -import { getVisiblePageIndices } from "@/pages/viewer/utils/visiblePageIndices"; /** حداکثر زمان پخش انیمیشن ورود (برای برداشتن حالت play) */ const MAX_ENTRANCE_PLAY_MS = @@ -18,15 +17,18 @@ type PageRef = { id: number }; type Options = { pages: PageRef[]; - portrait: boolean; - showCover: boolean; }; /** * کنترل یک‌بار پخش انیمیشن ورود هر صفحه — state در والد نگه‌داری می‌شود * تا با remount شدن BookPage هنگام ورق‌خوردن دوباره اجرا نشود. + * + * محاسبهٔ اینکه «کدام صفحات منطقی در اسپرد جاری قابل‌مشاهده‌اند» بر عهدهٔ صدا‌زننده + * (BookViewer) است: در کتاب RTL، ایندکس نمایشی (flip index) و ایندکس منطقی صفحه + * برعکس هم می‌شوند، پس جفت‌سازی باید در فضای نمایشی انجام و سپس به منطقی تبدیل شود. + * اینجا فقط لیست ایندکس‌های منطقیِ از قبل‌محاسبه‌شده را می‌گیرد. */ -export function useBookEntranceController({ pages, portrait, showCover }: Options) { +export function useBookEntranceController({ pages }: Options) { const playedPageIdsRef = useRef(new Set()); const [playingPageIds, setPlayingPageIds] = useState>(() => new Set()); const playTimersRef = useRef>>(new Map()); @@ -49,20 +51,17 @@ export function useBookEntranceController({ pages, portrait, showCover }: Option }); }, [clearPlayTimer]); - const scheduleEntranceForSpread = useCallback( - (currentIndex: number) => { - const indices = getVisiblePageIndices(currentIndex, pages.length, { - portrait, - showCover, - }); - + const scheduleEntranceForIndices = useCallback( + (visibleLogicalIndices: number[]) => { const visibleIds = new Set( - indices.map((i) => pages[i]?.id).filter((id): id is number => id != null), + visibleLogicalIndices + .map((i) => pages[i]?.id) + .filter((id): id is number => id != null), ); const newlyPlaying: number[] = []; - for (const i of indices) { + for (const i of visibleLogicalIndices) { const page = pages[i]; if (!page) continue; if (playedPageIdsRef.current.has(page.id)) continue; @@ -96,7 +95,7 @@ export function useBookEntranceController({ pages, portrait, showCover }: Option ); } }, - [pages, portrait, showCover, clearPlayTimer, finishPlaying], + [pages, clearPlayTimer, finishPlaying], ); const getEntrancePhase = useCallback( @@ -117,7 +116,7 @@ export function useBookEntranceController({ pages, portrait, showCover }: Option }, [clearPlayTimer]); return { - scheduleEntranceForSpread, + scheduleEntranceForIndices, getEntrancePhase, reset, };