next and prev page

This commit is contained in:
hamid zarghami
2026-05-03 09:53:51 +03:30
parent 3cffa1b107
commit 1bad82e680
2 changed files with 83 additions and 14 deletions
+1
View File
@@ -480,6 +480,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return ( return (
<div <div
ref={ref} ref={ref}
dir="rtl"
className="page" className="page"
data-density="soft" data-density="soft"
style={{ style={{
+82 -14
View File
@@ -34,6 +34,7 @@ interface PageFlipAPI {
flipNext: () => void; flipNext: () => void;
flipPrev: () => void; flipPrev: () => void;
turnToPage: (page: number) => void; turnToPage: (page: number) => void;
getCurrentPageIndex?: () => number;
} }
interface FlipBookInstance { interface FlipBookInstance {
@@ -44,6 +45,33 @@ interface FlipEvent {
data: number; data: number;
} }
type PageFlipInitEvent = { data: { page: number; mode: string } };
type PageFlipStateEvent = { data: string };
/** مختصات داخلی رندر کتاب (همان فضایی که getBoundsRect و flipNext استفاده می‌کنند) */
type FlipBookRect = { left: number; top: number; width: number; height: number; pageWidth: number };
type PageFlipWithFlipController = PageFlipAPI & {
getBoundsRect: () => FlipBookRect;
getFlipController: () => { flip: (p: { x: number; y: number }) => void };
};
/**
* page-flip داخل flipPrev با x=10 ثابت می‌زند؛ با disableFlipByClick آزمایش گوشه رد می‌شود
* (چون bookPos.x = 10 - rect.left معمولاً ≤ 0). flipNext همان rect.left را جمع می‌کند.
*/
function triggerFlipPrev(api: PageFlipWithFlipController | undefined) {
if (!api?.getBoundsRect || !api.getFlipController) {
api?.flipPrev();
return;
}
const rect = api.getBoundsRect();
api.getFlipController().flip({
x: rect.left + 12,
y: rect.top + 2,
});
}
/** /**
* کامپوننت اصلی کتابخوان با استفاده از react-pageflip * کامپوننت اصلی کتابخوان با استفاده از react-pageflip
* این کامپوننت از HTMLFlipBook استفاده می‌کند که یک wrapper برای PageFlip است * این کامپوننت از HTMLFlipBook استفاده می‌کند که یک wrapper برای PageFlip است
@@ -51,6 +79,7 @@ interface FlipEvent {
*/ */
const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => { const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const bookRef = useRef<FlipBookInstance | null>(null); const bookRef = useRef<FlipBookInstance | null>(null);
const pageFlipHandlerRef = useRef<(e: FlipEvent) => void>(() => {});
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const isDesktop = useIsDesktop(); const isDesktop = useIsDesktop();
@@ -189,6 +218,40 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
setCurrentPage(pageNum); setCurrentPage(pageNum);
}, [pages.length, playPageFlipSound]); }, [pages.length, playPageFlipSound]);
pageFlipHandlerRef.current = handlePageFlip;
const onFlipForwarded = useCallback((e: FlipEvent) => {
pageFlipHandlerRef.current(e);
}, []);
const syncPageIndexFromBook = useCallback(() => {
const api = bookRef.current?.pageFlip();
const idx = api?.getCurrentPageIndex?.();
if (typeof idx !== 'number' || pages.length <= 0) return;
const clamped = Math.max(0, Math.min(idx, pages.length - 1));
setCurrentPage(clamped);
}, [pages.length]);
const handleBookInit = useCallback(
(e: PageFlipInitEvent) => {
const p = e.data?.page;
if (typeof p !== 'number' || pages.length <= 0) return;
const clamped = Math.max(0, Math.min(p, pages.length - 1));
setCurrentPage(clamped);
lastFlipPageRef.current = clamped;
},
[pages.length]
);
/** بعد از اتمام انیمیشن، ایندکس واقعی کتاب را می‌گیریم (RTL روی روت کتاب مختصات page-flip را به‌هم می‌زند) */
const handleChangeState = useCallback(
(e: PageFlipStateEvent) => {
if (e.data !== 'read') return;
syncPageIndexFromBook();
},
[syncPageIndexFromBook]
);
const goToNextPage = useCallback(() => { const goToNextPage = useCallback(() => {
if (currentPage < pages.length - 1) { if (currentPage < pages.length - 1) {
bookRef.current?.pageFlip()?.flipNext(); bookRef.current?.pageFlip()?.flipNext();
@@ -196,15 +259,15 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
}, [currentPage, pages.length]); }, [currentPage, pages.length]);
const goToPrevPage = useCallback(() => { const goToPrevPage = useCallback(() => {
if (currentPage > 0) { if (currentPage <= 0) return;
bookRef.current?.pageFlip()?.flipPrev(); const api = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
} triggerFlipPrev(api);
}, [currentPage]); }, [currentPage]);
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://', '');
const pageFlipAPI = bookRef.current?.pageFlip(); const pageFlipAPI = bookRef.current?.pageFlip() as PageFlipWithFlipController | undefined;
if (!pageFlipAPI) return; if (!pageFlipAPI) return;
@@ -216,7 +279,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
// رفتن به صفحه اول // رفتن به صفحه اول
const flipToFirst = () => { const flipToFirst = () => {
if (currentPage > 0) { if (currentPage > 0) {
pageFlipAPI.flipPrev(); triggerFlipPrev(pageFlipAPI);
setTimeout(() => { setTimeout(() => {
setCurrentPage(prev => { setCurrentPage(prev => {
if (prev > 1) { if (prev > 1) {
@@ -262,7 +325,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
flipToTarget(remaining - 1); flipToTarget(remaining - 1);
}, 300); }, 300);
} else { } else {
pageFlipAPI.flipPrev(); triggerFlipPrev(pageFlipAPI);
setTimeout(() => { setTimeout(() => {
setCurrentPage(prev => prev - 1); setCurrentPage(prev => prev - 1);
flipToTarget(remaining + 1); flipToTarget(remaining + 1);
@@ -302,7 +365,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
boxShadow: '-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)', boxShadow: '-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)',
}} }}
/> />
{/*
page-flip با مختصات LTR کار می‌کند؛ direction:rtl روی روت کتاب باعث ناهماهنگی
ایندکس/onFlip با دکمه‌ها می‌شود. محتوای فارسی داخل BookPage با dir=rtl است.
*/}
<div <div
dir="ltr"
className="max-w-full flex justify-center" className="max-w-full flex justify-center"
style={{ overflow: isDesktop ? 'hidden' : 'visible', isolation: 'isolate' }} style={{ overflow: isDesktop ? 'hidden' : 'visible', isolation: 'isolate' }}
> >
@@ -331,10 +399,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
showPageCorners={true} showPageCorners={true}
disableFlipByClick={true} disableFlipByClick={true}
className="flipbook-container mx-auto max-w-full" className="flipbook-container mx-auto max-w-full"
style={{ style={{}}
direction: 'rtl', onFlip={onFlipForwarded}
}} onInit={handleBookInit}
onFlip={handlePageFlip} onChangeState={handleChangeState}
> >
{pages.map((page) => ( {pages.map((page) => (
<BookPage <BookPage
@@ -355,11 +423,11 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
<button <button
type="button" type="button"
onClick={goToNextPage} onClick={goToNextPage}
disabled={currentPage <= 0} disabled={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="صفحه بعد"
> >
<ArrowLeft2 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>
{/* شماره صفحه */} {/* شماره صفحه */}
@@ -373,11 +441,11 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
<button <button
type="button" type="button"
onClick={goToPrevPage} onClick={goToPrevPage}
disabled={currentPage >= pages.length - 1} disabled={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="صفحه قبل"
> >
<ArrowRight2 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>
</div> </div>
</div> </div>