refactor: Update PageThumbnail component to use page numbers instead of IDs for drag-and-drop functionality and selection management

- Changed selectedPages, draggedPage, dropTarget, and movingPage props from string to number type.
- Updated event handlers and state management to reflect the use of page numbers.
- Enhanced logging for better debugging during thumbnail generation and worker communication.

refactor: Simplify FileContext by removing mergedDocuments state and related actions

- Removed mergedDocuments from FileContextState and associated actions in the reducer.
- Updated selectedPageIds to selectedPageNumbers for consistency in page selection handling.

fix: Improve thumbnail generation service logging and error handling

- Added detailed logging for thumbnail generation progress and worker job management.
- Implemented better error handling for worker timeouts and main thread fallbacks.

style: Clean up FileGrid component for better readability and maintainability

- Adjusted formatting and spacing in the FileGrid component for improved code clarity.
This commit is contained in:
Reece 2025-07-08 00:02:48 +01:00
parent 2f9c88b000
commit 9b63bffb36
9 changed files with 806 additions and 478 deletions

View File

@ -8,16 +8,24 @@ try {
console.log('📦 Loading PDF.js locally...');
importScripts('/pdf.js');
if (self.pdfjsLib) {
// PDF.js exports to globalThis, check both self and globalThis
const pdfjsLib = self.pdfjsLib || globalThis.pdfjsLib;
if (pdfjsLib) {
// Make it available on self for consistency
self.pdfjsLib = pdfjsLib;
// Set up PDF.js worker
self.pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
pdfJsLoaded = true;
console.log('✓ PDF.js loaded successfully from local files');
console.log('✓ PDF.js version:', self.pdfjsLib.version || 'unknown');
} else {
throw new Error('pdfjsLib not available after import');
throw new Error('pdfjsLib not available after import - neither self.pdfjsLib nor globalThis.pdfjsLib found');
}
} catch (error) {
console.error('✗ Failed to load local PDF.js:', error);
console.error('✗ Failed to load local PDF.js:', error.message || error);
console.error('✗ Available globals:', Object.keys(self).filter(key => key.includes('pdf')));
pdfJsLoaded = false;
}
@ -34,12 +42,16 @@ self.onmessage = async function(e) {
try {
// Handle PING for worker health check
if (type === 'PING') {
console.log('🏓 Worker PING received, checking PDF.js status...');
// Check if PDF.js is loaded before responding
if (pdfJsLoaded && self.pdfjsLib) {
console.log('✓ Worker PONG - PDF.js ready');
self.postMessage({ type: 'PONG', jobId });
} else {
console.error('✗ PDF.js not loaded - worker not ready');
console.error('✗ pdfJsLoaded:', pdfJsLoaded);
console.error('✗ self.pdfjsLib:', !!self.pdfjsLib);
self.postMessage({
type: 'ERROR',
jobId,
@ -50,14 +62,19 @@ self.onmessage = async function(e) {
}
if (type === 'GENERATE_THUMBNAILS') {
console.log('🖼️ Starting thumbnail generation for', data.pageNumbers.length, 'pages');
if (!pdfJsLoaded || !self.pdfjsLib) {
throw new Error('PDF.js not available in worker');
const error = 'PDF.js not available in worker';
console.error('✗', error);
throw new Error(error);
}
const { pdfArrayBuffer, pageNumbers, scale = 0.2, quality = 0.8 } = data;
console.log('📄 Loading PDF document, size:', pdfArrayBuffer.byteLength, 'bytes');
// Load PDF in worker using imported PDF.js
const pdf = await self.pdfjsLib.getDocument({ data: pdfArrayBuffer }).promise;
console.log('✓ PDF loaded, total pages:', pdf.numPages);
const thumbnails = [];
@ -68,24 +85,33 @@ self.onmessage = async function(e) {
const batchPromises = batch.map(async (pageNumber) => {
try {
console.log(`🎯 Processing page ${pageNumber}...`);
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
console.log(`📐 Page ${pageNumber} viewport:`, viewport.width, 'x', viewport.height);
// Create OffscreenCanvas for better performance
const canvas = new OffscreenCanvas(viewport.width, viewport.height);
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Failed to get 2D context from OffscreenCanvas');
}
await page.render({ canvasContext: context, viewport }).promise;
console.log(`✓ Page ${pageNumber} rendered`);
// Convert to blob then to base64 (more efficient than toDataURL)
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const thumbnail = `data:image/jpeg;base64,${base64}`;
console.log(`✓ Page ${pageNumber} thumbnail generated (${base64.length} chars)`);
return { pageNumber, thumbnail, success: true };
} catch (error) {
return { pageNumber, error: error.message, success: false };
console.error(`✗ Failed to generate thumbnail for page ${pageNumber}:`, error.message || error);
return { pageNumber, error: error.message || String(error), success: false };
}
});
@ -93,6 +119,7 @@ self.onmessage = async function(e) {
thumbnails.push(...batchResults);
// Send progress update
console.log(`📊 Worker: Sending progress update - ${thumbnails.length}/${pageNumbers.length} completed, ${batchResults.filter(r => r.success).length} new thumbnails`);
self.postMessage({
type: 'PROGRESS',
jobId,
@ -105,6 +132,7 @@ self.onmessage = async function(e) {
// Small delay between batches to keep UI smooth
if (i + batchSize < pageNumbers.length) {
console.log(`⏸️ Worker: Pausing 100ms before next batch (${i + batchSize}/${pageNumbers.length})`);
await new Promise(resolve => setTimeout(resolve, 100)); // Increased to 100ms pause between batches for smoother scrolling
}
}

View File

@ -4,7 +4,7 @@ import { Paper, Group, TextInput, Button, Text } from '@mantine/core';
interface BulkSelectionPanelProps {
csvInput: string;
setCsvInput: (value: string) => void;
selectedPages: string[];
selectedPages: number[];
onUpdatePagesFromCSV: () => void;
}

View File

@ -9,21 +9,21 @@ interface DragDropItem {
interface DragDropGridProps<T extends DragDropItem> {
items: T[];
selectedItems: string[];
selectedItems: number[];
selectionMode: boolean;
isAnimating: boolean;
onDragStart: (itemId: string) => void;
onDragStart: (pageNumber: number) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent) => void;
onDragEnter: (itemId: string) => void;
onDragEnter: (pageNumber: number) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, targetId: string | 'end') => void;
onDrop: (e: React.DragEvent, targetPageNumber: number | 'end') => void;
onEndZoneDragEnter: () => void;
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
draggedItem: string | null;
dropTarget: string | null;
multiItemDrag: {itemIds: string[], count: number} | null;
draggedItem: number | null;
dropTarget: number | null;
multiItemDrag: {pageNumbers: number[], count: number} | null;
dragPosition: {x: number, y: number} | null;
}

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,9 @@ import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
// Ensure PDF.js worker is available
if (!GlobalWorkerOptions.workerSrc) {
GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
console.log('📸 PageThumbnail: Set PDF.js worker source to /pdf.worker.js');
} else {
console.log('📸 PageThumbnail: PDF.js worker source already set to', GlobalWorkerOptions.workerSrc);
}
interface PageThumbnailProps {
@ -23,24 +26,24 @@ interface PageThumbnailProps {
index: number;
totalPages: number;
originalFile?: File; // For lazy thumbnail generation
selectedPages: string[];
selectedPages: number[];
selectionMode: boolean;
draggedPage: string | null;
dropTarget: string | null;
movingPage: string | null;
draggedPage: number | null;
dropTarget: number | null;
movingPage: number | null;
isAnimating: boolean;
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
onDragStart: (pageId: string) => void;
onDragStart: (pageNumber: number) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent) => void;
onDragEnter: (pageId: string) => void;
onDragEnter: (pageNumber: number) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, pageId: string) => void;
onTogglePage: (pageId: string) => void;
onAnimateReorder: (pageId: string, targetIndex: number) => void;
onDrop: (e: React.DragEvent, pageNumber: number) => void;
onTogglePage: (pageNumber: number) => void;
onAnimateReorder: (pageNumber: number, targetIndex: number) => void;
onExecuteCommand: (command: Command) => void;
onSetStatus: (status: string) => void;
onSetMovingPage: (pageId: string | null) => void;
onSetMovingPage: (pageNumber: number | null) => void;
RotatePagesCommand: typeof RotatePagesCommand;
DeletePagesCommand: typeof DeletePagesCommand;
ToggleSplitCommand: typeof ToggleSplitCommand;
@ -83,23 +86,35 @@ const PageThumbnail = React.memo(({
// Update thumbnail URL when page prop changes
useEffect(() => {
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
console.log(`📸 PageThumbnail: Updating thumbnail URL for page ${page.pageNumber}`, page.thumbnail.substring(0, 50) + '...');
setThumbnailUrl(page.thumbnail);
}
}, [page.thumbnail, page.pageNumber, page.id, thumbnailUrl]);
// Listen for ready thumbnails from Web Workers (only if no existing thumbnail)
useEffect(() => {
if (thumbnailUrl) return; // Skip if we already have a thumbnail
if (thumbnailUrl) {
console.log(`📸 PageThumbnail: Page ${page.pageNumber} already has thumbnail, skipping worker listener`);
return; // Skip if we already have a thumbnail
}
console.log(`📸 PageThumbnail: Setting up worker listener for page ${page.pageNumber} (${page.id})`);
const handleThumbnailReady = (event: CustomEvent) => {
const { pageNumber, thumbnail, pageId } = event.detail;
console.log(`📸 PageThumbnail: Received worker thumbnail for page ${pageNumber}, looking for page ${page.pageNumber} (${page.id})`);
if (pageNumber === page.pageNumber && pageId === page.id) {
console.log(`✓ PageThumbnail: Thumbnail matched for page ${page.pageNumber}, setting URL`);
setThumbnailUrl(thumbnail);
}
};
window.addEventListener('thumbnailReady', handleThumbnailReady as EventListener);
return () => window.removeEventListener('thumbnailReady', handleThumbnailReady as EventListener);
return () => {
console.log(`📸 PageThumbnail: Cleaning up worker listener for page ${page.pageNumber}`);
window.removeEventListener('thumbnailReady', handleThumbnailReady as EventListener);
};
}, [page.pageNumber, page.id, thumbnailUrl]);
@ -115,7 +130,7 @@ const PageThumbnail = React.memo(({
return (
<div
ref={pageElementRef}
data-page-id={page.id}
data-page-number={page.pageNumber}
className={`
${styles.pageContainer}
!rounded-lg
@ -132,12 +147,12 @@ const PageThumbnail = React.memo(({
${selectionMode
? 'bg-white hover:bg-gray-50'
: 'bg-white hover:bg-gray-50'}
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
${movingPage === page.id ? 'page-moving' : ''}
${draggedPage === page.pageNumber ? 'opacity-50 scale-95' : ''}
${movingPage === page.pageNumber ? 'page-moving' : ''}
`}
style={{
transform: (() => {
if (!isAnimating && draggedPage && page.id !== draggedPage && dropTarget === page.id) {
if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) {
return 'translateX(20px)';
}
return 'translateX(0)';
@ -145,12 +160,12 @@ const PageThumbnail = React.memo(({
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
}}
draggable
onDragStart={() => onDragStart(page.id)}
onDragStart={() => onDragStart(page.pageNumber)}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
onDragEnter={() => onDragEnter(page.id)}
onDragEnter={() => onDragEnter(page.pageNumber)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, page.id)}
onDrop={(e) => onDrop(e, page.pageNumber)}
>
{selectionMode && (
<div
@ -159,26 +174,31 @@ const PageThumbnail = React.memo(({
position: 'absolute',
top: 8,
right: 8,
zIndex: 4,
backgroundColor: 'white',
zIndex: 10,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: '1px solid #ccc',
borderRadius: '4px',
padding: '2px',
padding: '4px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'auto'
pointerEvents: 'auto',
cursor: 'pointer'
}}
onMouseDown={(e) => e.stopPropagation()}
onDragStart={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
console.log('📸 Checkbox clicked for page', page.pageNumber);
e.stopPropagation();
onTogglePage(page.pageNumber);
}}
>
<Checkbox
checked={selectedPages.includes(page.id)}
onChange={(event) => {
event.stopPropagation();
onTogglePage(page.id);
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
onChange={() => {
// onChange is handled by the parent div click
}}
onClick={(e) => e.stopPropagation()}
size="sm"
/>
</div>
@ -272,8 +292,8 @@ const PageThumbnail = React.memo(({
onClick={(e) => {
e.stopPropagation();
if (index > 0 && !movingPage && !isAnimating) {
onSetMovingPage(page.id);
onAnimateReorder(page.id, index - 1);
onSetMovingPage(page.pageNumber);
onAnimateReorder(page.pageNumber, index - 1);
setTimeout(() => onSetMovingPage(null), 500);
onSetStatus(`Moved page ${page.pageNumber} left`);
}
@ -292,8 +312,8 @@ const PageThumbnail = React.memo(({
onClick={(e) => {
e.stopPropagation();
if (index < totalPages - 1 && !movingPage && !isAnimating) {
onSetMovingPage(page.id);
onAnimateReorder(page.id, index + 1);
onSetMovingPage(page.pageNumber);
onAnimateReorder(page.pageNumber, index + 1);
setTimeout(() => onSetMovingPage(null), 500);
onSetStatus(`Moved page ${page.pageNumber} right`);
}
@ -408,7 +428,7 @@ const PageThumbnail = React.memo(({
prevProps.page.pageNumber === nextProps.page.pageNumber &&
prevProps.page.rotation === nextProps.page.rotation &&
prevProps.page.thumbnail === nextProps.page.thumbnail &&
prevProps.selectedPages.includes(prevProps.page.id) === nextProps.selectedPages.includes(nextProps.page.id) &&
prevProps.selectedPages === nextProps.selectedPages && // Compare array reference - will re-render when selection changes
prevProps.selectionMode === nextProps.selectionMode &&
prevProps.draggedPage === nextProps.draggedPage &&
prevProps.dropTarget === nextProps.dropTarget &&

View File

@ -68,7 +68,7 @@ const FileGrid = ({
const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay;
return (
<Box>
<Box >
{/* Search and Sort Controls */}
{(showSearch || showSort) && (
<Group mb="md" justify="space-between" wrap="wrap" gap="sm">
@ -98,28 +98,11 @@ const FileGrid = ({
</Group>
)}
{/* File Count Badge */}
{(showSearch || showSort) && (
<Group mb="sm">
<Badge variant="light" size="sm">
{displayFiles.length} {displayFiles.length === 1 ? 'file' : 'files'}
{hasMoreFiles && ` (${sortedFiles.length - maxDisplay!} more)`}
</Badge>
</Group>
)}
{/* Files Grid */}
<Flex
wrap="wrap"
gap="lg"
justify="flex-start"
style={{
width: "100%",
// Responsive grid spacing
'@media (max-width: 768px)': {
{/* File Count Badge */}3
gap: 'md'
}
}}
h="30rem" style={{ overflowY: "auto", width: "100%" }}
>
{displayFiles.map((file, idx) => {
const originalIdx = files.findIndex(f => (f.id || f.name) === (file.id || file.name));

View File

@ -33,13 +33,12 @@ const initialViewerConfig: ViewerConfig = {
const initialState: FileContextState = {
activeFiles: [],
processedFiles: new Map(),
mergedDocuments: new Map(),
currentView: 'fileEditor',
currentTool: null,
fileEditHistory: new Map(),
globalFileOperations: [],
selectedFileIds: [],
selectedPageIds: [],
selectedPageNumbers: [],
viewerConfig: initialViewerConfig,
isProcessing: false,
processingProgress: 0,
@ -52,12 +51,11 @@ type FileContextAction =
| { type: 'ADD_FILES'; payload: File[] }
| { type: 'REMOVE_FILES'; payload: string[] }
| { type: 'SET_PROCESSED_FILES'; payload: Map<File, ProcessedFile> }
| { type: 'SET_MERGED_DOCUMENT'; payload: { key: string; document: PDFDocument } }
| { type: 'CLEAR_MERGED_DOCUMENTS' }
| { type: 'UPDATE_PROCESSED_FILE'; payload: { file: File; processedFile: ProcessedFile } }
| { type: 'SET_CURRENT_VIEW'; payload: ViewType }
| { type: 'SET_CURRENT_TOOL'; payload: ToolType }
| { type: 'SET_SELECTED_FILES'; payload: string[] }
| { type: 'SET_SELECTED_PAGES'; payload: string[] }
| { type: 'SET_SELECTED_PAGES'; payload: number[] }
| { type: 'CLEAR_SELECTIONS' }
| { type: 'SET_PROCESSING'; payload: { isProcessing: boolean; progress: number } }
| { type: 'UPDATE_VIEWER_CONFIG'; payload: Partial<ViewerConfig> }
@ -75,7 +73,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
...state,
activeFiles: action.payload,
selectedFileIds: [], // Clear selections when files change
selectedPageIds: []
selectedPageNumbers: []
};
case 'ADD_FILES':
@ -100,18 +98,12 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
processedFiles: action.payload
};
case 'SET_MERGED_DOCUMENT':
const newMergedDocuments = new Map(state.mergedDocuments);
newMergedDocuments.set(action.payload.key, action.payload.document);
case 'UPDATE_PROCESSED_FILE':
const updatedProcessedFiles = new Map(state.processedFiles);
updatedProcessedFiles.set(action.payload.file, action.payload.processedFile);
return {
...state,
mergedDocuments: newMergedDocuments
};
case 'CLEAR_MERGED_DOCUMENTS':
return {
...state,
mergedDocuments: new Map()
processedFiles: updatedProcessedFiles
};
case 'SET_CURRENT_VIEW':
@ -137,14 +129,14 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'SET_SELECTED_PAGES':
return {
...state,
selectedPageIds: action.payload
selectedPageNumbers: action.payload
};
case 'CLEAR_SELECTIONS':
return {
...state,
selectedFileIds: [],
selectedPageIds: []
selectedPageNumbers: []
};
case 'SET_PROCESSING':
@ -192,8 +184,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'RESET_CONTEXT':
return {
...initialState,
mergedDocuments: new Map() // Ensure clean state
...initialState
};
case 'LOAD_STATE':
@ -260,7 +251,7 @@ export function FileContextProvider({
if (state.currentView !== 'fileEditor') params.view = state.currentView;
if (state.currentTool) params.tool = state.currentTool;
if (state.selectedFileIds.length > 0) params.fileIds = state.selectedFileIds;
if (state.selectedPageIds.length > 0) params.pageIds = state.selectedPageIds;
// Note: selectedPageIds intentionally excluded from URL sync - page selection is transient UI state
if (state.viewerConfig.zoom !== 1.0) params.zoom = state.viewerConfig.zoom;
if (state.viewerConfig.currentPage !== 1) params.page = state.viewerConfig.currentPage;
@ -454,11 +445,6 @@ export function FileContextProvider({
dispatch({ type: 'REMOVE_FILES', payload: fileIds });
// Clear merged documents that included removed files
// This is a simple approach - clear all merged docs when any file is removed
// Could be optimized to only clear affected merged documents
dispatch({ type: 'CLEAR_MERGED_DOCUMENTS' });
// Remove from IndexedDB
if (enablePersistence) {
fileIds.forEach(async (fileId) => {
@ -483,7 +469,6 @@ export function FileContextProvider({
dispatch({ type: 'SET_ACTIVE_FILES', payload: [] });
dispatch({ type: 'CLEAR_SELECTIONS' });
dispatch({ type: 'CLEAR_MERGED_DOCUMENTS' });
}, [cleanupAllFiles]);
const setCurrentView = useCallback((view: ViewType) => {
@ -514,8 +499,12 @@ export function FileContextProvider({
dispatch({ type: 'SET_SELECTED_FILES', payload: fileIds });
}, []);
const setSelectedPages = useCallback((pageIds: string[]) => {
dispatch({ type: 'SET_SELECTED_PAGES', payload: pageIds });
const setSelectedPages = useCallback((pageNumbers: number[]) => {
dispatch({ type: 'SET_SELECTED_PAGES', payload: pageNumbers });
}, []);
const updateProcessedFile = useCallback((file: File, processedFile: ProcessedFile) => {
dispatch({ type: 'UPDATE_PROCESSED_FILE', payload: { file, processedFile } });
}, []);
const clearSelections = useCallback(() => {
@ -610,50 +599,6 @@ export function FileContextProvider({
}
}, [enablePersistence]);
// Merged document management functions
const generateMergedDocumentKey = useCallback((files: File[]): string => {
// Create stable key from file names and sizes
const fileDescriptors = files
.map(file => `${file.name}-${file.size}-${file.lastModified}`)
.sort() // Sort for consistent key regardless of order
.join('|');
return `merged:${fileDescriptors}`;
}, []);
const getMergedDocument = useCallback((fileIds: string[]): PDFDocument | undefined => {
// Convert fileIds to actual files to generate proper key
const files = fileIds.map(id => getFileById(id)).filter((f): f is File => f !== undefined);
if (files.length === 0) return undefined;
const key = generateMergedDocumentKey(files);
return state.mergedDocuments.get(key);
}, [state.mergedDocuments, getFileById, generateMergedDocumentKey]);
const setMergedDocument = useCallback((fileIds: string[], document: PDFDocument) => {
const files = fileIds.map(id => getFileById(id)).filter((f): f is File => f !== undefined);
if (files.length === 0) return;
const key = generateMergedDocumentKey(files);
dispatch({ type: 'SET_MERGED_DOCUMENT', payload: { key, document } });
}, [getFileById, generateMergedDocumentKey]);
const clearMergedDocuments = useCallback(() => {
dispatch({ type: 'CLEAR_MERGED_DOCUMENTS' });
}, []);
// Helper to get merged document from current active files
const getCurrentMergedDocument = useCallback((): PDFDocument | undefined => {
if (state.activeFiles.length === 0) return undefined;
const key = generateMergedDocumentKey(state.activeFiles);
return state.mergedDocuments.get(key);
}, [state.activeFiles, state.mergedDocuments, generateMergedDocumentKey]);
// Helper to set merged document for current active files
const setCurrentMergedDocument = useCallback((document: PDFDocument) => {
if (state.activeFiles.length === 0) return;
const key = generateMergedDocumentKey(state.activeFiles);
dispatch({ type: 'SET_MERGED_DOCUMENT', payload: { key, document } });
}, [state.activeFiles, generateMergedDocumentKey]);
// Auto-save context when it changes
useEffect(() => {
@ -686,6 +631,7 @@ export function FileContextProvider({
setCurrentTool,
setSelectedFiles,
setSelectedPages,
updateProcessedFile,
clearSelections,
applyPageOperations,
applyFileOperation,
@ -700,13 +646,6 @@ export function FileContextProvider({
loadContext,
resetContext,
// Merged document management
getMergedDocument,
setMergedDocument,
clearMergedDocuments,
getCurrentMergedDocument,
setCurrentMergedDocument,
// Memory management
trackBlobUrl,
trackPdfDocument,

View File

@ -145,9 +145,11 @@ export class ThumbnailGenerationService {
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
): Promise<ThumbnailResult[]> {
if (this.isGenerating) {
console.warn('🚨 ThumbnailService: Thumbnail generation already in progress, rejecting new request');
throw new Error('Thumbnail generation already in progress');
}
console.log(`🎬 ThumbnailService: Starting thumbnail generation for ${pageNumbers.length} pages`);
this.isGenerating = true;
const {
@ -166,6 +168,7 @@ export class ThumbnailGenerationService {
// Split pages across workers
const workerBatches = this.distributeWork(pageNumbers, this.workers.length);
console.log(`🔧 ThumbnailService: Distributing ${pageNumbers.length} pages across ${this.workers.length} workers:`, workerBatches.map(batch => batch.length));
const jobPromises: Promise<ThumbnailResult[]>[] = [];
for (let i = 0; i < workerBatches.length; i++) {
@ -174,23 +177,32 @@ export class ThumbnailGenerationService {
const worker = this.workers[i % this.workers.length];
const jobId = `job-${++this.jobCounter}`;
console.log(`🔧 ThumbnailService: Sending job ${jobId} with ${batch.length} pages to worker ${i}:`, batch);
const promise = new Promise<ThumbnailResult[]>((resolve, reject) => {
this.activeJobs.set(jobId, { resolve, reject, onProgress });
// Add timeout for worker jobs
const timeout = setTimeout(() => {
console.error(`⏰ ThumbnailService: Worker job ${jobId} timed out`);
this.activeJobs.delete(jobId);
reject(new Error(`Worker job ${jobId} timed out`));
}, 60000); // 1 minute timeout
// Clear timeout when job completes
const originalResolve = resolve;
const originalReject = reject;
// Create job with timeout handling
this.activeJobs.set(jobId, {
resolve: (result: any) => { clearTimeout(timeout); originalResolve(result); },
reject: (error: any) => { clearTimeout(timeout); originalReject(error); },
onProgress
resolve: (result: any) => {
console.log(`✅ ThumbnailService: Job ${jobId} completed with ${result.length} thumbnails`);
clearTimeout(timeout);
resolve(result);
},
reject: (error: any) => {
console.error(`❌ ThumbnailService: Job ${jobId} failed:`, error);
clearTimeout(timeout);
reject(error);
},
onProgress: onProgress ? (progressData: any) => {
console.log(`📊 ThumbnailService: Job ${jobId} progress - ${progressData.completed}/${progressData.total} (${progressData.thumbnails.length} new)`);
onProgress(progressData);
} : undefined
});
worker.postMessage({
@ -213,6 +225,7 @@ export class ThumbnailGenerationService {
// Flatten and sort results by page number
const allThumbnails = results.flat().sort((a, b) => a.pageNumber - b.pageNumber);
console.log(`🎯 ThumbnailService: All workers completed, returning ${allThumbnails.length} thumbnails`);
return allThumbnails;
@ -220,6 +233,7 @@ export class ThumbnailGenerationService {
console.error('Web Worker thumbnail generation failed, falling back to main thread:', error);
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
} finally {
console.log('🔄 ThumbnailService: Resetting isGenerating flag');
this.isGenerating = false;
}
}
@ -234,11 +248,15 @@ export class ThumbnailGenerationService {
quality: number,
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
): Promise<ThumbnailResult[]> {
console.log(`🔧 ThumbnailService: Fallback to main thread for ${pageNumbers.length} pages`);
// Import PDF.js dynamically for main thread
const { getDocument } = await import('pdfjs-dist');
// Load PDF once
const pdf = await getDocument({ data: pdfArrayBuffer }).promise;
console.log(`✓ ThumbnailService: PDF loaded on main thread`);
const allResults: ThumbnailResult[] = [];
let completed = 0;

View File

@ -35,9 +35,6 @@ export interface FileContextState {
activeFiles: File[];
processedFiles: Map<File, ProcessedFile>;
// Cached merged documents (for PageEditor performance)
mergedDocuments: Map<string, PDFDocument>;
// Current navigation state
currentView: ViewType;
currentTool: ToolType;
@ -48,7 +45,7 @@ export interface FileContextState {
// UI state that persists across views
selectedFileIds: string[];
selectedPageIds: string[];
selectedPageNumbers: number[];
viewerConfig: ViewerConfig;
// Processing state
@ -76,7 +73,8 @@ export interface FileContextActions {
// Selection management
setSelectedFiles: (fileIds: string[]) => void;
setSelectedPages: (pageIds: string[]) => void;
setSelectedPages: (pageNumbers: number[]) => void;
updateProcessedFile: (file: File, processedFile: ProcessedFile) => void;
clearSelections: () => void;
// Edit operations
@ -90,10 +88,6 @@ export interface FileContextActions {
// Export configuration
setExportConfig: (config: FileContextState['lastExportConfig']) => void;
// Merged document management
getMergedDocument: (fileIds: string[]) => PDFDocument | undefined;
setMergedDocument: (fileIds: string[], document: PDFDocument) => void;
clearMergedDocuments: () => void;
// Utility
getFileById: (fileId: string) => File | undefined;