diff --git a/frontend/src/components/editor/FileEditor.tsx b/frontend/src/components/editor/FileEditor.tsx index bd23b2ea0..b60ccba81 100644 --- a/frontend/src/components/editor/FileEditor.tsx +++ b/frontend/src/components/editor/FileEditor.tsx @@ -27,8 +27,8 @@ interface FileItem { interface FileEditorProps { onOpenPageEditor?: (file: File) => void; onMergeFiles?: (files: File[]) => void; - sharedFiles?: { file: File; url: string }[]; - setSharedFiles?: (files: { file: File; url: string }[]) => void; + activeFiles?: File[]; + setActiveFiles?: (files: File[]) => void; preSelectedFiles?: { file: File; url: string }[]; onClearPreSelection?: () => void; } @@ -36,15 +36,14 @@ interface FileEditorProps { const FileEditor = ({ onOpenPageEditor, onMergeFiles, - sharedFiles = [], - setSharedFiles, + activeFiles = [], + setActiveFiles, preSelectedFiles = [], onClearPreSelection }: FileEditorProps) => { const { t } = useTranslation(); - const files = sharedFiles; // Use sharedFiles as the source of truth - + const [files, setFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); @@ -74,6 +73,39 @@ const FileEditor = ({ }; }, []); + // Convert activeFiles to FileItem format + useEffect(() => { + const convertActiveFiles = async () => { + if (activeFiles.length > 0) { + setLoading(true); + try { + const convertedFiles = await Promise.all( + activeFiles.map(async (file) => { + const thumbnail = await generateThumbnailForFile(file); + return { + id: `file-${Date.now()}-${Math.random()}`, + name: file.name.replace(/\.pdf$/i, ''), + pageCount: Math.floor(Math.random() * 20) + 1, // Mock for now + thumbnail, + size: file.size, + file, + }; + }) + ); + setFiles(convertedFiles); + } catch (err) { + console.error('Error converting active files:', err); + } finally { + setLoading(false); + } + } else { + setFiles([]); + } + }; + + convertActiveFiles(); + }, [activeFiles]); + // Only load shared files when explicitly passed (not on mount) useEffect(() => { const loadSharedFiles = async () => { @@ -84,7 +116,10 @@ const FileEditor = ({ const convertedFiles = await Promise.all( preSelectedFiles.map(convertToFileItem) ); - setFiles(convertedFiles); + if (setActiveFiles) { + const updatedActiveFiles = convertedFiles.map(fileItem => fileItem.file); + setActiveFiles(updatedActiveFiles); + } } catch (err) { console.error('Error converting pre-selected files:', err); } finally { @@ -137,8 +172,8 @@ const FileEditor = ({ await fileStorage.storeFile(file, thumbnail); } - if (setSharedFiles) { - setSharedFiles(prev => [...prev, ...newFiles]); + if (setActiveFiles) { + setActiveFiles(prev => [...prev, ...newFiles.map(f => f.file)]); } setStatus(`Added ${newFiles.length} files`); @@ -149,7 +184,7 @@ const FileEditor = ({ } finally { setLoading(false); } - }, [setSharedFiles]); + }, [setActiveFiles]); const selectAll = useCallback(() => { setSelectedFiles(files.map(f => f.id)); @@ -283,8 +318,9 @@ const FileEditor = ({ ? selectedFiles : [draggedFile]; - if (setSharedFiles) { - setSharedFiles(prev => { + if (setActiveFiles) { + // Update the local files state and sync with activeFiles + setFiles(prev => { const newFiles = [...prev]; const movedFiles = filesToMove.map(id => newFiles.find(f => f.id === id)!).filter(Boolean); @@ -296,6 +332,10 @@ const FileEditor = ({ // Insert at target position newFiles.splice(targetIndex, 0, ...movedFiles); + + // Update activeFiles with the reordered File objects + setActiveFiles(newFiles.map(f => f.file)); + return newFiles; }); } @@ -304,7 +344,7 @@ const FileEditor = ({ setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); handleDragEnd(); - }, [draggedFile, files, selectionMode, selectedFiles, multiFileDrag, handleDragEnd, setSharedFiles]); + }, [draggedFile, files, selectionMode, selectedFiles, multiFileDrag, handleDragEnd, setActiveFiles]); const handleEndZoneDragEnter = useCallback(() => { if (draggedFile) { @@ -314,11 +354,16 @@ const FileEditor = ({ // File operations const handleDeleteFile = useCallback((fileId: string) => { - if (setSharedFiles) { - setSharedFiles(prev => prev.filter(f => f.id !== fileId)); + if (setActiveFiles) { + // Remove from local files and sync with activeFiles + setFiles(prev => { + const newFiles = prev.filter(f => f.id !== fileId); + setActiveFiles(newFiles.map(f => f.file)); + return newFiles; + }); } setSelectedFiles(prev => prev.filter(id => id !== fileId)); - }, [setSharedFiles]); + }, [setActiveFiles]); const handleViewFile = useCallback((fileId: string) => { const file = files.find(f => f.id === fileId); @@ -483,8 +528,9 @@ const FileEditor = ({ setShowFilePickerModal(false)} - sharedFiles={sharedFiles || []} + storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent onSelectFiles={handleLoadFromStorage} + allowMultiple={true} /> {status && ( diff --git a/frontend/src/components/editor/PageEditor.tsx b/frontend/src/components/editor/PageEditor.tsx index 9a84ab6d4..0adc4aba3 100644 --- a/frontend/src/components/editor/PageEditor.tsx +++ b/frontend/src/components/editor/PageEditor.tsx @@ -27,13 +27,13 @@ import FilePickerModal from '../shared/FilePickerModal'; import FileUploadSelector from '../shared/FileUploadSelector'; export interface PageEditorProps { - file: { file: File; url: string } | null; - setFile?: (file: { file: File; url: string } | null) => void; + activeFiles: File[]; + setActiveFiles: (files: File[]) => void; downloadUrl?: string | null; setDownloadUrl?: (url: string | null) => void; - sharedFiles?: { file: File; url: string }[]; + sharedFiles?: any[]; // For FileUploadSelector when no files loaded - // Optional callbacks to expose internal functions + // Optional callbacks to expose internal functions for PageEditorControls onFunctionsReady?: (functions: { handleUndo: () => void; handleRedo: () => void; @@ -43,6 +43,8 @@ export interface PageEditorProps { handleDelete: () => void; handleSplit: () => void; showExportPreview: (selectedOnly: boolean) => void; + onExportSelected: () => void; + onExportAll: () => void; exportLoading: boolean; selectionMode: boolean; selectedPages: string[]; @@ -51,31 +53,46 @@ export interface PageEditorProps { } const PageEditor = ({ - file, - setFile, + activeFiles, + setActiveFiles, downloadUrl, setDownloadUrl, + sharedFiles = [], onFunctionsReady, - sharedFiles, }: PageEditorProps) => { const { t } = useTranslation(); const { processPDFFile, loading: pdfLoading } = usePDFProcessor(); - const [pdfDocument, setPdfDocument] = useState(null); + // Multi-file state + const [currentFileIndex, setCurrentFileIndex] = useState(0); + const [processedFiles, setProcessedFiles] = useState>(new Map()); + + // Current file references + const currentFile = activeFiles[currentFileIndex] || null; + const currentFileKey = currentFile ? `${currentFile.name}-${currentFile.size}` : null; + const currentPdfDocument = currentFileKey ? processedFiles.get(currentFileKey) : null; + const [filename, setFilename] = useState(""); + + // Page editor state const [selectedPages, setSelectedPages] = useState([]); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [csvInput, setCsvInput] = useState(""); const [selectionMode, setSelectionMode] = useState(false); - const [filename, setFilename] = useState(""); + + // Drag and drop state const [draggedPage, setDraggedPage] = useState(null); const [dropTarget, setDropTarget] = useState(null); const [multiPageDrag, setMultiPageDrag] = useState<{pageIds: string[], count: number} | null>(null); const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null); + + // Export state const [exportLoading, setExportLoading] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); + + // Animation state const [movingPage, setMovingPage] = useState(null); const [pagePositions, setPagePositions] = useState>(new Map()); const [isAnimating, setIsAnimating] = useState(false); @@ -122,14 +139,26 @@ const PageEditor = ({ return; } + const fileKey = `${fileToProcess.name}-${fileToProcess.size}`; + + // Skip if already processed + if (processedFiles.has(fileKey)) return; + setLoading(true); setError(null); try { const document = await processPDFFile(fileToProcess); - setPdfDocument(document); + + // Store processed document + setProcessedFiles(prev => new Map(prev).set(fileKey, document)); setFilename(fileToProcess.name.replace(/\.pdf$/i, '')); setSelectedPages([]); + + // Add to activeFiles if not already there + if (!activeFiles.some(f => f.name === fileToProcess.name && f.size === fileToProcess.size)) { + setActiveFiles([...activeFiles, fileToProcess]); + } if (document.pages.length > 0) { // Only store if it's a new file (not from storage) @@ -138,12 +167,7 @@ const PageEditor = ({ await fileStorage.storeFile(fileToProcess, thumbnail); } } - - if (setFile) { - const fileUrl = URL.createObjectURL(fileToProcess); - setFile({ file: fileToProcess, url: fileUrl }); - } - + setStatus(`PDF loaded successfully with ${document.totalPages} pages`); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF'; @@ -152,13 +176,38 @@ const PageEditor = ({ } finally { setLoading(false); } - }, [processPDFFile, setFile]); + }, [processPDFFile, activeFiles, setActiveFiles, processedFiles]); + // Auto-process files from activeFiles useEffect(() => { - if (file?.file && !pdfDocument) { - handleFileUpload(file.file); + activeFiles.forEach(file => { + const fileKey = `${file.name}-${file.size}`; + if (!processedFiles.has(fileKey)) { + handleFileUpload(file); + } + }); + }, [activeFiles, processedFiles, handleFileUpload]); + + // Reset current file index when activeFiles changes + useEffect(() => { + if (currentFileIndex >= activeFiles.length) { + setCurrentFileIndex(0); } - }, [file, pdfDocument, handleFileUpload]); + }, [activeFiles.length, currentFileIndex]); + + // Clear selections when switching files + useEffect(() => { + setSelectedPages([]); + setCsvInput(""); + setSelectionMode(false); + }, [currentFileIndex]); + + // Update filename when current file changes + useEffect(() => { + if (currentFile) { + setFilename(currentFile.name.replace(/\.pdf$/i, '')); + } + }, [currentFile]); // Global drag cleanup to handle drops outside valid areas useEffect(() => { @@ -187,10 +236,10 @@ const PageEditor = ({ }, [draggedPage]); const selectAll = useCallback(() => { - if (pdfDocument) { - setSelectedPages(pdfDocument.pages.map(p => p.id)); + if (currentPdfDocument) { + setSelectedPages(currentPdfDocument.pages.map(p => p.id)); } - }, [pdfDocument]); + }, [currentPdfDocument]); const deselectAll = useCallback(() => setSelectedPages([]), []); @@ -215,7 +264,7 @@ const PageEditor = ({ }, []); const parseCSVInput = useCallback((csv: string) => { - if (!pdfDocument) return []; + if (!currentPdfDocument) return []; const pageIds: string[] = []; const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); @@ -223,23 +272,23 @@ const PageEditor = ({ ranges.forEach(range => { if (range.includes('-')) { const [start, end] = range.split('-').map(n => parseInt(n.trim())); - for (let i = start; i <= end && i <= pdfDocument.totalPages; i++) { + for (let i = start; i <= end && i <= currentPdfDocument.totalPages; i++) { if (i > 0) { - const page = pdfDocument.pages.find(p => p.pageNumber === i); + const page = currentPdfDocument.pages.find(p => p.pageNumber === i); if (page) pageIds.push(page.id); } } } else { const pageNum = parseInt(range); - if (pageNum > 0 && pageNum <= pdfDocument.totalPages) { - const page = pdfDocument.pages.find(p => p.pageNumber === pageNum); + if (pageNum > 0 && pageNum <= currentPdfDocument.totalPages) { + const page = currentPdfDocument.pages.find(p => p.pageNumber === pageNum); if (page) pageIds.push(page.id); } } }); return pageIds; - }, [pdfDocument]); + }, [currentPdfDocument]); const updatePagesFromCSV = useCallback(() => { const pageIds = parseCSVInput(csvInput); @@ -313,22 +362,29 @@ const PageEditor = ({ // Don't clear drop target on drag leave - let dragover handle it }, []); + // Create setPdfDocument wrapper for current file + const setPdfDocument = useCallback((updatedDoc: PDFDocument) => { + if (currentFileKey) { + setProcessedFiles(prev => new Map(prev).set(currentFileKey, updatedDoc)); + } + }, [currentFileKey]); + const animateReorder = useCallback((pageId: string, targetIndex: number) => { - if (!pdfDocument || isAnimating) return; + if (!currentPdfDocument || isAnimating) return; // In selection mode, if the dragged page is selected, move all selected pages const pagesToMove = selectionMode && selectedPages.includes(pageId) ? selectedPages : [pageId]; - const originalIndex = pdfDocument.pages.findIndex(p => p.id === pageId); + const originalIndex = currentPdfDocument.pages.findIndex(p => p.id === pageId); if (originalIndex === -1 || originalIndex === targetIndex) return; setIsAnimating(true); // Get current positions of all pages const currentPositions = new Map(); - pdfDocument.pages.forEach((page) => { + currentPdfDocument.pages.forEach((page) => { const element = pageRefs.current.get(page.id); if (element) { const rect = element.getBoundingClientRect(); @@ -339,11 +395,11 @@ const PageEditor = ({ // Execute the reorder - for multi-page, we use a different command if (pagesToMove.length > 1) { // Multi-page move - use MovePagesCommand - const command = new MovePagesCommand(pdfDocument, setPdfDocument, pagesToMove, targetIndex); + const command = new MovePagesCommand(currentPdfDocument, setPdfDocument, pagesToMove, targetIndex); executeCommand(command); } else { // Single page move - const command = new ReorderPageCommand(pdfDocument, setPdfDocument, pageId, targetIndex); + const command = new ReorderPageCommand(currentPdfDocument, setPdfDocument, pageId, targetIndex); executeCommand(command); } @@ -353,8 +409,7 @@ const PageEditor = ({ const newPositions = new Map(); // Get the updated document from the state after command execution - // The command has already updated the document, so we need to get the new order - const currentDoc = pdfDocument; // This should be the updated version after command + const currentDoc = currentPdfDocument; // This should be the updated version after command currentDoc.pages.forEach((page) => { const element = pageRefs.current.get(page.id); @@ -400,17 +455,17 @@ const PageEditor = ({ }, 400); }); }); - }, [pdfDocument, isAnimating, executeCommand, selectionMode, selectedPages]); + }, [currentPdfDocument, isAnimating, executeCommand, selectionMode, selectedPages, setPdfDocument]); const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => { e.preventDefault(); - if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return; + if (!draggedPage || !currentPdfDocument || draggedPage === targetPageId) return; let targetIndex: number; if (targetPageId === 'end') { - targetIndex = pdfDocument.pages.length; + targetIndex = currentPdfDocument.pages.length; } else { - targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId); + targetIndex = currentPdfDocument.pages.findIndex(p => p.id === targetPageId); if (targetIndex === -1) return; } @@ -423,7 +478,7 @@ const PageEditor = ({ const moveCount = multiPageDrag ? multiPageDrag.count : 1; setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`); - }, [draggedPage, pdfDocument, animateReorder, multiPageDrag]); + }, [draggedPage, currentPdfDocument, animateReorder, multiPageDrag]); const handleEndZoneDragEnter = useCallback(() => { if (draggedPage) { @@ -432,38 +487,38 @@ const PageEditor = ({ }, [draggedPage]); const handleRotate = useCallback((direction: 'left' | 'right') => { - if (!pdfDocument) return; + if (!currentPdfDocument) return; const rotation = direction === 'left' ? -90 : 90; const pagesToRotate = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : currentPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new RotatePagesCommand( - pdfDocument, + currentPdfDocument, setPdfDocument, pagesToRotate, rotation ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; setStatus(`Rotated ${pageCount} pages ${direction}`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleDelete = useCallback(() => { - if (!pdfDocument) return; + if (!currentPdfDocument) return; const pagesToDelete = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : currentPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new DeletePagesCommand( - pdfDocument, + currentPdfDocument, setPdfDocument, pagesToDelete ); @@ -472,55 +527,55 @@ const PageEditor = ({ if (selectionMode) { setSelectedPages([]); } - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; setStatus(`Deleted ${pageCount} pages`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleSplit = useCallback(() => { - if (!pdfDocument) return; + if (!currentPdfDocument) return; const pagesToSplit = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : currentPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new ToggleSplitCommand( - pdfDocument, + currentPdfDocument, setPdfDocument, pagesToSplit ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; setStatus(`Split markers toggled for ${pageCount} pages`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { - if (!pdfDocument) return; + if (!currentPdfDocument) return; const exportPageIds = selectedOnly ? selectedPages : []; - const preview = pdfExportService.getExportInfo(pdfDocument, exportPageIds, selectedOnly); + const preview = pdfExportService.getExportInfo(currentPdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); - }, [pdfDocument, selectedPages]); + }, [currentPdfDocument, selectedPages]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { - if (!pdfDocument) return; + if (!currentPdfDocument) return; setExportLoading(true); try { const exportPageIds = selectedOnly ? selectedPages : []; - const errors = pdfExportService.validateExport(pdfDocument, exportPageIds, selectedOnly); + const errors = pdfExportService.validateExport(currentPdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { setError(errors.join(', ')); return; } - const hasSplitMarkers = pdfDocument.pages.some(page => page.splitBefore); + const hasSplitMarkers = currentPdfDocument.pages.some(page => page.splitBefore); if (hasSplitMarkers) { - const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(currentPdfDocument, exportPageIds, { selectedOnly, filename, splitDocuments: true @@ -534,7 +589,7 @@ const PageEditor = ({ setStatus(`Exported ${result.blobs.length} split documents`); } else { - const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(currentPdfDocument, exportPageIds, { selectedOnly, filename }) as { blob: Blob; filename: string }; @@ -548,7 +603,7 @@ const PageEditor = ({ } finally { setExportLoading(false); } - }, [pdfDocument, selectedPages, filename]); + }, [currentPdfDocument, selectedPages, filename]); const handleUndo = useCallback(() => { if (undo()) { @@ -563,11 +618,17 @@ const PageEditor = ({ }, [redo]); const closePdf = useCallback(() => { - setPdfDocument(null); - setFile && setFile(null); - }, [setFile]); + setCurrentFileIndex(0); + setActiveFiles([]); + setProcessedFiles(new Map()); + setSelectedPages([]); + }, [setActiveFiles]); - // Expose functions to parent component + // PageEditorControls needs onExportSelected and onExportAll + const onExportSelected = useCallback(() => showExportPreview(true), [showExportPreview]); + const onExportAll = useCallback(() => showExportPreview(false), [showExportPreview]); + + // Expose functions to parent component for PageEditorControls useEffect(() => { if (onFunctionsReady) { onFunctionsReady({ @@ -579,6 +640,8 @@ const PageEditor = ({ handleDelete, handleSplit, showExportPreview, + onExportSelected, + onExportAll, exportLoading, selectionMode, selectedPages, @@ -595,13 +658,15 @@ const PageEditor = ({ handleDelete, handleSplit, showExportPreview, + onExportSelected, + onExportAll, exportLoading, selectionMode, selectedPages, closePdf ]); - if (!pdfDocument) { + if (!currentPdfDocument) { return ( @@ -610,7 +675,7 @@ const PageEditor = ({ + {/* File Switcher Tabs */} + {activeFiles.length > 1 && ( + + + {activeFiles.map((file, index) => { + const isActive = index === currentFileIndex; + + return ( + + ); + })} + + + )} + )} @@ -753,7 +849,7 @@ const PageEditor = ({ {exportPreview.estimatedSize} - {pdfDocument && pdfDocument.pages.some(p => p.splitBefore) && ( + {currentPdfDocument && currentPdfDocument.pages.some(p => p.splitBefore) && ( This will create multiple PDF files based on split markers. @@ -771,7 +867,7 @@ const PageEditor = ({ loading={exportLoading} onClick={() => { setShowExportModal(false); - const selectedOnly = exportPreview.pageCount < (pdfDocument?.totalPages || 0); + const selectedOnly = exportPreview.pageCount < (currentPdfDocument?.totalPages || 0); handleExport(selectedOnly); }} > @@ -800,9 +896,19 @@ const PageEditor = ({ )} + {error && ( + setError(null)} + style={{ position: 'fixed', bottom: 70, right: 20, zIndex: 1000 }} + > + {error} + + )} ); }; -export default PageEditor; +export default PageEditor; \ No newline at end of file diff --git a/frontend/src/components/editor/PageThumbnail.tsx b/frontend/src/components/editor/PageThumbnail.tsx index 89d0811b1..1dc70843d 100644 --- a/frontend/src/components/editor/PageThumbnail.tsx +++ b/frontend/src/components/editor/PageThumbnail.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Text, Checkbox, Tooltip, ActionIcon } from '@mantine/core'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; @@ -67,8 +67,18 @@ const PageThumbnail = ({ pdfDocument, setPdfDocument, }: PageThumbnailProps) => { + // 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 (
void; onOpenFileEditor?: (selectedFiles?: FileWithUrl[]) => void; + onOpenPageEditor?: (selectedFiles?: FileWithUrl[]) => void; onLoadFileToActive?: (file: File) => void; } @@ -31,6 +32,7 @@ const FileManager = ({ allowMultiple = true, setCurrentView, onOpenFileEditor, + onOpenPageEditor, onLoadFileToActive, }: FileManagerProps) => { const { t } = useTranslation(); @@ -335,6 +337,13 @@ const FileManager = ({ } }; + const handleOpenSelectedInPageEditor = () => { + if (onOpenPageEditor && selectedFiles.length > 0) { + const selected = files.filter(f => selectedFiles.includes(f.id || f.name)); + onOpenPageEditor(selected); + } + }; + return (
{t("fileManager.openInFileEditor", "Open in File Editor")} + diff --git a/frontend/src/components/shared/FilePickerModal.tsx b/frontend/src/components/shared/FilePickerModal.tsx index 2a5cfa76e..cad7fd373 100644 --- a/frontend/src/components/shared/FilePickerModal.tsx +++ b/frontend/src/components/shared/FilePickerModal.tsx @@ -19,15 +19,17 @@ import { useTranslation } from 'react-i18next'; interface FilePickerModalProps { opened: boolean; onClose: () => void; - sharedFiles: any[]; - onSelectFiles: (selectedFiles: any[]) => void; + storedFiles: any[]; // Files from storage (FileWithUrl format) + onSelectFiles: (selectedFiles: File[]) => void; + allowMultiple?: boolean; } const FilePickerModal = ({ opened, onClose, - sharedFiles, + storedFiles, onSelectFiles, + allowMultiple = true, }: FilePickerModalProps) => { const { t } = useTranslation(); const [selectedFileIds, setSelectedFileIds] = useState([]); @@ -40,15 +42,22 @@ const FilePickerModal = ({ }, [opened]); const toggleFileSelection = (fileId: string) => { - setSelectedFileIds(prev => - prev.includes(fileId) - ? prev.filter(id => id !== fileId) - : [...prev, fileId] - ); + setSelectedFileIds(prev => { + if (allowMultiple) { + return prev.includes(fileId) + ? prev.filter(id => id !== fileId) + : [...prev, fileId]; + } else { + // Single selection mode + return prev.includes(fileId) ? [] : [fileId]; + } + }); }; const selectAll = () => { - setSelectedFileIds(sharedFiles.map(f => f.id || f.name)); + if (allowMultiple) { + setSelectedFileIds(storedFiles.map(f => f.id || f.name)); + } }; const selectNone = () => { @@ -56,56 +65,54 @@ const FilePickerModal = ({ }; const handleConfirm = async () => { - const selectedFiles = sharedFiles.filter(f => + const selectedFiles = storedFiles.filter(f => selectedFileIds.includes(f.id || f.name) ); - // Convert FileWithUrl objects to proper File objects if needed + // Convert stored files to File objects const convertedFiles = await Promise.all( selectedFiles.map(async (fileItem) => { - console.log('Converting file item:', fileItem); - - // If it's already a File object, return as is - if (fileItem instanceof File) { - console.log('File is already a File object'); - return fileItem; - } - - // If it has a file property, use that - if (fileItem.file && fileItem.file instanceof File) { - console.log('Using .file property'); - return fileItem.file; - } - - // If it's a FileWithUrl from storage, reconstruct the File - if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { - try { - console.log('Reconstructing file from storage:', fileItem.name, fileItem); + try { + // If it's already a File object, return as is + if (fileItem instanceof File) { + return fileItem; + } + + // If it has a file property, use that + if (fileItem.file && fileItem.file instanceof File) { + return fileItem.file; + } + + // If it's from IndexedDB storage, reconstruct the File + if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { const arrayBuffer = await fileItem.arrayBuffer(); - console.log('Got arrayBuffer:', arrayBuffer); - const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); - console.log('Created blob:', blob); - - const reconstructedFile = new File([blob], fileItem.name, { + return new File([blob], fileItem.name, { type: fileItem.type || 'application/pdf', lastModified: fileItem.lastModified || Date.now() }); - console.log('Reconstructed file:', reconstructedFile, 'instanceof File:', reconstructedFile instanceof File); - return reconstructedFile; - } catch (error) { - console.error('Error reconstructing file:', error, fileItem); - return null; } + + // If it has data property, reconstruct the File + if (fileItem.data) { + const blob = new Blob([fileItem.data], { type: fileItem.type || 'application/pdf' }); + return new File([blob], fileItem.name, { + type: fileItem.type || 'application/pdf', + lastModified: fileItem.lastModified || Date.now() + }); + } + + console.warn('Could not convert file item:', fileItem); + return null; + } catch (error) { + console.error('Error converting file:', error, fileItem); + return null; } - - console.log('No valid conversion method found for:', fileItem); - return null; // Don't return invalid objects }) ); - // Filter out any null values from failed conversions - const validFiles = convertedFiles.filter(f => f !== null); + // Filter out any null values and return valid Files + const validFiles = convertedFiles.filter((f): f is File => f !== null); onSelectFiles(validFiles); onClose(); @@ -128,7 +135,7 @@ const FilePickerModal = ({ scrollAreaComponent={ScrollArea.Autosize} > - {sharedFiles.length === 0 ? ( + {storedFiles.length === 0 ? ( {t("fileUpload.noFilesInStorage", "No files available in storage. Upload some files first.")} @@ -137,22 +144,27 @@ const FilePickerModal = ({ {/* Selection controls */} - {sharedFiles.length} {t("fileUpload.filesAvailable", "files available")} + {storedFiles.length} {t("fileUpload.filesAvailable", "files available")} + {allowMultiple && selectedFileIds.length > 0 && ( + <> • {selectedFileIds.length} selected + )} - - - - + {allowMultiple && ( + + + + + )} {/* File grid */} - {sharedFiles.map((file) => { + {storedFiles.map((file) => { const fileId = file.id || file.name; const isSelected = selectedFileIds.includes(fileId); @@ -174,11 +186,21 @@ const FilePickerModal = ({ onClick={() => toggleFileSelection(fileId)} > - toggleFileSelection(fileId)} - onClick={(e) => e.stopPropagation()} - /> + {allowMultiple ? ( + toggleFileSelection(fileId)} + onClick={(e) => e.stopPropagation()} + /> + ) : ( + toggleFileSelection(fileId)} + onClick={(e) => e.stopPropagation()} + style={{ margin: '4px' }} + /> + )} {/* Thumbnail */} setShowFilePickerModal(false)} - sharedFiles={sharedFiles} + storedFiles={sharedFiles} onSelectFiles={handleStorageSelection} + allowMultiple={allowMultiple} /> ); diff --git a/frontend/src/hooks/useFileWithUrl.ts b/frontend/src/hooks/useFileWithUrl.ts index d06cb73f2..aeb954cd1 100644 --- a/frontend/src/hooks/useFileWithUrl.ts +++ b/frontend/src/hooks/useFileWithUrl.ts @@ -8,15 +8,26 @@ export function useFileWithUrl(file: File | null): { file: File; url: string } | return useMemo(() => { if (!file) return null; - const url = URL.createObjectURL(file); + // Validate that file is a proper File or Blob object + if (!(file instanceof File) && !(file instanceof Blob)) { + console.warn('useFileWithUrl: Expected File or Blob, got:', file); + return null; + } - // Return object with cleanup function - const result = { file, url }; - - // Store cleanup function for later use - (result as any)._cleanup = () => URL.revokeObjectURL(url); - - return result; + try { + const url = URL.createObjectURL(file); + + // Return object with cleanup function + const result = { file, url }; + + // Store cleanup function for later use + (result as any)._cleanup = () => URL.revokeObjectURL(url); + + return result; + } catch (error) { + console.error('useFileWithUrl: Failed to create object URL:', error, file); + return null; + } }, [file]); } diff --git a/frontend/src/hooks/useToolParams.ts b/frontend/src/hooks/useToolParams.ts index be6145b3e..9c422da14 100644 --- a/frontend/src/hooks/useToolParams.ts +++ b/frontend/src/hooks/useToolParams.ts @@ -121,7 +121,7 @@ export function useToolParams(selectedToolKey: string, currentView: string) { }); setSearchParams(newParams, { replace: true }); - }, [selectedToolKey, currentView, setSearchParams, searchParams]); + }, [selectedToolKey, currentView, setSearchParams]); return { toolParams, diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index acd8a85b6..fdc823532 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -173,15 +173,109 @@ export default function HomePage() { }, [addToActiveFiles]); // Handle opening file editor with selected files - const handleOpenFileEditor = useCallback((selectedFiles) => { - setPreSelectedFiles(selectedFiles || []); - handleViewChange("fileEditor"); - }, [handleViewChange]); + const handleOpenFileEditor = useCallback(async (selectedFiles) => { + if (!selectedFiles || selectedFiles.length === 0) { + setPreSelectedFiles([]); + handleViewChange("fileEditor"); + return; + } + + // Convert FileWithUrl[] to File[] and add to activeFiles + try { + const convertedFiles = await Promise.all( + selectedFiles.map(async (fileItem) => { + // If it's already a File, return as is + if (fileItem instanceof File) { + return fileItem; + } + + // If it has a file property, use that + if (fileItem.file && fileItem.file instanceof File) { + return fileItem.file; + } + + // If it's from IndexedDB storage, reconstruct the File + if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { + const arrayBuffer = await fileItem.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); + const file = new File([blob], fileItem.name, { + type: fileItem.type || 'application/pdf', + lastModified: fileItem.lastModified || Date.now() + }); + // Mark as from storage to avoid re-storing + (file as any).storedInIndexedDB = true; + return file; + } + + console.warn('Could not convert file item:', fileItem); + return null; + }) + ); + + // Filter out nulls and add to activeFiles + const validFiles = convertedFiles.filter((f): f is File => f !== null); + setActiveFiles(validFiles); + setPreSelectedFiles([]); // Clear preselected since we're using activeFiles now + handleViewChange("fileEditor"); + } catch (error) { + console.error('Error converting selected files:', error); + } + }, [handleViewChange, setActiveFiles]); + + // Handle opening page editor with selected files + const handleOpenPageEditor = useCallback(async (selectedFiles) => { + if (!selectedFiles || selectedFiles.length === 0) { + handleViewChange("pageEditor"); + return; + } + + // Convert FileWithUrl[] to File[] and add to activeFiles + try { + const convertedFiles = await Promise.all( + selectedFiles.map(async (fileItem) => { + // If it's already a File, return as is + if (fileItem instanceof File) { + return fileItem; + } + + // If it has a file property, use that + if (fileItem.file && fileItem.file instanceof File) { + return fileItem.file; + } + + // If it's from IndexedDB storage, reconstruct the File + if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { + const arrayBuffer = await fileItem.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); + const file = new File([blob], fileItem.name, { + type: fileItem.type || 'application/pdf', + lastModified: fileItem.lastModified || Date.now() + }); + // Mark as from storage to avoid re-storing + (file as any).storedInIndexedDB = true; + return file; + } + + console.warn('Could not convert file item:', fileItem); + return null; + }) + ); + + // Filter out nulls and add to activeFiles + const validFiles = convertedFiles.filter((f): f is File => f !== null); + setActiveFiles(validFiles); + handleViewChange("pageEditor"); + } catch (error) { + console.error('Error converting selected files for page editor:', error); + } + }, [handleViewChange, setActiveFiles]); const selectedTool = toolRegistry[selectedToolKey]; - // Convert current active file to format expected by Viewer/PageEditor - const currentFileWithUrl = useFileWithUrl(activeFiles[0] || null); + // For Viewer - convert first active file to expected format (only when needed) + const currentFileWithUrl = useFileWithUrl( + (currentView === "viewer" && activeFiles[0]) ? activeFiles[0] : null + ); return ( ) : (currentView != "fileManager") && !activeFiles[0] ? ( @@ -309,8 +404,8 @@ export default function HomePage() { ) : currentView === "fileEditor" ? ( setPreSelectedFiles([])} onOpenPageEditor={(file) => { @@ -339,18 +434,12 @@ export default function HomePage() { ) : currentView === "pageEditor" ? ( <> { - if (fileObj) { - setCurrentActiveFile(fileObj.file); - } else { - setActiveFiles([]); - } - }} + activeFiles={activeFiles} + setActiveFiles={setActiveFiles} downloadUrl={downloadUrl} setDownloadUrl={setDownloadUrl} + sharedFiles={storedFiles} onFunctionsReady={setPageEditorFunctions} - sharedFiles={activeFiles} /> {activeFiles[0] && pageEditorFunctions && ( pageEditorFunctions.showExportPreview(true)} - onExportAll={() => pageEditorFunctions.showExportPreview(false)} + onExportSelected={pageEditorFunctions.onExportSelected} + onExportAll={pageEditorFunctions.onExportAll} exportLoading={pageEditorFunctions.exportLoading} selectionMode={pageEditorFunctions.selectionMode} selectedPages={pageEditorFunctions.selectedPages} @@ -376,6 +465,7 @@ export default function HomePage() { setFiles={setStoredFiles} setCurrentView={handleViewChange} onOpenFileEditor={handleOpenFileEditor} + onOpenPageEditor={handleOpenPageEditor} onLoadFileToActive={addToActiveFiles} /> )}