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 BookViewer from './components/BookViewer';
|
||||||
import { transformViewerDataToPages } from './utils/dataTransformer';
|
import { transformViewerDataToPages } from './utils/dataTransformer';
|
||||||
import type { PageData } from './types';
|
import type { PageData } from './types';
|
||||||
|
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useGetCatalogById } from '@/pages/home/hooks/useHomeData';
|
import { useGetCatalogById } from '@/pages/home/hooks/useHomeData';
|
||||||
|
|
||||||
@@ -9,16 +10,21 @@ const Viewer: FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const { data, isLoading, error: queryError } = useGetCatalogById(id ?? '');
|
const { data, isLoading, error: queryError } = useGetCatalogById(id ?? '');
|
||||||
const [pages, setPages] = useState<PageData[]>([]);
|
const [pages, setPages] = useState<PageData[]>([]);
|
||||||
|
const [documentSettings, setDocumentSettings] = useState<Partial<DocumentSettings> | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data?.data?.content || !id) return;
|
if (!data?.data?.content || !id) return;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data.data.content);
|
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(
|
const transformedPages = transformViewerDataToPages(
|
||||||
{ pages: pagesArray } as Parameters<typeof transformViewerDataToPages>[0]
|
{ pages: pagesArray } as Parameters<typeof transformViewerDataToPages>[0]
|
||||||
);
|
);
|
||||||
setPages(transformedPages);
|
setPages(transformedPages);
|
||||||
|
setDocumentSettings(settings);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('خطا در پردازش محتوای کاتالوگ:', err);
|
console.error('خطا در پردازش محتوای کاتالوگ:', err);
|
||||||
}
|
}
|
||||||
@@ -58,7 +64,7 @@ const Viewer: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<BookViewer pages={pages} catalogSize={data?.data?.size} />
|
<BookViewer pages={pages} catalogSize={data?.data?.size} documentSettings={documentSettings} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ type BookPageProps = {
|
|||||||
pageWidth?: number;
|
pageWidth?: number;
|
||||||
pageHeight?: number;
|
pageHeight?: number;
|
||||||
onLinkClick?: (linkUrl: string) => void;
|
onLinkClick?: (linkUrl: string) => void;
|
||||||
|
backgroundType?: 'color' | 'image';
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundImageUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +21,7 @@ type BookPageProps = {
|
|||||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||||
*/
|
*/
|
||||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
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)
|
// تابع برای تبدیل opacity به رنگ (مثل editor)
|
||||||
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
|
||||||
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
|
||||||
@@ -477,6 +480,16 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
const width = pageWidth ?? 794 * scale;
|
const width = pageWidth ?? 794 * scale;
|
||||||
const height = pageHeight ?? 1123 * 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -485,7 +498,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
data-density="soft"
|
data-density="soft"
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#ffffff',
|
...bgStyle,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { type PageData } from '../types';
|
|||||||
import BookPage from './BookPage';
|
import BookPage from './BookPage';
|
||||||
import { ArrowLeft2, ArrowRight2 } from 'iconsax-react';
|
import { ArrowLeft2, ArrowRight2 } from 'iconsax-react';
|
||||||
import { getPaperDimensions } from '@/config/paperSizes';
|
import { getPaperDimensions } from '@/config/paperSizes';
|
||||||
|
import type { DocumentSettings } from '@/pages/editor/store/editorStore';
|
||||||
|
|
||||||
|
/** فاصله پیشفرض بین هر ورق در پخش خودکار (میلیثانیه) */
|
||||||
|
const AUTO_PLAY_INTERVAL_MS = 3000;
|
||||||
|
|
||||||
const VIEWER_SCALE = 0.5;
|
const VIEWER_SCALE = 0.5;
|
||||||
/** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشتهم دارد؛ فقط تا این سهم از طول پخش میشود */
|
/** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشتهم دارد؛ فقط تا این سهم از طول پخش میشود */
|
||||||
@@ -28,6 +32,7 @@ function useIsDesktop() {
|
|||||||
export type BookViewerProps = {
|
export type BookViewerProps = {
|
||||||
pages: PageData[];
|
pages: PageData[];
|
||||||
catalogSize?: string;
|
catalogSize?: string;
|
||||||
|
documentSettings?: Partial<DocumentSettings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PageFlipAPI {
|
interface PageFlipAPI {
|
||||||
@@ -77,12 +82,26 @@ function triggerFlipPrev(api: PageFlipWithFlipController | undefined) {
|
|||||||
* این کامپوننت از HTMLFlipBook استفاده میکند که یک wrapper برای PageFlip است
|
* این کامپوننت از HTMLFlipBook استفاده میکند که یک wrapper برای PageFlip است
|
||||||
* از HTML برای رندر صفحات استفاده میشود (نه Canvas) برای سازگاری با Konva در آینده
|
* از HTML برای رندر صفحات استفاده میشود (نه Canvas) برای سازگاری با Konva در آینده
|
||||||
*/
|
*/
|
||||||
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings }) => {
|
||||||
const bookRef = useRef<FlipBookInstance | null>(null);
|
const bookRef = useRef<FlipBookInstance | null>(null);
|
||||||
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
|
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [isAutoPlayActive, setIsAutoPlayActive] = useState(false);
|
||||||
|
const [startPage, setStartPage] = useState(0);
|
||||||
const isDesktop = useIsDesktop();
|
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)
|
// سایز کتاب بر اساس catalogSize (A3/A4/A5)
|
||||||
const { width: bookWidth, height: bookHeight } = useMemo(
|
const { width: bookWidth, height: bookHeight } = useMemo(
|
||||||
() => getPaperDimensions(catalogSize ?? 'a4'),
|
() => getPaperDimensions(catalogSize ?? 'a4'),
|
||||||
@@ -210,13 +229,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
pageNum = pages.length - 1;
|
pageNum = pages.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) {
|
if (pageFlipSound && lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) {
|
||||||
playPageFlipSound();
|
playPageFlipSound();
|
||||||
}
|
}
|
||||||
lastFlipPageRef.current = pageNum;
|
lastFlipPageRef.current = pageNum;
|
||||||
|
|
||||||
setCurrentPage(pageNum);
|
setCurrentPage(pageNum);
|
||||||
}, [pages.length, playPageFlipSound]);
|
}, [pages.length, pageFlipSound, playPageFlipSound]);
|
||||||
|
|
||||||
pageFlipHandlerRef.current = handlePageFlip;
|
pageFlipHandlerRef.current = handlePageFlip;
|
||||||
|
|
||||||
@@ -264,6 +283,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
triggerFlipPrev(api);
|
triggerFlipPrev(api);
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const stopAutoPlayByUser = useCallback(() => {
|
||||||
|
if (isAutoPlayActive) {
|
||||||
|
setIsAutoPlayActive(false);
|
||||||
|
}
|
||||||
|
}, [isAutoPlayActive]);
|
||||||
|
|
||||||
const handleLinkClick = useCallback((linkUrl: string) => {
|
const handleLinkClick = useCallback((linkUrl: string) => {
|
||||||
if (linkUrl.startsWith('page://')) {
|
if (linkUrl.startsWith('page://')) {
|
||||||
const action = linkUrl.replace('page://', '');
|
const action = linkUrl.replace('page://', '');
|
||||||
@@ -339,16 +364,33 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
}
|
}
|
||||||
}, [currentPage, pages.length, goToNextPage, goToPrevPage]);
|
}, [currentPage, pages.length, goToNextPage, goToPrevPage]);
|
||||||
|
|
||||||
const usePortrait = !isDesktop;
|
// تکصفحهای: موبایل یا تنظیم single، دوصفحهای: دسکتاپ + تنظیم double
|
||||||
|
const usePortrait = !isDesktop || displayStyle === 'single';
|
||||||
|
|
||||||
const prevFlipKeyRef = useRef(flipKey);
|
const prevFlipKeyRef = useRef(flipKey);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevFlipKeyRef.current !== flipKey) {
|
if (prevFlipKeyRef.current !== flipKey) {
|
||||||
prevFlipKeyRef.current = flipKey;
|
prevFlipKeyRef.current = flipKey;
|
||||||
lastFlipPageRef.current = currentPage;
|
lastFlipPageRef.current = currentPage;
|
||||||
|
setStartPage(currentPage);
|
||||||
}
|
}
|
||||||
}, [flipKey, 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 (
|
return (
|
||||||
<div
|
<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)]"
|
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}
|
drawShadow={isDesktop}
|
||||||
flippingTime={800}
|
flippingTime={800}
|
||||||
usePortrait={usePortrait}
|
usePortrait={usePortrait}
|
||||||
startPage={currentPage}
|
startPage={startPage}
|
||||||
autoSize={false}
|
autoSize={false}
|
||||||
maxShadowOpacity={isDesktop ? 0.5 : 0}
|
maxShadowOpacity={isDesktop ? 0.5 : 0}
|
||||||
showCover={true}
|
showCover={true}
|
||||||
@@ -412,6 +454,9 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
pageWidth={pagePixelWidth}
|
pageWidth={pagePixelWidth}
|
||||||
pageHeight={pagePixelHeight}
|
pageHeight={pagePixelHeight}
|
||||||
onLinkClick={handleLinkClick}
|
onLinkClick={handleLinkClick}
|
||||||
|
backgroundType={backgroundType}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
backgroundImageUrl={backgroundImageUrl}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</HTMLFlipBook>
|
</HTMLFlipBook>
|
||||||
@@ -419,13 +464,20 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
</div>
|
</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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={goToNextPage}
|
onClick={() => {
|
||||||
disabled={currentPage >= pages.length - 1}
|
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"
|
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" />
|
<ArrowRight2 color='black' size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -437,13 +489,20 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */}
|
{/* دکمه چپ: فارسی = صفحه قبل | لاتین = صفحه بعد */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={goToPrevPage}
|
onClick={() => {
|
||||||
disabled={currentPage <= 0}
|
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"
|
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" />
|
<ArrowLeft2 color='black' size={20} className="text-gray-700 md:w-6 md:h-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user