diff --git a/public/sounds/page-flip.mp3 b/public/sounds/page-flip.mp3 new file mode 100644 index 0000000..b5718bf Binary files /dev/null and b/public/sounds/page-flip.mp3 differ diff --git a/src/index.css b/src/index.css index fd5ef1f..9cca77e 100644 --- a/src/index.css +++ b/src/index.css @@ -115,6 +115,24 @@ div.stf__item.page, overflow: hidden; } +/* موبایل: بدون سایهٔ کادر کتاب تا لبهٔ صفحه هنگام ورق دیده شود؛ overflow و contain کمتر تا بریدگی ۳D نماند */ +@media (max-width: 767px) { + .flipbook-container { + box-shadow: none; + border-radius: 2px; + overflow: visible; + } + + .flipbook-container .stf__wrapper { + overflow: visible !important; + } + + .flipbook-container .stf__item { + contain: none; + overflow: visible !important; + } +} + /* استایل برای صفحات داخل flipbook */ .flipbook-container .stf__item, .flipbook-container .page { diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx index 682c67a..2e964e8 100644 --- a/src/pages/viewer/components/BookViewer.tsx +++ b/src/pages/viewer/components/BookViewer.tsx @@ -6,6 +6,8 @@ import { ArrowLeft2, ArrowRight2 } from 'iconsax-react'; import { getPaperDimensions } from '@/config/paperSizes'; const VIEWER_SCALE = 0.5; +/** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشت‌هم دارد؛ فقط تا این سهم از طول پخش می‌شود */ +const PAGE_FLIP_SOUND_PLAY_FRACTION = 0.5; /** هم‌راستا با `md` تیلویند (768px) — موبایل: زیر این عرض */ const MOBILE_QUERY = '(min-width: 768px)'; @@ -79,10 +81,11 @@ const BookViewer: FC = ({ pages, catalogSize }) => { const mobileFit = useMemo(() => { if (isDesktop) return null; - const padX = 16; + const padX = 20; + const edgeBleed = 6; const reservedNav = 120; - const maxW = Math.max(1, viewport.w - padX); - const maxH = Math.max(1, viewport.h - reservedNav); + const maxW = Math.max(1, viewport.w - padX - edgeBleed * 2); + const maxH = Math.max(1, viewport.h - reservedNav - edgeBleed); const baseW = bookWidth * VIEWER_SCALE; const baseH = bookHeight * VIEWER_SCALE; const scale = Math.min(1, maxW / baseW, maxH / baseH); @@ -111,6 +114,61 @@ const BookViewer: FC = ({ pages, catalogSize }) => { } }, [currentPage, pages.length]); + const pageFlipSoundRef = useRef(null); + const pageFlipSoundTrimListenerRef = useRef<(() => void) | null>(null); + const lastFlipPageRef = useRef(null); + + useEffect(() => { + const base = import.meta.env.BASE_URL; + const src = `${base}${base.endsWith('/') ? '' : '/'}sounds/page-flip.mp3`; + const audio = new Audio(src); + audio.preload = 'auto'; + pageFlipSoundRef.current = audio; + return () => { + const trim = pageFlipSoundTrimListenerRef.current; + if (trim) { + audio.removeEventListener('timeupdate', trim); + pageFlipSoundTrimListenerRef.current = null; + } + audio.pause(); + pageFlipSoundRef.current = null; + }; + }, []); + + const playPageFlipSound = useCallback(() => { + const audio = pageFlipSoundRef.current; + if (!audio) return; + const prevTrim = pageFlipSoundTrimListenerRef.current; + if (prevTrim) { + audio.removeEventListener('timeupdate', prevTrim); + pageFlipSoundTrimListenerRef.current = null; + } + const onTimeUpdate = () => { + const d = audio.duration; + if (!Number.isFinite(d) || d <= 0.04) return; + const end = d * PAGE_FLIP_SOUND_PLAY_FRACTION; + if (audio.currentTime >= end) { + audio.pause(); + audio.currentTime = 0; + audio.removeEventListener('timeupdate', onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + } + }; + pageFlipSoundTrimListenerRef.current = onTimeUpdate; + try { + audio.currentTime = 0; + audio.addEventListener('timeupdate', onTimeUpdate); + void audio.play().catch(() => { + audio.removeEventListener('timeupdate', onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + /* autoplay policy — معمولاً بعد از تعامل کاربر ورق می‌خورد */ + }); + } catch { + audio.removeEventListener('timeupdate', onTimeUpdate); + pageFlipSoundTrimListenerRef.current = null; + } + }, []); + const handlePageFlip = useCallback((e: FlipEvent) => { // react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند // اما باید مطمئن شویم که در محدوده معتبر است @@ -123,8 +181,13 @@ const BookViewer: FC = ({ pages, catalogSize }) => { pageNum = pages.length - 1; } + if (lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) { + playPageFlipSound(); + } + lastFlipPageRef.current = pageNum; + setCurrentPage(pageNum); - }, [pages.length]); + }, [pages.length, playPageFlipSound]); const goToNextPage = useCallback(() => { if (currentPage < pages.length - 1) { @@ -215,6 +278,14 @@ const BookViewer: FC = ({ pages, catalogSize }) => { const usePortrait = !isDesktop; + const prevFlipKeyRef = useRef(flipKey); + useEffect(() => { + if (prevFlipKeyRef.current !== flipKey) { + prevFlipKeyRef.current = flipKey; + lastFlipPageRef.current = currentPage; + } + }, [flipKey, currentPage]); + return (
= ({ pages, catalogSize }) => { >
{/* Border و Shadow عمودی در وسط (فقط دوسوّره/دسکتاپ) */}
= ({ pages, catalogSize }) => { />
= ({ pages, catalogSize }) => { minHeight={pagePixelHeight} maxWidth={pagePixelWidth} maxHeight={pagePixelHeight} - drawShadow={true} + drawShadow={isDesktop} flippingTime={800} usePortrait={usePortrait} startPage={currentPage} autoSize={false} - maxShadowOpacity={0.5} + maxShadowOpacity={isDesktop ? 0.5 : 0} showCover={true} mobileScrollSupport={!usePortrait} clickEventForward={true}