mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-29 15:49:22 +00:00
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:
parent
581bafbd37
commit
e142af2863
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,12 @@ import {
|
|||||||
Divider
|
Divider
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
// 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 || [];
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,7 +174,8 @@ 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,7 +184,8 @@ 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);
|
||||||
|
|
||||||
@ -213,7 +216,7 @@ const PageEditor = ({
|
|||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(splitCommand);
|
undoManagerRef.current.executeCommand(splitCommand);
|
||||||
}
|
}
|
||||||
}), [splitPositions]);
|
}), [splitPositions]);
|
||||||
|
|
||||||
// Command executor for PageThumbnail
|
// Command executor for PageThumbnail
|
||||||
const executeCommand = useCallback((command: any) => {
|
const executeCommand = useCallback((command: any) => {
|
||||||
@ -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);
|
||||||
@ -632,7 +636,7 @@ const PageEditor = ({
|
|||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
// For now, trigger the actual export directly
|
// For now, trigger the actual export directly
|
||||||
// In the original, this would show a preview modal first
|
// In the original, this would show a preview modal first
|
||||||
if (selectedOnly) {
|
if (selectedOnly) {
|
||||||
onExportSelected();
|
onExportSelected();
|
||||||
} else {
|
} else {
|
||||||
|
@ -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);
|
||||||
|
@ -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 [];
|
||||||
|
@ -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 (
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 &&
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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) => {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user