mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Refactor
This commit is contained in:
parent
62f92da0fe
commit
7cf0806749
@ -13,7 +13,9 @@
|
|||||||
"Bash(npx tsc:*)",
|
"Bash(npx tsc:*)",
|
||||||
"Bash(node:*)",
|
"Bash(node:*)",
|
||||||
"Bash(npm run dev:*)",
|
"Bash(npm run dev:*)",
|
||||||
"Bash(sed:*)"
|
"Bash(sed:*)",
|
||||||
|
"Bash(cp:*)",
|
||||||
|
"Bash(rm:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"defaultMode": "acceptEdits"
|
"defaultMode": "acceptEdits"
|
||||||
|
@ -9,426 +9,28 @@ import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "
|
|||||||
import { ModeType } from "../../contexts/NavigationContext";
|
import { ModeType } from "../../contexts/NavigationContext";
|
||||||
import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
||||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||||
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessingService";
|
|
||||||
import { fileProcessingService } from "../../services/fileProcessingService";
|
|
||||||
import { pdfProcessingService } from "../../services/pdfProcessingService";
|
|
||||||
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
|
||||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||||
import { fileStorage } from "../../services/fileStorage";
|
|
||||||
import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager";
|
|
||||||
import './PageEditor.module.css';
|
import './PageEditor.module.css';
|
||||||
import PageThumbnail from './PageThumbnail';
|
import PageThumbnail from './PageThumbnail';
|
||||||
import DragDropGrid from './DragDropGrid';
|
import DragDropGrid from './DragDropGrid';
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||||
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
||||||
|
|
||||||
// V1-style DOM-first command system (replaces the old React state commands)
|
import {
|
||||||
abstract class DOMCommand {
|
DOMCommand,
|
||||||
abstract execute(): void;
|
RotatePageCommand,
|
||||||
abstract undo(): void;
|
DeletePagesCommand,
|
||||||
abstract description: string;
|
ReorderPagesCommand,
|
||||||
}
|
SplitCommand,
|
||||||
|
BulkRotateCommand,
|
||||||
class RotatePageCommand extends DOMCommand {
|
BulkSplitCommand,
|
||||||
constructor(
|
SplitAllCommand,
|
||||||
private pageId: string,
|
UndoManager
|
||||||
private degrees: number
|
} from './commands/pageCommands';
|
||||||
) {
|
import { usePageDocument } from './hooks/usePageDocument';
|
||||||
super();
|
import { usePageEditorState } from './hooks/usePageEditorState';
|
||||||
}
|
|
||||||
|
|
||||||
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'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()];
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
this.hasExecuted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out deleted pages by ID (stable across undo/redo)
|
|
||||||
const remainingPages = currentDoc.pages.filter(page =>
|
|
||||||
!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)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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[];
|
|
||||||
|
|
||||||
const remainingPages = newPages.filter(page => !this.selectedPages!.includes(page.pageNumber));
|
|
||||||
remainingPages.splice(this.targetIndex, 0, ...selectedPageObjects);
|
|
||||||
|
|
||||||
remainingPages.forEach((page, index) => {
|
|
||||||
page.pageNumber = index + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
newPages.splice(0, newPages.length, ...remainingPages);
|
|
||||||
} else {
|
|
||||||
// Single page reorder
|
|
||||||
const [movedPage] = newPages.splice(sourceIndex, 1);
|
|
||||||
newPages.splice(this.targetIndex, 0, movedPage);
|
|
||||||
|
|
||||||
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)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (newPositions.has(this.position)) {
|
|
||||||
newPositions.delete(this.position);
|
|
||||||
} else {
|
|
||||||
newPositions.add(this.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple undo manager for DOM commands
|
|
||||||
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?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageEditorProps {
|
export interface PageEditorProps {
|
||||||
onFunctionsReady?: (functions: {
|
onFunctionsReady?: (functions: {
|
||||||
@ -485,127 +87,20 @@ const PageEditor = ({
|
|||||||
// DOM-first undo manager (replaces the old React state undo system)
|
// DOM-first undo manager (replaces the old React state undo system)
|
||||||
const undoManagerRef = useRef(new UndoManager());
|
const undoManagerRef = useRef(new UndoManager());
|
||||||
|
|
||||||
// Thumbnail generation is now handled on-demand by individual PageThumbnail components using modern services
|
// Document state management
|
||||||
|
const { document: mergedPdfDocument, isVeryLargeDocument, isLoading: documentLoading } = usePageDocument();
|
||||||
|
|
||||||
|
|
||||||
// Get primary file record outside useMemo to track processedFile changes
|
// UI state management
|
||||||
const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
|
const {
|
||||||
const processedFilePages = primaryFileRecord?.processedFile?.pages;
|
selectionMode, selectedPageNumbers, movingPage, isAnimating, splitPositions, exportLoading,
|
||||||
const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages;
|
setSelectionMode, setSelectedPageNumbers, setMovingPage, setIsAnimating, setSplitPositions, setExportLoading,
|
||||||
|
togglePage, toggleSelectAll, animateReorder
|
||||||
// Compute merged document with stable signature (prevents infinite loops)
|
} = usePageEditorState();
|
||||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
|
||||||
if (activeFileIds.length === 0) return null;
|
|
||||||
|
|
||||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
|
||||||
|
|
||||||
// If we have file IDs but no file record, something is wrong - return null to show loading
|
|
||||||
if (!primaryFileRecord) {
|
|
||||||
console.log('🎬 PageEditor: No primary file record found, showing loading');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name =
|
|
||||||
activeFileIds.length === 1
|
|
||||||
? (primaryFileRecord.name ?? 'document.pdf')
|
|
||||||
: activeFileIds
|
|
||||||
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
|
||||||
.join(' + ');
|
|
||||||
|
|
||||||
// Debug logging for merged document creation
|
|
||||||
console.log(`🎬 PageEditor: Building merged document for ${name} with ${activeFileIds.length} files`);
|
|
||||||
|
|
||||||
// Collect pages from ALL active files, not just the primary file
|
|
||||||
let pages: PDFPage[] = [];
|
|
||||||
let totalPageCount = 0;
|
|
||||||
|
|
||||||
activeFileIds.forEach((fileId, fileIndex) => {
|
|
||||||
const fileRecord = selectors.getFileRecord(fileId);
|
|
||||||
if (!fileRecord) {
|
|
||||||
console.warn(`🎬 PageEditor: No record found for file ${fileId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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[] = [];
|
|
||||||
|
|
||||||
if (processedFile?.pages && processedFile.pages.length > 0) {
|
|
||||||
// Use fully processed pages with thumbnails
|
|
||||||
filePages = processedFile.pages.map((page, pageIndex) => ({
|
|
||||||
id: `${fileId}-${page.pageNumber}`,
|
|
||||||
pageNumber: totalPageCount + pageIndex + 1,
|
|
||||||
thumbnail: page.thumbnail || null,
|
|
||||||
rotation: page.rotation || 0,
|
|
||||||
selected: false,
|
|
||||||
splitAfter: page.splitAfter || false,
|
|
||||||
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
|
|
||||||
originalFileId: fileId,
|
|
||||||
}));
|
|
||||||
} else if (processedFile?.totalPages) {
|
|
||||||
// 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) => ({
|
|
||||||
id: `${fileId}-${pageIndex + 1}`,
|
|
||||||
pageNumber: totalPageCount + pageIndex + 1,
|
|
||||||
originalPageNumber: pageIndex + 1,
|
|
||||||
originalFileId: fileId,
|
|
||||||
rotation: 0,
|
|
||||||
thumbnail: null, // Will be generated later
|
|
||||||
selected: false,
|
|
||||||
splitAfter: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
pages = pages.concat(filePages);
|
|
||||||
totalPageCount += filePages.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pages.length === 0) {
|
|
||||||
console.warn('🎬 PageEditor: No pages found in any files');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🎬 PageEditor: Created merged document with ${pages.length} total pages`);
|
|
||||||
|
|
||||||
const mergedDoc: PDFDocument = {
|
|
||||||
id: activeFileIds.join('-'),
|
|
||||||
name,
|
|
||||||
file: primaryFile!,
|
|
||||||
pages,
|
|
||||||
totalPages: pages.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergedDoc;
|
|
||||||
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
|
||||||
|
|
||||||
// Large document detection for smart loading
|
|
||||||
const isVeryLargeDocument = useMemo(() => {
|
|
||||||
return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
|
|
||||||
}, [mergedPdfDocument?.totalPages]);
|
|
||||||
|
|
||||||
// Thumbnails are now generated on-demand by PageThumbnail components
|
|
||||||
// No bulk generation needed - modern thumbnail service handles this efficiently
|
|
||||||
|
|
||||||
// Selection and UI state management
|
|
||||||
const [selectionMode, setSelectionMode] = useState(false);
|
|
||||||
const [selectedPageNumbers, setSelectedPageNumbers] = useState<number[]>([]);
|
|
||||||
const [movingPage, setMovingPage] = useState<number | null>(null);
|
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
|
||||||
|
|
||||||
// Position-based split tracking (replaces page-based splitAfter)
|
|
||||||
const [splitPositions, setSplitPositions] = useState<Set<number>>(new Set());
|
|
||||||
|
|
||||||
// Grid container ref for positioning split indicators
|
// Grid container ref for positioning split indicators
|
||||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Export state
|
|
||||||
const [exportLoading, setExportLoading] = useState(false);
|
|
||||||
|
|
||||||
// Undo/Redo state
|
// Undo/Redo state
|
||||||
const [canUndo, setCanUndo] = useState(false);
|
const [canUndo, setCanUndo] = useState(false);
|
||||||
const [canRedo, setCanRedo] = useState(false);
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
@ -624,53 +119,28 @@ const PageEditor = ({
|
|||||||
}, [updateUndoRedoState]);
|
}, [updateUndoRedoState]);
|
||||||
|
|
||||||
|
|
||||||
|
// Interface functions for parent component
|
||||||
|
const displayDocument = editedDocument || mergedPdfDocument;
|
||||||
|
|
||||||
// DOM-first command handlers
|
// DOM-first command handlers
|
||||||
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
||||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Page selection handlers
|
// Command factory functions for PageThumbnail
|
||||||
const togglePage = useCallback((pageNumber: number) => {
|
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
|
||||||
setSelectedPageNumbers(prev =>
|
execute: () => {
|
||||||
prev.includes(pageNumber)
|
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||||
? prev.filter(n => n !== pageNumber)
|
|
||||||
: [...prev, pageNumber]
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleSelectAll = useCallback(() => {
|
|
||||||
if (!mergedPdfDocument) return;
|
|
||||||
|
|
||||||
const allPageNumbers = mergedPdfDocument.pages.map(p => p.pageNumber);
|
|
||||||
setSelectedPageNumbers(prev =>
|
|
||||||
prev.length === allPageNumbers.length ? [] : allPageNumbers
|
|
||||||
);
|
|
||||||
}, [mergedPdfDocument]);
|
|
||||||
|
|
||||||
|
|
||||||
// Animation helpers
|
|
||||||
const animateReorder = useCallback(() => {
|
|
||||||
setIsAnimating(true);
|
|
||||||
setTimeout(() => setIsAnimating(false), 500);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Placeholder command classes for PageThumbnail compatibility
|
|
||||||
class RotatePagesCommand {
|
|
||||||
constructor(public pageIds: string[], public rotation: number) {}
|
|
||||||
execute() {
|
|
||||||
const bulkRotateCommand = new BulkRotateCommand(this.pageIds, this.rotation);
|
|
||||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||||
}
|
}
|
||||||
}
|
}), []);
|
||||||
|
|
||||||
class DeletePagesWrapper {
|
const createDeleteCommand = useCallback((pageIds: string[]) => ({
|
||||||
constructor(public pageIds: string[]) {}
|
execute: () => {
|
||||||
execute() {
|
|
||||||
// Convert page IDs to page numbers for the real delete command
|
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
const pagesToDelete = this.pageIds.map(pageId => {
|
const pagesToDelete = pageIds.map(pageId => {
|
||||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||||
return page?.pageNumber || 0;
|
return page?.pageNumber || 0;
|
||||||
}).filter(num => num > 0);
|
}).filter(num => num > 0);
|
||||||
@ -688,19 +158,18 @@ const PageEditor = ({
|
|||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
undoManagerRef.current.executeCommand(deleteCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}), [displayDocument, splitPositions, selectedPageNumbers]);
|
||||||
|
|
||||||
class ToggleSplitCommand {
|
const createSplitCommand = useCallback((position: number) => ({
|
||||||
constructor(public position: number) {}
|
execute: () => {
|
||||||
execute() {
|
|
||||||
const splitCommand = new SplitCommand(
|
const splitCommand = new SplitCommand(
|
||||||
this.position,
|
position,
|
||||||
() => splitPositions,
|
() => splitPositions,
|
||||||
setSplitPositions
|
setSplitPositions
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(splitCommand);
|
undoManagerRef.current.executeCommand(splitCommand);
|
||||||
}
|
}
|
||||||
}
|
}), [splitPositions]);
|
||||||
|
|
||||||
// Command executor for PageThumbnail
|
// Command executor for PageThumbnail
|
||||||
const executeCommand = useCallback((command: any) => {
|
const executeCommand = useCallback((command: any) => {
|
||||||
@ -709,9 +178,6 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Interface functions for parent component
|
|
||||||
const displayDocument = editedDocument || mergedPdfDocument;
|
|
||||||
|
|
||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
undoManagerRef.current.undo();
|
undoManagerRef.current.undo();
|
||||||
@ -792,47 +258,11 @@ const PageEditor = ({
|
|||||||
const handleSplitAll = useCallback(() => {
|
const handleSplitAll = useCallback(() => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
// Create a command that toggles all splits
|
const splitAllCommand = new SplitAllCommand(
|
||||||
class SplitAllCommand extends DOMCommand {
|
displayDocument.pages.length,
|
||||||
private originalSplitPositions: Set<number> = new Set();
|
() => splitPositions,
|
||||||
private allPossibleSplits: Set<number> = new Set();
|
setSplitPositions
|
||||||
|
);
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// Calculate all possible split positions
|
|
||||||
for (let i = 0; i < displayDocument!.pages.length - 1; i++) {
|
|
||||||
this.allPossibleSplits.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
// Store original state for undo
|
|
||||||
this.originalSplitPositions = new Set(splitPositions);
|
|
||||||
|
|
||||||
// Check if all splits are already active
|
|
||||||
const hasAllSplits = Array.from(this.allPossibleSplits).every(pos => splitPositions.has(pos));
|
|
||||||
|
|
||||||
if (hasAllSplits) {
|
|
||||||
// Remove all splits
|
|
||||||
setSplitPositions(new Set());
|
|
||||||
} else {
|
|
||||||
// Add all splits
|
|
||||||
setSplitPositions(this.allPossibleSplits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
// Restore original split positions
|
|
||||||
setSplitPositions(this.originalSplitPositions);
|
|
||||||
}
|
|
||||||
|
|
||||||
get description(): string {
|
|
||||||
const hasAllSplits = Array.from(this.allPossibleSplits).every(pos => splitPositions.has(pos));
|
|
||||||
return hasAllSplits ? 'Remove all splits' : 'Split all pages';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitAllCommand = new SplitAllCommand();
|
|
||||||
undoManagerRef.current.executeCommand(splitAllCommand);
|
undoManagerRef.current.executeCommand(splitAllCommand);
|
||||||
}, [displayDocument, splitPositions]);
|
}, [displayDocument, splitPositions]);
|
||||||
|
|
||||||
@ -1059,7 +489,7 @@ const PageEditor = ({
|
|||||||
</Button>
|
</Button>
|
||||||
{selectionMode && (
|
{selectionMode && (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={toggleSelectAll}>
|
<Button variant="outline" onClick={() => toggleSelectAll(displayDocument?.pages.length || 0)}>
|
||||||
{selectedPageNumbers.length === displayDocument.pages.length ? "Deselect All" : "Select All"}
|
{selectedPageNumbers.length === displayDocument.pages.length ? "Deselect All" : "Select All"}
|
||||||
</Button>
|
</Button>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
@ -1139,9 +569,9 @@ const PageEditor = ({
|
|||||||
onSetStatus={() => {}}
|
onSetStatus={() => {}}
|
||||||
onSetMovingPage={setMovingPage}
|
onSetMovingPage={setMovingPage}
|
||||||
onDeletePage={handleDeletePage}
|
onDeletePage={handleDeletePage}
|
||||||
RotatePagesCommand={RotatePagesCommand}
|
createRotateCommand={createRotateCommand}
|
||||||
DeletePagesCommand={DeletePagesWrapper}
|
createDeleteCommand={createDeleteCommand}
|
||||||
ToggleSplitCommand={ToggleSplitCommand}
|
createSplitCommand={createSplitCommand}
|
||||||
pdfDocument={displayDocument}
|
pdfDocument={displayDocument}
|
||||||
setPdfDocument={setEditedDocument}
|
setPdfDocument={setEditedDocument}
|
||||||
splitPositions={splitPositions}
|
splitPositions={splitPositions}
|
||||||
|
@ -11,12 +11,6 @@ import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
|||||||
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
|
||||||
import styles from './PageEditor.module.css';
|
import styles from './PageEditor.module.css';
|
||||||
|
|
||||||
// DOM Command types (match what PageEditor expects)
|
|
||||||
abstract class DOMCommand {
|
|
||||||
abstract execute(): void;
|
|
||||||
abstract undo(): void;
|
|
||||||
abstract description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageThumbnailProps {
|
interface PageThumbnailProps {
|
||||||
page: PDFPage;
|
page: PDFPage;
|
||||||
@ -31,13 +25,13 @@ interface PageThumbnailProps {
|
|||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
||||||
onTogglePage: (pageNumber: number) => void;
|
onTogglePage: (pageNumber: number) => void;
|
||||||
onAnimateReorder: () => void;
|
onAnimateReorder: () => void;
|
||||||
onExecuteCommand: (command: DOMCommand) => void;
|
onExecuteCommand: (command: { execute: () => void }) => void;
|
||||||
onSetStatus: (status: string) => void;
|
onSetStatus: (status: string) => void;
|
||||||
onSetMovingPage: (page: number | null) => void;
|
onSetMovingPage: (page: number | null) => void;
|
||||||
onDeletePage: (pageNumber: number) => void;
|
onDeletePage: (pageNumber: number) => void;
|
||||||
RotatePagesCommand: any;
|
createRotateCommand: (pageIds: string[], rotation: number) => { execute: () => void };
|
||||||
DeletePagesCommand: any;
|
createDeleteCommand: (pageIds: string[]) => { execute: () => void };
|
||||||
ToggleSplitCommand: any;
|
createSplitCommand: (position: number) => { execute: () => void };
|
||||||
pdfDocument: PDFDocument;
|
pdfDocument: PDFDocument;
|
||||||
setPdfDocument: (doc: PDFDocument) => void;
|
setPdfDocument: (doc: PDFDocument) => void;
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<number>;
|
||||||
@ -60,18 +54,18 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
onSetStatus,
|
onSetStatus,
|
||||||
onSetMovingPage,
|
onSetMovingPage,
|
||||||
onDeletePage,
|
onDeletePage,
|
||||||
RotatePagesCommand,
|
createRotateCommand,
|
||||||
DeletePagesCommand,
|
createDeleteCommand,
|
||||||
ToggleSplitCommand,
|
createSplitCommand,
|
||||||
pdfDocument,
|
pdfDocument,
|
||||||
setPdfDocument,
|
setPdfDocument,
|
||||||
splitPositions,
|
splitPositions,
|
||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||||
|
|
||||||
// Update thumbnail URL when page prop changes
|
// Update thumbnail URL when page prop changes
|
||||||
@ -79,9 +73,9 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
||||||
setThumbnailUrl(page.thumbnail);
|
setThumbnailUrl(page.thumbnail);
|
||||||
}
|
}
|
||||||
}, [page.thumbnail, page.id]);
|
}, [page.thumbnail, thumbnailUrl]);
|
||||||
|
|
||||||
// Request thumbnail on-demand using modern service
|
// Request thumbnail if missing (on-demand, virtualized approach)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCancelled = false;
|
let isCancelled = false;
|
||||||
|
|
||||||
@ -100,8 +94,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
|
|
||||||
// Request thumbnail generation if we have the original file
|
// Request thumbnail generation if we have the original file
|
||||||
if (originalFile) {
|
if (originalFile) {
|
||||||
// Extract page number from page.id (format: fileId-pageNumber)
|
const pageNumber = page.originalPageNumber;
|
||||||
const pageNumber = parseInt(page.id.split('-').pop() || '1');
|
|
||||||
|
|
||||||
requestThumbnail(page.id, originalFile, pageNumber)
|
requestThumbnail(page.id, originalFile, pageNumber)
|
||||||
.then(thumbnail => {
|
.then(thumbnail => {
|
||||||
@ -153,6 +146,8 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const pagesToMove = selectionMode && selectedPages.includes(page.pageNumber)
|
const pagesToMove = selectionMode && selectedPages.includes(page.pageNumber)
|
||||||
? selectedPages
|
? selectedPages
|
||||||
: undefined;
|
: undefined;
|
||||||
|
// Trigger animation for drag & drop
|
||||||
|
onAnimateReorder();
|
||||||
onReorderPages(page.pageNumber, targetIndex, pagesToMove);
|
onReorderPages(page.pageNumber, targetIndex, pagesToMove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,18 +181,18 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
|
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Use the command system for undo/redo support
|
// Use the command system for undo/redo support
|
||||||
const command = new RotatePagesCommand([page.id], -90);
|
const command = createRotateCommand([page.id], -90);
|
||||||
onExecuteCommand(command);
|
onExecuteCommand(command);
|
||||||
onSetStatus(`Rotated page ${page.pageNumber} left`);
|
onSetStatus(`Rotated page ${page.pageNumber} left`);
|
||||||
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, RotatePagesCommand]);
|
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, createRotateCommand]);
|
||||||
|
|
||||||
const handleRotateRight = useCallback((e: React.MouseEvent) => {
|
const handleRotateRight = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Use the command system for undo/redo support
|
// Use the command system for undo/redo support
|
||||||
const command = new RotatePagesCommand([page.id], 90);
|
const command = createRotateCommand([page.id], 90);
|
||||||
onExecuteCommand(command);
|
onExecuteCommand(command);
|
||||||
onSetStatus(`Rotated page ${page.pageNumber} right`);
|
onSetStatus(`Rotated page ${page.pageNumber} right`);
|
||||||
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, RotatePagesCommand]);
|
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, createRotateCommand]);
|
||||||
|
|
||||||
const handleDelete = useCallback((e: React.MouseEvent) => {
|
const handleDelete = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -209,13 +204,13 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Create a command to toggle split at this position
|
// Create a command to toggle split at this position
|
||||||
const command = new ToggleSplitCommand(index);
|
const command = createSplitCommand(index);
|
||||||
onExecuteCommand(command);
|
onExecuteCommand(command);
|
||||||
|
|
||||||
const hasSplit = splitPositions.has(index);
|
const hasSplit = splitPositions.has(index);
|
||||||
const action = hasSplit ? 'removed' : 'added';
|
const action = hasSplit ? 'removed' : 'added';
|
||||||
onSetStatus(`Split marker ${action} after position ${index + 1}`);
|
onSetStatus(`Split marker ${action} after position ${index + 1}`);
|
||||||
}, [index, splitPositions, onExecuteCommand, onSetStatus, ToggleSplitCommand]);
|
}, [index, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
|
||||||
|
|
||||||
// Handle click vs drag differentiation
|
// Handle click vs drag differentiation
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
@ -254,8 +249,8 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={pageElementRef}
|
ref={pageElementRef}
|
||||||
data-page-number={page.pageNumber}
|
|
||||||
data-page-id={page.id}
|
data-page-id={page.id}
|
||||||
|
data-page-number={page.pageNumber}
|
||||||
className={`
|
className={`
|
||||||
${styles.pageContainer}
|
${styles.pageContainer}
|
||||||
!rounded-lg
|
!rounded-lg
|
||||||
@ -402,8 +397,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (index > 0 && !movingPage && !isAnimating) {
|
if (index > 0 && !movingPage && !isAnimating) {
|
||||||
onSetMovingPage(page.pageNumber);
|
onSetMovingPage(page.pageNumber);
|
||||||
|
// Trigger animation
|
||||||
onAnimateReorder();
|
onAnimateReorder();
|
||||||
setTimeout(() => onSetMovingPage(null), 500);
|
// Actually move the page left (swap with previous page)
|
||||||
|
onReorderPages(page.pageNumber, index - 1);
|
||||||
|
setTimeout(() => onSetMovingPage(null), 650);
|
||||||
onSetStatus(`Moved page ${page.pageNumber} left`);
|
onSetStatus(`Moved page ${page.pageNumber} left`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -422,8 +420,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (index < totalPages - 1 && !movingPage && !isAnimating) {
|
if (index < totalPages - 1 && !movingPage && !isAnimating) {
|
||||||
onSetMovingPage(page.pageNumber);
|
onSetMovingPage(page.pageNumber);
|
||||||
|
// Trigger animation
|
||||||
onAnimateReorder();
|
onAnimateReorder();
|
||||||
setTimeout(() => onSetMovingPage(null), 500);
|
// Actually move the page right (swap with next page)
|
||||||
|
onReorderPages(page.pageNumber, index + 1);
|
||||||
|
setTimeout(() => onSetMovingPage(null), 650);
|
||||||
onSetStatus(`Moved page ${page.pageNumber} right`);
|
onSetStatus(`Moved page ${page.pageNumber} right`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
451
frontend/src/components/pageEditor/commands/pageCommands.ts
Normal file
451
frontend/src/components/pageEditor/commands/pageCommands.ts
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
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()];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
this.hasExecuted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out deleted pages by ID (stable across undo/redo)
|
||||||
|
const remainingPages = currentDoc.pages.filter(page =>
|
||||||
|
!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[];
|
||||||
|
|
||||||
|
const remainingPages = newPages.filter(page => !this.selectedPages!.includes(page.pageNumber));
|
||||||
|
remainingPages.splice(this.targetIndex, 0, ...selectedPageObjects);
|
||||||
|
|
||||||
|
remainingPages.forEach((page, index) => {
|
||||||
|
page.pageNumber = index + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
newPages.splice(0, newPages.length, ...remainingPages);
|
||||||
|
} else {
|
||||||
|
// Single page reorder
|
||||||
|
const [movedPage] = newPages.splice(sourceIndex, 1);
|
||||||
|
newPages.splice(this.targetIndex, 0, movedPage);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (newPositions.has(this.position)) {
|
||||||
|
newPositions.delete(this.position);
|
||||||
|
} else {
|
||||||
|
newPositions.add(this.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): void {
|
||||||
|
// Store original state for undo
|
||||||
|
this.originalSplitPositions = new Set(this.getSplitPositions());
|
||||||
|
|
||||||
|
// Check if all splits are already active
|
||||||
|
const currentSplits = this.getSplitPositions();
|
||||||
|
const hasAllSplits = Array.from(this.allPossibleSplits).every(pos => currentSplits.has(pos));
|
||||||
|
|
||||||
|
if (hasAllSplits) {
|
||||||
|
// Remove all splits
|
||||||
|
this.setSplitPositions(new Set());
|
||||||
|
} else {
|
||||||
|
// Add all splits
|
||||||
|
this.setSplitPositions(this.allPossibleSplits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
// Restore original split positions
|
||||||
|
this.setSplitPositions(this.originalSplitPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.();
|
||||||
|
}
|
||||||
|
}
|
136
frontend/src/components/pageEditor/hooks/usePageDocument.ts
Normal file
136
frontend/src/components/pageEditor/hooks/usePageDocument.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useFileState } from '../../../contexts/FileContext';
|
||||||
|
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
|
||||||
|
|
||||||
|
export interface PageDocumentHook {
|
||||||
|
document: PDFDocument | null;
|
||||||
|
isVeryLargeDocument: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing PDF document state and metadata in PageEditor
|
||||||
|
* Handles document merging, large document detection, and loading states
|
||||||
|
*/
|
||||||
|
export function usePageDocument(): PageDocumentHook {
|
||||||
|
const { state, selectors } = useFileState();
|
||||||
|
|
||||||
|
// Prefer IDs + selectors to avoid array identity churn
|
||||||
|
const activeFileIds = state.files.ids;
|
||||||
|
const primaryFileId = activeFileIds[0] ?? null;
|
||||||
|
|
||||||
|
// Stable signature for effects (prevents loops)
|
||||||
|
const filesSignature = selectors.getFilesSignature();
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const globalProcessing = state.ui.isProcessing;
|
||||||
|
|
||||||
|
// Get primary file record outside useMemo to track processedFile changes
|
||||||
|
const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
|
||||||
|
const processedFilePages = primaryFileRecord?.processedFile?.pages;
|
||||||
|
const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages;
|
||||||
|
|
||||||
|
// Compute merged document with stable signature (prevents infinite loops)
|
||||||
|
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||||
|
if (activeFileIds.length === 0) return null;
|
||||||
|
|
||||||
|
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||||
|
|
||||||
|
// If we have file IDs but no file record, something is wrong - return null to show loading
|
||||||
|
if (!primaryFileRecord) {
|
||||||
|
console.log('🎬 PageEditor: No primary file record found, showing loading');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name =
|
||||||
|
activeFileIds.length === 1
|
||||||
|
? (primaryFileRecord.name ?? 'document.pdf')
|
||||||
|
: activeFileIds
|
||||||
|
.map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
|
||||||
|
.join(' + ');
|
||||||
|
|
||||||
|
// Debug logging for merged document creation
|
||||||
|
console.log(`🎬 PageEditor: Building merged document for ${name} with ${activeFileIds.length} files`);
|
||||||
|
|
||||||
|
// Collect pages from ALL active files, not just the primary file
|
||||||
|
let pages: PDFPage[] = [];
|
||||||
|
let totalPageCount = 0;
|
||||||
|
|
||||||
|
activeFileIds.forEach((fileId, fileIndex) => {
|
||||||
|
const fileRecord = selectors.getFileRecord(fileId);
|
||||||
|
if (!fileRecord) {
|
||||||
|
console.warn(`🎬 PageEditor: No record found for file ${fileId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
if (processedFile?.pages && processedFile.pages.length > 0) {
|
||||||
|
// Use fully processed pages with thumbnails
|
||||||
|
filePages = processedFile.pages.map((page, pageIndex) => ({
|
||||||
|
id: `${fileId}-${page.pageNumber}`,
|
||||||
|
pageNumber: totalPageCount + pageIndex + 1,
|
||||||
|
thumbnail: page.thumbnail || null,
|
||||||
|
rotation: page.rotation || 0,
|
||||||
|
selected: false,
|
||||||
|
splitAfter: page.splitAfter || false,
|
||||||
|
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
|
||||||
|
originalFileId: fileId,
|
||||||
|
}));
|
||||||
|
} else if (processedFile?.totalPages) {
|
||||||
|
// 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) => ({
|
||||||
|
id: `${fileId}-${pageIndex + 1}`,
|
||||||
|
pageNumber: totalPageCount + pageIndex + 1,
|
||||||
|
originalPageNumber: pageIndex + 1,
|
||||||
|
originalFileId: fileId,
|
||||||
|
rotation: 0,
|
||||||
|
thumbnail: null, // Will be generated later
|
||||||
|
selected: false,
|
||||||
|
splitAfter: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pages = pages.concat(filePages);
|
||||||
|
totalPageCount += filePages.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
console.warn('🎬 PageEditor: No pages found in any files');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎬 PageEditor: Created merged document with ${pages.length} total pages`);
|
||||||
|
|
||||||
|
const mergedDoc: PDFDocument = {
|
||||||
|
id: activeFileIds.join('-'),
|
||||||
|
name,
|
||||||
|
file: primaryFile!,
|
||||||
|
pages,
|
||||||
|
totalPages: pages.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return mergedDoc;
|
||||||
|
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
||||||
|
|
||||||
|
// Large document detection for smart loading
|
||||||
|
const isVeryLargeDocument = useMemo(() => {
|
||||||
|
return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
|
||||||
|
}, [mergedPdfDocument?.totalPages]);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
const isLoading = globalProcessing && !mergedPdfDocument;
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: mergedPdfDocument,
|
||||||
|
isVeryLargeDocument,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface PageEditorState {
|
||||||
|
// Selection state
|
||||||
|
selectionMode: boolean;
|
||||||
|
selectedPageNumbers: number[];
|
||||||
|
|
||||||
|
// Animation state
|
||||||
|
movingPage: number | null;
|
||||||
|
isAnimating: boolean;
|
||||||
|
|
||||||
|
// Split state
|
||||||
|
splitPositions: Set<number>;
|
||||||
|
|
||||||
|
// Export state
|
||||||
|
exportLoading: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setSelectionMode: (mode: boolean) => void;
|
||||||
|
setSelectedPageNumbers: (pages: number[]) => void;
|
||||||
|
setMovingPage: (pageNumber: number | null) => void;
|
||||||
|
setIsAnimating: (animating: boolean) => void;
|
||||||
|
setSplitPositions: (positions: Set<number>) => void;
|
||||||
|
setExportLoading: (loading: boolean) => void;
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
togglePage: (pageNumber: number) => void;
|
||||||
|
toggleSelectAll: (totalPages: number) => void;
|
||||||
|
animateReorder: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing PageEditor UI state
|
||||||
|
* Handles selection, animation, splits, and export states
|
||||||
|
*/
|
||||||
|
export function usePageEditorState(): PageEditorState {
|
||||||
|
// Selection state
|
||||||
|
const [selectionMode, setSelectionMode] = useState(false);
|
||||||
|
const [selectedPageNumbers, setSelectedPageNumbers] = useState<number[]>([]);
|
||||||
|
|
||||||
|
// Animation state
|
||||||
|
const [movingPage, setMovingPage] = useState<number | null>(null);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
|
// Split state - position-based split tracking (replaces page-based splitAfter)
|
||||||
|
const [splitPositions, setSplitPositions] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
// Export state
|
||||||
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const togglePage = useCallback((pageNumber: number) => {
|
||||||
|
setSelectedPageNumbers(prev =>
|
||||||
|
prev.includes(pageNumber)
|
||||||
|
? prev.filter(n => n !== pageNumber)
|
||||||
|
: [...prev, pageNumber]
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleSelectAll = useCallback((totalPages: number) => {
|
||||||
|
if (!totalPages) return;
|
||||||
|
|
||||||
|
const allPageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
setSelectedPageNumbers(prev =>
|
||||||
|
prev.length === allPageNumbers.length ? [] : allPageNumbers
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const animateReorder = useCallback(() => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
setTimeout(() => setIsAnimating(false), 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
selectionMode,
|
||||||
|
selectedPageNumbers,
|
||||||
|
movingPage,
|
||||||
|
isAnimating,
|
||||||
|
splitPositions,
|
||||||
|
exportLoading,
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
setSelectionMode,
|
||||||
|
setSelectedPageNumbers,
|
||||||
|
setMovingPage,
|
||||||
|
setIsAnimating,
|
||||||
|
setSplitPositions,
|
||||||
|
setExportLoading,
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
togglePage,
|
||||||
|
toggleSelectAll,
|
||||||
|
animateReorder,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user