2025-08-28 10:56:07 +01:00
import { FileId } from '../../../types/file' ;
2025-08-26 15:30:58 +01:00
import { PDFDocument , PDFPage } from '../../../types/pageEditor' ;
// V1-style DOM-first command system (replaces the old React state commands)
export abstract class DOMCommand {
abstract execute ( ) : void ;
abstract undo ( ) : void ;
abstract description : string ;
}
export class RotatePageCommand extends DOMCommand {
constructor (
private pageId : string ,
private degrees : number
) {
super ( ) ;
}
execute ( ) : void {
// Only update DOM for immediate visual feedback
const pageElement = document . querySelector ( ` [data-page-id=" ${ this . pageId } "] ` ) ;
if ( pageElement ) {
const img = pageElement . querySelector ( 'img' ) ;
if ( img ) {
// Extract current rotation from transform property to match the animated CSS
const currentTransform = img . style . transform || '' ;
const rotateMatch = currentTransform . match ( /rotate\(([^)]+)\)/ ) ;
const currentRotation = rotateMatch ? parseInt ( rotateMatch [ 1 ] ) : 0 ;
const newRotation = currentRotation + this . degrees ;
img . style . transform = ` rotate( ${ newRotation } deg) ` ;
}
}
}
undo ( ) : void {
// Only update DOM
const pageElement = document . querySelector ( ` [data-page-id=" ${ this . pageId } "] ` ) ;
if ( pageElement ) {
const img = pageElement . querySelector ( 'img' ) ;
if ( img ) {
// Extract current rotation from transform property
const currentTransform = img . style . transform || '' ;
const rotateMatch = currentTransform . match ( /rotate\(([^)]+)\)/ ) ;
const currentRotation = rotateMatch ? parseInt ( rotateMatch [ 1 ] ) : 0 ;
const previousRotation = currentRotation - this . degrees ;
img . style . transform = ` rotate( ${ previousRotation } deg) ` ;
}
}
}
get description ( ) : string {
return ` Rotate page ${ this . degrees > 0 ? 'right' : 'left' } ` ;
}
}
export class DeletePagesCommand extends DOMCommand {
private originalDocument : PDFDocument | null = null ;
private originalSplitPositions : Set < number > = new Set ( ) ;
private originalSelectedPages : number [ ] = [ ] ;
private hasExecuted : boolean = false ;
private pageIdsToDelete : string [ ] = [ ] ;
constructor (
private pagesToDelete : number [ ] ,
private getCurrentDocument : ( ) = > PDFDocument | null ,
private setDocument : ( doc : PDFDocument ) = > void ,
private setSelectedPages : ( pages : number [ ] ) = > void ,
private getSplitPositions : ( ) = > Set < number > ,
private setSplitPositions : ( positions : Set < number > ) = > void ,
private getSelectedPages : ( ) = > number [ ]
) {
super ( ) ;
}
execute ( ) : void {
const currentDoc = this . getCurrentDocument ( ) ;
if ( ! currentDoc || this . pagesToDelete . length === 0 ) return ;
// Store complete original state for undo (only on first execution)
if ( ! this . hasExecuted ) {
this . originalDocument = {
. . . currentDoc ,
pages : currentDoc.pages.map ( page = > ( { . . . page } ) ) // Deep copy pages
} ;
this . originalSplitPositions = new Set ( this . getSplitPositions ( ) ) ;
this . originalSelectedPages = [ . . . this . getSelectedPages ( ) ] ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Convert page numbers to page IDs for stable identification
this . pageIdsToDelete = this . pagesToDelete . map ( pageNum = > {
const page = currentDoc . pages . find ( p = > p . pageNumber === pageNum ) ;
return page ? . id || '' ;
} ) . filter ( id = > id ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
this . hasExecuted = true ;
}
// Filter out deleted pages by ID (stable across undo/redo)
2025-08-28 10:56:07 +01:00
const remainingPages = currentDoc . pages . filter ( page = >
2025-08-26 15:30:58 +01:00
! this . pageIdsToDelete . includes ( page . id )
) ;
if ( remainingPages . length === 0 ) return ; // Safety check
// Renumber remaining pages
remainingPages . forEach ( ( page , index ) = > {
page . pageNumber = index + 1 ;
} ) ;
// Update document
const updatedDocument : PDFDocument = {
. . . currentDoc ,
pages : remainingPages ,
totalPages : remainingPages.length ,
} ;
// Adjust split positions
const currentSplitPositions = this . getSplitPositions ( ) ;
const newPositions = new Set < number > ( ) ;
currentSplitPositions . forEach ( pos = > {
if ( pos < remainingPages . length - 1 ) {
newPositions . add ( pos ) ;
}
} ) ;
// Apply changes
this . setDocument ( updatedDocument ) ;
this . setSelectedPages ( [ ] ) ;
this . setSplitPositions ( newPositions ) ;
}
undo ( ) : void {
if ( ! this . originalDocument ) return ;
// Simply restore the complete original document state
this . setDocument ( this . originalDocument ) ;
this . setSplitPositions ( this . originalSplitPositions ) ;
this . setSelectedPages ( this . originalSelectedPages ) ;
}
get description ( ) : string {
return ` Delete ${ this . pagesToDelete . length } page(s) ` ;
}
}
export class ReorderPagesCommand extends DOMCommand {
private originalPages : PDFPage [ ] = [ ] ;
constructor (
private sourcePageNumber : number ,
private targetIndex : number ,
private selectedPages : number [ ] | undefined ,
private getCurrentDocument : ( ) = > PDFDocument | null ,
private setDocument : ( doc : PDFDocument ) = > void
) {
super ( ) ;
}
execute ( ) : void {
const currentDoc = this . getCurrentDocument ( ) ;
if ( ! currentDoc ) return ;
// Store original state for undo
this . originalPages = currentDoc . pages . map ( page = > ( { . . . page } ) ) ;
// Perform the reorder
const sourceIndex = currentDoc . pages . findIndex ( p = > p . pageNumber === this . sourcePageNumber ) ;
if ( sourceIndex === - 1 ) return ;
const newPages = [ . . . currentDoc . pages ] ;
if ( this . selectedPages && this . selectedPages . length > 1 && this . selectedPages . includes ( this . sourcePageNumber ) ) {
// Multi-page reorder
const selectedPageObjects = this . selectedPages
. map ( pageNum = > currentDoc . pages . find ( p = > p . pageNumber === pageNum ) )
. filter ( page = > page !== undefined ) as PDFPage [ ] ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
const remainingPages = newPages . filter ( page = > ! this . selectedPages ! . includes ( page . pageNumber ) ) ;
remainingPages . splice ( this . targetIndex , 0 , . . . selectedPageObjects ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
remainingPages . forEach ( ( page , index ) = > {
page . pageNumber = index + 1 ;
} ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
newPages . splice ( 0 , newPages . length , . . . remainingPages ) ;
} else {
// Single page reorder
const [ movedPage ] = newPages . splice ( sourceIndex , 1 ) ;
newPages . splice ( this . targetIndex , 0 , movedPage ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
newPages . forEach ( ( page , index ) = > {
page . pageNumber = index + 1 ;
} ) ;
}
const reorderedDocument : PDFDocument = {
. . . currentDoc ,
pages : newPages ,
totalPages : newPages.length ,
} ;
this . setDocument ( reorderedDocument ) ;
}
undo ( ) : void {
const currentDoc = this . getCurrentDocument ( ) ;
if ( ! currentDoc || this . originalPages . length === 0 ) return ;
// Restore original page order
const restoredDocument : PDFDocument = {
. . . currentDoc ,
pages : this.originalPages ,
totalPages : this.originalPages.length ,
} ;
this . setDocument ( restoredDocument ) ;
}
get description ( ) : string {
return ` Reorder page(s) ` ;
}
}
export class SplitCommand extends DOMCommand {
private originalSplitPositions : Set < number > = new Set ( ) ;
constructor (
private position : number ,
private getSplitPositions : ( ) = > Set < number > ,
private setSplitPositions : ( positions : Set < number > ) = > void
) {
super ( ) ;
}
execute ( ) : void {
// Store original state for undo
this . originalSplitPositions = new Set ( this . getSplitPositions ( ) ) ;
// Toggle the split position
const currentPositions = this . getSplitPositions ( ) ;
const newPositions = new Set ( currentPositions ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
if ( newPositions . has ( this . position ) ) {
newPositions . delete ( this . position ) ;
} else {
newPositions . add ( this . position ) ;
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
this . setSplitPositions ( newPositions ) ;
}
undo ( ) : void {
// Restore original split positions
this . setSplitPositions ( this . originalSplitPositions ) ;
}
get description ( ) : string {
const currentPositions = this . getSplitPositions ( ) ;
const willAdd = ! currentPositions . has ( this . position ) ;
return ` ${ willAdd ? 'Add' : 'Remove' } split at position ${ this . position + 1 } ` ;
}
}
export class BulkRotateCommand extends DOMCommand {
private originalRotations : Map < string , number > = new Map ( ) ;
constructor (
private pageIds : string [ ] ,
private degrees : number
) {
super ( ) ;
}
execute ( ) : void {
this . pageIds . forEach ( pageId = > {
const pageElement = document . querySelector ( ` [data-page-id=" ${ pageId } "] ` ) ;
if ( pageElement ) {
const img = pageElement . querySelector ( 'img' ) ;
if ( img ) {
// Store original rotation for undo (only on first execution)
if ( ! this . originalRotations . has ( pageId ) ) {
const currentTransform = img . style . transform || '' ;
const rotateMatch = currentTransform . match ( /rotate\(([^)]+)\)/ ) ;
const currentRotation = rotateMatch ? parseInt ( rotateMatch [ 1 ] ) : 0 ;
this . originalRotations . set ( pageId , currentRotation ) ;
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Apply rotation using transform to trigger CSS animation
const currentTransform = img . style . transform || '' ;
const rotateMatch = currentTransform . match ( /rotate\(([^)]+)\)/ ) ;
const currentRotation = rotateMatch ? parseInt ( rotateMatch [ 1 ] ) : 0 ;
const newRotation = currentRotation + this . degrees ;
img . style . transform = ` rotate( ${ newRotation } deg) ` ;
}
}
} ) ;
}
undo ( ) : void {
this . pageIds . forEach ( pageId = > {
const pageElement = document . querySelector ( ` [data-page-id=" ${ pageId } "] ` ) ;
if ( pageElement ) {
const img = pageElement . querySelector ( 'img' ) ;
if ( img && this . originalRotations . has ( pageId ) ) {
img . style . transform = ` rotate( ${ this . originalRotations . get ( pageId ) } deg) ` ;
}
}
} ) ;
}
get description ( ) : string {
return ` Rotate ${ this . pageIds . length } page(s) ${ this . degrees > 0 ? 'right' : 'left' } ` ;
}
}
export class BulkSplitCommand extends DOMCommand {
private originalSplitPositions : Set < number > = new Set ( ) ;
constructor (
private positions : number [ ] ,
private getSplitPositions : ( ) = > Set < number > ,
private setSplitPositions : ( positions : Set < number > ) = > void
) {
super ( ) ;
}
execute ( ) : void {
// Store original state for undo (only on first execution)
if ( this . originalSplitPositions . size === 0 ) {
this . originalSplitPositions = new Set ( this . getSplitPositions ( ) ) ;
}
// Toggle each position
const currentPositions = new Set ( this . getSplitPositions ( ) ) ;
this . positions . forEach ( position = > {
if ( currentPositions . has ( position ) ) {
currentPositions . delete ( position ) ;
} else {
currentPositions . add ( position ) ;
}
} ) ;
this . setSplitPositions ( currentPositions ) ;
}
undo ( ) : void {
// Restore original split positions
this . setSplitPositions ( this . originalSplitPositions ) ;
}
get description ( ) : string {
return ` Toggle ${ this . positions . length } split position(s) ` ;
}
}
export class SplitAllCommand extends DOMCommand {
private originalSplitPositions : Set < number > = new Set ( ) ;
private allPossibleSplits : Set < number > = new Set ( ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
constructor (
private totalPages : number ,
private getSplitPositions : ( ) = > Set < number > ,
private setSplitPositions : ( positions : Set < number > ) = > void
) {
super ( ) ;
// Calculate all possible split positions (between pages, not after last page)
for ( let i = 0 ; i < this . totalPages - 1 ; i ++ ) {
this . allPossibleSplits . add ( i ) ;
}
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
execute ( ) : void {
// Store original state for undo
this . originalSplitPositions = new Set ( this . getSplitPositions ( ) ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Check if all splits are already active
const currentSplits = this . getSplitPositions ( ) ;
const hasAllSplits = Array . from ( this . allPossibleSplits ) . every ( pos = > currentSplits . has ( pos ) ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
if ( hasAllSplits ) {
// Remove all splits
this . setSplitPositions ( new Set ( ) ) ;
} else {
// Add all splits
this . setSplitPositions ( this . allPossibleSplits ) ;
}
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
undo ( ) : void {
// Restore original split positions
this . setSplitPositions ( this . originalSplitPositions ) ;
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
get description ( ) : string {
const currentSplits = this . getSplitPositions ( ) ;
const hasAllSplits = Array . from ( this . allPossibleSplits ) . every ( pos = > currentSplits . has ( pos ) ) ;
return hasAllSplits ? 'Remove all splits' : 'Split all pages' ;
}
}
export class PageBreakCommand extends DOMCommand {
private insertedPages : PDFPage [ ] = [ ] ;
private originalDocument : PDFDocument | null = null ;
constructor (
private selectedPageNumbers : number [ ] ,
private getCurrentDocument : ( ) = > PDFDocument | null ,
2025-08-26 17:26:30 +01:00
private setDocument : ( doc : PDFDocument ) = > void
2025-08-26 15:30:58 +01:00
) {
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 ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 17:26:30 +01:00
// No need to maintain selection - page IDs remain stable, so selection persists automatically
2025-08-26 15:30:58 +01:00
}
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 ;
private originalSelectedPages : number [ ] = [ ] ;
constructor (
private getCurrentDocument : ( ) = > PDFDocument | null ,
private setDocument : ( doc : PDFDocument ) = > void ,
private setSelectedPages : ( pages : number [ ] ) = > void ,
private getSelectedPages : ( ) = > number [ ]
) {
super ( ) ;
}
execute ( ) : void {
const currentDoc = this . getCurrentDocument ( ) ;
if ( ! currentDoc ) return ;
// Store original selection to restore later
this . originalSelectedPages = this . getSelectedPages ( ) ;
// 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 ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Maintain existing selection by mapping original selected pages to their new positions
const updatedSelection : number [ ] = [ ] ;
this . originalSelectedPages . forEach ( originalPageNum = > {
// Find the original page by matching the page ID from the original document
const originalPage = this . originalDocument ? . pages [ originalPageNum - 1 ] ;
if ( originalPage ) {
const foundPage = newPages . find ( page = > page . id === originalPage . id && ! page . isBlankPage ) ;
if ( foundPage ) {
updatedSelection . push ( foundPage . pageNumber ) ;
}
}
} ) ;
this . setSelectedPages ( updatedSelection ) ;
}
undo ( ) : void {
if ( ! this . originalDocument ) return ;
this . setDocument ( this . originalDocument ) ;
}
get description ( ) : string {
return ` Insert page breaks after all pages ` ;
}
}
export class InsertFilesCommand extends DOMCommand {
private insertedPages : PDFPage [ ] = [ ] ;
private originalDocument : PDFDocument | null = null ;
2025-08-28 10:56:07 +01:00
private fileDataMap = new Map < FileId , ArrayBuffer > ( ) ; // Store file data for thumbnail generation
2025-08-26 15:30:58 +01:00
private originalProcessedFile : any = null ; // Store original ProcessedFile for undo
2025-08-28 10:56:07 +01:00
private insertedFileMap = new Map < FileId , File > ( ) ; // Store inserted files for export
2025-08-26 15:30:58 +01:00
constructor (
private files : File [ ] ,
private insertAfterPageNumber : number ,
private getCurrentDocument : ( ) = > PDFDocument | null ,
private setDocument : ( doc : PDFDocument ) = > void ,
private setSelectedPages : ( pages : number [ ] ) = > void ,
private getSelectedPages : ( ) = > number [ ] ,
2025-08-28 10:56:07 +01:00
private updateFileContext ? : ( updatedDocument : PDFDocument , insertedFiles? : Map < FileId , File > ) = > void
2025-08-26 15:30:58 +01:00
) {
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 [ ] = [ ] ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Process all files and wait for their completion
const baseTimestamp = Date . now ( ) ;
const extractionPromises = this . files . map ( async ( file , index ) = > {
2025-08-28 10:56:07 +01:00
const fileId = ` inserted- ${ file . name } - ${ baseTimestamp + index } ` as FileId ;
2025-08-26 15:30:58 +01:00
// 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 ) ;
} ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
const extractedPageArrays = await Promise . all ( extractionPromises ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// 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 [ ] = [ ] ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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 ) ;
}
} ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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' ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Group pages by file ID to generate thumbnails efficiently
2025-08-28 10:56:07 +01:00
const pagesByFileId = new Map < FileId , PDFPage [ ] > ( ) ;
2025-08-26 15:30:58 +01:00
for ( const page of this . insertedPages ) {
2025-08-28 10:56:07 +01:00
const fileId = page . id . substring ( 0 , page . id . lastIndexOf ( '-page-' ) ) as FileId /* FIX ME: This looks wrong - like we've thrown away info too early and need to recreate it */ ;
2025-08-26 15:30:58 +01:00
if ( ! pagesByFileId . has ( fileId ) ) {
pagesByFileId . set ( fileId , [ ] ) ;
}
pagesByFileId . get ( fileId ) ! . push ( page ) ;
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Generate thumbnails for each file
for ( const [ fileId , pages ] of pagesByFileId ) {
const arrayBuffer = this . fileDataMap . get ( fileId ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
console . log ( 'Generating thumbnails for file:' , fileId ) ;
console . log ( 'Pages:' , pages . length ) ;
console . log ( 'ArrayBuffer size:' , arrayBuffer ? . byteLength || 'undefined' ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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 ;
} ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
console . log ( 'Generating thumbnails for page numbers:' , pageNumbers ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Generate thumbnails for all pages from this file at once
const results = await thumbnailGenerationService . generateThumbnails (
fileId ,
arrayBuffer ,
pageNumbers ,
{ scale : 0.2 , quality : 0.8 }
) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
console . log ( 'Thumbnail generation results:' , results . length , 'thumbnails generated' ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Update pages with generated thumbnails
for ( let i = 0 ; i < results . length && i < pages . length ; i ++ ) {
const result = results [ i ] ;
const page = pages [ i ] ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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 ) ;
}
}
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// 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' ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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 ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Use PDF.js via the worker manager to extract pages
const { pdfWorkerManager } = await import ( '../../../services/pdfWorkerManager' ) ;
const pdf = await pdfWorkerManager . createDocument ( clonedArrayBuffer ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
const pageCount = pdf . numPages ;
const pages : PDFPage [ ] = [ ] ;
2025-08-28 10:56:07 +01:00
const fileId = ` inserted- ${ file . name } - ${ baseTimestamp } ` as FileId ;
2025-08-26 15:30:58 +01:00
console . log ( 'Original ArrayBuffer size:' , arrayBuffer . byteLength ) ;
console . log ( 'Storing ArrayBuffer for fileId:' , fileId , 'size:' , arrayBuffer . byteLength ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Store the original ArrayBuffer for thumbnail generation
this . fileDataMap . set ( fileId , arrayBuffer ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
console . log ( 'After storing - fileDataMap size:' , this . fileDataMap . size ) ;
console . log ( 'Stored value size:' , this . fileDataMap . get ( fileId ) ? . byteLength || 'undefined' ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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
} ) ;
}
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
// Clean up PDF document
pdfWorkerManager . destroyDocument ( pdf ) ;
2025-08-28 10:56:07 +01:00
2025-08-26 15:30:58 +01:00
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
export class UndoManager {
private undoStack : DOMCommand [ ] = [ ] ;
private redoStack : DOMCommand [ ] = [ ] ;
private onStateChange ? : ( ) = > void ;
setStateChangeCallback ( callback : ( ) = > void ) : void {
this . onStateChange = callback ;
}
executeCommand ( command : DOMCommand ) : void {
command . execute ( ) ;
this . undoStack . push ( command ) ;
this . redoStack = [ ] ;
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 {
const command = this . undoStack . pop ( ) ;
if ( command ) {
command . undo ( ) ;
this . redoStack . push ( command ) ;
this . onStateChange ? . ( ) ;
return true ;
}
return false ;
}
redo ( ) : boolean {
const command = this . redoStack . pop ( ) ;
if ( command ) {
command . execute ( ) ;
this . undoStack . push ( command ) ;
this . onStateChange ? . ( ) ;
return true ;
}
return false ;
}
canUndo ( ) : boolean {
return this . undoStack . length > 0 ;
}
canRedo ( ) : boolean {
return this . redoStack . length > 0 ;
}
clear ( ) : void {
this . undoStack = [ ] ;
this . redoStack = [ ] ;
this . onStateChange ? . ( ) ;
}
2025-08-28 10:56:07 +01:00
}