From 540e628e60e2895215e44ee5fec0426ab7d07bb4 Mon Sep 17 00:00:00 2001 From: Reece Date: Mon, 2 Feb 2026 22:14:06 +0000 Subject: [PATCH] Fix load on big pages/ select issue/ addfiles bug --- .../components/pageEditor/DragDropGrid.tsx | 26 +++- .../core/components/pageEditor/PageEditor.tsx | 140 ++++++++++++------ .../components/pageEditor/PageThumbnail.tsx | 3 +- .../pageEditor/hooks/useEditorCommands.ts | 28 +++- .../pageEditor/hooks/usePageDocument.ts | 87 ++++++++--- .../pageEditor/hooks/usePageEditorState.ts | 11 +- .../src/core/contexts/PageEditorContext.tsx | 90 +++++++++-- .../src/core/contexts/file/fileActions.ts | 14 +- frontend/src/core/utils/thumbnailUtils.ts | 19 ++- 9 files changed, 320 insertions(+), 98 deletions(-) diff --git a/frontend/src/core/components/pageEditor/DragDropGrid.tsx b/frontend/src/core/components/pageEditor/DragDropGrid.tsx index 8f8598c292..5612cde22e 100644 --- a/frontend/src/core/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/core/components/pageEditor/DragDropGrid.tsx @@ -37,6 +37,7 @@ interface DragDropGridProps { getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null; zoomLevel?: number; selectedFileIds?: string[]; + selectedPageIds?: string[]; onVisibleItemsChange?: (items: T[]) => void; } @@ -197,6 +198,7 @@ interface DraggableItemProps { onUpdateDropTarget: (itemId: string | null) => void; renderItem: (item: T, index: number, refs: React.MutableRefObject>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, activeDragIds: string[], justMoved: boolean, isOver: boolean, dragHandleProps?: any, zoomLevel?: number) => React.ReactNode; zoomLevel: number; + selectedPageIds?: string[]; } const DraggableItemInner = ({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps) => { @@ -266,6 +268,18 @@ const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => { return false; // Props changed, re-render needed } + // Check if page selection changed (for checkbox selection, not box selection) + const prevSelectedSet = prevProps.selectedPageIds ? new Set(prevProps.selectedPageIds) : null; + const nextSelectedSet = nextProps.selectedPageIds ? new Set(nextProps.selectedPageIds) : null; + + if (prevSelectedSet && nextSelectedSet) { + const prevSelected = prevSelectedSet.has(prevProps.item.id); + const nextSelected = nextSelectedSet.has(nextProps.item.id); + if (prevSelected !== nextSelected) { + return false; // Selection state changed for this item, re-render needed + } + } + // Item reference is same, check other props return ( prevProps.item.id === nextProps.item.id && @@ -285,6 +299,7 @@ const DragDropGrid = ({ getThumbnailData, zoomLevel = 1.0, selectedFileIds, + selectedPageIds, onVisibleItemsChange, }: DragDropGridProps) => { const itemRefs = useRef>(new Map()); @@ -294,6 +309,10 @@ const DragDropGrid = ({ return containerRef.current?.closest('[data-scrolling-container]') as HTMLElement | null; }, []); + // Create stable signature for items to ensure useMemo detects changes + const itemsSignature = useMemo(() => items.map(item => item.id).join(','), [items]); + const selectedFileIdsSignature = useMemo(() => selectedFileIds?.join(',') || '', [selectedFileIds]); + const { filteredItems: visibleItems, filteredToOriginalIndex } = useMemo(() => { const filtered: T[] = []; const indexMap: number[] = []; @@ -318,7 +337,7 @@ const DragDropGrid = ({ }); return { filteredItems: filtered, filteredToOriginalIndex: indexMap }; - }, [items, selectedFileIds]); + }, [items, selectedFileIds, itemsSignature, selectedFileIdsSignature]); useEffect(() => { const visibleIdSet = new Set(visibleItems.map(item => item.id)); @@ -464,9 +483,11 @@ const DragDropGrid = ({ }, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]); // Re-measure virtualizer when zoom or items per row changes + // Also remeasure when items change (not just length) to handle item additions/removals + const visibleItemsSignature = useMemo(() => visibleItems.map(item => item.id).join(','), [visibleItems]); useEffect(() => { rowVirtualizer.measure(); - }, [zoomLevel, itemsPerRow, visibleItems.length]); + }, [zoomLevel, itemsPerRow, visibleItems.length, visibleItemsSignature, rowVirtualizer]); // Cleanup highlight timeout on unmount useEffect(() => { @@ -799,6 +820,7 @@ const DragDropGrid = ({ onUpdateDropTarget={setHoveredItemId} renderItem={renderItem} zoomLevel={zoomLevel} + selectedPageIds={selectedPageIds} /> ); })} diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx index 6c8f554ec8..0772e6e6c6 100644 --- a/frontend/src/core/components/pageEditor/PageEditor.tsx +++ b/frontend/src/core/components/pageEditor/PageEditor.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; import { useFileState, useFileActions } from "@app/contexts/FileContext"; -import { useNavigationGuard } from "@app/contexts/NavigationContext"; +import { useNavigationGuard, useNavigationState } from "@app/contexts/NavigationContext"; import { usePageEditor } from "@app/contexts/PageEditorContext"; import { PageEditorFunctions, PDFPage } from "@app/types/pageEditor"; // Thumbnail generation is now handled by individual PageThumbnail components @@ -39,6 +39,7 @@ const PageEditor = ({ // Navigation guard for unsaved changes const { setHasUnsavedChanges } = useNavigationGuard(); + const navigationState = useNavigationState(); // Get PageEditor coordination functions const { @@ -177,13 +178,20 @@ const PageEditor = ({ useEffect(() => { return () => { + // Only save persisted document if we're still in page editor mode + // (e.g., component unmounting due to hot reload, not navigation away) + if (navigationState.workbench !== 'pageEditor') { + // Navigating away from page editor - don't save stale state + return; + } + const doc = displayDocumentRef.current; if (doc && doc.pages.length > 0) { const signature = doc.pages.map(page => page.id).join(','); savePersistedDocument(doc, signature); } }; - }, [savePersistedDocument]); + }, [savePersistedDocument, navigationState.workbench]); // UI state management const { @@ -272,8 +280,18 @@ const PageEditor = ({ return; } + // Count how many thumbnails have already been loaded (not including pending) + const loadedCount = displayDocument.pages.filter(p => p.thumbnail).length; + const pending = thumbnailRequestsRef.current.size; - const MAX_CONCURRENT_THUMBNAILS = 12; + + // First 8 pages: load 1 at a time for immediate visual feedback + // After that: batch load based on document size + const MAX_CONCURRENT_THUMBNAILS = loadedCount < 8 ? 1 + : displayDocument.totalPages < 20 ? 3 + : displayDocument.totalPages < 50 ? 5 + : 8; + const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending); if (available === 0) { return; @@ -466,6 +484,81 @@ const PageEditor = ({ // Track color assignments by insertion order (files keep their color) const fileColorIndexMap = useFileColorMap(orderedFileIds); + // Memoize renderItem to prevent DragDropGrid's React.memo from blocking updates + // when selectedPageIds changes + const renderItemCallback = useCallback(( + page: PDFPage, + index: number, + refs: React.MutableRefObject>, + boxSelectedIds: string[], + clearBoxSelection: () => void, + getBoxSelection: () => string[], + activeId: string | null, + activeDragIds: string[], + justMoved: boolean, + isOver: boolean, + dragHandleProps?: any, + zoomLevelParam?: number + ) => { + gridItemRefsRef.current = refs; + const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0; + const isBoxSelected = boxSelectedIds.includes(page.id); + return ( + {}} + onSetMovingPage={setMovingPage} + onDeletePage={handleDeletePage} + createRotateCommand={createRotateCommand} + createDeleteCommand={createDeleteCommand} + createSplitCommand={createSplitCommand} + pdfDocument={displayDocument!} + setPdfDocument={setEditedDocument} + splitPositions={splitPositions} + onInsertFiles={handleInsertFiles} + zoomLevel={zoomLevelParam || zoomLevel} + /> + ); + }, [ + selectedPageIds, + selectionMode, + movingPage, + isAnimating, + displayDocument, + fileColorIndexMap, + handleReorderPages, + togglePage, + animateReorder, + executeCommand, + setMovingPage, + handleDeletePage, + createRotateCommand, + createDeleteCommand, + createSplitCommand, + setEditedDocument, + splitPositions, + handleInsertFiles, + zoomLevel, + ]); + return (
{ const page = displayDocument.pages.find(p => p.id === pageId); @@ -586,45 +680,7 @@ const PageEditor = ({ rotation: page.rotation || 0 }; }} - renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, _getBoxSelection, _activeId, activeDragIds, justMoved, _isOver, dragHandleProps, zoomLevel) => { - gridItemRefsRef.current = refs; - const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0; - const isBoxSelected = boxSelectedIds.includes(page.id); - return ( - {}} - onSetMovingPage={setMovingPage} - onDeletePage={handleDeletePage} - createRotateCommand={createRotateCommand} - createDeleteCommand={createDeleteCommand} - createSplitCommand={createSplitCommand} - pdfDocument={displayDocument} - setPdfDocument={setEditedDocument} - splitPositions={splitPositions} - onInsertFiles={handleInsertFiles} - zoomLevel={zoomLevel} - /> - ); - }} + renderItem={renderItemCallback} /> )} diff --git a/frontend/src/core/components/pageEditor/PageThumbnail.tsx b/frontend/src/core/components/pageEditor/PageThumbnail.tsx index 851b0a8d5b..8c495ee29e 100644 --- a/frontend/src/core/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/core/components/pageEditor/PageThumbnail.tsx @@ -78,6 +78,7 @@ const PageThumbnail: React.FC = ({ justMoved = false, }: PageThumbnailProps) => { const pageIndex = page.pageNumber - 1; + const isSelected = Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false; const [isMouseDown, setIsMouseDown] = useState(false); const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null); @@ -365,7 +366,7 @@ const PageThumbnail: React.FC = ({ }} > { // Selection is handled by container mouseDown }} diff --git a/frontend/src/core/components/pageEditor/hooks/useEditorCommands.ts b/frontend/src/core/components/pageEditor/hooks/useEditorCommands.ts index 45253492f5..3d5dab1b8a 100644 --- a/frontend/src/core/components/pageEditor/hooks/useEditorCommands.ts +++ b/frontend/src/core/components/pageEditor/hooks/useEditorCommands.ts @@ -283,16 +283,36 @@ export const usePageEditorCommands = ({ insertAfterPage: number, isFromStorage?: boolean ) => { + console.log('[PageEditor] handleInsertFiles called:', { + fileCount: files.length, + insertAfterPage, + isFromStorage, + }); + const workingDocument = getEditedDocument(); - if (!workingDocument || files.length === 0) return; + if (!workingDocument || files.length === 0) { + console.log('[PageEditor] handleInsertFiles early return:', { + hasDocument: !!workingDocument, + fileCount: files.length, + }); + return; + } try { const targetPage = workingDocument.pages.find( (p) => p.pageNumber === insertAfterPage ); - if (!targetPage) return; + if (!targetPage) { + console.log('[PageEditor] Target page not found:', insertAfterPage); + return; + } const insertAfterPageId = targetPage.id; + console.log('[PageEditor] Inserting files after page:', { + pageNumber: insertAfterPage, + pageId: insertAfterPageId, + }); + let addedFileIds: FileId[] = []; if (isFromStorage) { const stubs = files as StirlingFileStub[]; @@ -307,6 +327,10 @@ export const usePageEditorCommands = ({ insertAfterPageId, }); addedFileIds = result.map((file) => file.fileId); + console.log('[PageEditor] Files added to context:', { + addedCount: addedFileIds.length, + fileIds: addedFileIds, + }); } await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts index 279d71d505..b579aec07b 100644 --- a/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useState } from 'react'; +import { useMemo, useEffect, useRef, useState } from 'react'; import { useFileState } from '@app/contexts/FileContext'; import { usePageEditor } from '@app/contexts/PageEditorContext'; import { PDFDocument, PDFPage } from '@app/types/pageEditor'; @@ -64,22 +64,26 @@ export function usePageDocument(): PageDocumentHook { const processedFilePages = primaryStirlingFileStub?.processedFile?.pages; const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages; - const [placeholderDocument, setPlaceholderDocument] = useState(null); + const placeholderDocumentRef = useRef(null); + const [placeholderVersion, setPlaceholderVersion] = useState(0); useEffect(() => { if (!primaryFileId) { - setPlaceholderDocument(null); + placeholderDocumentRef.current = null; + setPlaceholderVersion(v => v + 1); return; } if (primaryStirlingFileStub?.processedFile) { - setPlaceholderDocument(null); + placeholderDocumentRef.current = null; + setPlaceholderVersion(v => v + 1); return; } const file = selectors.getFile(primaryFileId); if (!file) { - setPlaceholderDocument(null); + placeholderDocumentRef.current = null; + setPlaceholderVersion(v => v + 1); return; } @@ -100,16 +104,20 @@ export function usePageDocument(): PageDocumentHook { originalPageNumber: index + 1, })); - setPlaceholderDocument({ - id: `placeholder-${primaryFileId}`, - name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name, - file, - pages, - totalPages, - }); + if (!canceled) { + placeholderDocumentRef.current = { + id: `placeholder-${primaryFileId}`, + name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name, + file, + pages, + totalPages, + }; + setPlaceholderVersion(v => v + 1); + } } catch { if (!canceled) { - setPlaceholderDocument(null); + placeholderDocumentRef.current = null; + setPlaceholderVersion(v => v + 1); } } }; @@ -131,6 +139,7 @@ export function usePageDocument(): PageDocumentHook { activeFileIds: activeFileIds.length, selectedActiveFileIds: selectedActiveFileIds.length, hasPersistedDoc: !!persistedDocument, + persistedDocPages: persistedDocument?.pages.length, persistedSig: persistedDocumentSignature?.substring(0, 50), currentSig: currentPagesSignature.substring(0, 50), }); @@ -140,18 +149,31 @@ export function usePageDocument(): PageDocumentHook { return null; } - if ( + // Check if persisted document is still valid + // Must match signature AND have the same number of source files + const persistedFileIds = persistedDocument + ? Array.from(new Set(persistedDocument.pages.map(p => p.originalFileId).filter(Boolean))) + : []; + const persistedIsValid = persistedDocument && persistedDocumentSignature && - persistedDocumentSignature === currentPagesSignature && - currentPagesSignature.length > 0 - ) { + persistedDocumentSignature === currentPagesSignature && + currentPagesSignature.length > 0 && + persistedFileIds.length === activeFileIds.length; // Ensure file count matches + + if (persistedIsValid) { console.log('[usePageDocument] Using persisted document'); return persistedDocument; + } else if (persistedDocument) { + console.log('[usePageDocument] Persisted document invalid - rebuilding:', { + sigMatch: persistedDocumentSignature === currentPagesSignature, + persistedFiles: persistedFileIds.length, + activeFiles: activeFileIds.length, + }); } - if (!primaryStirlingFileStub?.processedFile && placeholderDocument) { - return placeholderDocument; + if (!primaryStirlingFileStub?.processedFile && placeholderDocumentRef.current) { + return placeholderDocumentRef.current; } const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; @@ -180,6 +202,10 @@ export function usePageDocument(): PageDocumentHook { activeFileIds.forEach(fileId => { const record = selectors.getStirlingFileStub(fileId); if (record?.insertAfterPageId !== undefined) { + console.log('[usePageDocument] File has insertAfterPageId:', { + fileId, + insertAfterPageId: record.insertAfterPageId, + }); if (!insertionMap.has(record.insertAfterPageId)) { insertionMap.set(record.insertAfterPageId, []); } @@ -189,6 +215,12 @@ export function usePageDocument(): PageDocumentHook { } }); + console.log('[usePageDocument] File categorization:', { + originalFiles: originalFileIds.length, + filesToInsert: insertionMap.size, + totalActive: activeFileIds.length, + }); + // Build pages by interleaving original pages with insertions let pages: PDFPage[] = []; @@ -243,6 +275,20 @@ export function usePageDocument(): PageDocumentHook { splitAfter: false, isPlaceholder: false, })); + } else { + // No processedFile yet - create a single loading placeholder + // This will be replaced when processing completes + filePages = [{ + id: `${fileId}-loading`, + pageNumber: startPageNumber, + originalPageNumber: 1, + originalFileId: fileId, + rotation: 0, + thumbnail: null, + selected: false, + splitAfter: false, + isPlaceholder: true, + }]; } return filePages; @@ -344,12 +390,13 @@ export function usePageDocument(): PageDocumentHook { activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds, + state.files.byId, // Force recompute when any file stub changes (including processedFile updates) allFileIds, currentPagesSignature, currentPages, persistedDocument, persistedDocumentSignature, - placeholderDocument, + placeholderVersion, ]); // Large document detection for smart loading diff --git a/frontend/src/core/components/pageEditor/hooks/usePageEditorState.ts b/frontend/src/core/components/pageEditor/hooks/usePageEditorState.ts index 2c37990d16..8fd5821356 100644 --- a/frontend/src/core/components/pageEditor/hooks/usePageEditorState.ts +++ b/frontend/src/core/components/pageEditor/hooks/usePageEditorState.ts @@ -51,12 +51,13 @@ export function usePageEditorState(): PageEditorState { // Helper functions const togglePage = useCallback((pageId: string) => { - setSelectedPageIds(prev => - prev.includes(pageId) + setSelectedPageIds(prev => { + const newSelection = prev.includes(pageId) ? prev.filter(id => id !== pageId) - : [...prev, pageId] - ); - }, []); + : [...prev, pageId]; + return newSelection; + }); + }, []); // Empty deps - uses updater function so always has latest state const toggleSelectAll = useCallback((allPageIds: string[]) => { if (!allPageIds.length) return; diff --git a/frontend/src/core/contexts/PageEditorContext.tsx b/frontend/src/core/contexts/PageEditorContext.tsx index a091f41258..c73b72e311 100644 --- a/frontend/src/core/contexts/PageEditorContext.tsx +++ b/frontend/src/core/contexts/PageEditorContext.tsx @@ -3,6 +3,7 @@ import { FileId } from '@app/types/file'; import { useFileActions, useFileState } from '@app/contexts/FileContext'; import { PDFDocument, PDFPage } from '@app/types/pageEditor'; import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors'; +import { useNavigationState } from '@app/contexts/NavigationContext'; // PageEditorFile is now defined locally in consuming components // Components should derive file list directly from FileContext @@ -154,10 +155,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { }, []); const clearPersistedDocument = useCallback(() => { - console.log('[PageEditorContext] Clearing persisted document and currentPages'); + console.log('[PageEditorContext] Clearing persisted document'); setPersistedDocument(null); setPersistedDocumentSignature(null); - setCurrentPages(null); // Also clear currentPages to avoid stale signature comparison + setCurrentPages(null); }, []); // Page editor's own file order (independent of FileContext) @@ -167,6 +168,42 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { const { actions: fileActions } = useFileActions(); const { state } = useFileState(); + const navigationState = useNavigationState(); + const prevWorkbenchRef = useRef(navigationState.workbench); + useEffect(() => { + const prevWorkbench = prevWorkbenchRef.current; + const nextWorkbench = navigationState.workbench; + const isLeavingPageEditor = prevWorkbench === 'pageEditor' && nextWorkbench !== 'pageEditor'; + const isEnteringPageEditor = prevWorkbench !== 'pageEditor' && nextWorkbench === 'pageEditor'; + + if (isLeavingPageEditor) { + clearPersistedDocument(); + } + + if (isEnteringPageEditor) { + prevFileContextIdsRef.current = state.files.ids; + setReorderedPages(null); + setCurrentPages(null); // Force clear current pages when entering + setFileOrder(currentOrder => { + const validOrder = currentOrder.filter(id => state.files.ids.includes(id)); + const newIds = state.files.ids.filter(id => !validOrder.includes(id)); + if (newIds.length === 0 && validOrder.length === currentOrder.length) { + return currentOrder; + } + return [...validOrder, ...newIds]; + }); + clearPersistedDocument(); + } + + prevWorkbenchRef.current = nextWorkbench; + }, [ + navigationState.workbench, + clearPersistedDocument, + state.files.ids, + setFileOrder, + setReorderedPages, + ]); + const fileContextSignature = useMemo(() => { const sig = state.files.ids .map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`) @@ -179,16 +216,41 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { }, [state.files.ids, state.files.byId]); const prevFileContextSignature = useRef(null); - useEffect(() => { - if (prevFileContextSignature.current !== fileContextSignature) { - console.log('[PageEditorContext] File signature changed, clearing persisted document:', { - prev: prevFileContextSignature.current?.substring(0, 50), - current: fileContextSignature.substring(0, 50), - }); - prevFileContextSignature.current = fileContextSignature; - clearPersistedDocument(); + const haveFileIdSetsChanged = (prevIds: FileId[], currentIds: FileId[]) => { + if (prevIds.length !== currentIds.length) { + return true; } - }, [fileContextSignature, clearPersistedDocument]); + const prevSet = new Set(prevIds); + for (const id of currentIds) { + if (!prevSet.has(id)) { + return true; + } + } + return false; + }; + useEffect(() => { + const currentFileIds = state.files.ids; + const prevFileIds = prevFileContextIdsRef.current; + const idsChanged = haveFileIdSetsChanged(prevFileIds, currentFileIds); + + if (!idsChanged && prevFileContextSignature.current === fileContextSignature) { + return; + } + + const previousSignature = prevFileContextSignature.current; + prevFileContextSignature.current = fileContextSignature; + + if (!idsChanged) { + // Signature changed due to metadata/version updates but file set is unchanged. + return; + } + + console.log('[PageEditorContext] File signature changed (IDs/versions changed), clearing persisted document:', { + prev: previousSignature?.substring(0, 50), + current: fileContextSignature.substring(0, 50), + }); + clearPersistedDocument(); + }, [fileContextSignature, clearPersistedDocument, state.files.ids]); // Keep a ref to always read latest state in stable callbacks const stateRef = useRef(state); @@ -205,9 +267,7 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { const prevFileIds = prevFileContextIdsRef.current; // Only react to FileContext changes, not our own fileOrder changes - const fileContextChanged = - currentFileIds.length !== prevFileIds.length || - !currentFileIds.every((id, idx) => id === prevFileIds[idx]); + const fileContextChanged = haveFileIdSetsChanged(prevFileIds, currentFileIds); if (!fileContextChanged) { return; @@ -220,6 +280,8 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { removed: prevFileIds.filter(id => !currentFileIds.includes(id)).length, }); + clearPersistedDocument(); + prevFileContextIdsRef.current = currentFileIds; // Collect new file IDs outside the setState callback so we can clear them after diff --git a/frontend/src/core/contexts/file/fileActions.ts b/frontend/src/core/contexts/file/fileActions.ts index 574e3a692b..adf05f9e0d 100644 --- a/frontend/src/core/contexts/file/fileActions.ts +++ b/frontend/src/core/contexts/file/fileActions.ts @@ -31,14 +31,16 @@ const scheduleMetadataHydration = (task: () => Promise): void => { }; const drainHydrationQueue = (): void => { - if (activeHydrations >= HYDRATION_CONCURRENCY) return; + if (activeHydrations >= HYDRATION_CONCURRENCY) { + return; + } const nextTask = hydrationQueue.shift(); if (!nextTask) return; activeHydrations++; nextTask() - .catch(() => { - // Silently handle hydration failures + .catch((error) => { + console.error('[Hydration] Task failed with error:', error); }) .finally(() => { activeHydrations--; @@ -341,8 +343,8 @@ export async function addFiles( try { const { generateThumbnailForFile } = await import('@app/utils/thumbnailUtils'); thumbnail = await generateThumbnailForFile(targetFile); - } catch { - // Silently handle thumbnail generation failures + } catch (error) { + console.warn(`[addFiles] Thumbnail generation failed for ${fileId}:`, error); } } @@ -640,7 +642,6 @@ export async function addStirlingFileStubs( scheduleMetadataHydration(async () => { const stirlingFile = await fileStorage.getStirlingFile(fileId); if (!stirlingFile) { - console.warn(`📄 Failed to load StirlingFile for stub: ${stub.name} (${fileId})`); return; } @@ -657,6 +658,7 @@ export async function addStirlingFileStubs( if (needsProcessing) { // Regenerate metadata const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile); + if (processedFileMetadata) { const updates: Partial = { processedFile: processedFileMetadata diff --git a/frontend/src/core/utils/thumbnailUtils.ts b/frontend/src/core/utils/thumbnailUtils.ts index eda7825500..89d793d632 100644 --- a/frontend/src/core/utils/thumbnailUtils.ts +++ b/frontend/src/core/utils/thumbnailUtils.ts @@ -390,13 +390,8 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b return { thumbnail, pageCount: 0 }; } - // Skip very large files - if (file.size >= 100 * 1024 * 1024) { - const thumbnail = generatePlaceholderThumbnail(file); - return { thumbnail, pageCount: 1 }; - } - const scale = calculateScaleFromFileSize(file.size); + const isVeryLarge = file.size >= 100 * 1024 * 1024; // 100MB threshold try { const arrayBuffer = await file.arrayBuffer(); @@ -430,6 +425,18 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b await page.render({ canvasContext: context, viewport, canvas }).promise; const thumbnail = canvas.toDataURL(); + // For very large files, skip reading rotation/dimensions for all pages (just use first page data) + if (isVeryLarge) { + const rotation = page.rotate || 0; + pdfWorkerManager.destroyDocument(pdf); + return { + thumbnail, + pageCount, + pageRotations: [rotation], + pageDimensions: [pageDimensions[0]] + }; + } + // Read rotation for all pages const pageRotations: number[] = []; for (let i = 1; i <= pageCount; i++) {