mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Fully working with performant thumbnail generation
This commit is contained in:
parent
e1c30edddb
commit
62f92da0fe
@ -16,8 +16,7 @@ import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessi
|
|||||||
import { fileProcessingService } from "../../services/fileProcessingService";
|
import { fileProcessingService } from "../../services/fileProcessingService";
|
||||||
import { pdfProcessingService } from "../../services/pdfProcessingService";
|
import { pdfProcessingService } from "../../services/pdfProcessingService";
|
||||||
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
|
||||||
import { useThumbnailGeneration } from "../../hooks/useThumbnailGeneration";
|
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||||
import { calculateScaleFromFileSize } from "../../utils/thumbnailUtils";
|
|
||||||
import { fileStorage } from "../../services/fileStorage";
|
import { fileStorage } from "../../services/fileStorage";
|
||||||
import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager";
|
import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager";
|
||||||
import './PageEditor.module.css';
|
import './PageEditor.module.css';
|
||||||
@ -47,9 +46,12 @@ class RotatePageCommand extends DOMCommand {
|
|||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0');
|
// Extract current rotation from transform property to match the animated CSS
|
||||||
|
const currentTransform = img.style.transform || '';
|
||||||
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
const newRotation = currentRotation + this.degrees;
|
const newRotation = currentRotation + this.degrees;
|
||||||
img.style.rotate = `${newRotation}deg`;
|
img.style.transform = `rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,9 +62,12 @@ class RotatePageCommand extends DOMCommand {
|
|||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0');
|
// Extract current rotation from transform property
|
||||||
|
const currentTransform = img.style.transform || '';
|
||||||
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
const previousRotation = currentRotation - this.degrees;
|
const previousRotation = currentRotation - this.degrees;
|
||||||
img.style.rotate = `${previousRotation}deg`;
|
img.style.transform = `rotate(${previousRotation}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,14 +302,18 @@ class BulkRotateCommand extends DOMCommand {
|
|||||||
if (img) {
|
if (img) {
|
||||||
// Store original rotation for undo (only on first execution)
|
// Store original rotation for undo (only on first execution)
|
||||||
if (!this.originalRotations.has(pageId)) {
|
if (!this.originalRotations.has(pageId)) {
|
||||||
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0');
|
const currentTransform = img.style.transform || '';
|
||||||
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
this.originalRotations.set(pageId, currentRotation);
|
this.originalRotations.set(pageId, currentRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply rotation
|
// Apply rotation using transform to trigger CSS animation
|
||||||
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0');
|
const currentTransform = img.style.transform || '';
|
||||||
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
const newRotation = currentRotation + this.degrees;
|
const newRotation = currentRotation + this.degrees;
|
||||||
img.style.rotate = `${newRotation}deg`;
|
img.style.transform = `rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -316,7 +325,7 @@ class BulkRotateCommand extends DOMCommand {
|
|||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img && this.originalRotations.has(pageId)) {
|
if (img && this.originalRotations.has(pageId)) {
|
||||||
img.style.rotate = `${this.originalRotations.get(pageId)}deg`;
|
img.style.transform = `rotate(${this.originalRotations.get(pageId)}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -476,50 +485,7 @@ const PageEditor = ({
|
|||||||
// DOM-first undo manager (replaces the old React state undo system)
|
// DOM-first undo manager (replaces the old React state undo system)
|
||||||
const undoManagerRef = useRef(new UndoManager());
|
const undoManagerRef = useRef(new UndoManager());
|
||||||
|
|
||||||
// Thumbnail generation (opt-in for visual tools) - MUST be before mergedPdfDocument
|
// Thumbnail generation is now handled on-demand by individual PageThumbnail components using modern services
|
||||||
const {
|
|
||||||
generateThumbnails,
|
|
||||||
addThumbnailToCache,
|
|
||||||
getThumbnailFromCache,
|
|
||||||
stopGeneration,
|
|
||||||
destroyThumbnails
|
|
||||||
} = useThumbnailGeneration();
|
|
||||||
|
|
||||||
// Helper function to generate thumbnails in batches
|
|
||||||
const generateThumbnailBatch = useCallback(async (file: File, fileId: string, pageNumbers: number[]) => {
|
|
||||||
console.log(`📸 PageEditor: Starting thumbnail batch for ${file.name}, pages: [${pageNumbers.join(', ')}]`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load PDF array buffer for Web Workers
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
// Calculate quality scale based on file size
|
|
||||||
const scale = calculateScaleFromFileSize(selectors.getFileRecord(fileId)?.size || 0);
|
|
||||||
|
|
||||||
// Start parallel thumbnail generation
|
|
||||||
const results = await generateThumbnails(
|
|
||||||
fileId,
|
|
||||||
arrayBuffer,
|
|
||||||
pageNumbers,
|
|
||||||
{
|
|
||||||
scale,
|
|
||||||
parallelBatches: Math.min(4, pageNumbers.length),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cache all generated thumbnails
|
|
||||||
results.forEach(({ pageNumber, thumbnail }) => {
|
|
||||||
if (thumbnail) {
|
|
||||||
const pageId = `${fileId}-${pageNumber}`;
|
|
||||||
addThumbnailToCache(pageId, thumbnail);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📸 PageEditor: Thumbnail batch completed for ${file.name}. Generated ${results.length} thumbnails`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`PageEditor: Thumbnail generation failed for ${file.name}:`, error);
|
|
||||||
}
|
|
||||||
}, [generateThumbnails, addThumbnailToCache, selectors]);
|
|
||||||
|
|
||||||
|
|
||||||
// Get primary file record outside useMemo to track processedFile changes
|
// Get primary file record outside useMemo to track processedFile changes
|
||||||
@ -617,52 +583,13 @@ const PageEditor = ({
|
|||||||
return mergedDoc;
|
return mergedDoc;
|
||||||
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
|
||||||
|
|
||||||
// Generate missing thumbnails for all loaded files
|
// Large document detection for smart loading
|
||||||
const generateMissingThumbnails = useCallback(async () => {
|
const isVeryLargeDocument = useMemo(() => {
|
||||||
if (!mergedPdfDocument || activeFileIds.length === 0) {
|
return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
|
||||||
return;
|
}, [mergedPdfDocument?.totalPages]);
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📸 PageEditor: Generating thumbnails for ${activeFileIds.length} files with ${mergedPdfDocument.totalPages} total pages`);
|
// Thumbnails are now generated on-demand by PageThumbnail components
|
||||||
|
// No bulk generation needed - modern thumbnail service handles this efficiently
|
||||||
// Process files sequentially to avoid PDF document contention
|
|
||||||
for (const fileId of activeFileIds) {
|
|
||||||
const file = selectors.getFile(fileId);
|
|
||||||
const fileRecord = selectors.getFileRecord(fileId);
|
|
||||||
|
|
||||||
if (!file || !fileRecord?.processedFile) continue;
|
|
||||||
|
|
||||||
const fileTotalPages = fileRecord.processedFile.totalPages;
|
|
||||||
if (!fileTotalPages) continue;
|
|
||||||
|
|
||||||
// Find missing thumbnails for this file
|
|
||||||
const pageNumbersToGenerate: number[] = [];
|
|
||||||
for (let pageNum = 1; pageNum <= fileTotalPages; pageNum++) {
|
|
||||||
const pageId = `${fileId}-${pageNum}`;
|
|
||||||
if (!getThumbnailFromCache(pageId)) {
|
|
||||||
pageNumbersToGenerate.push(pageNum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageNumbersToGenerate.length > 0) {
|
|
||||||
console.log(`📸 PageEditor: Generating thumbnails for ${fileRecord.name}: pages [${pageNumbersToGenerate.join(', ')}]`);
|
|
||||||
await generateThumbnailBatch(file, fileId, pageNumbersToGenerate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between files to ensure proper sequential processing
|
|
||||||
if (activeFileIds.length > 1) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [mergedPdfDocument, activeFileIds, selectors, getThumbnailFromCache, generateThumbnailBatch]);
|
|
||||||
|
|
||||||
// Generate missing thumbnails when document is ready
|
|
||||||
useEffect(() => {
|
|
||||||
if (mergedPdfDocument && mergedPdfDocument.totalPages > 0) {
|
|
||||||
console.log(`📸 PageEditor: Document ready with ${mergedPdfDocument.totalPages} pages, checking for missing thumbnails`);
|
|
||||||
generateMissingThumbnails();
|
|
||||||
}
|
|
||||||
}, [mergedPdfDocument, generateMissingThumbnails]);
|
|
||||||
|
|
||||||
// Selection and UI state management
|
// Selection and UI state management
|
||||||
const [selectionMode, setSelectionMode] = useState(false);
|
const [selectionMode, setSelectionMode] = useState(false);
|
||||||
|
@ -72,7 +72,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||||
const { getThumbnailFromCache } = useThumbnailGeneration();
|
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||||
|
|
||||||
// Update thumbnail URL when page prop changes
|
// Update thumbnail URL when page prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -81,22 +81,43 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
}
|
}
|
||||||
}, [page.thumbnail, page.id]);
|
}, [page.thumbnail, page.id]);
|
||||||
|
|
||||||
// Poll for cached thumbnails as they're generated
|
// Request thumbnail on-demand using modern service
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkThumbnail = () => {
|
let isCancelled = false;
|
||||||
const cachedThumbnail = getThumbnailFromCache(page.id);
|
|
||||||
if (cachedThumbnail && cachedThumbnail !== thumbnailUrl) {
|
// If we already have a thumbnail, use it
|
||||||
setThumbnailUrl(cachedThumbnail);
|
if (page.thumbnail) {
|
||||||
|
setThumbnailUrl(page.thumbnail);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cachedThumbnail = getThumbnailFromCache(page.id);
|
||||||
|
if (cachedThumbnail) {
|
||||||
|
setThumbnailUrl(cachedThumbnail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request thumbnail generation if we have the original file
|
||||||
|
if (originalFile) {
|
||||||
|
// Extract page number from page.id (format: fileId-pageNumber)
|
||||||
|
const pageNumber = parseInt(page.id.split('-').pop() || '1');
|
||||||
|
|
||||||
|
requestThumbnail(page.id, originalFile, pageNumber)
|
||||||
|
.then(thumbnail => {
|
||||||
|
if (!isCancelled && thumbnail) {
|
||||||
|
setThumbnailUrl(thumbnail);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
};
|
};
|
||||||
|
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
|
||||||
// Check immediately
|
|
||||||
checkThumbnail();
|
|
||||||
|
|
||||||
// Poll every 500ms for new thumbnails
|
|
||||||
const pollInterval = setInterval(checkThumbnail, 500);
|
|
||||||
return () => clearInterval(pollInterval);
|
|
||||||
}, [page.id, getThumbnailFromCache, thumbnailUrl]);
|
|
||||||
|
|
||||||
const pageElementRef = useCallback((element: HTMLDivElement | null) => {
|
const pageElementRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
@ -270,13 +291,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
backgroundColor: 'white',
|
||||||
border: '1px solid #ccc',
|
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '4px',
|
padding: '2px',
|
||||||
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()}
|
||||||
onMouseUp={(e) => e.stopPropagation()}
|
onMouseUp={(e) => e.stopPropagation()}
|
||||||
@ -369,6 +388,9 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
onMouseUp={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Tooltip label="Move Left">
|
<Tooltip label="Move Left">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
Loading…
x
Reference in New Issue
Block a user