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 و 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 {
+79 -8
View File
@@ -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<BookViewerProps> = ({ 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<BookViewerProps> = ({ pages, catalogSize }) => {
}
}, [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) => {
// react-pageflip شماره صفحه را به صورت 0-based برمی‌گرداند
// اما باید مطمئن شویم که در محدوده معتبر است
@@ -123,8 +181,13 @@ const BookViewer: FC<BookViewerProps> = ({ 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<BookViewerProps> = ({ pages, catalogSize }) => {
const usePortrait = !isDesktop;
const prevFlipKeyRef = useRef(flipKey);
useEffect(() => {
if (prevFlipKeyRef.current !== flipKey) {
prevFlipKeyRef.current = flipKey;
lastFlipPageRef.current = currentPage;
}
}, [flipKey, currentPage]);
return (
<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)]"
@@ -222,7 +293,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
>
<div
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 عمودی در وسط (فقط دوسوّره/دسکتاپ) */}
<div
@@ -233,7 +304,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize }) => {
/>
<div
className="max-w-full flex justify-center"
style={{ overflow: 'hidden', isolation: 'isolate' }}
style={{ overflow: isDesktop ? 'hidden' : 'visible', isolation: 'isolate' }}
>
<HTMLFlipBook
key={flipKey}
@@ -245,12 +316,12 @@ const BookViewer: FC<BookViewerProps> = ({ 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}