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