Untitled

 avatar
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