Feature/v2/filehistory (#4370)

File History

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
ConnorYoh
2025-09-16 15:08:11 +01:00
committed by GitHub
parent 8e8b417f5e
commit 190178a471
61 changed files with 2279 additions and 1245 deletions

View File

@@ -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());

View File

@@ -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.'))
});
};

View File

@@ -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.'))
});
};

View File

@@ -16,7 +16,6 @@ export const adjustPageScaleOperationConfig = {
buildFormData: buildAdjustPageScaleFormData,
operationType: 'adjustPageScale',
endpoint: '/api/v1/general/scale-pages',
filePrefix: 'scaled_',
defaultParameters,
} as const;

View File

@@ -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;

View File

@@ -42,6 +42,5 @@ export function useAutomateOperation() {
toolType: ToolType.custom,
operationType: 'automate',
customProcessor,
filePrefix: '' // No prefix needed since automation handles naming internally
});
}

View File

@@ -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());

View File

@@ -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;

View File

@@ -28,7 +28,6 @@ export const compressOperationConfig = {
buildFormData: buildCompressFormData,
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
filePrefix: 'compressed_',
defaultParameters,
} as const;

View File

@@ -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;

View File

@@ -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.'))
});
};
};

View File

@@ -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;

View File

@@ -37,7 +37,6 @@ export const redactOperationConfig = {
throw new Error('Manual redaction not yet implemented');
}
},
filePrefix: 'redacted_',
defaultParameters,
} as const;

View File

@@ -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.'))
});
};

View File

@@ -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());

View File

@@ -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.'))
});
};

View File

@@ -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.'))
});
};

View File

@@ -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.'))
});
};

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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.'))
});
};

View File

@@ -73,7 +73,6 @@ export const splitOperationConfig = {
buildFormData: buildSplitFormData,
operationType: 'splitPdf',
endpoint: getSplitEndpoint,
filePrefix: 'split_',
defaultParameters,
} as const;

View File

@@ -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.'))
});
};

View File

@@ -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,
};
};

View File

@@ -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);

View File

@@ -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 };
}