diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 0e93313fc..35398eb14 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -112,6 +112,8 @@ export default function Workbench() { onDelete={pageEditorFunctions.handleDelete} onSplit={pageEditorFunctions.handleSplit} onSplitAll={pageEditorFunctions.handleSplitAll} + onPageBreak={pageEditorFunctions.handlePageBreak} + onPageBreakAll={pageEditorFunctions.handlePageBreakAll} onExportSelected={pageEditorFunctions.onExportSelected} onExportAll={pageEditorFunctions.onExportAll} exportLoading={pageEditorFunctions.exportLoading} diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index db828bdd2..21403731d 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -27,6 +27,8 @@ import { BulkRotateCommand, BulkSplitCommand, SplitAllCommand, + PageBreakCommand, + BulkPageBreakCommand, UndoManager } from './commands/pageCommands'; import { usePageDocument } from './hooks/usePageDocument'; @@ -42,6 +44,8 @@ export interface PageEditorProps { handleDelete: () => void; handleSplit: () => void; handleSplitAll: () => void; + handlePageBreak: () => void; + handlePageBreakAll: () => void; showExportPreview: (selectedOnly: boolean) => void; onExportSelected: () => void; onExportAll: () => void; @@ -266,6 +270,31 @@ const PageEditor = ({ undoManagerRef.current.executeCommand(splitAllCommand); }, [displayDocument, splitPositions]); + const handlePageBreak = useCallback(() => { + if (!displayDocument || selectedPageNumbers.length === 0) return; + + console.log('Insert page breaks after selected pages:', selectedPageNumbers); + + const pageBreakCommand = new PageBreakCommand( + selectedPageNumbers, + () => displayDocument, + setEditedDocument, + setSelectedPageNumbers + ); + undoManagerRef.current.executeCommand(pageBreakCommand); + }, [selectedPageNumbers, displayDocument]); + + const handlePageBreakAll = useCallback(() => { + if (!displayDocument) return; + + const pageBreakAllCommand = new BulkPageBreakCommand( + () => displayDocument, + setEditedDocument, + setSelectedPageNumbers + ); + undoManagerRef.current.executeCommand(pageBreakAllCommand); + }, [displayDocument]); + const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => { if (!displayDocument) return; @@ -429,6 +458,8 @@ const PageEditor = ({ handleDelete, handleSplit, handleSplitAll, + handlePageBreak, + handlePageBreakAll, showExportPreview: handleExportPreview, onExportSelected, onExportAll, @@ -443,8 +474,8 @@ const PageEditor = ({ } }, [ onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll, - handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageNumbers, - splitPositions, displayDocument?.pages.length, closePdf + handlePageBreak, handlePageBreakAll, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, + selectionMode, selectedPageNumbers, splitPositions, displayDocument?.pages.length, closePdf ]); // Display all pages - use edited or original document diff --git a/frontend/src/components/pageEditor/PageEditorControls.tsx b/frontend/src/components/pageEditor/PageEditorControls.tsx index 7d053760a..ca351477a 100644 --- a/frontend/src/components/pageEditor/PageEditorControls.tsx +++ b/frontend/src/components/pageEditor/PageEditorControls.tsx @@ -12,6 +12,7 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; +import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak"; interface PageEditorControlsProps { // Close/Reset functions @@ -28,6 +29,8 @@ interface PageEditorControlsProps { onDelete: () => void; onSplit: () => void; onSplitAll: () => void; + onPageBreak: () => void; + onPageBreakAll: () => void; // Export functions onExportSelected: () => void; @@ -53,6 +56,8 @@ const PageEditorControls = ({ onDelete, onSplit, onSplitAll, + onPageBreak, + onPageBreakAll, onExportSelected, onExportAll, exportLoading, @@ -79,6 +84,16 @@ const PageEditorControls = ({ return hasAllSplits ? "Remove All Splits" : "Split All"; }; + // Calculate page break tooltip text + const getPageBreakTooltip = () => { + if (selectionMode) { + return selectedPages.length > 0 + ? `Insert ${selectedPages.length} Page Break${selectedPages.length > 1 ? 's' : ''}` + : "Insert Page Breaks"; + } + return "Insert Page Breaks After All Pages"; + }; + return (
+ + 0 ? "light" : "default"} + color={selectionMode && selectedPages.length > 0 ? "orange" : undefined} + size="lg" + > + + +
diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index 1de6a4fa4..f734e2b85 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -95,7 +95,7 @@ const PageThumbnail: React.FC = ({ // Request thumbnail generation if we have the original file if (originalFile) { const pageNumber = page.originalPageNumber; - + requestThumbnail(page.id, originalFile, pageNumber) .then(thumbnail => { if (!isCancelled && thumbnail) { @@ -116,14 +116,14 @@ const PageThumbnail: React.FC = ({ if (element) { pageRefs.current.set(page.id, element); dragElementRef.current = element; - + const dragCleanup = draggable({ element, getInitialData: () => ({ pageNumber: page.pageNumber, pageId: page.id, - selectedPages: selectionMode && selectedPages.includes(page.pageNumber) - ? selectedPages + selectedPages: selectionMode && selectedPages.includes(page.pageNumber) + ? selectedPages : [page.pageNumber] }), onDragStart: () => { @@ -131,14 +131,14 @@ const PageThumbnail: React.FC = ({ }, onDrop: ({ location }) => { setIsDragging(false); - + if (location.current.dropTargets.length === 0) { return; } - + const dropTarget = location.current.dropTargets[0]; const targetData = dropTarget.data; - + if (targetData.type === 'page') { const targetPageNumber = targetData.pageNumber as number; const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber); @@ -155,7 +155,7 @@ const PageThumbnail: React.FC = ({ }); element.style.cursor = 'grab'; - + const dropCleanup = dropTargetForElements({ element, getData: () => ({ @@ -164,7 +164,7 @@ const PageThumbnail: React.FC = ({ }), onDrop: ({ source }) => {} }); - + (element as any).__dragCleanup = () => { dragCleanup(); dropCleanup(); @@ -202,11 +202,11 @@ const PageThumbnail: React.FC = ({ const handleSplit = useCallback((e: React.MouseEvent) => { e.stopPropagation(); - + // Create a command to toggle split at this position const command = createSplitCommand(index); onExecuteCommand(command); - + const hasSplit = splitPositions.has(index); const action = hasSplit ? 'removed' : 'added'; onSetStatus(`Split marker ${action} after position ${index + 1}`); @@ -215,7 +215,7 @@ const PageThumbnail: React.FC = ({ // Handle click vs drag differentiation const handleMouseDown = useCallback((e: React.MouseEvent) => { if (!selectionMode) return; - + setIsMouseDown(true); setMouseStartPos({ x: e.clientX, y: e.clientY }); }, [selectionMode]); @@ -292,7 +292,10 @@ const PageThumbnail: React.FC = ({ boxShadow: '0 2px 4px rgba(0,0,0,0.1)', pointerEvents: 'auto' }} - onMouseDown={(e) => e.stopPropagation()} + onMouseDown={(e) => { + e.stopPropagation(); + onTogglePage(page.pageNumber); + }} onMouseUp={(e) => e.stopPropagation()} onDragStart={(e) => { e.preventDefault(); @@ -302,9 +305,10 @@ const PageThumbnail: React.FC = ({ { - // onChange is handled by the parent div click + // Selection is handled by container mouseDown }} size="sm" + style={{ pointerEvents: 'none' }} />
)} @@ -323,7 +327,14 @@ const PageThumbnail: React.FC = ({ justifyContent: 'center' }} > - {thumbnailUrl ? ( + {page.isBlankPage ? ( +
+ ) : thumbnailUrl ? ( {`Page = ({ position: 'absolute', top: 5, left: 5, - background: 'rgba(162, 201, 255, 0.8)', + background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)', padding: '6px 8px', borderRadius: 8, zIndex: 2, @@ -486,4 +497,4 @@ const PageThumbnail: React.FC = ({ ); }; -export default PageThumbnail; \ No newline at end of file +export default PageThumbnail; diff --git a/frontend/src/components/pageEditor/commands/pageCommands.ts b/frontend/src/components/pageEditor/commands/pageCommands.ts index 0cb7d0c15..a12144dcc 100644 --- a/frontend/src/components/pageEditor/commands/pageCommands.ts +++ b/frontend/src/components/pageEditor/commands/pageCommands.ts @@ -396,6 +396,147 @@ export class SplitAllCommand extends DOMCommand { } } +export class PageBreakCommand extends DOMCommand { + private insertedPages: PDFPage[] = []; + private originalDocument: PDFDocument | null = null; + + constructor( + private selectedPageNumbers: number[], + private getCurrentDocument: () => PDFDocument | null, + private setDocument: (doc: PDFDocument) => void, + private setSelectedPages: (pages: number[]) => void + ) { + super(); + } + + execute(): void { + const currentDoc = this.getCurrentDocument(); + if (!currentDoc || this.selectedPageNumbers.length === 0) return; + + // Store original state for undo + this.originalDocument = { + ...currentDoc, + pages: currentDoc.pages.map(page => ({...page})) + }; + + // Create new pages array with blank pages inserted + const newPages: PDFPage[] = []; + this.insertedPages = []; + let pageNumberCounter = 1; + + currentDoc.pages.forEach((page, index) => { + // Add the current page + const updatedPage = { ...page, pageNumber: pageNumberCounter++ }; + newPages.push(updatedPage); + + // If this page is selected for page break insertion, add a blank page after it + if (this.selectedPageNumbers.includes(page.pageNumber)) { + const blankPage: PDFPage = { + id: `blank-${Date.now()}-${index}`, + pageNumber: pageNumberCounter++, + originalPageNumber: -1, // Mark as blank page + thumbnail: null, + rotation: 0, + selected: false, + splitAfter: false, + isBlankPage: true // Custom flag for blank pages + }; + newPages.push(blankPage); + this.insertedPages.push(blankPage); + } + }); + + // Update document + const updatedDocument: PDFDocument = { + ...currentDoc, + pages: newPages, + totalPages: newPages.length, + }; + + this.setDocument(updatedDocument); + this.setSelectedPages([]); + } + + undo(): void { + if (!this.originalDocument) return; + this.setDocument(this.originalDocument); + } + + get description(): string { + return `Insert ${this.selectedPageNumbers.length} page break(s)`; + } +} + +export class BulkPageBreakCommand extends DOMCommand { + private insertedPages: PDFPage[] = []; + private originalDocument: PDFDocument | null = null; + + constructor( + private getCurrentDocument: () => PDFDocument | null, + private setDocument: (doc: PDFDocument) => void, + private setSelectedPages: (pages: number[]) => void + ) { + super(); + } + + execute(): void { + const currentDoc = this.getCurrentDocument(); + if (!currentDoc) return; + + // Store original state for undo + this.originalDocument = { + ...currentDoc, + pages: currentDoc.pages.map(page => ({...page})) + }; + + // Create new pages array with blank pages inserted after each page (except the last) + const newPages: PDFPage[] = []; + this.insertedPages = []; + let pageNumberCounter = 1; + + currentDoc.pages.forEach((page, index) => { + // Add the current page + const updatedPage = { ...page, pageNumber: pageNumberCounter++ }; + newPages.push(updatedPage); + + // Add blank page after each page except the last one + if (index < currentDoc.pages.length - 1) { + const blankPage: PDFPage = { + id: `blank-${Date.now()}-${index}`, + pageNumber: pageNumberCounter++, + originalPageNumber: -1, + thumbnail: null, + rotation: 0, + selected: false, + splitAfter: false, + isBlankPage: true + }; + newPages.push(blankPage); + this.insertedPages.push(blankPage); + } + }); + + // Update document + const updatedDocument: PDFDocument = { + ...currentDoc, + pages: newPages, + totalPages: newPages.length, + }; + + this.setDocument(updatedDocument); + this.setSelectedPages([]); + } + + undo(): void { + if (!this.originalDocument) return; + this.setDocument(this.originalDocument); + } + + get description(): string { + return `Insert page breaks after all pages`; + } +} + // Simple undo manager for DOM commands export class UndoManager { private undoStack: DOMCommand[] = []; diff --git a/frontend/src/services/documentManipulationService.ts b/frontend/src/services/documentManipulationService.ts index 9d3a64be6..f84623c6e 100644 --- a/frontend/src/services/documentManipulationService.ts +++ b/frontend/src/services/documentManipulationService.ts @@ -121,10 +121,10 @@ export class DocumentManipulationService { */ private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number { const img = pageElement.querySelector('img'); - if (img && img.style.rotate) { - // Parse rotation from DOM (e.g., "90deg" -> 90) - const rotationMatch = img.style.rotate.match(/-?\d+/); - const domRotation = rotationMatch ? parseInt(rotationMatch[0]) : 0; + if (img && img.style.transform) { + // Parse rotation from transform property (e.g., "rotate(90deg)" -> 90) + const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/); + const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0; console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`); return domRotation; @@ -146,7 +146,7 @@ export class DocumentManipulationService { const img = pageElement.querySelector('img'); if (img) { // Reset rotation to match document state - img.style.rotate = `${page.rotation}deg`; + img.style.transform = `rotate(${page.rotation}deg)`; } } }); diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts index b4f02bd5d..8eaff25cd 100644 --- a/frontend/src/services/pdfExportService.ts +++ b/frontend/src/services/pdfExportService.ts @@ -50,19 +50,29 @@ export class PDFExportService { const newDoc = await PDFLibDocument.create(); for (const page of pages) { - // Get the original page from source document using originalPageNumber - const sourcePageIndex = page.originalPageNumber - 1; - - if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { - // Copy the page - const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]); - - // Apply rotation + if (page.isBlankPage || page.originalPageNumber === -1) { + // Create a blank page + const blankPage = newDoc.addPage(PageSizes.A4); + + // Apply rotation if needed if (page.rotation !== 0) { - copiedPage.setRotation(degrees(page.rotation)); + blankPage.setRotation(degrees(page.rotation)); } + } else { + // Get the original page from source document using originalPageNumber + const sourcePageIndex = page.originalPageNumber - 1; - newDoc.addPage(copiedPage); + if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { + // Copy the page + const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]); + + // Apply rotation + if (page.rotation !== 0) { + copiedPage.setRotation(degrees(page.rotation)); + } + + newDoc.addPage(copiedPage); + } } } diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts index 9622052da..dd27ef627 100644 --- a/frontend/src/types/pageEditor.ts +++ b/frontend/src/types/pageEditor.ts @@ -6,6 +6,7 @@ export interface PDFPage { rotation: number; selected: boolean; splitAfter?: boolean; + isBlankPage?: boolean; } export interface PDFDocument {