mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Bug/pageeditor virtualisation (#5614)
This commit is contained in:
@@ -143,11 +143,12 @@ export default function Workbench() {
|
|||||||
case "pageEditor":
|
case "pageEditor":
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ position: 'relative', flex: '1 1 0', height: 0 }}>
|
||||||
<PageEditor
|
<PageEditor
|
||||||
onFunctionsReady={setPageEditorFunctions}
|
onFunctionsReady={setPageEditorFunctions}
|
||||||
/>
|
/>
|
||||||
{pageEditorFunctions && (
|
{pageEditorFunctions && (
|
||||||
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 100 }}>
|
||||||
<PageEditorControls
|
<PageEditorControls
|
||||||
onClosePdf={pageEditorFunctions.closePdf}
|
onClosePdf={pageEditorFunctions.closePdf}
|
||||||
onUndo={pageEditorFunctions.handleUndo}
|
onUndo={pageEditorFunctions.handleUndo}
|
||||||
@@ -168,8 +169,9 @@ export default function Workbench() {
|
|||||||
splitPositions={pageEditorFunctions.splitPositions}
|
splitPositions={pageEditorFunctions.splitPositions}
|
||||||
totalPages={pageEditorFunctions.totalPages}
|
totalPages={pageEditorFunctions.totalPages}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -207,9 +209,10 @@ export default function Workbench() {
|
|||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<Box
|
<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={{
|
style={{
|
||||||
transition: 'opacity 0.15s ease-in-out',
|
transition: 'opacity 0.15s ease-in-out',
|
||||||
|
...(currentView === 'pageEditor' && { height: 0 }),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderMainContent()}
|
{renderMainContent()}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
|||||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
selectedFileIds?: string[];
|
selectedFileIds?: string[];
|
||||||
|
onVisibleItemsChange?: (items: T[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropSide = 'left' | 'right' | null;
|
type DropSide = 'left' | 'right' | null;
|
||||||
@@ -198,7 +199,7 @@ interface DraggableItemProps<T extends DragDropItem> {
|
|||||||
zoomLevel: number;
|
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 isPlaceholder = Boolean(item.isPlaceholder);
|
||||||
const pageNumber = (item as any).pageNumber ?? index + 1;
|
const pageNumber = (item as any).pageNumber ?? index + 1;
|
||||||
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
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>({
|
const DragDropGrid = <T extends DragDropItem>({
|
||||||
items,
|
items,
|
||||||
renderItem,
|
renderItem,
|
||||||
@@ -259,6 +285,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
getThumbnailData,
|
getThumbnailData,
|
||||||
zoomLevel = 1.0,
|
zoomLevel = 1.0,
|
||||||
selectedFileIds,
|
selectedFileIds,
|
||||||
|
onVisibleItemsChange,
|
||||||
}: DragDropGridProps<T>) => {
|
}: DragDropGridProps<T>) => {
|
||||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -421,6 +448,21 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
overscan: OVERSCAN,
|
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
|
// Re-measure virtualizer when zoom or items per row changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rowVirtualizer.measure();
|
rowVirtualizer.measure();
|
||||||
@@ -719,7 +761,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{virtualRows.map((virtualRow) => {
|
||||||
const startIndex = virtualRow.index * itemsPerRow;
|
const startIndex = virtualRow.index * itemsPerRow;
|
||||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||||
const rowItems = visibleItems.slice(startIndex, endIndex);
|
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 { useFileState, useFileActions } from "@app/contexts/FileContext";
|
||||||
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
||||||
import { usePageEditor } from "@app/contexts/PageEditorContext";
|
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
|
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||||
import '@app/components/pageEditor/PageEditor.module.css';
|
import '@app/components/pageEditor/PageEditor.module.css';
|
||||||
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
|
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 { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePageSelectionManager";
|
||||||
import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands";
|
import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands";
|
||||||
import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport";
|
import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport";
|
||||||
|
import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration";
|
||||||
|
|
||||||
export interface PageEditorProps {
|
export interface PageEditorProps {
|
||||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||||
@@ -40,7 +41,27 @@ const PageEditor = ({
|
|||||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||||
|
|
||||||
// Get PageEditor coordination functions
|
// 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
|
// Zoom state management
|
||||||
const [zoomLevel, setZoomLevel] = useState(1.0);
|
const [zoomLevel, setZoomLevel] = useState(1.0);
|
||||||
@@ -149,6 +170,21 @@ const PageEditor = ({
|
|||||||
updateCurrentPages,
|
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
|
// UI state management
|
||||||
const {
|
const {
|
||||||
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
||||||
@@ -231,6 +267,92 @@ const PageEditor = ({
|
|||||||
setSplitPositions,
|
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)
|
// Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument)
|
||||||
const selectedPageCount = selectedPageIds.length;
|
const selectedPageCount = selectedPageIds.length;
|
||||||
const activeFileIds = selectedFileIds;
|
const activeFileIds = selectedFileIds;
|
||||||
@@ -345,12 +467,17 @@ const PageEditor = ({
|
|||||||
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
pos="relative"
|
|
||||||
data-scrolling-container="true"
|
data-scrolling-container="true"
|
||||||
onMouseEnter={() => setIsContainerHovered(true)}
|
onMouseEnter={() => setIsContainerHovered(true)}
|
||||||
onMouseLeave={() => setIsContainerHovered(false)}
|
onMouseLeave={() => setIsContainerHovered(false)}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LoadingOverlay visible={globalProcessing && !initialDocument} />
|
<LoadingOverlay visible={globalProcessing && !initialDocument} />
|
||||||
|
|
||||||
@@ -372,7 +499,7 @@ const PageEditor = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{displayDocument && (
|
{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 */}
|
{/* Split Lines Overlay */}
|
||||||
<div
|
<div
|
||||||
@@ -450,6 +577,7 @@ const PageEditor = ({
|
|||||||
onReorderPages={handleReorderPages}
|
onReorderPages={handleReorderPages}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
selectedFileIds={selectedFileIds}
|
selectedFileIds={selectedFileIds}
|
||||||
|
onVisibleItemsChange={handleVisibleItemsChange}
|
||||||
getThumbnailData={(pageId) => {
|
getThumbnailData={(pageId) => {
|
||||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||||
if (!page?.thumbnail) return null;
|
if (!page?.thumbnail) return null;
|
||||||
@@ -468,7 +596,6 @@ const PageEditor = ({
|
|||||||
page={page}
|
page={page}
|
||||||
index={index}
|
index={index}
|
||||||
totalPages={displayDocument.pages.length}
|
totalPages={displayDocument.pages.length}
|
||||||
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
|
|
||||||
fileColorIndex={fileColorIndex}
|
fileColorIndex={fileColorIndex}
|
||||||
selectedPageIds={selectedPageIds}
|
selectedPageIds={selectedPageIds}
|
||||||
selectionMode={selectionMode}
|
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 ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
|
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
|
||||||
import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration';
|
|
||||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||||
import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors';
|
import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors';
|
||||||
import styles from '@app/components/pageEditor/PageEditor.module.css';
|
import styles from '@app/components/pageEditor/PageEditor.module.css';
|
||||||
@@ -22,7 +21,6 @@ interface PageThumbnailProps {
|
|||||||
page: PDFPage;
|
page: PDFPage;
|
||||||
index: number;
|
index: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
originalFile?: File;
|
|
||||||
fileColorIndex: number;
|
fileColorIndex: number;
|
||||||
selectedPageIds: string[];
|
selectedPageIds: string[];
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
@@ -55,7 +53,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
page,
|
page,
|
||||||
index: _index,
|
index: _index,
|
||||||
totalPages,
|
totalPages,
|
||||||
originalFile,
|
|
||||||
fileColorIndex,
|
fileColorIndex,
|
||||||
selectedPageIds,
|
selectedPageIds,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
@@ -90,7 +87,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
|
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration();
|
|
||||||
const { openFilesModal } = useFilesModalContext();
|
const { openFilesModal } = useFilesModalContext();
|
||||||
|
|
||||||
// Check if this page is currently being dragged
|
// Check if this page is currently being dragged
|
||||||
@@ -115,43 +111,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
}
|
}
|
||||||
}, [page.thumbnail, thumbnailUrl]);
|
}, [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
|
// Merge refs - combine our ref tracking with dnd-kit's ref
|
||||||
const mergedRef = useCallback((element: HTMLDivElement | null) => {
|
const mergedRef = useCallback((element: HTMLDivElement | null) => {
|
||||||
// Track in our refs map
|
// Track in our refs map
|
||||||
|
|||||||
@@ -748,6 +748,7 @@ export class InsertFilesCommand extends DOMCommand {
|
|||||||
console.log('Pages:', pages.length);
|
console.log('Pages:', pages.length);
|
||||||
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
||||||
|
|
||||||
|
try {
|
||||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||||
// Extract page numbers for all pages from this file
|
// Extract page numbers for all pages from this file
|
||||||
const pageNumbers = pages.map(page => {
|
const pageNumbers = pages.map(page => {
|
||||||
@@ -786,6 +787,11 @@ export class InsertFilesCommand extends DOMCommand {
|
|||||||
} else {
|
} else {
|
||||||
console.error('No valid ArrayBuffer found for file ID:', fileId);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to generate thumbnails for inserted pages:', error);
|
console.error('Failed to generate thumbnails for inserted pages:', error);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ export const GRID_CONSTANTS = {
|
|||||||
ITEM_WIDTH: '20rem', // page width
|
ITEM_WIDTH: '20rem', // page width
|
||||||
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
|
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
|
||||||
ITEM_GAP: '1.5rem', // gap between items
|
ITEM_GAP: '1.5rem', // gap between items
|
||||||
OVERSCAN_SMALL: 4, // Overscan for normal documents
|
OVERSCAN_SMALL: 8, // Overscan for normal documents
|
||||||
OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages)
|
OVERSCAN_LARGE: 12, // Overscan for large documents (12 rows = ~96 pages pre-rendered)
|
||||||
} as const;
|
} as const;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useEffect, useState } from 'react';
|
||||||
import { useFileState } from '@app/contexts/FileContext';
|
import { useFileState } from '@app/contexts/FileContext';
|
||||||
import { usePageEditor } from '@app/contexts/PageEditorContext';
|
import { usePageEditor } from '@app/contexts/PageEditorContext';
|
||||||
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||||
import { FileId } from '@app/types/file';
|
import { FileId } from '@app/types/file';
|
||||||
|
import { FileAnalyzer } from '@app/services/fileAnalyzer';
|
||||||
|
|
||||||
export interface PageDocumentHook {
|
export interface PageDocumentHook {
|
||||||
document: PDFDocument | null;
|
document: PDFDocument | null;
|
||||||
@@ -16,7 +17,7 @@ export interface PageDocumentHook {
|
|||||||
*/
|
*/
|
||||||
export function usePageDocument(): PageDocumentHook {
|
export function usePageDocument(): PageDocumentHook {
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { fileOrder, currentPages } = usePageEditor();
|
const { fileOrder, currentPages, persistedDocument, persistedDocumentSignature } = usePageEditor();
|
||||||
|
|
||||||
// Use PageEditorContext's fileOrder instead of FileContext's global order
|
// Use PageEditorContext's fileOrder instead of FileContext's global order
|
||||||
// This ensures the page editor respects its own workspace ordering
|
// This ensures the page editor respects its own workspace ordering
|
||||||
@@ -58,6 +59,63 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
|
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
|
||||||
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
|
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)
|
// Compute merged document with stable signature (prevents infinite loops)
|
||||||
const currentPagesSignature = useMemo(() => {
|
const currentPagesSignature = useMemo(() => {
|
||||||
return currentPages ? currentPages.map(page => page.id).join(',') : '';
|
return currentPages ? currentPages.map(page => page.id).join(',') : '';
|
||||||
@@ -66,6 +124,19 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||||
if (activeFileIds.length === 0) return null;
|
if (activeFileIds.length === 0) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
persistedDocument &&
|
||||||
|
persistedDocumentSignature &&
|
||||||
|
persistedDocumentSignature === currentPagesSignature &&
|
||||||
|
currentPagesSignature.length > 0
|
||||||
|
) {
|
||||||
|
return persistedDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!primaryStirlingFileStub?.processedFile && placeholderDocument) {
|
||||||
|
return placeholderDocument;
|
||||||
|
}
|
||||||
|
|
||||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
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 we have file IDs but no file record, something is wrong - return null to show loading
|
||||||
@@ -245,7 +316,24 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return mergedDoc;
|
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
|
// Large document detection for smart loading
|
||||||
const isVeryLargeDocument = useMemo(() => {
|
const isVeryLargeDocument = useMemo(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo, useRef, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo, useRef, useEffect } from 'react';
|
||||||
import { FileId } from '@app/types/file';
|
import { FileId } from '@app/types/file';
|
||||||
import { useFileActions, useFileState } from '@app/contexts/FileContext';
|
import { useFileActions, useFileState } from '@app/contexts/FileContext';
|
||||||
import { PDFPage } from '@app/types/pageEditor';
|
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||||
import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors';
|
import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors';
|
||||||
|
|
||||||
// PageEditorFile is now defined locally in consuming components
|
// PageEditorFile is now defined locally in consuming components
|
||||||
@@ -129,6 +129,10 @@ interface PageEditorContextValue {
|
|||||||
|
|
||||||
// Update file order based on page positions (when pages are manually reordered)
|
// Update file order based on page positions (when pages are manually reordered)
|
||||||
updateFileOrderFromPages: (pages: PDFPage[]) => void;
|
updateFileOrderFromPages: (pages: PDFPage[]) => void;
|
||||||
|
persistedDocument: PDFDocument | null;
|
||||||
|
persistedDocumentSignature: string | null;
|
||||||
|
savePersistedDocument: (document: PDFDocument, signature: string) => void;
|
||||||
|
clearPersistedDocument: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageEditorContext = createContext<PageEditorContextValue | undefined>(undefined);
|
const PageEditorContext = createContext<PageEditorContextValue | undefined>(undefined);
|
||||||
@@ -141,6 +145,19 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
|
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
|
||||||
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
|
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
|
||||||
|
|
||||||
|
const [persistedDocument, setPersistedDocument] = useState<PDFDocument | null>(null);
|
||||||
|
const [persistedDocumentSignature, setPersistedDocumentSignature] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const savePersistedDocument = useCallback((document: PDFDocument, signature: string) => {
|
||||||
|
setPersistedDocument(document);
|
||||||
|
setPersistedDocumentSignature(signature);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearPersistedDocument = useCallback(() => {
|
||||||
|
setPersistedDocument(null);
|
||||||
|
setPersistedDocumentSignature(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Page editor's own file order (independent of FileContext)
|
// Page editor's own file order (independent of FileContext)
|
||||||
const [fileOrder, setFileOrder] = useState<FileId[]>([]);
|
const [fileOrder, setFileOrder] = useState<FileId[]>([]);
|
||||||
|
|
||||||
@@ -148,6 +165,20 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
const { actions: fileActions } = useFileActions();
|
const { actions: fileActions } = useFileActions();
|
||||||
const { state } = useFileState();
|
const { state } = useFileState();
|
||||||
|
|
||||||
|
const fileContextSignature = useMemo(() => {
|
||||||
|
return state.files.ids
|
||||||
|
.map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`)
|
||||||
|
.join(',');
|
||||||
|
}, [state.files.ids, state.files.byId]);
|
||||||
|
|
||||||
|
const prevFileContextSignature = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevFileContextSignature.current !== fileContextSignature) {
|
||||||
|
prevFileContextSignature.current = fileContextSignature;
|
||||||
|
clearPersistedDocument();
|
||||||
|
}
|
||||||
|
}, [fileContextSignature, clearPersistedDocument]);
|
||||||
|
|
||||||
// Keep a ref to always read latest state in stable callbacks
|
// Keep a ref to always read latest state in stable callbacks
|
||||||
const stateRef = useRef(state);
|
const stateRef = useRef(state);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -329,6 +360,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
deselectAll,
|
deselectAll,
|
||||||
reorderFiles,
|
reorderFiles,
|
||||||
updateFileOrderFromPages,
|
updateFileOrderFromPages,
|
||||||
|
persistedDocument,
|
||||||
|
persistedDocumentSignature,
|
||||||
|
savePersistedDocument,
|
||||||
|
clearPersistedDocument,
|
||||||
}), [
|
}), [
|
||||||
currentPages,
|
currentPages,
|
||||||
updateCurrentPages,
|
updateCurrentPages,
|
||||||
@@ -341,6 +376,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
deselectAll,
|
deselectAll,
|
||||||
reorderFiles,
|
reorderFiles,
|
||||||
updateFileOrderFromPages,
|
updateFileOrderFromPages,
|
||||||
|
persistedDocument,
|
||||||
|
persistedDocumentSignature,
|
||||||
|
savePersistedDocument,
|
||||||
|
clearPersistedDocument,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ let batchTimer: number | null = null;
|
|||||||
// Track active thumbnail requests to prevent duplicates across components
|
// Track active thumbnail requests to prevent duplicates across components
|
||||||
const activeRequests = new Map<string, Promise<string | null>>();
|
const activeRequests = new Map<string, Promise<string | null>>();
|
||||||
|
|
||||||
|
// Cache ArrayBuffers to avoid reading the same file multiple times
|
||||||
|
const fileArrayBufferCache = new Map<File, ArrayBuffer>();
|
||||||
|
|
||||||
// Batch processing configuration
|
// Batch processing configuration
|
||||||
const BATCH_SIZE = 20; // Process thumbnails in batches of 20 for better UI responsiveness
|
const BATCH_SIZE = 10; // Process thumbnails in batches of 10 for faster initial load
|
||||||
const BATCH_DELAY = 100; // Wait 100ms to collect requests before processing
|
const BATCH_DELAY = 50; // Wait 50ms to collect requests before processing
|
||||||
const PRIORITY_BATCH_DELAY = 50; // Faster processing for the first batch (visible pages)
|
const PRIORITY_BATCH_DELAY = 10; // Very fast processing for the first batch (visible pages)
|
||||||
|
|
||||||
// Process the queue in batches for better performance
|
// Process the queue in batches for better performance
|
||||||
async function processRequestQueue() {
|
async function processRequestQueue() {
|
||||||
@@ -68,8 +71,13 @@ async function processRequestQueue() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pageNumbers = requests.map(req => req.pageNumber);
|
const pageNumbers = requests.map(req => req.pageNumber);
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
|
// Get or create cached ArrayBuffer to avoid reading file multiple times
|
||||||
|
let arrayBuffer = fileArrayBufferCache.get(file);
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
arrayBuffer = await file.arrayBuffer();
|
||||||
|
fileArrayBufferCache.set(file, arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
// Use quickKey for PDF document caching (same metadata, consistent format)
|
// Use quickKey for PDF document caching (same metadata, consistent format)
|
||||||
const fileId = createQuickKey(file) as FileId;
|
const fileId = createQuickKey(file) as FileId;
|
||||||
@@ -106,6 +114,10 @@ async function processRequestQueue() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isProcessingQueue = false;
|
isProcessingQueue = false;
|
||||||
|
// Clean up ArrayBuffer cache when queue is empty
|
||||||
|
if (requestQueue.length === 0) {
|
||||||
|
fileArrayBufferCache.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +175,9 @@ export function useThumbnailGeneration() {
|
|||||||
activeRequests.clear();
|
activeRequests.clear();
|
||||||
isProcessingQueue = false;
|
isProcessingQueue = false;
|
||||||
|
|
||||||
|
// Clear ArrayBuffer cache
|
||||||
|
fileArrayBufferCache.clear();
|
||||||
|
|
||||||
thumbnailGenerationService.destroy();
|
thumbnailGenerationService.destroy();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -265,17 +265,13 @@ export class EnhancedPDFProcessingService {
|
|||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create placeholder pages for remaining pages
|
// Create placeholder pages for remaining pages without touching PDF.js
|
||||||
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
||||||
// Load page just to get rotation
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const rotation = page.rotate || 0;
|
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
rotation,
|
rotation: 0,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -337,17 +333,13 @@ export class EnhancedPDFProcessingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create placeholders for remaining pages
|
// Create placeholders for remaining pages without invoking PDF.js
|
||||||
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
||||||
// Load page just to get rotation
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const rotation = page.rotate || 0;
|
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation,
|
rotation: 0,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -377,15 +369,11 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholder pages without thumbnails
|
// Create placeholder pages without thumbnails
|
||||||
const pages: PDFPage[] = [];
|
const pages: PDFPage[] = [];
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
// Load page just to get rotation
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const rotation = page.rotate || 0;
|
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation,
|
rotation: 0,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class FileAnalyzer {
|
|||||||
/**
|
/**
|
||||||
* Quick PDF analysis without full processing
|
* Quick PDF analysis without full processing
|
||||||
*/
|
*/
|
||||||
private static async quickPDFAnalysis(file: File): Promise<{
|
static async quickPDFAnalysis(file: File): Promise<{
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
isCorrupted: boolean;
|
isCorrupted: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user