page flip sound

This commit is contained in:
hamid zarghami
2026-05-03 09:16:48 +03:30
parent acb2f33b67
commit 3cffa1b107
3 changed files with 97 additions and 8 deletions
Binary file not shown.
+18
View File
@@ -115,6 +115,24 @@ div.stf__item.page,
overflow: hidden; 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 */
.flipbook-container .stf__item, .flipbook-container .stf__item,
.flipbook-container .page { .flipbook-container .page {
+79 -8
View File
@@ -6,6 +6,8 @@ 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;
/** نسخهٔ فعلی `page-flip.mp3` دو ضربهٔ ورق پشت‌هم دارد؛ فقط تا این سهم از طول پخش می‌شود */
const PAGE_FLIP_SOUND_PLAY_FRACTION = 0.5;
/** هم‌راستا با `md` تیلویند (768px) — موبایل: زیر این عرض */ /** هم‌راستا با `md` تیلویند (768px) — موبایل: زیر این عرض */
const MOBILE_QUERY = '(min-width: 768px)'; const MOBILE_QUERY = '(min-width: 768px)';
@@ -79,10 +81,11 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const mobileFit = useMemo(() => { const mobileFit = useMemo(() => {
if (isDesktop) return null; if (isDesktop) return null;
const padX = 16; const padX = 20;
const edgeBleed = 6;
const reservedNav = 120; const reservedNav = 120;
const maxW = Math.max(1, viewport.w - padX); const maxW = Math.max(1, viewport.w - padX - edgeBleed * 2);
const maxH = Math.max(1, viewport.h - reservedNav); const maxH = Math.max(1, viewport.h - reservedNav - edgeBleed);
const baseW = bookWidth * VIEWER_SCALE; const baseW = bookWidth * VIEWER_SCALE;
const baseH = bookHeight * VIEWER_SCALE; const baseH = bookHeight * VIEWER_SCALE;
const scale = Math.min(1, maxW / baseW, maxH / baseH); const scale = Math.min(1, maxW / baseW, maxH / baseH);
@@ -111,6 +114,61 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
} }
}, [currentPage, pages.length]); }, [currentPage, pages.length]);
const pageFlipSoundRef = useRef<HTMLAudioElement | null>(null);
const pageFlipSoundTrimListenerRef = useRef<(() => void) | null>(null);
const lastFlipPageRef = useRef<number | null>(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) => { const handlePageFlip = useCallback((e: FlipEvent) => {
// react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند // react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند
// اما باید مطمئن شویم که در محدوده معتبر است // اما باید مطمئن شویم که در محدوده معتبر است
@@ -123,8 +181,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
pageNum = pages.length - 1; pageNum = pages.length - 1;
} }
if (lastFlipPageRef.current !== null && lastFlipPageRef.current !== pageNum) {
playPageFlipSound();
}
lastFlipPageRef.current = pageNum;
setCurrentPage(pageNum); setCurrentPage(pageNum);
}, [pages.length]); }, [pages.length, playPageFlipSound]);
const goToNextPage = useCallback(() => { const goToNextPage = useCallback(() => {
if (currentPage < pages.length - 1) { if (currentPage < pages.length - 1) {
@@ -215,6 +278,14 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
const usePortrait = !isDesktop; const usePortrait = !isDesktop;
const prevFlipKeyRef = useRef(flipKey);
useEffect(() => {
if (prevFlipKeyRef.current !== flipKey) {
prevFlipKeyRef.current = flipKey;
lastFlipPageRef.current = currentPage;
}
}, [flipKey, currentPage]);
return ( return (
<div <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)]" 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)]"
@@ -222,7 +293,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
> >
<div <div
className="relative w-full flex justify-center flex-1 max-md:min-h-0 items-center" className="relative w-full flex justify-center flex-1 max-md:min-h-0 items-center"
style={{ overflow: 'hidden' }} style={{ overflow: isDesktop ? 'hidden' : 'visible' }}
> >
{/* Border و Shadow عمودی در وسط (فقط دوسوّره/دسکتاپ) */} {/* Border و Shadow عمودی در وسط (فقط دوسوّره/دسکتاپ) */}
<div <div
@@ -233,7 +304,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
/> />
<div <div
className="max-w-full flex justify-center" className="max-w-full flex justify-center"
style={{ overflow: 'hidden', isolation: 'isolate' }} style={{ overflow: isDesktop ? 'hidden' : 'visible', isolation: 'isolate' }}
> >
<HTMLFlipBook <HTMLFlipBook
key={flipKey} key={flipKey}
@@ -245,12 +316,12 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
minHeight={pagePixelHeight} minHeight={pagePixelHeight}
maxWidth={pagePixelWidth} maxWidth={pagePixelWidth}
maxHeight={pagePixelHeight} maxHeight={pagePixelHeight}
drawShadow={true} drawShadow={isDesktop}
flippingTime={800} flippingTime={800}
usePortrait={usePortrait} usePortrait={usePortrait}
startPage={currentPage} startPage={currentPage}
autoSize={false} autoSize={false}
maxShadowOpacity={0.5} maxShadowOpacity={isDesktop ? 0.5 : 0}
showCover={true} showCover={true}
mobileScrollSupport={!usePortrait} mobileScrollSupport={!usePortrait}
clickEventForward={true} clickEventForward={true}