From ffe5b9577bdcdc24966901a691ec059117128013 Mon Sep 17 00:00:00 2001 From: Reece Date: Tue, 24 Jun 2025 23:31:21 +0100 Subject: [PATCH] Page editor fixes and improvements and restructuring --- .../BulkSelectionPanel.tsx | 0 .../shared => pageEditor}/DragDropGrid.tsx | 10 +- .../{editor => pageEditor}/FileEditor.tsx | 2 +- .../{editor => pageEditor}/FileThumbnail.tsx | 0 .../PageEditor.module.css | 0 .../{editor => pageEditor}/PageEditor.tsx | 402 ++++++++++-------- .../PageEditorControls.tsx | 0 .../{editor => pageEditor}/PageThumbnail.tsx | 4 +- .../src/components/shared/FilePickerModal.tsx | 57 +-- .../components/shared/FileUploadSelector.tsx | 75 ++-- frontend/src/pages/HomePage.tsx | 28 +- 11 files changed, 313 insertions(+), 265 deletions(-) rename frontend/src/components/{editor => pageEditor}/BulkSelectionPanel.tsx (100%) rename frontend/src/components/{editor/shared => pageEditor}/DragDropGrid.tsx (95%) rename frontend/src/components/{editor => pageEditor}/FileEditor.tsx (99%) rename frontend/src/components/{editor => pageEditor}/FileThumbnail.tsx (100%) rename frontend/src/components/{editor => pageEditor}/PageEditor.module.css (100%) rename frontend/src/components/{editor => pageEditor}/PageEditor.tsx (70%) rename frontend/src/components/{editor => pageEditor}/PageEditorControls.tsx (100%) rename frontend/src/components/{editor => pageEditor}/PageThumbnail.tsx (99%) 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 99% rename from frontend/src/components/editor/FileEditor.tsx rename to frontend/src/components/pageEditor/FileEditor.tsx index b60ccba81..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 { 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 70% rename from frontend/src/components/editor/PageEditor.tsx rename to frontend/src/components/pageEditor/PageEditor.tsx index 0adc4aba3..fc4f93e3d 100644 --- a/frontend/src/components/editor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -19,10 +19,10 @@ 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'; @@ -63,14 +63,9 @@ const PageEditor = ({ const { t } = useTranslation(); const { processPDFFile, loading: pdfLoading } = usePDFProcessor(); - // Multi-file state - const [currentFileIndex, setCurrentFileIndex] = useState(0); + // Single merged document state + const [mergedPdfDocument, setMergedPdfDocument] = useState(null); 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 @@ -80,18 +75,18 @@ const PageEditor = ({ const [error, setError] = useState(null); const [csvInput, setCsvInput] = useState(""); const [selectionMode, setSelectionMode] = useState(false); - + // 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()); @@ -140,8 +135,8 @@ const PageEditor = ({ } const fileKey = `${fileToProcess.name}-${fileToProcess.size}`; - - // Skip if already processed + + // Skip processing if already processed if (processedFiles.has(fileKey)) return; setLoading(true); @@ -149,16 +144,12 @@ const PageEditor = ({ try { const document = await processPDFFile(fileToProcess); - + // 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) @@ -167,7 +158,7 @@ const PageEditor = ({ await fileStorage.storeFile(fileToProcess, thumbnail); } } - + setStatus(`PDF loaded successfully with ${document.totalPages} pages`); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF'; @@ -178,36 +169,111 @@ const PageEditor = ({ } }, [processPDFFile, activeFiles, setActiveFiles, processedFiles]); + // 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; + } + + // 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]); - // Reset current file index when activeFiles changes + // Merge multiple PDF documents into one when all files are processed useEffect(() => { - if (currentFileIndex >= activeFiles.length) { - setCurrentFileIndex(0); - } - }, [activeFiles.length, currentFileIndex]); + if (activeFiles.length > 0) { + const allProcessed = activeFiles.every(file => { + const fileKey = `${file.name}-${file.size}`; + return processedFiles.has(fileKey); + }); - // Clear selections when switching files + if (allProcessed && activeFiles.length > 0) { + mergeAllPDFs(); + } + } + }, [activeFiles, processedFiles, mergeAllPDFs]); + + // Clear selections when files change useEffect(() => { setSelectedPages([]); setCsvInput(""); setSelectionMode(false); - }, [currentFileIndex]); - - // Update filename when current file changes - useEffect(() => { - if (currentFile) { - setFilename(currentFile.name.replace(/\.pdf$/i, '')); - } - }, [currentFile]); + }, [activeFiles]); // Global drag cleanup to handle drops outside valid areas useEffect(() => { @@ -236,10 +302,10 @@ const PageEditor = ({ }, [draggedPage]); const selectAll = useCallback(() => { - if (currentPdfDocument) { - setSelectedPages(currentPdfDocument.pages.map(p => p.id)); + if (mergedPdfDocument) { + setSelectedPages(mergedPdfDocument.pages.map(p => p.id)); } - }, [currentPdfDocument]); + }, [mergedPdfDocument]); const deselectAll = useCallback(() => setSelectedPages([]), []); @@ -264,7 +330,7 @@ const PageEditor = ({ }, []); const parseCSVInput = useCallback((csv: string) => { - if (!currentPdfDocument) return []; + if (!mergedPdfDocument) return []; const pageIds: string[] = []; const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); @@ -272,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 <= currentPdfDocument.totalPages; i++) { + for (let i = start; i <= end && i <= mergedPdfDocument.totalPages; i++) { if (i > 0) { - const page = currentPdfDocument.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 <= currentPdfDocument.totalPages) { - const page = currentPdfDocument.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; - }, [currentPdfDocument]); + }, [mergedPdfDocument]); const updatePagesFromCSV = useCallback(() => { const pageIds = parseCSVInput(csvInput); @@ -362,110 +428,127 @@ const PageEditor = ({ // Don't clear drop target on drag leave - let dragover handle it }, []); - // Create setPdfDocument wrapper for current file + // Create setPdfDocument wrapper for merged document const setPdfDocument = useCallback((updatedDoc: PDFDocument) => { - if (currentFileKey) { - setProcessedFiles(prev => new Map(prev).set(currentFileKey, updatedDoc)); - } - }, [currentFileKey]); + setMergedPdfDocument(updatedDoc); + // Return the updated document for immediate use in animations + return updatedDoc; + }, []); const animateReorder = useCallback((pageId: string, targetIndex: number) => { - if (!currentPdfDocument || 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 = currentPdfDocument.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(); - currentPdfDocument.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(currentPdfDocument, setPdfDocument, pagesToMove, targetIndex); + const command = new MovePagesCommand(mergedPdfDocument, setPdfDocument, pagesToMove, targetIndex); executeCommand(command); } else { // Single page move - const command = new ReorderPageCommand(currentPdfDocument, 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 - const currentDoc = currentPdfDocument; // 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); + }); }); - }); - }, [currentPdfDocument, isAnimating, executeCommand, selectionMode, selectedPages, setPdfDocument]); + }, 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 || !currentPdfDocument || draggedPage === targetPageId) return; + if (!draggedPage || !mergedPdfDocument || draggedPage === targetPageId) return; let targetIndex: number; if (targetPageId === 'end') { - targetIndex = currentPdfDocument.pages.length; + targetIndex = mergedPdfDocument.pages.length; } else { - targetIndex = currentPdfDocument.pages.findIndex(p => p.id === targetPageId); + targetIndex = mergedPdfDocument.pages.findIndex(p => p.id === targetPageId); if (targetIndex === -1) return; } @@ -478,7 +561,7 @@ const PageEditor = ({ const moveCount = multiPageDrag ? multiPageDrag.count : 1; setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`); - }, [draggedPage, currentPdfDocument, animateReorder, multiPageDrag]); + }, [draggedPage, mergedPdfDocument, animateReorder, multiPageDrag]); const handleEndZoneDragEnter = useCallback(() => { if (draggedPage) { @@ -487,38 +570,38 @@ const PageEditor = ({ }, [draggedPage]); const handleRotate = useCallback((direction: 'left' | 'right') => { - if (!currentPdfDocument) return; + if (!mergedPdfDocument) return; const rotation = direction === 'left' ? -90 : 90; const pagesToRotate = selectionMode ? selectedPages - : currentPdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new RotatePagesCommand( - currentPdfDocument, + mergedPdfDocument, setPdfDocument, pagesToRotate, rotation ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Rotated ${pageCount} pages ${direction}`); - }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleDelete = useCallback(() => { - if (!currentPdfDocument) return; + if (!mergedPdfDocument) return; const pagesToDelete = selectionMode ? selectedPages - : currentPdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new DeletePagesCommand( - currentPdfDocument, + mergedPdfDocument, setPdfDocument, pagesToDelete ); @@ -527,55 +610,55 @@ const PageEditor = ({ if (selectionMode) { setSelectedPages([]); } - const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Deleted ${pageCount} pages`); - }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const handleSplit = useCallback(() => { - if (!currentPdfDocument) return; + if (!mergedPdfDocument) return; const pagesToSplit = selectionMode ? selectedPages - : currentPdfDocument.pages.map(p => p.id); + : mergedPdfDocument.pages.map(p => p.id); if (selectionMode && selectedPages.length === 0) return; const command = new ToggleSplitCommand( - currentPdfDocument, + mergedPdfDocument, setPdfDocument, pagesToSplit ); executeCommand(command); - const pageCount = selectionMode ? selectedPages.length : currentPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length; setStatus(`Split markers toggled for ${pageCount} pages`); - }, [currentPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); + }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { - if (!currentPdfDocument) return; + if (!mergedPdfDocument) return; const exportPageIds = selectedOnly ? selectedPages : []; - const preview = pdfExportService.getExportInfo(currentPdfDocument, exportPageIds, selectedOnly); + const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); - }, [currentPdfDocument, selectedPages]); + }, [mergedPdfDocument, selectedPages]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { - if (!currentPdfDocument) return; + if (!mergedPdfDocument) return; setExportLoading(true); try { const exportPageIds = selectedOnly ? selectedPages : []; - const errors = pdfExportService.validateExport(currentPdfDocument, exportPageIds, selectedOnly); + const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { setError(errors.join(', ')); return; } - const hasSplitMarkers = currentPdfDocument.pages.some(page => page.splitBefore); + const hasSplitMarkers = mergedPdfDocument.pages.some(page => page.splitBefore); if (hasSplitMarkers) { - const result = await pdfExportService.exportPDF(currentPdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { selectedOnly, filename, splitDocuments: true @@ -589,7 +672,7 @@ const PageEditor = ({ setStatus(`Exported ${result.blobs.length} split documents`); } else { - const result = await pdfExportService.exportPDF(currentPdfDocument, exportPageIds, { + const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, { selectedOnly, filename }) as { blob: Blob; filename: string }; @@ -603,7 +686,7 @@ const PageEditor = ({ } finally { setExportLoading(false); } - }, [currentPdfDocument, selectedPages, filename]); + }, [mergedPdfDocument, selectedPages, filename]); const handleUndo = useCallback(() => { if (undo()) { @@ -618,9 +701,9 @@ const PageEditor = ({ }, [redo]); const closePdf = useCallback(() => { - setCurrentFileIndex(0); setActiveFiles([]); setProcessedFiles(new Map()); + setMergedPdfDocument(null); setSelectedPages([]); }, [setActiveFiles]); @@ -666,18 +749,17 @@ const PageEditor = ({ closePdf ]); - if (!currentPdfDocument) { + if (!mergedPdfDocument) { return ( @@ -690,36 +772,6 @@ const PageEditor = ({ - {/* File Switcher Tabs */} - {activeFiles.length > 1 && ( - - - {activeFiles.map((file, index) => { - const isActive = index === currentFileIndex; - - return ( - - ); - })} - - - )} @@ -762,7 +814,7 @@ const PageEditor = ({ )} )} @@ -849,7 +901,7 @@ const PageEditor = ({ {exportPreview.estimatedSize} - {currentPdfDocument && currentPdfDocument.pages.some(p => p.splitBefore) && ( + {mergedPdfDocument && mergedPdfDocument.pages.some(p => p.splitBefore) && ( This will create multiple PDF files based on split markers. @@ -867,7 +919,7 @@ const PageEditor = ({ loading={exportLoading} onClick={() => { setShowExportModal(false); - const selectedOnly = exportPreview.pageCount < (currentPdfDocument?.totalPages || 0); + const selectedOnly = exportPreview.pageCount < (mergedPdfDocument?.totalPages || 0); handleExport(selectedOnly); }} > @@ -911,4 +963,4 @@ const PageEditor = ({ ); }; -export default PageEditor; \ No newline at end of file +export default PageEditor; 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 99% rename from frontend/src/components/editor/PageThumbnail.tsx rename to frontend/src/components/pageEditor/PageThumbnail.tsx index 1dc70843d..53626fd05 100644 --- a/frontend/src/components/editor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -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 { @@ -355,4 +355,4 @@ const PageThumbnail = ({ ); }; -export default PageThumbnail; \ No newline at end of file +export default PageThumbnail; diff --git a/frontend/src/components/shared/FilePickerModal.tsx b/frontend/src/components/shared/FilePickerModal.tsx index cad7fd373..f489c5a11 100644 --- a/frontend/src/components/shared/FilePickerModal.tsx +++ b/frontend/src/components/shared/FilePickerModal.tsx @@ -21,7 +21,6 @@ interface FilePickerModalProps { onClose: () => void; storedFiles: any[]; // Files from storage (FileWithUrl format) onSelectFiles: (selectedFiles: File[]) => void; - allowMultiple?: boolean; } const FilePickerModal = ({ @@ -29,7 +28,6 @@ const FilePickerModal = ({ onClose, storedFiles, onSelectFiles, - allowMultiple = true, }: FilePickerModalProps) => { const { t } = useTranslation(); const [selectedFileIds, setSelectedFileIds] = useState([]); @@ -43,21 +41,14 @@ const FilePickerModal = ({ const toggleFileSelection = (fileId: string) => { setSelectedFileIds(prev => { - if (allowMultiple) { - return prev.includes(fileId) - ? prev.filter(id => id !== fileId) - : [...prev, fileId]; - } else { - // Single selection mode - return prev.includes(fileId) ? [] : [fileId]; - } + return prev.includes(fileId) + ? prev.filter(id => id !== fileId) + : [...prev, fileId]; }); }; const selectAll = () => { - if (allowMultiple) { - setSelectedFileIds(storedFiles.map(f => f.id || f.name)); - } + setSelectedFileIds(storedFiles.map(f => f.id || f.name)); }; const selectNone = () => { @@ -145,20 +136,18 @@ const FilePickerModal = ({ {storedFiles.length} {t("fileUpload.filesAvailable", "files available")} - {allowMultiple && selectedFileIds.length > 0 && ( + {selectedFileIds.length > 0 && ( <> • {selectedFileIds.length} selected )} - {allowMultiple && ( - - - - - )} + + + + {/* File grid */} @@ -186,21 +175,11 @@ const FilePickerModal = ({ onClick={() => toggleFileSelection(fileId)} > - {allowMultiple ? ( - toggleFileSelection(fileId)} - onClick={(e) => e.stopPropagation()} - /> - ) : ( - toggleFileSelection(fileId)} - onClick={(e) => e.stopPropagation()} - style={{ margin: '4px' }} - /> - )} + toggleFileSelection(fileId)} + onClick={(e) => e.stopPropagation()} + /> {/* Thumbnail */} void; - onFilesSelect?: (files: File[]) => void; - allowMultiple?: boolean; + onFileSelect?: (file: File) => void; + onFilesSelect: (files: File[]) => void; accept?: string[]; // Loading state @@ -30,39 +29,54 @@ const FileUploadSelector = ({ sharedFiles = [], onFileSelect, onFilesSelect, - allowMultiple = false, accept = ["application/pdf"], loading = false, disabled = false, }: FileUploadSelectorProps) => { const { t } = useTranslation(); const [showFilePickerModal, setShowFilePickerModal] = useState(false); + const fileInputRef = useRef(null); const handleFileUpload = useCallback((uploadedFiles: File[]) => { if (uploadedFiles.length === 0) return; - if (allowMultiple && onFilesSelect) { + if (onFilesSelect) { onFilesSelect(uploadedFiles); - } else { + } else if (onFileSelect) { onFileSelect(uploadedFiles[0]); } - }, [allowMultiple, onFileSelect, onFilesSelect]); + }, [onFileSelect, onFilesSelect]); + + const handleFileInputChange = useCallback((event: React.ChangeEvent) => { + const files = event.target.files; + if (files && files.length > 0) { + const fileArray = Array.from(files); + console.log('File input change:', fileArray.length, 'files'); + handleFileUpload(fileArray); + } + // Reset input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, [handleFileUpload]); + + const openFileDialog = useCallback(() => { + fileInputRef.current?.click(); + }, []); const handleStorageSelection = useCallback((selectedFiles: File[]) => { if (selectedFiles.length === 0) return; - if (allowMultiple && onFilesSelect) { + if (onFilesSelect) { onFilesSelect(selectedFiles); - } else { + } else if (onFileSelect) { onFileSelect(selectedFiles[0]); } - }, [allowMultiple, onFileSelect, onFilesSelect]); + }, [onFileSelect, onFilesSelect]); // Get default title and subtitle from translations if not provided - const displayTitle = title || t(allowMultiple ? "fileUpload.selectFiles" : "fileUpload.selectFile", - allowMultiple ? "Select files" : "Select a file"); - const displaySubtitle = subtitle || t(allowMultiple ? "fileUpload.chooseFromStorageMultiple" : "fileUpload.chooseFromStorage", - allowMultiple ? "Choose files from storage or upload new PDFs" : "Choose a file from storage or upload a new PDF"); + const displayTitle = title || t("fileUpload.selectFiles", "Select files"); + const displaySubtitle = subtitle || t("fileUpload.chooseFromStorageMultiple", "Choose files from storage or upload new PDFs"); return ( <> @@ -98,15 +112,15 @@ const FileUploadSelector = ({
- {t(allowMultiple ? "fileUpload.dropFilesHere" : "fileUpload.dropFileHere", - allowMultiple ? "Drop files here or click to upload" : "Drop file here or click to upload")} + {t("fileUpload.dropFilesHere", "Drop files here or click to upload")} {accept.includes('application/pdf') @@ -118,23 +132,27 @@ const FileUploadSelector = ({
) : ( - + - + + {/* Manual file input as backup */} + + )} @@ -145,7 +163,6 @@ const FileUploadSelector = ({ onClose={() => setShowFilePickerModal(false)} storedFiles={sharedFiles} onSelectFiles={handleStorageSelection} - allowMultiple={allowMultiple} /> ); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index fdc823532..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"; @@ -188,12 +188,12 @@ export default function HomePage() { 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(); @@ -206,12 +206,12 @@ export default function HomePage() { (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); @@ -222,7 +222,7 @@ export default function HomePage() { } }, [handleViewChange, setActiveFiles]); - // Handle opening page editor with selected files + // Handle opening page editor with selected files const handleOpenPageEditor = useCallback(async (selectedFiles) => { if (!selectedFiles || selectedFiles.length === 0) { handleViewChange("pageEditor"); @@ -237,12 +237,12 @@ export default function HomePage() { 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(); @@ -255,12 +255,12 @@ export default function HomePage() { (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); @@ -388,8 +388,8 @@ export default function HomePage() { ) : (currentView != "fileManager") && !activeFiles[0] ? (