mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-29 08:35:30 +00:00
commit
29916d85b1
@ -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>
|
||||||
|
@ -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;
|
@ -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 && (
|
@ -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>
|
||||||
);
|
);
|
@ -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}
|
@ -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);
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user