This commit is contained in:
@@ -26,6 +26,8 @@ const BOOK_TOP_GAP_MOBILE = 32;
|
||||
const PAGE_FLIP_SOUND_PLAY_FRACTION = 0.5;
|
||||
/** همراستا با `md` تیلویند (768px) — موبایل: زیر این عرض */
|
||||
const MOBILE_QUERY = "(min-width: 768px)";
|
||||
/** شناسهٔ صفحهٔ خالیِ کمکی برای اصلاح جفتسازی جلد در کتاب RTL با تعداد صفحات فرد (هرگز به کاربر نمایش داده نمیشود) */
|
||||
const RTL_COVER_PAD_PAGE_ID = -1;
|
||||
|
||||
function subscribeDesktopMql(callback: () => void) {
|
||||
const mq = window.matchMedia(MOBILE_QUERY);
|
||||
@@ -135,12 +137,47 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const autoPlay = documentSettings?.autoPlay ?? false;
|
||||
const pageFlipSound = documentSettings?.pageFlipSound ?? true;
|
||||
const leftToRightFlip = documentSettings?.leftToRightFlip ?? false;
|
||||
const displayStyle = documentSettings?.displayStyle ?? "double";
|
||||
const usePortrait = !isDesktop || displayStyle === "single";
|
||||
/** react-pageflip از RTL پشتیبانی نمیکند؛ ترتیب صفحات برعکس + شروع از آخر (فقط دسکتاپ) */
|
||||
const isRtlBook = !leftToRightFlip && isDesktop;
|
||||
const displayPages = useMemo(() => (isRtlBook ? [...pages].reverse() : pages), [pages, isRtlBook]);
|
||||
const toLogicalIndex = useCallback((flipIndex: number) => (isRtlBook ? pages.length - 1 - flipIndex : flipIndex), [isRtlBook, pages.length]);
|
||||
const toFlipIndex = useCallback((logicalIndex: number) => (isRtlBook ? pages.length - 1 - logicalIndex : logicalIndex), [isRtlBook, pages.length]);
|
||||
const displayStyle = documentSettings?.displayStyle ?? "double";
|
||||
/**
|
||||
* page-flip فقط صفحهٔ با ایندکس 0 را بهعنوان جلد تکی در نظر میگیرد. بعد از برعکسکردن
|
||||
* آرایه برای RTL، جلد واقعی (صفحهٔ منطقی 0) در انتهای آرایه قرار میگیرد و اگر تعداد کل
|
||||
* صفحات فرد باشد، جفتسازی داخلی کتابخانه آن را با صفحهٔ بعدی هماسپرد میکند (بهجای تکی).
|
||||
* با افزودن یک صفحهٔ خالی نامرئی به ابتدای آرایهٔ برعکسشده، توازن جفتسازی طوری اصلاح میشود
|
||||
* که جلد واقعی همیشه تکی نمایش داده شود.
|
||||
*/
|
||||
const needsRtlCoverPad = isRtlBook && !usePortrait && pages.length > 1 && pages.length % 2 === 1;
|
||||
const displayPages = useMemo(() => {
|
||||
if (!isRtlBook) return pages;
|
||||
const reversed = [...pages].reverse();
|
||||
if (!needsRtlCoverPad) return reversed;
|
||||
const fillerPage: PageData = {
|
||||
id: RTL_COVER_PAD_PAGE_ID,
|
||||
width: pages[0]?.width ?? 794,
|
||||
height: pages[0]?.height ?? 1123,
|
||||
elements: [],
|
||||
backgroundType: "color",
|
||||
backgroundColor: "#ffffff",
|
||||
};
|
||||
return [fillerPage, ...reversed];
|
||||
}, [pages, isRtlBook, needsRtlCoverPad]);
|
||||
const flipIndexCount = displayPages.length;
|
||||
const toLogicalIndex = useCallback(
|
||||
(flipIndex: number) => {
|
||||
if (!isRtlBook) return flipIndex;
|
||||
return needsRtlCoverPad ? pages.length - flipIndex : pages.length - 1 - flipIndex;
|
||||
},
|
||||
[isRtlBook, needsRtlCoverPad, pages.length],
|
||||
);
|
||||
const toFlipIndex = useCallback(
|
||||
(logicalIndex: number) => {
|
||||
if (!isRtlBook) return logicalIndex;
|
||||
return needsRtlCoverPad ? pages.length - logicalIndex : pages.length - 1 - logicalIndex;
|
||||
},
|
||||
[isRtlBook, needsRtlCoverPad, pages.length],
|
||||
);
|
||||
const backgroundType = documentSettings?.backgroundType ?? "color";
|
||||
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
||||
const backgroundGradient = documentSettings?.backgroundGradient;
|
||||
@@ -158,7 +195,6 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const { width: bookWidth, height: bookHeight } = useMemo(() => getPaperDimensions(catalogSize ?? "a4"), [catalogSize]);
|
||||
const hasMultiplePages = pages.length > 1;
|
||||
const viewport = useViewerViewport();
|
||||
const usePortrait = !isDesktop || displayStyle === "single";
|
||||
|
||||
const viewportFit = useMemo(() => {
|
||||
const padX = isDesktop ? 48 : 20;
|
||||
@@ -183,7 +219,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const pagePixelHeight = viewportFit.h;
|
||||
const contentScale = viewportFit.contentScale;
|
||||
|
||||
const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}`;
|
||||
const flipKey = `${usePortrait ? "portrait" : "spread"}-${pagePixelWidth}-${pagePixelHeight}-${flipIndexCount}`;
|
||||
|
||||
const {
|
||||
scheduleEntranceForSpread,
|
||||
@@ -198,10 +234,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const commitEntranceAtCurrentSpread = useCallback(() => {
|
||||
const api = bookRef.current?.pageFlip();
|
||||
const idx = api?.getCurrentPageIndex?.();
|
||||
if (typeof idx !== "number" || pages.length <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(idx, pages.length - 1));
|
||||
if (typeof idx !== "number" || flipIndexCount <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(idx, flipIndexCount - 1));
|
||||
scheduleEntranceForSpread(toLogicalIndex(flipIdx));
|
||||
}, [pages.length, scheduleEntranceForSpread, toLogicalIndex]);
|
||||
}, [flipIndexCount, scheduleEntranceForSpread, toLogicalIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
resetEntrance();
|
||||
@@ -284,8 +320,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
// اگر flipIndex منفی است یا بزرگتر از تعداد صفحات، آن را محدود میکنیم
|
||||
if (flipIndex < 0) {
|
||||
flipIndex = 0;
|
||||
} else if (flipIndex >= pages.length) {
|
||||
flipIndex = pages.length - 1;
|
||||
} else if (flipIndex >= flipIndexCount) {
|
||||
flipIndex = flipIndexCount - 1;
|
||||
}
|
||||
|
||||
const pageNum = toLogicalIndex(flipIndex);
|
||||
@@ -297,7 +333,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
|
||||
setCurrentPage(pageNum);
|
||||
},
|
||||
[pages.length, pageFlipSound, playPageFlipSound, toLogicalIndex],
|
||||
[flipIndexCount, pageFlipSound, playPageFlipSound, toLogicalIndex],
|
||||
);
|
||||
|
||||
pageFlipHandlerRef.current = handlePageFlip;
|
||||
@@ -309,23 +345,23 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
const syncPageIndexFromBook = useCallback(() => {
|
||||
const api = bookRef.current?.pageFlip();
|
||||
const idx = api?.getCurrentPageIndex?.();
|
||||
if (typeof idx !== "number" || pages.length <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(idx, pages.length - 1));
|
||||
if (typeof idx !== "number" || flipIndexCount <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(idx, flipIndexCount - 1));
|
||||
setCurrentPage(toLogicalIndex(flipIdx));
|
||||
}, [pages.length, toLogicalIndex]);
|
||||
}, [flipIndexCount, toLogicalIndex]);
|
||||
|
||||
const handleBookInit = useCallback(
|
||||
(e: PageFlipInitEvent) => {
|
||||
const p = e.data?.page;
|
||||
if (typeof p !== "number" || pages.length <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(p, pages.length - 1));
|
||||
if (typeof p !== "number" || flipIndexCount <= 0) return;
|
||||
const flipIdx = Math.max(0, Math.min(p, flipIndexCount - 1));
|
||||
const logicalIdx = toLogicalIndex(flipIdx);
|
||||
setCurrentPage(logicalIdx);
|
||||
lastFlipPageRef.current = logicalIdx;
|
||||
// اولین اسپرد قبل از onChangeState('read') هم باید انیمیشن ورود بگیرد
|
||||
scheduleEntranceForSpread(logicalIdx);
|
||||
},
|
||||
[pages.length, scheduleEntranceForSpread, toLogicalIndex],
|
||||
[flipIndexCount, scheduleEntranceForSpread, toLogicalIndex],
|
||||
);
|
||||
|
||||
/** بعد از اتمام انیمیشن ورق، ایندکس واقعی کتاب را میگیریم و انیمیشن ورود را یکبار شروع میکنیم */
|
||||
@@ -436,19 +472,17 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
|
||||
useEffect(() => {
|
||||
if (pages.length <= 0) return;
|
||||
const initialFlipPage = isRtlBook ? pages.length - 1 : 0;
|
||||
setStartPage(initialFlipPage);
|
||||
setStartPage(toFlipIndex(0));
|
||||
setCurrentPage(0);
|
||||
lastFlipPageRef.current = 0;
|
||||
}, [pages.length, pages[0]?.id, isRtlBook]);
|
||||
}, [pages.length, pages[0]?.id, toFlipIndex]);
|
||||
|
||||
// پخش خودکار
|
||||
useEffect(() => {
|
||||
if (!isAutoPlayActive || pages.length <= 1) return;
|
||||
const timer = setInterval(() => {
|
||||
if (currentPage >= pages.length - 1) {
|
||||
const firstFlipPage = isRtlBook ? pages.length - 1 : 0;
|
||||
bookRef.current?.pageFlip()?.turnToPage?.(firstFlipPage);
|
||||
bookRef.current?.pageFlip()?.turnToPage?.(toFlipIndex(0));
|
||||
setCurrentPage(0);
|
||||
lastFlipPageRef.current = 0;
|
||||
return;
|
||||
@@ -461,7 +495,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
||||
}
|
||||
}, AUTO_PLAY_INTERVAL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook]);
|
||||
}, [isAutoPlayActive, currentPage, pages.length, isRtlBook, toFlipIndex]);
|
||||
|
||||
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)]" dir="rtl">
|
||||
|
||||
Reference in New Issue
Block a user