diff --git a/frontend/src/components/fileManagement/FileManager.tsx b/frontend/src/components/fileManagement/FileManager.tsx index 370ea32ec..45bf95b5b 100644 --- a/frontend/src/components/fileManagement/FileManager.tsx +++ b/frontend/src/components/fileManagement/FileManager.tsx @@ -22,6 +22,7 @@ interface FileManagerProps { allowMultiple?: boolean; setCurrentView?: (view: string) => 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/editor/BulkSelectionPanel.tsx b/frontend/src/components/pageEditor/BulkSelectionPanel.tsx similarity index 100% rename from frontend/src/components/editor/BulkSelectionPanel.tsx rename to frontend/src/components/pageEditor/BulkSelectionPanel.tsx diff --git a/frontend/src/components/editor/shared/DragDropGrid.tsx b/frontend/src/components/pageEditor/DragDropGrid.tsx similarity index 95% rename from frontend/src/components/editor/shared/DragDropGrid.tsx rename to frontend/src/components/pageEditor/DragDropGrid.tsx index 30bfe26bd..18ccda8f9 100644 --- a/frontend/src/components/editor/shared/DragDropGrid.tsx +++ b/frontend/src/components/pageEditor/DragDropGrid.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { Box } from '@mantine/core'; -import styles from '../PageEditor.module.css'; +import styles from './PageEditor.module.css'; interface DragDropItem { id: string; @@ -84,7 +84,7 @@ const DragDropGrid = ({ {/* Split marker */} {renderSplitMarker && item.splitBefore && index > 0 && renderSplitMarker(item, index)} - + {/* Item */} {renderItem(item, index, itemRefs)} @@ -95,8 +95,8 @@ const DragDropGrid = ({
({ ); }; -export default DragDropGrid; \ No newline at end of file +export default DragDropGrid; diff --git a/frontend/src/components/editor/FileEditor.tsx b/frontend/src/components/pageEditor/FileEditor.tsx similarity index 87% rename from frontend/src/components/editor/FileEditor.tsx rename to frontend/src/components/pageEditor/FileEditor.tsx index bd23b2ea0..7224f6453 100644 --- a/frontend/src/components/editor/FileEditor.tsx +++ b/frontend/src/components/pageEditor/FileEditor.tsx @@ -11,7 +11,7 @@ import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; import styles from './PageEditor.module.css'; import FileThumbnail from './FileThumbnail'; import BulkSelectionPanel from './BulkSelectionPanel'; -import DragDropGrid from './shared/DragDropGrid'; +import DragDropGrid from './DragDropGrid'; import FilePickerModal from '../shared/FilePickerModal'; interface FileItem { @@ -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/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx similarity index 100% rename from frontend/src/components/editor/FileThumbnail.tsx rename to frontend/src/components/pageEditor/FileThumbnail.tsx diff --git a/frontend/src/components/editor/PageEditor.module.css b/frontend/src/components/pageEditor/PageEditor.module.css similarity index 100% rename from frontend/src/components/editor/PageEditor.module.css rename to frontend/src/components/pageEditor/PageEditor.module.css diff --git a/frontend/src/components/editor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx similarity index 63% rename from frontend/src/components/editor/PageEditor.tsx rename to frontend/src/components/pageEditor/PageEditor.tsx index 9a84ab6d4..fc4f93e3d 100644 --- a/frontend/src/components/editor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -19,21 +19,21 @@ import { ToggleSplitCommand } from "../../commands/pageCommands"; import { pdfExportService } from "../../services/pdfExportService"; -import styles from './PageEditor.module.css'; +import styles from './pageEditor.module.css'; import PageThumbnail from './PageThumbnail'; import BulkSelectionPanel from './BulkSelectionPanel'; -import DragDropGrid from './shared/DragDropGrid'; +import DragDropGrid from './DragDropGrid'; 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,41 @@ 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); + // Single merged document state + const [mergedPdfDocument, setMergedPdfDocument] = useState(null); + const [processedFiles, setProcessedFiles] = useState>(new Map()); + 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,15 +134,23 @@ const PageEditor = ({ return; } + const fileKey = `${fileToProcess.name}-${fileToProcess.size}`; + + // Skip processing 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([]); + if (document.pages.length > 0) { // Only store if it's a new file (not from storage) if (!uploadedFile.storedInIndexedDB) { @@ -139,11 +159,6 @@ const PageEditor = ({ } } - 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 +167,113 @@ const PageEditor = ({ } finally { setLoading(false); } - }, [processPDFFile, setFile]); + }, [processPDFFile, activeFiles, setActiveFiles, processedFiles]); - useEffect(() => { - if (file?.file && !pdfDocument) { - handleFileUpload(file.file); + // Process multiple uploaded files - just add them to activeFiles like FileManager does + const handleMultipleFileUpload = useCallback((uploadedFiles: File[]) => { + if (!uploadedFiles || uploadedFiles.length === 0) { + setError('No files provided'); + return; } - }, [file, pdfDocument, handleFileUpload]); + + // Simply set the activeFiles to the selected files (same as FileManager approach) + setActiveFiles(uploadedFiles); + }, []); + + // Merge multiple PDF documents into one + const mergeAllPDFs = useCallback(() => { + if (activeFiles.length === 0) { + setMergedPdfDocument(null); + return; + } + + if (activeFiles.length === 1) { + // Single file - use it directly + const fileKey = `${activeFiles[0].name}-${activeFiles[0].size}`; + const pdfDoc = processedFiles.get(fileKey); + if (pdfDoc) { + setMergedPdfDocument(pdfDoc); + setFilename(activeFiles[0].name.replace(/\.pdf$/i, '')); + } + } else { + // Multiple files - merge them + const allPages: PDFPage[] = []; + let totalPages = 0; + const filenames: string[] = []; + + activeFiles.forEach((file, fileIndex) => { + const fileKey = `${file.name}-${file.size}`; + const pdfDoc = processedFiles.get(fileKey); + if (pdfDoc) { + filenames.push(file.name.replace(/\.pdf$/i, '')); + pdfDoc.pages.forEach((page, pageIndex) => { + // Create new page with updated IDs and page numbers for merged document + const newPage: PDFPage = { + ...page, + id: `${fileIndex}-${page.id}`, // Unique ID across all files + pageNumber: totalPages + pageIndex + 1, + sourceFile: file.name // Track which file this page came from + }; + allPages.push(newPage); + }); + totalPages += pdfDoc.pages.length; + } + }); + + const mergedDocument: PDFDocument = { + pages: allPages, + totalPages: totalPages, + title: filenames.join(' + '), + metadata: { + title: filenames.join(' + '), + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + } + }; + + setMergedPdfDocument(mergedDocument); + setFilename(filenames.join('_')); + } + }, [activeFiles, processedFiles]); + + // Auto-process files from activeFiles + useEffect(() => { + console.log('Auto-processing effect triggered:', { + activeFilesCount: activeFiles.length, + processedFilesCount: processedFiles.size, + activeFileNames: activeFiles.map(f => f.name) + }); + + activeFiles.forEach(file => { + const fileKey = `${file.name}-${file.size}`; + console.log(`Checking file ${file.name}: processed =`, processedFiles.has(fileKey)); + if (!processedFiles.has(fileKey)) { + console.log('Processing file:', file.name); + handleFileUpload(file); + } + }); + }, [activeFiles, processedFiles, handleFileUpload]); + + // Merge multiple PDF documents into one when all files are processed + useEffect(() => { + if (activeFiles.length > 0) { + const allProcessed = activeFiles.every(file => { + const fileKey = `${file.name}-${file.size}`; + return processedFiles.has(fileKey); + }); + + if (allProcessed && activeFiles.length > 0) { + mergeAllPDFs(); + } + } + }, [activeFiles, processedFiles, mergeAllPDFs]); + + // Clear selections when files change + useEffect(() => { + setSelectedPages([]); + setCsvInput(""); + setSelectionMode(false); + }, [activeFiles]); // Global drag cleanup to handle drops outside valid areas useEffect(() => { @@ -187,10 +302,10 @@ const PageEditor = ({ }, [draggedPage]); const selectAll = useCallback(() => { - if (pdfDocument) { - setSelectedPages(pdfDocument.pages.map(p => p.id)); + if (mergedPdfDocument) { + setSelectedPages(mergedPdfDocument.pages.map(p => p.id)); } - }, [pdfDocument]); + }, [mergedPdfDocument]); const deselectAll = useCallback(() => setSelectedPages([]), []); @@ -215,7 +330,7 @@ const PageEditor = ({ }, []); const parseCSVInput = useCallback((csv: string) => { - if (!pdfDocument) return []; + if (!mergedPdfDocument) return []; const pageIds: string[] = []; const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); @@ -223,23 +338,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 <= mergedPdfDocument.totalPages; i++) { if (i > 0) { - const page = pdfDocument.pages.find(p => p.pageNumber === i); + const page = mergedPdfDocument.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 <= mergedPdfDocument.totalPages) { + const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); if (page) pageIds.push(page.id); } } }); return pageIds; - }, [pdfDocument]); + }, [mergedPdfDocument]); const updatePagesFromCSV = useCallback(() => { const pageIds = parseCSVInput(csvInput); @@ -313,104 +428,127 @@ const PageEditor = ({ // Don't clear drop target on drag leave - let dragover handle it }, []); + // Create setPdfDocument wrapper for merged document + const setPdfDocument = useCallback((updatedDoc: PDFDocument) => { + setMergedPdfDocument(updatedDoc); + // Return the updated document for immediate use in animations + return updatedDoc; + }, []); + const animateReorder = useCallback((pageId: string, targetIndex: number) => { - if (!pdfDocument || isAnimating) return; + if (!mergedPdfDocument || 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 = mergedPdfDocument.pages.findIndex(p => p.id === pageId); if (originalIndex === -1 || originalIndex === targetIndex) return; setIsAnimating(true); - // Get current positions of all pages + // Get current positions of all pages by querying DOM directly const currentPositions = new Map(); - pdfDocument.pages.forEach((page) => { - const element = pageRefs.current.get(page.id); - if (element) { + const allCurrentElements = Array.from(document.querySelectorAll('[data-page-id]')); + + + // Capture positions from actual DOM elements + allCurrentElements.forEach((element) => { + const pageId = element.getAttribute('data-page-id'); + if (pageId) { const rect = element.getBoundingClientRect(); - currentPositions.set(page.id, { x: rect.left, y: rect.top }); + currentPositions.set(pageId, { x: rect.left, y: rect.top }); } }); + // 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(mergedPdfDocument, setPdfDocument, pagesToMove, targetIndex); executeCommand(command); } else { // Single page move - const command = new ReorderPageCommand(pdfDocument, setPdfDocument, pageId, targetIndex); + const command = new ReorderPageCommand(mergedPdfDocument, setPdfDocument, pageId, targetIndex); executeCommand(command); } - // Wait for DOM to update, then get new positions and animate - requestAnimationFrame(() => { + // Wait for state update and DOM to update, then get new positions and animate + setTimeout(() => { requestAnimationFrame(() => { - const newPositions = new Map(); + requestAnimationFrame(() => { + 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 + // Re-get all page elements after state update + const allPageElements = Array.from(document.querySelectorAll('[data-page-id]')); - currentDoc.pages.forEach((page) => { - const element = pageRefs.current.get(page.id); - if (element) { - const rect = element.getBoundingClientRect(); - newPositions.set(page.id, { x: rect.left, y: rect.top }); - } - }); - - // Calculate and apply animations - currentDoc.pages.forEach((page) => { - const element = pageRefs.current.get(page.id); - const currentPos = currentPositions.get(page.id); - const newPos = newPositions.get(page.id); - - if (element && currentPos && newPos) { - const deltaX = currentPos.x - newPos.x; - const deltaY = currentPos.y - newPos.y; - - // Apply initial transform (from new position back to old position) - element.style.transform = `translate(${deltaX}px, ${deltaY}px)`; - element.style.transition = 'none'; - - // Force reflow - element.offsetHeight; - - // Animate to final position - element.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; - element.style.transform = 'translate(0px, 0px)'; - } - }); - - // Clean up after animation - setTimeout(() => { - currentDoc.pages.forEach((page) => { - const element = pageRefs.current.get(page.id); - if (element) { - element.style.transform = ''; - element.style.transition = ''; + allPageElements.forEach((element) => { + const pageId = element.getAttribute('data-page-id'); + if (pageId) { + const rect = element.getBoundingClientRect(); + newPositions.set(pageId, { x: rect.left, y: rect.top }); } }); - setIsAnimating(false); - }, 400); + + let animationCount = 0; + + // Calculate and apply animations using DOM elements directly + allPageElements.forEach((element) => { + const pageId = element.getAttribute('data-page-id'); + if (!pageId) return; + + const currentPos = currentPositions.get(pageId); + const newPos = newPositions.get(pageId); + + if (element && currentPos && newPos) { + const deltaX = currentPos.x - newPos.x; + const deltaY = currentPos.y - newPos.y; + + + if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { + animationCount++; + const htmlElement = element as HTMLElement; + // Apply initial transform (from new position back to old position) + htmlElement.style.transform = `translate(${deltaX}px, ${deltaY}px)`; + htmlElement.style.transition = 'none'; + + // Force reflow + htmlElement.offsetHeight; + + // Animate to final position + htmlElement.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; + htmlElement.style.transform = 'translate(0px, 0px)'; + } + } + }); + + + // Clean up after animation + setTimeout(() => { + const elementsToCleanup = Array.from(document.querySelectorAll('[data-page-id]')); + elementsToCleanup.forEach((element) => { + const htmlElement = element as HTMLElement; + htmlElement.style.transform = ''; + htmlElement.style.transition = ''; + }); + setIsAnimating(false); + }, 400); + }); }); - }); - }, [pdfDocument, isAnimating, executeCommand, selectionMode, selectedPages]); + }, 10); // Small delay to allow state update + }, [mergedPdfDocument, isAnimating, executeCommand, selectionMode, selectedPages, setPdfDocument]); const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => { e.preventDefault(); - if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return; + if (!draggedPage || !mergedPdfDocument || draggedPage === targetPageId) return; let targetIndex: number; if (targetPageId === 'end') { - targetIndex = pdfDocument.pages.length; + targetIndex = mergedPdfDocument.pages.length; } else { - targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId); + targetIndex = mergedPdfDocument.pages.findIndex(p => p.id === targetPageId); if (targetIndex === -1) return; } @@ -423,7 +561,7 @@ const PageEditor = ({ const moveCount = multiPageDrag ? multiPageDrag.count : 1; setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`); - }, [draggedPage, pdfDocument, animateReorder, multiPageDrag]); + }, [draggedPage, mergedPdfDocument, animateReorder, multiPageDrag]); const handleEndZoneDragEnter = useCallback(() => { if (draggedPage) { @@ -432,38 +570,38 @@ const PageEditor = ({ }, [draggedPage]); const handleRotate = useCallback((direction: 'left' | 'right') => { - if (!pdfDocument) return; + if (!mergedPdfDocument) return; const rotation = direction === 'left' ? -90 : 90; const pagesToRotate = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new RotatePagesCommand( - pdfDocument, + mergedPdfDocument, setPdfDocument, pagesToRotate, rotation ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Rotated ${pageCount} pages ${direction}`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleDelete = useCallback(() => { - if (!pdfDocument) return; + if (!mergedPdfDocument) return; const pagesToDelete = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new DeletePagesCommand( - pdfDocument, + mergedPdfDocument, setPdfDocument, pagesToDelete ); @@ -472,55 +610,55 @@ const PageEditor = ({ if (selectionMode) { setSelectedPages([]); } - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Deleted ${pageCount} pages`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleSplit = useCallback(() => { - if (!pdfDocument) return; + if (!mergedPdfDocument) return; const pagesToSplit = selectionMode ? selectedPages - : pdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new ToggleSplitCommand( - pdfDocument, + mergedPdfDocument, setPdfDocument, pagesToSplit ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Split markers toggled for ${pageCount} pages`); - }, [pdfDocument, selectedPages, selectionMode, executeCommand]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { - if (!pdfDocument) return; + if (!mergedPdfDocument) return; const exportPageIds = selectedOnly ? selectedPages : []; - const preview = pdfExportService.getExportInfo(pdfDocument, exportPageIds, selectedOnly); + const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); - }, [pdfDocument, selectedPages]); + }, [mergedPdfDocument, selectedPages]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { - if (!pdfDocument) return; + if (!mergedPdfDocument) return; setExportLoading(true); try { const exportPageIds = selectedOnly ? selectedPages : []; - const errors = pdfExportService.validateExport(pdfDocument, exportPageIds, selectedOnly); + const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { setError(errors.join(', ')); return; } - const hasSplitMarkers = pdfDocument.pages.some(page => page.splitBefore); + const hasSplitMarkers = mergedPdfDocument.pages.some(page => page.splitBefore); if (hasSplitMarkers) { - const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { selectedOnly, filename, splitDocuments: true @@ -534,7 +672,7 @@ const PageEditor = ({ setStatus(`Exported ${result.blobs.length} split documents`); } else { - const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { selectedOnly, filename }) as { blob: Blob; filename: string }; @@ -548,7 +686,7 @@ const PageEditor = ({ } finally { setExportLoading(false); } - }, [pdfDocument, selectedPages, filename]); + }, [mergedPdfDocument, selectedPages, filename]); const handleUndo = useCallback(() => { if (undo()) { @@ -563,11 +701,17 @@ const PageEditor = ({ }, [redo]); const closePdf = useCallback(() => { - setPdfDocument(null); - setFile && setFile(null); - }, [setFile]); + setActiveFiles([]); + setProcessedFiles(new Map()); + setMergedPdfDocument(null); + 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 +723,8 @@ const PageEditor = ({ handleDelete, handleSplit, showExportPreview, + onExportSelected, + onExportAll, exportLoading, selectionMode, selectedPages, @@ -595,24 +741,25 @@ const PageEditor = ({ handleDelete, handleSplit, showExportPreview, + onExportSelected, + onExportAll, exportLoading, selectionMode, selectedPages, closePdf ]); - if (!pdfDocument) { + if (!mergedPdfDocument) { return ( @@ -625,6 +772,7 @@ const PageEditor = ({ + )} @@ -753,7 +901,7 @@ const PageEditor = ({ {exportPreview.estimatedSize} - {pdfDocument && pdfDocument.pages.some(p => p.splitBefore) && ( + {mergedPdfDocument && mergedPdfDocument.pages.some(p => p.splitBefore) && ( This will create multiple PDF files based on split markers. @@ -771,7 +919,7 @@ const PageEditor = ({ loading={exportLoading} onClick={() => { setShowExportModal(false); - const selectedOnly = exportPreview.pageCount < (pdfDocument?.totalPages || 0); + const selectedOnly = exportPreview.pageCount < (mergedPdfDocument?.totalPages || 0); handleExport(selectedOnly); }} > @@ -800,6 +948,16 @@ const PageEditor = ({ )} + {error && ( + setError(null)} + style={{ position: 'fixed', bottom: 70, right: 20, zIndex: 1000 }} + > + {error} + + )} ); diff --git a/frontend/src/components/editor/PageEditorControls.tsx b/frontend/src/components/pageEditor/PageEditorControls.tsx similarity index 100% rename from frontend/src/components/editor/PageEditorControls.tsx rename to frontend/src/components/pageEditor/PageEditorControls.tsx diff --git a/frontend/src/components/editor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx similarity index 95% rename from frontend/src/components/editor/PageThumbnail.tsx rename to frontend/src/components/pageEditor/PageThumbnail.tsx index 89d0811b1..53626fd05 100644 --- a/frontend/src/components/editor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/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'; @@ -7,7 +7,7 @@ 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 } from '../../types/pageEditor'; +import { PDFPage } from '../../../types/pageEditor'; import styles from './PageEditor.module.css'; interface PageThumbnailProps { @@ -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; - sharedFiles: any[]; - onSelectFiles: (selectedFiles: any[]) => void; + storedFiles: any[]; // Files from storage (FileWithUrl format) + onSelectFiles: (selectedFiles: File[]) => void; } const FilePickerModal = ({ opened, onClose, - sharedFiles, + storedFiles, onSelectFiles, }: FilePickerModalProps) => { const { t } = useTranslation(); @@ -40,15 +40,15 @@ const FilePickerModal = ({ }, [opened]); const toggleFileSelection = (fileId: string) => { - setSelectedFileIds(prev => - prev.includes(fileId) + setSelectedFileIds(prev => { + return prev.includes(fileId) ? prev.filter(id => id !== fileId) - : [...prev, fileId] - ); + : [...prev, fileId]; + }); }; const selectAll = () => { - setSelectedFileIds(sharedFiles.map(f => f.id || f.name)); + setSelectedFileIds(storedFiles.map(f => f.id || f.name)); }; const selectNone = () => { @@ -56,56 +56,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 +126,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,7 +135,10 @@ const FilePickerModal = ({ {/* Selection controls */} - {sharedFiles.length} {t("fileUpload.filesAvailable", "files available")} + {storedFiles.length} {t("fileUpload.filesAvailable", "files available")} + {selectedFileIds.length > 0 && ( + <> • {selectedFileIds.length} selected + )} - + + {/* Manual file input as backup */} + + )} @@ -143,7 +161,7 @@ const FileUploadSelector = ({ setShowFilePickerModal(false)} - sharedFiles={sharedFiles} + storedFiles={sharedFiles} onSelectFiles={handleStorageSelection} /> 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..515676bd8 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -14,9 +14,9 @@ import rainbowStyles from '../styles/rainbow.module.css'; import ToolPicker from "../components/tools/ToolPicker"; import TopControls from "../components/shared/TopControls"; import FileManager from "../components/fileManagement/FileManager"; -import FileEditor from "../components/editor/FileEditor"; -import PageEditor from "../components/editor/PageEditor"; -import PageEditorControls from "../components/editor/PageEditorControls"; +import FileEditor from "../components/pageEditor/FileEditor"; +import PageEditor from "../components/pageEditor/PageEditor"; +import PageEditorControls from "../components/pageEditor/PageEditorControls"; import Viewer from "../components/viewer/Viewer"; import FileUploadSelector from "../components/shared/FileUploadSelector"; import SplitPdfPanel from "../tools/Split"; @@ -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] ? ( ) : 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} /> )}