mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Bug/pageeditor virtualisation (#5614)
This commit is contained in:
@@ -141,14 +141,15 @@ export default function Workbench() {
|
||||
);
|
||||
|
||||
case "pageEditor":
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative', flex: '1 1 0', height: 0 }}>
|
||||
<PageEditor
|
||||
onFunctionsReady={setPageEditorFunctions}
|
||||
/>
|
||||
{pageEditorFunctions && (
|
||||
<PageEditorControls
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 100 }}>
|
||||
<PageEditorControls
|
||||
onClosePdf={pageEditorFunctions.closePdf}
|
||||
onUndo={pageEditorFunctions.handleUndo}
|
||||
onRedo={pageEditorFunctions.handleRedo}
|
||||
@@ -168,8 +169,9 @@ export default function Workbench() {
|
||||
splitPositions={pageEditorFunctions.splitPositions}
|
||||
totalPages={pageEditorFunctions.totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
@@ -207,9 +209,10 @@ export default function Workbench() {
|
||||
|
||||
{/* Main content area */}
|
||||
<Box
|
||||
className={`flex-1 min-h-0 relative z-10 ${styles.workbenchScrollable}`}
|
||||
className={`flex-1 min-h-0 z-10 ${currentView === 'pageEditor' ? 'relative flex flex-col' : `relative ${styles.workbenchScrollable}`}`}
|
||||
style={{
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
...(currentView === 'pageEditor' && { height: 0 }),
|
||||
}}
|
||||
>
|
||||
{renderMainContent()}
|
||||
|
||||
@@ -37,6 +37,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
zoomLevel?: number;
|
||||
selectedFileIds?: string[];
|
||||
onVisibleItemsChange?: (items: T[]) => void;
|
||||
}
|
||||
|
||||
type DropSide = 'left' | 'right' | null;
|
||||
@@ -198,7 +199,7 @@ interface DraggableItemProps<T extends DragDropItem> {
|
||||
zoomLevel: number;
|
||||
}
|
||||
|
||||
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
|
||||
const isPlaceholder = Boolean(item.isPlaceholder);
|
||||
const pageNumber = (item as any).pageNumber ?? index + 1;
|
||||
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
||||
@@ -252,6 +253,31 @@ const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelec
|
||||
);
|
||||
};
|
||||
|
||||
// Memoize to prevent unnecessary re-renders and hook thrashing
|
||||
const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => {
|
||||
// Return true to SKIP re-render (props are equal)
|
||||
// Return false to RE-RENDER (props changed)
|
||||
|
||||
// Check if item reference or content changed (including thumbnail)
|
||||
const itemChanged = prevProps.item !== nextProps.item;
|
||||
|
||||
// If item object reference changed, we need to re-render
|
||||
if (itemChanged) {
|
||||
return false; // Props changed, re-render needed
|
||||
}
|
||||
|
||||
// Item reference is same, check other props
|
||||
return (
|
||||
prevProps.item.id === nextProps.item.id &&
|
||||
prevProps.index === nextProps.index &&
|
||||
prevProps.activeId === nextProps.activeId &&
|
||||
prevProps.justMoved === nextProps.justMoved &&
|
||||
prevProps.zoomLevel === nextProps.zoomLevel &&
|
||||
prevProps.activeDragIds.length === nextProps.activeDragIds.length &&
|
||||
prevProps.boxSelectedPageIds.length === nextProps.boxSelectedPageIds.length
|
||||
);
|
||||
}) as typeof DraggableItemInner;
|
||||
|
||||
const DragDropGrid = <T extends DragDropItem>({
|
||||
items,
|
||||
renderItem,
|
||||
@@ -259,6 +285,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
getThumbnailData,
|
||||
zoomLevel = 1.0,
|
||||
selectedFileIds,
|
||||
onVisibleItemsChange,
|
||||
}: DragDropGridProps<T>) => {
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -421,6 +448,21 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
overscan: OVERSCAN,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useEffect(() => {
|
||||
if (!onVisibleItemsChange) return;
|
||||
|
||||
const visibleItemsForCallback: T[] = [];
|
||||
virtualRows.forEach((row) => {
|
||||
const startIndex = row.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
visibleItemsForCallback.push(...visibleItems.slice(startIndex, endIndex));
|
||||
});
|
||||
|
||||
onVisibleItemsChange(visibleItemsForCallback);
|
||||
}, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]);
|
||||
|
||||
// Re-measure virtualizer when zoom or items per row changes
|
||||
useEffect(() => {
|
||||
rowVirtualizer.measure();
|
||||
@@ -719,7 +761,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
const rowItems = visibleItems.slice(startIndex, endIndex);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useFileState, useFileActions } from "@app/contexts/FileContext";
|
||||
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
||||
import { usePageEditor } from "@app/contexts/PageEditorContext";
|
||||
import { PageEditorFunctions } from "@app/types/pageEditor";
|
||||
import { PageEditorFunctions, PDFPage } from "@app/types/pageEditor";
|
||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||
import '@app/components/pageEditor/PageEditor.module.css';
|
||||
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
|
||||
@@ -23,6 +23,7 @@ import { useUndoManagerState } from "@app/components/pageEditor/hooks/useUndoMan
|
||||
import { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePageSelectionManager";
|
||||
import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands";
|
||||
import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport";
|
||||
import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration";
|
||||
|
||||
export interface PageEditorProps {
|
||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||
@@ -40,7 +41,27 @@ const PageEditor = ({
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
// Get PageEditor coordination functions
|
||||
const { updateFileOrderFromPages, fileOrder, reorderedPages, clearReorderedPages, updateCurrentPages } = usePageEditor();
|
||||
const {
|
||||
updateFileOrderFromPages,
|
||||
fileOrder,
|
||||
reorderedPages,
|
||||
clearReorderedPages,
|
||||
updateCurrentPages,
|
||||
savePersistedDocument,
|
||||
} = usePageEditor();
|
||||
|
||||
const [visiblePageIds, setVisiblePageIds] = useState<string[]>([]);
|
||||
const thumbnailRequestsRef = useRef<Set<string>>(new Set());
|
||||
const { requestThumbnail, getThumbnailFromCache } = useThumbnailGeneration();
|
||||
const handleVisibleItemsChange = useCallback((items: PDFPage[]) => {
|
||||
setVisiblePageIds(prev => {
|
||||
const ids = items.map(item => item.id);
|
||||
if (prev.length === ids.length && prev.every((id, index) => id === ids[index])) {
|
||||
return prev;
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Zoom state management
|
||||
const [zoomLevel, setZoomLevel] = useState(1.0);
|
||||
@@ -149,6 +170,21 @@ const PageEditor = ({
|
||||
updateCurrentPages,
|
||||
});
|
||||
|
||||
const displayDocumentRef = useRef(displayDocument);
|
||||
useEffect(() => {
|
||||
displayDocumentRef.current = displayDocument;
|
||||
}, [displayDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const doc = displayDocumentRef.current;
|
||||
if (doc && doc.pages.length > 0) {
|
||||
const signature = doc.pages.map(page => page.id).join(',');
|
||||
savePersistedDocument(doc, signature);
|
||||
}
|
||||
};
|
||||
}, [savePersistedDocument]);
|
||||
|
||||
// UI state management
|
||||
const {
|
||||
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
||||
@@ -231,6 +267,92 @@ const PageEditor = ({
|
||||
setSplitPositions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayDocument || visiblePageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = thumbnailRequestsRef.current.size;
|
||||
const MAX_CONCURRENT_THUMBNAILS = 12;
|
||||
const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending);
|
||||
if (available === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toLoad: string[] = [];
|
||||
for (const pageId of visiblePageIds) {
|
||||
if (toLoad.length >= available) break;
|
||||
if (thumbnailRequestsRef.current.has(pageId)) continue;
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page || page.thumbnail) continue;
|
||||
toLoad.push(pageId);
|
||||
}
|
||||
|
||||
if (toLoad.length === 0) return;
|
||||
|
||||
toLoad.forEach(pageId => {
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page) return;
|
||||
|
||||
const cached = getThumbnailFromCache(pageId);
|
||||
if (cached) {
|
||||
thumbnailRequestsRef.current.add(pageId);
|
||||
Promise.resolve(cached)
|
||||
.then(cache => {
|
||||
setEditedDocument(prev => {
|
||||
if (!prev) return prev;
|
||||
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
|
||||
if (pageIndex === -1) return prev;
|
||||
|
||||
// Only create new page object for the changed page, reuse rest
|
||||
const updated = [...prev.pages];
|
||||
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail: cache };
|
||||
return { ...prev, pages: updated };
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
thumbnailRequestsRef.current.delete(pageId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = page.originalFileId;
|
||||
if (!fileId) return;
|
||||
const file = selectors.getFile(fileId);
|
||||
if (!file) return;
|
||||
|
||||
thumbnailRequestsRef.current.add(pageId);
|
||||
requestThumbnail(pageId, file, page.originalPageNumber || page.pageNumber)
|
||||
.then(thumbnail => {
|
||||
if (thumbnail) {
|
||||
setEditedDocument(prev => {
|
||||
if (!prev) return prev;
|
||||
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
|
||||
if (pageIndex === -1) return prev;
|
||||
|
||||
// Only create new page object for the changed page, reuse rest
|
||||
const updated = [...prev.pages];
|
||||
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail };
|
||||
return { ...prev, pages: updated };
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Thumbnail Loading] Error:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
thumbnailRequestsRef.current.delete(pageId);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
displayDocument,
|
||||
visiblePageIds,
|
||||
selectors,
|
||||
requestThumbnail,
|
||||
getThumbnailFromCache,
|
||||
setEditedDocument,
|
||||
]);
|
||||
|
||||
// Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument)
|
||||
const selectedPageCount = selectedPageIds.length;
|
||||
const activeFileIds = selectedFileIds;
|
||||
@@ -345,12 +467,17 @@ const PageEditor = ({
|
||||
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
||||
|
||||
return (
|
||||
<Box
|
||||
<div
|
||||
ref={containerRef}
|
||||
pos="relative"
|
||||
data-scrolling-container="true"
|
||||
onMouseEnter={() => setIsContainerHovered(true)}
|
||||
onMouseLeave={() => setIsContainerHovered(false)}
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<LoadingOverlay visible={globalProcessing && !initialDocument} />
|
||||
|
||||
@@ -372,7 +499,7 @@ const PageEditor = ({
|
||||
)}
|
||||
|
||||
{displayDocument && (
|
||||
<Box ref={gridContainerRef} p={0} pt="2rem" pb="15rem" style={{ position: 'relative' }}>
|
||||
<Box ref={gridContainerRef} p={0} pt="2rem" pb="4rem" style={{ position: 'relative' }}>
|
||||
|
||||
{/* Split Lines Overlay */}
|
||||
<div
|
||||
@@ -450,6 +577,7 @@ const PageEditor = ({
|
||||
onReorderPages={handleReorderPages}
|
||||
zoomLevel={zoomLevel}
|
||||
selectedFileIds={selectedFileIds}
|
||||
onVisibleItemsChange={handleVisibleItemsChange}
|
||||
getThumbnailData={(pageId) => {
|
||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||
if (!page?.thumbnail) return null;
|
||||
@@ -468,7 +596,6 @@ const PageEditor = ({
|
||||
page={page}
|
||||
index={index}
|
||||
totalPages={displayDocument.pages.length}
|
||||
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
|
||||
fileColorIndex={fileColorIndex}
|
||||
selectedPageIds={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
@@ -512,7 +639,7 @@ const PageEditor = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
|
||||
import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors';
|
||||
import styles from '@app/components/pageEditor/PageEditor.module.css';
|
||||
@@ -22,7 +21,6 @@ interface PageThumbnailProps {
|
||||
page: PDFPage;
|
||||
index: number;
|
||||
totalPages: number;
|
||||
originalFile?: File;
|
||||
fileColorIndex: number;
|
||||
selectedPageIds: string[];
|
||||
selectionMode: boolean;
|
||||
@@ -55,7 +53,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
page,
|
||||
index: _index,
|
||||
totalPages,
|
||||
originalFile,
|
||||
fileColorIndex,
|
||||
selectedPageIds,
|
||||
selectionMode,
|
||||
@@ -90,7 +87,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
|
||||
// Check if this page is currently being dragged
|
||||
@@ -115,43 +111,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
}
|
||||
}, [page.thumbnail, thumbnailUrl]);
|
||||
|
||||
// Request thumbnail if missing (on-demand, virtualized approach)
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
// If we already have a thumbnail, use it
|
||||
if (page.thumbnail) {
|
||||
setThumbnailUrl(page.thumbnail);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const cachedThumbnail = getThumbnailFromCache(page.id);
|
||||
if (cachedThumbnail) {
|
||||
setThumbnailUrl(cachedThumbnail);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request thumbnail generation if we have the original file
|
||||
if (originalFile) {
|
||||
const pageNumber = page.originalPageNumber;
|
||||
|
||||
requestThumbnail(page.id, originalFile, pageNumber)
|
||||
.then(thumbnail => {
|
||||
if (!isCancelled && thumbnail) {
|
||||
setThumbnailUrl(thumbnail);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
|
||||
|
||||
// Merge refs - combine our ref tracking with dnd-kit's ref
|
||||
const mergedRef = useCallback((element: HTMLDivElement | null) => {
|
||||
// Track in our refs map
|
||||
|
||||
@@ -748,43 +748,49 @@ export class InsertFilesCommand extends DOMCommand {
|
||||
console.log('Pages:', pages.length);
|
||||
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
||||
|
||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||
// Extract page numbers for all pages from this file
|
||||
const pageNumbers = pages.map(page => {
|
||||
const pageNumMatch = page.id.match(/-page-(\d+)$/);
|
||||
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
|
||||
});
|
||||
try {
|
||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||
// Extract page numbers for all pages from this file
|
||||
const pageNumbers = pages.map(page => {
|
||||
const pageNumMatch = page.id.match(/-page-(\d+)$/);
|
||||
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
|
||||
});
|
||||
|
||||
console.log('Generating thumbnails for page numbers:', pageNumbers);
|
||||
console.log('Generating thumbnails for page numbers:', pageNumbers);
|
||||
|
||||
// Generate thumbnails for all pages from this file at once
|
||||
const results = await thumbnailGenerationService.generateThumbnails(
|
||||
fileId,
|
||||
arrayBuffer,
|
||||
pageNumbers,
|
||||
{ scale: 0.2, quality: 0.8 }
|
||||
);
|
||||
// Generate thumbnails for all pages from this file at once
|
||||
const results = await thumbnailGenerationService.generateThumbnails(
|
||||
fileId,
|
||||
arrayBuffer,
|
||||
pageNumbers,
|
||||
{ scale: 0.2, quality: 0.8 }
|
||||
);
|
||||
|
||||
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
|
||||
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
|
||||
|
||||
// Update pages with generated thumbnails
|
||||
for (let i = 0; i < results.length && i < pages.length; i++) {
|
||||
const result = results[i];
|
||||
const page = pages[i];
|
||||
// Update pages with generated thumbnails
|
||||
for (let i = 0; i < results.length && i < pages.length; i++) {
|
||||
const result = results[i];
|
||||
const page = pages[i];
|
||||
|
||||
if (result.success) {
|
||||
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
|
||||
if (pageIndex >= 0) {
|
||||
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
|
||||
console.log('Updated thumbnail for page:', page.id);
|
||||
if (result.success) {
|
||||
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
|
||||
if (pageIndex >= 0) {
|
||||
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
|
||||
console.log('Updated thumbnail for page:', page.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger re-render by updating the document
|
||||
this.setDocument({ ...updatedDocument });
|
||||
} else {
|
||||
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
||||
// Trigger re-render by updating the document
|
||||
this.setDocument({ ...updatedDocument });
|
||||
} else {
|
||||
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate thumbnails for file:', fileId, error);
|
||||
} finally {
|
||||
this.fileDataMap.delete(fileId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,6 @@ export const GRID_CONSTANTS = {
|
||||
ITEM_WIDTH: '20rem', // page width
|
||||
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
|
||||
ITEM_GAP: '1.5rem', // gap between items
|
||||
OVERSCAN_SMALL: 4, // Overscan for normal documents
|
||||
OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages)
|
||||
OVERSCAN_SMALL: 8, // Overscan for normal documents
|
||||
OVERSCAN_LARGE: 12, // Overscan for large documents (12 rows = ~96 pages pre-rendered)
|
||||
} as const;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { useFileState } from '@app/contexts/FileContext';
|
||||
import { usePageEditor } from '@app/contexts/PageEditorContext';
|
||||
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||
import { FileId } from '@app/types/file';
|
||||
import { FileAnalyzer } from '@app/services/fileAnalyzer';
|
||||
|
||||
export interface PageDocumentHook {
|
||||
document: PDFDocument | null;
|
||||
@@ -16,7 +17,7 @@ export interface PageDocumentHook {
|
||||
*/
|
||||
export function usePageDocument(): PageDocumentHook {
|
||||
const { state, selectors } = useFileState();
|
||||
const { fileOrder, currentPages } = usePageEditor();
|
||||
const { fileOrder, currentPages, persistedDocument, persistedDocumentSignature } = usePageEditor();
|
||||
|
||||
// Use PageEditorContext's fileOrder instead of FileContext's global order
|
||||
// This ensures the page editor respects its own workspace ordering
|
||||
@@ -58,6 +59,63 @@ export function usePageDocument(): PageDocumentHook {
|
||||
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
|
||||
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
|
||||
|
||||
const [placeholderDocument, setPlaceholderDocument] = useState<PDFDocument | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!primaryFileId) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (primaryStirlingFileStub?.processedFile) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = selectors.getFile(primaryFileId);
|
||||
if (!file) {
|
||||
setPlaceholderDocument(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
|
||||
const loadPlaceholder = async () => {
|
||||
try {
|
||||
const analysis = await FileAnalyzer.quickPDFAnalysis(file);
|
||||
if (canceled) return;
|
||||
const totalPages = Math.max(1, analysis.pageCount || 1);
|
||||
const pages: PDFPage[] = Array.from({ length: totalPages }, (_, index) => ({
|
||||
id: `placeholder-${primaryFileId}-page-${index + 1}`,
|
||||
pageNumber: index + 1,
|
||||
thumbnail: null,
|
||||
rotation: 0,
|
||||
selected: false,
|
||||
originalFileId: primaryFileId,
|
||||
originalPageNumber: index + 1,
|
||||
}));
|
||||
|
||||
setPlaceholderDocument({
|
||||
id: `placeholder-${primaryFileId}`,
|
||||
name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name,
|
||||
file,
|
||||
pages,
|
||||
totalPages,
|
||||
});
|
||||
} catch {
|
||||
if (!canceled) {
|
||||
setPlaceholderDocument(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPlaceholder();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [primaryFileId, primaryStirlingFileStub?.processedFile, selectors]);
|
||||
|
||||
// Compute merged document with stable signature (prevents infinite loops)
|
||||
const currentPagesSignature = useMemo(() => {
|
||||
return currentPages ? currentPages.map(page => page.id).join(',') : '';
|
||||
@@ -66,7 +124,20 @@ export function usePageDocument(): PageDocumentHook {
|
||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||
if (activeFileIds.length === 0) return null;
|
||||
|
||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||
if (
|
||||
persistedDocument &&
|
||||
persistedDocumentSignature &&
|
||||
persistedDocumentSignature === currentPagesSignature &&
|
||||
currentPagesSignature.length > 0
|
||||
) {
|
||||
return persistedDocument;
|
||||
}
|
||||
|
||||
if (!primaryStirlingFileStub?.processedFile && placeholderDocument) {
|
||||
return placeholderDocument;
|
||||
}
|
||||
|
||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||
|
||||
// If we have file IDs but no file record, something is wrong - return null to show loading
|
||||
if (!primaryStirlingFileStub) {
|
||||
@@ -245,7 +316,24 @@ export function usePageDocument(): PageDocumentHook {
|
||||
};
|
||||
|
||||
return mergedDoc;
|
||||
}, [activeFileIds, selectedActiveFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds, allFileIds, currentPagesSignature, currentPages]);
|
||||
}, [
|
||||
activeFileIds,
|
||||
selectedActiveFileIds,
|
||||
primaryFileId,
|
||||
primaryStirlingFileStub,
|
||||
processedFilePages,
|
||||
processedFileTotalPages,
|
||||
selectors,
|
||||
activeFilesSignature,
|
||||
selectedFileIdsKey,
|
||||
state.ui.selectedFileIds,
|
||||
allFileIds,
|
||||
currentPagesSignature,
|
||||
currentPages,
|
||||
persistedDocument,
|
||||
persistedDocumentSignature,
|
||||
placeholderDocument,
|
||||
]);
|
||||
|
||||
// Large document detection for smart loading
|
||||
const isVeryLargeDocument = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user