Merge pull request #3811 from reecebrowne/Stirling-2.0

Stirling 2.0
This commit is contained in:
ConnorYoh 2025-06-25 16:07:22 +01:00 committed by GitHub
commit 29916d85b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 616 additions and 265 deletions

View File

@ -22,6 +22,7 @@ interface FileManagerProps {
allowMultiple?: boolean; allowMultiple?: boolean;
setCurrentView?: (view: string) => void; setCurrentView?: (view: string) => void;
onOpenFileEditor?: (selectedFiles?: FileWithUrl[]) => void; onOpenFileEditor?: (selectedFiles?: FileWithUrl[]) => void;
onOpenPageEditor?: (selectedFiles?: FileWithUrl[]) => void;
onLoadFileToActive?: (file: File) => void; onLoadFileToActive?: (file: File) => void;
} }
@ -31,6 +32,7 @@ const FileManager = ({
allowMultiple = true, allowMultiple = true,
setCurrentView, setCurrentView,
onOpenFileEditor, onOpenFileEditor,
onOpenPageEditor,
onLoadFileToActive, onLoadFileToActive,
}: FileManagerProps) => { }: FileManagerProps) => {
const { t } = useTranslation(); 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 ( return (
<div style={{ <div style={{
width: "100%", width: "100%",
@ -379,6 +388,14 @@ const FileManager = ({
> >
{t("fileManager.openInFileEditor", "Open in File Editor")} {t("fileManager.openInFileEditor", "Open in File Editor")}
</Button> </Button>
<Button
size="xs"
color="blue"
onClick={handleOpenSelectedInPageEditor}
disabled={selectedFiles.length === 0}
>
{t("fileManager.openInPageEditor", "Open in Page Editor")}
</Button>
</Group> </Group>
</Group> </Group>
</Box> </Box>

View File

@ -1,6 +1,6 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'; import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
import styles from '../PageEditor.module.css'; import styles from './PageEditor.module.css';
interface DragDropItem { interface DragDropItem {
id: string; id: string;

View File

@ -11,7 +11,7 @@ import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
import styles from './PageEditor.module.css'; import styles from './PageEditor.module.css';
import FileThumbnail from './FileThumbnail'; import FileThumbnail from './FileThumbnail';
import BulkSelectionPanel from './BulkSelectionPanel'; import BulkSelectionPanel from './BulkSelectionPanel';
import DragDropGrid from './shared/DragDropGrid'; import DragDropGrid from './DragDropGrid';
import FilePickerModal from '../shared/FilePickerModal'; import FilePickerModal from '../shared/FilePickerModal';
interface FileItem { interface FileItem {
@ -27,8 +27,8 @@ interface FileItem {
interface FileEditorProps { interface FileEditorProps {
onOpenPageEditor?: (file: File) => void; onOpenPageEditor?: (file: File) => void;
onMergeFiles?: (files: File[]) => void; onMergeFiles?: (files: File[]) => void;
sharedFiles?: { file: File; url: string }[]; activeFiles?: File[];
setSharedFiles?: (files: { file: File; url: string }[]) => void; setActiveFiles?: (files: File[]) => void;
preSelectedFiles?: { file: File; url: string }[]; preSelectedFiles?: { file: File; url: string }[];
onClearPreSelection?: () => void; onClearPreSelection?: () => void;
} }
@ -36,15 +36,14 @@ interface FileEditorProps {
const FileEditor = ({ const FileEditor = ({
onOpenPageEditor, onOpenPageEditor,
onMergeFiles, onMergeFiles,
sharedFiles = [], activeFiles = [],
setSharedFiles, setActiveFiles,
preSelectedFiles = [], preSelectedFiles = [],
onClearPreSelection onClearPreSelection
}: FileEditorProps) => { }: FileEditorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const files = sharedFiles; // Use sharedFiles as the source of truth const [files, setFiles] = useState<FileItem[]>([]);
const [selectedFiles, setSelectedFiles] = useState<string[]>([]); const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [status, setStatus] = useState<string | null>(null); const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(false); 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) // Only load shared files when explicitly passed (not on mount)
useEffect(() => { useEffect(() => {
const loadSharedFiles = async () => { const loadSharedFiles = async () => {
@ -84,7 +116,10 @@ const FileEditor = ({
const convertedFiles = await Promise.all( const convertedFiles = await Promise.all(
preSelectedFiles.map(convertToFileItem) preSelectedFiles.map(convertToFileItem)
); );
setFiles(convertedFiles); if (setActiveFiles) {
const updatedActiveFiles = convertedFiles.map(fileItem => fileItem.file);
setActiveFiles(updatedActiveFiles);
}
} catch (err) { } catch (err) {
console.error('Error converting pre-selected files:', err); console.error('Error converting pre-selected files:', err);
} finally { } finally {
@ -137,8 +172,8 @@ const FileEditor = ({
await fileStorage.storeFile(file, thumbnail); await fileStorage.storeFile(file, thumbnail);
} }
if (setSharedFiles) { if (setActiveFiles) {
setSharedFiles(prev => [...prev, ...newFiles]); setActiveFiles(prev => [...prev, ...newFiles.map(f => f.file)]);
} }
setStatus(`Added ${newFiles.length} files`); setStatus(`Added ${newFiles.length} files`);
@ -149,7 +184,7 @@ const FileEditor = ({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [setSharedFiles]); }, [setActiveFiles]);
const selectAll = useCallback(() => { const selectAll = useCallback(() => {
setSelectedFiles(files.map(f => f.id)); setSelectedFiles(files.map(f => f.id));
@ -283,8 +318,9 @@ const FileEditor = ({
? selectedFiles ? selectedFiles
: [draggedFile]; : [draggedFile];
if (setSharedFiles) { if (setActiveFiles) {
setSharedFiles(prev => { // Update the local files state and sync with activeFiles
setFiles(prev => {
const newFiles = [...prev]; const newFiles = [...prev];
const movedFiles = filesToMove.map(id => newFiles.find(f => f.id === id)!).filter(Boolean); const movedFiles = filesToMove.map(id => newFiles.find(f => f.id === id)!).filter(Boolean);
@ -296,6 +332,10 @@ const FileEditor = ({
// Insert at target position // Insert at target position
newFiles.splice(targetIndex, 0, ...movedFiles); newFiles.splice(targetIndex, 0, ...movedFiles);
// Update activeFiles with the reordered File objects
setActiveFiles(newFiles.map(f => f.file));
return newFiles; return newFiles;
}); });
} }
@ -304,7 +344,7 @@ const FileEditor = ({
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
handleDragEnd(); handleDragEnd();
}, [draggedFile, files, selectionMode, selectedFiles, multiFileDrag, handleDragEnd, setSharedFiles]); }, [draggedFile, files, selectionMode, selectedFiles, multiFileDrag, handleDragEnd, setActiveFiles]);
const handleEndZoneDragEnter = useCallback(() => { const handleEndZoneDragEnter = useCallback(() => {
if (draggedFile) { if (draggedFile) {
@ -314,11 +354,16 @@ const FileEditor = ({
// File operations // File operations
const handleDeleteFile = useCallback((fileId: string) => { const handleDeleteFile = useCallback((fileId: string) => {
if (setSharedFiles) { if (setActiveFiles) {
setSharedFiles(prev => prev.filter(f => f.id !== fileId)); // 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)); setSelectedFiles(prev => prev.filter(id => id !== fileId));
}, [setSharedFiles]); }, [setActiveFiles]);
const handleViewFile = useCallback((fileId: string) => { const handleViewFile = useCallback((fileId: string) => {
const file = files.find(f => f.id === fileId); const file = files.find(f => f.id === fileId);
@ -483,8 +528,9 @@ const FileEditor = ({
<FilePickerModal <FilePickerModal
opened={showFilePickerModal} opened={showFilePickerModal}
onClose={() => setShowFilePickerModal(false)} onClose={() => setShowFilePickerModal(false)}
sharedFiles={sharedFiles || []} storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent
onSelectFiles={handleLoadFromStorage} onSelectFiles={handleLoadFromStorage}
allowMultiple={true}
/> />
{status && ( {status && (

View File

@ -19,21 +19,21 @@ import {
ToggleSplitCommand ToggleSplitCommand
} from "../../commands/pageCommands"; } from "../../commands/pageCommands";
import { pdfExportService } from "../../services/pdfExportService"; import { pdfExportService } from "../../services/pdfExportService";
import styles from './PageEditor.module.css'; import styles from './pageEditor.module.css';
import PageThumbnail from './PageThumbnail'; import PageThumbnail from './PageThumbnail';
import BulkSelectionPanel from './BulkSelectionPanel'; import BulkSelectionPanel from './BulkSelectionPanel';
import DragDropGrid from './shared/DragDropGrid'; import DragDropGrid from './DragDropGrid';
import FilePickerModal from '../shared/FilePickerModal'; import FilePickerModal from '../shared/FilePickerModal';
import FileUploadSelector from '../shared/FileUploadSelector'; import FileUploadSelector from '../shared/FileUploadSelector';
export interface PageEditorProps { export interface PageEditorProps {
file: { file: File; url: string } | null; activeFiles: File[];
setFile?: (file: { file: File; url: string } | null) => void; setActiveFiles: (files: File[]) => void;
downloadUrl?: string | null; downloadUrl?: string | null;
setDownloadUrl?: (url: string | null) => void; 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: { onFunctionsReady?: (functions: {
handleUndo: () => void; handleUndo: () => void;
handleRedo: () => void; handleRedo: () => void;
@ -43,6 +43,8 @@ export interface PageEditorProps {
handleDelete: () => void; handleDelete: () => void;
handleSplit: () => void; handleSplit: () => void;
showExportPreview: (selectedOnly: boolean) => void; showExportPreview: (selectedOnly: boolean) => void;
onExportSelected: () => void;
onExportAll: () => void;
exportLoading: boolean; exportLoading: boolean;
selectionMode: boolean; selectionMode: boolean;
selectedPages: string[]; selectedPages: string[];
@ -51,31 +53,41 @@ export interface PageEditorProps {
} }
const PageEditor = ({ const PageEditor = ({
file, activeFiles,
setFile, setActiveFiles,
downloadUrl, downloadUrl,
setDownloadUrl, setDownloadUrl,
sharedFiles = [],
onFunctionsReady, onFunctionsReady,
sharedFiles,
}: PageEditorProps) => { }: PageEditorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { processPDFFile, loading: pdfLoading } = usePDFProcessor(); const { processPDFFile, loading: pdfLoading } = usePDFProcessor();
const [pdfDocument, setPdfDocument] = useState<PDFDocument | null>(null); // Single merged document state
const [mergedPdfDocument, setMergedPdfDocument] = useState<PDFDocument | null>(null);
const [processedFiles, setProcessedFiles] = useState<Map<string, PDFDocument>>(new Map());
const [filename, setFilename] = useState<string>("");
// Page editor state
const [selectedPages, setSelectedPages] = useState<string[]>([]); const [selectedPages, setSelectedPages] = useState<string[]>([]);
const [status, setStatus] = useState<string | null>(null); const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [csvInput, setCsvInput] = useState<string>(""); const [csvInput, setCsvInput] = useState<string>("");
const [selectionMode, setSelectionMode] = useState(false); const [selectionMode, setSelectionMode] = useState(false);
const [filename, setFilename] = useState<string>("");
// Drag and drop state
const [draggedPage, setDraggedPage] = useState<string | null>(null); const [draggedPage, setDraggedPage] = useState<string | null>(null);
const [dropTarget, setDropTarget] = useState<string | null>(null); const [dropTarget, setDropTarget] = useState<string | null>(null);
const [multiPageDrag, setMultiPageDrag] = useState<{pageIds: string[], count: number} | null>(null); const [multiPageDrag, setMultiPageDrag] = useState<{pageIds: string[], count: number} | null>(null);
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null); const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
// Export state
const [exportLoading, setExportLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false);
const [showExportModal, setShowExportModal] = useState(false); const [showExportModal, setShowExportModal] = useState(false);
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
// Animation state
const [movingPage, setMovingPage] = useState<string | null>(null); const [movingPage, setMovingPage] = useState<string | null>(null);
const [pagePositions, setPagePositions] = useState<Map<string, { x: number; y: number }>>(new Map()); const [pagePositions, setPagePositions] = useState<Map<string, { x: number; y: number }>>(new Map());
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
@ -122,15 +134,23 @@ const PageEditor = ({
return; return;
} }
const fileKey = `${fileToProcess.name}-${fileToProcess.size}`;
// Skip processing if already processed
if (processedFiles.has(fileKey)) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const document = await processPDFFile(fileToProcess); const document = await processPDFFile(fileToProcess);
setPdfDocument(document);
// Store processed document
setProcessedFiles(prev => new Map(prev).set(fileKey, document));
setFilename(fileToProcess.name.replace(/\.pdf$/i, '')); setFilename(fileToProcess.name.replace(/\.pdf$/i, ''));
setSelectedPages([]); setSelectedPages([]);
if (document.pages.length > 0) { if (document.pages.length > 0) {
// Only store if it's a new file (not from storage) // Only store if it's a new file (not from storage)
if (!uploadedFile.storedInIndexedDB) { 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`); setStatus(`PDF loaded successfully with ${document.totalPages} pages`);
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF'; const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF';
@ -152,13 +167,113 @@ const PageEditor = ({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [processPDFFile, setFile]); }, [processPDFFile, activeFiles, setActiveFiles, processedFiles]);
useEffect(() => { // Process multiple uploaded files - just add them to activeFiles like FileManager does
if (file?.file && !pdfDocument) { const handleMultipleFileUpload = useCallback((uploadedFiles: File[]) => {
handleFileUpload(file.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 // Global drag cleanup to handle drops outside valid areas
useEffect(() => { useEffect(() => {
@ -187,10 +302,10 @@ const PageEditor = ({
}, [draggedPage]); }, [draggedPage]);
const selectAll = useCallback(() => { const selectAll = useCallback(() => {
if (pdfDocument) { if (mergedPdfDocument) {
setSelectedPages(pdfDocument.pages.map(p => p.id)); setSelectedPages(mergedPdfDocument.pages.map(p => p.id));
} }
}, [pdfDocument]); }, [mergedPdfDocument]);
const deselectAll = useCallback(() => setSelectedPages([]), []); const deselectAll = useCallback(() => setSelectedPages([]), []);
@ -215,7 +330,7 @@ const PageEditor = ({
}, []); }, []);
const parseCSVInput = useCallback((csv: string) => { const parseCSVInput = useCallback((csv: string) => {
if (!pdfDocument) return []; if (!mergedPdfDocument) return [];
const pageIds: string[] = []; const pageIds: string[] = [];
const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); const ranges = csv.split(',').map(s => s.trim()).filter(Boolean);
@ -223,23 +338,23 @@ const PageEditor = ({
ranges.forEach(range => { ranges.forEach(range => {
if (range.includes('-')) { if (range.includes('-')) {
const [start, end] = range.split('-').map(n => parseInt(n.trim())); 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) { 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); if (page) pageIds.push(page.id);
} }
} }
} else { } else {
const pageNum = parseInt(range); const pageNum = parseInt(range);
if (pageNum > 0 && pageNum <= pdfDocument.totalPages) { if (pageNum > 0 && pageNum <= mergedPdfDocument.totalPages) {
const page = pdfDocument.pages.find(p => p.pageNumber === pageNum); const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
if (page) pageIds.push(page.id); if (page) pageIds.push(page.id);
} }
} }
}); });
return pageIds; return pageIds;
}, [pdfDocument]); }, [mergedPdfDocument]);
const updatePagesFromCSV = useCallback(() => { const updatePagesFromCSV = useCallback(() => {
const pageIds = parseCSVInput(csvInput); const pageIds = parseCSVInput(csvInput);
@ -313,104 +428,127 @@ const PageEditor = ({
// Don't clear drop target on drag leave - let dragover handle it // 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) => { 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 // In selection mode, if the dragged page is selected, move all selected pages
const pagesToMove = selectionMode && selectedPages.includes(pageId) const pagesToMove = selectionMode && selectedPages.includes(pageId)
? selectedPages ? selectedPages
: [pageId]; : [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; if (originalIndex === -1 || originalIndex === targetIndex) return;
setIsAnimating(true); 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 }>(); const currentPositions = new Map<string, { x: number; y: number }>();
pdfDocument.pages.forEach((page) => { const allCurrentElements = Array.from(document.querySelectorAll('[data-page-id]'));
const element = pageRefs.current.get(page.id);
if (element) {
// Capture positions from actual DOM elements
allCurrentElements.forEach((element) => {
const pageId = element.getAttribute('data-page-id');
if (pageId) {
const rect = element.getBoundingClientRect(); 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 // Execute the reorder - for multi-page, we use a different command
if (pagesToMove.length > 1) { if (pagesToMove.length > 1) {
// Multi-page move - use MovePagesCommand // Multi-page move - use MovePagesCommand
const command = new MovePagesCommand(pdfDocument, setPdfDocument, pagesToMove, targetIndex); const command = new MovePagesCommand(mergedPdfDocument, setPdfDocument, pagesToMove, targetIndex);
executeCommand(command); executeCommand(command);
} else { } else {
// Single page move // Single page move
const command = new ReorderPageCommand(pdfDocument, setPdfDocument, pageId, targetIndex); const command = new ReorderPageCommand(mergedPdfDocument, setPdfDocument, pageId, targetIndex);
executeCommand(command); executeCommand(command);
} }
// Wait for DOM to update, then get new positions and animate // Wait for state update and DOM to update, then get new positions and animate
setTimeout(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const newPositions = new Map<string, { x: number; y: number }>(); const newPositions = new Map<string, { x: number; y: number }>();
// Get the updated document from the state after command execution // Re-get all page elements after state update
// The command has already updated the document, so we need to get the new order const allPageElements = Array.from(document.querySelectorAll('[data-page-id]'));
const currentDoc = pdfDocument; // This should be the updated version after command
currentDoc.pages.forEach((page) => { allPageElements.forEach((element) => {
const element = pageRefs.current.get(page.id); const pageId = element.getAttribute('data-page-id');
if (element) { if (pageId) {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
newPositions.set(page.id, { x: rect.left, y: rect.top }); newPositions.set(pageId, { x: rect.left, y: rect.top });
} }
}); });
// Calculate and apply animations let animationCount = 0;
currentDoc.pages.forEach((page) => {
const element = pageRefs.current.get(page.id); // Calculate and apply animations using DOM elements directly
const currentPos = currentPositions.get(page.id); allPageElements.forEach((element) => {
const newPos = newPositions.get(page.id); const pageId = element.getAttribute('data-page-id');
if (!pageId) return;
const currentPos = currentPositions.get(pageId);
const newPos = newPositions.get(pageId);
if (element && currentPos && newPos) { if (element && currentPos && newPos) {
const deltaX = currentPos.x - newPos.x; const deltaX = currentPos.x - newPos.x;
const deltaY = currentPos.y - newPos.y; 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) // Apply initial transform (from new position back to old position)
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`; htmlElement.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
element.style.transition = 'none'; htmlElement.style.transition = 'none';
// Force reflow // Force reflow
element.offsetHeight; htmlElement.offsetHeight;
// Animate to final position // Animate to final position
element.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; htmlElement.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
element.style.transform = 'translate(0px, 0px)'; htmlElement.style.transform = 'translate(0px, 0px)';
}
} }
}); });
// Clean up after animation // Clean up after animation
setTimeout(() => { setTimeout(() => {
currentDoc.pages.forEach((page) => { const elementsToCleanup = Array.from(document.querySelectorAll('[data-page-id]'));
const element = pageRefs.current.get(page.id); elementsToCleanup.forEach((element) => {
if (element) { const htmlElement = element as HTMLElement;
element.style.transform = ''; htmlElement.style.transform = '';
element.style.transition = ''; htmlElement.style.transition = '';
}
}); });
setIsAnimating(false); setIsAnimating(false);
}, 400); }, 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') => { const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => {
e.preventDefault(); e.preventDefault();
if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return; if (!draggedPage || !mergedPdfDocument || draggedPage === targetPageId) return;
let targetIndex: number; let targetIndex: number;
if (targetPageId === 'end') { if (targetPageId === 'end') {
targetIndex = pdfDocument.pages.length; targetIndex = mergedPdfDocument.pages.length;
} else { } else {
targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId); targetIndex = mergedPdfDocument.pages.findIndex(p => p.id === targetPageId);
if (targetIndex === -1) return; if (targetIndex === -1) return;
} }
@ -423,7 +561,7 @@ const PageEditor = ({
const moveCount = multiPageDrag ? multiPageDrag.count : 1; const moveCount = multiPageDrag ? multiPageDrag.count : 1;
setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`); setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`);
}, [draggedPage, pdfDocument, animateReorder, multiPageDrag]); }, [draggedPage, mergedPdfDocument, animateReorder, multiPageDrag]);
const handleEndZoneDragEnter = useCallback(() => { const handleEndZoneDragEnter = useCallback(() => {
if (draggedPage) { if (draggedPage) {
@ -432,38 +570,38 @@ const PageEditor = ({
}, [draggedPage]); }, [draggedPage]);
const handleRotate = useCallback((direction: 'left' | 'right') => { const handleRotate = useCallback((direction: 'left' | 'right') => {
if (!pdfDocument) return; if (!mergedPdfDocument) return;
const rotation = direction === 'left' ? -90 : 90; const rotation = direction === 'left' ? -90 : 90;
const pagesToRotate = selectionMode const pagesToRotate = selectionMode
? selectedPages ? selectedPages
: pdfDocument.pages.map(p => p.id); : mergedPdfDocument.pages.map(p => p.id);
if (selectionMode && selectedPages.length === 0) return; if (selectionMode && selectedPages.length === 0) return;
const command = new RotatePagesCommand( const command = new RotatePagesCommand(
pdfDocument, mergedPdfDocument,
setPdfDocument, setPdfDocument,
pagesToRotate, pagesToRotate,
rotation rotation
); );
executeCommand(command); executeCommand(command);
const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length;
setStatus(`Rotated ${pageCount} pages ${direction}`); setStatus(`Rotated ${pageCount} pages ${direction}`);
}, [pdfDocument, selectedPages, selectionMode, executeCommand]); }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
if (!pdfDocument) return; if (!mergedPdfDocument) return;
const pagesToDelete = selectionMode const pagesToDelete = selectionMode
? selectedPages ? selectedPages
: pdfDocument.pages.map(p => p.id); : mergedPdfDocument.pages.map(p => p.id);
if (selectionMode && selectedPages.length === 0) return; if (selectionMode && selectedPages.length === 0) return;
const command = new DeletePagesCommand( const command = new DeletePagesCommand(
pdfDocument, mergedPdfDocument,
setPdfDocument, setPdfDocument,
pagesToDelete pagesToDelete
); );
@ -472,55 +610,55 @@ const PageEditor = ({
if (selectionMode) { if (selectionMode) {
setSelectedPages([]); setSelectedPages([]);
} }
const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length; const pageCount = selectionMode ? selectedPages.length : mergedPdfDocument.pages.length;
setStatus(`Deleted ${pageCount} pages`); setStatus(`Deleted ${pageCount} pages`);
}, [pdfDocument, selectedPages, selectionMode, executeCommand]); }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]);
const handleSplit = useCallback(() => { const handleSplit = useCallback(() => {
if (!pdfDocument) return; if (!mergedPdfDocument) return;
const pagesToSplit = selectionMode const pagesToSplit = selectionMode
? selectedPages ? selectedPages
: pdfDocument.pages.map(p => p.id); : mergedPdfDocument.pages.map(p => p.id);
if (selectionMode && selectedPages.length === 0) return; if (selectionMode && selectedPages.length === 0) return;
const command = new ToggleSplitCommand( const command = new ToggleSplitCommand(
pdfDocument, mergedPdfDocument,
setPdfDocument, setPdfDocument,
pagesToSplit pagesToSplit
); );
executeCommand(command); 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`); setStatus(`Split markers toggled for ${pageCount} pages`);
}, [pdfDocument, selectedPages, selectionMode, executeCommand]); }, [mergedPdfDocument, selectedPages, selectionMode, executeCommand, setPdfDocument]);
const showExportPreview = useCallback((selectedOnly: boolean = false) => { const showExportPreview = useCallback((selectedOnly: boolean = false) => {
if (!pdfDocument) return; if (!mergedPdfDocument) return;
const exportPageIds = selectedOnly ? selectedPages : []; const exportPageIds = selectedOnly ? selectedPages : [];
const preview = pdfExportService.getExportInfo(pdfDocument, exportPageIds, selectedOnly); const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly);
setExportPreview(preview); setExportPreview(preview);
setShowExportModal(true); setShowExportModal(true);
}, [pdfDocument, selectedPages]); }, [mergedPdfDocument, selectedPages]);
const handleExport = useCallback(async (selectedOnly: boolean = false) => { const handleExport = useCallback(async (selectedOnly: boolean = false) => {
if (!pdfDocument) return; if (!mergedPdfDocument) return;
setExportLoading(true); setExportLoading(true);
try { try {
const exportPageIds = selectedOnly ? selectedPages : []; const exportPageIds = selectedOnly ? selectedPages : [];
const errors = pdfExportService.validateExport(pdfDocument, exportPageIds, selectedOnly); const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly);
if (errors.length > 0) { if (errors.length > 0) {
setError(errors.join(', ')); setError(errors.join(', '));
return; return;
} }
const hasSplitMarkers = pdfDocument.pages.some(page => page.splitBefore); const hasSplitMarkers = mergedPdfDocument.pages.some(page => page.splitBefore);
if (hasSplitMarkers) { if (hasSplitMarkers) {
const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, {
selectedOnly, selectedOnly,
filename, filename,
splitDocuments: true splitDocuments: true
@ -534,7 +672,7 @@ const PageEditor = ({
setStatus(`Exported ${result.blobs.length} split documents`); setStatus(`Exported ${result.blobs.length} split documents`);
} else { } else {
const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, { const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, {
selectedOnly, selectedOnly,
filename filename
}) as { blob: Blob; filename: string }; }) as { blob: Blob; filename: string };
@ -548,7 +686,7 @@ const PageEditor = ({
} finally { } finally {
setExportLoading(false); setExportLoading(false);
} }
}, [pdfDocument, selectedPages, filename]); }, [mergedPdfDocument, selectedPages, filename]);
const handleUndo = useCallback(() => { const handleUndo = useCallback(() => {
if (undo()) { if (undo()) {
@ -563,11 +701,17 @@ const PageEditor = ({
}, [redo]); }, [redo]);
const closePdf = useCallback(() => { const closePdf = useCallback(() => {
setPdfDocument(null); setActiveFiles([]);
setFile && setFile(null); setProcessedFiles(new Map());
}, [setFile]); 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(() => { useEffect(() => {
if (onFunctionsReady) { if (onFunctionsReady) {
onFunctionsReady({ onFunctionsReady({
@ -579,6 +723,8 @@ const PageEditor = ({
handleDelete, handleDelete,
handleSplit, handleSplit,
showExportPreview, showExportPreview,
onExportSelected,
onExportAll,
exportLoading, exportLoading,
selectionMode, selectionMode,
selectedPages, selectedPages,
@ -595,24 +741,25 @@ const PageEditor = ({
handleDelete, handleDelete,
handleSplit, handleSplit,
showExportPreview, showExportPreview,
onExportSelected,
onExportAll,
exportLoading, exportLoading,
selectionMode, selectionMode,
selectedPages, selectedPages,
closePdf closePdf
]); ]);
if (!pdfDocument) { if (!mergedPdfDocument) {
return ( return (
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}> <Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={loading || pdfLoading} /> <LoadingOverlay visible={loading || pdfLoading} />
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FileUploadSelector <FileUploadSelector
title="Select a PDF to edit" title="Select PDFs to edit"
subtitle="Choose a file from storage or upload a new PDF" subtitle="Choose files from storage or upload PDFs - multiple files will be merged"
sharedFiles={sharedFiles || []} sharedFiles={sharedFiles}
onFileSelect={handleFileUpload} onFilesSelect={handleMultipleFileUpload}
allowMultiple={false}
accept={["application/pdf"]} accept={["application/pdf"]}
loading={loading || pdfLoading} loading={loading || pdfLoading}
/> />
@ -625,6 +772,7 @@ const PageEditor = ({
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}> <Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={loading || pdfLoading} /> <LoadingOverlay visible={loading || pdfLoading} />
<Box p="md" pt="xl"> <Box p="md" pt="xl">
<Group mb="md"> <Group mb="md">
<TextInput <TextInput
@ -666,7 +814,7 @@ const PageEditor = ({
)} )}
<DragDropGrid <DragDropGrid
items={pdfDocument.pages} items={mergedPdfDocument.pages}
selectedItems={selectedPages} selectedItems={selectedPages}
selectionMode={selectionMode} selectionMode={selectionMode}
isAnimating={isAnimating} isAnimating={isAnimating}
@ -685,7 +833,7 @@ const PageEditor = ({
<PageThumbnail <PageThumbnail
page={page} page={page}
index={index} index={index}
totalPages={pdfDocument.pages.length} totalPages={mergedPdfDocument.pages.length}
selectedPages={selectedPages} selectedPages={selectedPages}
selectionMode={selectionMode} selectionMode={selectionMode}
draggedPage={draggedPage} draggedPage={draggedPage}
@ -707,7 +855,7 @@ const PageEditor = ({
RotatePagesCommand={RotatePagesCommand} RotatePagesCommand={RotatePagesCommand}
DeletePagesCommand={DeletePagesCommand} DeletePagesCommand={DeletePagesCommand}
ToggleSplitCommand={ToggleSplitCommand} ToggleSplitCommand={ToggleSplitCommand}
pdfDocument={pdfDocument} pdfDocument={mergedPdfDocument}
setPdfDocument={setPdfDocument} setPdfDocument={setPdfDocument}
/> />
)} )}
@ -753,7 +901,7 @@ const PageEditor = ({
<Text fw={500}>{exportPreview.estimatedSize}</Text> <Text fw={500}>{exportPreview.estimatedSize}</Text>
</Group> </Group>
{pdfDocument && pdfDocument.pages.some(p => p.splitBefore) && ( {mergedPdfDocument && mergedPdfDocument.pages.some(p => p.splitBefore) && (
<Alert color="blue"> <Alert color="blue">
This will create multiple PDF files based on split markers. This will create multiple PDF files based on split markers.
</Alert> </Alert>
@ -771,7 +919,7 @@ const PageEditor = ({
loading={exportLoading} loading={exportLoading}
onClick={() => { onClick={() => {
setShowExportModal(false); setShowExportModal(false);
const selectedOnly = exportPreview.pageCount < (pdfDocument?.totalPages || 0); const selectedOnly = exportPreview.pageCount < (mergedPdfDocument?.totalPages || 0);
handleExport(selectedOnly); handleExport(selectedOnly);
}} }}
> >
@ -800,6 +948,16 @@ const PageEditor = ({
</Notification> </Notification>
)} )}
{error && (
<Notification
color="red"
mt="md"
onClose={() => setError(null)}
style={{ position: 'fixed', bottom: 70, right: 20, zIndex: 1000 }}
>
{error}
</Notification>
)}
</Box> </Box>
); );

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Text, Checkbox, Tooltip, ActionIcon } from '@mantine/core'; import { Text, Checkbox, Tooltip, ActionIcon } from '@mantine/core';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; 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 DeleteIcon from '@mui/icons-material/Delete';
import ContentCutIcon from '@mui/icons-material/ContentCut'; import ContentCutIcon from '@mui/icons-material/ContentCut';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import { PDFPage } from '../../types/pageEditor'; import { PDFPage } from '../../../types/pageEditor';
import styles from './PageEditor.module.css'; import styles from './PageEditor.module.css';
interface PageThumbnailProps { interface PageThumbnailProps {
@ -67,8 +67,18 @@ const PageThumbnail = ({
pdfDocument, pdfDocument,
setPdfDocument, setPdfDocument,
}: PageThumbnailProps) => { }: 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 ( return (
<div <div
ref={pageElementRef}
data-page-id={page.id} data-page-id={page.id}
className={` className={`
${styles.pageContainer} ${styles.pageContainer}

View File

@ -19,14 +19,14 @@ import { useTranslation } from 'react-i18next';
interface FilePickerModalProps { interface FilePickerModalProps {
opened: boolean; opened: boolean;
onClose: () => void; onClose: () => void;
sharedFiles: any[]; storedFiles: any[]; // Files from storage (FileWithUrl format)
onSelectFiles: (selectedFiles: any[]) => void; onSelectFiles: (selectedFiles: File[]) => void;
} }
const FilePickerModal = ({ const FilePickerModal = ({
opened, opened,
onClose, onClose,
sharedFiles, storedFiles,
onSelectFiles, onSelectFiles,
}: FilePickerModalProps) => { }: FilePickerModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -40,15 +40,15 @@ const FilePickerModal = ({
}, [opened]); }, [opened]);
const toggleFileSelection = (fileId: string) => { const toggleFileSelection = (fileId: string) => {
setSelectedFileIds(prev => setSelectedFileIds(prev => {
prev.includes(fileId) return prev.includes(fileId)
? prev.filter(id => id !== fileId) ? prev.filter(id => id !== fileId)
: [...prev, fileId] : [...prev, fileId];
); });
}; };
const selectAll = () => { const selectAll = () => {
setSelectedFileIds(sharedFiles.map(f => f.id || f.name)); setSelectedFileIds(storedFiles.map(f => f.id || f.name));
}; };
const selectNone = () => { const selectNone = () => {
@ -56,56 +56,54 @@ const FilePickerModal = ({
}; };
const handleConfirm = async () => { const handleConfirm = async () => {
const selectedFiles = sharedFiles.filter(f => const selectedFiles = storedFiles.filter(f =>
selectedFileIds.includes(f.id || f.name) 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( const convertedFiles = await Promise.all(
selectedFiles.map(async (fileItem) => { selectedFiles.map(async (fileItem) => {
console.log('Converting file item:', fileItem); try {
// If it's already a File object, return as is // If it's already a File object, return as is
if (fileItem instanceof File) { if (fileItem instanceof File) {
console.log('File is already a File object');
return fileItem; return fileItem;
} }
// If it has a file property, use that // If it has a file property, use that
if (fileItem.file && fileItem.file instanceof File) { if (fileItem.file && fileItem.file instanceof File) {
console.log('Using .file property');
return fileItem.file; return fileItem.file;
} }
// If it's a FileWithUrl from storage, reconstruct the File // If it's from IndexedDB storage, reconstruct the File
if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') {
try {
console.log('Reconstructing file from storage:', fileItem.name, fileItem);
const arrayBuffer = await fileItem.arrayBuffer(); const arrayBuffer = await fileItem.arrayBuffer();
console.log('Got arrayBuffer:', arrayBuffer);
const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' });
console.log('Created blob:', blob); return new File([blob], fileItem.name, {
const reconstructedFile = new File([blob], fileItem.name, {
type: fileItem.type || 'application/pdf', type: fileItem.type || 'application/pdf',
lastModified: fileItem.lastModified || Date.now() 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;
}
} }
console.log('No valid conversion method found for:', fileItem); // If it has data property, reconstruct the File
return null; // Don't return invalid objects 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;
}
}) })
); );
// Filter out any null values from failed conversions // Filter out any null values and return valid Files
const validFiles = convertedFiles.filter(f => f !== null); const validFiles = convertedFiles.filter((f): f is File => f !== null);
onSelectFiles(validFiles); onSelectFiles(validFiles);
onClose(); onClose();
@ -128,7 +126,7 @@ const FilePickerModal = ({
scrollAreaComponent={ScrollArea.Autosize} scrollAreaComponent={ScrollArea.Autosize}
> >
<Stack gap="md"> <Stack gap="md">
{sharedFiles.length === 0 ? ( {storedFiles.length === 0 ? (
<Text c="dimmed" ta="center" py="xl"> <Text c="dimmed" ta="center" py="xl">
{t("fileUpload.noFilesInStorage", "No files available in storage. Upload some files first.")} {t("fileUpload.noFilesInStorage", "No files available in storage. Upload some files first.")}
</Text> </Text>
@ -137,7 +135,10 @@ const FilePickerModal = ({
{/* Selection controls */} {/* Selection controls */}
<Group justify="space-between"> <Group justify="space-between">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{sharedFiles.length} {t("fileUpload.filesAvailable", "files available")} {storedFiles.length} {t("fileUpload.filesAvailable", "files available")}
{selectedFileIds.length > 0 && (
<> {selectedFileIds.length} selected</>
)}
</Text> </Text>
<Group gap="xs"> <Group gap="xs">
<Button size="xs" variant="light" onClick={selectAll}> <Button size="xs" variant="light" onClick={selectAll}>
@ -152,7 +153,7 @@ const FilePickerModal = ({
{/* File grid */} {/* File grid */}
<ScrollArea.Autosize mah={400}> <ScrollArea.Autosize mah={400}>
<SimpleGrid cols={2} spacing="md"> <SimpleGrid cols={2} spacing="md">
{sharedFiles.map((file) => { {storedFiles.map((file) => {
const fileId = file.id || file.name; const fileId = file.id || file.name;
const isSelected = selectedFileIds.includes(fileId); const isSelected = selectedFileIds.includes(fileId);

View File

@ -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 { Stack, Button, Text, Center } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import UploadFileIcon from '@mui/icons-material/UploadFile'; import UploadFileIcon from '@mui/icons-material/UploadFile';
@ -13,9 +13,8 @@ interface FileUploadSelectorProps {
// File handling // File handling
sharedFiles?: any[]; sharedFiles?: any[];
onFileSelect: (file: File) => void; onFileSelect?: (file: File) => void;
onFilesSelect?: (files: File[]) => void; onFilesSelect: (files: File[]) => void;
allowMultiple?: boolean;
accept?: string[]; accept?: string[];
// Loading state // Loading state
@ -30,39 +29,54 @@ const FileUploadSelector = ({
sharedFiles = [], sharedFiles = [],
onFileSelect, onFileSelect,
onFilesSelect, onFilesSelect,
allowMultiple = false,
accept = ["application/pdf"], accept = ["application/pdf"],
loading = false, loading = false,
disabled = false, disabled = false,
}: FileUploadSelectorProps) => { }: FileUploadSelectorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showFilePickerModal, setShowFilePickerModal] = useState(false); const [showFilePickerModal, setShowFilePickerModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = useCallback((uploadedFiles: File[]) => { const handleFileUpload = useCallback((uploadedFiles: File[]) => {
if (uploadedFiles.length === 0) return; if (uploadedFiles.length === 0) return;
if (allowMultiple && onFilesSelect) { if (onFilesSelect) {
onFilesSelect(uploadedFiles); onFilesSelect(uploadedFiles);
} else { } else if (onFileSelect) {
onFileSelect(uploadedFiles[0]); 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[]) => { const handleStorageSelection = useCallback((selectedFiles: File[]) => {
if (selectedFiles.length === 0) return; if (selectedFiles.length === 0) return;
if (allowMultiple && onFilesSelect) { if (onFilesSelect) {
onFilesSelect(selectedFiles); onFilesSelect(selectedFiles);
} else { } else if (onFileSelect) {
onFileSelect(selectedFiles[0]); onFileSelect(selectedFiles[0]);
} }
}, [allowMultiple, onFileSelect, onFilesSelect]); }, [onFileSelect, onFilesSelect]);
// Get default title and subtitle from translations if not provided // Get default title and subtitle from translations if not provided
const displayTitle = title || t(allowMultiple ? "fileUpload.selectFiles" : "fileUpload.selectFile", const displayTitle = title || t("fileUpload.selectFiles", "Select files");
allowMultiple ? "Select files" : "Select a file"); const displaySubtitle = subtitle || t("fileUpload.chooseFromStorageMultiple", "Choose files from storage or upload new PDFs");
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");
return ( return (
<> <>
@ -98,15 +112,15 @@ const FileUploadSelector = ({
<Dropzone <Dropzone
onDrop={handleFileUpload} onDrop={handleFileUpload}
accept={accept} accept={accept}
multiple={allowMultiple} multiple={true}
disabled={disabled || loading} disabled={disabled || loading}
style={{ width: '100%', minHeight: 120 }} style={{ width: '100%', minHeight: 120 }}
activateOnClick={true}
> >
<Center> <Center>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Text size="md" fw={500}> <Text size="md" fw={500}>
{t(allowMultiple ? "fileUpload.dropFilesHere" : "fileUpload.dropFileHere", {t("fileUpload.dropFilesHere", "Drop files here or click to upload")}
allowMultiple ? "Drop files here or click to upload" : "Drop file here or click to upload")}
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{accept.includes('application/pdf') {accept.includes('application/pdf')
@ -118,23 +132,27 @@ const FileUploadSelector = ({
</Center> </Center>
</Dropzone> </Dropzone>
) : ( ) : (
<Dropzone <Stack align="center" gap="sm">
onDrop={handleFileUpload}
accept={accept}
multiple={allowMultiple}
disabled={disabled || loading}
style={{ display: 'contents' }}
>
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
disabled={disabled} disabled={disabled}
loading={loading} loading={loading}
onClick={openFileDialog}
> >
{t(allowMultiple ? "fileUpload.uploadFiles" : "fileUpload.uploadFile", {t("fileUpload.uploadFiles", "Upload Files")}
allowMultiple ? "Upload Files" : "Upload File")}
</Button> </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>
</Stack> </Stack>
@ -143,7 +161,7 @@ const FileUploadSelector = ({
<FilePickerModal <FilePickerModal
opened={showFilePickerModal} opened={showFilePickerModal}
onClose={() => setShowFilePickerModal(false)} onClose={() => setShowFilePickerModal(false)}
sharedFiles={sharedFiles} storedFiles={sharedFiles}
onSelectFiles={handleStorageSelection} onSelectFiles={handleStorageSelection}
/> />
</> </>

View File

@ -8,6 +8,13 @@ export function useFileWithUrl(file: File | null): { file: File; url: string } |
return useMemo(() => { return useMemo(() => {
if (!file) return null; if (!file) return null;
// 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;
}
try {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
// Return object with cleanup function // Return object with cleanup function
@ -17,6 +24,10 @@ export function useFileWithUrl(file: File | null): { file: File; url: string } |
(result as any)._cleanup = () => URL.revokeObjectURL(url); (result as any)._cleanup = () => URL.revokeObjectURL(url);
return result; return result;
} catch (error) {
console.error('useFileWithUrl: Failed to create object URL:', error, file);
return null;
}
}, [file]); }, [file]);
} }

View File

@ -121,7 +121,7 @@ export function useToolParams(selectedToolKey: string, currentView: string) {
}); });
setSearchParams(newParams, { replace: true }); setSearchParams(newParams, { replace: true });
}, [selectedToolKey, currentView, setSearchParams, searchParams]); }, [selectedToolKey, currentView, setSearchParams]);
return { return {
toolParams, toolParams,

View File

@ -14,9 +14,9 @@ import rainbowStyles from '../styles/rainbow.module.css';
import ToolPicker from "../components/tools/ToolPicker"; import ToolPicker from "../components/tools/ToolPicker";
import TopControls from "../components/shared/TopControls"; import TopControls from "../components/shared/TopControls";
import FileManager from "../components/fileManagement/FileManager"; import FileManager from "../components/fileManagement/FileManager";
import FileEditor from "../components/editor/FileEditor"; import FileEditor from "../components/pageEditor/FileEditor";
import PageEditor from "../components/editor/PageEditor"; import PageEditor from "../components/pageEditor/PageEditor";
import PageEditorControls from "../components/editor/PageEditorControls"; import PageEditorControls from "../components/pageEditor/PageEditorControls";
import Viewer from "../components/viewer/Viewer"; import Viewer from "../components/viewer/Viewer";
import FileUploadSelector from "../components/shared/FileUploadSelector"; import FileUploadSelector from "../components/shared/FileUploadSelector";
import SplitPdfPanel from "../tools/Split"; import SplitPdfPanel from "../tools/Split";
@ -173,15 +173,109 @@ export default function HomePage() {
}, [addToActiveFiles]); }, [addToActiveFiles]);
// Handle opening file editor with selected files // Handle opening file editor with selected files
const handleOpenFileEditor = useCallback((selectedFiles) => { const handleOpenFileEditor = useCallback(async (selectedFiles) => {
setPreSelectedFiles(selectedFiles || []); if (!selectedFiles || selectedFiles.length === 0) {
setPreSelectedFiles([]);
handleViewChange("fileEditor"); handleViewChange("fileEditor");
}, [handleViewChange]); 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]; const selectedTool = toolRegistry[selectedToolKey];
// Convert current active file to format expected by Viewer/PageEditor // For Viewer - convert first active file to expected format (only when needed)
const currentFileWithUrl = useFileWithUrl(activeFiles[0] || null); const currentFileWithUrl = useFileWithUrl(
(currentView === "viewer" && activeFiles[0]) ? activeFiles[0] : null
);
return ( return (
<Group <Group
@ -288,6 +382,7 @@ export default function HomePage() {
setFiles={setStoredFiles} setFiles={setStoredFiles}
setCurrentView={handleViewChange} setCurrentView={handleViewChange}
onOpenFileEditor={handleOpenFileEditor} onOpenFileEditor={handleOpenFileEditor}
onOpenPageEditor={handleOpenPageEditor}
onLoadFileToActive={addToActiveFiles} onLoadFileToActive={addToActiveFiles}
/> />
) : (currentView != "fileManager") && !activeFiles[0] ? ( ) : (currentView != "fileManager") && !activeFiles[0] ? (
@ -309,8 +404,8 @@ export default function HomePage() {
</Container> </Container>
) : currentView === "fileEditor" ? ( ) : currentView === "fileEditor" ? (
<FileEditor <FileEditor
sharedFiles={activeFiles} activeFiles={activeFiles}
setSharedFiles={setActiveFiles} setActiveFiles={setActiveFiles}
preSelectedFiles={preSelectedFiles} preSelectedFiles={preSelectedFiles}
onClearPreSelection={() => setPreSelectedFiles([])} onClearPreSelection={() => setPreSelectedFiles([])}
onOpenPageEditor={(file) => { onOpenPageEditor={(file) => {
@ -339,18 +434,12 @@ export default function HomePage() {
) : currentView === "pageEditor" ? ( ) : currentView === "pageEditor" ? (
<> <>
<PageEditor <PageEditor
file={currentFileWithUrl} activeFiles={activeFiles}
setFile={(fileObj) => { setActiveFiles={setActiveFiles}
if (fileObj) {
setCurrentActiveFile(fileObj.file);
} else {
setActiveFiles([]);
}
}}
downloadUrl={downloadUrl} downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl} setDownloadUrl={setDownloadUrl}
sharedFiles={storedFiles}
onFunctionsReady={setPageEditorFunctions} onFunctionsReady={setPageEditorFunctions}
sharedFiles={activeFiles}
/> />
{activeFiles[0] && pageEditorFunctions && ( {activeFiles[0] && pageEditorFunctions && (
<PageEditorControls <PageEditorControls
@ -362,8 +451,8 @@ export default function HomePage() {
onRotate={pageEditorFunctions.handleRotate} onRotate={pageEditorFunctions.handleRotate}
onDelete={pageEditorFunctions.handleDelete} onDelete={pageEditorFunctions.handleDelete}
onSplit={pageEditorFunctions.handleSplit} onSplit={pageEditorFunctions.handleSplit}
onExportSelected={() => pageEditorFunctions.showExportPreview(true)} onExportSelected={pageEditorFunctions.onExportSelected}
onExportAll={() => pageEditorFunctions.showExportPreview(false)} onExportAll={pageEditorFunctions.onExportAll}
exportLoading={pageEditorFunctions.exportLoading} exportLoading={pageEditorFunctions.exportLoading}
selectionMode={pageEditorFunctions.selectionMode} selectionMode={pageEditorFunctions.selectionMode}
selectedPages={pageEditorFunctions.selectedPages} selectedPages={pageEditorFunctions.selectedPages}
@ -376,6 +465,7 @@ export default function HomePage() {
setFiles={setStoredFiles} setFiles={setStoredFiles}
setCurrentView={handleViewChange} setCurrentView={handleViewChange}
onOpenFileEditor={handleOpenFileEditor} onOpenFileEditor={handleOpenFileEditor}
onOpenPageEditor={handleOpenPageEditor}
onLoadFileToActive={addToActiveFiles} onLoadFileToActive={addToActiveFiles}
/> />
)} )}