mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Bug/page editor additional fixes (#5660)
This commit is contained in:
@@ -33,10 +33,11 @@ interface DragDropItem {
|
|||||||
interface DragDropGridProps<T extends DragDropItem> {
|
interface DragDropGridProps<T extends DragDropItem> {
|
||||||
items: T[];
|
items: T[];
|
||||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => 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;
|
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, activeDragIds: string[], justMoved: boolean, dragHandleProps?: any, zoomLevel?: number) => React.ReactNode;
|
||||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
selectedFileIds?: string[];
|
selectedFileIds?: string[];
|
||||||
|
selectedPageIds?: string[];
|
||||||
onVisibleItemsChange?: (items: T[]) => void;
|
onVisibleItemsChange?: (items: T[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,17 +190,16 @@ interface DraggableItemProps<T extends DragDropItem> {
|
|||||||
itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||||
boxSelectedPageIds: string[];
|
boxSelectedPageIds: string[];
|
||||||
clearBoxSelection: () => void;
|
clearBoxSelection: () => void;
|
||||||
getBoxSelection: () => string[];
|
|
||||||
activeId: string | null;
|
|
||||||
activeDragIds: string[];
|
activeDragIds: string[];
|
||||||
justMoved: boolean;
|
justMoved: boolean;
|
||||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||||
onUpdateDropTarget: (itemId: string | null) => void;
|
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;
|
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, activeDragIds: string[], justMoved: boolean, dragHandleProps?: any, zoomLevel?: number) => React.ReactNode;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
|
selectedPageIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DraggableItemInner = <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, 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({
|
||||||
@@ -248,7 +248,7 @@ const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, box
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, isOver, { ref: setNodeRef, ...attributes, ...listeners }, zoomLevel)}
|
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, activeDragIds, justMoved, { ref: setNodeRef, ...attributes, ...listeners }, zoomLevel)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -266,11 +266,22 @@ const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => {
|
|||||||
return false; // Props changed, re-render needed
|
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
|
// Item reference is same, check other props
|
||||||
return (
|
return (
|
||||||
prevProps.item.id === nextProps.item.id &&
|
prevProps.item.id === nextProps.item.id &&
|
||||||
prevProps.index === nextProps.index &&
|
prevProps.index === nextProps.index &&
|
||||||
prevProps.activeId === nextProps.activeId &&
|
|
||||||
prevProps.justMoved === nextProps.justMoved &&
|
prevProps.justMoved === nextProps.justMoved &&
|
||||||
prevProps.zoomLevel === nextProps.zoomLevel &&
|
prevProps.zoomLevel === nextProps.zoomLevel &&
|
||||||
prevProps.activeDragIds.length === nextProps.activeDragIds.length &&
|
prevProps.activeDragIds.length === nextProps.activeDragIds.length &&
|
||||||
@@ -285,6 +296,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
getThumbnailData,
|
getThumbnailData,
|
||||||
zoomLevel = 1.0,
|
zoomLevel = 1.0,
|
||||||
selectedFileIds,
|
selectedFileIds,
|
||||||
|
selectedPageIds,
|
||||||
onVisibleItemsChange,
|
onVisibleItemsChange,
|
||||||
}: DragDropGridProps<T>) => {
|
}: DragDropGridProps<T>) => {
|
||||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
@@ -294,6 +306,10 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
return containerRef.current?.closest('[data-scrolling-container]') as HTMLElement | null;
|
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 { filteredItems: visibleItems, filteredToOriginalIndex } = useMemo(() => {
|
||||||
const filtered: T[] = [];
|
const filtered: T[] = [];
|
||||||
const indexMap: number[] = [];
|
const indexMap: number[] = [];
|
||||||
@@ -318,7 +334,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { filteredItems: filtered, filteredToOriginalIndex: indexMap };
|
return { filteredItems: filtered, filteredToOriginalIndex: indexMap };
|
||||||
}, [items, selectedFileIds]);
|
}, [items, selectedFileIds, itemsSignature, selectedFileIdsSignature]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visibleIdSet = new Set(visibleItems.map(item => item.id));
|
const visibleIdSet = new Set(visibleItems.map(item => item.id));
|
||||||
@@ -464,9 +480,11 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
}, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]);
|
}, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]);
|
||||||
|
|
||||||
// Re-measure virtualizer when zoom or items per row changes
|
// 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(() => {
|
useEffect(() => {
|
||||||
rowVirtualizer.measure();
|
rowVirtualizer.measure();
|
||||||
}, [zoomLevel, itemsPerRow, visibleItems.length]);
|
}, [zoomLevel, itemsPerRow, visibleItems.length, visibleItemsSignature, rowVirtualizer]);
|
||||||
|
|
||||||
// Cleanup highlight timeout on unmount
|
// Cleanup highlight timeout on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -565,11 +583,6 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
setBoxSelectedPageIds([]);
|
setBoxSelectedPageIds([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Function to get current box selection (exposed to child components)
|
|
||||||
const getBoxSelection = useCallback(() => {
|
|
||||||
return boxSelectedPageIds;
|
|
||||||
}, [boxSelectedPageIds]);
|
|
||||||
|
|
||||||
// Handle drag start
|
// Handle drag start
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
const activeId = event.active.id as string;
|
const activeId = event.active.id as string;
|
||||||
@@ -791,14 +804,13 @@ const DragDropGrid = <T extends DragDropItem>({
|
|||||||
itemRefs={itemRefs}
|
itemRefs={itemRefs}
|
||||||
boxSelectedPageIds={boxSelectedPageIds}
|
boxSelectedPageIds={boxSelectedPageIds}
|
||||||
clearBoxSelection={clearBoxSelection}
|
clearBoxSelection={clearBoxSelection}
|
||||||
getBoxSelection={getBoxSelection}
|
|
||||||
activeId={activeId}
|
|
||||||
activeDragIds={activeDragIds}
|
activeDragIds={activeDragIds}
|
||||||
justMoved={justMovedIds.includes(item.id)}
|
justMoved={justMovedIds.includes(item.id)}
|
||||||
getThumbnailData={getThumbnailData}
|
getThumbnailData={getThumbnailData}
|
||||||
onUpdateDropTarget={setHoveredItemId}
|
onUpdateDropTarget={setHoveredItemId}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
|
selectedPageIds={selectedPageIds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
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, useNavigationState } from "@app/contexts/NavigationContext";
|
||||||
import { usePageEditor } from "@app/contexts/PageEditorContext";
|
import { usePageEditor } from "@app/contexts/PageEditorContext";
|
||||||
import { PageEditorFunctions, PDFPage } 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
|
||||||
@@ -24,6 +24,7 @@ import { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePag
|
|||||||
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";
|
import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration";
|
||||||
|
import { convertSplitPageIdsToIndexes } from '@app/components/pageEditor/utils/splitPositions';
|
||||||
|
|
||||||
export interface PageEditorProps {
|
export interface PageEditorProps {
|
||||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||||
@@ -39,6 +40,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Navigation guard for unsaved changes
|
// Navigation guard for unsaved changes
|
||||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||||
|
const navigationState = useNavigationState();
|
||||||
|
|
||||||
// Get PageEditor coordination functions
|
// Get PageEditor coordination functions
|
||||||
const {
|
const {
|
||||||
@@ -48,6 +50,7 @@ const PageEditor = ({
|
|||||||
clearReorderedPages,
|
clearReorderedPages,
|
||||||
updateCurrentPages,
|
updateCurrentPages,
|
||||||
savePersistedDocument,
|
savePersistedDocument,
|
||||||
|
clearPersistedDocument,
|
||||||
} = usePageEditor();
|
} = usePageEditor();
|
||||||
|
|
||||||
const [visiblePageIds, setVisiblePageIds] = useState<string[]>([]);
|
const [visiblePageIds, setVisiblePageIds] = useState<string[]>([]);
|
||||||
@@ -175,15 +178,136 @@ const PageEditor = ({
|
|||||||
displayDocumentRef.current = displayDocument;
|
displayDocumentRef.current = displayDocument;
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
|
const queueThumbnailRequestsForPages = useCallback((pageIds: string[]) => {
|
||||||
|
const doc = displayDocumentRef.current;
|
||||||
|
if (!doc || pageIds.length === 0) return;
|
||||||
|
|
||||||
|
const loadedCount = doc.pages.filter(p => p.thumbnail).length;
|
||||||
|
const pending = thumbnailRequestsRef.current.size;
|
||||||
|
const MAX_CONCURRENT_THUMBNAILS = loadedCount < 8 ? 1
|
||||||
|
: doc.totalPages < 20 ? 3
|
||||||
|
: doc.totalPages < 50 ? 5
|
||||||
|
: 8;
|
||||||
|
const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending);
|
||||||
|
if (available === 0) return;
|
||||||
|
|
||||||
|
const toLoad: string[] = [];
|
||||||
|
for (const pageId of pageIds) {
|
||||||
|
if (toLoad.length >= available) break;
|
||||||
|
if (thumbnailRequestsRef.current.has(pageId)) continue;
|
||||||
|
const page = doc.pages.find(p => p.id === pageId);
|
||||||
|
if (!page || page.thumbnail) continue;
|
||||||
|
toLoad.push(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toLoad.length === 0) return;
|
||||||
|
|
||||||
|
toLoad.forEach(pageId => {
|
||||||
|
const page = doc.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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
getThumbnailFromCache,
|
||||||
|
requestThumbnail,
|
||||||
|
selectors,
|
||||||
|
setEditedDocument
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!displayDocument) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueThumbnailRequestsForPages(visiblePageIds);
|
||||||
|
}, [displayDocument, visiblePageIds, queueThumbnailRequestsForPages]);
|
||||||
|
|
||||||
|
const lastInitialDocumentSignatureRef = useRef<string | null>(null);
|
||||||
|
const displayDocumentId = displayDocument?.id ?? null;
|
||||||
|
const displayDocumentLength = displayDocument?.pages.length ?? 0;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!displayDocument || displayDocument.pages.length === 0) {
|
||||||
|
lastInitialDocumentSignatureRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = `${displayDocumentId}:${displayDocumentLength}`;
|
||||||
|
if (lastInitialDocumentSignatureRef.current === signature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_VISIBLE_PAGE_COUNT = 8;
|
||||||
|
const initialIds = displayDocument.pages
|
||||||
|
.slice(0, INITIAL_VISIBLE_PAGE_COUNT)
|
||||||
|
.map(page => page.id);
|
||||||
|
|
||||||
|
queueThumbnailRequestsForPages(initialIds);
|
||||||
|
lastInitialDocumentSignatureRef.current = signature;
|
||||||
|
}, [displayDocumentId, displayDocumentLength, queueThumbnailRequestsForPages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisiblePageIds([]);
|
||||||
|
}, [displayDocumentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (navigationState.workbench !== 'pageEditor') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const doc = displayDocumentRef.current;
|
const doc = displayDocumentRef.current;
|
||||||
if (doc && doc.pages.length > 0) {
|
if (doc && doc.pages.length > 0) {
|
||||||
const signature = doc.pages.map(page => page.id).join(',');
|
const signature = doc.pages.map(page => page.id).join(',');
|
||||||
savePersistedDocument(doc, signature);
|
savePersistedDocument(doc, signature);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [savePersistedDocument]);
|
}, [savePersistedDocument, navigationState.workbench]);
|
||||||
|
|
||||||
// UI state management
|
// UI state management
|
||||||
const {
|
const {
|
||||||
@@ -265,94 +389,10 @@ const PageEditor = ({
|
|||||||
exportLoading,
|
exportLoading,
|
||||||
setExportLoading,
|
setExportLoading,
|
||||||
setSplitPositions,
|
setSplitPositions,
|
||||||
|
clearPersistedDocument,
|
||||||
|
updateCurrentPages,
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
@@ -466,6 +506,78 @@ const PageEditor = ({
|
|||||||
// Track color assignments by insertion order (files keep their color)
|
// Track color assignments by insertion order (files keep their color)
|
||||||
const fileColorIndexMap = useFileColorMap(orderedFileIds);
|
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,
|
||||||
|
activeDragIds: string[],
|
||||||
|
justMoved: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -500,7 +612,6 @@ const PageEditor = ({
|
|||||||
|
|
||||||
{displayDocument && (
|
{displayDocument && (
|
||||||
<Box ref={gridContainerRef} p={0} pt="2rem" pb="4rem" style={{ position: 'relative' }}>
|
<Box ref={gridContainerRef} p={0} pt="2rem" pb="4rem" style={{ position: 'relative' }}>
|
||||||
|
|
||||||
{/* Split Lines Overlay */}
|
{/* Split Lines Overlay */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -521,8 +632,9 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const containerRect = containerEl.getBoundingClientRect();
|
const containerRect = containerEl.getBoundingClientRect();
|
||||||
|
const splitIndexes = convertSplitPageIdsToIndexes(displayDocument, splitPositions);
|
||||||
|
|
||||||
return Array.from(splitPositions).map((position) => {
|
return Array.from(splitIndexes).map((position) => {
|
||||||
const currentPage = displayedPages[position];
|
const currentPage = displayedPages[position];
|
||||||
if (!currentPage) {
|
if (!currentPage) {
|
||||||
return null;
|
return null;
|
||||||
@@ -577,6 +689,7 @@ const PageEditor = ({
|
|||||||
onReorderPages={handleReorderPages}
|
onReorderPages={handleReorderPages}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
selectedFileIds={selectedFileIds}
|
selectedFileIds={selectedFileIds}
|
||||||
|
selectedPageIds={selectedPageIds}
|
||||||
onVisibleItemsChange={handleVisibleItemsChange}
|
onVisibleItemsChange={handleVisibleItemsChange}
|
||||||
getThumbnailData={(pageId) => {
|
getThumbnailData={(pageId) => {
|
||||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||||
@@ -586,50 +699,11 @@ const PageEditor = ({
|
|||||||
rotation: page.rotation || 0
|
rotation: page.rotation || 0
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, _getBoxSelection, _activeId, activeDragIds, justMoved, _isOver, dragHandleProps, zoomLevel) => {
|
renderItem={renderItemCallback}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<NavigationWarningModal
|
<NavigationWarningModal
|
||||||
onApplyAndContinue={async () => {
|
onApplyAndContinue={async () => {
|
||||||
await applyChanges();
|
await applyChanges();
|
||||||
@@ -638,7 +712,6 @@ const PageEditor = ({
|
|||||||
await onExportAll();
|
await onExportAll();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ interface PageEditorControlsProps {
|
|||||||
displayDocument?: { pages: { id: string; pageNumber: number }[] };
|
displayDocument?: { pages: { id: string; pageNumber: number }[] };
|
||||||
|
|
||||||
// Split state (for tooltip logic)
|
// Split state (for tooltip logic)
|
||||||
splitPositions?: Set<number>;
|
splitPositions?: Set<string>;
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,35 +54,29 @@ const PageEditorControls = ({
|
|||||||
selectedPageIds,
|
selectedPageIds,
|
||||||
displayDocument,
|
displayDocument,
|
||||||
splitPositions,
|
splitPositions,
|
||||||
totalPages
|
|
||||||
}: PageEditorControlsProps) => {
|
}: PageEditorControlsProps) => {
|
||||||
// Calculate split tooltip text using smart toggle logic
|
// Calculate split tooltip text using smart toggle logic
|
||||||
const getSplitTooltip = () => {
|
const getSplitTooltip = () => {
|
||||||
if (!splitPositions || !totalPages || selectedPageIds.length === 0) {
|
if (!splitPositions || !displayDocument || selectedPageIds.length === 0) {
|
||||||
return "Split Selected";
|
return "Split Selected";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert selected pages to split positions (same logic as handleSplit)
|
const totalPages = displayDocument.pages.length;
|
||||||
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
|
const selectedValidPageIds = displayDocument.pages
|
||||||
const page = displayDocument.pages.find(p => p.id === id);
|
.filter((page, index) => selectedPageIds.includes(page.id) && index < totalPages - 1)
|
||||||
return page?.pageNumber || 0;
|
.map(page => page.id);
|
||||||
}).filter(num => num > 0) : [];
|
|
||||||
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
|
|
||||||
|
|
||||||
if (selectedSplitPositions.length === 0) {
|
if (selectedValidPageIds.length === 0) {
|
||||||
return "Split Selected";
|
return "Split Selected";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart toggle logic: follow the majority, default to adding splits if equal
|
const existingSplitsCount = selectedValidPageIds.filter(id => splitPositions.has(id)).length;
|
||||||
const existingSplitsCount = selectedSplitPositions.filter(pos => splitPositions.has(pos)).length;
|
const noSplitsCount = selectedValidPageIds.length - existingSplitsCount;
|
||||||
const noSplitsCount = selectedSplitPositions.length - existingSplitsCount;
|
|
||||||
|
|
||||||
// Remove splits only if majority already have splits
|
|
||||||
// If equal (50/50), default to adding splits
|
|
||||||
const willRemoveSplits = existingSplitsCount > noSplitsCount;
|
const willRemoveSplits = existingSplitsCount > noSplitsCount;
|
||||||
|
|
||||||
if (willRemoveSplits) {
|
if (willRemoveSplits) {
|
||||||
return existingSplitsCount === selectedSplitPositions.length
|
return existingSplitsCount === selectedValidPageIds.length
|
||||||
? "Remove All Selected Splits"
|
? "Remove All Selected Splits"
|
||||||
: "Remove Selected Splits";
|
: "Remove Selected Splits";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ interface PageThumbnailProps {
|
|||||||
onDeletePage: (pageNumber: number) => void;
|
onDeletePage: (pageNumber: number) => void;
|
||||||
createRotateCommand: (pageIds: string[], rotation: number) => { execute: () => void };
|
createRotateCommand: (pageIds: string[], rotation: number) => { execute: () => void };
|
||||||
createDeleteCommand: (pageIds: string[]) => { execute: () => void };
|
createDeleteCommand: (pageIds: string[]) => { execute: () => void };
|
||||||
createSplitCommand: (position: number) => { execute: () => void };
|
createSplitCommand: (pageId: string, pageNumber: number) => { execute: () => void };
|
||||||
pdfDocument: PDFDocument;
|
pdfDocument: PDFDocument;
|
||||||
setPdfDocument: (doc: PDFDocument) => void;
|
setPdfDocument: (doc: PDFDocument) => void;
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<string>;
|
||||||
onInsertFiles?: (files: File[] | StirlingFileStub[], insertAfterPage: number, isFromStorage?: boolean) => void;
|
onInsertFiles?: (files: File[] | StirlingFileStub[], insertAfterPage: number, isFromStorage?: boolean) => void;
|
||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
justMoved = false,
|
justMoved = false,
|
||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
const pageIndex = page.pageNumber - 1;
|
const pageIndex = page.pageNumber - 1;
|
||||||
|
const isSelected = Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false;
|
||||||
|
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||||
@@ -155,10 +156,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Create a command to toggle split at this position
|
// Create a command to toggle split at this position
|
||||||
const command = createSplitCommand(pageIndex);
|
const command = createSplitCommand(page.id, page.pageNumber);
|
||||||
onExecuteCommand(command);
|
onExecuteCommand(command);
|
||||||
|
|
||||||
const hasSplit = splitPositions.has(pageIndex);
|
const hasSplit = splitPositions.has(page.id);
|
||||||
const action = hasSplit ? 'removed' : 'added';
|
const action = hasSplit ? 'removed' : 'added';
|
||||||
onSetStatus(`Split marker ${action} after position ${pageIndex + 1}`);
|
onSetStatus(`Split marker ${action} after position ${pageIndex + 1}`);
|
||||||
}, [pageIndex, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
|
}, [pageIndex, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
|
||||||
@@ -365,7 +366,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false}
|
checked={isSelected}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
// Selection is handled by container mouseDown
|
// Selection is handled by container mouseDown
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class RotatePageCommand extends DOMCommand {
|
|||||||
|
|
||||||
export class DeletePagesCommand extends DOMCommand {
|
export class DeletePagesCommand extends DOMCommand {
|
||||||
private originalDocument: PDFDocument | null = null;
|
private originalDocument: PDFDocument | null = null;
|
||||||
private originalSplitPositions: Set<number> = new Set();
|
private originalSplitPositions: Set<string> = new Set();
|
||||||
private originalSelectedPages: number[] = [];
|
private originalSelectedPages: number[] = [];
|
||||||
private hasExecuted: boolean = false;
|
private hasExecuted: boolean = false;
|
||||||
private pageIdsToDelete: string[] = [];
|
private pageIdsToDelete: string[] = [];
|
||||||
@@ -68,8 +68,8 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
private getCurrentDocument: () => PDFDocument | null,
|
private getCurrentDocument: () => PDFDocument | null,
|
||||||
private setDocument: (doc: PDFDocument) => void,
|
private setDocument: (doc: PDFDocument) => void,
|
||||||
private setSelectedPageIds: (pageIds: string[]) => void,
|
private setSelectedPageIds: (pageIds: string[]) => void,
|
||||||
private getSplitPositions: () => Set<number>,
|
private getSplitPositions: () => Set<string>,
|
||||||
private setSplitPositions: (positions: Set<number>) => void,
|
private setSplitPositions: (positions: Set<string>) => void,
|
||||||
private getSelectedPages: () => number[],
|
private getSelectedPages: () => number[],
|
||||||
onAllPagesDeleted?: () => void
|
onAllPagesDeleted?: () => void
|
||||||
) {
|
) {
|
||||||
@@ -133,10 +133,15 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
|
|
||||||
// Adjust split positions
|
// Adjust split positions
|
||||||
const currentSplitPositions = this.getSplitPositions();
|
const currentSplitPositions = this.getSplitPositions();
|
||||||
const newPositions = new Set<number>();
|
const remainingIndexMap = new Map<string, number>();
|
||||||
currentSplitPositions.forEach(pos => {
|
remainingPages.forEach((page, index) => {
|
||||||
if (pos < remainingPages.length - 1) {
|
remainingIndexMap.set(page.id, index);
|
||||||
newPositions.add(pos);
|
});
|
||||||
|
const newPositions = new Set<string>();
|
||||||
|
currentSplitPositions.forEach((pageId) => {
|
||||||
|
const splitIndex = remainingIndexMap.get(pageId);
|
||||||
|
if (splitIndex !== undefined && splitIndex < remainingPages.length - 1) {
|
||||||
|
newPositions.add(pageId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,12 +266,13 @@ export class ReorderPagesCommand extends DOMCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SplitCommand extends DOMCommand {
|
export class SplitCommand extends DOMCommand {
|
||||||
private originalSplitPositions: Set<number> = new Set();
|
private originalSplitPositions: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private position: number,
|
private pageId: string,
|
||||||
private getSplitPositions: () => Set<number>,
|
private pageNumber: number,
|
||||||
private setSplitPositions: (positions: Set<number>) => void
|
private getSplitPositions: () => Set<string>,
|
||||||
|
private setSplitPositions: (positions: Set<string>) => void
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -279,10 +285,10 @@ export class SplitCommand extends DOMCommand {
|
|||||||
const currentPositions = this.getSplitPositions();
|
const currentPositions = this.getSplitPositions();
|
||||||
const newPositions = new Set(currentPositions);
|
const newPositions = new Set(currentPositions);
|
||||||
|
|
||||||
if (newPositions.has(this.position)) {
|
if (newPositions.has(this.pageId)) {
|
||||||
newPositions.delete(this.position);
|
newPositions.delete(this.pageId);
|
||||||
} else {
|
} else {
|
||||||
newPositions.add(this.position);
|
newPositions.add(this.pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSplitPositions(newPositions);
|
this.setSplitPositions(newPositions);
|
||||||
@@ -295,8 +301,8 @@ export class SplitCommand extends DOMCommand {
|
|||||||
|
|
||||||
get description(): string {
|
get description(): string {
|
||||||
const currentPositions = this.getSplitPositions();
|
const currentPositions = this.getSplitPositions();
|
||||||
const willAdd = !currentPositions.has(this.position);
|
const willAdd = !currentPositions.has(this.pageId);
|
||||||
return `${willAdd ? 'Add' : 'Remove'} split at position ${this.position + 1}`;
|
return `${willAdd ? 'Add' : 'Remove'} split at position ${this.pageNumber}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ export const useEditedDocumentState = ({
|
|||||||
}
|
}
|
||||||
}, [mergedPdfDocument]);
|
}, [mergedPdfDocument]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mergedPdfDocument) {
|
||||||
|
setEditedDocument(null);
|
||||||
|
}
|
||||||
|
}, [mergedPdfDocument, setEditedDocument]);
|
||||||
|
|
||||||
// Keep editedDocument in sync with out-of-band insert/remove events (e.g. uploads finishing)
|
// Keep editedDocument in sync with out-of-band insert/remove events (e.g. uploads finishing)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentEditedDocument = editedDocumentRef.current;
|
const currentEditedDocument = editedDocumentRef.current;
|
||||||
@@ -101,6 +107,8 @@ export const useEditedDocumentState = ({
|
|||||||
const sourcePages = mergedPdfDocument.pages;
|
const sourcePages = mergedPdfDocument.pages;
|
||||||
const sourceIds = new Set(sourcePages.map((p) => p.id));
|
const sourceIds = new Set(sourcePages.map((p) => p.id));
|
||||||
const prevIds = new Set(prev.pages.map((p) => p.id));
|
const prevIds = new Set(prev.pages.map((p) => p.id));
|
||||||
|
const hasOverlap = sourcePages.some((page) => prevIds.has(page.id));
|
||||||
|
const shouldResetToMerged = !hasOverlap;
|
||||||
|
|
||||||
const newPages: PDFPage[] = [];
|
const newPages: PDFPage[] = [];
|
||||||
for (const page of sourcePages) {
|
for (const page of sourcePages) {
|
||||||
@@ -121,7 +129,9 @@ export const useEditedDocumentState = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAdditions || hasRemovals) {
|
if (shouldResetToMerged) {
|
||||||
|
pages = sourcePages.map((page) => ({ ...page }));
|
||||||
|
} else if (hasAdditions || hasRemovals) {
|
||||||
pages = [...prev.pages];
|
pages = [...prev.pages];
|
||||||
|
|
||||||
const placeholderPositions = new Map<FileId, number>();
|
const placeholderPositions = new Map<FileId, number>();
|
||||||
@@ -194,6 +204,9 @@ export const useEditedDocumentState = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldResetToMerged || hasAdditions || hasRemovals) {
|
||||||
pages = pages.map((page, index) => ({
|
pages = pages.map((page, index) => ({
|
||||||
...page,
|
...page,
|
||||||
pageNumber: index + 1,
|
pageNumber: index + 1,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BulkRotateCommand,
|
BulkRotateCommand,
|
||||||
@@ -22,8 +22,8 @@ interface UsePageEditorCommandsParams {
|
|||||||
displayDocument: PDFDocument | null;
|
displayDocument: PDFDocument | null;
|
||||||
getEditedDocument: () => PDFDocument | null;
|
getEditedDocument: () => PDFDocument | null;
|
||||||
setEditedDocument: React.Dispatch<React.SetStateAction<PDFDocument | null>>;
|
setEditedDocument: React.Dispatch<React.SetStateAction<PDFDocument | null>>;
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<string>;
|
||||||
setSplitPositions: React.Dispatch<React.SetStateAction<Set<number>>>;
|
setSplitPositions: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||||
selectedPageIds: string[];
|
selectedPageIds: string[];
|
||||||
setSelectedPageIds: (ids: string[]) => void;
|
setSelectedPageIds: (ids: string[]) => void;
|
||||||
getPageNumbersFromIds: (pageIds: string[]) => number[];
|
getPageNumbersFromIds: (pageIds: string[]) => number[];
|
||||||
@@ -51,6 +51,12 @@ export const usePageEditorCommands = ({
|
|||||||
setSelectionMode,
|
setSelectionMode,
|
||||||
clearUndoHistory,
|
clearUndoHistory,
|
||||||
}: UsePageEditorCommandsParams) => {
|
}: UsePageEditorCommandsParams) => {
|
||||||
|
const splitPositionsRef = useRef(splitPositions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
splitPositionsRef.current = splitPositions;
|
||||||
|
}, [splitPositions]);
|
||||||
|
|
||||||
const closePdf = useCallback(() => {
|
const closePdf = useCallback(() => {
|
||||||
actions.clearAllFiles();
|
actions.clearAllFiles();
|
||||||
clearUndoHistory();
|
clearUndoHistory();
|
||||||
@@ -118,17 +124,18 @@ export const usePageEditorCommands = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const createSplitCommand = useCallback(
|
const createSplitCommand = useCallback(
|
||||||
(position: number) => ({
|
(pageId: string, pageNumber: number) => ({
|
||||||
execute: () => {
|
execute: () => {
|
||||||
const splitCommand = new SplitCommand(
|
const splitCommand = new SplitCommand(
|
||||||
position,
|
pageId,
|
||||||
() => splitPositions,
|
pageNumber,
|
||||||
|
() => splitPositionsRef.current,
|
||||||
setSplitPositions
|
setSplitPositions
|
||||||
);
|
);
|
||||||
executeCommandWithTracking(splitCommand);
|
executeCommandWithTracking(splitCommand);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[splitPositions, executeCommandWithTracking, setSplitPositions]
|
[executeCommandWithTracking, setSplitPositions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const executeCommand = useCallback((command: any) => {
|
const executeCommand = useCallback((command: any) => {
|
||||||
@@ -208,39 +215,34 @@ export const usePageEditorCommands = ({
|
|||||||
const handleSplit = useCallback(() => {
|
const handleSplit = useCallback(() => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
|
|
||||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
const selectedSplitPageIds = displayDocument.pages
|
||||||
const selectedPositions: number[] = [];
|
.filter((page, index) =>
|
||||||
selectedPageNumbers.forEach((pageNum) => {
|
selectedPageIds.includes(page.id) && index < displayDocument.pages.length - 1
|
||||||
const pageIndex = displayDocument.pages.findIndex(
|
)
|
||||||
(p) => p.pageNumber === pageNum
|
.map((page) => page.id);
|
||||||
);
|
|
||||||
if (pageIndex !== -1 && pageIndex < displayDocument.pages.length - 1) {
|
|
||||||
selectedPositions.push(pageIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedPositions.length === 0) return;
|
if (selectedSplitPageIds.length === 0) return;
|
||||||
|
|
||||||
const existingSplitsCount = selectedPositions.filter((pos) =>
|
const existingSplitsCount = selectedSplitPageIds.filter((id) =>
|
||||||
splitPositions.has(pos)
|
splitPositions.has(id)
|
||||||
).length;
|
).length;
|
||||||
const noSplitsCount = selectedPositions.length - existingSplitsCount;
|
const noSplitsCount = selectedSplitPageIds.length - existingSplitsCount;
|
||||||
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
||||||
|
|
||||||
const newSplitPositions = new Set(splitPositions);
|
const newSplitPositions = new Set(splitPositions);
|
||||||
|
|
||||||
if (shouldRemoveSplits) {
|
if (shouldRemoveSplits) {
|
||||||
selectedPositions.forEach((pos) => newSplitPositions.delete(pos));
|
selectedSplitPageIds.forEach((id) => newSplitPositions.delete(id));
|
||||||
} else {
|
} else {
|
||||||
selectedPositions.forEach((pos) => newSplitPositions.add(pos));
|
selectedSplitPageIds.forEach((id) => newSplitPositions.add(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const smartSplitCommand = {
|
const smartSplitCommand = {
|
||||||
execute: () => setSplitPositions(newSplitPositions),
|
execute: () => setSplitPositions(newSplitPositions),
|
||||||
undo: () => setSplitPositions(splitPositions),
|
undo: () => setSplitPositions(splitPositions),
|
||||||
description: shouldRemoveSplits
|
description: shouldRemoveSplits
|
||||||
? `Remove ${selectedPositions.length} split(s)`
|
? `Remove ${selectedSplitPageIds.length} split(s)`
|
||||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`,
|
: `Add ${selectedSplitPageIds.length - existingSplitsCount} split(s)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
executeCommandWithTracking(smartSplitCommand);
|
executeCommandWithTracking(smartSplitCommand);
|
||||||
@@ -283,16 +285,36 @@ export const usePageEditorCommands = ({
|
|||||||
insertAfterPage: number,
|
insertAfterPage: number,
|
||||||
isFromStorage?: boolean
|
isFromStorage?: boolean
|
||||||
) => {
|
) => {
|
||||||
|
console.log('[PageEditor] handleInsertFiles called:', {
|
||||||
|
fileCount: files.length,
|
||||||
|
insertAfterPage,
|
||||||
|
isFromStorage,
|
||||||
|
});
|
||||||
|
|
||||||
const workingDocument = getEditedDocument();
|
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 {
|
try {
|
||||||
const targetPage = workingDocument.pages.find(
|
const targetPage = workingDocument.pages.find(
|
||||||
(p) => p.pageNumber === insertAfterPage
|
(p) => p.pageNumber === insertAfterPage
|
||||||
);
|
);
|
||||||
if (!targetPage) return;
|
if (!targetPage) {
|
||||||
|
console.log('[PageEditor] Target page not found:', insertAfterPage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const insertAfterPageId = targetPage.id;
|
const insertAfterPageId = targetPage.id;
|
||||||
|
console.log('[PageEditor] Inserting files after page:', {
|
||||||
|
pageNumber: insertAfterPage,
|
||||||
|
pageId: insertAfterPageId,
|
||||||
|
});
|
||||||
|
|
||||||
let addedFileIds: FileId[] = [];
|
let addedFileIds: FileId[] = [];
|
||||||
if (isFromStorage) {
|
if (isFromStorage) {
|
||||||
const stubs = files as StirlingFileStub[];
|
const stubs = files as StirlingFileStub[];
|
||||||
@@ -307,6 +329,10 @@ export const usePageEditorCommands = ({
|
|||||||
insertAfterPageId,
|
insertAfterPageId,
|
||||||
});
|
});
|
||||||
addedFileIds = result.map((file) => file.fileId);
|
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));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { usePageDocument } from '@app/components/pageEditor/hooks/usePageDocument';
|
import { usePageDocument } from '@app/components/pageEditor/hooks/usePageDocument';
|
||||||
import { PDFDocument } from '@app/types/pageEditor';
|
import { PDFDocument } from '@app/types/pageEditor';
|
||||||
|
|
||||||
@@ -9,13 +9,29 @@ import { PDFDocument } from '@app/types/pageEditor';
|
|||||||
export function useInitialPageDocument(): PDFDocument | null {
|
export function useInitialPageDocument(): PDFDocument | null {
|
||||||
const { document: liveDocument } = usePageDocument();
|
const { document: liveDocument } = usePageDocument();
|
||||||
const [initialDocument, setInitialDocument] = useState<PDFDocument | null>(null);
|
const [initialDocument, setInitialDocument] = useState<PDFDocument | null>(null);
|
||||||
|
const lastDocumentIdRef = useRef<string | null>(null);
|
||||||
|
const liveDocumentId = liveDocument?.id ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only set once when we get the first non-null document
|
if (!liveDocumentId) {
|
||||||
if (liveDocument && !initialDocument) {
|
lastDocumentIdRef.current = null;
|
||||||
|
setInitialDocument(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveDocumentId !== lastDocumentIdRef.current) {
|
||||||
|
lastDocumentIdRef.current = liveDocumentId;
|
||||||
|
setInitialDocument(null);
|
||||||
|
}
|
||||||
|
}, [liveDocumentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!liveDocument || initialDocument) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📄 useInitialPageDocument: Captured initial document with', liveDocument.pages.length, 'pages');
|
console.log('📄 useInitialPageDocument: Captured initial document with', liveDocument.pages.length, 'pages');
|
||||||
setInitialDocument(liveDocument);
|
setInitialDocument(liveDocument);
|
||||||
}
|
|
||||||
}, [liveDocument, initialDocument]);
|
}, [liveDocument, initialDocument]);
|
||||||
|
|
||||||
return initialDocument;
|
return initialDocument;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useEffect, useState } from 'react';
|
import { useMemo, useEffect, useRef, 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';
|
||||||
@@ -59,22 +59,26 @@ 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);
|
const placeholderDocumentRef = useRef<PDFDocument | null>(null);
|
||||||
|
const [placeholderVersion, setPlaceholderVersion] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!primaryFileId) {
|
if (!primaryFileId) {
|
||||||
setPlaceholderDocument(null);
|
placeholderDocumentRef.current = null;
|
||||||
|
setPlaceholderVersion(v => v + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primaryStirlingFileStub?.processedFile) {
|
if (primaryStirlingFileStub?.processedFile) {
|
||||||
setPlaceholderDocument(null);
|
placeholderDocumentRef.current = null;
|
||||||
|
setPlaceholderVersion(v => v + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = selectors.getFile(primaryFileId);
|
const file = selectors.getFile(primaryFileId);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setPlaceholderDocument(null);
|
placeholderDocumentRef.current = null;
|
||||||
|
setPlaceholderVersion(v => v + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,16 +99,20 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
originalPageNumber: index + 1,
|
originalPageNumber: index + 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPlaceholderDocument({
|
if (!canceled) {
|
||||||
|
placeholderDocumentRef.current = {
|
||||||
id: `placeholder-${primaryFileId}`,
|
id: `placeholder-${primaryFileId}`,
|
||||||
name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name,
|
name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name,
|
||||||
file,
|
file,
|
||||||
pages,
|
pages,
|
||||||
totalPages,
|
totalPages,
|
||||||
});
|
};
|
||||||
|
setPlaceholderVersion(v => v + 1);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!canceled) {
|
if (!canceled) {
|
||||||
setPlaceholderDocument(null);
|
placeholderDocumentRef.current = null;
|
||||||
|
setPlaceholderVersion(v => v + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -122,19 +130,45 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
}, [currentPages]);
|
}, [currentPages]);
|
||||||
|
|
||||||
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
const mergedPdfDocument = useMemo((): PDFDocument | null => {
|
||||||
if (activeFileIds.length === 0) return null;
|
console.log('[usePageDocument] Building document:', {
|
||||||
|
activeFileIds: activeFileIds.length,
|
||||||
|
selectedActiveFileIds: selectedActiveFileIds.length,
|
||||||
|
hasPersistedDoc: !!persistedDocument,
|
||||||
|
persistedDocPages: persistedDocument?.pages.length,
|
||||||
|
persistedSig: persistedDocumentSignature?.substring(0, 50),
|
||||||
|
currentSig: currentPagesSignature.substring(0, 50),
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (activeFileIds.length === 0) {
|
||||||
|
console.log('[usePageDocument] No active files, returning null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 &&
|
persistedDocument &&
|
||||||
persistedDocumentSignature &&
|
persistedDocumentSignature &&
|
||||||
persistedDocumentSignature === currentPagesSignature &&
|
persistedDocumentSignature === currentPagesSignature &&
|
||||||
currentPagesSignature.length > 0
|
currentPagesSignature.length > 0 &&
|
||||||
) {
|
persistedFileIds.length === activeFileIds.length; // Ensure file count matches
|
||||||
|
|
||||||
|
if (persistedIsValid) {
|
||||||
|
console.log('[usePageDocument] Using persisted document');
|
||||||
return persistedDocument;
|
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) {
|
if (!primaryStirlingFileStub?.processedFile && placeholderDocumentRef.current) {
|
||||||
return placeholderDocument;
|
return placeholderDocumentRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
|
||||||
@@ -163,6 +197,10 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
activeFileIds.forEach(fileId => {
|
activeFileIds.forEach(fileId => {
|
||||||
const record = selectors.getStirlingFileStub(fileId);
|
const record = selectors.getStirlingFileStub(fileId);
|
||||||
if (record?.insertAfterPageId !== undefined) {
|
if (record?.insertAfterPageId !== undefined) {
|
||||||
|
console.log('[usePageDocument] File has insertAfterPageId:', {
|
||||||
|
fileId,
|
||||||
|
insertAfterPageId: record.insertAfterPageId,
|
||||||
|
});
|
||||||
if (!insertionMap.has(record.insertAfterPageId)) {
|
if (!insertionMap.has(record.insertAfterPageId)) {
|
||||||
insertionMap.set(record.insertAfterPageId, []);
|
insertionMap.set(record.insertAfterPageId, []);
|
||||||
}
|
}
|
||||||
@@ -172,6 +210,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
|
// Build pages by interleaving original pages with insertions
|
||||||
let pages: PDFPage[] = [];
|
let pages: PDFPage[] = [];
|
||||||
|
|
||||||
@@ -203,13 +247,15 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
if (processedFile?.pages && processedFile.pages.length > 0) {
|
if (processedFile?.pages && processedFile.pages.length > 0) {
|
||||||
// Use fully processed pages with thumbnails
|
// Use fully processed pages with thumbnails
|
||||||
filePages = processedFile.pages.map((page, pageIndex) => ({
|
filePages = processedFile.pages.map((page, pageIndex) => ({
|
||||||
id: `${fileId}-${page.pageNumber}`,
|
id: `${fileId}-${pageIndex + 1}`,
|
||||||
pageNumber: startPageNumber + pageIndex,
|
pageNumber: startPageNumber + pageIndex,
|
||||||
thumbnail: page.thumbnail || null,
|
thumbnail: page.thumbnail || null,
|
||||||
rotation: page.rotation || 0,
|
rotation: page.rotation || 0,
|
||||||
selected: false,
|
selected: false,
|
||||||
splitAfter: page.splitAfter || false,
|
splitAfter: page.splitAfter || false,
|
||||||
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
|
// Always use pageIndex + 1 for originalPageNumber to ensure correct numbering
|
||||||
|
// This prevents stale or incorrect page numbers from being cached
|
||||||
|
originalPageNumber: pageIndex + 1,
|
||||||
originalFileId: fileId,
|
originalFileId: fileId,
|
||||||
isPlaceholder: false,
|
isPlaceholder: false,
|
||||||
}));
|
}));
|
||||||
@@ -226,6 +272,20 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
splitAfter: false,
|
splitAfter: false,
|
||||||
isPlaceholder: 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;
|
return filePages;
|
||||||
@@ -327,12 +387,13 @@ export function usePageDocument(): PageDocumentHook {
|
|||||||
activeFilesSignature,
|
activeFilesSignature,
|
||||||
selectedFileIdsKey,
|
selectedFileIdsKey,
|
||||||
state.ui.selectedFileIds,
|
state.ui.selectedFileIds,
|
||||||
|
state.files.byId, // Force recompute when any file stub changes (including processedFile updates)
|
||||||
allFileIds,
|
allFileIds,
|
||||||
currentPagesSignature,
|
currentPagesSignature,
|
||||||
currentPages,
|
currentPages,
|
||||||
persistedDocument,
|
persistedDocument,
|
||||||
persistedDocumentSignature,
|
persistedDocumentSignature,
|
||||||
placeholderDocument,
|
placeholderVersion,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Large document detection for smart loading
|
// Large document detection for smart loading
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { documentManipulationService } from "@app/services/documentManipulationS
|
|||||||
import { pdfExportService } from "@app/services/pdfExportService";
|
import { pdfExportService } from "@app/services/pdfExportService";
|
||||||
import { exportProcessedDocumentsToFiles } from "@app/services/pdfExportHelpers";
|
import { exportProcessedDocumentsToFiles } from "@app/services/pdfExportHelpers";
|
||||||
import { FileId } from "@app/types/file";
|
import { FileId } from "@app/types/file";
|
||||||
import { PDFDocument } from "@app/types/pageEditor";
|
import { PDFDocument, PDFPage } from "@app/types/pageEditor";
|
||||||
|
|
||||||
type FileActions = ReturnType<typeof useFileActions>["actions"];
|
type FileActions = ReturnType<typeof useFileActions>["actions"];
|
||||||
type FileSelectors = ReturnType<typeof useFileState>["selectors"];
|
type FileSelectors = ReturnType<typeof useFileState>["selectors"];
|
||||||
@@ -16,14 +16,16 @@ type FileSelectors = ReturnType<typeof useFileState>["selectors"];
|
|||||||
interface UsePageEditorExportParams {
|
interface UsePageEditorExportParams {
|
||||||
displayDocument: PDFDocument | null;
|
displayDocument: PDFDocument | null;
|
||||||
selectedPageIds: string[];
|
selectedPageIds: string[];
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<string>;
|
||||||
selectedFileIds: FileId[];
|
selectedFileIds: FileId[];
|
||||||
selectors: FileSelectors;
|
selectors: FileSelectors;
|
||||||
actions: FileActions;
|
actions: FileActions;
|
||||||
setHasUnsavedChanges: (dirty: boolean) => void;
|
setHasUnsavedChanges: (dirty: boolean) => void;
|
||||||
exportLoading: boolean;
|
exportLoading: boolean;
|
||||||
setExportLoading: (loading: boolean) => void;
|
setExportLoading: (loading: boolean) => void;
|
||||||
setSplitPositions: Dispatch<SetStateAction<Set<number>>>;
|
setSplitPositions: Dispatch<SetStateAction<Set<string>>>;
|
||||||
|
clearPersistedDocument: () => void;
|
||||||
|
updateCurrentPages: (pages: PDFPage[] | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removePlaceholderPages = (document: PDFDocument): PDFDocument => {
|
const removePlaceholderPages = (document: PDFDocument): PDFDocument => {
|
||||||
@@ -67,6 +69,8 @@ export const usePageEditorExport = ({
|
|||||||
exportLoading,
|
exportLoading,
|
||||||
setExportLoading,
|
setExportLoading,
|
||||||
setSplitPositions,
|
setSplitPositions,
|
||||||
|
clearPersistedDocument,
|
||||||
|
updateCurrentPages,
|
||||||
}: UsePageEditorExportParams) => {
|
}: UsePageEditorExportParams) => {
|
||||||
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
||||||
const sourceFiles = new Map<FileId, File>();
|
const sourceFiles = new Map<FileId, File>();
|
||||||
@@ -272,6 +276,18 @@ export const usePageEditorExport = ({
|
|||||||
// Store source file IDs before adding new files
|
// Store source file IDs before adding new files
|
||||||
const sourceFileIds = [...selectedFileIds];
|
const sourceFileIds = [...selectedFileIds];
|
||||||
|
|
||||||
|
// Clear all cached page state to prevent stale data from being merged
|
||||||
|
clearPersistedDocument();
|
||||||
|
updateCurrentPages(null);
|
||||||
|
|
||||||
|
// Deselect old files immediately so the view can reset before we mutate the file list
|
||||||
|
actions.setSelectedFiles([]);
|
||||||
|
|
||||||
|
// Remove the original files before inserting the newly generated versions
|
||||||
|
if (sourceFileIds.length > 0) {
|
||||||
|
await actions.removeFiles(sourceFileIds, true);
|
||||||
|
}
|
||||||
|
|
||||||
const newStirlingFiles = await actions.addFiles(renamedFiles, {
|
const newStirlingFiles = await actions.addFiles(renamedFiles, {
|
||||||
selectFiles: true,
|
selectFiles: true,
|
||||||
});
|
});
|
||||||
@@ -279,11 +295,6 @@ export const usePageEditorExport = ({
|
|||||||
actions.setSelectedFiles(newStirlingFiles.map((file) => file.fileId));
|
actions.setSelectedFiles(newStirlingFiles.map((file) => file.fileId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove source files from context
|
|
||||||
if (sourceFileIds.length > 0) {
|
|
||||||
await actions.removeFiles(sourceFileIds, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
setSplitPositions(new Set());
|
setSplitPositions(new Set());
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
@@ -300,6 +311,8 @@ export const usePageEditorExport = ({
|
|||||||
selectedFileIds,
|
selectedFileIds,
|
||||||
setHasUnsavedChanges,
|
setHasUnsavedChanges,
|
||||||
setExportLoading,
|
setExportLoading,
|
||||||
|
clearPersistedDocument,
|
||||||
|
updateCurrentPages,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface PageEditorState {
|
|||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
|
|
||||||
// Split state
|
// Split state
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<string>;
|
||||||
|
|
||||||
// Export state
|
// Export state
|
||||||
exportLoading: boolean;
|
exportLoading: boolean;
|
||||||
@@ -21,7 +21,7 @@ export interface PageEditorState {
|
|||||||
setSelectedPageIds: (pages: string[]) => void;
|
setSelectedPageIds: (pages: string[]) => void;
|
||||||
setMovingPage: (pageNumber: number | null) => void;
|
setMovingPage: (pageNumber: number | null) => void;
|
||||||
setIsAnimating: (animating: boolean) => void;
|
setIsAnimating: (animating: boolean) => void;
|
||||||
setSplitPositions: React.Dispatch<React.SetStateAction<Set<number>>>;
|
setSplitPositions: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||||
setExportLoading: (loading: boolean) => void;
|
setExportLoading: (loading: boolean) => void;
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
@@ -44,19 +44,20 @@ export function usePageEditorState(): PageEditorState {
|
|||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
// Split state - position-based split tracking (replaces page-based splitAfter)
|
// Split state - position-based split tracking (replaces page-based splitAfter)
|
||||||
const [splitPositions, setSplitPositions] = useState<Set<number>>(new Set());
|
const [splitPositions, setSplitPositions] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Export state
|
// Export state
|
||||||
const [exportLoading, setExportLoading] = useState(false);
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const togglePage = useCallback((pageId: string) => {
|
const togglePage = useCallback((pageId: string) => {
|
||||||
setSelectedPageIds(prev =>
|
setSelectedPageIds(prev => {
|
||||||
prev.includes(pageId)
|
const newSelection = prev.includes(pageId)
|
||||||
? prev.filter(id => id !== 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[]) => {
|
const toggleSelectAll = useCallback((allPageIds: string[]) => {
|
||||||
if (!allPageIds.length) return;
|
if (!allPageIds.length) return;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { PDFDocument } from '@app/types/pageEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a map from page ID to its index in the provided document.
|
||||||
|
*/
|
||||||
|
export function buildPageIdIndexMap(document: PDFDocument | null): Map<string, number> {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
if (!document) return map;
|
||||||
|
document.pages.forEach((page, index) => {
|
||||||
|
map.set(page.id, index);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a set of split page IDs (the page preceding each split) into
|
||||||
|
* the current index positions inside the document.
|
||||||
|
*/
|
||||||
|
export function convertSplitPageIdsToIndexes(document: PDFDocument | null, splitPageIds: Set<string>): Set<number> {
|
||||||
|
const indexes = new Set<number>();
|
||||||
|
if (!document || !splitPageIds || splitPageIds.size === 0) {
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = document.pages.length;
|
||||||
|
document.pages.forEach((page, index) => {
|
||||||
|
if (index >= totalPages - 1) {
|
||||||
|
return; // Cannot split after the last page.
|
||||||
|
}
|
||||||
|
if (splitPageIds.has(page.id)) {
|
||||||
|
indexes.add(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { FileId } from '@app/types/file';
|
|||||||
import { useFileActions, useFileState } from '@app/contexts/FileContext';
|
import { useFileActions, useFileState } from '@app/contexts/FileContext';
|
||||||
import { PDFDocument, 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';
|
||||||
|
import { useNavigationState } from '@app/contexts/NavigationContext';
|
||||||
|
|
||||||
// PageEditorFile is now defined locally in consuming components
|
// PageEditorFile is now defined locally in consuming components
|
||||||
// Components should derive file list directly from FileContext
|
// Components should derive file list directly from FileContext
|
||||||
@@ -154,8 +155,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearPersistedDocument = useCallback(() => {
|
const clearPersistedDocument = useCallback(() => {
|
||||||
|
console.log('[PageEditorContext] Clearing persisted document');
|
||||||
setPersistedDocument(null);
|
setPersistedDocument(null);
|
||||||
setPersistedDocumentSignature(null);
|
setPersistedDocumentSignature(null);
|
||||||
|
setCurrentPages(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Page editor's own file order (independent of FileContext)
|
// Page editor's own file order (independent of FileContext)
|
||||||
@@ -165,6 +168,42 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
const { actions: fileActions } = useFileActions();
|
const { actions: fileActions } = useFileActions();
|
||||||
const { state } = useFileState();
|
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 fileContextSignature = useMemo(() => {
|
||||||
return state.files.ids
|
return state.files.ids
|
||||||
.map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`)
|
.map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`)
|
||||||
@@ -172,12 +211,41 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
}, [state.files.ids, state.files.byId]);
|
}, [state.files.ids, state.files.byId]);
|
||||||
|
|
||||||
const prevFileContextSignature = useRef<string | null>(null);
|
const prevFileContextSignature = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
const haveFileIdSetsChanged = (prevIds: FileId[], currentIds: FileId[]) => {
|
||||||
if (prevFileContextSignature.current !== fileContextSignature) {
|
if (prevIds.length !== currentIds.length) {
|
||||||
prevFileContextSignature.current = fileContextSignature;
|
return true;
|
||||||
clearPersistedDocument();
|
|
||||||
}
|
}
|
||||||
}, [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
|
// Keep a ref to always read latest state in stable callbacks
|
||||||
const stateRef = useRef(state);
|
const stateRef = useRef(state);
|
||||||
@@ -194,14 +262,21 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
|||||||
const prevFileIds = prevFileContextIdsRef.current;
|
const prevFileIds = prevFileContextIdsRef.current;
|
||||||
|
|
||||||
// Only react to FileContext changes, not our own fileOrder changes
|
// Only react to FileContext changes, not our own fileOrder changes
|
||||||
const fileContextChanged =
|
const fileContextChanged = haveFileIdSetsChanged(prevFileIds, currentFileIds);
|
||||||
currentFileIds.length !== prevFileIds.length ||
|
|
||||||
!currentFileIds.every((id, idx) => id === prevFileIds[idx]);
|
|
||||||
|
|
||||||
if (!fileContextChanged) {
|
if (!fileContextChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[PageEditorContext] FileContext files changed:', {
|
||||||
|
prevCount: prevFileIds.length,
|
||||||
|
currentCount: currentFileIds.length,
|
||||||
|
added: currentFileIds.filter(id => !prevFileIds.includes(id)).length,
|
||||||
|
removed: prevFileIds.filter(id => !currentFileIds.includes(id)).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearPersistedDocument();
|
||||||
|
|
||||||
prevFileContextIdsRef.current = currentFileIds;
|
prevFileContextIdsRef.current = currentFileIds;
|
||||||
|
|
||||||
// Collect new file IDs outside the setState callback so we can clear them after
|
// 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 => {
|
const drainHydrationQueue = (): void => {
|
||||||
if (activeHydrations >= HYDRATION_CONCURRENCY) return;
|
if (activeHydrations >= HYDRATION_CONCURRENCY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const nextTask = hydrationQueue.shift();
|
const nextTask = hydrationQueue.shift();
|
||||||
if (!nextTask) return;
|
if (!nextTask) return;
|
||||||
|
|
||||||
activeHydrations++;
|
activeHydrations++;
|
||||||
nextTask()
|
nextTask()
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
// Silently handle hydration failures
|
console.error('[Hydration] Task failed with error:', error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
activeHydrations--;
|
activeHydrations--;
|
||||||
@@ -341,8 +343,8 @@ export async function addFiles(
|
|||||||
try {
|
try {
|
||||||
const { generateThumbnailForFile } = await import('@app/utils/thumbnailUtils');
|
const { generateThumbnailForFile } = await import('@app/utils/thumbnailUtils');
|
||||||
thumbnail = await generateThumbnailForFile(targetFile);
|
thumbnail = await generateThumbnailForFile(targetFile);
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Silently handle thumbnail generation failures
|
console.warn(`[addFiles] Thumbnail generation failed for ${fileId}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +642,6 @@ export async function addStirlingFileStubs(
|
|||||||
scheduleMetadataHydration(async () => {
|
scheduleMetadataHydration(async () => {
|
||||||
const stirlingFile = await fileStorage.getStirlingFile(fileId);
|
const stirlingFile = await fileStorage.getStirlingFile(fileId);
|
||||||
if (!stirlingFile) {
|
if (!stirlingFile) {
|
||||||
console.warn(`📄 Failed to load StirlingFile for stub: ${stub.name} (${fileId})`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,6 +658,7 @@ export async function addStirlingFileStubs(
|
|||||||
if (needsProcessing) {
|
if (needsProcessing) {
|
||||||
// Regenerate metadata
|
// Regenerate metadata
|
||||||
const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile);
|
const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile);
|
||||||
|
|
||||||
if (processedFileMetadata) {
|
if (processedFileMetadata) {
|
||||||
const updates: Partial<StirlingFileStub> = {
|
const updates: Partial<StirlingFileStub> = {
|
||||||
processedFile: processedFileMetadata
|
processedFile: processedFileMetadata
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useAppConfig } from "@app/contexts/AppConfigContext";
|
|||||||
import { useLogoPath } from "@app/hooks/useLogoPath";
|
import { useLogoPath } from "@app/hooks/useLogoPath";
|
||||||
import { useLogoAssets } from '@app/hooks/useLogoAssets';
|
import { useLogoAssets } from '@app/hooks/useLogoAssets';
|
||||||
import { useFileContext } from "@app/contexts/file/fileHooks";
|
import { useFileContext } from "@app/contexts/file/fileHooks";
|
||||||
import { useNavigationActions } from "@app/contexts/NavigationContext";
|
import { useNavigationState, useNavigationActions } from "@app/contexts/NavigationContext";
|
||||||
import { useViewer } from "@app/contexts/ViewerContext";
|
import { useViewer } from "@app/contexts/ViewerContext";
|
||||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export default function HomePage() {
|
|||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
const { activeFiles } = useFileContext();
|
const { activeFiles } = useFileContext();
|
||||||
|
const navigationState = useNavigationState();
|
||||||
const { actions } = useNavigationActions();
|
const { actions } = useNavigationActions();
|
||||||
const { setActiveFileIndex } = useViewer();
|
const { setActiveFileIndex } = useViewer();
|
||||||
const prevFileCountRef = useRef(activeFiles.length);
|
const prevFileCountRef = useRef(activeFiles.length);
|
||||||
@@ -64,7 +65,11 @@ export default function HomePage() {
|
|||||||
const prevCount = prevFileCountRef.current;
|
const prevCount = prevFileCountRef.current;
|
||||||
const currentCount = activeFiles.length;
|
const currentCount = activeFiles.length;
|
||||||
|
|
||||||
if (prevCount === 0 && currentCount === 1) {
|
if (
|
||||||
|
navigationState.workbench !== 'fileEditor' &&
|
||||||
|
prevCount === 0 &&
|
||||||
|
currentCount === 1
|
||||||
|
) {
|
||||||
// PDF Text Editor handles its own empty state with a dropzone
|
// PDF Text Editor handles its own empty state with a dropzone
|
||||||
if (selectedToolKey !== 'pdfTextEditor') {
|
if (selectedToolKey !== 'pdfTextEditor') {
|
||||||
actions.setWorkbench('viewer');
|
actions.setWorkbench('viewer');
|
||||||
@@ -73,7 +78,13 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevFileCountRef.current = currentCount;
|
prevFileCountRef.current = currentCount;
|
||||||
}, [activeFiles.length, actions, setActiveFileIndex, selectedToolKey]);
|
}, [
|
||||||
|
activeFiles.length,
|
||||||
|
actions,
|
||||||
|
setActiveFileIndex,
|
||||||
|
selectedToolKey,
|
||||||
|
navigationState.workbench,
|
||||||
|
]);
|
||||||
|
|
||||||
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||||
const brandIconSrc = useLogoPath();
|
const brandIconSrc = useLogoPath();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||||
|
import { convertSplitPageIdsToIndexes } from '@app/components/pageEditor/utils/splitPositions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for applying DOM changes to PDF document state
|
* Service for applying DOM changes to PDF document state
|
||||||
@@ -9,7 +10,7 @@ export class DocumentManipulationService {
|
|||||||
* Apply all DOM changes (rotations, splits, reordering) to document state
|
* Apply all DOM changes (rotations, splits, reordering) to document state
|
||||||
* Returns single document or multiple documents if splits are present
|
* Returns single document or multiple documents if splits are present
|
||||||
*/
|
*/
|
||||||
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<number>): PDFDocument | PDFDocument[] {
|
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<string>): PDFDocument | PDFDocument[] {
|
||||||
// Use current display order (from React state) if provided, otherwise use original order
|
// Use current display order (from React state) if provided, otherwise use original order
|
||||||
const baseDocument = currentDisplayOrder || pdfDocument;
|
const baseDocument = currentDisplayOrder || pdfDocument;
|
||||||
|
|
||||||
@@ -17,10 +18,14 @@ export class DocumentManipulationService {
|
|||||||
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
|
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
|
||||||
|
|
||||||
// Convert position-based splits to page-based splits for export
|
// Convert position-based splits to page-based splits for export
|
||||||
if (splitPositions && splitPositions.size > 0) {
|
const resolvedSplitIndexes = splitPositions && splitPositions.size > 0
|
||||||
|
? convertSplitPageIdsToIndexes(baseDocument, splitPositions)
|
||||||
|
: new Set<number>();
|
||||||
|
|
||||||
|
if (resolvedSplitIndexes.size > 0) {
|
||||||
updatedPages = updatedPages.map((page, index) => ({
|
updatedPages = updatedPages.map((page, index) => ({
|
||||||
...page,
|
...page,
|
||||||
splitAfter: splitPositions.has(index)
|
splitAfter: resolvedSplitIndexes.has(index)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +36,7 @@ export class DocumentManipulationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check for splits and return multiple documents if needed
|
// Check for splits and return multiple documents if needed
|
||||||
if (splitPositions && splitPositions.size > 0) {
|
if (resolvedSplitIndexes.size > 0) {
|
||||||
return this.createSplitDocuments(finalDocument);
|
return this.createSplitDocuments(finalDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,6 @@ export interface PageEditorFunctions {
|
|||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
selectedPageIds: string[];
|
selectedPageIds: string[];
|
||||||
displayDocument?: PDFDocument;
|
displayDocument?: PDFDocument;
|
||||||
splitPositions: Set<number>;
|
splitPositions: Set<string>;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -390,13 +390,8 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b
|
|||||||
return { thumbnail, pageCount: 0 };
|
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 scale = calculateScaleFromFileSize(file.size);
|
||||||
|
const isVeryLarge = file.size >= 100 * 1024 * 1024; // 100MB threshold
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
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;
|
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||||
const thumbnail = canvas.toDataURL();
|
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
|
// Read rotation for all pages
|
||||||
const pageRotations: number[] = [];
|
const pageRotations: number[] = [];
|
||||||
for (let i = 1; i <= pageCount; i++) {
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user