Untitled
unknown
plain_text
7 months ago
16 kB
8
Indexable
import React, { useEffect, useRef, useState } from 'react';
import HTMLFlipBook from 'react-pageflip';
import HtmlContent from './HtmlContent';
import { useSelector, useDispatch } from 'react-redux';
// Make sure the correct slice action is imported if needed, e.g.:
import { setCurrentReadingElementId } from '../store/ebookSlice';
import { Icon } from '@iconify/react';
import { useScale } from '@/hooks/useScale';
// import JumpToPage from './JumpToPage'; // Uncomment if needed and implement noteTakingLayer check
const FlipBookContainer = ({ data, setClickedPageId }) => {
const scale = useScale(0.5, 0.75, 900); // Custom hook for scaling
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// --- Redux State ---
const {
pitch,
rate,
volume,
voice,
isTextToSpeechEnabled,
noteTakingLayer, // <-- Get the note taking state
} = useSelector((state) => ({ // Assuming state structure
...state.ebook.textToSpeechSettings,
isTextToSpeechEnabled: state.ebook.isTextToSpeechEnabled,
noteTakingLayer: state.ebook.noteTakingLayer, // <-- Access the flag
}));
// Alternatively, use a selector if you have one:
// const { isTextToSpeechEnabled, noteTakingLayer, ...ttsSettings } = useSelector(selectEbookState);
const dispatch = useDispatch();
const speechSynthesisRef = useRef(window.speechSynthesis);
const flipBookRef = useRef(null);
const [currentPage, setCurrentPage] = useState(0); // 0-based index for flipbook state
// --- Text-to-Speech Logic (Largely Unchanged) ---
// ... (createUtterance, readElement, readAllElementsOnPage, etc.) ...
const createUtterance = (text, callback) => {
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = rate;
utterance.pitch = pitch;
utterance.volume = volume;
const availableVoices = speechSynthesisRef.current.getVoices();
const selectedVoice = availableVoices.find((v) => v.name === voice);
if (selectedVoice) {
utterance.voice = selectedVoice;
}
utterance.onend = () => {
if (callback) callback();
};
return utterance;
};
const readElement = (element, callback) => {
// ... (logic for reading different element types - unchanged) ...
let textToSpeak = '';
switch (element.type) {
case 'text': {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = element.content;
textToSpeak = tempDiv.innerText;
break;
}
case 'mcq': textToSpeak = `Question: ${element.content.question}. Options: ${element.content.options.join(', ')}`; break;
case 'true-false': textToSpeak = `Question: ${element.content.question}`; break;
case 'video':
textToSpeak = 'This is a video element.';
break;
case 'flashcard':
textToSpeak = 'This is a flashcard element.';
break;
case 'shape':
textToSpeak = 'This is a shape element.';
break;
default: textToSpeak = '';
}
if (textToSpeak) {
dispatch(setCurrentReadingElementId(element.id));
const utterance = createUtterance(textToSpeak, callback);
speechSynthesisRef.current.speak(utterance);
} else if (callback) {
callback();
}
};
const readAllElementsOnPage = (pageIndex, onDone) => {
if (!data?.pages[pageIndex]) { onDone?.(); return; }
const elements = data.pages[pageIndex].elements || [];
const readNextElement = (idx) => {
if (idx >= elements.length) { onDone?.(); return; }
readElement(elements[idx], () => readNextElement(idx + 1));
};
readNextElement(0);
};
const readSinglePage = (pageIndex) => {
if (noteTakingLayer || pageIndex >= data.pages.length) { dispatch(setCurrentReadingElementId(null)); return; }
speechSynthesisRef.current.cancel();
readAllElementsOnPage(pageIndex, () => {
setTimeout(() => {
if (!noteTakingLayer && !speechSynthesisRef.current.speaking) {
handleNext();
setTimeout(() => { readSinglePage(pageIndex + 1); }, 800);
}
}, 500);
});
};
const readDoublePage = (leftPageIndex) => {
if (noteTakingLayer || leftPageIndex >= data.pages.length) { dispatch(setCurrentReadingElementId(null)); return; }
const rightPageIndex = leftPageIndex + 1;
speechSynthesisRef.current.cancel();
readAllElementsOnPage(leftPageIndex, () => {
if (rightPageIndex < data.pages.length) {
readAllElementsOnPage(rightPageIndex, () => {
setTimeout(() => {
if (!noteTakingLayer && !speechSynthesisRef.current.speaking) {
handleNext();
setTimeout(() => { readDoublePage(leftPageIndex + 2); }, 800);
}
}, 500);
});
} else {
setTimeout(() => { if (!noteTakingLayer && !speechSynthesisRef.current.speaking) { handleNext(); } }, 500);
}
});
};
const autoReadPages = () => {
if (noteTakingLayer || !data.pages || data.pages.length === 0) return; // Don't read if notes active
if (isMobile) { readSinglePage(currentPage); }
else { readDoublePage(currentPage); }
};
// Effect to handle TTS start/stop
useEffect(() => {
if (noteTakingLayer) {
// Stop speech immediately if note taking starts
speechSynthesisRef.current.cancel();
dispatch(setCurrentReadingElementId(null));
} else if (isTextToSpeechEnabled) {
// Start reading if TTS enabled and not taking notes
autoReadPages();
} else {
// Stop speech if TTS is simply disabled
speechSynthesisRef.current.cancel();
dispatch(setCurrentReadingElementId(null));
}
// Cleanup function to stop speech on unmount or dependency change
return () => speechSynthesisRef.current.cancel();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTextToSpeechEnabled, noteTakingLayer, currentPage, voice]); // Removed TTS settings from deps if they don't restart reading
// --- PAGE FLIP HANDLERS ---
// Block programmatic flips if note taking is active
const handlePrevious = () => {
// Check if note taking layer is active BEFORE flipping
if (noteTakingLayer) return;
if (flipBookRef.current) {
flipBookRef.current.pageFlip().flipPrev();
}
};
const handleNext = () => {
// Check if note taking layer is active BEFORE flipping
if (noteTakingLayer) return;
if (flipBookRef.current) {
// Use existing timeout or remove if not needed
setTimeout(() => {
// Double check noteTakingLayer *inside* timeout in case state changes rapidly
if (!noteTakingLayer) {
flipBookRef.current.pageFlip().flipNext();
}
}, 300);
}
};
// Update state only if not taking notes
const onPageChange = (e) => {
// Check if note taking layer is active when flip completes
if (!noteTakingLayer) {
setCurrentPage(e.data); // e.data is the new page index (0-based)
} else {
// If a flip somehow completed while note taking was active,
// try to force the book back to the state's currentPage.
// This might cause a visual flicker but ensures consistency.
const currentBookIndex = flipBookRef.current?.pageFlip().getCurrentPageIndex();
if (currentBookIndex !== undefined && currentBookIndex !== currentPage) {
console.warn("Flip detected during note taking, reverting...");
flipBookRef.current.pageFlip().flip(currentPage, 'bottom'); // Or 'top'
}
}
};
const handleJumpToPage = (pageNumber) => {
// Check if note taking layer is active BEFORE jumping
if (noteTakingLayer) return;
if (flipBookRef.current) {
// pageNumber from JumpToPage is likely 1-based, flipbook uses 0-based
const zeroBasedIndex = pageNumber - 1;
if (zeroBasedIndex >= 0 && zeroBasedIndex < (data?.pages?.length || 0)) {
flipBookRef.current.pageFlip().flip(zeroBasedIndex);
// setCurrentPage(zeroBasedIndex); // Let onPageChange handle state update
}
}
};
// --- Calculated Button Disabled States ---
const isPrevDisabled = noteTakingLayer || currentPage === 0;
// Ensure data.pages exists before accessing length
const pageCount = data?.pages?.length || 0;
const lastPageIndex = pageCount > 0 ? (isMobile ? pageCount - 1 : (pageCount % 2 === 0 ? pageCount - 2 : pageCount - 1)) : 0;
const isNextDisabled = noteTakingLayer || currentPage >= lastPageIndex;
return (
<div className="relative w-full flex justify-center">
{/* Jump to Page component */}
{/* Consider adding noteTakingLayer check to disable JumpToPage interaction */}
{/* ... */}
{/* Container for scaling */}
<div
style={{
transform: `scale(${scale})`,
transformOrigin: 'top center',
marginTop: isMobile ? '60px' : '0',
position: 'relative', // Establish stacking context
zIndex: 1, // Base layer for book content
}}
>
{/* === Wrapper Div for pointer-events === */}
<div
style={{
// Conditionally disable interactions on the flipbook itself
pointerEvents: noteTakingLayer ? 'none' : 'auto',
// Ensure wrapper doesn't shrink if content is complex
width: data.width * (isMobile ? 1 : 2), // Match display width
height: data.height,
margin: '0 auto', // Center the book within the scaled div
}}
>
<HTMLFlipBook
ref={flipBookRef}
width={data.width} // Width of a SINGLE page
height={data.height}
size="fixed" // Use fixed size based on data props
startPage={currentPage}
className="flipbook" // Basic class, avoid overflow hidden here if wrapper handles it
// --- Interaction Control ---
// These are now secondary to the pointer-events wrapper, but good as fallback
disableFlipByClick={isMobile ? false : true}
useMouseEvents={!noteTakingLayer} // Only allow mouse drag if not taking notes
mobileScrollSupport={!noteTakingLayer} // Only allow swipe if not taking notes
showPageCorners={!noteTakingLayer} // Hide peel effect when drawing
clickEventForward={false} // Prevent clicks passing through unnecessarily
// --- Other Props ---
usePortrait={isMobile} // Controls single/double page view based on state
onFlip={onPageChange} // Update state after flip completes
// Key to help with re-renders on critical state changes
key={`flipbook-${isMobile}-${noteTakingLayer}`}
>
{/* --- Page Content Mapping --- */}
{data?.pages?.map((page, index) => (
<div
key={page.id || index} // Prefer unique page.id
className={`page-wrapper bg-white shadow-sm border border-gray-100 ${index % 2 === 0 ? 'left-page' : 'right-page'}`}
// onClick logic might be needed here depending on how setClickedPageId is used
// Make sure this click is also blocked by the pointer-events wrapper
onClick={() => !noteTakingLayer && setClickedPageId(page.id)}
style={{ width: data.width, height: data.height }} // Explicit size
>
<HtmlContent
page={page}
index={index}
html={page?.content || ''}
bg_color={page?.bg_color || 'white'}
margin={page?.margin || '0'}
width={data.width}
height={data.height}
bookmarks={data?.bookmarks}
// Pass down noteTakingLayer if HtmlContent needs to change behavior
// isNoteTakingActive={noteTakingLayer}
/>
</div>
))}
</HTMLFlipBook>
</div> {/* === End of pointer-events wrapper === */}
</div> {/* End of scaling container */}
{/* --- Pagination Buttons --- */}
{/* Ensure these have higher z-index than the book container */}
{/* Desktop Pagination */}
<div className="md:block hidden">
<div className="fixed top-1/2 left-3 transform -translate-y-1/2 z-20"> {/* z-20 */}
<button
aria-label="Previous Page"
className={`p-3 rounded-full flex items-center justify-center transition-all duration-300 shadow-md ${
isPrevDisabled
? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-70'
: 'bg-gray-800 text-white hover:bg-gray-900'
}`}
onClick={handlePrevious}
disabled={isPrevDisabled} // Use calculated disabled state
>
<Icon icon="mdi:chevron-left" width="26" height="26" />
</button>
</div>
<div className="fixed top-1/2 right-3 transform -translate-y-1/2 z-20"> {/* z-20 */}
<button
aria-label="Next Page"
className={`p-3 rounded-full flex items-center justify-center transition-all duration-300 shadow-md ${
isNextDisabled
? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-70'
: 'bg-gray-800 text-white hover:bg-gray-900'
}`}
onClick={handleNext}
disabled={isNextDisabled} // Use calculated disabled state
>
<Icon icon="mdi:chevron-right" width="26" height="26" />
</button>
</div>
</div>
{/* --- Mobile Pagination --- */}
{/* Apply same disabling logic if mobile buttons are implemented */}
{/* {isMobile && (...)} */}
{!isMobile && (
<div>
<div className="fixed top-1/2 left-3 transform -translate-y-1/2">
<button
className={`p-3 rounded-full flex items-center justify-center transition-all duration-300 shadow-md ${currentPage === 0
? 'bg-gray-300 cursor-not-allowed'
: 'bg-gray-800 text-white hover:bg-gray-900'
}`}
onClick={handlePrevious}
disabled={currentPage === 0}
>
<Icon icon="mdi:chevron-left" width="26" height="26" />
</button>
</div>
<div className="fixed top-1/2 right-3 transform -translate-y-1/2">
<button
className={`p-3 rounded-full flex items-center justify-center transition-all duration-300 shadow-md ${currentPage >= data?.pages?.length - 1
? 'bg-gray-300 cursor-not-allowed'
: 'bg-gray-800 text-white hover:bg-gray-900'
}`}
onClick={handleNext}
disabled={currentPage >= data?.pages?.length - 1}
>
<Icon icon="mdi:chevron-right" width="26" height="26" />
</button>
</div>
</div>
)}
</div> // End of main relative container
);
};
export default FlipBookContainer;Editor is loading...
Leave a Comment