V2 Make FileId type opaque and use consistently throughout project (#4307)

# Description of Changes
The `FileId` type in V2 currently is just defined to be a string. This
makes it really easy to accidentally pass strings into things accepting
file IDs (such as file names). This PR makes the `FileId` type [an
opaque
type](https://www.geeksforgeeks.org/typescript/opaque-types-in-typescript/),
so it is compatible with things accepting strings (arguably not ideal
for this...) but strings are not compatible with it without explicit
conversion.

The PR also includes changes to use `FileId` consistently throughout the
project (everywhere I could find uses of `fileId: string`), so that we
have the maximum benefit from the type safety.

> [!note]
> I've marked quite a few things as `FIX ME` where we're passing names
in as IDs. If that is intended behaviour, I'm happy to remove the fix me
and insert a cast instead, but they probably need comments explaining
why we're using a file name as an ID.
This commit is contained in:
James Brunton 2025-08-28 10:56:07 +01:00 committed by GitHub
parent 581bafbd37
commit e142af2863
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 600 additions and 574 deletions

View File

@ -16,6 +16,7 @@ import styles from './FileEditor.module.css';
import FileEditorThumbnail from './FileEditorThumbnail'; import FileEditorThumbnail from './FileEditorThumbnail';
import FilePickerModal from '../shared/FilePickerModal'; import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader'; import SkeletonLoader from '../shared/SkeletonLoader';
import { FileId } from '../../types/file';
interface FileEditorProps { interface FileEditorProps {
@ -88,7 +89,7 @@ const FileEditor = ({
const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : []; const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
// Create refs for frequently changing values to stabilize callbacks // Create refs for frequently changing values to stabilize callbacks
const contextSelectedIdsRef = useRef<string[]>([]); const contextSelectedIdsRef = useRef<FileId[]>([]);
contextSelectedIdsRef.current = contextSelectedIds; contextSelectedIdsRef.current = contextSelectedIds;
// Use activeFileRecords directly - no conversion needed // Use activeFileRecords directly - no conversion needed
@ -166,7 +167,7 @@ const FileEditor = ({
id: operationId, id: operationId,
type: 'convert', type: 'convert',
timestamp: Date.now(), timestamp: Date.now(),
fileIds: extractionResult.extractedFiles.map(f => f.name), fileIds: extractionResult.extractedFiles.map(f => f.name) as FileId[] /* FIX ME: This doesn't seem right */,
status: 'pending', status: 'pending',
metadata: { metadata: {
originalFileName: file.name, originalFileName: file.name,
@ -219,7 +220,7 @@ const FileEditor = ({
id: operationId, id: operationId,
type: 'upload', type: 'upload',
timestamp: Date.now(), timestamp: Date.now(),
fileIds: [file.name], fileIds: [file.name as FileId /* This doesn't seem right */],
status: 'pending', status: 'pending',
metadata: { metadata: {
originalFileName: file.name, originalFileName: file.name,
@ -268,7 +269,7 @@ const FileEditor = ({
setSelectedFiles([]); setSelectedFiles([]);
}, [activeFileRecords, removeFiles, setSelectedFiles]); }, [activeFileRecords, removeFiles, setSelectedFiles]);
const toggleFile = useCallback((fileId: string) => { const toggleFile = useCallback((fileId: FileId) => {
const currentSelectedIds = contextSelectedIdsRef.current; const currentSelectedIds = contextSelectedIdsRef.current;
const targetRecord = activeFileRecords.find(r => r.id === fileId); const targetRecord = activeFileRecords.find(r => r.id === fileId);
@ -277,7 +278,7 @@ const FileEditor = ({
const contextFileId = fileId; // No need to create a new ID const contextFileId = fileId; // No need to create a new ID
const isSelected = currentSelectedIds.includes(contextFileId); const isSelected = currentSelectedIds.includes(contextFileId);
let newSelection: string[]; let newSelection: FileId[];
if (isSelected) { if (isSelected) {
// Remove file from selection // Remove file from selection
@ -314,7 +315,7 @@ const FileEditor = ({
}, [setSelectedFiles]); }, [setSelectedFiles]);
// File reordering handler for drag and drop // File reordering handler for drag and drop
const handleReorderFiles = useCallback((sourceFileId: string, targetFileId: string, selectedFileIds: string[]) => { const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
const currentIds = activeFileRecords.map(r => r.id); const currentIds = activeFileRecords.map(r => r.id);
// Find indices // Find indices
@ -372,7 +373,7 @@ const FileEditor = ({
// File operations using context // File operations using context
const handleDeleteFile = useCallback((fileId: string) => { const handleDeleteFile = useCallback((fileId: FileId) => {
const record = activeFileRecords.find(r => r.id === fileId); const record = activeFileRecords.find(r => r.id === fileId);
const file = record ? selectors.getFile(record.id) : null; const file = record ? selectors.getFile(record.id) : null;
@ -385,7 +386,7 @@ const FileEditor = ({
id: operationId, id: operationId,
type: 'remove', type: 'remove',
timestamp: Date.now(), timestamp: Date.now(),
fileIds: [fileName], fileIds: [fileName as FileId /* FIX ME: This doesn't seem right */],
status: 'pending', status: 'pending',
metadata: { metadata: {
originalFileName: fileName, originalFileName: fileName,
@ -406,7 +407,7 @@ const FileEditor = ({
} }
}, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]); }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
const handleViewFile = useCallback((fileId: string) => { const handleViewFile = useCallback((fileId: FileId) => {
const record = activeFileRecords.find(r => r.id === fileId); const record = activeFileRecords.find(r => r.id === fileId);
if (record) { if (record) {
// Set the file as selected in context and switch to viewer for preview // Set the file as selected in context and switch to viewer for preview
@ -415,7 +416,7 @@ const FileEditor = ({
} }
}, [activeFileRecords, setSelectedFiles, navActions.setMode]); }, [activeFileRecords, setSelectedFiles, navActions.setMode]);
const handleMergeFromHere = useCallback((fileId: string) => { const handleMergeFromHere = useCallback((fileId: FileId) => {
const startIndex = activeFileRecords.findIndex(r => r.id === fileId); const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
if (startIndex === -1) return; if (startIndex === -1) return;
@ -426,14 +427,14 @@ const FileEditor = ({
} }
}, [activeFileRecords, selectors, onMergeFiles]); }, [activeFileRecords, selectors, onMergeFiles]);
const handleSplitFile = useCallback((fileId: string) => { const handleSplitFile = useCallback((fileId: FileId) => {
const file = selectors.getFile(fileId); const file = selectors.getFile(fileId);
if (file && onOpenPageEditor) { if (file && onOpenPageEditor) {
onOpenPageEditor(file); onOpenPageEditor(file);
} }
}, [selectors, onOpenPageEditor]); }, [selectors, onOpenPageEditor]);
const handleLoadFromStorage = useCallback(async (selectedFiles: any[]) => { const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
if (selectedFiles.length === 0) return; if (selectedFiles.length === 0) return;
try { try {

View File

@ -11,9 +11,10 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
import styles from './FileEditor.module.css'; import styles from './FileEditor.module.css';
import { useFileContext } from '../../contexts/FileContext'; import { useFileContext } from '../../contexts/FileContext';
import { FileId } from '../../types/file';
interface FileItem { interface FileItem {
id: string; id: FileId;
name: string; name: string;
pageCount: number; pageCount: number;
thumbnail: string | null; thumbnail: string | null;
@ -25,14 +26,14 @@ interface FileEditorThumbnailProps {
file: FileItem; file: FileItem;
index: number; index: number;
totalFiles: number; totalFiles: number;
selectedFiles: string[]; selectedFiles: FileId[];
selectionMode: boolean; selectionMode: boolean;
onToggleFile: (fileId: string) => void; onToggleFile: (fileId: FileId) => void;
onDeleteFile: (fileId: string) => void; onDeleteFile: (fileId: FileId) => void;
onViewFile: (fileId: string) => void; onViewFile: (fileId: FileId) => void;
onSetStatus: (status: string) => void; onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: string, targetFileId: string, selectedFileIds: string[]) => void; onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
onDownloadFile?: (fileId: string) => void; onDownloadFile?: (fileId: FileId) => void;
toolMode?: boolean; toolMode?: boolean;
isSupported?: boolean; isSupported?: boolean;
} }
@ -161,8 +162,8 @@ const FileEditorThumbnail = ({
onDrop: ({ source }) => { onDrop: ({ source }) => {
const sourceData = source.data; const sourceData = source.data;
if (sourceData.type === 'file' && onReorderFiles) { if (sourceData.type === 'file' && onReorderFiles) {
const sourceFileId = sourceData.fileId as string; const sourceFileId = sourceData.fileId as FileId;
const selectedFileIds = sourceData.selectedFiles as string[]; const selectedFileIds = sourceData.selectedFiles as FileId[];
onReorderFiles(sourceFileId, file.id, selectedFileIds); onReorderFiles(sourceFileId, file.id, selectedFileIds);
} }
} }

View File

@ -14,9 +14,10 @@ import {
// FileContext no longer needed - these were stub functions anyway // FileContext no longer needed - these were stub functions anyway
import { FileOperation, FileOperationHistory as FileOperationHistoryType } from '../../types/fileContext'; import { FileOperation, FileOperationHistory as FileOperationHistoryType } from '../../types/fileContext';
import { PageOperation } from '../../types/pageEditor'; import { PageOperation } from '../../types/pageEditor';
import { FileId } from '../../types/file';
interface FileOperationHistoryProps { interface FileOperationHistoryProps {
fileId: string; fileId: FileId;
showOnlyApplied?: boolean; showOnlyApplied?: boolean;
maxHeight?: number; maxHeight?: number;
} }
@ -27,8 +28,8 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
maxHeight = 400 maxHeight = 400
}) => { }) => {
// These were stub functions in the old context - replace with empty stubs // These were stub functions in the old context - replace with empty stubs
const getFileHistory = (fileId: string) => ({ operations: [], createdAt: Date.now(), lastModified: Date.now() }); const getFileHistory = (fileId: FileId) => ({ operations: [], createdAt: Date.now(), lastModified: Date.now() });
const getAppliedOperations = (fileId: string) => []; const getAppliedOperations = (fileId: FileId) => [];
const history = getFileHistory(fileId); const history = getFileHistory(fileId);
const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || []; const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];

View File

@ -11,9 +11,10 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
import styles from './PageEditor.module.css'; import styles from './PageEditor.module.css';
import { useFileContext } from '../../contexts/FileContext'; import { useFileContext } from '../../contexts/FileContext';
import { FileId } from '../../types/file';
interface FileItem { interface FileItem {
id: string; id: FileId;
name: string; name: string;
pageCount: number; pageCount: number;
thumbnail: string | null; thumbnail: string | null;
@ -27,12 +28,12 @@ interface FileThumbnailProps {
totalFiles: number; totalFiles: number;
selectedFiles: string[]; selectedFiles: string[];
selectionMode: boolean; selectionMode: boolean;
onToggleFile: (fileId: string) => void; onToggleFile: (fileId: FileId) => void;
onDeleteFile: (fileId: string) => void; onDeleteFile: (fileId: FileId) => void;
onViewFile: (fileId: string) => void; onViewFile: (fileId: FileId) => void;
onSetStatus: (status: string) => void; onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: string, targetFileId: string, selectedFileIds: string[]) => void; onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
onDownloadFile?: (fileId: string) => void; onDownloadFile?: (fileId: FileId) => void;
toolMode?: boolean; toolMode?: boolean;
isSupported?: boolean; isSupported?: boolean;
} }
@ -161,8 +162,8 @@ const FileThumbnail = ({
onDrop: ({ source }) => { onDrop: ({ source }) => {
const sourceData = source.data; const sourceData = source.data;
if (sourceData.type === 'file' && onReorderFiles) { if (sourceData.type === 'file' && onReorderFiles) {
const sourceFileId = sourceData.fileId as string; const sourceFileId = sourceData.fileId as FileId;
const selectedFileIds = sourceData.selectedFiles as string[]; const selectedFileIds = sourceData.selectedFiles as FileId[];
onReorderFiles(sourceFileId, file.id, selectedFileIds); onReorderFiles(sourceFileId, file.id, selectedFileIds);
} }
} }

View File

@ -17,6 +17,7 @@ import PageThumbnail from './PageThumbnail';
import DragDropGrid from './DragDropGrid'; import DragDropGrid from './DragDropGrid';
import SkeletonLoader from '../shared/SkeletonLoader'; import SkeletonLoader from '../shared/SkeletonLoader';
import NavigationWarningModal from '../shared/NavigationWarningModal'; import NavigationWarningModal from '../shared/NavigationWarningModal';
import { FileId } from "../../types/file";
import { import {
DOMCommand, DOMCommand,
@ -173,6 +174,7 @@ const PageEditor = ({
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({ const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
execute: () => { execute: () => {
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation); const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
undoManagerRef.current.executeCommand(bulkRotateCommand); undoManagerRef.current.executeCommand(bulkRotateCommand);
} }
}), []); }), []);
@ -182,6 +184,7 @@ const PageEditor = ({
if (!displayDocument) return; if (!displayDocument) return;
const pagesToDelete = pageIds.map(pageId => { const pagesToDelete = pageIds.map(pageId => {
const page = displayDocument.pages.find(p => p.id === pageId); const page = displayDocument.pages.find(p => p.id === pageId);
return page?.pageNumber || 0; return page?.pageNumber || 0;
}).filter(num => num > 0); }).filter(num => num > 0);
@ -443,8 +446,8 @@ const PageEditor = ({
}, [displayDocument, getPageNumbersFromIds]); }, [displayDocument, getPageNumbersFromIds]);
// Helper function to collect source files for multi-file export // Helper function to collect source files for multi-file export
const getSourceFiles = useCallback((): Map<string, File> | null => { const getSourceFiles = useCallback((): Map<FileId, File> | null => {
const sourceFiles = new Map<string, File>(); const sourceFiles = new Map<FileId, File>();
// Always include original files // Always include original files
activeFileIds.forEach(fileId => { activeFileIds.forEach(fileId => {
@ -622,6 +625,7 @@ const PageEditor = ({
const closePdf = useCallback(() => { const closePdf = useCallback(() => {
actions.clearAllFiles(); actions.clearAllFiles();
undoManagerRef.current.clear(); undoManagerRef.current.clear();
setSelectedPageIds([]); setSelectedPageIds([]);
setSelectionMode(false); setSelectionMode(false);

View File

@ -1,3 +1,4 @@
import { FileId } from '../../../types/file';
import { PDFDocument, PDFPage } from '../../../types/pageEditor'; import { PDFDocument, PDFPage } from '../../../types/pageEditor';
// V1-style DOM-first command system (replaces the old React state commands) // V1-style DOM-first command system (replaces the old React state commands)
@ -558,9 +559,9 @@ export class BulkPageBreakCommand extends DOMCommand {
export class InsertFilesCommand extends DOMCommand { export class InsertFilesCommand extends DOMCommand {
private insertedPages: PDFPage[] = []; private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null; private originalDocument: PDFDocument | null = null;
private fileDataMap = new Map<string, ArrayBuffer>(); // Store file data for thumbnail generation private fileDataMap = new Map<FileId, ArrayBuffer>(); // Store file data for thumbnail generation
private originalProcessedFile: any = null; // Store original ProcessedFile for undo private originalProcessedFile: any = null; // Store original ProcessedFile for undo
private insertedFileMap = new Map<string, File>(); // Store inserted files for export private insertedFileMap = new Map<FileId, File>(); // Store inserted files for export
constructor( constructor(
private files: File[], private files: File[],
@ -569,7 +570,7 @@ export class InsertFilesCommand extends DOMCommand {
private setDocument: (doc: PDFDocument) => void, private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void, private setSelectedPages: (pages: number[]) => void,
private getSelectedPages: () => number[], private getSelectedPages: () => number[],
private updateFileContext?: (updatedDocument: PDFDocument, insertedFiles?: Map<string, File>) => void private updateFileContext?: (updatedDocument: PDFDocument, insertedFiles?: Map<FileId, File>) => void
) { ) {
super(); super();
} }
@ -591,7 +592,7 @@ export class InsertFilesCommand extends DOMCommand {
// Process all files and wait for their completion // Process all files and wait for their completion
const baseTimestamp = Date.now(); const baseTimestamp = Date.now();
const extractionPromises = this.files.map(async (file, index) => { const extractionPromises = this.files.map(async (file, index) => {
const fileId = `inserted-${file.name}-${baseTimestamp + index}`; const fileId = `inserted-${file.name}-${baseTimestamp + index}` as FileId;
// Store inserted file for export // Store inserted file for export
this.insertedFileMap.set(fileId, file); this.insertedFileMap.set(fileId, file);
// Use base timestamp + index to ensure unique but predictable file IDs // Use base timestamp + index to ensure unique but predictable file IDs
@ -685,10 +686,10 @@ export class InsertFilesCommand extends DOMCommand {
const { thumbnailGenerationService } = await import('../../../services/thumbnailGenerationService'); const { thumbnailGenerationService } = await import('../../../services/thumbnailGenerationService');
// Group pages by file ID to generate thumbnails efficiently // Group pages by file ID to generate thumbnails efficiently
const pagesByFileId = new Map<string, PDFPage[]>(); const pagesByFileId = new Map<FileId, PDFPage[]>();
for (const page of this.insertedPages) { for (const page of this.insertedPages) {
const fileId = page.id.substring(0, page.id.lastIndexOf('-page-')); const fileId = page.id.substring(0, page.id.lastIndexOf('-page-')) as FileId /* FIX ME: This looks wrong - like we've thrown away info too early and need to recreate it */;
if (!pagesByFileId.has(fileId)) { if (!pagesByFileId.has(fileId)) {
pagesByFileId.set(fileId, []); pagesByFileId.set(fileId, []);
} }
@ -769,7 +770,7 @@ export class InsertFilesCommand extends DOMCommand {
const pageCount = pdf.numPages; const pageCount = pdf.numPages;
const pages: PDFPage[] = []; const pages: PDFPage[] = [];
const fileId = `inserted-${file.name}-${baseTimestamp}`; const fileId = `inserted-${file.name}-${baseTimestamp}` as FileId;
console.log('Original ArrayBuffer size:', arrayBuffer.byteLength); console.log('Original ArrayBuffer size:', arrayBuffer.byteLength);
console.log('Storing ArrayBuffer for fileId:', fileId, 'size:', arrayBuffer.byteLength); console.log('Storing ArrayBuffer for fileId:', fileId, 'size:', arrayBuffer.byteLength);

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useFileState } from '../../../contexts/FileContext'; import { useFileState } from '../../../contexts/FileContext';
import { PDFDocument, PDFPage } from '../../../types/pageEditor'; import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '../../../types/file';
export interface PageDocumentHook { export interface PageDocumentHook {
document: PDFDocument | null; document: PDFDocument | null;
@ -50,8 +51,8 @@ export function usePageDocument(): PageDocumentHook {
.join(' + '); .join(' + ');
// Build page insertion map from files with insertion positions // Build page insertion map from files with insertion positions
const insertionMap = new Map<string, string[]>(); // insertAfterPageId -> fileIds const insertionMap = new Map<string, FileId[]>(); // insertAfterPageId -> fileIds
const originalFileIds: string[] = []; const originalFileIds: FileId[] = [];
activeFileIds.forEach(fileId => { activeFileIds.forEach(fileId => {
const record = selectors.getFileRecord(fileId); const record = selectors.getFileRecord(fileId);
@ -70,7 +71,7 @@ export function usePageDocument(): PageDocumentHook {
let totalPageCount = 0; let totalPageCount = 0;
// Helper function to create pages from a file // Helper function to create pages from a file
const createPagesFromFile = (fileId: string, startPageNumber: number): PDFPage[] => { const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
const fileRecord = selectors.getFileRecord(fileId); const fileRecord = selectors.getFileRecord(fileId);
if (!fileRecord) { if (!fileRecord) {
return []; return [];

View File

@ -5,6 +5,7 @@ import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort"; import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard"; import FileCard from "./FileCard";
import { FileRecord } from "../../types/fileContext"; import { FileRecord } from "../../types/fileContext";
import { FileId } from "../../types/file";
interface FileGridProps { interface FileGridProps {
files: Array<{ file: File; record?: FileRecord }>; files: Array<{ file: File; record?: FileRecord }>;
@ -12,8 +13,8 @@ interface FileGridProps {
onDoubleClick?: (item: { file: File; record?: FileRecord }) => void; onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
onView?: (item: { file: File; record?: FileRecord }) => void; onView?: (item: { file: File; record?: FileRecord }) => void;
onEdit?: (item: { file: File; record?: FileRecord }) => void; onEdit?: (item: { file: File; record?: FileRecord }) => void;
onSelect?: (fileId: string) => void; onSelect?: (fileId: FileId) => void;
selectedFiles?: string[]; selectedFiles?: FileId[];
showSearch?: boolean; showSearch?: boolean;
showSort?: boolean; showSort?: boolean;
maxDisplay?: number; // If set, shows only this many files with "Show All" option maxDisplay?: number; // If set, shows only this many files with "Show All" option
@ -123,7 +124,7 @@ const FileGrid = ({
style={{ overflowY: "auto", width: "100%" }} style={{ overflowY: "auto", width: "100%" }}
> >
{displayFiles.map((item, idx) => { {displayFiles.map((item, idx) => {
const fileId = item.record?.id || item.file.name; const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */;
const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId); const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true; const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return ( return (

View File

@ -15,6 +15,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FileId } from '../../types/file';
interface FilePickerModalProps { interface FilePickerModalProps {
opened: boolean; opened: boolean;
@ -30,7 +31,7 @@ const FilePickerModal = ({
onSelectFiles, onSelectFiles,
}: FilePickerModalProps) => { }: FilePickerModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]); const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
// Reset selection when modal opens // Reset selection when modal opens
useEffect(() => { useEffect(() => {
@ -39,7 +40,7 @@ const FilePickerModal = ({
} }
}, [opened]); }, [opened]);
const toggleFileSelection = (fileId: string) => { const toggleFileSelection = (fileId: FileId) => {
setSelectedFileIds(prev => { setSelectedFileIds(prev => {
return prev.includes(fileId) return prev.includes(fileId)
? prev.filter(id => id !== fileId) ? prev.filter(id => id !== fileId)

View File

@ -22,6 +22,7 @@ import {
OUTPUT_OPTIONS, OUTPUT_OPTIONS,
FIT_OPTIONS FIT_OPTIONS
} from "../../../constants/convertConstants"; } from "../../../constants/convertConstants";
import { FileId } from "../../../types/file";
interface ConvertSettingsProps { interface ConvertSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
@ -155,7 +156,7 @@ const ConvertSettings = ({
record.lastModified === file.lastModified record.lastModified === file.lastModified
); );
return fileRecord?.id; return fileRecord?.id;
}).filter((id): id is string => id !== undefined); // Type guard to ensure only strings }).filter((id): id is FileId => id !== undefined); // Type guard to ensure only strings
setSelectedFiles(fileIds); setSelectedFiles(fileIds);
}; };

View File

@ -15,6 +15,7 @@ import { fileStorage } from "../../services/fileStorage";
import SkeletonLoader from '../shared/SkeletonLoader'; import SkeletonLoader from '../shared/SkeletonLoader';
import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext"; import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl"; import { useFileWithUrl } from "../../hooks/useFileWithUrl";
import { FileId } from "../../types/file";
// Lazy loading page image component // Lazy loading page image component
@ -378,7 +379,7 @@ const Viewer = ({
} }
// Handle special IndexedDB URLs for large files // Handle special IndexedDB URLs for large files
else if (effectiveFile.url?.startsWith('indexeddb:')) { else if (effectiveFile.url?.startsWith('indexeddb:')) {
const fileId = effectiveFile.url.replace('indexeddb:', ''); const fileId = effectiveFile.url.replace('indexeddb:', '') as FileId /* FIX ME: Not sure this is right - at least probably not the right place for this logic */;
// Get data directly from IndexedDB // Get data directly from IndexedDB
const arrayBuffer = await fileStorage.getFileData(fileId); const arrayBuffer = await fileStorage.getFileData(fileId);

View File

@ -19,7 +19,6 @@ import {
FileContextStateValue, FileContextStateValue,
FileContextActionsValue, FileContextActionsValue,
FileContextActions, FileContextActions,
FileId,
FileRecord FileRecord
} from '../types/fileContext'; } from '../types/fileContext';
@ -30,6 +29,7 @@ import { addFiles, consumeFiles, createFileActions } from './file/fileActions';
import { FileLifecycleManager } from './file/lifecycle'; import { FileLifecycleManager } from './file/lifecycle';
import { FileStateContext, FileActionsContext } from './file/contexts'; import { FileStateContext, FileActionsContext } from './file/contexts';
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext'; import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
import { FileId } from '../types/file';
const DEBUG = process.env.NODE_ENV === 'development'; const DEBUG = process.env.NODE_ENV === 'development';
@ -75,7 +75,6 @@ function FileContextInner({
// File operations using unified addFiles helper with persistence // File operations using unified addFiles helper with persistence
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise<File[]> => { const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise<File[]> => {
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager); const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
// Persist to IndexedDB if enabled // Persist to IndexedDB if enabled
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) { if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => { await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => {
@ -110,7 +109,7 @@ function FileContextInner({
// Helper to find FileId from File object // Helper to find FileId from File object
const findFileId = useCallback((file: File): FileId | undefined => { const findFileId = useCallback((file: File): FileId | undefined => {
return Object.keys(stateRef.current.files.byId).find(id => { return (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
const storedFile = filesRef.current.get(id); const storedFile = filesRef.current.get(id);
return storedFile && return storedFile &&
storedFile.name === file.name && storedFile.name === file.name &&
@ -191,8 +190,8 @@ function FileContextInner({
consumeFiles: consumeFilesWrapper, consumeFiles: consumeFilesWrapper,
setHasUnsavedChanges, setHasUnsavedChanges,
trackBlobUrl: lifecycleManager.trackBlobUrl, trackBlobUrl: lifecycleManager.trackBlobUrl,
cleanupFile: (fileId: string) => lifecycleManager.cleanupFile(fileId, stateRef), cleanupFile: (fileId: FileId) => lifecycleManager.cleanupFile(fileId, stateRef),
scheduleCleanup: (fileId: string, delay?: number) => scheduleCleanup: (fileId: FileId, delay?: number) =>
lifecycleManager.scheduleCleanup(fileId, delay, stateRef) lifecycleManager.scheduleCleanup(fileId, delay, stateRef)
}), [ }), [
baseActions, baseActions,

View File

@ -2,12 +2,13 @@ import React, { createContext, useContext, useState, useRef, useCallback, useEff
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { StoredFile, fileStorage } from '../services/fileStorage'; import { StoredFile, fileStorage } from '../services/fileStorage';
import { downloadFiles } from '../utils/downloadUtils'; import { downloadFiles } from '../utils/downloadUtils';
import { FileId } from '../types/file';
// Type for the context value - now contains everything directly // Type for the context value - now contains everything directly
interface FileManagerContextValue { interface FileManagerContextValue {
// State // State
activeSource: 'recent' | 'local' | 'drive'; activeSource: 'recent' | 'local' | 'drive';
selectedFileIds: string[]; selectedFileIds: FileId[];
searchTerm: string; searchTerm: string;
selectedFiles: FileMetadata[]; selectedFiles: FileMetadata[];
filteredFiles: FileMetadata[]; filteredFiles: FileMetadata[];
@ -64,7 +65,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
refreshRecentFiles, refreshRecentFiles,
}) => { }) => {
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent'); const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]); const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null); const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);

View File

@ -1,6 +1,7 @@
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react'; import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
import { useFileHandler } from '../hooks/useFileHandler'; import { useFileHandler } from '../hooks/useFileHandler';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { FileId } from '../types/file';
interface FilesModalContextType { interface FilesModalContextType {
isFilesModalOpen: boolean; isFilesModalOpen: boolean;
@ -8,7 +9,7 @@ interface FilesModalContextType {
closeFilesModal: () => void; closeFilesModal: () => void;
onFileSelect: (file: File) => void; onFileSelect: (file: File) => void;
onFilesSelect: (files: File[]) => void; onFilesSelect: (files: File[]) => void;
onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => void; onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => void;
onModalClose?: () => void; onModalClose?: () => void;
setOnModalClose: (callback: () => void) => void; setOnModalClose: (callback: () => void) => void;
} }
@ -57,7 +58,7 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
closeFilesModal(); closeFilesModal();
}, [addMultipleFiles, closeFilesModal, insertAfterPage, customHandler]); }, [addMultipleFiles, closeFilesModal, insertAfterPage, customHandler]);
const handleStoredFilesSelect = useCallback((filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => { const handleStoredFilesSelect = useCallback((filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => {
if (customHandler) { if (customHandler) {
// Use custom handler for special cases (like page insertion) // Use custom handler for special cases (like page insertion)
const files = filesWithMetadata.map(item => item.file); const files = filesWithMetadata.map(item => item.file);

View File

@ -7,7 +7,7 @@ import React, { createContext, useContext, useCallback, useRef } from 'react';
const DEBUG = process.env.NODE_ENV === 'development'; const DEBUG = process.env.NODE_ENV === 'development';
import { fileStorage, StoredFile } from '../services/fileStorage'; import { fileStorage, StoredFile } from '../services/fileStorage';
import { FileId } from '../types/fileContext'; import { FileId } from '../types/file';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { generateThumbnailForFile } from '../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../utils/thumbnailUtils';

View File

@ -2,10 +2,10 @@
* FileContext reducer - Pure state management for file operations * FileContext reducer - Pure state management for file operations
*/ */
import { FileId } from '../../types/file';
import { import {
FileContextState, FileContextState,
FileContextAction, FileContextAction,
FileId,
FileRecord FileRecord
} from '../../types/fileContext'; } from '../../types/fileContext';

View File

@ -3,7 +3,6 @@
*/ */
import { import {
FileId,
FileRecord, FileRecord,
FileContextAction, FileContextAction,
FileContextState, FileContextState,
@ -11,7 +10,7 @@ import {
createFileId, createFileId,
createQuickKey createQuickKey
} from '../../types/fileContext'; } from '../../types/fileContext';
import { FileMetadata } from '../../types/file'; import { FileId, FileMetadata } from '../../types/file';
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils'; import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
import { FileLifecycleManager } from './lifecycle'; import { FileLifecycleManager } from './lifecycle';
import { fileProcessingService } from '../../services/fileProcessingService'; import { fileProcessingService } from '../../services/fileProcessingService';

View File

@ -9,7 +9,8 @@ import {
FileContextStateValue, FileContextStateValue,
FileContextActionsValue FileContextActionsValue
} from './contexts'; } from './contexts';
import { FileId, FileRecord } from '../../types/fileContext'; import { FileRecord } from '../../types/fileContext';
import { FileId } from '../../types/file';
/** /**
* Hook for accessing file state (will re-render on any state change) * Hook for accessing file state (will re-render on any state change)
@ -164,9 +165,9 @@ export function useFileContext() {
// File management // File management
addFiles: actions.addFiles, addFiles: actions.addFiles,
consumeFiles: actions.consumeFiles, consumeFiles: actions.consumeFiles,
recordOperation: (fileId: string, operation: any) => {}, // Operation tracking not implemented recordOperation: (fileId: FileId, operation: any) => {}, // Operation tracking not implemented
markOperationApplied: (fileId: string, operationId: string) => {}, // Operation tracking not implemented markOperationApplied: (fileId: FileId, operationId: string) => {}, // Operation tracking not implemented
markOperationFailed: (fileId: string, operationId: string, error: string) => {}, // Operation tracking not implemented markOperationFailed: (fileId: FileId, operationId: string, error: string) => {}, // Operation tracking not implemented
// File ID lookup // File ID lookup
findFileId: (file: File) => { findFileId: (file: File) => {

View File

@ -2,8 +2,8 @@
* File selectors - Pure functions for accessing file state * File selectors - Pure functions for accessing file state
*/ */
import { FileId } from '../../types/file';
import { import {
FileId,
FileRecord, FileRecord,
FileContextState, FileContextState,
FileContextSelectors FileContextSelectors
@ -64,7 +64,7 @@ export function createFileSelectors(
isFilePinned: (file: File) => { isFilePinned: (file: File) => {
// Find FileId by matching File object properties // Find FileId by matching File object properties
const fileId = Object.keys(stateRef.current.files.byId).find(id => { const fileId = (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => {
const storedFile = filesRef.current.get(id); const storedFile = filesRef.current.get(id);
return storedFile && return storedFile &&
storedFile.name === file.name && storedFile.name === file.name &&

View File

@ -2,7 +2,8 @@
* File lifecycle management - Resource cleanup and memory management * File lifecycle management - Resource cleanup and memory management
*/ */
import { FileId, FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext'; import { FileId } from '../../types/file';
import { FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext';
const DEBUG = process.env.NODE_ENV === 'development'; const DEBUG = process.env.NODE_ENV === 'development';
@ -33,7 +34,7 @@ export class FileLifecycleManager {
/** /**
* Clean up resources for a specific file (with stateRef access for complete cleanup) * Clean up resources for a specific file (with stateRef access for complete cleanup)
*/ */
cleanupFile = (fileId: string, stateRef?: React.MutableRefObject<any>): void => { cleanupFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
// Use comprehensive cleanup (same as removeFiles) // Use comprehensive cleanup (same as removeFiles)
this.cleanupAllResourcesForFile(fileId, stateRef); this.cleanupAllResourcesForFile(fileId, stateRef);
@ -67,7 +68,7 @@ export class FileLifecycleManager {
/** /**
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup * Schedule delayed cleanup for a file with generation token to prevent stale cleanup
*/ */
scheduleCleanup = (fileId: string, delay: number = 30000, stateRef?: React.MutableRefObject<any>): void => { scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.MutableRefObject<any>): void => {
// Cancel existing timer // Cancel existing timer
const existingTimer = this.cleanupTimers.get(fileId); const existingTimer = this.cleanupTimers.get(fileId);
if (existingTimer) { if (existingTimer) {

View File

@ -8,6 +8,7 @@ import { useToolResources } from './useToolResources';
import { extractErrorMessage } from '../../../utils/toolErrorHandler'; import { extractErrorMessage } from '../../../utils/toolErrorHandler';
import { createOperation } from '../../../utils/toolOperationTracker'; import { createOperation } from '../../../utils/toolOperationTracker';
import { ResponseHandler } from '../../../utils/toolResponseProcessor'; import { ResponseHandler } from '../../../utils/toolResponseProcessor';
import { FileId } from '../../../types/file';
// Re-export for backwards compatibility // Re-export for backwards compatibility
export type { ProcessingProgress, ResponseHandler }; export type { ProcessingProgress, ResponseHandler };
@ -231,7 +232,7 @@ export const useToolOperation = <TParams>(
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename); actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
// Replace input files with processed files (consumeFiles handles pinning) // Replace input files with processed files (consumeFiles handles pinning)
const inputFileIds = validFiles.map(file => findFileId(file)).filter(Boolean) as string[]; const inputFileIds = validFiles.map(file => findFileId(file)).filter(Boolean) as FileId[];
await consumeFiles(inputFileIds, processedFiles); await consumeFiles(inputFileIds, processedFiles);
markOperationApplied(fileId, operationId); markOperationApplied(fileId, operationId);

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useFileState, useFileActions } from '../contexts/FileContext'; import { useFileState, useFileActions } from '../contexts/FileContext';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { FileId } from '../types/file';
export const useFileHandler = () => { export const useFileHandler = () => {
const { state } = useFileState(); // Still needed for addStoredFiles const { state } = useFileState(); // Still needed for addStoredFiles
@ -17,7 +18,7 @@ export const useFileHandler = () => {
}, [actions.addFiles]); }, [actions.addFiles]);
// Add stored files preserving their original IDs to prevent session duplicates // Add stored files preserving their original IDs to prevent session duplicates
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => { const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => {
// Filter out files that already exist with the same ID (exact match) // Filter out files that already exist with the same ID (exact match)
const newFiles = filesWithMetadata.filter(({ originalId }) => { const newFiles = filesWithMetadata.filter(({ originalId }) => {
return state.files.byId[originalId] === undefined; return state.files.byId[originalId] === undefined;

View File

@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
import { useIndexedDB } from '../contexts/IndexedDBContext'; import { useIndexedDB } from '../contexts/IndexedDBContext';
import { FileMetadata } from '../types/file'; import { FileMetadata } from '../types/file';
import { generateThumbnailForFile } from '../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../utils/thumbnailUtils';
import { FileId } from '../types/file';
export const useFileManager = () => { export const useFileManager = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -94,7 +95,7 @@ export const useFileManager = () => {
} }
}, [indexedDB]); }, [indexedDB]);
const storeFile = useCallback(async (file: File, fileId: string) => { const storeFile = useCallback(async (file: File, fileId: FileId) => {
if (!indexedDB) { if (!indexedDB) {
throw new Error('IndexedDB context not available'); throw new Error('IndexedDB context not available');
} }
@ -122,10 +123,10 @@ export const useFileManager = () => {
}, [indexedDB]); }, [indexedDB]);
const createFileSelectionHandlers = useCallback(( const createFileSelectionHandlers = useCallback((
selectedFiles: string[], selectedFiles: FileId[],
setSelectedFiles: (files: string[]) => void setSelectedFiles: (files: FileId[]) => void
) => { ) => {
const toggleSelection = (fileId: string) => { const toggleSelection = (fileId: FileId) => {
setSelectedFiles( setSelectedFiles(
selectedFiles.includes(fileId) selectedFiles.includes(fileId)
? selectedFiles.filter(id => id !== fileId) ? selectedFiles.filter(id => id !== fileId)
@ -137,7 +138,7 @@ export const useFileManager = () => {
setSelectedFiles([]); setSelectedFiles([]);
}; };
const selectMultipleFiles = async (files: FileMetadata[], onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: string; metadata: FileMetadata }>) => void) => { const selectMultipleFiles = async (files: FileMetadata[], onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => void) => {
if (selectedFiles.length === 0) return; if (selectedFiles.length === 0) return;
try { try {
@ -168,7 +169,7 @@ export const useFileManager = () => {
}; };
}, [convertToFile]); }, [convertToFile]);
const touchFile = useCallback(async (id: string) => { const touchFile = useCallback(async (id: FileId) => {
if (!indexedDB) { if (!indexedDB) {
console.warn('IndexedDB context not available for touch operation'); console.warn('IndexedDB context not available for touch operation');
return; return;

View File

@ -1,5 +1,6 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { thumbnailGenerationService } from '../services/thumbnailGenerationService'; import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
import { FileId } from '../types/file';
// Request queue to handle concurrent thumbnail requests // Request queue to handle concurrent thumbnail requests
interface ThumbnailRequest { interface ThumbnailRequest {
@ -71,7 +72,7 @@ async function processRequestQueue() {
console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`); console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
// Use file name as fileId for PDF document caching // Use file name as fileId for PDF document caching
const fileId = file.name + '_' + file.size + '_' + file.lastModified; const fileId = file.name + '_' + file.size + '_' + file.lastModified as FileId;
const results = await thumbnailGenerationService.generateThumbnails( const results = await thumbnailGenerationService.generateThumbnails(
fileId, fileId,
@ -115,7 +116,7 @@ async function processRequestQueue() {
*/ */
export function useThumbnailGeneration() { export function useThumbnailGeneration() {
const generateThumbnails = useCallback(async ( const generateThumbnails = useCallback(async (
fileId: string, fileId: FileId,
pdfArrayBuffer: ArrayBuffer, pdfArrayBuffer: ArrayBuffer,
pageNumbers: number[], pageNumbers: number[],
options: { options: {
@ -166,7 +167,7 @@ export function useThumbnailGeneration() {
thumbnailGenerationService.destroy(); thumbnailGenerationService.destroy();
}, []); }, []);
const clearPDFCacheForFile = useCallback((fileId: string) => { const clearPDFCacheForFile = useCallback((fileId: FileId) => {
thumbnailGenerationService.clearPDFCacheForFile(fileId); thumbnailGenerationService.clearPDFCacheForFile(fileId);
}, []); }, []);

View File

@ -3,19 +3,20 @@ import { useTranslation } from 'react-i18next';
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy"; import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { FileId } from '../types/file';
interface ToolManagementResult { interface ToolManagementResult {
selectedTool: ToolRegistryEntry | null; selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: string[]; toolSelectedFileIds: FileId[];
toolRegistry: Record<string, ToolRegistryEntry>; toolRegistry: Record<string, ToolRegistryEntry>;
setToolSelectedFileIds: (fileIds: string[]) => void; setToolSelectedFileIds: (fileIds: FileId[]) => void;
getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null; getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null;
} }
export const useToolManagement = (): ToolManagementResult => { export const useToolManagement = (): ToolManagementResult => {
const { t } = useTranslation(); const { t } = useTranslation();
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]); const [toolSelectedFileIds, setToolSelectedFileIds] = useState<FileId[]>([]);
// Build endpoints list from registry entries with fallback to legacy mapping // Build endpoints list from registry entries with fallback to legacy mapping
const baseRegistry = useFlatToolRegistry(); const baseRegistry = useFlatToolRegistry();

View File

@ -7,6 +7,7 @@
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { generateThumbnailForFile } from '../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../utils/thumbnailUtils';
import { pdfWorkerManager } from './pdfWorkerManager'; import { pdfWorkerManager } from './pdfWorkerManager';
import { FileId } from '../types/file';
export interface ProcessedFileMetadata { export interface ProcessedFileMetadata {
totalPages: number; totalPages: number;
@ -38,7 +39,7 @@ class FileProcessingService {
* Process a file to extract metadata, page count, and generate thumbnails * Process a file to extract metadata, page count, and generate thumbnails
* This is the single source of truth for file processing * This is the single source of truth for file processing
*/ */
async processFile(file: File, fileId: string): Promise<FileProcessingResult> { async processFile(file: File, fileId: FileId): Promise<FileProcessingResult> {
// Check if we're already processing this file // Check if we're already processing this file
const existingOperation = this.processingCache.get(fileId); const existingOperation = this.processingCache.get(fileId);
if (existingOperation) { if (existingOperation) {
@ -67,7 +68,7 @@ class FileProcessingService {
return processingPromise; return processingPromise;
} }
private async performProcessing(file: File, fileId: string, abortController: AbortController): Promise<FileProcessingResult> { private async performProcessing(file: File, fileId: FileId, abortController: AbortController): Promise<FileProcessingResult> {
console.log(`📁 FileProcessingService: Starting processing for ${file.name} (${fileId})`); console.log(`📁 FileProcessingService: Starting processing for ${file.name} (${fileId})`);
try { try {
@ -167,14 +168,14 @@ class FileProcessingService {
/** /**
* Check if a file is currently being processed * Check if a file is currently being processed
*/ */
isProcessing(fileId: string): boolean { isProcessing(fileId: FileId): boolean {
return this.processingCache.has(fileId); return this.processingCache.has(fileId);
} }
/** /**
* Cancel processing for a specific file * Cancel processing for a specific file
*/ */
cancelProcessing(fileId: string): boolean { cancelProcessing(fileId: FileId): boolean {
const operation = this.processingCache.get(fileId); const operation = this.processingCache.get(fileId);
if (operation) { if (operation) {
operation.abortController.abort(); operation.abortController.abort();

View File

@ -4,10 +4,11 @@
* Now uses centralized IndexedDB manager * Now uses centralized IndexedDB manager
*/ */
import { FileId } from '../types/file';
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager'; import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
export interface StoredFile { export interface StoredFile {
id: string; id: FileId;
name: string; name: string;
type: string; type: string;
size: number; size: number;
@ -38,7 +39,7 @@ class FileStorageService {
/** /**
* Store a file in IndexedDB with external UUID * Store a file in IndexedDB with external UUID
*/ */
async storeFile(file: File, fileId: string, thumbnail?: string): Promise<StoredFile> { async storeFile(file: File, fileId: FileId, thumbnail?: string): Promise<StoredFile> {
const db = await this.getDatabase(); const db = await this.getDatabase();
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
@ -88,7 +89,7 @@ class FileStorageService {
/** /**
* Retrieve a file from IndexedDB * Retrieve a file from IndexedDB
*/ */
async getFile(id: string): Promise<StoredFile | null> { async getFile(id: FileId): Promise<StoredFile | null> {
const db = await this.getDatabase(); const db = await this.getDatabase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -166,7 +167,7 @@ class FileStorageService {
/** /**
* Delete a file from IndexedDB * Delete a file from IndexedDB
*/ */
async deleteFile(id: string): Promise<void> { async deleteFile(id: FileId): Promise<void> {
const db = await this.getDatabase(); const db = await this.getDatabase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -182,7 +183,7 @@ class FileStorageService {
/** /**
* Update the lastModified timestamp of a file (for most recently used sorting) * Update the lastModified timestamp of a file (for most recently used sorting)
*/ */
async touchFile(id: string): Promise<boolean> { async touchFile(id: FileId): Promise<boolean> {
const db = await this.getDatabase(); const db = await this.getDatabase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite'); const transaction = db.transaction([this.storeName], 'readwrite');
@ -438,7 +439,7 @@ class FileStorageService {
* Convert StoredFile to the format expected by FileContext.addStoredFiles() * Convert StoredFile to the format expected by FileContext.addStoredFiles()
* This is the recommended way to load stored files into FileContext * This is the recommended way to load stored files into FileContext
*/ */
createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: string; metadata: { thumbnail?: string } } { createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: FileId; metadata: { thumbnail?: string } } {
const file = this.createFileFromStored(storedFile); const file = this.createFileFromStored(storedFile);
return { return {
@ -461,7 +462,7 @@ class FileStorageService {
/** /**
* Get file data as ArrayBuffer for streaming/chunked processing * Get file data as ArrayBuffer for streaming/chunked processing
*/ */
async getFileData(id: string): Promise<ArrayBuffer | null> { async getFileData(id: FileId): Promise<ArrayBuffer | null> {
try { try {
const storedFile = await this.getFile(id); const storedFile = await this.getFile(id);
return storedFile ? storedFile.data : null; return storedFile ? storedFile.data : null;
@ -474,7 +475,7 @@ class FileStorageService {
/** /**
* Create a temporary blob URL that gets revoked automatically * Create a temporary blob URL that gets revoked automatically
*/ */
async createTemporaryBlobUrl(id: string): Promise<string | null> { async createTemporaryBlobUrl(id: FileId): Promise<string | null> {
const data = await this.getFileData(id); const data = await this.getFileData(id);
if (!data) return null; if (!data) return null;
@ -492,7 +493,7 @@ class FileStorageService {
/** /**
* Update thumbnail for an existing file * Update thumbnail for an existing file
*/ */
async updateThumbnail(id: string, thumbnail: string): Promise<boolean> { async updateThumbnail(id: FileId, thumbnail: string): Promise<boolean> {
const db = await this.getDatabase(); const db = await this.getDatabase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -2,6 +2,7 @@
* High-performance thumbnail generation service using main thread processing * High-performance thumbnail generation service using main thread processing
*/ */
import { FileId } from '../types/file';
import { pdfWorkerManager } from './pdfWorkerManager'; import { pdfWorkerManager } from './pdfWorkerManager';
interface ThumbnailResult { interface ThumbnailResult {
@ -32,12 +33,12 @@ interface CachedPDFDocument {
export class ThumbnailGenerationService { export class ThumbnailGenerationService {
// Session-based thumbnail cache // Session-based thumbnail cache
private thumbnailCache = new Map<string, CachedThumbnail>(); private thumbnailCache = new Map<FileId | string /* FIX ME: Page ID */, CachedThumbnail>();
private maxCacheSizeBytes = 1024 * 1024 * 1024; // 1GB cache limit private maxCacheSizeBytes = 1024 * 1024 * 1024; // 1GB cache limit
private currentCacheSize = 0; private currentCacheSize = 0;
// PDF document cache to reuse PDF instances and avoid creating multiple workers // PDF document cache to reuse PDF instances and avoid creating multiple workers
private pdfDocumentCache = new Map<string, CachedPDFDocument>(); private pdfDocumentCache = new Map<FileId, CachedPDFDocument>();
private maxPdfCacheSize = 10; // Keep up to 10 PDF documents cached private maxPdfCacheSize = 10; // Keep up to 10 PDF documents cached
constructor(private maxWorkers: number = 10) { constructor(private maxWorkers: number = 10) {
@ -47,7 +48,7 @@ export class ThumbnailGenerationService {
/** /**
* Get or create a cached PDF document * Get or create a cached PDF document
*/ */
private async getCachedPDFDocument(fileId: string, pdfArrayBuffer: ArrayBuffer): Promise<any> { private async getCachedPDFDocument(fileId: FileId, pdfArrayBuffer: ArrayBuffer): Promise<any> {
const cached = this.pdfDocumentCache.get(fileId); const cached = this.pdfDocumentCache.get(fileId);
if (cached) { if (cached) {
cached.lastUsed = Date.now(); cached.lastUsed = Date.now();
@ -79,7 +80,7 @@ export class ThumbnailGenerationService {
/** /**
* Release a reference to a cached PDF document * Release a reference to a cached PDF document
*/ */
private releasePDFDocument(fileId: string): void { private releasePDFDocument(fileId: FileId): void {
const cached = this.pdfDocumentCache.get(fileId); const cached = this.pdfDocumentCache.get(fileId);
if (cached) { if (cached) {
cached.refCount--; cached.refCount--;
@ -91,7 +92,7 @@ export class ThumbnailGenerationService {
* Evict the least recently used PDF document * Evict the least recently used PDF document
*/ */
private evictLeastRecentlyUsedPDF(): void { private evictLeastRecentlyUsedPDF(): void {
let oldestEntry: [string, CachedPDFDocument] | null = null; let oldestEntry: [FileId, CachedPDFDocument] | null = null;
let oldestTime = Date.now(); let oldestTime = Date.now();
for (const [key, value] of this.pdfDocumentCache.entries()) { for (const [key, value] of this.pdfDocumentCache.entries()) {
@ -111,7 +112,7 @@ export class ThumbnailGenerationService {
* Generate thumbnails for multiple pages using main thread processing * Generate thumbnails for multiple pages using main thread processing
*/ */
async generateThumbnails( async generateThumbnails(
fileId: string, fileId: FileId,
pdfArrayBuffer: ArrayBuffer, pdfArrayBuffer: ArrayBuffer,
pageNumbers: number[], pageNumbers: number[],
options: ThumbnailGenerationOptions = {}, options: ThumbnailGenerationOptions = {},
@ -142,7 +143,7 @@ export class ThumbnailGenerationService {
* Main thread thumbnail generation with batching for UI responsiveness * Main thread thumbnail generation with batching for UI responsiveness
*/ */
private async generateThumbnailsMainThread( private async generateThumbnailsMainThread(
fileId: string, fileId: FileId,
pdfArrayBuffer: ArrayBuffer, pdfArrayBuffer: ArrayBuffer,
pageNumbers: number[], pageNumbers: number[],
scale: number, scale: number,
@ -284,7 +285,7 @@ export class ThumbnailGenerationService {
this.pdfDocumentCache.clear(); this.pdfDocumentCache.clear();
} }
clearPDFCacheForFile(fileId: string): void { clearPDFCacheForFile(fileId: FileId): void {
const cached = this.pdfDocumentCache.get(fileId); const cached = this.pdfDocumentCache.get(fileId);
if (cached) { if (cached) {
pdfWorkerManager.destroyDocument(cached.pdf); pdfWorkerManager.destroyDocument(cached.pdf);
@ -296,7 +297,7 @@ export class ThumbnailGenerationService {
* Clean up a PDF document from cache when thumbnail generation is complete * Clean up a PDF document from cache when thumbnail generation is complete
* This frees up workers faster for better performance * This frees up workers faster for better performance
*/ */
cleanupCompletedDocument(fileId: string): void { cleanupCompletedDocument(fileId: FileId): void {
const cached = this.pdfDocumentCache.get(fileId); const cached = this.pdfDocumentCache.get(fileId);
if (cached && cached.refCount <= 0) { if (cached && cached.refCount <= 0) {
pdfWorkerManager.destroyDocument(cached.pdf); pdfWorkerManager.destroyDocument(cached.pdf);

View File

@ -3,13 +3,15 @@
* FileContext uses pure File objects with separate ID tracking * FileContext uses pure File objects with separate ID tracking
*/ */
declare const tag: unique symbol;
export type FileId = string & { readonly [tag]: 'FileId' };
/** /**
* File metadata for efficient operations without loading full file data * File metadata for efficient operations without loading full file data
* Used by IndexedDBContext and FileContext for lazy file loading * Used by IndexedDBContext and FileContext for lazy file loading
*/ */
export interface FileMetadata { export interface FileMetadata {
id: string; id: FileId;
name: string; name: string;
type: string; type: string;
size: number; size: number;

View File

@ -2,9 +2,8 @@
* Types for global file context management across views and tools * Types for global file context management across views and tools
*/ */
import { ProcessedFile } from './processing'; import { PageOperation } from './pageEditor';
import { PDFDocument, PDFPage, PageOperation } from './pageEditor'; import { FileId, FileMetadata } from './file';
import { FileMetadata } from './file';
export type ModeType = export type ModeType =
| 'viewer' | 'viewer'
@ -26,8 +25,6 @@ export type ModeType =
| 'removeCertificateSign'; | 'removeCertificateSign';
// Normalized state types // Normalized state types
export type FileId = string;
export interface ProcessedFilePage { export interface ProcessedFilePage {
thumbnail?: string; thumbnail?: string;
pageNumber?: number; pageNumber?: number;
@ -69,14 +66,14 @@ export interface FileContextNormalizedFiles {
export function createFileId(): FileId { export function createFileId(): FileId {
// Use crypto.randomUUID for authoritative primary key // Use crypto.randomUUID for authoritative primary key
if (typeof window !== 'undefined' && window.crypto?.randomUUID) { if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
return window.crypto.randomUUID(); return window.crypto.randomUUID() as FileId;
} }
// Fallback for environments without randomUUID // Fallback for environments without randomUUID
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0; const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8); const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16); return v.toString(16);
}); }) as FileId;
} }
// Generate quick deduplication key from file metadata // Generate quick deduplication key from file metadata
@ -136,7 +133,7 @@ export interface FileOperation {
id: string; id: string;
type: OperationType; type: OperationType;
timestamp: number; timestamp: number;
fileIds: string[]; fileIds: FileId[];
status: 'pending' | 'applied' | 'failed'; status: 'pending' | 'applied' | 'failed';
data?: any; data?: any;
metadata?: { metadata?: {
@ -150,7 +147,7 @@ export interface FileOperation {
} }
export interface FileOperationHistory { export interface FileOperationHistory {
fileId: string; fileId: FileId;
fileName: string; fileName: string;
operations: (FileOperation | PageOperation)[]; operations: (FileOperation | PageOperation)[];
createdAt: number; createdAt: number;
@ -165,7 +162,7 @@ export interface ViewerConfig {
} }
export interface FileEditHistory { export interface FileEditHistory {
fileId: string; fileId: FileId;
pageOperations: PageOperation[]; pageOperations: PageOperation[];
lastModified: number; lastModified: number;
} }
@ -248,8 +245,8 @@ export interface FileContextActions {
// Resource management // Resource management
trackBlobUrl: (url: string) => void; trackBlobUrl: (url: string) => void;
scheduleCleanup: (fileId: string, delay?: number) => void; scheduleCleanup: (fileId: FileId, delay?: number) => void;
cleanupFile: (fileId: string) => void; cleanupFile: (fileId: FileId) => void;
} }
// File selectors (separate from actions to avoid re-renders) // File selectors (separate from actions to avoid re-renders)

View File

@ -1,3 +1,5 @@
import { FileId } from './file';
export interface PDFPage { export interface PDFPage {
id: string; id: string;
pageNumber: number; pageNumber: number;
@ -7,7 +9,7 @@ export interface PDFPage {
selected: boolean; selected: boolean;
splitAfter?: boolean; splitAfter?: boolean;
isBlankPage?: boolean; isBlankPage?: boolean;
originalFileId?: string; originalFileId?: FileId;
} }
export interface PDFDocument { export interface PDFDocument {

View File

@ -1,3 +1,4 @@
import { FileId } from '../types/file';
import { FileOperation } from '../types/fileContext'; import { FileOperation } from '../types/fileContext';
/** /**
@ -7,9 +8,9 @@ export const createOperation = <TParams = void>(
operationType: string, operationType: string,
params: TParams, params: TParams,
selectedFiles: File[] selectedFiles: File[]
): { operation: FileOperation; operationId: string; fileId: string } => { ): { operation: FileOperation; operationId: string; fileId: FileId } => {
const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const fileId = selectedFiles.map(f => f.name).join(','); const fileId = selectedFiles.map(f => f.name).join(',') as FileId;
const operation: FileOperation = { const operation: FileOperation = {
id: operationId, id: operationId,