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...');
|
console.log('📦 Loading PDF.js locally...');
|
||||||
importScripts('/pdf.js');
|
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
|
// Set up PDF.js worker
|
||||||
self.pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
self.pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||||
pdfJsLoaded = true;
|
pdfJsLoaded = true;
|
||||||
console.log('✓ PDF.js loaded successfully from local files');
|
console.log('✓ PDF.js loaded successfully from local files');
|
||||||
|
console.log('✓ PDF.js version:', self.pdfjsLib.version || 'unknown');
|
||||||
} else {
|
} 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) {
|
} 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;
|
pdfJsLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,12 +42,16 @@ self.onmessage = async function(e) {
|
|||||||
try {
|
try {
|
||||||
// Handle PING for worker health check
|
// Handle PING for worker health check
|
||||||
if (type === 'PING') {
|
if (type === 'PING') {
|
||||||
|
console.log('🏓 Worker PING received, checking PDF.js status...');
|
||||||
|
|
||||||
// Check if PDF.js is loaded before responding
|
// Check if PDF.js is loaded before responding
|
||||||
if (pdfJsLoaded && self.pdfjsLib) {
|
if (pdfJsLoaded && self.pdfjsLib) {
|
||||||
|
console.log('✓ Worker PONG - PDF.js ready');
|
||||||
self.postMessage({ type: 'PONG', jobId });
|
self.postMessage({ type: 'PONG', jobId });
|
||||||
} else {
|
} else {
|
||||||
console.error('✗ PDF.js not loaded - worker not ready');
|
console.error('✗ PDF.js not loaded - worker not ready');
|
||||||
|
console.error('✗ pdfJsLoaded:', pdfJsLoaded);
|
||||||
|
console.error('✗ self.pdfjsLib:', !!self.pdfjsLib);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'ERROR',
|
type: 'ERROR',
|
||||||
jobId,
|
jobId,
|
||||||
@ -50,14 +62,19 @@ self.onmessage = async function(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'GENERATE_THUMBNAILS') {
|
if (type === 'GENERATE_THUMBNAILS') {
|
||||||
|
console.log('🖼️ Starting thumbnail generation for', data.pageNumbers.length, 'pages');
|
||||||
|
|
||||||
if (!pdfJsLoaded || !self.pdfjsLib) {
|
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;
|
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
|
// Load PDF in worker using imported PDF.js
|
||||||
const pdf = await self.pdfjsLib.getDocument({ data: pdfArrayBuffer }).promise;
|
const pdf = await self.pdfjsLib.getDocument({ data: pdfArrayBuffer }).promise;
|
||||||
|
console.log('✓ PDF loaded, total pages:', pdf.numPages);
|
||||||
|
|
||||||
const thumbnails = [];
|
const thumbnails = [];
|
||||||
|
|
||||||
@ -68,24 +85,33 @@ self.onmessage = async function(e) {
|
|||||||
|
|
||||||
const batchPromises = batch.map(async (pageNumber) => {
|
const batchPromises = batch.map(async (pageNumber) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`🎯 Processing page ${pageNumber}...`);
|
||||||
const page = await pdf.getPage(pageNumber);
|
const page = await pdf.getPage(pageNumber);
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
|
console.log(`📐 Page ${pageNumber} viewport:`, viewport.width, 'x', viewport.height);
|
||||||
|
|
||||||
// Create OffscreenCanvas for better performance
|
// Create OffscreenCanvas for better performance
|
||||||
const canvas = new OffscreenCanvas(viewport.width, viewport.height);
|
const canvas = new OffscreenCanvas(viewport.width, viewport.height);
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Failed to get 2D context from OffscreenCanvas');
|
||||||
|
}
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
console.log(`✓ Page ${pageNumber} rendered`);
|
||||||
|
|
||||||
// Convert to blob then to base64 (more efficient than toDataURL)
|
// Convert to blob then to base64 (more efficient than toDataURL)
|
||||||
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
|
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
const thumbnail = `data:image/jpeg;base64,${base64}`;
|
const thumbnail = `data:image/jpeg;base64,${base64}`;
|
||||||
|
console.log(`✓ Page ${pageNumber} thumbnail generated (${base64.length} chars)`);
|
||||||
|
|
||||||
return { pageNumber, thumbnail, success: true };
|
return { pageNumber, thumbnail, success: true };
|
||||||
} catch (error) {
|
} 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);
|
thumbnails.push(...batchResults);
|
||||||
|
|
||||||
// Send progress update
|
// Send progress update
|
||||||
|
console.log(`📊 Worker: Sending progress update - ${thumbnails.length}/${pageNumbers.length} completed, ${batchResults.filter(r => r.success).length} new thumbnails`);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'PROGRESS',
|
type: 'PROGRESS',
|
||||||
jobId,
|
jobId,
|
||||||
@ -105,6 +132,7 @@ self.onmessage = async function(e) {
|
|||||||
|
|
||||||
// Small delay between batches to keep UI smooth
|
// Small delay between batches to keep UI smooth
|
||||||
if (i + batchSize < pageNumbers.length) {
|
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
|
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 {
|
interface BulkSelectionPanelProps {
|
||||||
csvInput: string;
|
csvInput: string;
|
||||||
setCsvInput: (value: string) => void;
|
setCsvInput: (value: string) => void;
|
||||||
selectedPages: string[];
|
selectedPages: number[];
|
||||||
onUpdatePagesFromCSV: () => void;
|
onUpdatePagesFromCSV: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,21 +9,21 @@ interface DragDropItem {
|
|||||||
|
|
||||||
interface DragDropGridProps<T extends DragDropItem> {
|
interface DragDropGridProps<T extends DragDropItem> {
|
||||||
items: T[];
|
items: T[];
|
||||||
selectedItems: string[];
|
selectedItems: number[];
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
onDragStart: (itemId: string) => void;
|
onDragStart: (pageNumber: number) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
onDragOver: (e: React.DragEvent) => void;
|
onDragOver: (e: React.DragEvent) => void;
|
||||||
onDragEnter: (itemId: string) => void;
|
onDragEnter: (pageNumber: number) => void;
|
||||||
onDragLeave: () => void;
|
onDragLeave: () => void;
|
||||||
onDrop: (e: React.DragEvent, targetId: string | 'end') => void;
|
onDrop: (e: React.DragEvent, targetPageNumber: number | 'end') => void;
|
||||||
onEndZoneDragEnter: () => void;
|
onEndZoneDragEnter: () => void;
|
||||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||||
draggedItem: string | null;
|
draggedItem: number | null;
|
||||||
dropTarget: string | null;
|
dropTarget: number | null;
|
||||||
multiItemDrag: {itemIds: string[], count: number} | null;
|
multiItemDrag: {pageNumbers: number[], count: number} | null;
|
||||||
dragPosition: {x: number, y: 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
|
// Ensure PDF.js worker is available
|
||||||
if (!GlobalWorkerOptions.workerSrc) {
|
if (!GlobalWorkerOptions.workerSrc) {
|
||||||
GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
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 {
|
interface PageThumbnailProps {
|
||||||
@ -23,24 +26,24 @@ interface PageThumbnailProps {
|
|||||||
index: number;
|
index: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
originalFile?: File; // For lazy thumbnail generation
|
originalFile?: File; // For lazy thumbnail generation
|
||||||
selectedPages: string[];
|
selectedPages: number[];
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
draggedPage: string | null;
|
draggedPage: number | null;
|
||||||
dropTarget: string | null;
|
dropTarget: number | null;
|
||||||
movingPage: string | null;
|
movingPage: number | null;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
onDragStart: (pageId: string) => void;
|
onDragStart: (pageNumber: number) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
onDragOver: (e: React.DragEvent) => void;
|
onDragOver: (e: React.DragEvent) => void;
|
||||||
onDragEnter: (pageId: string) => void;
|
onDragEnter: (pageNumber: number) => void;
|
||||||
onDragLeave: () => void;
|
onDragLeave: () => void;
|
||||||
onDrop: (e: React.DragEvent, pageId: string) => void;
|
onDrop: (e: React.DragEvent, pageNumber: number) => void;
|
||||||
onTogglePage: (pageId: string) => void;
|
onTogglePage: (pageNumber: number) => void;
|
||||||
onAnimateReorder: (pageId: string, targetIndex: number) => void;
|
onAnimateReorder: (pageNumber: number, targetIndex: number) => void;
|
||||||
onExecuteCommand: (command: Command) => void;
|
onExecuteCommand: (command: Command) => void;
|
||||||
onSetStatus: (status: string) => void;
|
onSetStatus: (status: string) => void;
|
||||||
onSetMovingPage: (pageId: string | null) => void;
|
onSetMovingPage: (pageNumber: number | null) => void;
|
||||||
RotatePagesCommand: typeof RotatePagesCommand;
|
RotatePagesCommand: typeof RotatePagesCommand;
|
||||||
DeletePagesCommand: typeof DeletePagesCommand;
|
DeletePagesCommand: typeof DeletePagesCommand;
|
||||||
ToggleSplitCommand: typeof ToggleSplitCommand;
|
ToggleSplitCommand: typeof ToggleSplitCommand;
|
||||||
@ -83,23 +86,35 @@ const PageThumbnail = React.memo(({
|
|||||||
// Update thumbnail URL when page prop changes
|
// Update thumbnail URL when page prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
||||||
|
console.log(`📸 PageThumbnail: Updating thumbnail URL for page ${page.pageNumber}`, page.thumbnail.substring(0, 50) + '...');
|
||||||
setThumbnailUrl(page.thumbnail);
|
setThumbnailUrl(page.thumbnail);
|
||||||
}
|
}
|
||||||
}, [page.thumbnail, page.pageNumber, page.id, thumbnailUrl]);
|
}, [page.thumbnail, page.pageNumber, page.id, thumbnailUrl]);
|
||||||
|
|
||||||
// Listen for ready thumbnails from Web Workers (only if no existing thumbnail)
|
// Listen for ready thumbnails from Web Workers (only if no existing thumbnail)
|
||||||
useEffect(() => {
|
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 handleThumbnailReady = (event: CustomEvent) => {
|
||||||
const { pageNumber, thumbnail, pageId } = event.detail;
|
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) {
|
if (pageNumber === page.pageNumber && pageId === page.id) {
|
||||||
|
console.log(`✓ PageThumbnail: Thumbnail matched for page ${page.pageNumber}, setting URL`);
|
||||||
setThumbnailUrl(thumbnail);
|
setThumbnailUrl(thumbnail);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('thumbnailReady', handleThumbnailReady as EventListener);
|
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]);
|
}, [page.pageNumber, page.id, thumbnailUrl]);
|
||||||
|
|
||||||
|
|
||||||
@ -115,7 +130,7 @@ const PageThumbnail = React.memo(({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={pageElementRef}
|
ref={pageElementRef}
|
||||||
data-page-id={page.id}
|
data-page-number={page.pageNumber}
|
||||||
className={`
|
className={`
|
||||||
${styles.pageContainer}
|
${styles.pageContainer}
|
||||||
!rounded-lg
|
!rounded-lg
|
||||||
@ -132,12 +147,12 @@ const PageThumbnail = React.memo(({
|
|||||||
${selectionMode
|
${selectionMode
|
||||||
? 'bg-white hover:bg-gray-50'
|
? 'bg-white hover:bg-gray-50'
|
||||||
: 'bg-white hover:bg-gray-50'}
|
: 'bg-white hover:bg-gray-50'}
|
||||||
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
|
${draggedPage === page.pageNumber ? 'opacity-50 scale-95' : ''}
|
||||||
${movingPage === page.id ? 'page-moving' : ''}
|
${movingPage === page.pageNumber ? 'page-moving' : ''}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
transform: (() => {
|
transform: (() => {
|
||||||
if (!isAnimating && draggedPage && page.id !== draggedPage && dropTarget === page.id) {
|
if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) {
|
||||||
return 'translateX(20px)';
|
return 'translateX(20px)';
|
||||||
}
|
}
|
||||||
return 'translateX(0)';
|
return 'translateX(0)';
|
||||||
@ -145,12 +160,12 @@ const PageThumbnail = React.memo(({
|
|||||||
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
|
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
|
||||||
}}
|
}}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={() => onDragStart(page.id)}
|
onDragStart={() => onDragStart(page.pageNumber)}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDragEnter={() => onDragEnter(page.id)}
|
onDragEnter={() => onDragEnter(page.pageNumber)}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
onDrop={(e) => onDrop(e, page.id)}
|
onDrop={(e) => onDrop(e, page.pageNumber)}
|
||||||
>
|
>
|
||||||
{selectionMode && (
|
{selectionMode && (
|
||||||
<div
|
<div
|
||||||
@ -159,26 +174,31 @@ const PageThumbnail = React.memo(({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
zIndex: 4,
|
zIndex: 10,
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
border: '1px solid #ccc',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '2px',
|
padding: '4px',
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||||
pointerEvents: 'auto'
|
pointerEvents: 'auto',
|
||||||
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
console.log('📸 Checkbox clicked for page', page.pageNumber);
|
||||||
|
e.stopPropagation();
|
||||||
|
onTogglePage(page.pageNumber);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedPages.includes(page.id)}
|
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
|
||||||
onChange={(event) => {
|
onChange={() => {
|
||||||
event.stopPropagation();
|
// onChange is handled by the parent div click
|
||||||
onTogglePage(page.id);
|
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -272,8 +292,8 @@ const PageThumbnail = React.memo(({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (index > 0 && !movingPage && !isAnimating) {
|
if (index > 0 && !movingPage && !isAnimating) {
|
||||||
onSetMovingPage(page.id);
|
onSetMovingPage(page.pageNumber);
|
||||||
onAnimateReorder(page.id, index - 1);
|
onAnimateReorder(page.pageNumber, index - 1);
|
||||||
setTimeout(() => onSetMovingPage(null), 500);
|
setTimeout(() => onSetMovingPage(null), 500);
|
||||||
onSetStatus(`Moved page ${page.pageNumber} left`);
|
onSetStatus(`Moved page ${page.pageNumber} left`);
|
||||||
}
|
}
|
||||||
@ -292,8 +312,8 @@ const PageThumbnail = React.memo(({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (index < totalPages - 1 && !movingPage && !isAnimating) {
|
if (index < totalPages - 1 && !movingPage && !isAnimating) {
|
||||||
onSetMovingPage(page.id);
|
onSetMovingPage(page.pageNumber);
|
||||||
onAnimateReorder(page.id, index + 1);
|
onAnimateReorder(page.pageNumber, index + 1);
|
||||||
setTimeout(() => onSetMovingPage(null), 500);
|
setTimeout(() => onSetMovingPage(null), 500);
|
||||||
onSetStatus(`Moved page ${page.pageNumber} right`);
|
onSetStatus(`Moved page ${page.pageNumber} right`);
|
||||||
}
|
}
|
||||||
@ -408,7 +428,7 @@ const PageThumbnail = React.memo(({
|
|||||||
prevProps.page.pageNumber === nextProps.page.pageNumber &&
|
prevProps.page.pageNumber === nextProps.page.pageNumber &&
|
||||||
prevProps.page.rotation === nextProps.page.rotation &&
|
prevProps.page.rotation === nextProps.page.rotation &&
|
||||||
prevProps.page.thumbnail === nextProps.page.thumbnail &&
|
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.selectionMode === nextProps.selectionMode &&
|
||||||
prevProps.draggedPage === nextProps.draggedPage &&
|
prevProps.draggedPage === nextProps.draggedPage &&
|
||||||
prevProps.dropTarget === nextProps.dropTarget &&
|
prevProps.dropTarget === nextProps.dropTarget &&
|
||||||
|
@ -98,28 +98,11 @@ const FileGrid = ({
|
|||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* File Count Badge */}
|
{/* File Count Badge */}3
|
||||||
{(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)': {
|
|
||||||
gap: 'md'
|
gap: 'md'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
h="30rem" style={{ overflowY: "auto", width: "100%" }}
|
||||||
>
|
>
|
||||||
{displayFiles.map((file, idx) => {
|
{displayFiles.map((file, idx) => {
|
||||||
const originalIdx = files.findIndex(f => (f.id || f.name) === (file.id || file.name));
|
const originalIdx = files.findIndex(f => (f.id || f.name) === (file.id || file.name));
|
||||||
|
@ -33,13 +33,12 @@ const initialViewerConfig: ViewerConfig = {
|
|||||||
const initialState: FileContextState = {
|
const initialState: FileContextState = {
|
||||||
activeFiles: [],
|
activeFiles: [],
|
||||||
processedFiles: new Map(),
|
processedFiles: new Map(),
|
||||||
mergedDocuments: new Map(),
|
|
||||||
currentView: 'fileEditor',
|
currentView: 'fileEditor',
|
||||||
currentTool: null,
|
currentTool: null,
|
||||||
fileEditHistory: new Map(),
|
fileEditHistory: new Map(),
|
||||||
globalFileOperations: [],
|
globalFileOperations: [],
|
||||||
selectedFileIds: [],
|
selectedFileIds: [],
|
||||||
selectedPageIds: [],
|
selectedPageNumbers: [],
|
||||||
viewerConfig: initialViewerConfig,
|
viewerConfig: initialViewerConfig,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
processingProgress: 0,
|
processingProgress: 0,
|
||||||
@ -52,12 +51,11 @@ type FileContextAction =
|
|||||||
| { type: 'ADD_FILES'; payload: File[] }
|
| { type: 'ADD_FILES'; payload: File[] }
|
||||||
| { type: 'REMOVE_FILES'; payload: string[] }
|
| { type: 'REMOVE_FILES'; payload: string[] }
|
||||||
| { type: 'SET_PROCESSED_FILES'; payload: Map<File, ProcessedFile> }
|
| { type: 'SET_PROCESSED_FILES'; payload: Map<File, ProcessedFile> }
|
||||||
| { type: 'SET_MERGED_DOCUMENT'; payload: { key: string; document: PDFDocument } }
|
| { type: 'UPDATE_PROCESSED_FILE'; payload: { file: File; processedFile: ProcessedFile } }
|
||||||
| { type: 'CLEAR_MERGED_DOCUMENTS' }
|
|
||||||
| { type: 'SET_CURRENT_VIEW'; payload: ViewType }
|
| { type: 'SET_CURRENT_VIEW'; payload: ViewType }
|
||||||
| { type: 'SET_CURRENT_TOOL'; payload: ToolType }
|
| { type: 'SET_CURRENT_TOOL'; payload: ToolType }
|
||||||
| { type: 'SET_SELECTED_FILES'; payload: string[] }
|
| { type: 'SET_SELECTED_FILES'; payload: string[] }
|
||||||
| { type: 'SET_SELECTED_PAGES'; payload: string[] }
|
| { type: 'SET_SELECTED_PAGES'; payload: number[] }
|
||||||
| { type: 'CLEAR_SELECTIONS' }
|
| { type: 'CLEAR_SELECTIONS' }
|
||||||
| { type: 'SET_PROCESSING'; payload: { isProcessing: boolean; progress: number } }
|
| { type: 'SET_PROCESSING'; payload: { isProcessing: boolean; progress: number } }
|
||||||
| { type: 'UPDATE_VIEWER_CONFIG'; payload: Partial<ViewerConfig> }
|
| { type: 'UPDATE_VIEWER_CONFIG'; payload: Partial<ViewerConfig> }
|
||||||
@ -75,7 +73,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
...state,
|
...state,
|
||||||
activeFiles: action.payload,
|
activeFiles: action.payload,
|
||||||
selectedFileIds: [], // Clear selections when files change
|
selectedFileIds: [], // Clear selections when files change
|
||||||
selectedPageIds: []
|
selectedPageNumbers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'ADD_FILES':
|
case 'ADD_FILES':
|
||||||
@ -100,18 +98,12 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
processedFiles: action.payload
|
processedFiles: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'SET_MERGED_DOCUMENT':
|
case 'UPDATE_PROCESSED_FILE':
|
||||||
const newMergedDocuments = new Map(state.mergedDocuments);
|
const updatedProcessedFiles = new Map(state.processedFiles);
|
||||||
newMergedDocuments.set(action.payload.key, action.payload.document);
|
updatedProcessedFiles.set(action.payload.file, action.payload.processedFile);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
mergedDocuments: newMergedDocuments
|
processedFiles: updatedProcessedFiles
|
||||||
};
|
|
||||||
|
|
||||||
case 'CLEAR_MERGED_DOCUMENTS':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
mergedDocuments: new Map()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'SET_CURRENT_VIEW':
|
case 'SET_CURRENT_VIEW':
|
||||||
@ -137,14 +129,14 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
case 'SET_SELECTED_PAGES':
|
case 'SET_SELECTED_PAGES':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedPageIds: action.payload
|
selectedPageNumbers: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'CLEAR_SELECTIONS':
|
case 'CLEAR_SELECTIONS':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedFileIds: [],
|
selectedFileIds: [],
|
||||||
selectedPageIds: []
|
selectedPageNumbers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'SET_PROCESSING':
|
case 'SET_PROCESSING':
|
||||||
@ -192,8 +184,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
|
|
||||||
case 'RESET_CONTEXT':
|
case 'RESET_CONTEXT':
|
||||||
return {
|
return {
|
||||||
...initialState,
|
...initialState
|
||||||
mergedDocuments: new Map() // Ensure clean state
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'LOAD_STATE':
|
case 'LOAD_STATE':
|
||||||
@ -260,7 +251,7 @@ export function FileContextProvider({
|
|||||||
if (state.currentView !== 'fileEditor') params.view = state.currentView;
|
if (state.currentView !== 'fileEditor') params.view = state.currentView;
|
||||||
if (state.currentTool) params.tool = state.currentTool;
|
if (state.currentTool) params.tool = state.currentTool;
|
||||||
if (state.selectedFileIds.length > 0) params.fileIds = state.selectedFileIds;
|
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.zoom !== 1.0) params.zoom = state.viewerConfig.zoom;
|
||||||
if (state.viewerConfig.currentPage !== 1) params.page = state.viewerConfig.currentPage;
|
if (state.viewerConfig.currentPage !== 1) params.page = state.viewerConfig.currentPage;
|
||||||
|
|
||||||
@ -454,11 +445,6 @@ export function FileContextProvider({
|
|||||||
|
|
||||||
dispatch({ type: 'REMOVE_FILES', payload: fileIds });
|
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
|
// Remove from IndexedDB
|
||||||
if (enablePersistence) {
|
if (enablePersistence) {
|
||||||
fileIds.forEach(async (fileId) => {
|
fileIds.forEach(async (fileId) => {
|
||||||
@ -483,7 +469,6 @@ export function FileContextProvider({
|
|||||||
|
|
||||||
dispatch({ type: 'SET_ACTIVE_FILES', payload: [] });
|
dispatch({ type: 'SET_ACTIVE_FILES', payload: [] });
|
||||||
dispatch({ type: 'CLEAR_SELECTIONS' });
|
dispatch({ type: 'CLEAR_SELECTIONS' });
|
||||||
dispatch({ type: 'CLEAR_MERGED_DOCUMENTS' });
|
|
||||||
}, [cleanupAllFiles]);
|
}, [cleanupAllFiles]);
|
||||||
|
|
||||||
const setCurrentView = useCallback((view: ViewType) => {
|
const setCurrentView = useCallback((view: ViewType) => {
|
||||||
@ -514,8 +499,12 @@ export function FileContextProvider({
|
|||||||
dispatch({ type: 'SET_SELECTED_FILES', payload: fileIds });
|
dispatch({ type: 'SET_SELECTED_FILES', payload: fileIds });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setSelectedPages = useCallback((pageIds: string[]) => {
|
const setSelectedPages = useCallback((pageNumbers: number[]) => {
|
||||||
dispatch({ type: 'SET_SELECTED_PAGES', payload: pageIds });
|
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(() => {
|
const clearSelections = useCallback(() => {
|
||||||
@ -610,50 +599,6 @@ export function FileContextProvider({
|
|||||||
}
|
}
|
||||||
}, [enablePersistence]);
|
}, [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
|
// Auto-save context when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -686,6 +631,7 @@ export function FileContextProvider({
|
|||||||
setCurrentTool,
|
setCurrentTool,
|
||||||
setSelectedFiles,
|
setSelectedFiles,
|
||||||
setSelectedPages,
|
setSelectedPages,
|
||||||
|
updateProcessedFile,
|
||||||
clearSelections,
|
clearSelections,
|
||||||
applyPageOperations,
|
applyPageOperations,
|
||||||
applyFileOperation,
|
applyFileOperation,
|
||||||
@ -700,13 +646,6 @@ export function FileContextProvider({
|
|||||||
loadContext,
|
loadContext,
|
||||||
resetContext,
|
resetContext,
|
||||||
|
|
||||||
// Merged document management
|
|
||||||
getMergedDocument,
|
|
||||||
setMergedDocument,
|
|
||||||
clearMergedDocuments,
|
|
||||||
getCurrentMergedDocument,
|
|
||||||
setCurrentMergedDocument,
|
|
||||||
|
|
||||||
// Memory management
|
// Memory management
|
||||||
trackBlobUrl,
|
trackBlobUrl,
|
||||||
trackPdfDocument,
|
trackPdfDocument,
|
||||||
|
@ -145,9 +145,11 @@ export class ThumbnailGenerationService {
|
|||||||
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
||||||
): Promise<ThumbnailResult[]> {
|
): Promise<ThumbnailResult[]> {
|
||||||
if (this.isGenerating) {
|
if (this.isGenerating) {
|
||||||
|
console.warn('🚨 ThumbnailService: Thumbnail generation already in progress, rejecting new request');
|
||||||
throw new Error('Thumbnail generation already in progress');
|
throw new Error('Thumbnail generation already in progress');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🎬 ThumbnailService: Starting thumbnail generation for ${pageNumbers.length} pages`);
|
||||||
this.isGenerating = true;
|
this.isGenerating = true;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -166,6 +168,7 @@ export class ThumbnailGenerationService {
|
|||||||
|
|
||||||
// Split pages across workers
|
// Split pages across workers
|
||||||
const workerBatches = this.distributeWork(pageNumbers, this.workers.length);
|
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[]>[] = [];
|
const jobPromises: Promise<ThumbnailResult[]>[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < workerBatches.length; i++) {
|
for (let i = 0; i < workerBatches.length; i++) {
|
||||||
@ -174,23 +177,32 @@ export class ThumbnailGenerationService {
|
|||||||
|
|
||||||
const worker = this.workers[i % this.workers.length];
|
const worker = this.workers[i % this.workers.length];
|
||||||
const jobId = `job-${++this.jobCounter}`;
|
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) => {
|
const promise = new Promise<ThumbnailResult[]>((resolve, reject) => {
|
||||||
this.activeJobs.set(jobId, { resolve, reject, onProgress });
|
|
||||||
|
|
||||||
// Add timeout for worker jobs
|
// Add timeout for worker jobs
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
console.error(`⏰ ThumbnailService: Worker job ${jobId} timed out`);
|
||||||
this.activeJobs.delete(jobId);
|
this.activeJobs.delete(jobId);
|
||||||
reject(new Error(`Worker job ${jobId} timed out`));
|
reject(new Error(`Worker job ${jobId} timed out`));
|
||||||
}, 60000); // 1 minute timeout
|
}, 60000); // 1 minute timeout
|
||||||
|
|
||||||
// Clear timeout when job completes
|
// Create job with timeout handling
|
||||||
const originalResolve = resolve;
|
|
||||||
const originalReject = reject;
|
|
||||||
this.activeJobs.set(jobId, {
|
this.activeJobs.set(jobId, {
|
||||||
resolve: (result: any) => { clearTimeout(timeout); originalResolve(result); },
|
resolve: (result: any) => {
|
||||||
reject: (error: any) => { clearTimeout(timeout); originalReject(error); },
|
console.log(`✅ ThumbnailService: Job ${jobId} completed with ${result.length} thumbnails`);
|
||||||
onProgress
|
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({
|
worker.postMessage({
|
||||||
@ -213,6 +225,7 @@ export class ThumbnailGenerationService {
|
|||||||
|
|
||||||
// Flatten and sort results by page number
|
// Flatten and sort results by page number
|
||||||
const allThumbnails = results.flat().sort((a, b) => a.pageNumber - b.pageNumber);
|
const allThumbnails = results.flat().sort((a, b) => a.pageNumber - b.pageNumber);
|
||||||
|
console.log(`🎯 ThumbnailService: All workers completed, returning ${allThumbnails.length} thumbnails`);
|
||||||
|
|
||||||
return allThumbnails;
|
return allThumbnails;
|
||||||
|
|
||||||
@ -220,6 +233,7 @@ export class ThumbnailGenerationService {
|
|||||||
console.error('Web Worker thumbnail generation failed, falling back to main thread:', error);
|
console.error('Web Worker thumbnail generation failed, falling back to main thread:', error);
|
||||||
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
|
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
|
||||||
} finally {
|
} finally {
|
||||||
|
console.log('🔄 ThumbnailService: Resetting isGenerating flag');
|
||||||
this.isGenerating = false;
|
this.isGenerating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,11 +248,15 @@ export class ThumbnailGenerationService {
|
|||||||
quality: number,
|
quality: number,
|
||||||
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
||||||
): Promise<ThumbnailResult[]> {
|
): Promise<ThumbnailResult[]> {
|
||||||
|
console.log(`🔧 ThumbnailService: Fallback to main thread for ${pageNumbers.length} pages`);
|
||||||
|
|
||||||
// Import PDF.js dynamically for main thread
|
// Import PDF.js dynamically for main thread
|
||||||
const { getDocument } = await import('pdfjs-dist');
|
const { getDocument } = await import('pdfjs-dist');
|
||||||
|
|
||||||
// Load PDF once
|
// Load PDF once
|
||||||
const pdf = await getDocument({ data: pdfArrayBuffer }).promise;
|
const pdf = await getDocument({ data: pdfArrayBuffer }).promise;
|
||||||
|
console.log(`✓ ThumbnailService: PDF loaded on main thread`);
|
||||||
|
|
||||||
|
|
||||||
const allResults: ThumbnailResult[] = [];
|
const allResults: ThumbnailResult[] = [];
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
|
@ -35,9 +35,6 @@ export interface FileContextState {
|
|||||||
activeFiles: File[];
|
activeFiles: File[];
|
||||||
processedFiles: Map<File, ProcessedFile>;
|
processedFiles: Map<File, ProcessedFile>;
|
||||||
|
|
||||||
// Cached merged documents (for PageEditor performance)
|
|
||||||
mergedDocuments: Map<string, PDFDocument>;
|
|
||||||
|
|
||||||
// Current navigation state
|
// Current navigation state
|
||||||
currentView: ViewType;
|
currentView: ViewType;
|
||||||
currentTool: ToolType;
|
currentTool: ToolType;
|
||||||
@ -48,7 +45,7 @@ export interface FileContextState {
|
|||||||
|
|
||||||
// UI state that persists across views
|
// UI state that persists across views
|
||||||
selectedFileIds: string[];
|
selectedFileIds: string[];
|
||||||
selectedPageIds: string[];
|
selectedPageNumbers: number[];
|
||||||
viewerConfig: ViewerConfig;
|
viewerConfig: ViewerConfig;
|
||||||
|
|
||||||
// Processing state
|
// Processing state
|
||||||
@ -76,7 +73,8 @@ export interface FileContextActions {
|
|||||||
|
|
||||||
// Selection management
|
// Selection management
|
||||||
setSelectedFiles: (fileIds: string[]) => void;
|
setSelectedFiles: (fileIds: string[]) => void;
|
||||||
setSelectedPages: (pageIds: string[]) => void;
|
setSelectedPages: (pageNumbers: number[]) => void;
|
||||||
|
updateProcessedFile: (file: File, processedFile: ProcessedFile) => void;
|
||||||
clearSelections: () => void;
|
clearSelections: () => void;
|
||||||
|
|
||||||
// Edit operations
|
// Edit operations
|
||||||
@ -90,10 +88,6 @@ export interface FileContextActions {
|
|||||||
// Export configuration
|
// Export configuration
|
||||||
setExportConfig: (config: FileContextState['lastExportConfig']) => void;
|
setExportConfig: (config: FileContextState['lastExportConfig']) => void;
|
||||||
|
|
||||||
// Merged document management
|
|
||||||
getMergedDocument: (fileIds: string[]) => PDFDocument | undefined;
|
|
||||||
setMergedDocument: (fileIds: string[], document: PDFDocument) => void;
|
|
||||||
clearMergedDocuments: () => void;
|
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
getFileById: (fileId: string) => File | undefined;
|
getFileById: (fileId: string) => File | undefined;
|
||||||
|
Loading…
Reference in New Issue
Block a user