import React, { useCallback, useState, useEffect, useRef } from 'react'; import { Text, Checkbox, Tooltip, ActionIcon, Loader } from '@mantine/core'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import RotateLeftIcon from '@mui/icons-material/RotateLeft'; import RotateRightIcon from '@mui/icons-material/RotateRight'; import DeleteIcon from '@mui/icons-material/Delete'; import ContentCutIcon from '@mui/icons-material/ContentCut'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import { PDFPage, PDFDocument } from '../../types/pageEditor'; import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../commands/pageCommands'; import { Command } from '../../hooks/useUndoRedo'; import { useFileState } from '../../contexts/FileContext'; import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration'; import styles from './PageEditor.module.css'; import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist'; // Ensure PDF.js worker is available if (!GlobalWorkerOptions.workerSrc) { GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; } interface PageThumbnailProps { page: PDFPage; index: number; totalPages: number; originalFile?: File; // For lazy thumbnail generation selectedPages: number[]; selectionMode: boolean; draggedPage: number | null; dropTarget: number | 'end' | null; movingPage: number | null; isAnimating: boolean; pageRefs: React.MutableRefObject>; onDragStart: (pageNumber: number) => void; onDragEnd: () => void; onDragOver: (e: React.DragEvent) => void; onDragEnter: (pageNumber: number) => void; onDragLeave: () => void; onDrop: (e: React.DragEvent, pageNumber: number) => void; onTogglePage: (pageNumber: number) => void; onAnimateReorder: (pageNumber: number, targetIndex: number) => void; onExecuteCommand: (command: Command) => void; onSetStatus: (status: string) => void; onSetMovingPage: (pageNumber: number | null) => void; RotatePagesCommand: typeof RotatePagesCommand; DeletePagesCommand: typeof DeletePagesCommand; ToggleSplitCommand: typeof ToggleSplitCommand; pdfDocument: PDFDocument; setPdfDocument: (doc: PDFDocument) => void; } const PageThumbnail = React.memo(({ page, index, totalPages, originalFile, selectedPages, selectionMode, draggedPage, dropTarget, movingPage, isAnimating, pageRefs, onDragStart, onDragEnd, onDragOver, onDragEnter, onDragLeave, onDrop, onTogglePage, onAnimateReorder, onExecuteCommand, onSetStatus, onSetMovingPage, RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand, pdfDocument, setPdfDocument, }: PageThumbnailProps) => { const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const { state, selectors } = useFileState(); const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration(); // Update thumbnail URL when page prop changes - prevent redundant updates useEffect(() => { if (page.thumbnail && page.thumbnail !== thumbnailUrl) { console.log(`📸 PageThumbnail: Updating thumbnail URL for page ${page.pageNumber}`, page.thumbnail.substring(0, 50) + '...'); setThumbnailUrl(page.thumbnail); } }, [page.thumbnail, page.id]); // Remove thumbnailUrl dependency to prevent redundant cycles // Request thumbnail generation if not available (optimized for performance) useEffect(() => { if (thumbnailUrl || !originalFile) { return; // Skip if we already have a thumbnail or no original file } // Check cache first without async call const cachedThumbnail = getThumbnailFromCache(page.id); if (cachedThumbnail) { setThumbnailUrl(cachedThumbnail); return; } let cancelled = false; const loadThumbnail = async () => { try { const thumbnail = await requestThumbnail(page.id, originalFile, page.pageNumber); // Only update if component is still mounted and we got a result if (!cancelled && thumbnail) { setThumbnailUrl(thumbnail); } } catch (error) { if (!cancelled) { console.warn(`📸 PageThumbnail: Failed to load thumbnail for page ${page.pageNumber}:`, error); } } }; loadThumbnail(); // Cleanup function to prevent state updates after unmount return () => { cancelled = true; }; }, [page.id, originalFile, requestThumbnail, getThumbnailFromCache]); // Removed thumbnailUrl to prevent loops // Register this component with pageRefs for animations const pageElementRef = useCallback((element: HTMLDivElement | null) => { if (element) { pageRefs.current.set(page.id, element); } else { pageRefs.current.delete(page.id); } }, [page.id, pageRefs]); return (
{ if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) { return 'translateX(20px)'; } return 'translateX(0)'; })(), transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out' }} draggable onDragStart={() => onDragStart(page.pageNumber)} onDragEnd={onDragEnd} onDragOver={onDragOver} onDragEnter={() => onDragEnter(page.pageNumber)} onDragLeave={onDragLeave} onDrop={(e) => onDrop(e, page.pageNumber)} > {selectionMode && (
e.stopPropagation()} onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={(e) => { console.log('📸 Checkbox clicked for page', page.pageNumber); e.stopPropagation(); onTogglePage(page.pageNumber); }} > { // onChange is handled by the parent div click }} size="sm" />
)}
{thumbnailUrl ? ( {`Page ) : (
📄 Page {page.pageNumber}
)}
{page.pageNumber}
{ e.stopPropagation(); if (index > 0 && !movingPage && !isAnimating) { onSetMovingPage(page.pageNumber); onAnimateReorder(page.pageNumber, index - 1); setTimeout(() => onSetMovingPage(null), 500); onSetStatus(`Moved page ${page.pageNumber} left`); } }} > { e.stopPropagation(); if (index < totalPages - 1 && !movingPage && !isAnimating) { onSetMovingPage(page.pageNumber); onAnimateReorder(page.pageNumber, index + 1); setTimeout(() => onSetMovingPage(null), 500); onSetStatus(`Moved page ${page.pageNumber} right`); } }} > { e.stopPropagation(); const command = new RotatePagesCommand( pdfDocument, setPdfDocument, [page.id], -90 ); onExecuteCommand(command); onSetStatus(`Rotated page ${page.pageNumber} left`); }} > { e.stopPropagation(); const command = new RotatePagesCommand( pdfDocument, setPdfDocument, [page.id], 90 ); onExecuteCommand(command); onSetStatus(`Rotated page ${page.pageNumber} right`); }} > { e.stopPropagation(); const command = new DeletePagesCommand( pdfDocument, setPdfDocument, [page.id] ); onExecuteCommand(command); onSetStatus(`Deleted page ${page.pageNumber}`); }} > {index > 0 && ( { e.stopPropagation(); const command = new ToggleSplitCommand( pdfDocument, setPdfDocument, [page.id] ); onExecuteCommand(command); onSetStatus(`Split marker toggled for page ${page.pageNumber}`); }} > )}
); }, (prevProps, nextProps) => { // Helper for shallow array comparison const arraysEqual = (a: number[], b: number[]) => { return a.length === b.length && a.every((val, i) => val === b[i]); }; // Only re-render if essential props change return ( prevProps.page.id === nextProps.page.id && prevProps.page.pageNumber === nextProps.page.pageNumber && prevProps.page.rotation === nextProps.page.rotation && prevProps.page.thumbnail === nextProps.page.thumbnail && // Shallow compare selectedPages array for better stability (prevProps.selectedPages === nextProps.selectedPages || arraysEqual(prevProps.selectedPages, nextProps.selectedPages)) && prevProps.selectionMode === nextProps.selectionMode && prevProps.draggedPage === nextProps.draggedPage && prevProps.dropTarget === nextProps.dropTarget && prevProps.movingPage === nextProps.movingPage && prevProps.isAnimating === nextProps.isAnimating ); }); export default PageThumbnail;