Fix load on big pages/ select issue/ addfiles bug
This commit is contained in:
@@ -37,6 +37,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
zoomLevel?: number;
|
||||
selectedFileIds?: string[];
|
||||
selectedPageIds?: string[];
|
||||
onVisibleItemsChange?: (items: T[]) => void;
|
||||
}
|
||||
|
||||
@@ -197,6 +198,7 @@ interface DraggableItemProps<T extends DragDropItem> {
|
||||
onUpdateDropTarget: (itemId: string | null) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, 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 = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
@@ -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 = <T extends DragDropItem>({
|
||||
getThumbnailData,
|
||||
zoomLevel = 1.0,
|
||||
selectedFileIds,
|
||||
selectedPageIds,
|
||||
onVisibleItemsChange,
|
||||
}: DragDropGridProps<T>) => {
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -294,6 +309,10 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
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 = <T extends DragDropItem>({
|
||||
});
|
||||
|
||||
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 = <T extends DragDropItem>({
|
||||
}, [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 = <T extends DragDropItem>({
|
||||
onUpdateDropTarget={setHoveredItemId}
|
||||
renderItem={renderItem}
|
||||
zoomLevel={zoomLevel}
|
||||
selectedPageIds={selectedPageIds}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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<Map<string, HTMLDivElement>>,
|
||||
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 (
|
||||
<PageThumbnail
|
||||
key={page.id}
|
||||
page={page}
|
||||
index={index}
|
||||
totalPages={displayDocument?.pages.length || 0}
|
||||
fileColorIndex={fileColorIndex}
|
||||
selectedPageIds={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
movingPage={movingPage}
|
||||
isAnimating={isAnimating}
|
||||
isBoxSelected={isBoxSelected}
|
||||
clearBoxSelection={clearBoxSelection}
|
||||
activeDragIds={activeDragIds}
|
||||
justMoved={justMoved}
|
||||
pageRefs={refs}
|
||||
dragHandleProps={dragHandleProps}
|
||||
onReorderPages={handleReorderPages}
|
||||
onTogglePage={togglePage}
|
||||
onAnimateReorder={animateReorder}
|
||||
onExecuteCommand={executeCommand}
|
||||
onSetStatus={() => {}}
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -577,6 +670,7 @@ const PageEditor = ({
|
||||
onReorderPages={handleReorderPages}
|
||||
zoomLevel={zoomLevel}
|
||||
selectedFileIds={selectedFileIds}
|
||||
selectedPageIds={selectedPageIds}
|
||||
onVisibleItemsChange={handleVisibleItemsChange}
|
||||
getThumbnailData={(pageId) => {
|
||||
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 (
|
||||
<PageThumbnail
|
||||
key={page.id}
|
||||
page={page}
|
||||
index={index}
|
||||
totalPages={displayDocument.pages.length}
|
||||
fileColorIndex={fileColorIndex}
|
||||
selectedPageIds={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
movingPage={movingPage}
|
||||
isAnimating={isAnimating}
|
||||
isBoxSelected={isBoxSelected}
|
||||
clearBoxSelection={clearBoxSelection}
|
||||
activeDragIds={activeDragIds}
|
||||
justMoved={justMoved}
|
||||
pageRefs={refs}
|
||||
dragHandleProps={dragHandleProps}
|
||||
onReorderPages={handleReorderPages}
|
||||
onTogglePage={togglePage}
|
||||
onAnimateReorder={animateReorder}
|
||||
onExecuteCommand={executeCommand}
|
||||
onSetStatus={() => {}}
|
||||
onSetMovingPage={setMovingPage}
|
||||
onDeletePage={handleDeletePage}
|
||||
createRotateCommand={createRotateCommand}
|
||||
createDeleteCommand={createDeleteCommand}
|
||||
createSplitCommand={createSplitCommand}
|
||||
pdfDocument={displayDocument}
|
||||
setPdfDocument={setEditedDocument}
|
||||
splitPositions={splitPositions}
|
||||
onInsertFiles={handleInsertFiles}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderItem={renderItemCallback}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -78,6 +78,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
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<PageThumbnailProps> = ({
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false}
|
||||
checked={isSelected}
|
||||
onChange={() => {
|
||||
// Selection is handled by container mouseDown
|
||||
}}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<PDFDocument | null>(null);
|
||||
const placeholderDocumentRef = useRef<PDFDocument | null>(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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string | null>(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
|
||||
|
||||
@@ -31,14 +31,16 @@ const scheduleMetadataHydration = (task: () => Promise<void>): 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<StirlingFileStub> = {
|
||||
processedFile: processedFileMetadata
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user