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