This commit is contained in:
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user