diff --git a/frontend/public/thumbnailWorker.js b/frontend/public/thumbnailWorker.js index cb92604ea..2654ce6a4 100644 --- a/frontend/public/thumbnailWorker.js +++ b/frontend/public/thumbnailWorker.js @@ -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 } } diff --git a/frontend/src/components/pageEditor/BulkSelectionPanel.tsx b/frontend/src/components/pageEditor/BulkSelectionPanel.tsx index e28d0c41f..5a6b4504f 100644 --- a/frontend/src/components/pageEditor/BulkSelectionPanel.tsx +++ b/frontend/src/components/pageEditor/BulkSelectionPanel.tsx @@ -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; } diff --git a/frontend/src/components/pageEditor/DragDropGrid.tsx b/frontend/src/components/pageEditor/DragDropGrid.tsx index 466f14a3b..39dbb396f 100644 --- a/frontend/src/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/components/pageEditor/DragDropGrid.tsx @@ -9,21 +9,21 @@ interface DragDropItem { interface DragDropGridProps { 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>) => 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; } diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index c97afd10e..5ad376881 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useEffect } from "react"; +import React, { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon, Notification, TextInput, LoadingOverlay, Modal, Alert, @@ -6,6 +6,7 @@ import { } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useFileContext, useCurrentFile } from "../../contexts/FileContext"; +import { ViewType } from "../../types/fileContext"; import { PDFDocument, PDFPage } from "../../types/pageEditor"; import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { useUndoRedo } from "../../hooks/useUndoRedo"; @@ -19,6 +20,7 @@ import { import { pdfExportService } from "../../services/pdfExportService"; import { useThumbnailGeneration } from "../../hooks/useThumbnailGeneration"; import { calculateScaleFromFileSize } from "../../utils/thumbnailUtils"; +import { fileStorage } from "../../services/fileStorage"; import './pageEditor.module.css'; import PageThumbnail from './PageThumbnail'; import BulkSelectionPanel from './BulkSelectionPanel'; @@ -58,27 +60,105 @@ const PageEditor = ({ const { activeFiles, processedFiles, - selectedPageIds, + selectedPageNumbers, setSelectedPages, + updateProcessedFile, + setCurrentView: originalSetCurrentView, isProcessing: globalProcessing, processingProgress, - clearAllFiles, - getCurrentMergedDocument, - setCurrentMergedDocument + clearAllFiles } = fileContext; - // Use cached merged document from context instead of local state + // Edit state management + const [editedDocument, setEditedDocument] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [showUnsavedModal, setShowUnsavedModal] = useState(false); + const [showResumeModal, setShowResumeModal] = useState(false); + const [foundDraft, setFoundDraft] = useState(null); + const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null); + const autoSaveTimer = useRef(null); + + // Override setCurrentView to check for unsaved changes + const setCurrentView = useCallback((view: ViewType) => { + if (hasUnsavedChanges && view !== 'pageEditor') { + // Show warning modal instead of immediately switching views + setPendingNavigation(() => () => originalSetCurrentView(view)); + setShowUnsavedModal(true); + } else { + originalSetCurrentView(view); + } + }, [hasUnsavedChanges, originalSetCurrentView]); + + // Simple computed document from processed files (no caching needed) + const mergedPdfDocument = useMemo(() => { + if (activeFiles.length === 0) return null; + + if (activeFiles.length === 1) { + // Single file + const processedFile = processedFiles.get(activeFiles[0]); + if (!processedFile) return null; + + return { + id: processedFile.id, + name: activeFiles[0].name, + file: activeFiles[0], + pages: processedFile.pages.map(page => ({ + ...page, + rotation: page.rotation || 0, + splitBefore: page.splitBefore || false + })), + totalPages: processedFile.totalPages + }; + } else { + // Multiple files - merge them + const allPages: PDFPage[] = []; + let totalPages = 0; + const filenames: string[] = []; + + activeFiles.forEach((file, i) => { + const processedFile = processedFiles.get(file); + if (processedFile) { + filenames.push(file.name.replace(/\.pdf$/i, '')); + + processedFile.pages.forEach((page, pageIndex) => { + const newPage: PDFPage = { + ...page, + id: `${i}-${page.id}`, // Unique ID across all files + pageNumber: totalPages + pageIndex + 1, + rotation: page.rotation || 0, + splitBefore: page.splitBefore || false + }; + allPages.push(newPage); + }); + + totalPages += processedFile.pages.length; + } + }); + + if (allPages.length === 0) return null; + + return { + id: `merged-${Date.now()}`, + name: filenames.join(' + '), + file: activeFiles[0], // Use first file as reference + pages: allPages, + totalPages: totalPages + }; + } + }, [activeFiles, processedFiles]); + + // Display document: Use edited version if exists, otherwise original + const displayDocument = editedDocument || mergedPdfDocument; + const [filename, setFilename] = useState(""); - const [isMerging, setIsMerging] = useState(false); - - // Get merged document from cache - const mergedPdfDocument = getCurrentMergedDocument(); // Debug render performance - console.time('PageEditor: Component render'); + const renderStartTime = useRef(performance.now()); useEffect(() => { - console.timeEnd('PageEditor: Component render'); + const renderTime = performance.now() - renderStartTime.current; + console.log('PageEditor: Component render:', renderTime.toFixed(2) + 'ms'); + renderStartTime.current = performance.now(); }); // Page editor state (use context for selectedPages) @@ -87,9 +167,9 @@ const PageEditor = ({ const [selectionMode, setSelectionMode] = useState(false); // Drag and drop state - const [draggedPage, setDraggedPage] = useState(null); - const [dropTarget, setDropTarget] = useState(null); - const [multiPageDrag, setMultiPageDrag] = useState<{pageIds: string[], count: number} | null>(null); + const [draggedPage, setDraggedPage] = useState(null); + const [dropTarget, setDropTarget] = useState(null); + const [multiPageDrag, setMultiPageDrag] = useState<{pageNumbers: number[], count: number} | null>(null); const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null); // Export state @@ -98,7 +178,7 @@ const PageEditor = ({ const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null); // Animation state - const [movingPage, setMovingPage] = useState(null); + const [movingPage, setMovingPage] = useState(null); const [pagePositions, setPagePositions] = useState>(new Map()); const [isAnimating, setIsAnimating] = useState(false); const pageRefs = useRef>(new Map()); @@ -107,119 +187,17 @@ const PageEditor = ({ // Undo/Redo system const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo(); - // Convert enhanced processed files to Page Editor format - const convertToPageEditorFormat = useCallback((enhancedFile: EnhancedProcessedFile, fileName: string, originalFile: File): PDFDocument => { - return { - id: enhancedFile.id, - name: fileName, - file: originalFile, // Keep reference to original file for export functionality - pages: enhancedFile.pages.map(page => ({ - ...page, - // Ensure compatibility with existing page editor types - splitBefore: page.splitBefore || false - })), - totalPages: enhancedFile.totalPages - }; - }, []); - - // Merge multiple PDF documents into one (async to avoid blocking UI) - const mergeAllPDFs = useCallback(async () => { - if (activeFiles.length === 0) { - setIsMerging(false); - return; - } - - console.time('PageEditor: mergeAllPDFs'); - - // Check if we already have this combination cached - const cached = getCurrentMergedDocument(); - if (cached) { - console.log('PageEditor: Using cached merged document with', cached.pages.length, 'pages'); - setFilename(cached.name); - setIsMerging(false); - console.timeEnd('PageEditor: mergeAllPDFs'); - return; - } - - console.log('PageEditor: Creating new merged document (not cached)'); - setIsMerging(true); - - if (activeFiles.length === 1) { - // Single file - use processed file from context - const processedFile = processedFiles.get(activeFiles[0]); - if (processedFile) { - // Defer to next frame to avoid blocking - await new Promise(resolve => requestAnimationFrame(resolve)); - const pdfDoc = convertToPageEditorFormat(processedFile, activeFiles[0].name, activeFiles[0]); - - // Cache the merged document - setCurrentMergedDocument(pdfDoc); + // Set initial filename when document changes + useEffect(() => { + if (mergedPdfDocument) { + if (activeFiles.length === 1) { setFilename(activeFiles[0].name.replace(/\.pdf$/i, '')); - } - } else { - // Multiple files - merge them with chunked processing - const allPages: PDFPage[] = []; - let totalPages = 0; - const filenames: string[] = []; - - // Process files in chunks to avoid blocking UI - for (let i = 0; i < activeFiles.length; i++) { - const file = activeFiles[i]; - const processedFile = processedFiles.get(file); - - if (processedFile) { - filenames.push(file.name.replace(/\.pdf$/i, '')); - - // Process pages in chunks to avoid blocking - const pages = processedFile.pages; - const chunkSize = 50; // Process 50 pages at a time - - for (let j = 0; j < pages.length; j += chunkSize) { - const chunk = pages.slice(j, j + chunkSize); - - chunk.forEach((page, pageIndex) => { - const newPage: PDFPage = { - ...page, - id: `${i}-${page.id}`, // Unique ID across all files - pageNumber: totalPages + j + pageIndex + 1, - splitBefore: page.splitBefore || false - }; - allPages.push(newPage); - }); - - // Yield to main thread after each chunk - if (j + chunkSize < pages.length) { - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - - totalPages += processedFile.pages.length; - } - - // Yield between files - if (i < activeFiles.length - 1) { - await new Promise(resolve => requestAnimationFrame(resolve)); - } - } - - if (allPages.length > 0) { - const mergedDocument: PDFDocument = { - id: `merged-${Date.now()}`, - name: filenames.join(' + '), - file: activeFiles[0], // Use first file as reference for export operations - pages: allPages, - totalPages: totalPages - }; - - // Cache the merged document - setCurrentMergedDocument(mergedDocument); + } else { + const filenames = activeFiles.map(f => f.name.replace(/\.pdf$/i, '')); setFilename(filenames.join('_')); } } - - setIsMerging(false); - console.timeEnd('PageEditor: mergeAllPDFs'); - }, [activeFiles, processedFiles]); // Removed function dependencies to prevent unnecessary re-runs + }, [mergedPdfDocument, activeFiles]); // Handle file upload from FileUploadSelector (now using context) const handleMultipleFileUpload = useCallback(async (uploadedFiles: File[]) => { @@ -233,33 +211,6 @@ const PageEditor = ({ setStatus(`Added ${uploadedFiles.length} file(s) for processing`); }, [fileContext]); - // Store mergeAllPDFs in ref to avoid effect dependency - const mergeAllPDFsRef = useRef(mergeAllPDFs); - mergeAllPDFsRef.current = mergeAllPDFs; - - // Auto-merge documents when processing completes (async) - useEffect(() => { - const doMerge = async () => { - console.time('PageEditor: doMerge effect'); - - if (activeFiles.length > 0) { - const allProcessed = activeFiles.every(file => processedFiles.has(file)); - - if (allProcessed) { - console.log('PageEditor: All files processed, calling mergeAllPDFs'); - await mergeAllPDFsRef.current(); - } else { - console.log('PageEditor: Not all files processed yet'); - } - } else { - console.log('PageEditor: No active files'); - } - - console.timeEnd('PageEditor: doMerge effect'); - }; - - doMerge(); - }, [activeFiles, processedFiles]); // Stable dependencies only // PageEditor no longer handles cleanup - it's centralized in FileContext @@ -278,13 +229,18 @@ const PageEditor = ({ // Start thumbnail generation process (separate from document loading) const startThumbnailGeneration = useCallback(() => { + console.log('🎬 PageEditor: startThumbnailGeneration called'); + console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'activeFiles:', activeFiles.length, 'started:', thumbnailGenerationStarted); + if (!mergedPdfDocument || activeFiles.length !== 1 || thumbnailGenerationStarted) { + console.log('🎬 PageEditor: Skipping thumbnail generation due to conditions'); return; } const file = activeFiles[0]; const totalPages = mergedPdfDocument.totalPages; + console.log('🎬 PageEditor: Starting thumbnail generation for', totalPages, 'pages'); setThumbnailGenerationStarted(true); // Run everything asynchronously to avoid blocking the main thread @@ -293,14 +249,26 @@ const PageEditor = ({ // Load PDF array buffer for Web Workers const arrayBuffer = await file.arrayBuffer(); - // Generate all page numbers - const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1); + // Generate page numbers for pages that don't have thumbnails yet + const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(pageNum => { + const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); + return !page?.thumbnail; // Only generate for pages without thumbnails + }); + + console.log(`🎬 PageEditor: Generating thumbnails for ${pageNumbers.length} pages (out of ${totalPages} total):`, pageNumbers.slice(0, 10), pageNumbers.length > 10 ? '...' : ''); + + // If no pages need thumbnails, we're done + if (pageNumbers.length === 0) { + console.log('🎬 PageEditor: All pages already have thumbnails, no generation needed'); + return; + } // Calculate quality scale based on file size const scale = activeFiles.length === 1 ? calculateScaleFromFileSize(activeFiles[0].size) : 0.2; // Start parallel thumbnail generation WITHOUT blocking the main thread - generateThumbnails( + const generationPromise = generateThumbnails( arrayBuffer, pageNumbers, { @@ -311,6 +279,7 @@ const PageEditor = ({ }, // Progress callback (throttled for better performance) (progress) => { + console.log(`🎬 PageEditor: Progress - ${progress.completed}/${progress.total} pages, ${progress.thumbnails.length} new thumbnails`); // Batch process thumbnails to reduce main thread work requestAnimationFrame(() => { progress.thumbnails.forEach(({ pageNumber, thumbnail }) => { @@ -325,14 +294,23 @@ const PageEditor = ({ window.dispatchEvent(new CustomEvent('thumbnailReady', { detail: { pageNumber, thumbnail, pageId } })); + console.log(`βœ“ PageEditor: Dispatched thumbnail for page ${pageNumber}`); } }); }); } - ).catch(error => { - console.error('Web Worker thumbnail generation failed:', error); - setThumbnailGenerationStarted(false); - }); + ); + + // Handle completion properly + generationPromise + .then((allThumbnails) => { + console.log(`βœ… PageEditor: Thumbnail generation completed! Generated ${allThumbnails.length} thumbnails`); + // Don't reset thumbnailGenerationStarted here - let it stay true to prevent restarts + }) + .catch(error => { + console.error('βœ— PageEditor: Web Worker thumbnail generation failed:', error); + setThumbnailGenerationStarted(false); + }); } catch (error) { console.error('Failed to start Web Worker thumbnail generation:', error); @@ -341,20 +319,36 @@ const PageEditor = ({ }, 0); // setTimeout with 0ms to defer to next tick }, [mergedPdfDocument, activeFiles, thumbnailGenerationStarted, getThumbnailFromCache, addThumbnailToCache]); - // Start thumbnail generation after document loads and UI settles + // Start thumbnail generation after document loads useEffect(() => { - if (mergedPdfDocument && !thumbnailGenerationStarted && !isMerging) { - // Check if pages already have thumbnails from processed files - const hasExistingThumbnails = mergedPdfDocument.pages.some(page => page.thumbnail); + console.log('🎬 PageEditor: Thumbnail generation effect triggered'); + console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'started:', thumbnailGenerationStarted); + + if (mergedPdfDocument && !thumbnailGenerationStarted) { + // Check if ALL pages already have thumbnails from processed files + const totalPages = mergedPdfDocument.pages.length; + const pagesWithThumbnails = mergedPdfDocument.pages.filter(page => page.thumbnail).length; + const hasAllThumbnails = pagesWithThumbnails === totalPages; - if (hasExistingThumbnails) { - return; // Skip generation if thumbnails already exist + console.log('🎬 PageEditor: Thumbnail status:', { + totalPages, + pagesWithThumbnails, + hasAllThumbnails, + missingThumbnails: totalPages - pagesWithThumbnails + }); + + if (hasAllThumbnails) { + console.log('🎬 PageEditor: Skipping generation - all thumbnails already exist'); + return; // Skip generation if ALL thumbnails already exist } + + console.log('🎬 PageEditor: Some thumbnails missing, proceeding with generation'); // Small delay to let document render, then start thumbnail generation - const timer = setTimeout(startThumbnailGeneration, 500); // Reduced delay + console.log('🎬 PageEditor: Scheduling thumbnail generation in 500ms'); + const timer = setTimeout(startThumbnailGeneration, 500); return () => clearTimeout(timer); } - }, [mergedPdfDocument, startThumbnailGeneration, thumbnailGenerationStarted, isMerging]); + }, [mergedPdfDocument, startThumbnailGeneration, thumbnailGenerationStarted]); // Cleanup shared PDF instance when component unmounts (but preserve cache) useEffect(() => { @@ -376,6 +370,14 @@ const PageEditor = ({ setSelectionMode(false); }, [activeFiles, setSelectedPages]); + // Sync csvInput with selectedPageNumbers changes + useEffect(() => { + // Simply sort the page numbers and join them + const sortedPageNumbers = [...selectedPageNumbers].sort((a, b) => a - b); + const newCsvInput = sortedPageNumbers.join(', '); + setCsvInput(newCsvInput); + }, [selectedPageNumbers]); + useEffect(() => { const handleGlobalDragEnd = () => { // Clean up drag state when drag operation ends anywhere @@ -403,19 +405,30 @@ const PageEditor = ({ const selectAll = useCallback(() => { if (mergedPdfDocument) { - setSelectedPages(mergedPdfDocument.pages.map(p => p.id)); + setSelectedPages(mergedPdfDocument.pages.map(p => p.pageNumber)); } }, [mergedPdfDocument, setSelectedPages]); const deselectAll = useCallback(() => setSelectedPages([]), [setSelectedPages]); - const togglePage = useCallback((pageId: string) => { - setSelectedPages(prev => - prev.includes(pageId) - ? prev.filter(id => id !== pageId) - : [...prev, pageId] - ); - }, [setSelectedPages]); + const togglePage = useCallback((pageNumber: number) => { + console.log('πŸ”„ Toggling page', pageNumber); + + // Check if currently selected and update accordingly + const isCurrentlySelected = selectedPageNumbers.includes(pageNumber); + + if (isCurrentlySelected) { + // Remove from selection + console.log('πŸ”„ Removing page', pageNumber); + const newSelectedPageNumbers = selectedPageNumbers.filter(num => num !== pageNumber); + setSelectedPages(newSelectedPageNumbers); + } else { + // Add to selection + console.log('πŸ”„ Adding page', pageNumber); + const newSelectedPageNumbers = [...selectedPageNumbers, pageNumber]; + setSelectedPages(newSelectedPageNumbers); + } + }, [selectedPageNumbers, setSelectedPages]); const toggleSelectionMode = useCallback(() => { setSelectionMode(prev => { @@ -432,7 +445,7 @@ const PageEditor = ({ const parseCSVInput = useCallback((csv: string) => { if (!mergedPdfDocument) return []; - const pageIds: string[] = []; + const pageNumbers: number[] = []; const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); ranges.forEach(range => { @@ -440,40 +453,38 @@ const PageEditor = ({ const [start, end] = range.split('-').map(n => parseInt(n.trim())); for (let i = start; i <= end && i <= mergedPdfDocument.totalPages; i++) { if (i > 0) { - const page = mergedPdfDocument.pages.find(p => p.pageNumber === i); - if (page) pageIds.push(page.id); + pageNumbers.push(i); } } } else { const pageNum = parseInt(range); if (pageNum > 0 && pageNum <= mergedPdfDocument.totalPages) { - const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); - if (page) pageIds.push(page.id); + pageNumbers.push(pageNum); } } }); - return pageIds; + return pageNumbers; }, [mergedPdfDocument]); const updatePagesFromCSV = useCallback(() => { - const pageIds = parseCSVInput(csvInput); - setSelectedPages(pageIds); - }, [csvInput, parseCSVInput]); + const pageNumbers = parseCSVInput(csvInput); + setSelectedPages(pageNumbers); + }, [csvInput, parseCSVInput, setSelectedPages]); - const handleDragStart = useCallback((pageId: string) => { - setDraggedPage(pageId); + const handleDragStart = useCallback((pageNumber: number) => { + setDraggedPage(pageNumber); // Check if this is a multi-page drag in selection mode - if (selectionMode && selectedPageIds.includes(pageId) && selectedPageIds.length > 1) { + if (selectionMode && selectedPageNumbers.includes(pageNumber) && selectedPageNumbers.length > 1) { setMultiPageDrag({ - pageIds: selectedPageIds, - count: selectedPageIds.length + pageNumbers: selectedPageNumbers, + count: selectedPageNumbers.length }); } else { setMultiPageDrag(null); } - }, [selectionMode, selectedPageIds]); + }, [selectionMode, selectedPageNumbers]); const handleDragEnd = useCallback(() => { // Clean up drag state regardless of where the drop happened @@ -498,11 +509,12 @@ const PageEditor = ({ if (!elementUnderCursor) return; // Find the closest page container - const pageContainer = elementUnderCursor.closest('[data-page-id]'); + const pageContainer = elementUnderCursor.closest('[data-page-number]'); if (pageContainer) { - const pageId = pageContainer.getAttribute('data-page-id'); - if (pageId && pageId !== draggedPage) { - setDropTarget(pageId); + const pageNumberStr = pageContainer.getAttribute('data-page-number'); + const pageNumber = pageNumberStr ? parseInt(pageNumberStr) : null; + if (pageNumber && pageNumber !== draggedPage) { + setDropTarget(pageNumber); return; } } @@ -518,9 +530,9 @@ const PageEditor = ({ setDropTarget(null); }, [draggedPage, multiPageDrag]); - const handleDragEnter = useCallback((pageId: string) => { - if (draggedPage && pageId !== draggedPage) { - setDropTarget(pageId); + const handleDragEnter = useCallback((pageNumber: number) => { + if (draggedPage && pageNumber !== draggedPage) { + setDropTarget(pageNumber); } }, [draggedPage]); @@ -528,35 +540,135 @@ const PageEditor = ({ // Don't clear drop target on drag leave - let dragover handle it }, []); - // Create setPdfDocument wrapper for merged document + // Update PDF document state with edit tracking const setPdfDocument = useCallback((updatedDoc: PDFDocument) => { - // Update the cached merged document - setCurrentMergedDocument(updatedDoc); - // Return the updated document for immediate use in animations + console.log('setPdfDocument called - setting edited state'); + + // Update local edit state for immediate visual feedback + setEditedDocument(updatedDoc); + setHasUnsavedChanges(true); + + // Auto-save to drafts (debounced) + if (autoSaveTimer.current) { + clearTimeout(autoSaveTimer.current); + } + + autoSaveTimer.current = setTimeout(() => { + saveDraftToIndexedDB(updatedDoc); + }, 2000); // Auto-save after 2 seconds of inactivity + return updatedDoc; - }, [setCurrentMergedDocument]); + }, []); - const animateReorder = useCallback((pageId: string, targetIndex: number) => { - if (!mergedPdfDocument || isAnimating) return; + // Save draft to separate IndexedDB location + const saveDraftToIndexedDB = useCallback(async (doc: PDFDocument) => { + try { + const draftKey = `draft-${doc.id || 'merged'}`; + const draftData = { + document: doc, + timestamp: Date.now(), + originalFiles: activeFiles.map(f => f.name) + }; + + // Save to 'pdf-drafts' store in IndexedDB + const request = indexedDB.open('stirling-pdf-drafts', 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains('drafts')) { + db.createObjectStore('drafts'); + } + }; + + request.onsuccess = () => { + const db = request.result; + const transaction = db.transaction('drafts', 'readwrite'); + const store = transaction.objectStore('drafts'); + store.put(draftData, draftKey); + console.log('Draft auto-saved to IndexedDB'); + }; + } catch (error) { + console.warn('Failed to auto-save draft:', error); + } + }, [activeFiles]); + + // Apply changes to create new processed file + const applyChanges = useCallback(async () => { + if (!editedDocument || !mergedPdfDocument) return; + + console.log('Applying changes - creating new processed file'); + + // Create new filename with (edited) suffix + const originalName = mergedPdfDocument.name.replace(/\.pdf$/i, ''); + const newName = `${originalName}(edited).pdf`; + + try { + // Convert edited document back to processedFiles format + if (activeFiles.length === 1) { + // Single file - update the existing processed file + const file = activeFiles[0]; + const currentProcessedFile = processedFiles.get(file); + + if (currentProcessedFile) { + const updatedProcessedFile = { + ...currentProcessedFile, + id: `${currentProcessedFile.id}-edited`, + pages: editedDocument.pages.map(page => ({ + ...page, + rotation: page.rotation || 0, + splitBefore: page.splitBefore || false + })), + totalPages: editedDocument.pages.length, + lastModified: Date.now() + }; + + // Use the proper FileContext action to update + updateProcessedFile(file, updatedProcessedFile); + + // Also save the updated file to IndexedDB for persistence + await fileStorage.storeProcessedFile(file, updatedProcessedFile); + } + } + + // Clear edit state + setEditedDocument(null); + setHasUnsavedChanges(false); + + // Clean up auto-save draft + cleanupDraft(); + + setStatus('Changes applied successfully'); + + } catch (error) { + console.error('Failed to apply changes:', error); + setStatus('Failed to apply changes'); + } + }, [editedDocument, mergedPdfDocument, processedFiles, activeFiles, updateProcessedFile]); + + const animateReorder = useCallback((pageNumber: number, targetIndex: number) => { + if (!displayDocument || isAnimating) return; // In selection mode, if the dragged page is selected, move all selected pages - const pagesToMove = selectionMode && selectedPageIds.includes(pageId) - ? selectedPageIds - : [pageId]; + const pagesToMove = selectionMode && selectedPageNumbers.includes(pageNumber) + ? selectedPageNumbers.map(num => { + const page = displayDocument.pages.find(p => p.pageNumber === num); + return page?.id || ''; + }).filter(id => id) + : [displayDocument.pages.find(p => p.pageNumber === pageNumber)?.id || ''].filter(id => id); - const originalIndex = mergedPdfDocument.pages.findIndex(p => p.id === pageId); + const originalIndex = displayDocument.pages.findIndex(p => p.pageNumber === pageNumber); if (originalIndex === -1 || originalIndex === targetIndex) return; // Skip animation for large documents (500+ pages) to improve performance - const isLargeDocument = mergedPdfDocument.pages.length > 500; + const isLargeDocument = displayDocument.pages.length > 500; if (isLargeDocument) { // For large documents, just execute the command without animation if (pagesToMove.length > 1) { - const command = new MovePagesCommand(mergedPdfDocument, setPdfDocument, pagesToMove, targetIndex); + const command = new MovePagesCommand(displayDocument, setPdfDocument, pagesToMove, targetIndex); executeCommand(command); } else { - const command = new ReorderPageCommand(mergedPdfDocument, setPdfDocument, pageId, targetIndex); + const pageId = pagesToMove[0]; + const command = new ReorderPageCommand(displayDocument, setPdfDocument, pageId, targetIndex); executeCommand(command); } return; @@ -567,15 +679,15 @@ const PageEditor = ({ // For smaller documents, determine which pages might be affected by the move const startIndex = Math.min(originalIndex, targetIndex); const endIndex = Math.max(originalIndex, targetIndex); - const affectedPageIds = mergedPdfDocument.pages - .slice(Math.max(0, startIndex - 5), Math.min(mergedPdfDocument.pages.length, endIndex + 5)) + const affectedPageIds = displayDocument.pages + .slice(Math.max(0, startIndex - 5), Math.min(displayDocument.pages.length, endIndex + 5)) .map(p => p.id); // Only capture positions for potentially affected pages const currentPositions = new Map(); affectedPageIds.forEach(pageId => { - const element = document.querySelector(`[data-page-id="${pageId}"]`); + const element = document.querySelector(`[data-page-number="${pageId}"]`); if (element) { const rect = element.getBoundingClientRect(); currentPositions.set(pageId, { x: rect.left, y: rect.top }); @@ -584,10 +696,11 @@ const PageEditor = ({ // Execute the reorder command if (pagesToMove.length > 1) { - const command = new MovePagesCommand(mergedPdfDocument, setPdfDocument, pagesToMove, targetIndex); + const command = new MovePagesCommand(displayDocument, setPdfDocument, pagesToMove, targetIndex); executeCommand(command); } else { - const command = new ReorderPageCommand(mergedPdfDocument, setPdfDocument, pageId, targetIndex); + const pageId = pagesToMove[0]; + const command = new ReorderPageCommand(displayDocument, setPdfDocument, pageId, targetIndex); executeCommand(command); } @@ -599,7 +712,7 @@ const PageEditor = ({ // Get new positions only for affected pages affectedPageIds.forEach(pageId => { - const element = document.querySelector(`[data-page-id="${pageId}"]`); + const element = document.querySelector(`[data-page-number="${pageId}"]`); if (element) { const rect = element.getBoundingClientRect(); newPositions.set(pageId, { x: rect.left, y: rect.top }); @@ -610,7 +723,7 @@ const PageEditor = ({ // Apply animations only to pages that actually moved affectedPageIds.forEach(pageId => { - const element = document.querySelector(`[data-page-id="${pageId}"]`) as HTMLElement; + const element = document.querySelector(`[data-page-number="${pageId}"]`) as HTMLElement; if (!element) return; const currentPos = currentPositions.get(pageId); @@ -648,17 +761,17 @@ const PageEditor = ({ }); }); }, 10); // Small delay to allow state update - }, [mergedPdfDocument, isAnimating, executeCommand, selectionMode, selectedPageIds, setPdfDocument]); + }, [displayDocument, isAnimating, executeCommand, selectionMode, selectedPageNumbers, setPdfDocument]); - const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => { + const handleDrop = useCallback((e: React.DragEvent, targetPageNumber: number | 'end') => { e.preventDefault(); - if (!draggedPage || !mergedPdfDocument || draggedPage === targetPageId) return; + if (!draggedPage || !displayDocument || draggedPage === targetPageNumber) return; let targetIndex: number; - if (targetPageId === 'end') { - targetIndex = mergedPdfDocument.pages.length; + if (targetPageNumber === 'end') { + targetIndex = displayDocument.pages.length; } else { - targetIndex = mergedPdfDocument.pages.findIndex(p => p.id === targetPageId); + targetIndex = displayDocument.pages.findIndex(p => p.pageNumber === targetPageNumber); if (targetIndex === -1) return; } @@ -671,7 +784,7 @@ const PageEditor = ({ const moveCount = multiPageDrag ? multiPageDrag.count : 1; setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`); - }, [draggedPage, mergedPdfDocument, animateReorder, multiPageDrag]); + }, [draggedPage, displayDocument, animateReorder, multiPageDrag]); const handleEndZoneDragEnter = useCallback(() => { if (draggedPage) { @@ -680,38 +793,44 @@ const PageEditor = ({ }, [draggedPage]); const handleRotate = useCallback((direction: 'left' | 'right') => { - if (!mergedPdfDocument) return; + if (!displayDocument) return; const rotation = direction === 'left' ? -90 : 90; const pagesToRotate = selectionMode - ? selectedPageIds - : mergedPdfDocument.pages.map(p => p.id); + ? selectedPageNumbers.map(pageNum => { + const page = displayDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id) + : displayDocument.pages.map(p => p.id); - if (selectionMode && selectedPageIds.length === 0) return; + if (selectionMode && selectedPageNumbers.length === 0) return; const command = new RotatePagesCommand( - mergedPdfDocument, + displayDocument, setPdfDocument, pagesToRotate, rotation ); executeCommand(command); - const pageCount = selectionMode ? selectedPageIds.length : mergedPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPageNumbers.length : displayDocument.pages.length; setStatus(`Rotated ${pageCount} pages ${direction}`); - }, [mergedPdfDocument, selectedPageIds, selectionMode, executeCommand, setPdfDocument]); + }, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument]); const handleDelete = useCallback(() => { - if (!mergedPdfDocument) return; + if (!displayDocument) return; const pagesToDelete = selectionMode - ? selectedPageIds - : mergedPdfDocument.pages.map(p => p.id); + ? selectedPageNumbers.map(pageNum => { + const page = displayDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id) + : displayDocument.pages.map(p => p.id); - if (selectionMode && selectedPageIds.length === 0) return; + if (selectionMode && selectedPageNumbers.length === 0) return; const command = new DeletePagesCommand( - mergedPdfDocument, + displayDocument, setPdfDocument, pagesToDelete ); @@ -720,45 +839,62 @@ const PageEditor = ({ if (selectionMode) { setSelectedPages([]); } - const pageCount = selectionMode ? selectedPageIds.length : mergedPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPageNumbers.length : displayDocument.pages.length; setStatus(`Deleted ${pageCount} pages`); - }, [mergedPdfDocument, selectedPageIds, selectionMode, executeCommand, setPdfDocument, setSelectedPages]); + }, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument, setSelectedPages]); const handleSplit = useCallback(() => { - if (!mergedPdfDocument) return; + if (!displayDocument) return; const pagesToSplit = selectionMode - ? selectedPageIds - : mergedPdfDocument.pages.map(p => p.id); + ? selectedPageNumbers.map(pageNum => { + const page = displayDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id) + : displayDocument.pages.map(p => p.id); - if (selectionMode && selectedPageIds.length === 0) return; + if (selectionMode && selectedPageNumbers.length === 0) return; const command = new ToggleSplitCommand( - mergedPdfDocument, + displayDocument, setPdfDocument, pagesToSplit ); executeCommand(command); - const pageCount = selectionMode ? selectedPageIds.length : mergedPdfDocument.pages.length; + const pageCount = selectionMode ? selectedPageNumbers.length : displayDocument.pages.length; setStatus(`Split markers toggled for ${pageCount} pages`); - }, [mergedPdfDocument, selectedPageIds, selectionMode, executeCommand, setPdfDocument]); + }, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument]); const showExportPreview = useCallback((selectedOnly: boolean = false) => { if (!mergedPdfDocument) return; - const exportPageIds = selectedOnly ? selectedPageIds : []; + // Convert page numbers to page IDs for export service + const exportPageIds = selectedOnly + ? selectedPageNumbers.map(pageNum => { + const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id) + : []; + const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly); setExportPreview(preview); setShowExportModal(true); - }, [mergedPdfDocument, selectedPageIds]); + }, [mergedPdfDocument, selectedPageNumbers]); const handleExport = useCallback(async (selectedOnly: boolean = false) => { if (!mergedPdfDocument) return; setExportLoading(true); try { - const exportPageIds = selectedOnly ? selectedPageIds : []; + // Convert page numbers to page IDs for export service + const exportPageIds = selectedOnly + ? selectedPageNumbers.map(pageNum => { + const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id) + : []; + const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly); if (errors.length > 0) { setError(errors.join(', ')); @@ -796,7 +932,7 @@ const PageEditor = ({ } finally { setExportLoading(false); } - }, [mergedPdfDocument, selectedPageIds, filename]); + }, [mergedPdfDocument, selectedPageNumbers, filename]); const handleUndo = useCallback(() => { if (undo()) { @@ -811,9 +947,18 @@ const PageEditor = ({ }, [redo]); const closePdf = useCallback(() => { - clearAllFiles(); // This now handles all cleanup centrally (including merged docs) - setSelectedPages([]); - }, [clearAllFiles, setSelectedPages]); + if (hasUnsavedChanges) { + // Show warning modal instead of immediately closing + setPendingNavigation(() => () => { + clearAllFiles(); // This now handles all cleanup centrally (including merged docs) + setSelectedPages([]); + }); + setShowUnsavedModal(true); + } else { + clearAllFiles(); // This now handles all cleanup centrally (including merged docs) + setSelectedPages([]); + } + }, [hasUnsavedChanges, clearAllFiles, setSelectedPages]); // PageEditorControls needs onExportSelected and onExportAll const onExportSelected = useCallback(() => showExportPreview(true), [showExportPreview]); @@ -835,7 +980,7 @@ const PageEditor = ({ onExportAll, exportLoading, selectionMode, - selectedPages: selectedPageIds, + selectedPages: selectedPageNumbers, closePdf, }); } @@ -853,26 +998,155 @@ const PageEditor = ({ onExportAll, exportLoading, selectionMode, - selectedPageIds, + selectedPageNumbers, closePdf ]); // Show loading or empty state instead of blocking - const showLoading = !mergedPdfDocument && (globalProcessing || isMerging || activeFiles.length > 0); - const showEmpty = !mergedPdfDocument && !globalProcessing && !isMerging && activeFiles.length === 0; + const showLoading = !mergedPdfDocument && (globalProcessing || activeFiles.length > 0); + const showEmpty = !mergedPdfDocument && !globalProcessing && activeFiles.length === 0; - // For large documents, implement pagination to avoid rendering too many components - const isLargeDocument = mergedPdfDocument && mergedPdfDocument.pages.length > 200; - const [currentPageRange, setCurrentPageRange] = useState({ start: 0, end: 200 }); - - // Reset pagination when document changes - useEffect(() => { - setCurrentPageRange({ start: 0, end: 200 }); + // Clean up draft from IndexedDB + const cleanupDraft = useCallback(async () => { + try { + const draftKey = `draft-${mergedPdfDocument?.id || 'merged'}`; + const request = indexedDB.open('stirling-pdf-drafts', 1); + + request.onsuccess = () => { + const db = request.result; + const transaction = db.transaction('drafts', 'readwrite'); + const store = transaction.objectStore('drafts'); + store.delete(draftKey); + console.log('Draft cleaned up from IndexedDB'); + }; + } catch (error) { + console.warn('Failed to cleanup draft:', error); + } }, [mergedPdfDocument]); - - const displayedPages = isLargeDocument - ? mergedPdfDocument.pages.slice(currentPageRange.start, currentPageRange.end) - : mergedPdfDocument?.pages || []; + + // Export and continue + const exportAndContinue = useCallback(async () => { + if (!editedDocument) return; + + // First apply changes + await applyChanges(); + + // Then export + await handleExport(false); + + // Continue with navigation if pending + if (pendingNavigation) { + pendingNavigation(); + setPendingNavigation(null); + } + + setShowUnsavedModal(false); + }, [editedDocument, applyChanges, handleExport, pendingNavigation]); + + // Discard changes + const discardChanges = useCallback(() => { + setEditedDocument(null); + setHasUnsavedChanges(false); + cleanupDraft(); + + if (pendingNavigation) { + pendingNavigation(); + setPendingNavigation(null); + } + + setShowUnsavedModal(false); + setStatus('Changes discarded'); + }, [cleanupDraft, pendingNavigation]); + + // Keep working (stay on page editor) + const keepWorking = useCallback(() => { + setShowUnsavedModal(false); + setPendingNavigation(null); + }, []); + + // Check for existing drafts + const checkForDrafts = useCallback(async () => { + if (!mergedPdfDocument) return; + + try { + const draftKey = `draft-${mergedPdfDocument.id || 'merged'}`; + const request = indexedDB.open('stirling-pdf-drafts', 1); + + request.onsuccess = () => { + const db = request.result; + if (!db.objectStoreNames.contains('drafts')) return; + + const transaction = db.transaction('drafts', 'readonly'); + const store = transaction.objectStore('drafts'); + const getRequest = store.get(draftKey); + + getRequest.onsuccess = () => { + const draft = getRequest.result; + if (draft && draft.timestamp) { + // Check if draft is recent (within last 24 hours) + const draftAge = Date.now() - draft.timestamp; + const twentyFourHours = 24 * 60 * 60 * 1000; + + if (draftAge < twentyFourHours) { + setFoundDraft(draft); + setShowResumeModal(true); + } + } + }; + }; + } catch (error) { + console.warn('Failed to check for drafts:', error); + } + }, [mergedPdfDocument]); + + // Resume work from draft + const resumeWork = useCallback(() => { + if (foundDraft && foundDraft.document) { + setEditedDocument(foundDraft.document); + setHasUnsavedChanges(true); + setFoundDraft(null); + setShowResumeModal(false); + setStatus('Resumed previous work'); + } + }, [foundDraft]); + + // Start fresh (ignore draft) + const startFresh = useCallback(() => { + if (foundDraft) { + // Clean up the draft + cleanupDraft(); + } + setFoundDraft(null); + setShowResumeModal(false); + }, [foundDraft, cleanupDraft]); + + // Cleanup on unmount + useEffect(() => { + return () => { + console.log('PageEditor unmounting - cleaning up resources'); + + // Clear auto-save timer + if (autoSaveTimer.current) { + clearTimeout(autoSaveTimer.current); + } + + // Clean up draft if component unmounts with unsaved changes + if (hasUnsavedChanges) { + cleanupDraft(); + } + }; + }, [hasUnsavedChanges, cleanupDraft]); + + // Check for drafts when document loads + useEffect(() => { + if (mergedPdfDocument && !editedDocument && !hasUnsavedChanges) { + // Small delay to let the component settle + setTimeout(checkForDrafts, 1000); + } + }, [mergedPdfDocument, editedDocument, hasUnsavedChanges, checkForDrafts]); + + // Display all pages - use edited or original document + const displayedPages = displayDocument?.pages || []; return ( @@ -896,10 +1170,10 @@ const PageEditor = ({ - {isMerging ? "Merging PDF documents..." : "Processing PDF files..."} + Processing PDF files... - {isMerging ? "" : `${Math.round(processingProgress || 0)}%`} + {Math.round(processingProgress || 0)}%
)} - {mergedPdfDocument && ( + {displayDocument && ( {/* Enhanced Processing Status */} {globalProcessing && processingProgress < 100 && ( @@ -976,55 +1250,33 @@ const PageEditor = ({ )} + + {/* Apply Changes Button */} + {hasUnsavedChanges && ( + + )} {selectionMode && ( )} - {isLargeDocument && ( - - - Large document detected ({mergedPdfDocument.pages.length} pages) - - - - {currentPageRange.start + 1}-{Math.min(currentPageRange.end, mergedPdfDocument.pages.length)} of {mergedPdfDocument.pages.length} - - - - - - )} )} @@ -1141,6 +1393,100 @@ const PageEditor = ({ )} + {/* Unsaved Changes Modal */} + + + + You have unsaved changes to your PDF. What would you like to do? + + + + + + + + + + + + + + + {/* Resume Work Modal */} + + + + We found unsaved changes from a previous session. Would you like to resume where you left off? + + + {foundDraft && ( + + Last saved: {new Date(foundDraft.timestamp).toLocaleString()} + + )} + + + + + + + + {status && ( >; - 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 (
{ - 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 && (
e.stopPropagation()} onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }} + onClick={(e) => { + console.log('πŸ“Έ Checkbox clicked for page', page.pageNumber); + e.stopPropagation(); + onTogglePage(page.pageNumber); + }} > { - 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" />
@@ -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 && diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index c1a5df58b..501d51d5a 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -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 ( - + {/* Search and Sort Controls */} {(showSearch || showSort) && ( @@ -81,7 +81,7 @@ const FileGrid = ({ style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }} /> )} - + {showSort && (