mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
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:
parent
2f9c88b000
commit
9b63bffb36
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
@ -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 &&
|
||||
|
@ -61,14 +61,14 @@ const FileGrid = ({
|
||||
});
|
||||
|
||||
// Apply max display limit if specified
|
||||
const displayFiles = maxDisplay && !showingAll
|
||||
const displayFiles = maxDisplay && !showingAll
|
||||
? sortedFiles.slice(0, maxDisplay)
|
||||
: sortedFiles;
|
||||
|
||||
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">
|
||||
@ -81,7 +81,7 @@ const FileGrid = ({
|
||||
style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{showSort && (
|
||||
<Select
|
||||
data={[
|
||||
@ -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));
|
||||
@ -154,7 +137,7 @@ const FileGrid = ({
|
||||
{displayFiles.length === 0 && (
|
||||
<Box style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<Text c="dimmed">
|
||||
{searchTerm
|
||||
{searchTerm
|
||||
? t("fileManager.noFilesFound", "No files found matching your search")
|
||||
: t("fileManager.noFiles", "No files available")
|
||||
}
|
||||
@ -165,4 +148,4 @@ const FileGrid = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default FileGrid;
|
||||
export default FileGrid;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user