fix viewer in even pages
deploy to danak / build_and_deploy (push) Has been cancelled

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