Untitled
unknown
plain_text
8 days ago
16 kB
3
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