diff --git a/frontend/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx b/frontend/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
index 86b788183..e805811fa 100644
--- a/frontend/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
+++ b/frontend/src/core/components/tools/overlayPdfs/OverlayPdfsSettings.tsx
@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import { type OverlayPdfsParameters, type OverlayMode } from '@app/hooks/tools/overlayPdfs/useOverlayPdfsParameters';
import LocalIcon from '@app/components/shared/LocalIcon';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
+import { StirlingFileStub } from '@app/types/fileContext';
+import { fileStorage } from '@app/services/fileStorage';
import styles from '@app/components/tools/overlayPdfs/OverlayPdfsSettings.module.css';
interface OverlayPdfsSettingsProps {
@@ -36,8 +38,22 @@ export default function OverlayPdfsSettings({ parameters, onParameterChange, dis
const handleOpenOverlayFilesModal = () => {
if (disabled) return;
openFilesModal({
- customHandler: (files: File[]) => {
- handleOverlayFilesChange([...(parameters.overlayFiles || []), ...files]);
+ customHandler: async (files: File[] | StirlingFileStub[], _insertAfterPage?: number, isFromStorage?: boolean) => {
+ let resolvedFiles: File[] = [];
+
+ if (isFromStorage) {
+ // Load actual File objects from storage
+ for (const stub of files as StirlingFileStub[]) {
+ const stirlingFile = await fileStorage.getStirlingFile(stub.id);
+ if (stirlingFile) {
+ resolvedFiles.push(stirlingFile);
+ }
+ }
+ } else {
+ resolvedFiles = files as File[];
+ }
+
+ handleOverlayFilesChange([...(parameters.overlayFiles || []), ...resolvedFiles]);
}
});
};
@@ -175,4 +191,3 @@ export default function OverlayPdfsSettings({ parameters, onParameterChange, dis
);
}
-
diff --git a/frontend/src/core/components/tools/shared/FilesToolStep.tsx b/frontend/src/core/components/tools/shared/FilesToolStep.tsx
index b1e8933f5..8804b9fad 100644
--- a/frontend/src/core/components/tools/shared/FilesToolStep.tsx
+++ b/frontend/src/core/components/tools/shared/FilesToolStep.tsx
@@ -10,20 +10,28 @@ export interface FilesToolStepProps {
minFiles?: number;
}
+export function CreateFilesToolStep(props: FilesToolStepProps & {
+ createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement
+}): React.ReactElement {
+ const { t } = useTranslation();
+ const { createStep, ...stepProps } = props;
+
+ return createStep(t("files.title", "Files"), {
+ isVisible: true,
+ isCollapsed: stepProps.isCollapsed,
+ onCollapsedClick: stepProps.onCollapsedClick
+ }, (
+
+ ));
+}
+
+// Backwards compatibility wrapper
export function createFilesToolStep(
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
props: FilesToolStepProps
): React.ReactElement {
- const { t } = useTranslation();
-
- return createStep(t("files.title", "Files"), {
- isVisible: true,
- isCollapsed: props.isCollapsed,
- onCollapsedClick: props.onCollapsedClick
- }, (
-
- ));
+ return
;
}
diff --git a/frontend/src/core/components/tools/shared/ReviewToolStep.tsx b/frontend/src/core/components/tools/shared/ReviewToolStep.tsx
index 30c8c906e..79e9989fd 100644
--- a/frontend/src/core/components/tools/shared/ReviewToolStep.tsx
+++ b/frontend/src/core/components/tools/shared/ReviewToolStep.tsx
@@ -103,21 +103,29 @@ function ReviewStepContent
({
);
}
-export function createReviewToolStep(
- createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
- props: ReviewToolStepProps
-): React.ReactElement {
+export function CreateReviewToolStep(props: ReviewToolStepProps & {
+ createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement
+}): React.ReactElement {
const { t } = useTranslation();
+ const { createStep, ...stepProps } = props;
return createStep(
t("review", "Review"),
{
- isVisible: props.isVisible,
- isCollapsed: props.isCollapsed,
- onCollapsedClick: props.onCollapsedClick,
+ isVisible: stepProps.isVisible,
+ isCollapsed: stepProps.isCollapsed,
+ onCollapsedClick: stepProps.onCollapsedClick,
_excludeFromCount: true,
_noPadding: true,
},
-
+
);
}
+
+// Backwards compatibility wrapper
+export function createReviewToolStep(
+ createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
+ props: ReviewToolStepProps
+): React.ReactElement {
+ return ;
+}
diff --git a/frontend/src/core/components/tools/shared/ToolStep.tsx b/frontend/src/core/components/tools/shared/ToolStep.tsx
index 89c39aad8..bff8b0357 100644
--- a/frontend/src/core/components/tools/shared/ToolStep.tsx
+++ b/frontend/src/core/components/tools/shared/ToolStep.tsx
@@ -80,8 +80,6 @@ const ToolStep = ({
alwaysShowTooltip = false,
tooltip
}: ToolStepProps) => {
- if (!isVisible) return null;
-
const parent = useContext(ToolStepContext);
// Auto-detect if we should show numbers based on sibling count or force option
@@ -93,6 +91,8 @@ const ToolStep = ({
const stepNumber = _stepNumber;
+ if (!isVisible) return null;
+
return (
{
+/**
+ * Hook that returns tooltip content for ALL split methods
+ * Can be called once and then looked up by method
+ */
+export const useSplitSettingsTips = (): Record
=> {
const { t } = useTranslation();
- if (!method) return null;
-
const tooltipMap: Record = {
[SPLIT_METHODS.BY_PAGES]: {
header: {
@@ -130,5 +132,5 @@ export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent |
}
};
- return tooltipMap[method];
+ return tooltipMap;
};
\ No newline at end of file
diff --git a/frontend/src/core/contexts/FilesModalContext.tsx b/frontend/src/core/contexts/FilesModalContext.tsx
index 468609f7d..1096eee11 100644
--- a/frontend/src/core/contexts/FilesModalContext.tsx
+++ b/frontend/src/core/contexts/FilesModalContext.tsx
@@ -2,11 +2,10 @@ import React, { createContext, useContext, useState, useCallback, useMemo } from
import { useFileHandler } from '@app/hooks/useFileHandler';
import { useFileActions } from '@app/contexts/FileContext';
import { StirlingFileStub } from '@app/types/fileContext';
-import { fileStorage } from '@app/services/fileStorage';
interface FilesModalContextType {
isFilesModalOpen: boolean;
- openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => void;
+ openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void }) => void;
closeFilesModal: () => void;
onFileUpload: (files: File[]) => void;
onRecentFileSelect: (stirlingFileStubs: StirlingFileStub[]) => void;
@@ -22,9 +21,9 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
const [insertAfterPage, setInsertAfterPage] = useState();
- const [customHandler, setCustomHandler] = useState<((files: File[], insertAfterPage?: number) => void) | undefined>();
+ const [customHandler, setCustomHandler] = useState<((files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void) | undefined>();
- const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => {
+ const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[] | StirlingFileStub[], insertAfterPage?: number, isFromStorage?: boolean) => void }) => {
setInsertAfterPage(options?.insertAfterPage);
setCustomHandler(() => options?.customHandler);
setIsFilesModalOpen(true);
@@ -50,22 +49,8 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const handleRecentFileSelect = useCallback(async (stirlingFileStubs: StirlingFileStub[]) => {
if (customHandler) {
- // Load the actual files from storage for custom handler
- try {
- const loadedFiles: File[] = [];
- for (const stub of stirlingFileStubs) {
- const stirlingFile = await fileStorage.getStirlingFile(stub.id);
- if (stirlingFile) {
- loadedFiles.push(stirlingFile);
- }
- }
-
- if (loadedFiles.length > 0) {
- customHandler(loadedFiles, insertAfterPage);
- }
- } catch (error) {
- console.error('Failed to load files for custom handler:', error);
- }
+ // Pass stubs directly to custom handler with flag indicating they're from storage
+ customHandler(stirlingFileStubs, insertAfterPage, true);
} else {
// Normal case - use addStirlingFileStubs to preserve metadata
if (actions.addStirlingFileStubs) {
diff --git a/frontend/src/core/contexts/PageEditorContext.tsx b/frontend/src/core/contexts/PageEditorContext.tsx
new file mode 100644
index 000000000..2cd95adfa
--- /dev/null
+++ b/frontend/src/core/contexts/PageEditorContext.tsx
@@ -0,0 +1,359 @@
+import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo } from 'react';
+import { FileId } from '@app/types/file';
+import { useFileActions, useFileState } from '@app/contexts/FileContext';
+import { PDFPage } from '@app/types/pageEditor';
+import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors';
+
+// PageEditorFile is now defined locally in consuming components
+// Components should derive file list directly from FileContext
+
+/**
+ * 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();
+
+ 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 interlacing and manual page order
+ * @param currentPages - Current page order (may include manual reordering and interlacing)
+ * @param fromIndex - Source file index in the file order
+ * @param toIndex - Target file index in the file order
+ * @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[] {
+ // Get the file ID being moved
+ const movedFileId = orderedFileIds[fromIndex];
+ const targetFileId = orderedFileIds[toIndex];
+
+ // Extract pages belonging to the moved file (maintaining their relative order)
+ const movedFilePages: PDFPage[] = [];
+ const remainingPages: PDFPage[] = [];
+
+ currentPages.forEach(page => {
+ if (page.originalFileId === movedFileId) {
+ movedFilePages.push(page);
+ } else {
+ remainingPages.push(page);
+ }
+ });
+
+ // Find the insertion point based on the target file
+ let insertionIndex = 0;
+
+ if (fromIndex < toIndex) {
+ // Moving down: insert AFTER the last page of ANY file that should come before us
+ // We need to find the last page belonging to any file at index <= toIndex in orderedFileIds
+ const filesBeforeUs = new Set(orderedFileIds.slice(0, toIndex + 1));
+ for (let i = remainingPages.length - 1; i >= 0; i--) {
+ const pageFileId = remainingPages[i].originalFileId;
+ if (pageFileId && filesBeforeUs.has(pageFileId)) {
+ insertionIndex = i + 1;
+ break;
+ }
+ }
+ } else {
+ // Moving up: insert BEFORE the first page of target file
+ for (let i = 0; i < remainingPages.length; i++) {
+ if (remainingPages[i].originalFileId === targetFileId) {
+ insertionIndex = i;
+ break;
+ }
+ }
+ }
+
+ // Insert moved pages at the calculated position
+ const reorderedPages = [
+ ...remainingPages.slice(0, insertionIndex),
+ ...movedFilePages,
+ ...remainingPages.slice(insertionIndex)
+ ];
+
+ // Renumber all pages sequentially (clone to avoid mutation)
+ return reorderedPages.map((page, index) => ({
+ ...page,
+ pageNumber: index + 1
+ }));
+}
+
+interface PageEditorContextValue {
+ // 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;
+
+ // Page editor's own file order (independent of FileContext global order)
+ fileOrder: FileId[];
+ setFileOrder: (order: FileId[]) => void;
+
+ // Set file selection (calls FileContext actions)
+ setFileSelection: (fileId: FileId, selected: boolean) => void;
+
+ // Toggle file selection (calls FileContext actions)
+ toggleFileSelection: (fileId: FileId) => void;
+
+ // Select/deselect all files (calls FileContext actions)
+ selectAll: () => void;
+ deselectAll: () => void;
+
+ // Reorder files (only affects page editor's local order)
+ reorderFiles: (fromIndex: number, toIndex: number) => void;
+
+ // Update file order based on page positions (when pages are manually reordered)
+ updateFileOrderFromPages: (pages: PDFPage[]) => void;
+}
+
+const PageEditorContext = createContext(undefined);
+
+interface PageEditorProviderProps {
+ children: ReactNode;
+}
+
+export function PageEditorProvider({ children }: PageEditorProviderProps) {
+ const [currentPages, setCurrentPages] = useState(null);
+ const [reorderedPages, setReorderedPages] = useState(null);
+
+ // Page editor's own file order (independent of FileContext)
+ const [fileOrder, setFileOrder] = useState([]);
+
+ // Read from FileContext (for file metadata only, not order)
+ const { actions: fileActions } = useFileActions();
+ const { state } = useFileState();
+
+ // Keep a ref to always read latest state in stable callbacks
+ const stateRef = React.useRef(state);
+ React.useEffect(() => {
+ stateRef.current = state;
+ }, [state]);
+
+ // Track the previous FileContext order to detect actual changes
+ const prevFileContextIdsRef = React.useRef([]);
+
+ // Initialize fileOrder from FileContext when files change (add/remove only)
+ React.useEffect(() => {
+ const currentFileIds = state.files.ids;
+ const prevFileIds = prevFileContextIdsRef.current;
+
+ // Only react to FileContext changes, not our own fileOrder changes
+ const fileContextChanged =
+ currentFileIds.length !== prevFileIds.length ||
+ !currentFileIds.every((id, idx) => id === prevFileIds[idx]);
+
+ if (!fileContextChanged) {
+ return;
+ }
+
+ prevFileContextIdsRef.current = currentFileIds;
+
+ // Collect new file IDs outside the setState callback so we can clear them after
+ let newFileIdsToProcess: FileId[] = [];
+
+ // Use functional setState to read latest fileOrder without depending on it
+ setFileOrder(currentOrder => {
+ // Identify new files
+ const newFileIds = currentFileIds.filter(id => !currentOrder.includes(id));
+ newFileIdsToProcess = newFileIds; // Store for cleanup
+
+ // Remove deleted files
+ const validFileOrder = currentOrder.filter(id => currentFileIds.includes(id));
+
+ if (newFileIds.length === 0 && validFileOrder.length === currentOrder.length) {
+ return currentOrder; // No changes needed
+ }
+
+ // Always append new files to end
+ // If files have insertAfterPageId, page-level insertion is handled by usePageDocument
+ return [...validFileOrder, ...newFileIds];
+ });
+
+ // Clear insertAfterPageId after a delay to allow usePageDocument to consume it first
+ setTimeout(() => {
+ newFileIdsToProcess.forEach(fileId => {
+ const stub = state.files.byId[fileId];
+ if (stub?.insertAfterPageId) {
+ fileActions.updateStirlingFileStub(fileId, { insertAfterPageId: undefined });
+ }
+ });
+ }, 100);
+ }, [state.files.ids, state.files.byId, fileActions]);
+
+ const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
+ setCurrentPages(pages);
+ }, []);
+
+ const clearReorderedPages = useCallback(() => {
+ setReorderedPages(null);
+ }, []);
+
+ const setFileSelection = useCallback((fileId: FileId, selected: boolean) => {
+ const currentSelection = stateRef.current.ui.selectedFileIds;
+ const isAlreadySelected = currentSelection.includes(fileId);
+
+ // Check if we're trying to select when at limit
+ if (selected && !isAlreadySelected && currentSelection.length >= MAX_PAGE_EDITOR_FILES) {
+ console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
+ return;
+ }
+
+ // Update FileContext selection
+ const newSelectedIds = selected
+ ? [...currentSelection, fileId]
+ : currentSelection.filter(id => id !== fileId);
+
+ fileActions.setSelectedFiles(newSelectedIds);
+ }, [fileActions]);
+
+ const toggleFileSelection = useCallback((fileId: FileId) => {
+ const currentSelection = stateRef.current.ui.selectedFileIds;
+ const isCurrentlySelected = currentSelection.includes(fileId);
+
+ // If toggling on and at limit, don't allow
+ if (!isCurrentlySelected && currentSelection.length >= MAX_PAGE_EDITOR_FILES) {
+ console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`);
+ return;
+ }
+
+ // Update FileContext selection
+ const newSelectedIds = isCurrentlySelected
+ ? currentSelection.filter(id => id !== fileId)
+ : [...currentSelection, fileId];
+
+ fileActions.setSelectedFiles(newSelectedIds);
+ }, [fileActions]);
+
+ const selectAll = useCallback(() => {
+ const allFileIds = stateRef.current.files.ids;
+
+ 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.`);
+ fileActions.setSelectedFiles(allFileIds.slice(0, MAX_PAGE_EDITOR_FILES));
+ } else {
+ fileActions.setSelectedFiles(allFileIds);
+ }
+ }, [fileActions]);
+
+ const deselectAll = useCallback(() => {
+ fileActions.setSelectedFiles([]);
+ }, [fileActions]);
+
+ const reorderFiles = useCallback((fromIndex: number, toIndex: number) => {
+ // Reorder local fileOrder array (page editor workspace only)
+ const newOrder = [...fileOrder];
+ const [movedFileId] = newOrder.splice(fromIndex, 1);
+ newOrder.splice(toIndex, 0, movedFileId);
+ setFileOrder(newOrder);
+
+ // If current pages available, reorder them based on file move
+ if (currentPages && currentPages.length > 0 && fromIndex !== toIndex) {
+ // Get the current file order from pages (files that have pages loaded)
+ const currentFileOrder: FileId[] = [];
+ const filesSeen = new Set();
+ currentPages.forEach(page => {
+ const fileId = page.originalFileId;
+ if (fileId && !filesSeen.has(fileId)) {
+ filesSeen.add(fileId);
+ currentFileOrder.push(fileId);
+ }
+ });
+
+ // Get the target file ID from the NEW order (after the move)
+ // When moving down: we want to position after the file at toIndex-1 (file just before insertion)
+ // When moving up: we want to position before the file at toIndex+1 (file just after insertion)
+ const targetFileId = fromIndex < toIndex
+ ? newOrder[toIndex - 1] // Moving down: target is the file just before where we inserted
+ : newOrder[toIndex + 1]; // Moving up: target is the file just after where we inserted
+
+ // Find their positions in the current page order (not the full file list)
+ const pageOrderFromIndex = currentFileOrder.findIndex(id => id === movedFileId);
+ const pageOrderToIndex = currentFileOrder.findIndex(id => id === targetFileId);
+
+ // Only reorder pages if both files have pages loaded
+ if (pageOrderFromIndex >= 0 && pageOrderToIndex >= 0) {
+ const reorderedPagesResult = reorderPagesForFileMove(currentPages, pageOrderFromIndex, pageOrderToIndex, currentFileOrder);
+ setReorderedPages(reorderedPagesResult);
+ }
+ }
+ }, [fileOrder, 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 local page editor file order (not FileContext)
+ setFileOrder(newFileOrder);
+ }
+ }, []);
+
+
+ const value: PageEditorContextValue = useMemo(() => ({
+ currentPages,
+ updateCurrentPages,
+ reorderedPages,
+ clearReorderedPages,
+ fileOrder,
+ setFileOrder,
+ setFileSelection,
+ toggleFileSelection,
+ selectAll,
+ deselectAll,
+ reorderFiles,
+ updateFileOrderFromPages,
+ }), [
+ currentPages,
+ updateCurrentPages,
+ reorderedPages,
+ clearReorderedPages,
+ fileOrder,
+ setFileSelection,
+ toggleFileSelection,
+ selectAll,
+ deselectAll,
+ reorderFiles,
+ updateFileOrderFromPages,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePageEditor() {
+ const context = useContext(PageEditorContext);
+ if (!context) {
+ throw new Error('usePageEditor must be used within PageEditorProvider');
+ }
+ return context;
+}
diff --git a/frontend/src/core/contexts/PreferencesContext.tsx b/frontend/src/core/contexts/PreferencesContext.tsx
index b28d1b35b..482e0fed0 100644
--- a/frontend/src/core/contexts/PreferencesContext.tsx
+++ b/frontend/src/core/contexts/PreferencesContext.tsx
@@ -34,14 +34,14 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setPreferences(preferencesService.getAllPreferences());
}, []);
+ const value = React.useMemo(() => ({
+ preferences,
+ updatePreference,
+ resetPreferences,
+ }), [preferences, updatePreference, resetPreferences]);
+
return (
-
+
{children}
);
diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx
index 8e0bea44a..8afc1a9ea 100644
--- a/frontend/src/core/contexts/ViewerContext.tsx
+++ b/frontend/src/core/contexts/ViewerContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
+import React, { createContext, useContext, useState, ReactNode, useRef, useMemo, useCallback } from 'react';
import { SpreadMode } from '@embedpdf/plugin-spread/react';
import { useNavigation } from '@app/contexts/NavigationContext';
@@ -280,21 +280,21 @@ export const ViewerProvider: React.FC = ({ children }) => {
}
};
- const toggleThumbnailSidebar = () => {
+ const toggleThumbnailSidebar = useCallback(() => {
setIsThumbnailSidebarVisible(prev => !prev);
- };
+ }, []);
- const toggleAnnotationsVisibility = () => {
+ const toggleAnnotationsVisibility = useCallback(() => {
setIsAnnotationsVisible(prev => !prev);
- };
+ }, []);
- const setAnnotationMode = (enabled: boolean) => {
+ const setAnnotationMode = useCallback((enabled: boolean) => {
setIsAnnotationModeState(enabled);
- };
+ }, []);
- const toggleAnnotationMode = () => {
+ const toggleAnnotationMode = useCallback(() => {
setIsAnnotationModeState(prev => !prev);
- };
+ }, []);
// State getters - read from bridge refs
const getScrollState = (): ScrollState => {
@@ -334,7 +334,7 @@ export const ViewerProvider: React.FC = ({ children }) => {
};
// Action handlers - call APIs directly
- const scrollActions = {
+ const scrollActions = useMemo(() => ({
scrollToPage: (page: number) => {
const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToPage) {
@@ -366,9 +366,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
api.scrollToPage({ pageNumber: scrollState.totalPages });
}
}
- };
+ }), []);
- const zoomActions = {
+ const zoomActions = useMemo(() => ({
zoomIn: () => {
const api = bridgeRefs.current.zoom?.api;
if (api?.zoomIn) {
@@ -405,9 +405,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
api.requestZoom(level);
}
}
- };
+ }), []);
- const panActions = {
+ const panActions = useMemo(() => ({
enablePan: () => {
const api = bridgeRefs.current.pan?.api;
if (api?.enable) {
@@ -426,9 +426,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
api.toggle();
}
}
- };
+ }), []);
- const selectionActions = {
+ const selectionActions = useMemo(() => ({
copyToClipboard: () => {
const api = bridgeRefs.current.selection?.api;
if (api?.copyToClipboard) {
@@ -449,9 +449,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
}
return null;
}
- };
+ }), []);
- const spreadActions = {
+ const spreadActions = useMemo(() => ({
setSpreadMode: (mode: SpreadMode) => {
const api = bridgeRefs.current.spread?.api;
if (api?.setSpreadMode) {
@@ -471,9 +471,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
api.toggleSpreadMode();
}
}
- };
+ }), []);
- const rotationActions = {
+ const rotationActions = useMemo(() => ({
rotateForward: () => {
const api = bridgeRefs.current.rotation?.api;
if (api?.rotateForward) {
@@ -499,9 +499,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
}
return 0;
}
- };
+ }), []);
- const searchActions = {
+ const searchActions = useMemo(() => ({
search: async (query: string) => {
const api = bridgeRefs.current.search?.api;
if (api?.search) {
@@ -526,9 +526,9 @@ export const ViewerProvider: React.FC = ({ children }) => {
api.clear();
}
}
- };
+ }), []);
- const exportActions = {
+ const exportActions = useMemo(() => ({
download: () => {
const api = bridgeRefs.current.export?.api;
if (api?.download) {
@@ -548,29 +548,29 @@ export const ViewerProvider: React.FC = ({ children }) => {
}
return null;
}
- };
+ }), []);
- const registerImmediateZoomUpdate = (callback: (percent: number) => void) => {
+ const registerImmediateZoomUpdate = useCallback((callback: (percent: number) => void) => {
immediateZoomUpdateCallback.current = callback;
- };
+ }, []);
- const registerImmediateScrollUpdate = (callback: (currentPage: number, totalPages: number) => void) => {
+ const registerImmediateScrollUpdate = useCallback((callback: (currentPage: number, totalPages: number) => void) => {
immediateScrollUpdateCallback.current = callback;
- };
+ }, []);
- const triggerImmediateScrollUpdate = (currentPage: number, totalPages: number) => {
+ const triggerImmediateScrollUpdate = useCallback((currentPage: number, totalPages: number) => {
if (immediateScrollUpdateCallback.current) {
immediateScrollUpdateCallback.current(currentPage, totalPages);
}
- };
+ }, []);
- const triggerImmediateZoomUpdate = (zoomPercent: number) => {
+ const triggerImmediateZoomUpdate = useCallback((zoomPercent: number) => {
if (immediateZoomUpdateCallback.current) {
immediateZoomUpdateCallback.current(zoomPercent);
}
- };
+ }, []);
- const value: ViewerContextType = {
+ const value = useMemo(() => ({
// UI state
isThumbnailSidebarVisible,
toggleThumbnailSidebar,
@@ -615,7 +615,20 @@ export const ViewerProvider: React.FC = ({ children }) => {
// Bridge registration
registerBridge,
- };
+ }), [
+ isThumbnailSidebarVisible,
+ isAnnotationsVisible,
+ isAnnotationMode,
+ activeFileIndex,
+ scrollActions,
+ zoomActions,
+ panActions,
+ selectionActions,
+ spreadActions,
+ rotationActions,
+ searchActions,
+ exportActions,
+ ]);
return (
diff --git a/frontend/src/core/contexts/file/FileReducer.ts b/frontend/src/core/contexts/file/FileReducer.ts
index 2baa22a86..7cb46c05c 100644
--- a/frontend/src/core/contexts/file/FileReducer.ts
+++ b/frontend/src/core/contexts/file/FileReducer.ts
@@ -75,20 +75,43 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
const { stirlingFileStubs } = action.payload;
const newIds: FileId[] = [];
const newById: Record = { ...state.files.byId };
+ let hasInsertionPosition = false;
stirlingFileStubs.forEach(record => {
// Only add if not already present (dedupe by stable ID)
if (!newById[record.id]) {
newIds.push(record.id);
+
+ // Track if any file has an insertion position
+ if (record.insertAfterPageId) {
+ hasInsertionPosition = true;
+ }
+
+ // Store record WITH insertAfterPageId temporarily
+ // PageEditorContext will read it and clear it
newById[record.id] = record;
}
});
+ // Determine final file order
+ // NOTE: If files have insertAfterPageId, we just append to end
+ // The page-level insertion is handled by usePageDocument
+ const 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
}
};
}
@@ -149,17 +172,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
// Validate that all IDs exist in current state
const validIds = orderedFileIds.filter(id => state.files.byId[id]);
- // Reorder selected files by passed order
- const selectedFileIds = orderedFileIds.filter(id => state.ui.selectedFileIds.includes(id));
+
+ // Don't touch selectedFileIds - it's just a reference list, order doesn't matter
return {
...state,
files: {
...state.files,
ids: validIds
- },
- ui: {
- ...state.ui,
- selectedFileIds,
}
};
}
diff --git a/frontend/src/core/hooks/tools/shared/useBaseParameters.ts b/frontend/src/core/hooks/tools/shared/useBaseParameters.ts
index 941f8cdd2..7dd021752 100644
--- a/frontend/src/core/hooks/tools/shared/useBaseParameters.ts
+++ b/frontend/src/core/hooks/tools/shared/useBaseParameters.ts
@@ -34,16 +34,17 @@ export function useBaseParameters(config: BaseParametersConfig): BaseParam
}, [parameters, config.validateFn]);
const endpointName = config.endpointName;
- let getEndpointName: () => string;
- if (typeof endpointName === "string") {
- getEndpointName = useCallback(() => {
- return endpointName;
- }, []);
- } else {
- getEndpointName = useCallback(() => {
- return endpointName(parameters);
- }, [parameters]);
- }
+ const isStringEndpoint = typeof endpointName === "string";
+
+ const getEndpointNameString = useCallback(() => {
+ return endpointName as string;
+ }, [endpointName]);
+
+ const getEndpointNameFunction = useCallback(() => {
+ return (endpointName as (params: T) => string)(parameters);
+ }, [endpointName, parameters]);
+
+ const getEndpointName = isStringEndpoint ? getEndpointNameString : getEndpointNameFunction;
return {
parameters,
diff --git a/frontend/src/core/tools/Split.tsx b/frontend/src/core/tools/Split.tsx
index 769a25a08..d90afb6f8 100644
--- a/frontend/src/core/tools/Split.tsx
+++ b/frontend/src/core/tools/Split.tsx
@@ -21,11 +21,16 @@ const Split = (props: BaseToolProps) => {
);
const methodTips = useSplitMethodTips();
- const settingsTips = useSplitSettingsTips(base.params.parameters.method);
+ const allSettingsTips = useSplitSettingsTips();
+
+ // Get tooltip content for the currently selected method
+ const settingsTips = base.params.parameters.method
+ ? allSettingsTips[base.params.parameters.method]
+ : null;
// Get tooltip content for a specific method
const getMethodTooltip = (option: MethodOption) => {
- const tooltipContent = useSplitSettingsTips(option.value);
+ const tooltipContent = allSettingsTips[option.value];
return tooltipContent?.tips || [];
};
@@ -50,8 +55,7 @@ const Split = (props: BaseToolProps) => {
{
title: t("split.steps.chooseMethod", "Choose Method"),
isCollapsed: !!base.params.parameters.method, // Collapse when method is selected
- onCollapsedClick: () => base.params.updateParameter('method', '')
- ,
+ onCollapsedClick: () => base.params.updateParameter('method', ''),
tooltip: methodTips,
content: (
@@ -86,7 +90,7 @@ const Split = (props: BaseToolProps) => {
review: {
isVisible: base.hasResults,
operation: base.operation,
- title: "Split Results",
+ title: t("split.resultsTitle", "Split Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
diff --git a/frontend/src/core/types/pageEditor.ts b/frontend/src/core/types/pageEditor.ts
index 5c46ed063..60c1e1988 100644
--- a/frontend/src/core/types/pageEditor.ts
+++ b/frontend/src/core/types/pageEditor.ts
@@ -1,4 +1,9 @@
+<<<<<<< HEAD:frontend/src/core/types/pageEditor.ts
import { FileId } from '@app/types/file';
+=======
+import { FileId } from './file';
+import { PageBreakSettings } from '../components/pageEditor/commands/pageCommands';
+>>>>>>> feature/v2/selected-pageeditor:frontend/src/types/pageEditor.ts
export interface PDFPage {
id: string;
@@ -9,7 +14,9 @@ export interface PDFPage {
selected: boolean;
splitAfter?: boolean;
isBlankPage?: boolean;
+ isPlaceholder?: boolean;
originalFileId?: FileId;
+ pageBreakSettings?: PageBreakSettings;
}
export interface PDFDocument {