update book viewer auto play
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user