update book viewer auto play

This commit is contained in:
hamid zarghami
2026-05-06 14:44:45 +03:30
parent 192036e519
commit b1068eec26
3 changed files with 95 additions and 17 deletions
+8 -2
View File
@@ -2,6 +2,7 @@ import { type FC, useEffect, useState } from 'react';
import BookViewer from './components/BookViewer';
import { transformViewerDataToPages } from './utils/dataTransformer';
import type { PageData } from './types';
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
import { useParams } from 'react-router-dom';
import { useGetCatalogById } from '@/pages/home/hooks/useHomeData';
@@ -9,16 +10,21 @@ const Viewer: FC = () => {
const { id } = useParams<{ id: string }>();
const { data, isLoading, error: queryError } = useGetCatalogById(id ?? '');
const [pages, setPages] = useState<PageData[]>([]);
const [documentSettings, setDocumentSettings] = useState<Partial<DocumentSettings> | undefined>(undefined);
useEffect(() => {
if (!data?.data?.content || !id) return;
try {
const parsed = JSON.parse(data.data.content);
const pagesArray = Array.isArray(parsed) ? parsed : [];
const pagesArray = Array.isArray(parsed) ? parsed : (parsed.pages ?? []);
const settings: Partial<DocumentSettings> | undefined = Array.isArray(parsed)
? undefined
: parsed.documentSettings;
const transformedPages = transformViewerDataToPages(
{ pages: pagesArray } as Parameters<typeof transformViewerDataToPages>[0]
);
setPages(transformedPages);
setDocumentSettings(settings);
} catch (err) {
console.error('خطا در پردازش محتوای کاتالوگ:', err);
}
@@ -58,7 +64,7 @@ const Viewer: FC = () => {
return (
<div className="w-full h-full">
<BookViewer pages={pages} catalogSize={data?.data?.size} />
<BookViewer pages={pages} catalogSize={data?.data?.size} documentSettings={documentSettings} />
</div>
);
};
+15 -2
View File
@@ -8,6 +8,9 @@ type BookPageProps = {
pageWidth?: number;
pageHeight?: number;
onLinkClick?: (linkUrl: string) => void;
backgroundType?: 'color' | 'image';
backgroundColor?: string;
backgroundImageUrl?: string;
};
/**
@@ -18,7 +21,7 @@ type BookPageProps = {
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
*/
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
({ page, scale = 1, pageWidth, pageHeight, onLinkClick }, ref) => {
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType = 'color', backgroundColor = '#ffffff', backgroundImageUrl = '' }, ref) => {
// تابع برای تبدیل opacity به رنگ (مثل editor)
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
@@ -477,6 +480,16 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
const width = pageWidth ?? 794 * scale;
const height = pageHeight ?? 1123 * scale;
const bgStyle: React.CSSProperties =
backgroundType === 'image' && backgroundImageUrl
? {
backgroundImage: `url(${backgroundImageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}
: { backgroundColor: backgroundColor || '#ffffff' };
return (
<div
ref={ref}
@@ -485,7 +498,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
data-density="soft"
style={{
position: 'relative',
backgroundColor: '#ffffff',
...bgStyle,
overflow: 'hidden',
boxSizing: 'border-box',
width: `${width}px`,
+72 -13
View File
@@ -4,6 +4,10 @@ import { type PageData } from '../types';
import BookPage from './BookPage';
import { ArrowLeft2, ArrowRight2 } from 'iconsax-react';
import { getPaperDimensions } from '@/config/paperSizes';
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
/** فاصله پیش‌فرض بین هر ورق در پخش خودکار (میلی‌ثانیه) */
const AUTO_PLAY_INTERVAL_MS = 3000;
const VIEWER_SCALE = 0.5;
/** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشت‌هم دارد؛ فقط تا این سهم از طول پخش می‌شود */
@@ -28,6 +32,7 @@ function useIsDesktop() {
export type BookViewerProps = {
pages: PageData[];
catalogSize?: string;
documentSettings?: Partial<DocumentSettings>;
};
interface PageFlipAPI {
@@ -77,12 +82,26 @@ function triggerFlipPrev(api: PageFlipWithFlipController | undefined) {
* این کامپوننت از HTMLFlipBook استفاده می‌کند که یک wrapper برای PageFlip است
* از HTML برای رندر صفحات استفاده می‌شود (نه Canvas) برای سازگاری با Konva در آینده
*/
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings }) => {
const bookRef = useRef<FlipBookInstance | null>(null);
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
const [currentPage, setCurrentPage] = useState(0);
const [isAutoPlayActive, setIsAutoPlayActive] = useState(false);
const [startPage, setStartPage] = useState(0);
const isDesktop = useIsDesktop();
const autoPlay = documentSettings?.autoPlay ?? false;
const pageFlipSound = documentSettings?.pageFlipSound ?? true;
const leftToRightFlip = documentSettings?.leftToRightFlip ?? false;
const displayStyle = documentSettings?.displayStyle ?? 'double';
const backgroundType = documentSettings?.backgroundType ?? 'color';
const backgroundColor = documentSettings?.backgroundColor ?? '#ffffff';
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? '';
useEffect(() => {
setIsAutoPlayActive(autoPlay);
}, [autoPlay]);
// سایز کتاب بر اساس catalogSize (A3/A4/A5)
const { width: bookWidth, height: bookHeight } = useMemo(
() => getPaperDimensions(catalogSize ?? 'a4'),
@@ -210,13 +229,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
pageNum = pages.length - 1;
}
if (lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) {
if (pageFlipSound && lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) {
playPageFlipSound();
}
lastFlipPageRef.current = pageNum;
setCurrentPage(pageNum);
}, [pages.length, playPageFlipSound]);
}, [pages.length, pageFlipSound, playPageFlipSound]);
pageFlipHandlerRef.current = handlePageFlip;
@@ -264,6 +283,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
triggerFlipPrev(api);
}, [currentPage]);
const stopAutoPlayByUser = useCallback(() => {
if (isAutoPlayActive) {
setIsAutoPlayActive(false);
}
}, [isAutoPlayActive]);
const handleLinkClick = useCallback((linkUrl: string) => {
if (linkUrl.startsWith('page://')) {
const action = linkUrl.replace('page://', '');
@@ -339,16 +364,33 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
}
}, [currentPage, pages.length, goToNextPage, goToPrevPage]);
const usePortrait = !isDesktop;
// تک‌صفحه‌ای: موبایل یا تنظیم single، دوصفحه‌ای: دسکتاپ + تنظیم double
const usePortrait = !isDesktop || displayStyle === 'single';
const prevFlipKeyRef = useRef(flipKey);
useEffect(() => {
if (prevFlipKeyRef.current !== flipKey) {
prevFlipKeyRef.current = flipKey;
lastFlipPageRef.current = currentPage;
setStartPage(currentPage);
}
}, [flipKey, currentPage]);
// پخش خودکار
useEffect(() => {
if (!isAutoPlayActive || pages.length <= 1) return;
const timer = setInterval(() => {
if (currentPage >= pages.length - 1) {
bookRef.current?.pageFlip()?.turnToPage?.(0);
setCurrentPage(0);
lastFlipPageRef.current = 0;
return;
}
bookRef.current?.pageFlip()?.flipNext();
}, AUTO_PLAY_INTERVAL_MS);
return () => clearInterval(timer);
}, [isAutoPlayActive, currentPage, pages.length]);
return (
<div
className="flex flex-col items-center gap-4 md:gap-6 w-full h-full max-md:min-h-0 max-md:max-w-full max-md:overflow-x-hidden max-md:pb-[env(safe-area-inset-bottom,0px)]"
@@ -387,7 +429,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
drawShadow={isDesktop}
flippingTime={800}
usePortrait={usePortrait}
startPage={currentPage}
startPage={startPage}
autoSize={false}
maxShadowOpacity={isDesktop ? 0.5 : 0}
showCover={true}
@@ -412,6 +454,9 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
pageWidth={pagePixelWidth}
pageHeight={pagePixelHeight}
onLinkClick={handleLinkClick}
backgroundType={backgroundType}
backgroundColor={backgroundColor}
backgroundImageUrl={backgroundImageUrl}
/>
))}
</HTMLFlipBook>
@@ -419,13 +464,20 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
</div>
<div className="shrink-0 max-md:min-h-12 flex items-center gap-3 md:gap-6 bg-white rounded-full px-4 md:px-6 py-2 md:py-3 shadow-lg">
{/* دکمه صفحه بعد (در RTL، صفحه سمت چپ) */}
{/* دکمه راست: فارسی = صفحه بعد | لاتین = صفحه قبل */}
<button
type="button"
onClick={goToNextPage}
disabled={currentPage >= pages.length - 1}
onClick={() => {
stopAutoPlayByUser();
if (leftToRightFlip) {
goToPrevPage();
} else {
goToNextPage();
}
}}
disabled={leftToRightFlip ? currentPage <= 0 : currentPage >= pages.length - 1}
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
aria-label="صفحه بعد"
aria-label={leftToRightFlip ? 'صفحه قبل' : 'صفحه بعد'}
>
<ArrowRight2 color='black' size={20} className="text-gray-700 md:w-6 md:h-6" />
</button>
@@ -437,13 +489,20 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
</span>
</div>
{/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */}
{/* دکمه چپ: فارسی = صفحه قبل | لاتین = صفحه بعد */}
<button
type="button"
onClick={goToPrevPage}
disabled={currentPage <= 0}
onClick={() => {
stopAutoPlayByUser();
if (leftToRightFlip) {
goToNextPage();
} else {
goToPrevPage();
}
}}
disabled={leftToRightFlip ? currentPage >= pages.length - 1 : currentPage <= 0}
className="p-2 md:p-2 min-h-11 min-w-11 md:min-h-0 md:min-w-0 inline-flex items-center justify-center rounded-full hover:bg-gray-100 active:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
aria-label="صفحه قبل"
aria-label={leftToRightFlip ? 'صفحه بعد' : 'صفحه قبل'}
>
<ArrowLeft2 color='black' size={20} className="text-gray-700 md:w-6 md:h-6" />
</button>