From 3c764a1652fda423c0807b6adec6fe5ae9c762e0 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 5 Jul 2026 16:26:37 +0330 Subject: [PATCH] spinner to loading page compeletely --- src/pages/viewer/components/BookPage.tsx | 135 +++++++++++++----- src/pages/viewer/components/BookViewer.tsx | 2 + .../viewer/components/Magnifier/Magnifier.tsx | 1 + .../viewer/hooks/useBookEntranceController.ts | 22 +++ src/pages/viewer/hooks/usePageMediaReady.ts | 69 +++++++++ src/pages/viewer/utils/pageMediaUrls.ts | 6 +- 6 files changed, 195 insertions(+), 40 deletions(-) create mode 100644 src/pages/viewer/hooks/usePageMediaReady.ts diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index d03f85e..33f9815 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -1,4 +1,4 @@ -import { forwardRef, memo } from 'react'; +import { forwardRef, memo, useEffect, useRef } from 'react'; import { type PageData } from '../types'; import type { EditorObject } from '@/pages/editor/store/editorStore'; import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/utils/gradient'; @@ -17,6 +17,7 @@ import '@/pages/viewer/styles/entranceAnimations.css'; import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle'; import { getMaskImageStyle, getMaskedLayout } from '@/pages/viewer/utils/maskStyle'; import type { EntrancePhase } from '@/pages/viewer/hooks/useBookEntranceController'; +import { usePageMediaReady } from '@/pages/viewer/hooks/usePageMediaReady'; const getRasterObjectStyle = ( obj: EditorObject, @@ -67,6 +68,10 @@ type BookPageProps = { entrancePhase?: EntrancePhase; /** در پیش‌نمایش کاتالوگ انیمی션 ورود غیرفعال باشد */ disableEntranceAnimations?: boolean; + /** وقتی رسانهٔ صفحه دیرتر از زمان‌بندی انیمیشن لود شد */ + onDeferredEntranceReady?: (pageId: number) => void; + /** لایهٔ اسپینر لود رسانه را نشان نده (مثلاً ذره‌بین) */ + hideMediaLoadingOverlay?: boolean; /** ویدیو/صوت فقط به‌صورت کاور — بدون کنترل و بدون کلیک */ staticMedia?: boolean; /** سایهٔ لبهٔ کاغذ (گرادیان داخل DOM — سازگار با iOS قدیمی) */ @@ -89,8 +94,40 @@ type BookPageProps = { * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند */ const BookPage = memo(forwardRef( - ({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => { + ({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, onDeferredEntranceReady, hideMediaLoadingOverlay = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => { + const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color'; + const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff'; + const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient; + const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? ''; + const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? ''; + + const isMediaReady = usePageMediaReady( + page, + { + backgroundType: effectiveBackgroundType, + backgroundImageUrl: effectiveBackgroundImageUrl, + backgroundVideoUrl: effectiveBackgroundVideoUrl, + }, + !hideMediaLoadingOverlay, + ); + const deferredEntranceRef = useRef(false); + const phase: EntrancePhase = disableEntranceAnimations ? 'settled' : entrancePhase; + const effectivePhase: EntrancePhase = + hideMediaLoadingOverlay || isMediaReady ? phase : 'idle'; + + useEffect(() => { + if (disableEntranceAnimations || hideMediaLoadingOverlay) return; + if (phase === 'play' && !isMediaReady) { + deferredEntranceRef.current = true; + } + }, [phase, isMediaReady, disableEntranceAnimations, hideMediaLoadingOverlay]); + + useEffect(() => { + if (!isMediaReady || !deferredEntranceRef.current) return; + deferredEntranceRef.current = false; + onDeferredEntranceReady?.(page.id); + }, [isMediaReady, onDeferredEntranceReady, page.id]); // تابع برای تبدیل opacity به رنگ (مثل editor) // در editor، getColorWithOpacity انتظار opacity 0-100 دارد // در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است @@ -134,7 +171,7 @@ const BookPage = memo(forwardRef( rotationDeg?: number; }, ) => - mergeEntranceAnimationStyle(style, obj, scale, index, phase, { + mergeEntranceAnimationStyle(style, obj, scale, index, effectivePhase, { ...extra, flyLayoutPx, }); @@ -982,12 +1019,6 @@ const BookPage = memo(forwardRef( const width = pageWidth ?? 794 * scale; const height = pageHeight ?? 1123 * scale; - const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color'; - const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff'; - const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient; - const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? ''; - const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? ''; - const bgStyle: React.CSSProperties = effectiveBackgroundType === 'image' && effectiveBackgroundImageUrl ? { @@ -1002,6 +1033,8 @@ const BookPage = memo(forwardRef( } : { backgroundColor: effectiveBackgroundColor }; + const showLoadingOverlay = !hideMediaLoadingOverlay && !isMediaReady; + return (
( width: `${width}px`, height: `${height}px`, zIndex: 1, - ...(useHardPage + ...(useHardPage && !showLoadingOverlay ? { backgroundColor: effectiveBackgroundColor, ...bgStyle } : {}), }} > - {!useHardPage && ( - <> -
- {effectiveBackgroundType === 'video' && effectiveBackgroundVideoUrl && ( -