mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
add file after page
This commit is contained in:
parent
6de5f92e83
commit
4b2e868aad
@ -385,6 +385,19 @@ const PageEditor = ({
|
|||||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||||
}, [selectedPageNumbers, displayDocument]);
|
}, [selectedPageNumbers, displayDocument]);
|
||||||
|
|
||||||
|
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
||||||
|
if (!displayDocument || files.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage);
|
||||||
|
if (!targetPage) return;
|
||||||
|
|
||||||
|
await actions.addFiles(files, { insertAfterPageId: targetPage.id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert files:', error);
|
||||||
|
}
|
||||||
|
}, [displayDocument, actions]);
|
||||||
|
|
||||||
const handleSelectAll = useCallback(() => {
|
const handleSelectAll = useCallback(() => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
const allPageNumbers = Array.from({ length: displayDocument.pages.length }, (_, i) => i + 1);
|
const allPageNumbers = Array.from({ length: displayDocument.pages.length }, (_, i) => i + 1);
|
||||||
@ -416,12 +429,7 @@ const PageEditor = ({
|
|||||||
const getSourceFiles = useCallback((): Map<string, File> | null => {
|
const getSourceFiles = useCallback((): Map<string, File> | null => {
|
||||||
const sourceFiles = new Map<string, File>();
|
const sourceFiles = new Map<string, File>();
|
||||||
|
|
||||||
// Check if we have multiple files by looking at active file IDs
|
// Always include original files
|
||||||
if (activeFileIds.length <= 1) {
|
|
||||||
return null; // Use single-file export method
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all source files
|
|
||||||
activeFileIds.forEach(fileId => {
|
activeFileIds.forEach(fileId => {
|
||||||
const file = selectors.getFile(fileId);
|
const file = selectors.getFile(fileId);
|
||||||
if (file) {
|
if (file) {
|
||||||
@ -429,6 +437,14 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use multi-file export if we have multiple original files
|
||||||
|
const hasInsertedFiles = false;
|
||||||
|
const hasMultipleOriginalFiles = activeFileIds.length > 1;
|
||||||
|
|
||||||
|
if (!hasInsertedFiles && !hasMultipleOriginalFiles) {
|
||||||
|
return null; // Use single-file export method
|
||||||
|
}
|
||||||
|
|
||||||
return sourceFiles.size > 0 ? sourceFiles : null;
|
return sourceFiles.size > 0 ? sourceFiles : null;
|
||||||
}, [activeFileIds, selectors]);
|
}, [activeFileIds, selectors]);
|
||||||
|
|
||||||
@ -755,6 +771,7 @@ const PageEditor = ({
|
|||||||
pdfDocument={displayDocument}
|
pdfDocument={displayDocument}
|
||||||
setPdfDocument={setEditedDocument}
|
setPdfDocument={setEditedDocument}
|
||||||
splitPositions={splitPositions}
|
splitPositions={splitPositions}
|
||||||
|
onInsertFiles={handleInsertFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -6,9 +6,11 @@ import RotateLeftIcon from '@mui/icons-material/RotateLeft';
|
|||||||
import RotateRightIcon from '@mui/icons-material/RotateRight';
|
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 AddIcon from '@mui/icons-material/Add';
|
||||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||||
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
||||||
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
||||||
|
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||||
import styles from './PageEditor.module.css';
|
import styles from './PageEditor.module.css';
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ interface PageThumbnailProps {
|
|||||||
pdfDocument: PDFDocument;
|
pdfDocument: PDFDocument;
|
||||||
setPdfDocument: (doc: PDFDocument) => void;
|
setPdfDocument: (doc: PDFDocument) => void;
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<number>;
|
||||||
|
onInsertFiles?: (files: File[], insertAfterPage: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||||
@ -60,6 +63,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
pdfDocument,
|
pdfDocument,
|
||||||
setPdfDocument,
|
setPdfDocument,
|
||||||
splitPositions,
|
splitPositions,
|
||||||
|
onInsertFiles,
|
||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
@ -67,6 +71,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||||
|
const { openFilesModal } = useFilesModalContext();
|
||||||
|
|
||||||
// Calculate document aspect ratio from first non-blank page
|
// Calculate document aspect ratio from first non-blank page
|
||||||
const getDocumentAspectRatio = useCallback(() => {
|
const getDocumentAspectRatio = useCallback(() => {
|
||||||
@ -224,6 +229,27 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
onSetStatus(`Split marker ${action} after position ${index + 1}`);
|
onSetStatus(`Split marker ${action} after position ${index + 1}`);
|
||||||
}, [index, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
|
}, [index, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
|
||||||
|
|
||||||
|
const handleInsertFileAfter = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (onInsertFiles) {
|
||||||
|
// Open file manager modal with custom handler for page insertion
|
||||||
|
openFilesModal({
|
||||||
|
insertAfterPage: page.pageNumber,
|
||||||
|
customHandler: (files: File[], insertAfterPage?: number) => {
|
||||||
|
if (insertAfterPage !== undefined) {
|
||||||
|
onInsertFiles(files, insertAfterPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onSetStatus(`Select files to insert after page ${page.pageNumber}`);
|
||||||
|
} else {
|
||||||
|
// Fallback to normal file handling
|
||||||
|
openFilesModal({ insertAfterPage: page.pageNumber });
|
||||||
|
onSetStatus(`Select files to insert after page ${page.pageNumber}`);
|
||||||
|
}
|
||||||
|
}, [openFilesModal, page.pageNumber, onSetStatus, onInsertFiles]);
|
||||||
|
|
||||||
// Handle click vs drag differentiation
|
// Handle click vs drag differentiation
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
setIsMouseDown(true);
|
setIsMouseDown(true);
|
||||||
@ -509,6 +535,17 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Tooltip label="Insert File After">
|
||||||
|
<ActionIcon
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||||
|
onClick={handleInsertFileAfter}
|
||||||
|
>
|
||||||
|
<AddIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -568,6 +568,268 @@ export class BulkPageBreakCommand extends DOMCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InsertFilesCommand extends DOMCommand {
|
||||||
|
private insertedPages: PDFPage[] = [];
|
||||||
|
private originalDocument: PDFDocument | null = null;
|
||||||
|
private fileDataMap = new Map<string, ArrayBuffer>(); // Store file data for thumbnail generation
|
||||||
|
private originalProcessedFile: any = null; // Store original ProcessedFile for undo
|
||||||
|
private insertedFileMap = new Map<string, File>(); // Store inserted files for export
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private files: File[],
|
||||||
|
private insertAfterPageNumber: number,
|
||||||
|
private getCurrentDocument: () => PDFDocument | null,
|
||||||
|
private setDocument: (doc: PDFDocument) => void,
|
||||||
|
private setSelectedPages: (pages: number[]) => void,
|
||||||
|
private getSelectedPages: () => number[],
|
||||||
|
private updateFileContext?: (updatedDocument: PDFDocument, insertedFiles?: Map<string, File>) => void
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(): Promise<void> {
|
||||||
|
const currentDoc = this.getCurrentDocument();
|
||||||
|
if (!currentDoc || this.files.length === 0) return;
|
||||||
|
|
||||||
|
// Store original state for undo
|
||||||
|
this.originalDocument = {
|
||||||
|
...currentDoc,
|
||||||
|
pages: currentDoc.pages.map(page => ({...page}))
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process each file to extract pages and wait for all to complete
|
||||||
|
const allNewPages: PDFPage[] = [];
|
||||||
|
|
||||||
|
// Process all files and wait for their completion
|
||||||
|
const baseTimestamp = Date.now();
|
||||||
|
const extractionPromises = this.files.map(async (file, index) => {
|
||||||
|
const fileId = `inserted-${file.name}-${baseTimestamp + index}`;
|
||||||
|
// Store inserted file for export
|
||||||
|
this.insertedFileMap.set(fileId, file);
|
||||||
|
// Use base timestamp + index to ensure unique but predictable file IDs
|
||||||
|
return await this.extractPagesFromFile(file, baseTimestamp + index);
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractedPageArrays = await Promise.all(extractionPromises);
|
||||||
|
|
||||||
|
// Flatten all extracted pages
|
||||||
|
for (const pages of extractedPageArrays) {
|
||||||
|
allNewPages.push(...pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allNewPages.length === 0) return;
|
||||||
|
|
||||||
|
// Find insertion point (after the specified page)
|
||||||
|
const insertIndex = this.insertAfterPageNumber; // Insert after page N means insert at index N
|
||||||
|
|
||||||
|
// Create new pages array with inserted pages
|
||||||
|
const newPages: PDFPage[] = [];
|
||||||
|
let pageNumberCounter = 1;
|
||||||
|
|
||||||
|
// Add pages before insertion point
|
||||||
|
for (let i = 0; i < insertIndex && i < currentDoc.pages.length; i++) {
|
||||||
|
const page = { ...currentDoc.pages[i], pageNumber: pageNumberCounter++ };
|
||||||
|
newPages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserted pages
|
||||||
|
for (const newPage of allNewPages) {
|
||||||
|
const insertedPage: PDFPage = {
|
||||||
|
...newPage,
|
||||||
|
pageNumber: pageNumberCounter++,
|
||||||
|
selected: false,
|
||||||
|
splitAfter: false
|
||||||
|
};
|
||||||
|
newPages.push(insertedPage);
|
||||||
|
this.insertedPages.push(insertedPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining pages after insertion point
|
||||||
|
for (let i = insertIndex; i < currentDoc.pages.length; i++) {
|
||||||
|
const page = { ...currentDoc.pages[i], pageNumber: pageNumberCounter++ };
|
||||||
|
newPages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update document
|
||||||
|
const updatedDocument: PDFDocument = {
|
||||||
|
...currentDoc,
|
||||||
|
pages: newPages,
|
||||||
|
totalPages: newPages.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setDocument(updatedDocument);
|
||||||
|
|
||||||
|
// Update FileContext with the new document structure and inserted files
|
||||||
|
if (this.updateFileContext) {
|
||||||
|
this.updateFileContext(updatedDocument, this.insertedFileMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate thumbnails for inserted pages (all files should be read by now)
|
||||||
|
this.generateThumbnailsForInsertedPages(updatedDocument);
|
||||||
|
|
||||||
|
// Maintain existing selection by mapping original selected pages to their new positions
|
||||||
|
const originalSelection = this.getSelectedPages();
|
||||||
|
const updatedSelection: number[] = [];
|
||||||
|
|
||||||
|
originalSelection.forEach(originalPageNum => {
|
||||||
|
if (originalPageNum <= this.insertAfterPageNumber) {
|
||||||
|
// Pages before insertion point keep same number
|
||||||
|
updatedSelection.push(originalPageNum);
|
||||||
|
} else {
|
||||||
|
// Pages after insertion point are shifted by number of inserted pages
|
||||||
|
updatedSelection.push(originalPageNum + allNewPages.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setSelectedPages(updatedSelection);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert files:', error);
|
||||||
|
// Revert to original state if error occurs
|
||||||
|
if (this.originalDocument) {
|
||||||
|
this.setDocument(this.originalDocument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateThumbnailsForInsertedPages(updatedDocument: PDFDocument): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { thumbnailGenerationService } = await import('../../../services/thumbnailGenerationService');
|
||||||
|
|
||||||
|
// Group pages by file ID to generate thumbnails efficiently
|
||||||
|
const pagesByFileId = new Map<string, PDFPage[]>();
|
||||||
|
|
||||||
|
for (const page of this.insertedPages) {
|
||||||
|
const fileId = page.id.substring(0, page.id.lastIndexOf('-page-'));
|
||||||
|
if (!pagesByFileId.has(fileId)) {
|
||||||
|
pagesByFileId.set(fileId, []);
|
||||||
|
}
|
||||||
|
pagesByFileId.get(fileId)!.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate thumbnails for each file
|
||||||
|
for (const [fileId, pages] of pagesByFileId) {
|
||||||
|
const arrayBuffer = this.fileDataMap.get(fileId);
|
||||||
|
|
||||||
|
console.log('Generating thumbnails for file:', fileId);
|
||||||
|
console.log('Pages:', pages.length);
|
||||||
|
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
||||||
|
|
||||||
|
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||||
|
// Extract page numbers for all pages from this file
|
||||||
|
const pageNumbers = pages.map(page => {
|
||||||
|
const pageNumMatch = page.id.match(/-page-(\d+)$/);
|
||||||
|
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Generating thumbnails for page numbers:', pageNumbers);
|
||||||
|
|
||||||
|
// Generate thumbnails for all pages from this file at once
|
||||||
|
const results = await thumbnailGenerationService.generateThumbnails(
|
||||||
|
fileId,
|
||||||
|
arrayBuffer,
|
||||||
|
pageNumbers,
|
||||||
|
{ scale: 0.2, quality: 0.8 }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
|
||||||
|
|
||||||
|
// Update pages with generated thumbnails
|
||||||
|
for (let i = 0; i < results.length && i < pages.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
const page = pages[i];
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
|
||||||
|
if (pageIndex >= 0) {
|
||||||
|
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
|
||||||
|
console.log('Updated thumbnail for page:', page.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger re-render by updating the document
|
||||||
|
this.setDocument({ ...updatedDocument });
|
||||||
|
} else {
|
||||||
|
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate thumbnails for inserted pages:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractPagesFromFile(file: File, baseTimestamp: number): Promise<PDFPage[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = event.target?.result as ArrayBuffer;
|
||||||
|
console.log('File reader onload - arrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
||||||
|
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
reject(new Error('Failed to read file'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the ArrayBuffer before passing to PDF.js (it might consume it)
|
||||||
|
const clonedArrayBuffer = arrayBuffer.slice(0);
|
||||||
|
|
||||||
|
// Use PDF.js via the worker manager to extract pages
|
||||||
|
const { pdfWorkerManager } = await import('../../../services/pdfWorkerManager');
|
||||||
|
const pdf = await pdfWorkerManager.createDocument(clonedArrayBuffer);
|
||||||
|
|
||||||
|
const pageCount = pdf.numPages;
|
||||||
|
const pages: PDFPage[] = [];
|
||||||
|
const fileId = `inserted-${file.name}-${baseTimestamp}`;
|
||||||
|
|
||||||
|
console.log('Original ArrayBuffer size:', arrayBuffer.byteLength);
|
||||||
|
console.log('Storing ArrayBuffer for fileId:', fileId, 'size:', arrayBuffer.byteLength);
|
||||||
|
|
||||||
|
// Store the original ArrayBuffer for thumbnail generation
|
||||||
|
this.fileDataMap.set(fileId, arrayBuffer);
|
||||||
|
|
||||||
|
console.log('After storing - fileDataMap size:', this.fileDataMap.size);
|
||||||
|
console.log('Stored value size:', this.fileDataMap.get(fileId)?.byteLength || 'undefined');
|
||||||
|
|
||||||
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
|
const pageId = `${fileId}-page-${i}`;
|
||||||
|
pages.push({
|
||||||
|
id: pageId,
|
||||||
|
pageNumber: i, // Will be renumbered in execute()
|
||||||
|
originalPageNumber: i,
|
||||||
|
thumbnail: null, // Will be generated after insertion
|
||||||
|
rotation: 0,
|
||||||
|
selected: false,
|
||||||
|
splitAfter: false,
|
||||||
|
isBlankPage: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up PDF document
|
||||||
|
pdfWorkerManager.destroyDocument(pdf);
|
||||||
|
|
||||||
|
resolve(pages);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
if (!this.originalDocument) return;
|
||||||
|
this.setDocument(this.originalDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
get description(): string {
|
||||||
|
return `Insert ${this.files.length} file(s) after page ${this.insertAfterPageNumber}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simple undo manager for DOM commands
|
// Simple undo manager for DOM commands
|
||||||
export class UndoManager {
|
export class UndoManager {
|
||||||
private undoStack: DOMCommand[] = [];
|
private undoStack: DOMCommand[] = [];
|
||||||
@ -585,6 +847,13 @@ export class UndoManager {
|
|||||||
this.onStateChange?.();
|
this.onStateChange?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For async commands that need to be executed manually
|
||||||
|
addToUndoStack(command: DOMCommand): void {
|
||||||
|
this.undoStack.push(command);
|
||||||
|
this.redoStack = [];
|
||||||
|
this.onStateChange?.();
|
||||||
|
}
|
||||||
|
|
||||||
undo(): boolean {
|
undo(): boolean {
|
||||||
const command = this.undoStack.pop();
|
const command = this.undoStack.pop();
|
||||||
if (command) {
|
if (command) {
|
||||||
|
@ -49,33 +49,41 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
||||||
.join(' + ');
|
.join(' + ');
|
||||||
|
|
||||||
// Debug logging for merged document creation
|
// Build page insertion map from files with insertion positions
|
||||||
console.log(`🎬 PageEditor: Building merged document for ${name} with ${activeFileIds.length} files`);
|
const insertionMap = new Map<string, string[]>(); // insertAfterPageId -> fileIds
|
||||||
|
const originalFileIds: string[] = [];
|
||||||
|
|
||||||
// Collect pages from ALL active files, not just the primary file
|
activeFileIds.forEach(fileId => {
|
||||||
|
const record = selectors.getFileRecord(fileId);
|
||||||
|
if (record?.insertAfterPageId !== undefined) {
|
||||||
|
if (!insertionMap.has(record.insertAfterPageId)) {
|
||||||
|
insertionMap.set(record.insertAfterPageId, []);
|
||||||
|
}
|
||||||
|
insertionMap.get(record.insertAfterPageId)!.push(fileId);
|
||||||
|
} else {
|
||||||
|
originalFileIds.push(fileId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build pages by interleaving original pages with insertions
|
||||||
let pages: PDFPage[] = [];
|
let pages: PDFPage[] = [];
|
||||||
let totalPageCount = 0;
|
let totalPageCount = 0;
|
||||||
|
|
||||||
activeFileIds.forEach((fileId, fileIndex) => {
|
// Helper function to create pages from a file
|
||||||
|
const createPagesFromFile = (fileId: string, startPageNumber: number): PDFPage[] => {
|
||||||
const fileRecord = selectors.getFileRecord(fileId);
|
const fileRecord = selectors.getFileRecord(fileId);
|
||||||
if (!fileRecord) {
|
if (!fileRecord) {
|
||||||
console.warn(`🎬 PageEditor: No record found for file ${fileId}`);
|
return [];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedFile = fileRecord.processedFile;
|
const processedFile = fileRecord.processedFile;
|
||||||
console.log(`🎬 PageEditor: Processing file ${fileIndex + 1}/${activeFileIds.length} (${fileRecord.name})`);
|
|
||||||
console.log(`🎬 ProcessedFile exists:`, !!processedFile);
|
|
||||||
console.log(`🎬 ProcessedFile pages:`, processedFile?.pages?.length || 0);
|
|
||||||
console.log(`🎬 ProcessedFile totalPages:`, processedFile?.totalPages || 'unknown');
|
|
||||||
|
|
||||||
let filePages: PDFPage[] = [];
|
let filePages: PDFPage[] = [];
|
||||||
|
|
||||||
if (processedFile?.pages && processedFile.pages.length > 0) {
|
if (processedFile?.pages && processedFile.pages.length > 0) {
|
||||||
// Use fully processed pages with thumbnails
|
// Use fully processed pages with thumbnails
|
||||||
filePages = processedFile.pages.map((page, pageIndex) => ({
|
filePages = processedFile.pages.map((page, pageIndex) => ({
|
||||||
id: `${fileId}-${page.pageNumber}`,
|
id: `${fileId}-${page.pageNumber}`,
|
||||||
pageNumber: totalPageCount + pageIndex + 1,
|
pageNumber: startPageNumber + pageIndex,
|
||||||
thumbnail: page.thumbnail || null,
|
thumbnail: page.thumbnail || null,
|
||||||
rotation: page.rotation || 0,
|
rotation: page.rotation || 0,
|
||||||
selected: false,
|
selected: false,
|
||||||
@ -85,29 +93,61 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
}));
|
}));
|
||||||
} else if (processedFile?.totalPages) {
|
} else if (processedFile?.totalPages) {
|
||||||
// Fallback: create pages without thumbnails but with correct count
|
// Fallback: create pages without thumbnails but with correct count
|
||||||
console.log(`🎬 PageEditor: Creating placeholder pages for ${fileRecord.name} (${processedFile.totalPages} pages)`);
|
|
||||||
filePages = Array.from({ length: processedFile.totalPages }, (_, pageIndex) => ({
|
filePages = Array.from({ length: processedFile.totalPages }, (_, pageIndex) => ({
|
||||||
id: `${fileId}-${pageIndex + 1}`,
|
id: `${fileId}-${pageIndex + 1}`,
|
||||||
pageNumber: totalPageCount + pageIndex + 1,
|
pageNumber: startPageNumber + pageIndex,
|
||||||
originalPageNumber: pageIndex + 1,
|
originalPageNumber: pageIndex + 1,
|
||||||
originalFileId: fileId,
|
originalFileId: fileId,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
thumbnail: null, // Will be generated later
|
thumbnail: null,
|
||||||
selected: false,
|
selected: false,
|
||||||
splitAfter: false,
|
splitAfter: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pages = pages.concat(filePages);
|
return filePages;
|
||||||
totalPageCount += filePages.length;
|
};
|
||||||
|
|
||||||
|
// Collect all pages from original files (without renumbering yet)
|
||||||
|
const originalFilePages: PDFPage[] = [];
|
||||||
|
originalFileIds.forEach(fileId => {
|
||||||
|
const filePages = createPagesFromFile(fileId, 1); // Temporary numbering
|
||||||
|
originalFilePages.push(...filePages);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pages.length === 0) {
|
// Start with all original pages numbered sequentially
|
||||||
console.warn('🎬 PageEditor: No pages found in any files');
|
pages = originalFilePages.map((page, index) => ({
|
||||||
return null;
|
...page,
|
||||||
|
pageNumber: index + 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Process each insertion by finding the page ID and inserting after it
|
||||||
|
for (const [insertAfterPageId, fileIds] of insertionMap.entries()) {
|
||||||
|
const targetPageIndex = pages.findIndex(p => p.id === insertAfterPageId);
|
||||||
|
|
||||||
|
if (targetPageIndex === -1) continue;
|
||||||
|
|
||||||
|
// Collect all pages to insert
|
||||||
|
const allNewPages: PDFPage[] = [];
|
||||||
|
fileIds.forEach(fileId => {
|
||||||
|
const insertedPages = createPagesFromFile(fileId, 1);
|
||||||
|
allNewPages.push(...insertedPages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert all new pages after the target page
|
||||||
|
pages.splice(targetPageIndex + 1, 0, ...allNewPages);
|
||||||
|
|
||||||
|
// Renumber all pages after insertion
|
||||||
|
pages.forEach((page, index) => {
|
||||||
|
page.pageNumber = index + 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🎬 PageEditor: Created merged document with ${pages.length} total pages`);
|
totalPageCount = pages.length;
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const mergedDoc: PDFDocument = {
|
const mergedDoc: PDFDocument = {
|
||||||
id: activeFileIds.join('-'),
|
id: activeFileIds.join('-'),
|
||||||
|
@ -25,7 +25,7 @@ const FileStatusIndicator = ({
|
|||||||
{t("files.noFiles", "No files uploaded. ")}{" "}
|
{t("files.noFiles", "No files uploaded. ")}{" "}
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={openFilesModal}
|
onClick={() => openFilesModal()}
|
||||||
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
||||||
>
|
>
|
||||||
<FolderIcon style={{ fontSize: '14px' }} />
|
<FolderIcon style={{ fontSize: '14px' }} />
|
||||||
@ -42,7 +42,7 @@ const FileStatusIndicator = ({
|
|||||||
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={openFilesModal}
|
onClick={() => openFilesModal()}
|
||||||
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
||||||
>
|
>
|
||||||
<FolderIcon style={{ fontSize: '14px' }} />
|
<FolderIcon style={{ fontSize: '14px' }} />
|
||||||
|
@ -73,8 +73,8 @@ function FileContextInner({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// File operations using unified addFiles helper with persistence
|
// File operations using unified addFiles helper with persistence
|
||||||
const addRawFiles = useCallback(async (files: File[]): Promise<File[]> => {
|
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise<File[]> => {
|
||||||
const addedFilesWithIds = await addFiles('raw', { files }, stateRef, filesRef, dispatch, lifecycleManager);
|
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Persist to IndexedDB if enabled
|
// Persist to IndexedDB if enabled
|
||||||
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
||||||
|
@ -4,7 +4,7 @@ import { FileMetadata } from '../types/file';
|
|||||||
|
|
||||||
interface FilesModalContextType {
|
interface FilesModalContextType {
|
||||||
isFilesModalOpen: boolean;
|
isFilesModalOpen: boolean;
|
||||||
openFilesModal: () => void;
|
openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => void;
|
||||||
closeFilesModal: () => void;
|
closeFilesModal: () => void;
|
||||||
onFileSelect: (file: File) => void;
|
onFileSelect: (file: File) => void;
|
||||||
onFilesSelect: (files: File[]) => void;
|
onFilesSelect: (files: File[]) => void;
|
||||||
@ -19,30 +19,55 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||||||
const { addToActiveFiles, addMultipleFiles, addStoredFiles } = useFileHandler();
|
const { addToActiveFiles, addMultipleFiles, addStoredFiles } = useFileHandler();
|
||||||
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
||||||
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
|
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
|
||||||
|
const [insertAfterPage, setInsertAfterPage] = useState<number | undefined>();
|
||||||
|
const [customHandler, setCustomHandler] = useState<((files: File[], insertAfterPage?: number) => void) | undefined>();
|
||||||
|
|
||||||
const openFilesModal = useCallback(() => {
|
const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => {
|
||||||
|
setInsertAfterPage(options?.insertAfterPage);
|
||||||
|
setCustomHandler(() => options?.customHandler);
|
||||||
setIsFilesModalOpen(true);
|
setIsFilesModalOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeFilesModal = useCallback(() => {
|
const closeFilesModal = useCallback(() => {
|
||||||
setIsFilesModalOpen(false);
|
setIsFilesModalOpen(false);
|
||||||
|
setInsertAfterPage(undefined); // Clear insertion position
|
||||||
|
setCustomHandler(undefined); // Clear custom handler
|
||||||
onModalClose?.();
|
onModalClose?.();
|
||||||
}, [onModalClose]);
|
}, [onModalClose]);
|
||||||
|
|
||||||
const handleFileSelect = useCallback((file: File) => {
|
const handleFileSelect = useCallback((file: File) => {
|
||||||
addToActiveFiles(file);
|
if (customHandler) {
|
||||||
|
// Use custom handler for special cases (like page insertion)
|
||||||
|
customHandler([file], insertAfterPage);
|
||||||
|
} else {
|
||||||
|
// Use normal file handling
|
||||||
|
addToActiveFiles(file);
|
||||||
|
}
|
||||||
closeFilesModal();
|
closeFilesModal();
|
||||||
}, [addToActiveFiles, closeFilesModal]);
|
}, [addToActiveFiles, closeFilesModal, insertAfterPage, customHandler]);
|
||||||
|
|
||||||
const handleFilesSelect = useCallback((files: File[]) => {
|
const handleFilesSelect = useCallback((files: File[]) => {
|
||||||
addMultipleFiles(files);
|
if (customHandler) {
|
||||||
|
// Use custom handler for special cases (like page insertion)
|
||||||
|
customHandler(files, insertAfterPage);
|
||||||
|
} else {
|
||||||
|
// Use normal file handling
|
||||||
|
addMultipleFiles(files);
|
||||||
|
}
|
||||||
closeFilesModal();
|
closeFilesModal();
|
||||||
}, [addMultipleFiles, closeFilesModal]);
|
}, [addMultipleFiles, closeFilesModal, insertAfterPage, customHandler]);
|
||||||
|
|
||||||
const handleStoredFilesSelect = useCallback((filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => {
|
const handleStoredFilesSelect = useCallback((filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => {
|
||||||
addStoredFiles(filesWithMetadata);
|
if (customHandler) {
|
||||||
|
// Use custom handler for special cases (like page insertion)
|
||||||
|
const files = filesWithMetadata.map(item => item.file);
|
||||||
|
customHandler(files, insertAfterPage);
|
||||||
|
} else {
|
||||||
|
// Use normal file handling
|
||||||
|
addStoredFiles(filesWithMetadata);
|
||||||
|
}
|
||||||
closeFilesModal();
|
closeFilesModal();
|
||||||
}, [addStoredFiles, closeFilesModal]);
|
}, [addStoredFiles, closeFilesModal, insertAfterPage, customHandler]);
|
||||||
|
|
||||||
const setModalCloseCallback = useCallback((callback: () => void) => {
|
const setModalCloseCallback = useCallback((callback: () => void) => {
|
||||||
setOnModalClose(() => callback);
|
setOnModalClose(() => callback);
|
||||||
|
@ -84,6 +84,9 @@ interface AddFileOptions {
|
|||||||
|
|
||||||
// For 'stored' files
|
// For 'stored' files
|
||||||
filesWithMetadata?: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>;
|
filesWithMetadata?: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>;
|
||||||
|
|
||||||
|
// Insertion position
|
||||||
|
insertAfterPageId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,6 +167,11 @@ export async function addFiles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store insertion position if provided
|
||||||
|
if (options.insertAfterPageId !== undefined) {
|
||||||
|
record.insertAfterPageId = options.insertAfterPageId;
|
||||||
|
}
|
||||||
|
|
||||||
// Create initial processedFile metadata with page count
|
// Create initial processedFile metadata with page count
|
||||||
if (pageCount > 0) {
|
if (pageCount > 0) {
|
||||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||||
|
@ -55,6 +55,7 @@ export interface FileRecord {
|
|||||||
blobUrl?: string;
|
blobUrl?: string;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
processedFile?: ProcessedFileMetadata;
|
processedFile?: ProcessedFileMetadata;
|
||||||
|
insertAfterPageId?: string; // Page ID after which this file should be inserted
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
// Note: File object stored in provider ref, not in state
|
// Note: File object stored in provider ref, not in state
|
||||||
}
|
}
|
||||||
@ -216,7 +217,7 @@ export type FileContextAction =
|
|||||||
|
|
||||||
export interface FileContextActions {
|
export interface FileContextActions {
|
||||||
// File management - lightweight actions only
|
// File management - lightweight actions only
|
||||||
addFiles: (files: File[]) => Promise<File[]>;
|
addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise<File[]>;
|
||||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
|
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
|
||||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<File[]>;
|
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<File[]>;
|
||||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user