mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-20 13:47:46 +02:00
Page editor fixes and improvements and restructuring
This commit is contained in:
parent
2f90220b7b
commit
ffe5b9577b
@ -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;
|
@ -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 {
|
@ -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<PDFDocument | null>(null);
|
||||
const [processedFiles, setProcessedFiles] = useState<Map<string, PDFDocument>>(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<string>("");
|
||||
|
||||
// Page editor state
|
||||
@ -141,7 +136,7 @@ const PageEditor = ({
|
||||
|
||||
const fileKey = `${fileToProcess.name}-${fileToProcess.size}`;
|
||||
|
||||
// Skip if already processed
|
||||
// Skip processing if already processed
|
||||
if (processedFiles.has(fileKey)) return;
|
||||
|
||||
setLoading(true);
|
||||
@ -155,10 +150,6 @@ const PageEditor = ({
|
||||
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)
|
||||
@ -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<string, { x: number; y: number }>();
|
||||
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<string, { x: number; y: number }>();
|
||||
requestAnimationFrame(() => {
|
||||
const newPositions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
// 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 (
|
||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FileUploadSelector
|
||||
title="Select a PDF to edit"
|
||||
subtitle="Choose a file from storage or upload a new PDF"
|
||||
title="Select PDFs to edit"
|
||||
subtitle="Choose files from storage or upload PDFs - multiple files will be merged"
|
||||
sharedFiles={sharedFiles}
|
||||
onFileSelect={handleFileUpload}
|
||||
allowMultiple={false}
|
||||
onFilesSelect={handleMultipleFileUpload}
|
||||
accept={["application/pdf"]}
|
||||
loading={loading || pdfLoading}
|
||||
/>
|
||||
@ -690,36 +772,6 @@ const PageEditor = ({
|
||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
{/* File Switcher Tabs */}
|
||||
{activeFiles.length > 1 && (
|
||||
<Box p="md" pb={0} style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}>
|
||||
<Group gap="xs">
|
||||
{activeFiles.map((file, index) => {
|
||||
const isActive = index === currentFileIndex;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={`${file.name}-${file.size}`}
|
||||
size="sm"
|
||||
variant={isActive ? "filled" : "light"}
|
||||
color={isActive ? "blue" : "gray"}
|
||||
onClick={() => setCurrentFileIndex(index)}
|
||||
styles={{
|
||||
root: {
|
||||
maxWidth: 200,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{file.name.replace('.pdf', '')}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box p="md" pt="xl">
|
||||
<Group mb="md">
|
||||
@ -762,7 +814,7 @@ const PageEditor = ({
|
||||
)}
|
||||
|
||||
<DragDropGrid
|
||||
items={currentPdfDocument.pages}
|
||||
items={mergedPdfDocument.pages}
|
||||
selectedItems={selectedPages}
|
||||
selectionMode={selectionMode}
|
||||
isAnimating={isAnimating}
|
||||
@ -781,7 +833,7 @@ const PageEditor = ({
|
||||
<PageThumbnail
|
||||
page={page}
|
||||
index={index}
|
||||
totalPages={currentPdfDocument.pages.length}
|
||||
totalPages={mergedPdfDocument.pages.length}
|
||||
selectedPages={selectedPages}
|
||||
selectionMode={selectionMode}
|
||||
draggedPage={draggedPage}
|
||||
@ -803,7 +855,7 @@ const PageEditor = ({
|
||||
RotatePagesCommand={RotatePagesCommand}
|
||||
DeletePagesCommand={DeletePagesCommand}
|
||||
ToggleSplitCommand={ToggleSplitCommand}
|
||||
pdfDocument={currentPdfDocument}
|
||||
pdfDocument={mergedPdfDocument}
|
||||
setPdfDocument={setPdfDocument}
|
||||
/>
|
||||
)}
|
||||
@ -849,7 +901,7 @@ const PageEditor = ({
|
||||
<Text fw={500}>{exportPreview.estimatedSize}</Text>
|
||||
</Group>
|
||||
|
||||
{currentPdfDocument && currentPdfDocument.pages.some(p => p.splitBefore) && (
|
||||
{mergedPdfDocument && mergedPdfDocument.pages.some(p => p.splitBefore) && (
|
||||
<Alert color="blue">
|
||||
This will create multiple PDF files based on split markers.
|
||||
</Alert>
|
||||
@ -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);
|
||||
}}
|
||||
>
|
@ -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 {
|
@ -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<string[]>([]);
|
||||
@ -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 = ({
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
{storedFiles.length} {t("fileUpload.filesAvailable", "files available")}
|
||||
{allowMultiple && selectedFileIds.length > 0 && (
|
||||
{selectedFileIds.length > 0 && (
|
||||
<> • {selectedFileIds.length} selected</>
|
||||
)}
|
||||
</Text>
|
||||
{allowMultiple && (
|
||||
<Group gap="xs">
|
||||
<Button size="xs" variant="light" onClick={selectAll}>
|
||||
{t("pageEdit.selectAll", "Select All")}
|
||||
</Button>
|
||||
<Button size="xs" variant="light" onClick={selectNone}>
|
||||
{t("pageEdit.deselectAll", "Select None")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
<Group gap="xs">
|
||||
<Button size="xs" variant="light" onClick={selectAll}>
|
||||
{t("pageEdit.selectAll", "Select All")}
|
||||
</Button>
|
||||
<Button size="xs" variant="light" onClick={selectNone}>
|
||||
{t("pageEdit.deselectAll", "Select None")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* File grid */}
|
||||
@ -186,21 +175,11 @@ const FilePickerModal = ({
|
||||
onClick={() => toggleFileSelection(fileId)}
|
||||
>
|
||||
<Group gap="sm" align="flex-start">
|
||||
{allowMultiple ? (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelection(fileId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="radio"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelection(fileId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ margin: '4px' }}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelection(fileId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
{/* Thumbnail */}
|
||||
<Box
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { Stack, Button, Text, Center } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
@ -13,9 +13,8 @@ interface FileUploadSelectorProps {
|
||||
|
||||
// File handling
|
||||
sharedFiles?: any[];
|
||||
onFileSelect: (file: File) => 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<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||
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 = ({
|
||||
<Dropzone
|
||||
onDrop={handleFileUpload}
|
||||
accept={accept}
|
||||
multiple={allowMultiple}
|
||||
multiple={true}
|
||||
disabled={disabled || loading}
|
||||
style={{ width: '100%', minHeight: 120 }}
|
||||
activateOnClick={true}
|
||||
>
|
||||
<Center>
|
||||
<Stack align="center" gap="sm">
|
||||
<Text size="md" fw={500}>
|
||||
{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")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{accept.includes('application/pdf')
|
||||
@ -118,23 +132,27 @@ const FileUploadSelector = ({
|
||||
</Center>
|
||||
</Dropzone>
|
||||
) : (
|
||||
<Dropzone
|
||||
onDrop={handleFileUpload}
|
||||
accept={accept}
|
||||
multiple={allowMultiple}
|
||||
disabled={disabled || loading}
|
||||
style={{ display: 'contents' }}
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={openFileDialog}
|
||||
>
|
||||
{t(allowMultiple ? "fileUpload.uploadFiles" : "fileUpload.uploadFile",
|
||||
allowMultiple ? "Upload Files" : "Upload File")}
|
||||
{t("fileUpload.uploadFiles", "Upload Files")}
|
||||
</Button>
|
||||
</Dropzone>
|
||||
|
||||
{/* Manual file input as backup */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple={true}
|
||||
accept={accept.join(',')}
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
@ -145,7 +163,6 @@ const FileUploadSelector = ({
|
||||
onClose={() => setShowFilePickerModal(false)}
|
||||
storedFiles={sharedFiles}
|
||||
onSelectFiles={handleStorageSelection}
|
||||
allowMultiple={allowMultiple}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user