mobile viewer

This commit is contained in:
hamid zarghami
2026-04-27 11:37:07 +03:30
parent b74e994de5
commit 24872d9544
+95 -27
View File
@@ -1,4 +1,4 @@
import { type FC, useRef, useCallback, useState, useEffect, useMemo } from 'react'; import { type FC, useRef, useCallback, useState, useEffect, useLayoutEffect, useMemo, useSyncExternalStore } from 'react';
import HTMLFlipBook from 'react-pageflip'; import HTMLFlipBook from 'react-pageflip';
import { type PageData } from '../types'; import { type PageData } from '../types';
import BookPage from './BookPage'; import BookPage from './BookPage';
@@ -6,6 +6,22 @@ import { ArrowLeft2, ArrowRight2 } from 'iconsax-react';
import { getPaperDimensions } from '@/config/paperSizes'; import { getPaperDimensions } from '@/config/paperSizes';
const VIEWER_SCALE = 0.5; const VIEWER_SCALE = 0.5;
/** هم‌راستا با `md` تیلویند (768px) — موبایل: زیر این عرض */
const MOBILE_QUERY = '(min-width: 768px)';
function subscribeDesktopMql(callback: () => void) {
const mq = window.matchMedia(MOBILE_QUERY);
mq.addEventListener('change', callback);
return () => mq.removeEventListener('change', callback);
}
function useIsDesktop() {
return useSyncExternalStore(
subscribeDesktopMql,
() => window.matchMedia(MOBILE_QUERY).matches,
() => true
);
}
export type BookViewerProps = { export type BookViewerProps = {
pages: PageData[]; pages: PageData[];
@@ -34,6 +50,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 [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const isDesktop = useIsDesktop();
// سایز کتاب بر اساس catalogSize (A3/A4/A5) // سایز کتاب بر اساس catalogSize (A3/A4/A5)
const { width: bookWidth, height: bookHeight } = useMemo( const { width: bookWidth, height: bookHeight } = useMemo(
@@ -43,8 +60,47 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const displayWidth = Math.round(bookWidth * VIEWER_SCALE); const displayWidth = Math.round(bookWidth * VIEWER_SCALE);
const displayHeight = Math.round(bookHeight * VIEWER_SCALE); const displayHeight = Math.round(bookHeight * VIEWER_SCALE);
// scale محتوا همان VIEWER_SCALE است چون سایز کتاب با سایز محتوا یکی است // موبایل: از ابعاد viewport برای جا دادن کتاب در صفحه
const contentScale = VIEWER_SCALE; const [viewport, setViewport] = useState(() =>
typeof window !== 'undefined'
? { w: window.innerWidth, h: window.innerHeight }
: { w: 1200, h: 800 }
);
useLayoutEffect(() => {
const onResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight });
onResize();
window.addEventListener('resize', onResize);
window.addEventListener('orientationchange', onResize);
return () => {
window.removeEventListener('resize', onResize);
window.removeEventListener('orientationchange', onResize);
};
}, []);
const mobileFit = useMemo(() => {
if (isDesktop) return null;
const padX = 16;
const reservedNav = 120;
const maxW = Math.max(1, viewport.w - padX);
const maxH = Math.max(1, viewport.h - reservedNav);
const baseW = bookWidth * VIEWER_SCALE;
const baseH = bookHeight * VIEWER_SCALE;
const scale = Math.min(1, maxW / baseW, maxH / baseH);
return {
w: Math.max(1, Math.round(baseW * scale)),
h: Math.max(1, Math.round(baseH * scale)),
contentScale: VIEWER_SCALE * scale,
};
}, [isDesktop, bookWidth, bookHeight, viewport.w, viewport.h]);
// دسکتاپ: دقیقاً مثل قبل — موبایل: مقیاس‌شده
const pagePixelWidth = isDesktop ? displayWidth : (mobileFit?.w ?? displayWidth);
const pagePixelHeight = isDesktop ? displayHeight : (mobileFit?.h ?? displayHeight);
const contentScale = isDesktop ? VIEWER_SCALE : (mobileFit?.contentScale ?? VIEWER_SCALE);
const flipKey = isDesktop
? 'desktop'
: `mobile-${pagePixelWidth}-${pagePixelHeight}`;
// اطمینان از اینکه currentPage همیشه در محدوده معتبر است // اطمینان از اینکه currentPage همیشه در محدوده معتبر است
useEffect(() => { useEffect(() => {
@@ -157,42 +213,53 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
} }
}, [currentPage, pages.length, goToNextPage, goToPrevPage]); }, [currentPage, pages.length, goToNextPage, goToPrevPage]);
const usePortrait = !isDesktop;
return ( return (
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full" 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)]"
<div className="relative w-full flex justify-center flex-1 items-center" style={{ overflow: 'hidden' }}> dir="rtl"
{/* Border و Shadow عمودی در وسط کتاب */} >
<div
className="relative w-full flex justify-center flex-1 max-md:min-h-0 items-center"
style={{ overflow: 'hidden' }}
>
{/* Border و Shadow عمودی در وسط (فقط دوسوّره/دسکتاپ) */}
<div <div
className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none z-50" className="hidden md:block absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none z-50"
style={{ style={{
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)',
}} }}
/> />
<div style={{ overflow: 'hidden', isolation: 'isolate' }}> <div
className="max-w-full flex justify-center"
style={{ overflow: 'hidden', isolation: 'isolate' }}
>
<HTMLFlipBook <HTMLFlipBook
key={flipKey}
ref={bookRef} ref={bookRef}
width={displayWidth} width={pagePixelWidth}
height={displayHeight} height={pagePixelHeight}
size="fixed" size="fixed"
minWidth={displayWidth} minWidth={pagePixelWidth}
minHeight={displayHeight} minHeight={pagePixelHeight}
maxWidth={displayWidth} maxWidth={pagePixelWidth}
maxHeight={displayHeight} maxHeight={pagePixelHeight}
drawShadow={true} drawShadow={true}
flippingTime={800} flippingTime={800}
usePortrait={false} usePortrait={usePortrait}
startPage={0} startPage={currentPage}
autoSize={false} autoSize={false}
maxShadowOpacity={0.5} maxShadowOpacity={0.5}
showCover={true} showCover={true}
mobileScrollSupport={true} mobileScrollSupport={!usePortrait}
clickEventForward={true} clickEventForward={true}
useMouseEvents={true} useMouseEvents={true}
swipeDistance={30} swipeDistance={usePortrait ? 48 : 30}
startZIndex={1} startZIndex={1}
showPageCorners={true} showPageCorners={true}
disableFlipByClick={true} disableFlipByClick={true}
className="flipbook-container mx-auto" className="flipbook-container mx-auto max-w-full"
style={{ style={{
direction: 'rtl', direction: 'rtl',
}} }}
@@ -203,8 +270,8 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
key={page.id} key={page.id}
page={page} page={page}
scale={contentScale} scale={contentScale}
pageWidth={displayWidth} pageWidth={pagePixelWidth}
pageHeight={displayHeight} pageHeight={pagePixelHeight}
onLinkClick={handleLinkClick} onLinkClick={handleLinkClick}
/> />
))} ))}
@@ -212,13 +279,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
</div> </div>
</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="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، صفحه سمت چپ) */} {/* دکمه صفحه بعد (در RTL، صفحه سمت چپ) */}
<button <button
type="button"
onClick={goToNextPage} onClick={goToNextPage}
disabled={currentPage <= 0} disabled={currentPage <= 0}
className="p-2 md:p-2 rounded-full hover:bg-gray-100 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" /> <ArrowLeft2 size={20} className="text-gray-700 md:w-6 md:h-6" />
@@ -233,9 +300,10 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
{/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */} {/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */}
<button <button
type="button"
onClick={goToPrevPage} onClick={goToPrevPage}
disabled={currentPage >= pages.length - 1} disabled={currentPage >= pages.length - 1}
className="p-2 md:p-2 rounded-full hover:bg-gray-100 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" /> <ArrowRight2 size={20} className="text-gray-700 md:w-6 md:h-6" />