page flip sound
This commit is contained in:
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user