mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
feat: Introduce new file context management with hooks and lifecycle management
- Added new contexts for file state and actions to improve performance. - Implemented unified file actions with a single `addFiles` helper for various file types. - Created performant hooks for accessing file state and actions, including selection and management. - Developed file selectors for efficient state access. - Introduced a lifecycle manager for resource cleanup and memory management. - Updated HomePage and tool components to utilize new navigation actions. - Refactored file context types to streamline state management and remove legacy compatibility.
This commit is contained in:
parent
1730402eff
commit
9b14609236
@ -10,7 +10,8 @@
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(node:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
||||
import { FileContextProvider } from './contexts/FileContext';
|
||||
import { NavigationProvider } from './contexts/NavigationContext';
|
||||
import { FilesModalProvider } from './contexts/FilesModalContext';
|
||||
import HomePage from './pages/HomePage';
|
||||
|
||||
@ -12,9 +13,11 @@ export default function App() {
|
||||
return (
|
||||
<RainbowThemeProvider>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<FilesModalProvider>
|
||||
<HomePage />
|
||||
</FilesModalProvider>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<HomePage />
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</FileContextProvider>
|
||||
</RainbowThemeProvider>
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||
import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext';
|
||||
import { useFileHandler } from '../../hooks/useFileHandler';
|
||||
import { useFileState, useFileActions } from '../../contexts/FileContext';
|
||||
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
||||
|
||||
import TopControls from '../shared/TopControls';
|
||||
import FileEditor from '../fileEditor/FileEditor';
|
||||
@ -22,9 +23,10 @@ export default function Workbench() {
|
||||
// Use context-based hooks to eliminate all prop drilling
|
||||
const { state } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
const { currentMode: currentView } = useNavigationState();
|
||||
const { actions: navActions } = useNavigationActions();
|
||||
const setCurrentView = navActions.setMode;
|
||||
const activeFiles = state.files.ids;
|
||||
const currentView = state.ui.currentMode;
|
||||
const setCurrentView = actions.setCurrentMode;
|
||||
const {
|
||||
previewFile,
|
||||
pageEditorFunctions,
|
||||
@ -51,7 +53,7 @@ export default function Workbench() {
|
||||
handleToolSelect('convert');
|
||||
sessionStorage.removeItem('previousMode');
|
||||
} else {
|
||||
actions.setMode('fileEditor');
|
||||
setCurrentView('fileEditor');
|
||||
}
|
||||
};
|
||||
|
||||
@ -73,11 +75,11 @@ export default function Workbench() {
|
||||
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
||||
{...(!selectedToolKey && {
|
||||
onOpenPageEditor: (file) => {
|
||||
actions.setMode("pageEditor");
|
||||
setCurrentView("pageEditor");
|
||||
},
|
||||
onMergeFiles: (filesToMerge) => {
|
||||
filesToMerge.forEach(addToActiveFiles);
|
||||
actions.setMode("viewer");
|
||||
setCurrentView("viewer");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
@ -65,7 +65,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
}, [items.length, isLargeDocument, BUFFER_SIZE]);
|
||||
|
||||
// Throttled scroll handler to prevent excessive re-renders
|
||||
const throttleRef = useRef<number>();
|
||||
const throttleRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Detect scroll position from parent container
|
||||
useEffect(() => {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
|
||||
import { ModeType } from "../../types/fileContext";
|
||||
import { ModeType } from "../../contexts/NavigationContext";
|
||||
import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||
import { useFileState, useFileActions } from '../../contexts/FileContext';
|
||||
import { useNavigationGuard } from '../../contexts/NavigationContext';
|
||||
|
||||
interface NavigationWarningModalProps {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
@ -11,34 +11,37 @@ const NavigationWarningModal = ({
|
||||
onApplyAndContinue,
|
||||
onExportAndContinue
|
||||
}: NavigationWarningModalProps) => {
|
||||
const { state } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
const showNavigationWarning = state.ui.showNavigationWarning;
|
||||
const hasUnsavedChanges = state.ui.hasUnsavedChanges;
|
||||
const {
|
||||
showNavigationWarning,
|
||||
hasUnsavedChanges,
|
||||
cancelNavigation,
|
||||
confirmNavigation,
|
||||
setHasUnsavedChanges
|
||||
} = useNavigationGuard();
|
||||
|
||||
const handleKeepWorking = () => {
|
||||
actions.cancelNavigation();
|
||||
cancelNavigation();
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
actions.setHasUnsavedChanges(false);
|
||||
actions.confirmNavigation();
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleApplyAndContinue = async () => {
|
||||
if (onApplyAndContinue) {
|
||||
await onApplyAndContinue();
|
||||
}
|
||||
actions.setHasUnsavedChanges(false);
|
||||
actions.confirmNavigation();
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleExportAndContinue = async () => {
|
||||
if (onExportAndContinue) {
|
||||
await onExportAndContinue();
|
||||
}
|
||||
actions.setHasUnsavedChanges(false);
|
||||
actions.confirmNavigation();
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
if (!hasUnsavedChanges) {
|
||||
|
@ -10,7 +10,7 @@ import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import { Group } from "@mantine/core";
|
||||
import { ModeType } from '../../types/fileContext';
|
||||
import { ModeType } from '../../contexts/NavigationContext';
|
||||
|
||||
// Stable view option objects that don't recreate on every render
|
||||
const VIEW_OPTIONS_BASE = [
|
||||
|
File diff suppressed because it is too large
Load Diff
218
frontend/src/contexts/NavigationContext.tsx
Normal file
218
frontend/src/contexts/NavigationContext.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* NavigationContext - Complete navigation management system
|
||||
*
|
||||
* Handles navigation modes, navigation guards for unsaved changes,
|
||||
* and breadcrumb/history navigation. Separated from FileContext to
|
||||
* maintain clear separation of concerns.
|
||||
*/
|
||||
|
||||
// Navigation mode types
|
||||
export type ModeType =
|
||||
| 'viewer'
|
||||
| 'pageEditor'
|
||||
| 'fileEditor'
|
||||
| 'merge'
|
||||
| 'split'
|
||||
| 'compress'
|
||||
| 'ocr'
|
||||
| 'convert'
|
||||
| 'addPassword'
|
||||
| 'changePermissions'
|
||||
| 'sanitize';
|
||||
|
||||
// Navigation state
|
||||
interface NavigationState {
|
||||
currentMode: ModeType;
|
||||
hasUnsavedChanges: boolean;
|
||||
pendingNavigation: (() => void) | null;
|
||||
showNavigationWarning: boolean;
|
||||
}
|
||||
|
||||
// Navigation actions
|
||||
type NavigationAction =
|
||||
| { type: 'SET_MODE'; payload: { mode: ModeType } }
|
||||
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
||||
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } };
|
||||
|
||||
// Navigation reducer
|
||||
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
|
||||
switch (action.type) {
|
||||
case 'SET_MODE':
|
||||
return { ...state, currentMode: action.payload.mode };
|
||||
|
||||
case 'SET_UNSAVED_CHANGES':
|
||||
return { ...state, hasUnsavedChanges: action.payload.hasChanges };
|
||||
|
||||
case 'SET_PENDING_NAVIGATION':
|
||||
return { ...state, pendingNavigation: action.payload.navigationFn };
|
||||
|
||||
case 'SHOW_NAVIGATION_WARNING':
|
||||
return { ...state, showNavigationWarning: action.payload.show };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Initial state
|
||||
const initialState: NavigationState = {
|
||||
currentMode: 'pageEditor',
|
||||
hasUnsavedChanges: false,
|
||||
pendingNavigation: null,
|
||||
showNavigationWarning: false
|
||||
};
|
||||
|
||||
// Navigation context actions interface
|
||||
export interface NavigationContextActions {
|
||||
setMode: (mode: ModeType) => void;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
showNavigationWarning: (show: boolean) => void;
|
||||
requestNavigation: (navigationFn: () => void) => void;
|
||||
confirmNavigation: () => void;
|
||||
cancelNavigation: () => void;
|
||||
}
|
||||
|
||||
// Split context values
|
||||
export interface NavigationContextStateValue {
|
||||
currentMode: ModeType;
|
||||
hasUnsavedChanges: boolean;
|
||||
pendingNavigation: (() => void) | null;
|
||||
showNavigationWarning: boolean;
|
||||
}
|
||||
|
||||
export interface NavigationContextActionsValue {
|
||||
actions: NavigationContextActions;
|
||||
}
|
||||
|
||||
// Create contexts
|
||||
const NavigationStateContext = createContext<NavigationContextStateValue | undefined>(undefined);
|
||||
const NavigationActionsContext = createContext<NavigationContextActionsValue | undefined>(undefined);
|
||||
|
||||
// Provider component
|
||||
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||
|
||||
const actions: NavigationContextActions = {
|
||||
setMode: useCallback((mode: ModeType) => {
|
||||
dispatch({ type: 'SET_MODE', payload: { mode } });
|
||||
}, []),
|
||||
|
||||
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||
}, []),
|
||||
|
||||
showNavigationWarning: useCallback((show: boolean) => {
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
||||
}, []),
|
||||
|
||||
requestNavigation: useCallback((navigationFn: () => void) => {
|
||||
// If no unsaved changes, navigate immediately
|
||||
if (!state.hasUnsavedChanges) {
|
||||
navigationFn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, store the navigation and show warning
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||
}, [state.hasUnsavedChanges]),
|
||||
|
||||
confirmNavigation: useCallback(() => {
|
||||
// Execute pending navigation
|
||||
if (state.pendingNavigation) {
|
||||
state.pendingNavigation();
|
||||
}
|
||||
|
||||
// Clear navigation state
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||
}, [state.pendingNavigation]),
|
||||
|
||||
cancelNavigation: useCallback(() => {
|
||||
// Clear navigation without executing
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||
}, [])
|
||||
};
|
||||
|
||||
const stateValue: NavigationContextStateValue = {
|
||||
currentMode: state.currentMode,
|
||||
hasUnsavedChanges: state.hasUnsavedChanges,
|
||||
pendingNavigation: state.pendingNavigation,
|
||||
showNavigationWarning: state.showNavigationWarning
|
||||
};
|
||||
|
||||
const actionsValue: NavigationContextActionsValue = {
|
||||
actions
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationStateContext.Provider value={stateValue}>
|
||||
<NavigationActionsContext.Provider value={actionsValue}>
|
||||
{children}
|
||||
</NavigationActionsContext.Provider>
|
||||
</NavigationStateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Navigation hooks
|
||||
export const useNavigationState = () => {
|
||||
const context = useContext(NavigationStateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useNavigationState must be used within NavigationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useNavigationActions = () => {
|
||||
const context = useContext(NavigationActionsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useNavigationActions must be used within NavigationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Combined hook for convenience
|
||||
export const useNavigation = () => {
|
||||
const state = useNavigationState();
|
||||
const { actions } = useNavigationActions();
|
||||
return { ...state, ...actions };
|
||||
};
|
||||
|
||||
// Navigation guard hook (equivalent to old useFileNavigation)
|
||||
export const useNavigationGuard = () => {
|
||||
const state = useNavigationState();
|
||||
const { actions } = useNavigationActions();
|
||||
|
||||
return {
|
||||
pendingNavigation: state.pendingNavigation,
|
||||
showNavigationWarning: state.showNavigationWarning,
|
||||
hasUnsavedChanges: state.hasUnsavedChanges,
|
||||
requestNavigation: actions.requestNavigation,
|
||||
confirmNavigation: actions.confirmNavigation,
|
||||
cancelNavigation: actions.cancelNavigation,
|
||||
setHasUnsavedChanges: actions.setHasUnsavedChanges,
|
||||
setShowNavigationWarning: actions.showNavigationWarning
|
||||
};
|
||||
};
|
||||
|
||||
// Utility functions for mode handling
|
||||
export const isValidMode = (mode: string): mode is ModeType => {
|
||||
const validModes: ModeType[] = [
|
||||
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
|
||||
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
|
||||
];
|
||||
return validModes.includes(mode as ModeType);
|
||||
};
|
||||
|
||||
export const getDefaultMode = (): ModeType => 'pageEditor';
|
||||
|
||||
// TODO: This will be expanded for URL-based routing system
|
||||
// - URL parsing utilities
|
||||
// - Route definitions
|
||||
// - Navigation hooks with URL sync
|
||||
// - History management
|
||||
// - Breadcrumb restoration from URL params
|
164
frontend/src/contexts/file/FileReducer.ts
Normal file
164
frontend/src/contexts/file/FileReducer.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* FileContext reducer - Pure state management for file operations
|
||||
*/
|
||||
|
||||
import {
|
||||
FileContextState,
|
||||
FileContextAction,
|
||||
FileId,
|
||||
FileRecord
|
||||
} from '../../types/fileContext';
|
||||
|
||||
// Initial state
|
||||
export const initialFileContextState: FileContextState = {
|
||||
files: {
|
||||
ids: [],
|
||||
byId: {}
|
||||
},
|
||||
ui: {
|
||||
selectedFileIds: [],
|
||||
selectedPageNumbers: [],
|
||||
isProcessing: false,
|
||||
processingProgress: 0,
|
||||
hasUnsavedChanges: false
|
||||
}
|
||||
};
|
||||
|
||||
// Pure reducer function
|
||||
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
|
||||
switch (action.type) {
|
||||
case 'ADD_FILES': {
|
||||
const { fileRecords } = action.payload;
|
||||
const newIds: FileId[] = [];
|
||||
const newById: Record<FileId, FileRecord> = { ...state.files.byId };
|
||||
|
||||
fileRecords.forEach(record => {
|
||||
// Only add if not already present (dedupe by stable ID)
|
||||
if (!newById[record.id]) {
|
||||
newIds.push(record.id);
|
||||
newById[record.id] = record;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: [...state.files.ids, ...newIds],
|
||||
byId: newById
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'REMOVE_FILES': {
|
||||
const { fileIds } = action.payload;
|
||||
const remainingIds = state.files.ids.filter(id => !fileIds.includes(id));
|
||||
const newById = { ...state.files.byId };
|
||||
|
||||
// Remove files from state (resource cleanup handled by lifecycle manager)
|
||||
fileIds.forEach(id => {
|
||||
delete newById[id];
|
||||
});
|
||||
|
||||
// Clear selections that reference removed files
|
||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !fileIds.includes(id));
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: remainingIds,
|
||||
byId: newById
|
||||
},
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: validSelectedFileIds
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'UPDATE_FILE_RECORD': {
|
||||
const { id, updates } = action.payload;
|
||||
const existingRecord = state.files.byId[id];
|
||||
|
||||
if (!existingRecord) {
|
||||
return state; // File doesn't exist, no-op
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
...state.files,
|
||||
byId: {
|
||||
...state.files.byId,
|
||||
[id]: {
|
||||
...existingRecord,
|
||||
...updates
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_SELECTED_FILES': {
|
||||
const { fileIds } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: fileIds
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_SELECTED_PAGES': {
|
||||
const { pageNumbers } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedPageNumbers: pageNumbers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'CLEAR_SELECTIONS': {
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: [],
|
||||
selectedPageNumbers: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_PROCESSING': {
|
||||
const { isProcessing, progress } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
...state.ui,
|
||||
isProcessing,
|
||||
processingProgress: progress
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_UNSAVED_CHANGES': {
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
...state.ui,
|
||||
hasUnsavedChanges: action.payload.hasChanges
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'RESET_CONTEXT': {
|
||||
// Reset UI state to clean slate (resource cleanup handled by lifecycle manager)
|
||||
return { ...initialFileContextState };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
13
frontend/src/contexts/file/contexts.ts
Normal file
13
frontend/src/contexts/file/contexts.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* React contexts for file state and actions
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { FileContextStateValue, FileContextActionsValue } from '../../types/fileContext';
|
||||
|
||||
// Split contexts for performance
|
||||
export const FileStateContext = createContext<FileContextStateValue | undefined>(undefined);
|
||||
export const FileActionsContext = createContext<FileContextActionsValue | undefined>(undefined);
|
||||
|
||||
// Export types for use in hooks
|
||||
export type { FileContextStateValue, FileContextActionsValue };
|
225
frontend/src/contexts/file/fileActions.ts
Normal file
225
frontend/src/contexts/file/fileActions.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* File actions - Unified file operations with single addFiles helper
|
||||
*/
|
||||
|
||||
import {
|
||||
FileId,
|
||||
FileRecord,
|
||||
FileContextAction,
|
||||
FileContextState,
|
||||
toFileRecord,
|
||||
createFileId,
|
||||
createQuickKey
|
||||
} from '../../types/fileContext';
|
||||
import { FileMetadata } from '../../types/file';
|
||||
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
||||
import { fileProcessingService } from '../../services/fileProcessingService';
|
||||
import { buildQuickKeySet } from './fileSelectors';
|
||||
|
||||
const DEBUG = process.env.NODE_ENV === 'development';
|
||||
|
||||
/**
|
||||
* Helper to create ProcessedFile metadata structure
|
||||
*/
|
||||
export function createProcessedFile(pageCount: number, thumbnail?: string) {
|
||||
return {
|
||||
totalPages: pageCount,
|
||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||
pageNumber: index + 1,
|
||||
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
||||
rotation: 0,
|
||||
splitBefore: false
|
||||
})),
|
||||
thumbnailUrl: thumbnail,
|
||||
lastProcessed: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* File addition types
|
||||
*/
|
||||
type AddFileKind = 'raw' | 'processed' | 'stored';
|
||||
|
||||
interface AddFileOptions {
|
||||
// For 'raw' files
|
||||
files?: File[];
|
||||
|
||||
// For 'processed' files
|
||||
filesWithThumbnails?: Array<{ file: File; thumbnail?: string; pageCount?: number }>;
|
||||
|
||||
// For 'stored' files
|
||||
filesWithMetadata?: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified file addition helper - replaces addFiles/addProcessedFiles/addStoredFiles
|
||||
*/
|
||||
export async function addFiles(
|
||||
kind: AddFileKind,
|
||||
options: AddFileOptions,
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
): Promise<File[]> {
|
||||
const fileRecords: FileRecord[] = [];
|
||||
const addedFiles: File[] = [];
|
||||
|
||||
// Build quickKey lookup from existing files for deduplication
|
||||
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
|
||||
|
||||
switch (kind) {
|
||||
case 'raw': {
|
||||
const { files = [] } = options;
|
||||
if (DEBUG) console.log(`📄 addFiles(raw): Adding ${files.length} files with immediate thumbnail generation`);
|
||||
|
||||
for (const file of files) {
|
||||
const quickKey = createQuickKey(file);
|
||||
|
||||
// Soft deduplication: Check if file already exists by metadata
|
||||
if (existingQuickKeys.has(quickKey)) {
|
||||
if (DEBUG) console.log(`📄 Skipping duplicate file: ${file.name} (already exists)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileId = createFileId();
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
// Generate thumbnail and page count immediately
|
||||
let thumbnail: string | undefined;
|
||||
let pageCount: number = 1;
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log(`📄 Generating immediate thumbnail and metadata for ${file.name}`);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
thumbnail = result.thumbnail;
|
||||
pageCount = result.pageCount;
|
||||
if (DEBUG) console.log(`📄 Generated immediate metadata for ${file.name}: ${pageCount} pages, thumbnail: ${!!thumbnail}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`📄 Failed to generate immediate metadata for ${file.name}:`, error);
|
||||
}
|
||||
|
||||
// Create record with immediate thumbnail and page metadata
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
// Create initial processedFile metadata with page count
|
||||
if (pageCount > 0) {
|
||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||
if (DEBUG) console.log(`📄 addFiles(raw): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
|
||||
}
|
||||
|
||||
existingQuickKeys.add(quickKey);
|
||||
fileRecords.push(record);
|
||||
addedFiles.push(file);
|
||||
|
||||
// Start background processing for validation only (we already have thumbnail and page count)
|
||||
fileProcessingService.processFile(file, fileId).then(result => {
|
||||
// Only update if file still exists in context
|
||||
if (filesRef.current.has(fileId)) {
|
||||
if (result.success && result.metadata) {
|
||||
// Only log if page count differs from our immediate calculation
|
||||
const initialPageCount = pageCount;
|
||||
if (result.metadata.totalPages !== initialPageCount) {
|
||||
if (DEBUG) console.log(`📄 Background processing found different page count for ${file.name}: ${result.metadata.totalPages} vs immediate ${initialPageCount}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'processed': {
|
||||
const { filesWithThumbnails = [] } = options;
|
||||
if (DEBUG) console.log(`📄 addFiles(processed): Adding ${filesWithThumbnails.length} processed files with pre-existing thumbnails`);
|
||||
|
||||
for (const { file, thumbnail, pageCount = 1 } of filesWithThumbnails) {
|
||||
const quickKey = createQuickKey(file);
|
||||
|
||||
if (existingQuickKeys.has(quickKey)) {
|
||||
if (DEBUG) console.log(`📄 Skipping duplicate processed file: ${file.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileId = createFileId();
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
// Create processedFile with provided metadata
|
||||
if (pageCount > 0) {
|
||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||
if (DEBUG) console.log(`📄 addFiles(processed): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
|
||||
}
|
||||
|
||||
existingQuickKeys.add(quickKey);
|
||||
fileRecords.push(record);
|
||||
addedFiles.push(file);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'stored': {
|
||||
const { filesWithMetadata = [] } = options;
|
||||
if (DEBUG) console.log(`📄 addFiles(stored): Restoring ${filesWithMetadata.length} files from storage with existing metadata`);
|
||||
|
||||
for (const { file, originalId, metadata } of filesWithMetadata) {
|
||||
const quickKey = createQuickKey(file);
|
||||
|
||||
if (existingQuickKeys.has(quickKey)) {
|
||||
if (DEBUG) console.log(`📄 Skipping duplicate stored file: ${file.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to preserve original ID, but generate new if it conflicts
|
||||
let fileId = originalId;
|
||||
if (filesRef.current.has(originalId)) {
|
||||
fileId = createFileId();
|
||||
if (DEBUG) console.log(`📄 ID conflict for stored file ${file.name}, using new ID: ${fileId}`);
|
||||
}
|
||||
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
const record = toFileRecord(file, fileId);
|
||||
|
||||
// Restore metadata from storage
|
||||
if (metadata.thumbnail) {
|
||||
record.thumbnailUrl = metadata.thumbnail;
|
||||
}
|
||||
|
||||
// Note: For stored files, processedFile will be restored from FileRecord if it exists
|
||||
// The metadata here is just basic file info, not processed file data
|
||||
|
||||
existingQuickKeys.add(quickKey);
|
||||
fileRecords.push(record);
|
||||
addedFiles.push(file);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch ADD_FILES action if we have new files
|
||||
if (fileRecords.length > 0) {
|
||||
dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
|
||||
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`);
|
||||
}
|
||||
|
||||
return addedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action factory functions
|
||||
*/
|
||||
export const createFileActions = (dispatch: React.Dispatch<FileContextAction>) => ({
|
||||
setSelectedFiles: (fileIds: FileId[]) => dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds } }),
|
||||
setSelectedPages: (pageNumbers: number[]) => dispatch({ type: 'SET_SELECTED_PAGES', payload: { pageNumbers } }),
|
||||
clearSelections: () => dispatch({ type: 'CLEAR_SELECTIONS' }),
|
||||
setProcessing: (isProcessing: boolean, progress = 0) => dispatch({ type: 'SET_PROCESSING', payload: { isProcessing, progress } }),
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }),
|
||||
resetContext: () => dispatch({ type: 'RESET_CONTEXT' })
|
||||
});
|
228
frontend/src/contexts/file/fileHooks.ts
Normal file
228
frontend/src/contexts/file/fileHooks.ts
Normal file
@ -0,0 +1,228 @@
|
||||
/**
|
||||
* New performant file hooks - Clean API without legacy compatibility
|
||||
*/
|
||||
|
||||
import { useContext, useMemo } from 'react';
|
||||
import {
|
||||
FileStateContext,
|
||||
FileActionsContext,
|
||||
FileContextStateValue,
|
||||
FileContextActionsValue
|
||||
} from './contexts';
|
||||
import { FileId, FileRecord } from '../../types/fileContext';
|
||||
|
||||
/**
|
||||
* Hook for accessing file state (will re-render on any state change)
|
||||
* Use individual selector hooks below for better performance
|
||||
*/
|
||||
export function useFileState(): FileContextStateValue {
|
||||
const context = useContext(FileStateContext);
|
||||
if (!context) {
|
||||
throw new Error('useFileState must be used within a FileContextProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for accessing file actions (stable - won't cause re-renders)
|
||||
*/
|
||||
export function useFileActions(): FileContextActionsValue {
|
||||
const context = useContext(FileActionsContext);
|
||||
if (!context) {
|
||||
throw new Error('useFileActions must be used within a FileContextProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for current/primary file (first in list)
|
||||
*/
|
||||
export function useCurrentFile(): { file?: File; record?: FileRecord } {
|
||||
const { state, selectors } = useFileState();
|
||||
|
||||
const primaryFileId = state.files.ids[0];
|
||||
return useMemo(() => ({
|
||||
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
|
||||
record: primaryFileId ? selectors.getFileRecord(primaryFileId) : undefined
|
||||
}), [primaryFileId, selectors]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for file selection state and actions
|
||||
*/
|
||||
export function useFileSelection() {
|
||||
const { state } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
return useMemo(() => ({
|
||||
selectedFileIds: state.ui.selectedFileIds,
|
||||
selectedPageNumbers: state.ui.selectedPageNumbers,
|
||||
setSelectedFiles: actions.setSelectedFiles,
|
||||
setSelectedPages: actions.setSelectedPages,
|
||||
clearSelections: actions.clearSelections
|
||||
}), [
|
||||
state.ui.selectedFileIds,
|
||||
state.ui.selectedPageNumbers,
|
||||
actions.setSelectedFiles,
|
||||
actions.setSelectedPages,
|
||||
actions.clearSelections
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for file management operations
|
||||
*/
|
||||
export function useFileManagement() {
|
||||
const { actions } = useFileActions();
|
||||
|
||||
return useMemo(() => ({
|
||||
addFiles: actions.addFiles,
|
||||
removeFiles: actions.removeFiles,
|
||||
clearAllFiles: actions.clearAllFiles,
|
||||
updateFileRecord: actions.updateFileRecord
|
||||
}), [actions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for UI state
|
||||
*/
|
||||
export function useFileUI() {
|
||||
const { state } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
return useMemo(() => ({
|
||||
isProcessing: state.ui.isProcessing,
|
||||
processingProgress: state.ui.processingProgress,
|
||||
hasUnsavedChanges: state.ui.hasUnsavedChanges,
|
||||
setProcessing: actions.setProcessing,
|
||||
setUnsavedChanges: actions.setHasUnsavedChanges
|
||||
}), [state.ui, actions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for specific file by ID (optimized for individual file access)
|
||||
*/
|
||||
export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } {
|
||||
const { selectors } = useFileState();
|
||||
|
||||
return useMemo(() => ({
|
||||
file: selectors.getFile(fileId),
|
||||
record: selectors.getFileRecord(fileId)
|
||||
}), [fileId, selectors]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for all files (use sparingly - causes re-renders on file list changes)
|
||||
*/
|
||||
export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
||||
const { state, selectors } = useFileState();
|
||||
|
||||
return useMemo(() => ({
|
||||
files: selectors.getFiles(),
|
||||
records: selectors.getFileRecords(),
|
||||
fileIds: state.files.ids
|
||||
}), [state.files.ids, selectors]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for selected files (optimized for selection-based UI)
|
||||
*/
|
||||
export function useSelectedFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } {
|
||||
const { state, selectors } = useFileState();
|
||||
|
||||
return useMemo(() => ({
|
||||
files: selectors.getSelectedFiles(),
|
||||
records: selectors.getSelectedFileRecords(),
|
||||
fileIds: state.ui.selectedFileIds
|
||||
}), [state.ui.selectedFileIds, selectors]);
|
||||
}
|
||||
|
||||
// Navigation management removed - moved to NavigationContext
|
||||
|
||||
/**
|
||||
* Primary API hook for file context operations
|
||||
* Used by tools for core file context functionality
|
||||
*/
|
||||
export function useFileContext() {
|
||||
const { actions } = useFileActions();
|
||||
|
||||
return useMemo(() => ({
|
||||
trackBlobUrl: actions.trackBlobUrl,
|
||||
trackPdfDocument: actions.trackPdfDocument,
|
||||
scheduleCleanup: actions.scheduleCleanup,
|
||||
setUnsavedChanges: actions.setHasUnsavedChanges
|
||||
}), [actions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary API hook for tool file selection workflow
|
||||
* Used by tools for managing file selection and tool-specific operations
|
||||
*/
|
||||
export function useToolFileSelection() {
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
// Memoize selected files to avoid recreating arrays
|
||||
const selectedFiles = useMemo(() => {
|
||||
return selectors.getSelectedFiles();
|
||||
}, [state.ui.selectedFileIds, selectors]);
|
||||
|
||||
return useMemo(() => ({
|
||||
selectedFiles,
|
||||
selectedFileIds: state.ui.selectedFileIds,
|
||||
selectedPageNumbers: state.ui.selectedPageNumbers,
|
||||
setSelectedFiles: actions.setSelectedFiles,
|
||||
setSelectedPages: actions.setSelectedPages,
|
||||
clearSelections: actions.clearSelections,
|
||||
// Tool workflow properties
|
||||
maxFiles: 10, // Default value for tools
|
||||
isToolMode: true,
|
||||
setMaxFiles: (maxFiles: number) => { /* Tool-specific - can be implemented if needed */ },
|
||||
setIsToolMode: (isToolMode: boolean) => { /* Tool-specific - can be implemented if needed */ }
|
||||
}), [
|
||||
selectedFiles,
|
||||
state.ui.selectedFileIds,
|
||||
state.ui.selectedPageNumbers,
|
||||
actions.setSelectedFiles,
|
||||
actions.setSelectedPages,
|
||||
actions.clearSelections
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for processed files (compatibility with old FileContext)
|
||||
* Provides access to files with their processed metadata
|
||||
*/
|
||||
export function useProcessedFiles() {
|
||||
const { state, selectors } = useFileState();
|
||||
|
||||
// Create a Map-like interface for backward compatibility
|
||||
const compatibilityMap = useMemo(() => ({
|
||||
size: state.files.ids.length,
|
||||
get: (file: File) => {
|
||||
// Find file record by matching File object properties
|
||||
const record = Object.values(state.files.byId).find(r =>
|
||||
r.name === file.name && r.size === file.size && r.lastModified === file.lastModified
|
||||
);
|
||||
return record?.processedFile || null;
|
||||
},
|
||||
has: (file: File) => {
|
||||
// Find file record by matching File object properties
|
||||
const record = Object.values(state.files.byId).find(r =>
|
||||
r.name === file.name && r.size === file.size && r.lastModified === file.lastModified
|
||||
);
|
||||
return !!record?.processedFile;
|
||||
},
|
||||
set: () => {
|
||||
console.warn('processedFiles.set is deprecated - use FileRecord updates instead');
|
||||
}
|
||||
}), [state.files.byId, state.files.ids.length]);
|
||||
|
||||
return useMemo(() => ({
|
||||
processedFiles: compatibilityMap,
|
||||
getProcessedFile: (file: File) => compatibilityMap.get(file),
|
||||
updateProcessedFile: () => {
|
||||
console.warn('updateProcessedFile is deprecated - processed files are now stored in FileRecord');
|
||||
}
|
||||
}), [compatibilityMap]);
|
||||
}
|
86
frontend/src/contexts/file/fileSelectors.ts
Normal file
86
frontend/src/contexts/file/fileSelectors.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* File selectors - Pure functions for accessing file state
|
||||
*/
|
||||
|
||||
import {
|
||||
FileId,
|
||||
FileRecord,
|
||||
FileContextState,
|
||||
FileContextSelectors
|
||||
} from '../../types/fileContext';
|
||||
|
||||
/**
|
||||
* Create stable selectors using stateRef and filesRef
|
||||
*/
|
||||
export function createFileSelectors(
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||
): FileContextSelectors {
|
||||
return {
|
||||
getFile: (id: FileId) => filesRef.current.get(id),
|
||||
|
||||
getFiles: (ids?: FileId[]) => {
|
||||
const currentIds = ids || stateRef.current.files.ids;
|
||||
return currentIds.map(id => filesRef.current.get(id)).filter(Boolean) as File[];
|
||||
},
|
||||
|
||||
getFileRecord: (id: FileId) => stateRef.current.files.byId[id],
|
||||
|
||||
getFileRecords: (ids?: FileId[]) => {
|
||||
const currentIds = ids || stateRef.current.files.ids;
|
||||
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
|
||||
},
|
||||
|
||||
getAllFileIds: () => stateRef.current.files.ids,
|
||||
|
||||
getSelectedFiles: () => {
|
||||
return stateRef.current.ui.selectedFileIds
|
||||
.map(id => filesRef.current.get(id))
|
||||
.filter(Boolean) as File[];
|
||||
},
|
||||
|
||||
getSelectedFileRecords: () => {
|
||||
return stateRef.current.ui.selectedFileIds
|
||||
.map(id => stateRef.current.files.byId[id])
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
// Stable signature for effects - prevents unnecessary re-renders
|
||||
getFilesSignature: () => {
|
||||
return stateRef.current.files.ids
|
||||
.map(id => {
|
||||
const record = stateRef.current.files.byId[id];
|
||||
return record ? `${id}:${record.size}:${record.lastModified}` : '';
|
||||
})
|
||||
.join('|');
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for building quickKey sets for deduplication
|
||||
*/
|
||||
export function buildQuickKeySet(fileRecords: Record<FileId, FileRecord>): Set<string> {
|
||||
const quickKeys = new Set<string>();
|
||||
Object.values(fileRecords).forEach(record => {
|
||||
quickKeys.add(record.quickKey);
|
||||
});
|
||||
return quickKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary file (first in list) - commonly used pattern
|
||||
*/
|
||||
export function getPrimaryFile(
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||
): { file?: File; record?: FileRecord } {
|
||||
const primaryFileId = stateRef.current.files.ids[0];
|
||||
if (!primaryFileId) return {};
|
||||
|
||||
return {
|
||||
file: filesRef.current.get(primaryFileId),
|
||||
record: stateRef.current.files.byId[primaryFileId]
|
||||
};
|
||||
}
|
257
frontend/src/contexts/file/lifecycle.ts
Normal file
257
frontend/src/contexts/file/lifecycle.ts
Normal file
@ -0,0 +1,257 @@
|
||||
/**
|
||||
* File lifecycle management - Resource cleanup and memory management
|
||||
*/
|
||||
|
||||
import { FileId, FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext';
|
||||
|
||||
const DEBUG = process.env.NODE_ENV === 'development';
|
||||
|
||||
/**
|
||||
* Resource tracking and cleanup utilities
|
||||
*/
|
||||
export class FileLifecycleManager {
|
||||
private cleanupTimers = new Map<string, number>();
|
||||
private blobUrls = new Set<string>();
|
||||
private pdfDocuments = new Map<string, any>();
|
||||
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
||||
|
||||
constructor(
|
||||
private filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
private dispatch: React.Dispatch<FileContextAction>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Track blob URLs for cleanup
|
||||
*/
|
||||
trackBlobUrl = (url: string): void => {
|
||||
// Only track actual blob URLs to avoid trying to revoke other schemes
|
||||
if (url.startsWith('blob:')) {
|
||||
this.blobUrls.add(url);
|
||||
if (DEBUG) console.log(`🗂️ Tracking blob URL: ${url.substring(0, 50)}...`);
|
||||
} else {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to track non-blob URL: ${url.substring(0, 50)}...`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Track PDF documents for cleanup
|
||||
*/
|
||||
trackPdfDocument = (key: string, pdfDoc: any): void => {
|
||||
// Clean up existing PDF document if present
|
||||
const existing = this.pdfDocuments.get(key);
|
||||
if (existing && typeof existing.destroy === 'function') {
|
||||
try {
|
||||
existing.destroy();
|
||||
if (DEBUG) console.log(`🗂️ Destroyed existing PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error destroying existing PDF document:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.pdfDocuments.set(key, pdfDoc);
|
||||
if (DEBUG) console.log(`🗂️ Tracking PDF document for key: ${key}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up resources for a specific file (with stateRef access for complete cleanup)
|
||||
*/
|
||||
cleanupFile = (fileId: string, stateRef?: React.MutableRefObject<any>): void => {
|
||||
if (DEBUG) console.log(`🗂️ Cleaning up resources for file: ${fileId}`);
|
||||
|
||||
// Use comprehensive cleanup (same as removeFiles)
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
|
||||
// Remove file from state
|
||||
this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds: [fileId] } });
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up all files and resources
|
||||
*/
|
||||
cleanupAllFiles = (): void => {
|
||||
if (DEBUG) console.log('🗂️ Cleaning up all files and resources');
|
||||
|
||||
// Clean up all PDF documents
|
||||
this.pdfDocuments.forEach((pdfDoc, key) => {
|
||||
if (pdfDoc && typeof pdfDoc.destroy === 'function') {
|
||||
try {
|
||||
pdfDoc.destroy();
|
||||
if (DEBUG) console.log(`🗂️ Destroyed PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`Error destroying PDF document for key ${key}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.pdfDocuments.clear();
|
||||
|
||||
// Revoke all blob URLs
|
||||
this.blobUrls.forEach(url => {
|
||||
try {
|
||||
URL.revokeObjectURL(url);
|
||||
if (DEBUG) console.log(`🗂️ Revoked blob URL: ${url.substring(0, 50)}...`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error revoking blob URL:', error);
|
||||
}
|
||||
});
|
||||
this.blobUrls.clear();
|
||||
|
||||
// Clear all cleanup timers and generations
|
||||
this.cleanupTimers.forEach(timer => clearTimeout(timer));
|
||||
this.cleanupTimers.clear();
|
||||
this.fileGenerations.clear();
|
||||
|
||||
// Clear files ref
|
||||
this.filesRef.current.clear();
|
||||
|
||||
if (DEBUG) console.log('🗂️ All resources cleaned up');
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup
|
||||
*/
|
||||
scheduleCleanup = (fileId: string, delay: number = 30000, stateRef?: React.MutableRefObject<any>): void => {
|
||||
// Cancel existing timer
|
||||
const existingTimer = this.cleanupTimers.get(fileId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
this.cleanupTimers.delete(fileId);
|
||||
}
|
||||
|
||||
// If delay is negative, just cancel (don't reschedule)
|
||||
if (delay < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment generation for this file to invalidate any pending cleanup
|
||||
const currentGen = (this.fileGenerations.get(fileId) || 0) + 1;
|
||||
this.fileGenerations.set(fileId, currentGen);
|
||||
|
||||
// Schedule new cleanup with generation token
|
||||
const timer = window.setTimeout(() => {
|
||||
// Check if this cleanup is still valid (file hasn't been re-added)
|
||||
if (this.fileGenerations.get(fileId) === currentGen) {
|
||||
this.cleanupFile(fileId, stateRef);
|
||||
} else {
|
||||
if (DEBUG) console.log(`🗂️ Skipped stale cleanup for file ${fileId} (generation mismatch)`);
|
||||
}
|
||||
}, delay);
|
||||
|
||||
this.cleanupTimers.set(fileId, timer);
|
||||
if (DEBUG) console.log(`🗂️ Scheduled cleanup for file ${fileId} in ${delay}ms (gen ${currentGen})`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a file immediately with complete resource cleanup
|
||||
*/
|
||||
removeFiles = (fileIds: FileId[], stateRef?: React.MutableRefObject<any>): void => {
|
||||
if (DEBUG) console.log(`🗂️ Removing ${fileIds.length} files immediately`);
|
||||
|
||||
fileIds.forEach(fileId => {
|
||||
// Clean up all resources for this file
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
});
|
||||
|
||||
// Dispatch removal action once for all files (reducer only updates state)
|
||||
this.dispatch({ type: 'REMOVE_FILES', payload: { fileIds } });
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete resource cleanup for a single file
|
||||
*/
|
||||
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
|
||||
// Remove from files ref
|
||||
this.filesRef.current.delete(fileId);
|
||||
|
||||
// Clean up PDF documents (scan all keys that start with fileId)
|
||||
const keysToDelete: string[] = [];
|
||||
this.pdfDocuments.forEach((pdfDoc, key) => {
|
||||
if (key === fileId || key.startsWith(`${fileId}:`)) {
|
||||
if (pdfDoc && typeof pdfDoc.destroy === 'function') {
|
||||
try {
|
||||
pdfDoc.destroy();
|
||||
keysToDelete.push(key);
|
||||
if (DEBUG) console.log(`🗂️ Destroyed PDF document for key: ${key}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`Error destroying PDF document for key ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
keysToDelete.forEach(key => this.pdfDocuments.delete(key));
|
||||
|
||||
// Cancel cleanup timer and generation
|
||||
const timer = this.cleanupTimers.get(fileId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.cleanupTimers.delete(fileId);
|
||||
if (DEBUG) console.log(`🗂️ Cancelled cleanup timer for file: ${fileId}`);
|
||||
}
|
||||
this.fileGenerations.delete(fileId);
|
||||
|
||||
// Clean up blob URLs from file record if we have access to state
|
||||
if (stateRef) {
|
||||
const record = stateRef.current.files.byId[fileId];
|
||||
if (record) {
|
||||
// Revoke blob URLs from file record
|
||||
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(record.thumbnailUrl);
|
||||
if (DEBUG) console.log(`🗂️ Revoked thumbnail blob URL for file: ${fileId}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error revoking thumbnail URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (record.blobUrl && record.blobUrl.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(record.blobUrl);
|
||||
if (DEBUG) console.log(`🗂️ Revoked file blob URL for file: ${fileId}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error revoking file URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up processed file thumbnails
|
||||
if (record.processedFile?.pages) {
|
||||
record.processedFile.pages.forEach((page: ProcessedFilePage, index: number) => {
|
||||
if (page.thumbnail && page.thumbnail.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(page.thumbnail);
|
||||
if (DEBUG) console.log(`🗂️ Revoked page ${index} thumbnail for file: ${fileId}`);
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn('Error revoking page thumbnail URL:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update file record with race condition guards
|
||||
*/
|
||||
updateFileRecord = (fileId: FileId, updates: Partial<FileRecord>, stateRef?: React.MutableRefObject<any>): void => {
|
||||
// Guard against updating removed files (race condition protection)
|
||||
if (!this.filesRef.current.has(fileId)) {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional state guard for rare race conditions
|
||||
if (stateRef && !stateRef.current.files.byId[fileId]) {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (state): ${fileId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatch({ type: 'UPDATE_FILE_RECORD', payload: { id: fileId, updates } });
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleanup on unmount
|
||||
*/
|
||||
destroy = (): void => {
|
||||
if (DEBUG) console.log('🗂️ FileLifecycleManager destroying - cleaning up all resources');
|
||||
this.cleanupAllFiles();
|
||||
};
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileActions, useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext";
|
||||
import { Group } from "@mantine/core";
|
||||
import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext";
|
||||
@ -65,10 +66,15 @@ function HomePageContent() {
|
||||
}
|
||||
|
||||
function HomePageWithProviders() {
|
||||
const { actions } = useFileActions();
|
||||
const { actions } = useNavigationActions();
|
||||
|
||||
// Wrapper to convert string to ModeType
|
||||
const handleViewChange = (view: string) => {
|
||||
actions.setMode(view as any); // ToolWorkflowContext should validate this
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolWorkflowProvider onViewChange={actions.setMode as any /* FIX ME */}>
|
||||
<ToolWorkflowProvider onViewChange={handleViewChange}>
|
||||
<SidebarProvider>
|
||||
<HomePageContent />
|
||||
</SidebarProvider>
|
||||
|
@ -3,7 +3,7 @@ import { Box, Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext, useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -22,8 +22,8 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
// const setCurrentMode = (mode) => console.log('Navigate to:', mode); // TODO: Hook up to URL routing
|
||||
|
||||
const [collapsedPermissions, setCollapsedPermissions] = useState(true);
|
||||
|
||||
@ -58,14 +58,11 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem('previousMode', 'addPassword');
|
||||
setCurrentMode('viewer');
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
addPasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode('addPassword');
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
|
@ -3,7 +3,8 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext, useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -20,7 +21,8 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const changePermissionsParams = useChangePermissionsParameters();
|
||||
|
@ -3,8 +3,8 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileActions } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -21,8 +21,8 @@ import { useCompressTips } from "../components/tooltips/useCompressTips";
|
||||
|
||||
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useFileActions();
|
||||
const setCurrentMode = actions.setCurrentMode;
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const compressParams = useCompressParameters();
|
||||
|
@ -3,8 +3,9 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileActions, useFileState } from "../contexts/FileContext";
|
||||
import { useFileState } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -20,9 +21,9 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useFileActions();
|
||||
const { selectors } = useFileState();
|
||||
const setCurrentMode = actions.setCurrentMode;
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
const activeFiles = selectors.getFiles();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -3,8 +3,8 @@ import { Button, Stack, Text, Box } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileActions } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -22,8 +22,8 @@ import { useOCRTips } from "../components/tooltips/useOCRTips";
|
||||
|
||||
const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useFileActions();
|
||||
const setCurrentMode = actions.setCurrentMode;
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const ocrParams = useOCRParameters();
|
||||
|
@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -15,13 +16,13 @@ import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings";
|
||||
import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters";
|
||||
import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation";
|
||||
import { BaseToolProps } from "../types/tool";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
|
||||
const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
|
||||
const sanitizeParams = useSanitizeParameters();
|
||||
const sanitizeOperation = useSanitizeOperation();
|
||||
|
@ -3,8 +3,8 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileActions } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||
|
||||
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||
import OperationButton from "../components/tools/shared/OperationButton";
|
||||
@ -19,8 +19,8 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { actions } = useFileActions();
|
||||
const setCurrentMode = actions.setCurrentMode;
|
||||
const { actions } = useNavigationActions();
|
||||
const setCurrentMode = actions.setMode;
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const splitParams = useSplitParameters();
|
||||
|
@ -6,11 +6,26 @@ import { ProcessedFile } from './processing';
|
||||
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
||||
import { FileMetadata } from './file';
|
||||
|
||||
export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr' | 'convert';
|
||||
|
||||
// Normalized state types
|
||||
export type FileId = string;
|
||||
|
||||
export interface ProcessedFilePage {
|
||||
thumbnail?: string;
|
||||
pageNumber?: number;
|
||||
rotation?: number;
|
||||
splitBefore?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ProcessedFileMetadata {
|
||||
pages: ProcessedFilePage[];
|
||||
totalPages?: number;
|
||||
thumbnailUrl?: string;
|
||||
lastProcessed?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FileRecord {
|
||||
id: FileId;
|
||||
name: string;
|
||||
@ -21,13 +36,7 @@ export interface FileRecord {
|
||||
thumbnailUrl?: string;
|
||||
blobUrl?: string;
|
||||
createdAt: number;
|
||||
processedFile?: {
|
||||
pages: Array<{
|
||||
thumbnail?: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
};
|
||||
processedFile?: ProcessedFileMetadata;
|
||||
// Note: File object stored in provider ref, not in state
|
||||
}
|
||||
|
||||
@ -153,16 +162,13 @@ export interface FileContextState {
|
||||
byId: Record<FileId, FileRecord>;
|
||||
};
|
||||
|
||||
// UI state - flat structure for performance
|
||||
// UI state - file-related UI state only
|
||||
ui: {
|
||||
currentMode: ModeType;
|
||||
selectedFileIds: FileId[];
|
||||
selectedPageNumbers: number[];
|
||||
isProcessing: boolean;
|
||||
processingProgress: number;
|
||||
hasUnsavedChanges: boolean;
|
||||
pendingNavigation: (() => void) | null;
|
||||
showNavigationWarning: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -173,17 +179,14 @@ export type FileContextAction =
|
||||
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
|
||||
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<FileRecord> } }
|
||||
|
||||
// UI actions
|
||||
| { type: 'SET_CURRENT_MODE'; payload: ModeType }
|
||||
// UI actions
|
||||
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
|
||||
| { type: 'SET_SELECTED_PAGES'; payload: { pageNumbers: number[] } }
|
||||
| { type: 'CLEAR_SELECTIONS' }
|
||||
| { type: 'SET_PROCESSING'; payload: { isProcessing: boolean; progress: number } }
|
||||
|
||||
// Navigation guard actions
|
||||
// Navigation guard actions (minimal for file-related unsaved changes only)
|
||||
| { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
||||
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }
|
||||
|
||||
// Context management
|
||||
| { type: 'RESET_CONTEXT' };
|
||||
@ -198,9 +201,6 @@ export interface FileContextActions {
|
||||
clearAllFiles: () => void;
|
||||
|
||||
|
||||
// Navigation
|
||||
setCurrentMode: (mode: ModeType) => void;
|
||||
|
||||
// Selection management
|
||||
setSelectedFiles: (fileIds: FileId[]) => void;
|
||||
setSelectedPages: (pageNumbers: number[]) => void;
|
||||
@ -209,16 +209,17 @@ export interface FileContextActions {
|
||||
// Processing state - simple flags only
|
||||
setProcessing: (isProcessing: boolean, progress?: number) => void;
|
||||
|
||||
// Navigation guard system
|
||||
// File-related unsaved changes (minimal navigation guard support)
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
|
||||
// Context management
|
||||
resetContext: () => void;
|
||||
|
||||
// Legacy compatibility
|
||||
setMode: (mode: ModeType) => void;
|
||||
confirmNavigation: () => void;
|
||||
cancelNavigation: () => void;
|
||||
// Resource management
|
||||
trackBlobUrl: (url: string) => void;
|
||||
trackPdfDocument: (key: string, pdfDoc: any) => void;
|
||||
scheduleCleanup: (fileId: string, delay?: number) => void;
|
||||
cleanupFile: (fileId: string) => void;
|
||||
}
|
||||
|
||||
// File selectors (separate from actions to avoid re-renders)
|
||||
@ -258,12 +259,5 @@ export interface FileContextActionsValue {
|
||||
dispatch: (action: FileContextAction) => void;
|
||||
}
|
||||
|
||||
// URL parameter types for deep linking
|
||||
export interface FileContextUrlParams {
|
||||
mode?: ModeType;
|
||||
fileIds?: string[];
|
||||
pageIds?: string[];
|
||||
zoom?: number;
|
||||
page?: number;
|
||||
}
|
||||
// TODO: URL parameter types will be redesigned for new routing system
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user