mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Feature/v2/filehistory (#4370)
File History --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
@@ -119,7 +119,6 @@ describe('useAddPasswordOperation', () => {
|
||||
test.each([
|
||||
{ property: 'toolType' as const, expectedValue: ToolType.singleFile },
|
||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
|
||||
{ property: 'filePrefix' as const, expectedValue: 'translated-addPassword.filenamePrefix_' },
|
||||
{ property: 'operationType' as const, expectedValue: 'addPassword' }
|
||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||
renderHook(() => useAddPasswordOperation());
|
||||
|
||||
@@ -30,7 +30,6 @@ export const addPasswordOperationConfig = {
|
||||
buildFormData: buildAddPasswordFormData,
|
||||
operationType: 'addPassword',
|
||||
endpoint: '/api/v1/security/add-password',
|
||||
filePrefix: 'encrypted_', // Will be overridden in hook with translation
|
||||
defaultParameters: fullDefaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -39,7 +38,6 @@ export const useAddPasswordOperation = () => {
|
||||
|
||||
return useToolOperation<AddPasswordFullParameters>({
|
||||
...addPasswordOperationConfig,
|
||||
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ export const addWatermarkOperationConfig = {
|
||||
buildFormData: buildAddWatermarkFormData,
|
||||
operationType: 'watermark',
|
||||
endpoint: '/api/v1/security/add-watermark',
|
||||
filePrefix: 'watermarked_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -48,7 +47,6 @@ export const useAddWatermarkOperation = () => {
|
||||
|
||||
return useToolOperation<AddWatermarkParameters>({
|
||||
...addWatermarkOperationConfig,
|
||||
filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ export const adjustPageScaleOperationConfig = {
|
||||
buildFormData: buildAdjustPageScaleFormData,
|
||||
operationType: 'adjustPageScale',
|
||||
endpoint: '/api/v1/general/scale-pages',
|
||||
filePrefix: 'scaled_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export const autoRenameOperationConfig = {
|
||||
buildFormData: buildAutoRenameFormData,
|
||||
operationType: 'autoRename',
|
||||
endpoint: '/api/v1/misc/auto-rename',
|
||||
filePrefix: 'autoRename_',
|
||||
preserveBackendFilename: true, // Use filename from backend response headers
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -42,6 +42,5 @@ export function useAutomateOperation() {
|
||||
toolType: ToolType.custom,
|
||||
operationType: 'automate',
|
||||
customProcessor,
|
||||
filePrefix: '' // No prefix needed since automation handles naming internally
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ describe('useChangePermissionsOperation', () => {
|
||||
test.each([
|
||||
{ property: 'toolType' as const, expectedValue: ToolType.singleFile },
|
||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/add-password' },
|
||||
{ property: 'filePrefix' as const, expectedValue: 'permissions_' },
|
||||
{ property: 'operationType' as const, expectedValue: 'change-permissions' }
|
||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||
renderHook(() => useChangePermissionsOperation());
|
||||
|
||||
@@ -28,7 +28,6 @@ export const changePermissionsOperationConfig = {
|
||||
buildFormData: buildChangePermissionsFormData,
|
||||
operationType: 'change-permissions',
|
||||
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
|
||||
filePrefix: 'permissions_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export const compressOperationConfig = {
|
||||
buildFormData: buildCompressFormData,
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
filePrefix: 'compressed_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export const createFileFromResponse = (
|
||||
targetExtension = 'pdf';
|
||||
}
|
||||
|
||||
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
|
||||
const fallbackFilename = `${originalName}.${targetExtension}`;
|
||||
|
||||
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
};
|
||||
@@ -136,7 +136,6 @@ export const convertOperationConfig = {
|
||||
toolType: ToolType.custom,
|
||||
customProcessor: convertProcessor, // Can't use callback version here
|
||||
operationType: 'convert',
|
||||
filePrefix: 'converted_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ export const flattenOperationConfig = {
|
||||
buildFormData: buildFlattenFormData,
|
||||
operationType: 'flatten',
|
||||
endpoint: '/api/v1/misc/flatten',
|
||||
filePrefix: 'flattened_', // Will be overridden in hook with translation
|
||||
multiFileEndpoint: false,
|
||||
defaultParameters,
|
||||
} as const;
|
||||
@@ -27,7 +26,6 @@ export const useFlattenOperation = () => {
|
||||
|
||||
return useToolOperation<FlattenParameters>({
|
||||
...flattenOperationConfig,
|
||||
filePrefix: t('flatten.filenamePrefix', 'flattened') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('flatten.error.failed', 'An error occurred while flattening the PDF.'))
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -88,8 +88,8 @@ export const ocrResponseHandler = async (blob: Blob, originalFiles: File[], extr
|
||||
throw new Error(`Response is not a valid PDF. Header: "${head}"`);
|
||||
}
|
||||
|
||||
const base = stripExt(originalFiles[0].name);
|
||||
return [new File([blob], `ocr_${base}.pdf`, { type: 'application/pdf' })];
|
||||
const originalName = originalFiles[0].name;
|
||||
return [new File([blob], originalName, { type: 'application/pdf' })];
|
||||
};
|
||||
|
||||
// Static configuration object (without t function dependencies)
|
||||
@@ -98,7 +98,6 @@ export const ocrOperationConfig = {
|
||||
buildFormData: buildOCRFormData,
|
||||
operationType: 'ocr',
|
||||
endpoint: '/api/v1/misc/ocr-pdf',
|
||||
filePrefix: 'ocr_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export const redactOperationConfig = {
|
||||
throw new Error('Manual redaction not yet implemented');
|
||||
}
|
||||
},
|
||||
filePrefix: 'redacted_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export const removeCertificateSignOperationConfig = {
|
||||
buildFormData: buildRemoveCertificateSignFormData,
|
||||
operationType: 'remove-certificate-sign',
|
||||
endpoint: '/api/v1/security/remove-cert-sign',
|
||||
filePrefix: 'unsigned_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -25,7 +24,6 @@ export const useRemoveCertificateSignOperation = () => {
|
||||
|
||||
return useToolOperation<RemoveCertificateSignParameters>({
|
||||
...removeCertificateSignOperationConfig,
|
||||
filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,7 +97,6 @@ describe('useRemovePasswordOperation', () => {
|
||||
test.each([
|
||||
{ property: 'toolType' as const, expectedValue: ToolType.singleFile },
|
||||
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/remove-password' },
|
||||
{ property: 'filePrefix' as const, expectedValue: 'translated-removePassword.filenamePrefix_' },
|
||||
{ property: 'operationType' as const, expectedValue: 'removePassword' }
|
||||
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||
renderHook(() => useRemovePasswordOperation());
|
||||
|
||||
@@ -17,7 +17,6 @@ export const removePasswordOperationConfig = {
|
||||
buildFormData: buildRemovePasswordFormData,
|
||||
operationType: 'removePassword',
|
||||
endpoint: '/api/v1/security/remove-password',
|
||||
filePrefix: 'decrypted_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -26,7 +25,6 @@ export const useRemovePasswordOperation = () => {
|
||||
|
||||
return useToolOperation<RemovePasswordParameters>({
|
||||
...removePasswordOperationConfig,
|
||||
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ export const repairOperationConfig = {
|
||||
buildFormData: buildRepairFormData,
|
||||
operationType: 'repair',
|
||||
endpoint: '/api/v1/misc/repair',
|
||||
filePrefix: 'repaired_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -25,7 +24,6 @@ export const useRepairOperation = () => {
|
||||
|
||||
return useToolOperation<RepairParameters>({
|
||||
...repairOperationConfig,
|
||||
filePrefix: t('repair.filenamePrefix', 'repaired') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ export const sanitizeOperationConfig = {
|
||||
buildFormData: buildSanitizeFormData,
|
||||
operationType: 'sanitize',
|
||||
endpoint: '/api/v1/security/sanitize-pdf',
|
||||
filePrefix: 'sanitized_', // Will be overridden in hook with translation
|
||||
multiFileEndpoint: false,
|
||||
defaultParameters,
|
||||
} as const;
|
||||
@@ -35,7 +34,6 @@ export const useSanitizeOperation = () => {
|
||||
|
||||
return useToolOperation<SanitizeParameters>({
|
||||
...sanitizeOperationConfig,
|
||||
filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ProcessingProgress } from './useToolState';
|
||||
export interface ApiCallsConfig<TParams = void> {
|
||||
endpoint: string | ((params: TParams) => string);
|
||||
buildFormData: (params: TParams, file: File) => FormData;
|
||||
filePrefix: string;
|
||||
filePrefix?: string;
|
||||
responseHandler?: ResponseHandler;
|
||||
preserveBackendFilename?: boolean;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { useToolState, type ProcessingProgress } from './useToolState';
|
||||
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
||||
import { useToolResources } from './useToolResources';
|
||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||
import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext';
|
||||
import { StirlingFile, extractFiles, FileId, StirlingFileStub, createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
|
||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||
import { createChildStub, generateProcessedFileMetadata } from '../../../contexts/file/fileActions';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { ProcessingProgress, ResponseHandler };
|
||||
@@ -31,7 +32,7 @@ interface BaseToolOperationConfig<TParams> {
|
||||
operationType: string;
|
||||
|
||||
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
|
||||
filePrefix: string;
|
||||
filePrefix?: string;
|
||||
|
||||
/**
|
||||
* Whether to preserve the filename provided by the backend in response headers.
|
||||
@@ -165,18 +166,20 @@ export const useToolOperation = <TParams>(
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Reset state
|
||||
actions.setLoading(true);
|
||||
actions.setError(null);
|
||||
actions.resetResults();
|
||||
cleanupBlobUrls();
|
||||
|
||||
// Prepare files with history metadata injection (for PDFs)
|
||||
actions.setStatus('Processing files...');
|
||||
|
||||
try {
|
||||
let processedFiles: File[];
|
||||
|
||||
// Convert StirlingFile to regular File objects for API processing
|
||||
const validRegularFiles = extractFiles(validFiles);
|
||||
// Use original files directly (no PDF metadata injection - history stored in IndexedDB)
|
||||
const filesForAPI = extractFiles(validFiles);
|
||||
|
||||
switch (config.toolType) {
|
||||
case ToolType.singleFile: {
|
||||
@@ -190,18 +193,17 @@ export const useToolOperation = <TParams>(
|
||||
};
|
||||
processedFiles = await processFiles(
|
||||
params,
|
||||
validRegularFiles,
|
||||
filesForAPI,
|
||||
apiCallsConfig,
|
||||
actions.setProgress,
|
||||
actions.setStatus
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case ToolType.multiFile: {
|
||||
// Multi-file processing - single API call with all files
|
||||
actions.setStatus('Processing files...');
|
||||
const formData = config.buildFormData(params, validRegularFiles);
|
||||
const formData = config.buildFormData(params, filesForAPI);
|
||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
@@ -209,11 +211,11 @@ export const useToolOperation = <TParams>(
|
||||
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||
if (config.responseHandler) {
|
||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||
processedFiles = await config.responseHandler(response.data, validRegularFiles);
|
||||
processedFiles = await config.responseHandler(response.data, filesForAPI);
|
||||
} else if (response.data.type === 'application/pdf' ||
|
||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||
// Single PDF response (e.g. split with merge option) - use original filename
|
||||
const originalFileName = validRegularFiles[0]?.name || 'document.pdf';
|
||||
const originalFileName = filesForAPI[0]?.name || 'document.pdf';
|
||||
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||
processedFiles = [singleFile];
|
||||
} else {
|
||||
@@ -230,13 +232,14 @@ export const useToolOperation = <TParams>(
|
||||
|
||||
case ToolType.custom:
|
||||
actions.setStatus('Processing files...');
|
||||
processedFiles = await config.customProcessor(params, validRegularFiles);
|
||||
processedFiles = await config.customProcessor(params, filesForAPI);
|
||||
break;
|
||||
}
|
||||
|
||||
if (processedFiles.length > 0) {
|
||||
actions.setFiles(processedFiles);
|
||||
|
||||
|
||||
// Generate thumbnails and download URL concurrently
|
||||
actions.setGeneratingThumbnails(true);
|
||||
const [thumbnails, downloadInfo] = await Promise.all([
|
||||
@@ -264,7 +267,40 @@ export const useToolOperation = <TParams>(
|
||||
}
|
||||
}
|
||||
|
||||
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
|
||||
// Create new tool operation
|
||||
const newToolOperation = {
|
||||
toolName: config.operationType,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Generate fresh processedFileMetadata for all processed files to ensure accuracy
|
||||
actions.setStatus('Generating metadata for processed files...');
|
||||
const processedFileMetadataArray = await Promise.all(
|
||||
processedFiles.map(file => generateProcessedFileMetadata(file))
|
||||
);
|
||||
const shouldBranchHistory = processedFiles.length != inputStirlingFileStubs.length;
|
||||
// Create output stubs with fresh metadata (no inheritance of stale processedFile data)
|
||||
const outputStirlingFileStubs = shouldBranchHistory
|
||||
? processedFiles.map((file, index) =>
|
||||
createNewStirlingFileStub(file, undefined, thumbnails[index], processedFileMetadataArray[index])
|
||||
)
|
||||
: processedFiles.map((resultingFile, index) =>
|
||||
createChildStub(
|
||||
inputStirlingFileStubs[index],
|
||||
newToolOperation,
|
||||
resultingFile,
|
||||
thumbnails[index],
|
||||
processedFileMetadataArray[index]
|
||||
)
|
||||
);
|
||||
|
||||
// Create StirlingFile objects from processed files and child stubs
|
||||
const outputStirlingFiles = processedFiles.map((file, index) => {
|
||||
const childStub = outputStirlingFileStubs[index];
|
||||
return createStirlingFile(file, childStub.id);
|
||||
});
|
||||
|
||||
const outputFileIds = await consumeFiles(inputFileIds, outputStirlingFiles, outputStirlingFileStubs);
|
||||
|
||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||
lastOperationRef.current = {
|
||||
|
||||
@@ -16,7 +16,6 @@ export const singleLargePageOperationConfig = {
|
||||
buildFormData: buildSingleLargePageFormData,
|
||||
operationType: 'single-large-page',
|
||||
endpoint: '/api/v1/general/pdf-to-single-page',
|
||||
filePrefix: 'single_page_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -25,7 +24,6 @@ export const useSingleLargePageOperation = () => {
|
||||
|
||||
return useToolOperation<SingleLargePageParameters>({
|
||||
...singleLargePageOperationConfig,
|
||||
filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -73,7 +73,6 @@ export const splitOperationConfig = {
|
||||
buildFormData: buildSplitFormData,
|
||||
operationType: 'splitPdf',
|
||||
endpoint: getSplitEndpoint,
|
||||
filePrefix: 'split_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export const unlockPdfFormsOperationConfig = {
|
||||
buildFormData: buildUnlockPdfFormsFormData,
|
||||
operationType: 'unlock-pdf-forms',
|
||||
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
||||
filePrefix: 'unlocked_forms_', // Will be overridden in hook with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
@@ -25,7 +24,6 @@ export const useUnlockPdfFormsOperation = () => {
|
||||
|
||||
return useToolOperation<UnlockPdfFormsParameters>({
|
||||
...unlockPdfFormsOperationConfig,
|
||||
filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,39 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useFileState, useFileActions } from '../contexts/FileContext';
|
||||
import { FileMetadata } from '../types/file';
|
||||
import { FileId } from '../types/file';
|
||||
import { useFileActions } from '../contexts/FileContext';
|
||||
|
||||
export const useFileHandler = () => {
|
||||
const { state } = useFileState(); // Still needed for addStoredFiles
|
||||
const { actions } = useFileActions();
|
||||
|
||||
const addToActiveFiles = useCallback(async (file: File) => {
|
||||
const addFiles = useCallback(async (files: File[], options: { insertAfterPageId?: string; selectFiles?: boolean } = {}) => {
|
||||
// Merge default options with passed options - passed options take precedence
|
||||
const mergedOptions = { selectFiles: true, ...options };
|
||||
// Let FileContext handle deduplication with quickKey logic
|
||||
await actions.addFiles([file], { selectFiles: true });
|
||||
await actions.addFiles(files, mergedOptions);
|
||||
}, [actions.addFiles]);
|
||||
|
||||
const addMultipleFiles = useCallback(async (files: File[]) => {
|
||||
// Let FileContext handle deduplication with quickKey logic
|
||||
await actions.addFiles(files, { selectFiles: true });
|
||||
}, [actions.addFiles]);
|
||||
|
||||
// Add stored files preserving their original IDs to prevent session duplicates
|
||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => {
|
||||
// Filter out files that already exist with the same ID (exact match)
|
||||
const newFiles = filesWithMetadata.filter(({ originalId }) => {
|
||||
return state.files.byId[originalId] === undefined;
|
||||
});
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
await actions.addStoredFiles(newFiles, { selectFiles: true });
|
||||
}
|
||||
|
||||
console.log(`📁 Added ${newFiles.length} stored files (${filesWithMetadata.length - newFiles.length} skipped as duplicates)`);
|
||||
}, [state.files.byId, actions.addStoredFiles]);
|
||||
|
||||
return {
|
||||
addToActiveFiles,
|
||||
addMultipleFiles,
|
||||
addStoredFiles,
|
||||
addFiles,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,72 +1,40 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
||||
import { FileMetadata } from '../types/file';
|
||||
import { fileStorage } from '../services/fileStorage';
|
||||
import { StirlingFileStub, StirlingFile } from '../types/fileContext';
|
||||
import { FileId } from '../types/fileContext';
|
||||
|
||||
export const useFileManager = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const indexedDB = useIndexedDB();
|
||||
|
||||
const convertToFile = useCallback(async (fileMetadata: FileMetadata): Promise<File> => {
|
||||
const convertToFile = useCallback(async (fileStub: StirlingFileStub): Promise<File> => {
|
||||
if (!indexedDB) {
|
||||
throw new Error('IndexedDB context not available');
|
||||
}
|
||||
|
||||
// Handle drafts differently from regular files
|
||||
if (fileMetadata.isDraft) {
|
||||
// Load draft from the drafts database
|
||||
try {
|
||||
const { indexedDBManager, DATABASE_CONFIGS } = await import('../services/indexedDBManager');
|
||||
const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(['drafts'], 'readonly');
|
||||
const store = transaction.objectStore('drafts');
|
||||
const request = store.get(fileMetadata.id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const draft = request.result;
|
||||
if (draft && draft.pdfData) {
|
||||
const file = new File([draft.pdfData], fileMetadata.name, {
|
||||
type: 'application/pdf',
|
||||
lastModified: fileMetadata.lastModified
|
||||
});
|
||||
resolve(file);
|
||||
} else {
|
||||
reject(new Error('Draft data not found'));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load draft: ${fileMetadata.name} (${error})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular file loading
|
||||
if (fileMetadata.id) {
|
||||
const file = await indexedDB.loadFile(fileMetadata.id);
|
||||
if (fileStub.id) {
|
||||
const file = await indexedDB.loadFile(fileStub.id);
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
throw new Error(`File not found in storage: ${fileMetadata.name} (ID: ${fileMetadata.id})`);
|
||||
throw new Error(`File not found in storage: ${fileStub.name} (ID: ${fileStub.id})`);
|
||||
}, [indexedDB]);
|
||||
|
||||
const loadRecentFiles = useCallback(async (): Promise<FileMetadata[]> => {
|
||||
const loadRecentFiles = useCallback(async (): Promise<StirlingFileStub[]> => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!indexedDB) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Load regular files metadata only
|
||||
const storedFileMetadata = await indexedDB.loadAllMetadata();
|
||||
// Load only leaf files metadata (processed files that haven't been used as input for other tools)
|
||||
const stirlingFileStubs = await fileStorage.getLeafStirlingFileStubs();
|
||||
|
||||
// For now, only regular files - drafts will be handled separately in the future
|
||||
const allFiles = storedFileMetadata;
|
||||
const sortedFiles = allFiles.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
|
||||
const sortedFiles = stirlingFileStubs.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
|
||||
|
||||
return sortedFiles;
|
||||
} catch (error) {
|
||||
@@ -77,7 +45,7 @@ export const useFileManager = () => {
|
||||
}
|
||||
}, [indexedDB]);
|
||||
|
||||
const handleRemoveFile = useCallback(async (index: number, files: FileMetadata[], setFiles: (files: FileMetadata[]) => void) => {
|
||||
const handleRemoveFile = useCallback(async (index: number, files: StirlingFileStub[], setFiles: (files: StirlingFileStub[]) => void) => {
|
||||
const file = files[index];
|
||||
if (!file.id) {
|
||||
throw new Error('File ID is required for removal');
|
||||
@@ -102,10 +70,10 @@ export const useFileManager = () => {
|
||||
// Store file with provided UUID from FileContext (thumbnail generated internally)
|
||||
const metadata = await indexedDB.saveFile(file, fileId);
|
||||
|
||||
// Convert file to ArrayBuffer for StoredFile interface compatibility
|
||||
// Convert file to ArrayBuffer for storage compatibility
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Return StoredFile format for compatibility with old API
|
||||
// This method is deprecated - use FileStorage directly instead
|
||||
return {
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
@@ -113,7 +81,7 @@ export const useFileManager = () => {
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
data: arrayBuffer,
|
||||
thumbnail: metadata.thumbnail
|
||||
thumbnail: metadata.thumbnailUrl
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to store file:', error);
|
||||
@@ -137,23 +105,24 @@ export const useFileManager = () => {
|
||||
setSelectedFiles([]);
|
||||
};
|
||||
|
||||
const selectMultipleFiles = async (files: FileMetadata[], onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => void) => {
|
||||
const selectMultipleFiles = async (files: StirlingFileStub[], onStirlingFilesSelect: (stirlingFiles: StirlingFile[]) => void) => {
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
try {
|
||||
// Filter by UUID and convert to File objects
|
||||
// Filter by UUID and load full StirlingFile objects directly
|
||||
const selectedFileObjects = files.filter(f => selectedFiles.includes(f.id));
|
||||
|
||||
// Use stored files flow that preserves IDs
|
||||
const filesWithMetadata = await Promise.all(
|
||||
selectedFileObjects.map(async (metadata) => ({
|
||||
file: await convertToFile(metadata),
|
||||
originalId: metadata.id,
|
||||
metadata
|
||||
}))
|
||||
const stirlingFiles = await Promise.all(
|
||||
selectedFileObjects.map(async (stub) => {
|
||||
const stirlingFile = await fileStorage.getStirlingFile(stub.id);
|
||||
if (!stirlingFile) {
|
||||
throw new Error(`File not found in storage: ${stub.name}`);
|
||||
}
|
||||
return stirlingFile;
|
||||
})
|
||||
);
|
||||
onStoredFilesSelect(filesWithMetadata);
|
||||
|
||||
onStirlingFilesSelect(stirlingFiles);
|
||||
clearSelection();
|
||||
} catch (error) {
|
||||
console.error('Failed to load selected files:', error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { FileMetadata } from "../types/file";
|
||||
import { StirlingFileStub } from "../types/fileContext";
|
||||
import { useIndexedDB } from "../contexts/IndexedDBContext";
|
||||
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
||||
import { FileId } from "../types/fileContext";
|
||||
@@ -9,7 +9,7 @@ import { FileId } from "../types/fileContext";
|
||||
* Hook for IndexedDB-aware thumbnail loading
|
||||
* Handles thumbnail generation for files not in IndexedDB
|
||||
*/
|
||||
export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
||||
export function useIndexedDBThumbnail(file: StirlingFileStub | undefined | null): {
|
||||
thumbnail: string | null;
|
||||
isGenerating: boolean
|
||||
} {
|
||||
@@ -27,8 +27,8 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
||||
}
|
||||
|
||||
// First priority: use stored thumbnail
|
||||
if (file.thumbnail) {
|
||||
setThumb(file.thumbnail);
|
||||
if (file.thumbnailUrl) {
|
||||
setThumb(file.thumbnailUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
||||
|
||||
loadThumbnail();
|
||||
return () => { cancelled = true; };
|
||||
}, [file, file?.thumbnail, file?.id, indexedDB, generating]);
|
||||
}, [file, file?.thumbnailUrl, file?.id, indexedDB, generating]);
|
||||
|
||||
return { thumbnail: thumb, isGenerating: generating };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user