Reworked page editor - dirty commit

This commit is contained in:
Reece 2025-10-14 12:41:50 +01:00
parent 6acce968a5
commit 39267e795c
6 changed files with 541 additions and 99 deletions

View File

@ -44,7 +44,7 @@ const PageEditor = ({
const { setHasUnsavedChanges } = useNavigationGuard();
// Get selected files from PageEditorContext instead of all files
const { selectedFileIds, syncWithFileContext } = usePageEditor();
const { selectedFileIds, syncWithFileContext, updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages } = usePageEditor();
// Stable reference to file IDs to prevent infinite loops
const fileIdsString = state.files.ids.join(',');
@ -137,6 +137,56 @@ const PageEditor = ({
// Interface functions for parent component
const displayDocument = editedDocument || mergedPdfDocument;
// Update current pages in context whenever displayDocument changes
useEffect(() => {
if (displayDocument?.pages) {
updateCurrentPages(displayDocument.pages);
}
}, [displayDocument?.pages, updateCurrentPages]);
// Apply reordered pages from file reordering
useEffect(() => {
if (reorderedPages && reorderedPages.length > 0 && displayDocument) {
// Create a new document with reordered pages
const reorderedDocument: PDFDocument = {
...displayDocument,
pages: reorderedPages,
totalPages: reorderedPages.length,
};
setEditedDocument(reorderedDocument);
clearReorderedPages();
}
}, [reorderedPages, displayDocument, clearReorderedPages]);
// Update file order when pages are manually reordered
useEffect(() => {
if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) {
// Compute the file order based on page positions
const fileFirstPagePositions = new Map<FileId, number>();
editedDocument.pages.forEach((page, index) => {
const fileId = page.originalFileId;
if (!fileId) return;
if (!fileFirstPagePositions.has(fileId)) {
fileFirstPagePositions.set(fileId, index);
}
});
// Sort files by their first page position
const computedFileOrder = Array.from(fileFirstPagePositions.entries())
.sort((a, b) => a[1] - b[1])
.map(entry => entry[0]);
// Check if the order has actually changed
const currentFileOrder = state.files.ids.filter(id => selectedFileIds.has(id));
const orderChanged = computedFileOrder.length !== currentFileOrder.length ||
computedFileOrder.some((id, index) => id !== currentFileOrder[index]);
if (orderChanged && computedFileOrder.length > 0) {
updateFileOrderFromPages(editedDocument.pages);
}
}
}, [editedDocument?.pages, activeFileIds.length, state.files.ids, selectedFileIds, updateFileOrderFromPages]);
// Utility functions to convert between page IDs and page numbers
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
if (!displayDocument) return [];
@ -679,6 +729,15 @@ const PageEditor = ({
// Display all pages - use edited or original document
const displayedPages = displayDocument?.pages || [];
// Create a mapping of fileId to color index for page highlighting
const fileColorIndexMap = useMemo(() => {
const map = new Map<FileId, number>();
activeFileIds.forEach((fileId, index) => {
map.set(fileId, index);
});
return map;
}, [activeFileIds]);
return (
<Box pos="relative" h='100%' style={{ overflow: 'auto' }} data-scrolling-container="true">
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
@ -767,34 +826,38 @@ const PageEditor = ({
selectionMode={selectionMode}
isAnimating={isAnimating}
onReorderPages={handleReorderPages}
renderItem={(page, index, refs) => (
<PageThumbnail
key={page.id}
page={page}
index={index}
totalPages={displayDocument.pages.length}
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
selectedPageIds={selectedPageIds}
selectionMode={selectionMode}
movingPage={movingPage}
isAnimating={isAnimating}
pageRefs={refs}
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}
/>
)}
renderItem={(page, index, refs) => {
const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0;
return (
<PageThumbnail
key={page.id}
page={page}
index={index}
totalPages={displayDocument.pages.length}
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
fileColorIndex={fileColorIndex}
selectedPageIds={selectedPageIds}
selectionMode={selectionMode}
movingPage={movingPage}
isAnimating={isAnimating}
pageRefs={refs}
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}
/>
);
}}
/>
</Box>

View File

@ -11,6 +11,7 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
import { PDFPage, PDFDocument } from '../../types/pageEditor';
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { getFileColorWithOpacity } from './fileColors';
import styles from './PageEditor.module.css';
@ -19,6 +20,7 @@ interface PageThumbnailProps {
index: number;
totalPages: number;
originalFile?: File;
fileColorIndex: number;
selectedPageIds: string[];
selectionMode: boolean;
movingPage: number | null;
@ -45,6 +47,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
index,
totalPages,
originalFile,
fileColorIndex,
selectedPageIds,
selectionMode,
movingPage,
@ -272,6 +275,8 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
setMouseStartPos(null);
}, []);
const fileColorBorder = getFileColorWithOpacity(fileColorIndex, 0.5);
return (
<div
ref={pageElementRef}
@ -290,14 +295,12 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
hover:shadow-md
transition-all
relative
${selectionMode
? 'bg-white hover:bg-gray-50'
: 'bg-white hover:bg-gray-50'}
bg-white hover:bg-gray-50
${isDragging ? 'opacity-50 scale-95' : ''}
${movingPage === page.pageNumber ? 'page-moving' : ''}
`}
style={{
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
}}
draggable={false}
onMouseDown={handleMouseDown}
@ -346,7 +349,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
height: '100%',
backgroundColor: 'var(--mantine-color-gray-1)',
borderRadius: 6,
border: '1px solid var(--mantine-color-gray-3)',
border: `4px solid ${fileColorBorder}`,
padding: 4,
display: 'flex',
alignItems: 'center',

View File

@ -0,0 +1,61 @@
/**
* File color palette for page editor
* Each file gets a distinct color for visual organization
* Colors are applied at 0.3 opacity for subtle highlighting
* Maximum 20 files supported in page editor
*/
export const FILE_COLORS = [
// Subtle colors (1-6) - fit well with UI theme
'rgb(59, 130, 246)', // Blue
'rgb(16, 185, 129)', // Green
'rgb(139, 92, 246)', // Purple
'rgb(6, 182, 212)', // Cyan
'rgb(20, 184, 166)', // Teal
'rgb(99, 102, 241)', // Indigo
// Mid-range colors (7-12) - more distinct
'rgb(244, 114, 182)', // Pink
'rgb(251, 146, 60)', // Orange
'rgb(234, 179, 8)', // Yellow
'rgb(132, 204, 22)', // Lime
'rgb(248, 113, 113)', // Red
'rgb(168, 85, 247)', // Violet
// Vibrant colors (13-20) - maximum distinction
'rgb(236, 72, 153)', // Fuchsia
'rgb(245, 158, 11)', // Amber
'rgb(34, 197, 94)', // Emerald
'rgb(14, 165, 233)', // Sky
'rgb(239, 68, 68)', // Rose
'rgb(168, 162, 158)', // Stone
'rgb(251, 191, 36)', // Gold
'rgb(192, 132, 252)', // Light Purple
] as const;
export const MAX_PAGE_EDITOR_FILES = 20;
/**
* Get color for a file by its index
* @param index - Zero-based file index
* @returns RGB color string
*/
export function getFileColor(index: number): string {
if (index < 0 || index >= FILE_COLORS.length) {
console.warn(`File index ${index} out of range, using default color`);
return FILE_COLORS[0];
}
return FILE_COLORS[index];
}
/**
* Get color with specified opacity
* @param index - Zero-based file index
* @param opacity - Opacity value (0-1), defaults to 0.3
* @returns RGBA color string
*/
export function getFileColorWithOpacity(index: number, opacity: number = 0.2): string {
const rgb = getFileColor(index);
// Convert rgb(r, g, b) to rgba(r, g, b, a)
return rgb.replace('rgb(', 'rgba(').replace(')', `, ${opacity})`);
}

View File

@ -1,13 +1,215 @@
import React from 'react';
import React, { useRef, useCallback, useState } from 'react';
import { Menu, Loader, Group, Text, Checkbox, ActionIcon } from '@mantine/core';
import EditNoteIcon from '@mui/icons-material/EditNote';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import VerticalAlignTopIcon from '@mui/icons-material/VerticalAlignTop';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import FitText from './FitText';
import { getFileColorWithOpacity } from '../pageEditor/fileColors';
import { FileId } from '../../types/file';
interface FileMenuItemProps {
file: { fileId: FileId; name: string; versionNumber?: number };
index: number;
isSelected: boolean;
isFirst: boolean;
isLast: boolean;
onToggleSelection: (fileId: FileId) => void;
onMoveUp: (e: React.MouseEvent, index: number) => void;
onMoveDown: (e: React.MouseEvent, index: number) => void;
onMoveToTop: (e: React.MouseEvent, index: number) => void;
onMoveToBottom: (e: React.MouseEvent, index: number) => void;
onReorder: (fromIndex: number, toIndex: number) => void;
}
const FileMenuItem: React.FC<FileMenuItemProps> = ({
file,
index,
isSelected,
isFirst,
isLast,
onToggleSelection,
onMoveUp,
onMoveDown,
onMoveToTop,
onMoveToBottom,
onReorder,
}) => {
const [isDragging, setIsDragging] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const itemRef = useRef<HTMLDivElement>(null);
const itemElementRef = useCallback((element: HTMLDivElement | null) => {
if (element) {
itemRef.current = element;
const dragCleanup = draggable({
element,
getInitialData: () => ({
type: 'file-item',
fileId: file.fileId,
fromIndex: index,
}),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
canDrag: () => true,
});
const dropCleanup = dropTargetForElements({
element,
getData: () => ({
type: 'file-item',
fileId: file.fileId,
toIndex: index,
}),
onDragEnter: () => {
setIsDragOver(true);
},
onDragLeave: () => {
setIsDragOver(false);
},
onDrop: ({ source }) => {
setIsDragOver(false);
const sourceData = source.data;
if (sourceData.type === 'file-item') {
const fromIndex = sourceData.fromIndex as number;
if (fromIndex !== index) {
onReorder(fromIndex, index);
}
}
}
});
(element as any).__dragCleanup = () => {
dragCleanup();
dropCleanup();
};
} else {
if (itemRef.current && (itemRef.current as any).__dragCleanup) {
(itemRef.current as any).__dragCleanup();
}
}
}, [file.fileId, index, onReorder]);
const itemName = file?.name || 'Untitled';
const fileColorBorder = getFileColorWithOpacity(index, 1);
const fileColorBorderHover = getFileColorWithOpacity(index, 1.0);
return (
<div
ref={itemElementRef}
onClick={(e) => {
e.stopPropagation();
onToggleSelection(file.fileId);
}}
style={{
padding: '0.75rem 0.75rem',
marginBottom: '0.5rem',
cursor: isDragging ? 'grabbing' : 'grab',
backgroundColor: isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
borderLeft: `6px solid ${fileColorBorder}`,
borderTop: isDragOver ? '2px solid rgba(0, 0, 0, 0.5)' : 'none',
borderBottom: isDragOver ? '2px solid rgba(0, 0, 0, 0.5)' : 'none',
opacity: isDragging ? 0.5 : 1,
transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease, border-color 0.15s ease',
userSelect: 'none',
}}
onMouseEnter={(e) => {
if (!isDragging) {
(e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
(e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover;
}
}}
onMouseLeave={(e) => {
if (!isDragging) {
(e.currentTarget as HTMLDivElement).style.backgroundColor = isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent';
(e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder;
}
}}
>
<Group gap="xs" style={{ width: '100%', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1, minWidth: 0 }}>
<div
style={{
cursor: 'grab',
display: 'flex',
alignItems: 'center',
color: 'var(--mantine-color-dimmed)',
}}
>
<DragIndicatorIcon fontSize="small" />
</div>
<Checkbox
checked={isSelected}
onChange={() => onToggleSelection(file.fileId)}
onClick={(e) => e.stopPropagation()}
size="sm"
/>
<div style={{ flex: 1, textAlign: 'left', minWidth: 0 }}>
<FitText text={itemName} fontSize={14} minimumFontScale={0.7} />
</div>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
</div>
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }} onClick={(e) => e.stopPropagation()}>
<ActionIcon
component="div"
size="sm"
variant="subtle"
disabled={isFirst}
onClick={onMoveToTop}
title="Move to top"
>
<VerticalAlignTopIcon fontSize="small" />
</ActionIcon>
<ActionIcon
component="div"
size="sm"
variant="subtle"
disabled={isFirst}
onClick={onMoveUp}
title="Move up"
>
<ArrowUpwardIcon fontSize="small" />
</ActionIcon>
<ActionIcon
component="div"
size="sm"
variant="subtle"
disabled={isLast}
onClick={onMoveDown}
title="Move down"
>
<ArrowDownwardIcon fontSize="small" />
</ActionIcon>
<ActionIcon
component="div"
size="sm"
variant="subtle"
disabled={isLast}
onClick={onMoveToBottom}
title="Move to bottom"
>
<VerticalAlignBottomIcon fontSize="small" />
</ActionIcon>
</div>
</Group>
</div>
);
};
interface PageEditorFileDropdownProps {
displayName: string;
allFiles: Array<{ fileId: FileId; name: string; versionNumber?: number }>;
@ -27,11 +229,6 @@ export const PageEditorFileDropdown: React.FC<PageEditorFileDropdownProps> = ({
switchingTo,
viewOptionStyle,
}) => {
const handleCheckboxClick = (e: React.MouseEvent, fileId: FileId) => {
e.stopPropagation();
onToggleSelection(fileId);
};
const handleMoveUp = (e: React.MouseEvent, index: number) => {
e.stopPropagation();
if (index > 0) {
@ -46,8 +243,22 @@ export const PageEditorFileDropdown: React.FC<PageEditorFileDropdownProps> = ({
}
};
const handleMoveToTop = (e: React.MouseEvent, index: number) => {
e.stopPropagation();
if (index > 0) {
onReorder(index, 0);
}
};
const handleMoveToBottom = (e: React.MouseEvent, index: number) => {
e.stopPropagation();
if (index < allFiles.length - 1) {
onReorder(index, allFiles.length - 1);
}
};
return (
<Menu trigger="click" position="bottom" width="30rem">
<Menu trigger="click" position="bottom" width="40rem">
<Menu.Target>
<div style={{...viewOptionStyle, cursor: 'pointer'}}>
{switchingTo === "pageEditor" ? (
@ -64,64 +275,29 @@ export const PageEditorFileDropdown: React.FC<PageEditorFileDropdownProps> = ({
border: '1px solid var(--border-subtle)',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
maxHeight: '50vh',
maxHeight: '80vh',
overflowY: 'auto'
}}>
{allFiles.map((file, index) => {
const itemName = file?.name || 'Untitled';
const isSelected = selectedFileIds.has(file.fileId);
const isFirst = index === 0;
const isLast = index === allFiles.length - 1;
return (
<Menu.Item
<FileMenuItem
key={file.fileId}
onClick={(e) => e.stopPropagation()}
style={{
justifyContent: 'flex-start',
cursor: 'default',
backgroundColor: isSelected ? 'var(--bg-hover)' : undefined,
}}
>
<Group gap="xs" style={{ width: '100%', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1, minWidth: 0 }}>
<Checkbox
checked={isSelected}
onChange={() => {}}
onClick={(e) => handleCheckboxClick(e, file.fileId)}
size="sm"
/>
<div style={{ flex: 1, textAlign: 'left', minWidth: 0 }}>
<FitText text={itemName} fontSize={14} minimumFontScale={0.7} />
</div>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
</div>
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }}>
<ActionIcon
size="sm"
variant="subtle"
disabled={isFirst}
onClick={(e) => handleMoveUp(e, index)}
title="Move up"
>
<ArrowUpwardIcon fontSize="small" />
</ActionIcon>
<ActionIcon
size="sm"
variant="subtle"
disabled={isLast}
onClick={(e) => handleMoveDown(e, index)}
title="Move down"
>
<ArrowDownwardIcon fontSize="small" />
</ActionIcon>
</div>
</Group>
</Menu.Item>
file={file}
index={index}
isSelected={isSelected}
isFirst={isFirst}
isLast={isLast}
onToggleSelection={onToggleSelection}
onMoveUp={(e) => handleMoveUp(e, index)}
onMoveDown={(e) => handleMoveDown(e, index)}
onMoveToTop={(e) => handleMoveToTop(e, index)}
onMoveToBottom={(e) => handleMoveToBottom(e, index)}
onReorder={onReorder}
/>
);
})}
</Menu.Dropdown>

View File

@ -75,11 +75,7 @@ const createViewOptions = (
let pageEditorDisplayName = 'Page Editor';
if (isInPageEditor && pageEditorState) {
if (pageEditorState.selectedCount === pageEditorState.totalCount) {
pageEditorDisplayName = `${pageEditorState.selectedCount} file${pageEditorState.selectedCount !== 1 ? 's' : ''}`;
} else {
pageEditorDisplayName = `${pageEditorState.selectedCount}/${pageEditorState.totalCount} selected`;
}
pageEditorDisplayName = `${pageEditorState.selectedCount}/${pageEditorState.totalCount} selected`;
}
const pageEditorOption = {

View File

@ -1,11 +1,95 @@
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { FileId } from '../types/file';
import { useFileActions } from './FileContext';
import { PDFPage } from '../types/pageEditor';
import { MAX_PAGE_EDITOR_FILES } from '../components/pageEditor/fileColors';
/**
* Computes file order based on the position of each file's first page
* @param pages - Current page order
* @returns Array of FileIds in order based on first page positions
*/
function computeFileOrderFromPages(pages: PDFPage[]): FileId[] {
// Find the first page for each file
const fileFirstPagePositions = new Map<FileId, number>();
pages.forEach((page, index) => {
const fileId = page.originalFileId;
if (!fileId) return;
if (!fileFirstPagePositions.has(fileId)) {
fileFirstPagePositions.set(fileId, index);
}
});
// Sort files by their first page position
const fileOrder = Array.from(fileFirstPagePositions.entries())
.sort((a, b) => a[1] - b[1])
.map(entry => entry[0]);
return fileOrder;
}
/**
* Reorders pages based on file reordering while preserving manual page order within files
* @param currentPages - Current page order (may include manual reordering)
* @param fromIndex - Source file index
* @param toIndex - Target file index
* @param orderedFileIds - File IDs in their current order
* @returns Reordered pages with updated page numbers
*/
function reorderPagesForFileMove(
currentPages: PDFPage[],
fromIndex: number,
toIndex: number,
orderedFileIds: FileId[]
): PDFPage[] {
// Group pages by originalFileId, preserving their current relative positions
const fileGroups = new Map<FileId, PDFPage[]>();
currentPages.forEach(page => {
const fileId = page.originalFileId;
if (!fileId) return;
if (!fileGroups.has(fileId)) {
fileGroups.set(fileId, []);
}
fileGroups.get(fileId)!.push(page);
});
// Reorder the file IDs
const newFileOrder = [...orderedFileIds];
const [movedFileId] = newFileOrder.splice(fromIndex, 1);
newFileOrder.splice(toIndex, 0, movedFileId);
// Rebuild pages in new file order, preserving page order within each file
const reorderedPages: PDFPage[] = [];
newFileOrder.forEach(fileId => {
const filePages = fileGroups.get(fileId) || [];
reorderedPages.push(...filePages);
});
// Renumber all pages sequentially
reorderedPages.forEach((page, index) => {
page.pageNumber = index + 1;
});
return reorderedPages;
}
interface PageEditorContextValue {
// Set of selected file IDs (for quick lookup)
selectedFileIds: Set<FileId>;
// Current page order (updated by PageEditor, used for file reordering)
currentPages: PDFPage[] | null;
updateCurrentPages: (pages: PDFPage[] | null) => void;
// Reordered pages (when file reordering happens)
reorderedPages: PDFPage[] | null;
clearReorderedPages: () => void;
// Toggle file selection
toggleFileSelection: (fileId: FileId) => void;
@ -13,9 +97,12 @@ interface PageEditorContextValue {
selectAll: (fileIds: FileId[]) => void;
deselectAll: () => void;
// Reorder ALL files in FileContext (maintains selection state)
// Reorder ALL files in FileContext (maintains selection state and page order)
reorderFiles: (fromIndex: number, toIndex: number, allFileIds: FileId[]) => void;
// Update file order based on page positions (when pages are manually reordered)
updateFileOrderFromPages: (pages: PDFPage[]) => void;
// Sync with FileContext when files change
syncWithFileContext: (allFileIds: FileId[]) => void;
}
@ -30,14 +117,29 @@ interface PageEditorProviderProps {
export function PageEditorProvider({ children, initialFileIds = [] }: PageEditorProviderProps) {
// Use Set for O(1) selection lookup
const [selectedFileIds, setSelectedFileIds] = useState<Set<FileId>>(new Set(initialFileIds));
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
const { actions: fileActions } = useFileActions();
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
setCurrentPages(pages);
}, []);
const clearReorderedPages = useCallback(() => {
setReorderedPages(null);
}, []);
const toggleFileSelection = useCallback((fileId: FileId) => {
setSelectedFileIds(prev => {
const next = new Set(prev);
if (next.has(fileId)) {
next.delete(fileId);
} else {
// Check if adding this file would exceed the limit
if (next.size >= MAX_PAGE_EDITOR_FILES) {
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
return prev;
}
next.add(fileId);
}
return next;
@ -45,7 +147,14 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor
}, []);
const selectAll = useCallback((fileIds: FileId[]) => {
setSelectedFileIds(new Set(fileIds));
// Enforce maximum file limit
if (fileIds.length > MAX_PAGE_EDITOR_FILES) {
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`);
const limitedFiles = fileIds.slice(0, MAX_PAGE_EDITOR_FILES);
setSelectedFileIds(new Set(limitedFiles));
} else {
setSelectedFileIds(new Set(fileIds));
}
}, []);
const deselectAll = useCallback(() => {
@ -60,6 +169,24 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor
// Update global FileContext order
fileActions.reorderFiles(newOrder);
// If current pages available, reorder them based on file move
if (currentPages && currentPages.length > 0) {
const reordered = reorderPagesForFileMove(currentPages, fromIndex, toIndex, allFileIds);
setReorderedPages(reordered);
}
}, [fileActions, currentPages]);
const updateFileOrderFromPages = useCallback((pages: PDFPage[]) => {
if (!pages || pages.length === 0) return;
// Compute the new file order based on page positions
const newFileOrder = computeFileOrderFromPages(pages);
if (newFileOrder.length > 0) {
// Update global FileContext order
fileActions.reorderFiles(newFileOrder);
}
}, [fileActions]);
const syncWithFileContext = useCallback((allFileIds: FileId[]) => {
@ -72,9 +199,20 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor
}
});
// If no files selected, select all by default
// If no files selected, select all by default (up to MAX_PAGE_EDITOR_FILES)
if (next.size === 0 && allFileIds.length > 0) {
return new Set(allFileIds);
const filesToSelect = allFileIds.slice(0, MAX_PAGE_EDITOR_FILES);
if (allFileIds.length > MAX_PAGE_EDITOR_FILES) {
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`);
}
return new Set(filesToSelect);
}
// Enforce maximum file limit
if (next.size > MAX_PAGE_EDITOR_FILES) {
console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Limiting selection.`);
const limitedFiles = Array.from(next).slice(0, MAX_PAGE_EDITOR_FILES);
return new Set(limitedFiles);
}
// Only update if there's an actual change
@ -88,10 +226,15 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor
const value: PageEditorContextValue = {
selectedFileIds,
currentPages,
updateCurrentPages,
reorderedPages,
clearReorderedPages,
toggleFileSelection,
selectAll,
deselectAll,
reorderFiles,
updateFileOrderFromPages,
syncWithFileContext,
};