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 { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; 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'; interface PageThumbnailProps { page: PDFPage; index: number; totalPages: number; originalFile?: File; // For lazy thumbnail generation selectedPages: number[]; selectionMode: boolean; movingPage: number | null; isAnimating: boolean; pageRefs: React.MutableRefObject>; onTogglePage: (pageNumber: number) => void; onAnimateReorder: (pageNumber: number, targetIndex: number) => void; onExecuteCommand: (command: Command) => void; onSetStatus: (status: string) => void; onSetMovingPage: (pageNumber: number | null) => void; onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => 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, movingPage, isAnimating, pageRefs, onTogglePage, onAnimateReorder, onExecuteCommand, onSetStatus, onSetMovingPage, onReorderPages, RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand, pdfDocument, setPdfDocument, }: PageThumbnailProps) => { const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const [isDragging, setIsDragging] = useState(false); const dragElementRef = useRef(null); 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 const pageElementRef = useCallback((element: HTMLDivElement | null) => { if (element) { pageRefs.current.set(page.id, element); dragElementRef.current = element; const dragCleanup = draggable({ element, getInitialData: () => ({ pageNumber: page.pageNumber, pageId: page.id, selectedPages: selectionMode && selectedPages.includes(page.pageNumber) ? selectedPages : [page.pageNumber] }), onDragStart: () => { setIsDragging(true); }, onDrop: ({ location }) => { setIsDragging(false); if (location.current.dropTargets.length === 0) { return; } const dropTarget = location.current.dropTargets[0]; const targetData = dropTarget.data; if (targetData.type === 'page') { const targetPageNumber = targetData.pageNumber as number; const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber); if (targetIndex !== -1) { const pagesToMove = selectionMode && selectedPages.includes(page.pageNumber) ? selectedPages : undefined; onReorderPages(page.pageNumber, targetIndex, pagesToMove); } } } }); element.style.cursor = 'grab'; const dropCleanup = dropTargetForElements({ element, getData: () => ({ type: 'page', pageNumber: page.pageNumber }), onDrop: ({ source }) => {} }); (element as any).__dragCleanup = () => { dragCleanup(); dropCleanup(); }; } else { pageRefs.current.delete(page.id); if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) { (dragElementRef.current as any).__dragCleanup(); } } }, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPages, pdfDocument.pages, onReorderPages]); return (
{
e.stopPropagation()} onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }} onClick={(e) => { 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.movingPage === nextProps.movingPage && prevProps.isAnimating === nextProps.isAnimating ); }); export default PageThumbnail;