mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
Working mostly
This commit is contained in:
parent
e7c6db082c
commit
74e8388bce
@ -6,6 +6,7 @@ import { GRID_CONSTANTS } from './constants';
|
||||
interface DragDropItem {
|
||||
id: string;
|
||||
splitAfter?: boolean;
|
||||
isPlaceholder?: boolean;
|
||||
}
|
||||
|
||||
interface DragDropGridProps<T extends DragDropItem> {
|
||||
@ -25,9 +26,12 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Filter out placeholder items (invisible pages for deselected files)
|
||||
const visibleItems = items.filter(item => !item.isPlaceholder);
|
||||
|
||||
// Responsive grid configuration
|
||||
const [itemsPerRow, setItemsPerRow] = useState(4);
|
||||
const OVERSCAN = items.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||
|
||||
// Calculate items per row based on container width
|
||||
const calculateItemsPerRow = useCallback(() => {
|
||||
@ -76,7 +80,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
|
||||
// Virtualization with react-virtual library
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: Math.ceil(items.length / itemsPerRow),
|
||||
count: Math.ceil(visibleItems.length / itemsPerRow),
|
||||
getScrollElement: () => containerRef.current?.closest('[data-scrolling-container]') as Element,
|
||||
estimateSize: () => {
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
@ -111,8 +115,8 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * itemsPerRow;
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, items.length);
|
||||
const rowItems = items.slice(startIndex, endIndex);
|
||||
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
|
||||
const rowItems = visibleItems.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Button, Select, Radio, Group, Stack } from '@mantine/core';
|
||||
|
||||
export type PageSize = 'A4' | 'Letter' | 'Legal' | 'A3' | 'A5';
|
||||
export type PageOrientation = 'portrait' | 'landscape';
|
||||
|
||||
export interface PageBreakSettings {
|
||||
size: PageSize;
|
||||
orientation: PageOrientation;
|
||||
}
|
||||
|
||||
interface PageBreakSettingsModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (settings: PageBreakSettings) => void;
|
||||
selectedPageCount: number;
|
||||
}
|
||||
|
||||
const PAGE_SIZES: { value: PageSize; label: string; dimensions: string }[] = [
|
||||
{ value: 'A4', label: 'A4', dimensions: '210 × 297 mm' },
|
||||
{ value: 'Letter', label: 'Letter', dimensions: '8.5 × 11 in' },
|
||||
{ value: 'Legal', label: 'Legal', dimensions: '8.5 × 14 in' },
|
||||
{ value: 'A3', label: 'A3', dimensions: '297 × 420 mm' },
|
||||
{ value: 'A5', label: 'A5', dimensions: '148 × 210 mm' },
|
||||
];
|
||||
|
||||
export const PageBreakSettingsModal: React.FC<PageBreakSettingsModalProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
onConfirm,
|
||||
selectedPageCount,
|
||||
}) => {
|
||||
const [size, setSize] = useState<PageSize>('A4');
|
||||
const [orientation, setOrientation] = useState<PageOrientation>('portrait');
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm({ size, orientation });
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={`Insert ${selectedPageCount} Page Break${selectedPageCount > 1 ? 's' : ''}`}
|
||||
centered
|
||||
size="md"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Page Size"
|
||||
value={size}
|
||||
onChange={(value) => setSize(value as PageSize)}
|
||||
data={PAGE_SIZES.map(ps => ({
|
||||
value: ps.value,
|
||||
label: `${ps.label} (${ps.dimensions})`
|
||||
}))}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div style={{ marginBottom: '0.5rem', fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
Orientation
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={orientation}
|
||||
onChange={(value) => setOrientation(value as PageOrientation)}
|
||||
>
|
||||
<Group gap="md">
|
||||
<Radio value="portrait" label="Portrait" />
|
||||
<Radio value="landscape" label="Landscape" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="subtle" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm}>
|
||||
Insert Page Break{selectedPageCount > 1 ? 's' : ''}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -22,11 +22,13 @@ import {
|
||||
SplitCommand,
|
||||
BulkRotateCommand,
|
||||
PageBreakCommand,
|
||||
UndoManager
|
||||
UndoManager,
|
||||
PageBreakSettings
|
||||
} from './commands/pageCommands';
|
||||
import { GRID_CONSTANTS } from './constants';
|
||||
import { usePageDocument } from './hooks/usePageDocument';
|
||||
import { usePageEditorState } from './hooks/usePageEditorState';
|
||||
import { PageBreakSettingsModal } from './PageBreakSettingsModal';
|
||||
|
||||
export interface PageEditorProps {
|
||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||
@ -44,13 +46,13 @@ const PageEditor = ({
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
// Get PageEditor coordination functions
|
||||
const { updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages } = usePageEditor();
|
||||
const { updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages, fileOrder } = usePageEditor();
|
||||
|
||||
// Derive page editor files from FileContext (single source of truth)
|
||||
// Derive page editor files from PageEditorContext's fileOrder (page editor workspace order)
|
||||
// Filter to only show PDF files (PageEditor only supports PDFs)
|
||||
// Use stable string keys to prevent infinite loops
|
||||
// Cache file objects to prevent infinite re-renders from new object references
|
||||
const fileIdsKey = state.files.ids.join(',');
|
||||
const fileOrderKey = fileOrder.join(',');
|
||||
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
|
||||
const filesSignature = selectors.getFilesSignature();
|
||||
|
||||
@ -60,7 +62,7 @@ const PageEditor = ({
|
||||
const cache = fileObjectsRef.current;
|
||||
const newFiles: any[] = [];
|
||||
|
||||
state.files.ids.forEach(fileId => {
|
||||
fileOrder.forEach(fileId => {
|
||||
const stub = selectors.getStirlingFileStub(fileId);
|
||||
const isSelected = state.ui.selectedFileIds.includes(fileId);
|
||||
const isPdf = stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
|
||||
@ -100,10 +102,15 @@ const PageEditor = ({
|
||||
|
||||
return newFiles;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fileIdsKey, selectedIdsKey, filesSignature]);
|
||||
}, [fileOrderKey, selectedIdsKey, filesSignature]);
|
||||
|
||||
// Get active file IDs from SELECTED files only
|
||||
const activeFileIds = useMemo(() => {
|
||||
// Get ALL file IDs in order (not filtered by selection)
|
||||
const orderedFileIds = useMemo(() => {
|
||||
return pageEditorFiles.map(f => f.fileId);
|
||||
}, [pageEditorFiles]);
|
||||
|
||||
// Get selected file IDs for filtering
|
||||
const selectedFileIds = useMemo(() => {
|
||||
return pageEditorFiles.filter(f => f.isSelected).map(f => f.fileId);
|
||||
}, [pageEditorFiles]);
|
||||
|
||||
@ -137,6 +144,10 @@ const PageEditor = ({
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
|
||||
// Page break modal state
|
||||
const [pageBreakModalOpened, setPageBreakModalOpened] = useState(false);
|
||||
const [pageBreakModalPageCount, setPageBreakModalPageCount] = useState(0);
|
||||
|
||||
// Update undo/redo state
|
||||
const updateUndoRedoState = useCallback(() => {
|
||||
setCanUndo(undoManagerRef.current.canUndo());
|
||||
@ -202,12 +213,89 @@ const PageEditor = ({
|
||||
}
|
||||
}, [reorderedPages, displayDocument, clearReorderedPages]);
|
||||
|
||||
// Update file order when pages are manually reordered
|
||||
// Function to reorder pages based on new file order
|
||||
const reorderPagesByFileOrder = useCallback((newFileOrder: FileId[]) => {
|
||||
const docToUpdate = editedDocument || mergedPdfDocument;
|
||||
if (!docToUpdate) return;
|
||||
|
||||
// Group pages by originalFileId
|
||||
const pagesByFileId = new Map<FileId, PDFPage[]>();
|
||||
docToUpdate.pages.forEach(page => {
|
||||
if (page.originalFileId) {
|
||||
if (!pagesByFileId.has(page.originalFileId)) {
|
||||
pagesByFileId.set(page.originalFileId, []);
|
||||
}
|
||||
pagesByFileId.get(page.originalFileId)!.push(page);
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild pages array in new file order
|
||||
const reorderedPages: PDFPage[] = [];
|
||||
newFileOrder.forEach(fileId => {
|
||||
const filePages = pagesByFileId.get(fileId);
|
||||
if (filePages) {
|
||||
reorderedPages.push(...filePages);
|
||||
}
|
||||
});
|
||||
|
||||
// Renumber pages
|
||||
const renumberedPages = reorderedPages.map((page, idx) => ({
|
||||
...page,
|
||||
pageNumber: idx + 1
|
||||
}));
|
||||
|
||||
setEditedDocument({
|
||||
...docToUpdate,
|
||||
pages: renumberedPages,
|
||||
totalPages: renumberedPages.length
|
||||
});
|
||||
}, [editedDocument, mergedPdfDocument]);
|
||||
|
||||
// When file selection changes, update placeholder flags in editedDocument
|
||||
const prevSelectedFileIdsRef = useRef<FileId[]>([]);
|
||||
useEffect(() => {
|
||||
if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) {
|
||||
updateFileOrderFromPages(editedDocument.pages);
|
||||
const prevIds = prevSelectedFileIdsRef.current;
|
||||
const currentIds = selectedFileIds;
|
||||
|
||||
// Skip if no change or first render
|
||||
if (prevIds.length === 0 && currentIds.length > 0) {
|
||||
prevSelectedFileIdsRef.current = currentIds;
|
||||
return;
|
||||
}
|
||||
}, [editedDocument?.pages, activeFileIds.length, updateFileOrderFromPages]);
|
||||
|
||||
if (prevIds.length === currentIds.length && prevIds.every((id, idx) => id === currentIds[idx])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use editedDocument if available, otherwise use mergedPdfDocument
|
||||
const docToUpdate = editedDocument || mergedPdfDocument;
|
||||
if (!docToUpdate) {
|
||||
prevSelectedFileIdsRef.current = currentIds;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSet = new Set(currentIds);
|
||||
|
||||
// Update placeholder flags on existing pages based on selection
|
||||
const updatedPages = docToUpdate.pages.map(page => {
|
||||
const isSelected = page.originalFileId ? currentSet.has(page.originalFileId) : true;
|
||||
|
||||
// If file is deselected, mark all its pages as placeholders
|
||||
// If file is selected, remove placeholder flag
|
||||
if (!isSelected) {
|
||||
return { ...page, isPlaceholder: true };
|
||||
} else {
|
||||
return { ...page, isPlaceholder: false };
|
||||
}
|
||||
});
|
||||
|
||||
setEditedDocument({
|
||||
...docToUpdate,
|
||||
pages: updatedPages
|
||||
});
|
||||
|
||||
prevSelectedFileIdsRef.current = currentIds;
|
||||
}, [selectedFileIds, editedDocument, mergedPdfDocument]);
|
||||
|
||||
// Utility functions to convert between page IDs and page numbers
|
||||
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
||||
@ -451,13 +539,22 @@ const PageEditor = ({
|
||||
const handlePageBreak = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Show modal to get page break settings
|
||||
setPageBreakModalPageCount(selectedPageIds.length);
|
||||
setPageBreakModalOpened(true);
|
||||
}, [selectedPageIds, displayDocument]);
|
||||
|
||||
const handlePageBreakConfirm = useCallback((settings: PageBreakSettings) => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page IDs to page numbers for the command
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
|
||||
const pageBreakCommand = new PageBreakCommand(
|
||||
selectedPageNumbers,
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
setEditedDocument,
|
||||
settings
|
||||
);
|
||||
executeCommandWithTracking(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
@ -515,17 +612,18 @@ const PageEditor = ({
|
||||
targetIndex,
|
||||
selectedPages,
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
setEditedDocument,
|
||||
(newPages) => updateFileOrderFromPages(newPages)
|
||||
);
|
||||
executeCommandWithTracking(reorderCommand);
|
||||
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking, updateFileOrderFromPages]);
|
||||
|
||||
// Helper function to collect source files for multi-file export
|
||||
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
||||
const sourceFiles = new Map<FileId, File>();
|
||||
|
||||
// Always include original files
|
||||
activeFileIds.forEach(fileId => {
|
||||
// Always include selected files
|
||||
selectedFileIds.forEach(fileId => {
|
||||
const file = selectors.getFile(fileId);
|
||||
if (file) {
|
||||
sourceFiles.set(fileId, file);
|
||||
@ -534,31 +632,31 @@ const PageEditor = ({
|
||||
|
||||
// Use multi-file export if we have multiple original files
|
||||
const hasInsertedFiles = false;
|
||||
const hasMultipleOriginalFiles = activeFileIds.length > 1;
|
||||
const hasMultipleOriginalFiles = selectedFileIds.length > 1;
|
||||
|
||||
if (!hasInsertedFiles && !hasMultipleOriginalFiles) {
|
||||
return null; // Use single-file export method
|
||||
}
|
||||
|
||||
return sourceFiles.size > 0 ? sourceFiles : null;
|
||||
}, [activeFileIds, selectors]);
|
||||
}, [selectedFileIds, selectors]);
|
||||
|
||||
// Helper function to generate proper filename for exports
|
||||
const getExportFilename = useCallback((): string => {
|
||||
if (activeFileIds.length <= 1) {
|
||||
if (selectedFileIds.length <= 1) {
|
||||
// Single file - use original name
|
||||
return displayDocument?.name || 'document.pdf';
|
||||
}
|
||||
|
||||
// Multiple files - use first file name with " (merged)" suffix
|
||||
const firstFile = selectors.getFile(activeFileIds[0]);
|
||||
const firstFile = selectors.getFile(selectedFileIds[0]);
|
||||
if (firstFile) {
|
||||
const baseName = firstFile.name.replace(/\.pdf$/i, '');
|
||||
return `${baseName} (merged).pdf`;
|
||||
}
|
||||
|
||||
return 'merged-document.pdf';
|
||||
}, [activeFileIds, selectors, displayDocument]);
|
||||
}, [selectedFileIds, selectors, displayDocument]);
|
||||
|
||||
const onExportSelected = useCallback(async () => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@ -674,13 +772,13 @@ const PageEditor = ({
|
||||
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
|
||||
|
||||
// Step 3: Create StirlingFiles and stubs for version history
|
||||
const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
|
||||
const parentStub = selectors.getStirlingFileStub(selectedFileIds[0]);
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool');
|
||||
|
||||
// Step 4: Consume files (replace in context)
|
||||
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
|
||||
await actions.consumeFiles(selectedFileIds, stirlingFiles, stubs);
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
setExportLoading(false);
|
||||
@ -688,7 +786,7 @@ const PageEditor = ({
|
||||
console.error('Apply changes failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, selectedFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
|
||||
|
||||
|
||||
const closePdf = useCallback(() => {
|
||||
@ -733,6 +831,7 @@ const PageEditor = ({
|
||||
onExportSelected,
|
||||
onExportAll,
|
||||
applyChanges,
|
||||
reorderPagesByFileOrder,
|
||||
exportLoading,
|
||||
selectionMode,
|
||||
selectedPageIds,
|
||||
@ -744,7 +843,7 @@ const PageEditor = ({
|
||||
}
|
||||
}, [
|
||||
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
|
||||
handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
|
||||
handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, reorderPagesByFileOrder, exportLoading,
|
||||
selectionMode, selectedPageIds, splitPositions, displayDocument?.pages.length, closePdf
|
||||
]);
|
||||
|
||||
@ -757,28 +856,28 @@ const PageEditor = ({
|
||||
// Create a stable mapping of fileId to color index (preserves colors on reorder)
|
||||
const fileColorIndexMap = useMemo(() => {
|
||||
// Assign colors to new files based on insertion order
|
||||
activeFileIds.forEach(fileId => {
|
||||
orderedFileIds.forEach(fileId => {
|
||||
if (!fileColorAssignments.current.has(fileId)) {
|
||||
fileColorAssignments.current.set(fileId, fileColorAssignments.current.size);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up removed files
|
||||
const activeSet = new Set(activeFileIds);
|
||||
// Clean up removed files (only remove files that are completely gone, not just deselected)
|
||||
const allFilesSet = new Set(orderedFileIds);
|
||||
for (const fileId of fileColorAssignments.current.keys()) {
|
||||
if (!activeSet.has(fileId)) {
|
||||
if (!allFilesSet.has(fileId)) {
|
||||
fileColorAssignments.current.delete(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
return fileColorAssignments.current;
|
||||
}, [activeFileIds.join(',')]); // Only recalculate when the set of files changes, not the order
|
||||
}, [orderedFileIds.join(',')]); // Only recalculate when the set of files changes, not the order
|
||||
|
||||
return (
|
||||
<Box pos="relative" h='100%' style={{ overflow: 'auto' }} data-scrolling-container="true">
|
||||
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
|
||||
|
||||
{!mergedPdfDocument && !globalProcessing && activeFileIds.length === 0 && (
|
||||
{!mergedPdfDocument && !globalProcessing && selectedFileIds.length === 0 && (
|
||||
<Center h='100%'>
|
||||
<Stack align="center" gap="md">
|
||||
<Text size="lg" c="dimmed">📄</Text>
|
||||
@ -908,6 +1007,13 @@ const PageEditor = ({
|
||||
await onExportAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
<PageBreakSettingsModal
|
||||
opened={pageBreakModalOpened}
|
||||
onClose={() => setPageBreakModalOpened(false)}
|
||||
onConfirm={handlePageBreakConfirm}
|
||||
selectedPageCount={pageBreakModalPageCount}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -281,7 +281,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
setMouseStartPos(null);
|
||||
}, []);
|
||||
|
||||
const fileColorBorder = getFileColorWithOpacity(fileColorIndex, 0.3);
|
||||
const fileColorBorder = page.isBlankPage ? 'transparent' : getFileColorWithOpacity(fileColorIndex, 0.3);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -410,7 +410,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
left: 5,
|
||||
background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)',
|
||||
background: 'rgba(162, 201, 255, 0.8)',
|
||||
padding: '6px 8px',
|
||||
borderRadius: 8,
|
||||
zIndex: 2,
|
||||
|
||||
@ -161,7 +161,8 @@ export class ReorderPagesCommand extends DOMCommand {
|
||||
private targetIndex: number,
|
||||
private selectedPages: number[] | undefined,
|
||||
private getCurrentDocument: () => PDFDocument | null,
|
||||
private setDocument: (doc: PDFDocument) => void
|
||||
private setDocument: (doc: PDFDocument) => void,
|
||||
private onReorderComplete?: (newPages: PDFPage[]) => void
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -210,6 +211,11 @@ export class ReorderPagesCommand extends DOMCommand {
|
||||
};
|
||||
|
||||
this.setDocument(reorderedDocument);
|
||||
|
||||
// Notify that reordering is complete
|
||||
if (this.onReorderComplete) {
|
||||
this.onReorderComplete(newPages);
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
@ -408,6 +414,14 @@ export class SplitAllCommand extends DOMCommand {
|
||||
}
|
||||
}
|
||||
|
||||
export type PageSize = 'A4' | 'Letter' | 'Legal' | 'A3' | 'A5';
|
||||
export type PageOrientation = 'portrait' | 'landscape';
|
||||
|
||||
export interface PageBreakSettings {
|
||||
size: PageSize;
|
||||
orientation: PageOrientation;
|
||||
}
|
||||
|
||||
export class PageBreakCommand extends DOMCommand {
|
||||
private insertedPages: PDFPage[] = [];
|
||||
private originalDocument: PDFDocument | null = null;
|
||||
@ -415,7 +429,8 @@ export class PageBreakCommand extends DOMCommand {
|
||||
constructor(
|
||||
private selectedPageNumbers: number[],
|
||||
private getCurrentDocument: () => PDFDocument | null,
|
||||
private setDocument: (doc: PDFDocument) => void
|
||||
private setDocument: (doc: PDFDocument) => void,
|
||||
private settings?: PageBreakSettings
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -450,7 +465,8 @@ export class PageBreakCommand extends DOMCommand {
|
||||
rotation: 0,
|
||||
selected: false,
|
||||
splitAfter: false,
|
||||
isBlankPage: true // Custom flag for blank pages
|
||||
isBlankPage: true, // Custom flag for blank pages
|
||||
pageBreakSettings: this.settings // Store settings for export
|
||||
};
|
||||
newPages.push(blankPage);
|
||||
this.insertedPages.push(blankPage);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useFileState } from '../../../contexts/FileContext';
|
||||
import { usePageEditor } from '../../../contexts/PageEditorContext';
|
||||
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
|
||||
import { FileId } from '../../../types/file';
|
||||
|
||||
@ -15,9 +16,11 @@ export interface PageDocumentHook {
|
||||
*/
|
||||
export function usePageDocument(): PageDocumentHook {
|
||||
const { state, selectors } = useFileState();
|
||||
const { fileOrder } = usePageEditor();
|
||||
|
||||
// Convert Set to array and filter to maintain file order from FileContext
|
||||
const allFileIds = state.files.ids;
|
||||
// Use PageEditorContext's fileOrder instead of FileContext's global order
|
||||
// This ensures the page editor respects its own workspace ordering
|
||||
const allFileIds = fileOrder;
|
||||
|
||||
// Derive selected file IDs directly from FileContext (single source of truth)
|
||||
// Filter to only include PDF files (PageEditor only supports PDFs)
|
||||
@ -26,15 +29,14 @@ export function usePageDocument(): PageDocumentHook {
|
||||
const selectedIdsKey = [...state.ui.selectedFileIds].sort().join(',');
|
||||
const filesSignature = selectors.getFilesSignature();
|
||||
|
||||
// Get ALL PDF files (selected or not) for document building with placeholders
|
||||
const activeFileIds = useMemo(() => {
|
||||
const selectedFileIds = new Set(state.ui.selectedFileIds);
|
||||
return allFileIds.filter(id => {
|
||||
if (!selectedFileIds.has(id)) return false;
|
||||
const stub = selectors.getStirlingFileStub(id);
|
||||
return stub?.name?.toLowerCase().endsWith('.pdf') ?? false;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allFileIdsKey, selectedIdsKey, filesSignature]);
|
||||
}, [allFileIdsKey, filesSignature]);
|
||||
|
||||
const primaryFileId = activeFileIds[0] ?? null;
|
||||
|
||||
@ -84,13 +86,28 @@ export function usePageDocument(): PageDocumentHook {
|
||||
// Build pages by interleaving original pages with insertions
|
||||
let pages: PDFPage[] = [];
|
||||
|
||||
// Helper function to create pages from a file
|
||||
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
|
||||
// Helper function to create pages from a file (or placeholder if deselected)
|
||||
const createPagesFromFile = (fileId: FileId, startPageNumber: number, isSelected: boolean): PDFPage[] => {
|
||||
const stirlingFileStub = selectors.getStirlingFileStub(fileId);
|
||||
if (!stirlingFileStub) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If file is deselected, create a single placeholder page
|
||||
if (!isSelected) {
|
||||
return [{
|
||||
id: `${fileId}-placeholder`,
|
||||
pageNumber: startPageNumber,
|
||||
originalPageNumber: 1,
|
||||
originalFileId: fileId,
|
||||
rotation: 0,
|
||||
thumbnail: null,
|
||||
selected: false,
|
||||
splitAfter: false,
|
||||
isPlaceholder: true,
|
||||
}];
|
||||
}
|
||||
|
||||
const processedFile = stirlingFileStub.processedFile;
|
||||
let filePages: PDFPage[] = [];
|
||||
|
||||
@ -105,6 +122,7 @@ export function usePageDocument(): PageDocumentHook {
|
||||
splitAfter: page.splitAfter || false,
|
||||
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
|
||||
originalFileId: fileId,
|
||||
isPlaceholder: false,
|
||||
}));
|
||||
} else if (processedFile?.totalPages) {
|
||||
// Fallback: create pages without thumbnails but with correct count
|
||||
@ -117,6 +135,7 @@ export function usePageDocument(): PageDocumentHook {
|
||||
thumbnail: null,
|
||||
selected: false,
|
||||
splitAfter: false,
|
||||
isPlaceholder: false,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -124,9 +143,11 @@ export function usePageDocument(): PageDocumentHook {
|
||||
};
|
||||
|
||||
// Collect all pages from original files (without renumbering yet)
|
||||
const selectedFileIdsSet = new Set(state.ui.selectedFileIds);
|
||||
const originalFilePages: PDFPage[] = [];
|
||||
originalFileIds.forEach(fileId => {
|
||||
const filePages = createPagesFromFile(fileId, 1); // Temporary numbering
|
||||
const isSelected = selectedFileIdsSet.has(fileId);
|
||||
const filePages = createPagesFromFile(fileId, 1, isSelected); // Temporary numbering
|
||||
originalFilePages.push(...filePages);
|
||||
});
|
||||
|
||||
@ -145,7 +166,8 @@ export function usePageDocument(): PageDocumentHook {
|
||||
// Collect all pages to insert
|
||||
const allNewPages: PDFPage[] = [];
|
||||
fileIds.forEach(fileId => {
|
||||
const insertedPages = createPagesFromFile(fileId, 1);
|
||||
const isSelected = selectedFileIdsSet.has(fileId);
|
||||
const insertedPages = createPagesFromFile(fileId, 1, isSelected);
|
||||
allNewPages.push(...insertedPages);
|
||||
});
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { FileDropdownMenu } from './FileDropdownMenu';
|
||||
import { PageEditorFileDropdown } from './PageEditorFileDropdown';
|
||||
import { usePageEditor } from '../../contexts/PageEditorContext';
|
||||
import { useFileState } from '../../contexts/FileContext';
|
||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||
import { FileId } from '../../types/file';
|
||||
|
||||
// Local interface for PageEditor file display
|
||||
@ -262,10 +263,24 @@ const TopControls = ({
|
||||
return fileColorAssignments.current;
|
||||
}, [fileIdsString]);
|
||||
|
||||
// Memoize the reorder handler - now much simpler!
|
||||
// Get pageEditorFunctions from ToolWorkflowContext
|
||||
const { pageEditorFunctions } = useToolWorkflow();
|
||||
|
||||
// Memoize the reorder handler
|
||||
const handleReorder = useCallback((fromIndex: number, toIndex: number) => {
|
||||
// Reorder files in PageEditorContext (updates fileOrder)
|
||||
pageEditorReorderFiles(fromIndex, toIndex);
|
||||
}, [pageEditorReorderFiles]);
|
||||
|
||||
// Also reorder pages directly
|
||||
const newOrder = [...pageEditorFileOrder];
|
||||
const [movedFileId] = newOrder.splice(fromIndex, 1);
|
||||
newOrder.splice(toIndex, 0, movedFileId);
|
||||
|
||||
// Call reorderPagesByFileOrder if available
|
||||
if (pageEditorFunctions?.reorderPagesByFileOrder) {
|
||||
pageEditorFunctions.reorderPagesByFileOrder(newOrder);
|
||||
}
|
||||
}, [pageEditorReorderFiles, pageEditorFileOrder, pageEditorFunctions]);
|
||||
|
||||
const handleViewChange = useCallback((view: string) => {
|
||||
if (!isValidWorkbench(view)) {
|
||||
|
||||
@ -155,16 +155,47 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
|
||||
React.useEffect(() => {
|
||||
const currentFileIds = state.files.ids;
|
||||
|
||||
// Add new files to the end
|
||||
// Identify new files
|
||||
const newFileIds = currentFileIds.filter(id => !fileOrder.includes(id));
|
||||
|
||||
// Remove deleted files
|
||||
const validFileOrder = fileOrder.filter(id => currentFileIds.includes(id));
|
||||
|
||||
if (newFileIds.length > 0 || validFileOrder.length !== fileOrder.length) {
|
||||
setFileOrder([...validFileOrder, ...newFileIds]);
|
||||
// Check if new files have insertion positions
|
||||
let hasInsertionPosition = false;
|
||||
for (const fileId of newFileIds) {
|
||||
const stub = state.files.byId[fileId];
|
||||
if (stub?.insertAfterPageId) {
|
||||
hasInsertionPosition = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInsertionPosition) {
|
||||
// Respect FileContext order when files have insertion positions
|
||||
// FileContext already handled the positioning logic
|
||||
const orderedNewFiles = currentFileIds.filter(id => newFileIds.includes(id));
|
||||
const orderedValidFiles = currentFileIds.filter(id => validFileOrder.includes(id));
|
||||
|
||||
// Merge while preserving FileContext order
|
||||
const newOrder: FileId[] = [];
|
||||
const newFilesSet = new Set(orderedNewFiles);
|
||||
const validFilesSet = new Set(orderedValidFiles);
|
||||
|
||||
currentFileIds.forEach(id => {
|
||||
if (newFilesSet.has(id) || validFilesSet.has(id)) {
|
||||
newOrder.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
setFileOrder(newOrder);
|
||||
} else {
|
||||
// No insertion positions - append new files to end
|
||||
setFileOrder([...validFileOrder, ...newFileIds]);
|
||||
}
|
||||
}
|
||||
}, [state.files.ids, fileOrder]);
|
||||
}, [state.files.ids, state.files.byId, fileOrder]);
|
||||
|
||||
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
|
||||
setCurrentPages(pages);
|
||||
|
||||
@ -75,20 +75,73 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
||||
const { stirlingFileStubs } = action.payload;
|
||||
const newIds: FileId[] = [];
|
||||
const newById: Record<FileId, StirlingFileStub> = { ...state.files.byId };
|
||||
let hasInsertionPosition = false;
|
||||
let insertAfterPageId: string | undefined;
|
||||
|
||||
stirlingFileStubs.forEach(record => {
|
||||
// Only add if not already present (dedupe by stable ID)
|
||||
if (!newById[record.id]) {
|
||||
newIds.push(record.id);
|
||||
newById[record.id] = record;
|
||||
|
||||
// Track if any file has an insertion position
|
||||
if (record.insertAfterPageId) {
|
||||
hasInsertionPosition = true;
|
||||
insertAfterPageId = record.insertAfterPageId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Determine final file order
|
||||
let finalIds: FileId[];
|
||||
|
||||
if (hasInsertionPosition && insertAfterPageId) {
|
||||
// Find the file that contains the page with insertAfterPageId
|
||||
let insertIndex = state.files.ids.length; // Default to end
|
||||
|
||||
for (let i = 0; i < state.files.ids.length; i++) {
|
||||
const fileId = state.files.ids[i];
|
||||
const fileStub = state.files.byId[fileId];
|
||||
|
||||
if (fileStub?.processedFile?.pages) {
|
||||
const hasPage = fileStub.processedFile.pages.some(page => {
|
||||
// Page ID format: fileId-pageNumber
|
||||
const pageId = `${fileId}-${page.pageNumber}`;
|
||||
return pageId === insertAfterPageId;
|
||||
});
|
||||
|
||||
if (hasPage) {
|
||||
insertIndex = i + 1; // Insert after this file
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new files at the calculated position
|
||||
finalIds = [
|
||||
...state.files.ids.slice(0, insertIndex),
|
||||
...newIds,
|
||||
...state.files.ids.slice(insertIndex)
|
||||
];
|
||||
} else {
|
||||
// No insertion position - append to end
|
||||
finalIds = [...state.files.ids, ...newIds];
|
||||
}
|
||||
|
||||
// Auto-select inserted files
|
||||
const newSelectedFileIds = hasInsertionPosition
|
||||
? [...state.ui.selectedFileIds, ...newIds]
|
||||
: state.ui.selectedFileIds;
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: [...state.files.ids, ...newIds],
|
||||
ids: finalIds,
|
||||
byId: newById
|
||||
},
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: newSelectedFileIds
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FileId } from './file';
|
||||
import { PageBreakSettings } from '../components/pageEditor/commands/pageCommands';
|
||||
|
||||
export interface PDFPage {
|
||||
id: string;
|
||||
@ -9,7 +10,9 @@ export interface PDFPage {
|
||||
selected: boolean;
|
||||
splitAfter?: boolean;
|
||||
isBlankPage?: boolean;
|
||||
isPlaceholder?: boolean;
|
||||
originalFileId?: FileId;
|
||||
pageBreakSettings?: PageBreakSettings;
|
||||
}
|
||||
|
||||
export interface PDFDocument {
|
||||
@ -62,6 +65,7 @@ export interface PageEditorFunctions {
|
||||
onExportSelected: () => void;
|
||||
onExportAll: () => void;
|
||||
applyChanges: () => void;
|
||||
reorderPagesByFileOrder: (newFileOrder: FileId[]) => void;
|
||||
exportLoading: boolean;
|
||||
selectionMode: boolean;
|
||||
selectedPageIds: string[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user