mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
Enforce type checking in CI (#4126)
# Description of Changes Currently, the `tsconfig.json` file enforces strict type checking, but nothing in CI checks that the code is actually correctly typed. [Vite only transpiles TypeScript code](https://vite.dev/guide/features.html#transpile-only) so doesn't ensure that the TS code we're running is correct. This PR adds running of the type checker to CI and fixes the type errors that have already crept into the codebase. Note that many of the changes I've made to 'fix the types' are just using `any` to disable the type checker because the code is under too much churn to fix anything properly at the moment. I still think enabling the type checker now is the best course of action though because otherwise we'll never be able to fix all of them, and it should at least help us not break things when adding new code. Co-authored-by: James <james@crosscourtanalytics.com>
This commit is contained in:
@@ -8,19 +8,19 @@ import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperatio
|
||||
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
||||
|
||||
const shouldProcessFilesSeparately = (
|
||||
selectedFiles: File[],
|
||||
selectedFiles: File[],
|
||||
parameters: ConvertParameters
|
||||
): boolean => {
|
||||
return selectedFiles.length > 1 && (
|
||||
// Image to PDF with combineImages = false
|
||||
((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') &&
|
||||
((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') &&
|
||||
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
|
||||
// PDF to image conversions (each PDF should generate its own image file)
|
||||
(parameters.fromExtension === 'pdf' && isImageFormat(parameters.toExtension)) ||
|
||||
// PDF to PDF/A conversions (each PDF should be processed separately)
|
||||
(parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') ||
|
||||
// Web files to PDF conversions (each web file should generate its own PDF)
|
||||
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
|
||||
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
|
||||
parameters.toExtension === 'pdf') ||
|
||||
// Web files smart detection
|
||||
(parameters.isSmartDetection && parameters.smartDetectionType === 'web') ||
|
||||
@@ -31,7 +31,7 @@ const shouldProcessFilesSeparately = (
|
||||
|
||||
const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => {
|
||||
const formData = new FormData();
|
||||
|
||||
|
||||
selectedFiles.forEach(file => {
|
||||
formData.append("fileInput", file);
|
||||
});
|
||||
@@ -77,13 +77,13 @@ const createFileFromResponse = (
|
||||
): File => {
|
||||
const originalName = originalFileName.split('.')[0];
|
||||
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
|
||||
|
||||
|
||||
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||
};
|
||||
|
||||
export const useConvertOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const customConvertProcessor = useCallback(async (
|
||||
parameters: ConvertParameters,
|
||||
selectedFiles: File[]
|
||||
@@ -91,7 +91,7 @@ export const useConvertOperation = () => {
|
||||
|
||||
const processedFiles: File[] = [];
|
||||
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
|
||||
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error(t('errorNotSupported', 'Unsupported conversion format'));
|
||||
}
|
||||
@@ -103,9 +103,9 @@ export const useConvertOperation = () => {
|
||||
try {
|
||||
const formData = buildFormData(parameters, [file]);
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
|
||||
|
||||
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
||||
|
||||
|
||||
processedFiles.push(convertedFile);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to convert file ${file.name}:`, error);
|
||||
@@ -115,11 +115,11 @@ export const useConvertOperation = () => {
|
||||
// Batch processing for simple cases (image→PDF combine)
|
||||
const formData = buildFormData(parameters, selectedFiles);
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
|
||||
const baseFilename = selectedFiles.length === 1
|
||||
|
||||
const baseFilename = selectedFiles.length === 1
|
||||
? selectedFiles[0].name
|
||||
: 'converted_files';
|
||||
|
||||
|
||||
const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension);
|
||||
processedFiles.push(convertedFile);
|
||||
|
||||
@@ -131,7 +131,7 @@ export const useConvertOperation = () => {
|
||||
return useToolOperation<ConvertParameters>({
|
||||
operationType: 'convert',
|
||||
endpoint: '', // Not used with customProcessor but required
|
||||
buildFormData, // Not used with customProcessor but required
|
||||
buildFormData, // Not used with customProcessor but required
|
||||
filePrefix: 'converted_',
|
||||
customProcessor: customConvertProcessor, // Convert handles its own routing
|
||||
validateParams: (params) => {
|
||||
@@ -147,4 +147,4 @@ export const useConvertOperation = () => {
|
||||
return t("convert.errorConversion", "An error occurred while converting the file.");
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,18 +8,18 @@ import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useConvertParameters } from './useConvertParameters';
|
||||
|
||||
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
|
||||
describe('Single File Detection', () => {
|
||||
|
||||
|
||||
test('should detect single file extension and set auto-target', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const pdfFile = [{ name: 'document.pdf' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(pdfFile);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||
expect(result.current.parameters.toExtension).toBe(''); // No auto-selection for multiple targets
|
||||
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||
@@ -28,13 +28,13 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should handle unknown file types with file-to-pdf fallback', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const unknownFile = [{ name: 'document.xyz' }, { name: 'image.jpggg' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(unknownFile);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('any');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
@@ -42,35 +42,35 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should handle files without extensions', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const noExtFile = [{ name: 'document' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(noExtFile);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('any');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('Multiple Identical Files', () => {
|
||||
|
||||
|
||||
test('should detect multiple PDF files and set auto-target', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const pdfFiles = [
|
||||
{ name: 'doc1.pdf' },
|
||||
{ name: 'doc2.pdf' },
|
||||
{ name: 'doc3.pdf' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(pdfFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||
expect(result.current.parameters.toExtension).toBe(''); // Auto-selected
|
||||
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||
@@ -79,37 +79,37 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should handle multiple unknown file types with fallback', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const unknownFiles = [
|
||||
{ name: 'file1.xyz' },
|
||||
{ name: 'file2.xyz' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(unknownFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('any');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
expect(result.current.parameters.isSmartDetection).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Smart Detection - All Images', () => {
|
||||
|
||||
|
||||
test('should detect all image files and enable smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const imageFiles = [
|
||||
{ name: 'photo1.jpg' },
|
||||
{ name: 'photo2.png' },
|
||||
{ name: 'photo3.gif' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(imageFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('image');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
@@ -118,35 +118,35 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should handle mixed case image extensions', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const imageFiles = [
|
||||
{ name: 'photo1.JPG' },
|
||||
{ name: 'photo2.PNG' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(imageFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('images');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Smart Detection - All Web Files', () => {
|
||||
|
||||
|
||||
test('should detect all web files and enable web smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const webFiles = [
|
||||
{ name: 'page1.html' },
|
||||
{ name: 'archive.zip' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(webFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('html');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
@@ -155,54 +155,54 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should handle mixed case web extensions', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const webFiles = [
|
||||
{ name: 'page1.HTML' },
|
||||
{ name: 'archive.ZIP' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(webFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('web');
|
||||
});
|
||||
|
||||
test('should detect multiple web files and enable web smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const zipFiles = [
|
||||
{ name: 'site1.zip' },
|
||||
{ name: 'site2.html' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(zipFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('html');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('web');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Smart Detection - Mixed File Types', () => {
|
||||
|
||||
|
||||
test('should detect mixed file types and enable smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const mixedFiles = [
|
||||
{ name: 'document.pdf' },
|
||||
{ name: 'spreadsheet.xlsx' },
|
||||
{ name: 'presentation.pptx' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(mixedFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('any');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
@@ -211,155 +211,155 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
|
||||
test('should detect mixed images and documents as mixed type', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const mixedFiles = [
|
||||
{ name: 'photo.jpg' },
|
||||
{ name: 'document.pdf' },
|
||||
{ name: 'text.txt' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(mixedFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||
});
|
||||
|
||||
test('should handle mixed with unknown file types', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const mixedFiles = [
|
||||
{ name: 'document.pdf' },
|
||||
{ name: 'unknown.xyz' },
|
||||
{ name: 'noextension' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(mixedFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Smart Detection Endpoint Resolution', () => {
|
||||
|
||||
|
||||
test('should return correct endpoint for image smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const imageFiles = [
|
||||
{ name: 'photo1.jpg' },
|
||||
{ name: 'photo2.png' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(imageFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('img-to-pdf');
|
||||
expect(result.current.getEndpoint()).toBe('/api/v1/convert/img/pdf');
|
||||
});
|
||||
|
||||
test('should return correct endpoint for web smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const webFiles = [
|
||||
{ name: 'page1.html' },
|
||||
{ name: 'archive.zip' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(webFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('html-to-pdf');
|
||||
expect(result.current.getEndpoint()).toBe('/api/v1/convert/html/pdf');
|
||||
});
|
||||
|
||||
test('should return correct endpoint for mixed smart detection', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const mixedFiles = [
|
||||
{ name: 'document.pdf' },
|
||||
{ name: 'spreadsheet.xlsx' }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(mixedFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('file-to-pdf');
|
||||
expect(result.current.getEndpoint()).toBe('/api/v1/convert/file/pdf');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Auto-Target Selection Logic', () => {
|
||||
|
||||
|
||||
test('should select single available target automatically', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
// Markdown has only one conversion target (PDF)
|
||||
const mdFile = [{ name: 'readme.md' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(mdFile);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('md');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf'); // Only available target
|
||||
});
|
||||
|
||||
test('should not auto-select when multiple targets available', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
// PDF has multiple conversion targets, so no auto-selection
|
||||
const pdfFile = [{ name: 'document.pdf' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(pdfFile);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('pdf');
|
||||
// Should NOT auto-select when multiple targets available
|
||||
expect(result.current.parameters.toExtension).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
|
||||
|
||||
test('should handle empty file names', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
|
||||
const emptyFiles = [{ name: '' }];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(emptyFiles);
|
||||
});
|
||||
|
||||
|
||||
expect(result.current.parameters.fromExtension).toBe('any');
|
||||
expect(result.current.parameters.toExtension).toBe('pdf');
|
||||
});
|
||||
|
||||
test('should handle malformed file objects', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
const malformedFiles = [
|
||||
|
||||
const malformedFiles: Array<{name: string}> = [
|
||||
{ name: 'valid.pdf' },
|
||||
// @ts-ignore - Testing runtime resilience
|
||||
{ name: null },
|
||||
// @ts-ignore
|
||||
{ name: undefined }
|
||||
];
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current.analyzeFileTypes(malformedFiles);
|
||||
});
|
||||
|
||||
|
||||
// Should still process the valid file and handle gracefully
|
||||
expect(result.current.parameters.isSmartDetection).toBe(true);
|
||||
expect(result.current.parameters.smartDetectionType).toBe('mixed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ export const useOCROperation = () => {
|
||||
const ocrConfig: ToolOperationConfig<OCRParameters> = {
|
||||
operationType: 'ocr',
|
||||
endpoint: '/api/v1/misc/ocr-pdf',
|
||||
buildFormData,
|
||||
buildFormData: buildFormData as any /* FIX ME */,
|
||||
filePrefix: 'ocr_',
|
||||
multiFileEndpoint: false, // Process files individually
|
||||
responseHandler, // use shared flow
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import axios, { CancelTokenSource } from 'axios';
|
||||
import { processResponse } from '../../../utils/toolResponseProcessor';
|
||||
import type { ResponseHandler, ProcessingProgress } from './useToolState';
|
||||
import { processResponse, ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||
import type { ProcessingProgress } from './useToolState';
|
||||
|
||||
export interface ApiCallsConfig<TParams = void> {
|
||||
endpoint: string | ((params: TParams) => string);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
|
||||
import { useToolResources } from './useToolResources';
|
||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||
import { createOperation } from '../../../utils/toolOperationTracker';
|
||||
import { type ResponseHandler, processResponse } from '../../../utils/toolResponseProcessor';
|
||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
@@ -176,7 +176,7 @@ export const useToolOperation = <TParams = void>(
|
||||
} else {
|
||||
// Default: assume ZIP response for multi-file endpoints
|
||||
processedFiles = await extractZipFiles(response.data);
|
||||
|
||||
|
||||
if (processedFiles.length === 0) {
|
||||
// Try the generic extraction as fallback
|
||||
processedFiles = await extractAllZipFiles(response.data);
|
||||
@@ -186,7 +186,7 @@ export const useToolOperation = <TParams = void>(
|
||||
// Individual file processing - separate API call per file
|
||||
const apiCallsConfig: ApiCallsConfig<TParams> = {
|
||||
endpoint: config.endpoint,
|
||||
buildFormData: (file: File, params: TParams) => (config.buildFormData as (file: File, params: TParams) => FormData)(file, params),
|
||||
buildFormData: (file: File, params: TParams) => (config.buildFormData as any /* FIX ME */)(file, params),
|
||||
filePrefix: config.filePrefix,
|
||||
responseHandler: config.responseHandler
|
||||
};
|
||||
|
||||
@@ -36,17 +36,19 @@ export const useToolResources = () => {
|
||||
|
||||
const generateThumbnails = useCallback(async (files: File[]): Promise<string[]> => {
|
||||
const thumbnails: string[] = [];
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
thumbnails.push(thumbnail);
|
||||
if (thumbnail) {
|
||||
thumbnails.push(thumbnail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||
thumbnails.push('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return thumbnails;
|
||||
}, []);
|
||||
|
||||
@@ -65,12 +67,12 @@ export const useToolResources = () => {
|
||||
try {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
|
||||
const arrayBuffer = await zipBlob.arrayBuffer();
|
||||
const zipContent = await zip.loadAsync(arrayBuffer);
|
||||
|
||||
|
||||
const extractedFiles: File[] = [];
|
||||
|
||||
|
||||
for (const [filename, file] of Object.entries(zipContent.files)) {
|
||||
if (!file.dir) {
|
||||
const content = await file.async('blob');
|
||||
@@ -78,7 +80,7 @@ export const useToolResources = () => {
|
||||
extractedFiles.push(extractedFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return extractedFiles;
|
||||
} catch (error) {
|
||||
console.error('Error in extractAllZipFiles:', error);
|
||||
@@ -87,7 +89,7 @@ export const useToolResources = () => {
|
||||
}, []);
|
||||
|
||||
const createDownloadInfo = useCallback(async (
|
||||
files: File[],
|
||||
files: File[],
|
||||
operationType: string
|
||||
): Promise<{ url: string; filename: string }> => {
|
||||
if (files.length === 1) {
|
||||
@@ -100,7 +102,7 @@ export const useToolResources = () => {
|
||||
const { zipFile } = await zipFileService.createZipFromFiles(files, `${operationType}_results.zip`);
|
||||
const url = URL.createObjectURL(zipFile);
|
||||
addBlobUrl(url);
|
||||
|
||||
|
||||
return { url, filename: zipFile.name };
|
||||
}, [addBlobUrl]);
|
||||
|
||||
@@ -111,4 +113,4 @@ export const useToolResources = () => {
|
||||
extractAllZipFiles,
|
||||
cleanupBlobUrls,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { fileStorage } from '../services/fileStorage';
|
||||
import { FileWithUrl } from '../types/file';
|
||||
import { createEnhancedFileFromStored } from '../utils/fileUtils';
|
||||
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
||||
|
||||
export const useFileManager = () => {
|
||||
@@ -42,7 +43,7 @@ export const useFileManager = () => {
|
||||
try {
|
||||
const files = await fileStorage.getAllFiles();
|
||||
const sortedFiles = files.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
|
||||
return sortedFiles;
|
||||
return sortedFiles.map(file => createEnhancedFileFromStored(file));
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent files:', error);
|
||||
return [];
|
||||
@@ -66,10 +67,10 @@ export const useFileManager = () => {
|
||||
try {
|
||||
// Generate thumbnail for the file
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
|
||||
|
||||
// Store file with thumbnail
|
||||
const storedFile = await fileStorage.storeFile(file, thumbnail);
|
||||
|
||||
|
||||
// Add the ID to the file object
|
||||
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||
return storedFile;
|
||||
@@ -134,4 +135,4 @@ export const useFileManager = () => {
|
||||
touchFile,
|
||||
createFileSelectionHandlers
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,25 +4,25 @@ import { useMemo } from 'react';
|
||||
* Hook to convert a File object to { file: File; url: string } format
|
||||
* Creates blob URL on-demand and handles cleanup
|
||||
*/
|
||||
export function useFileWithUrl(file: File | null): { file: File; url: string } | null {
|
||||
export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; url: string } | null {
|
||||
return useMemo(() => {
|
||||
if (!file) return null;
|
||||
|
||||
|
||||
// Validate that file is a proper File or Blob object
|
||||
if (!(file instanceof File) && !(file instanceof Blob)) {
|
||||
console.warn('useFileWithUrl: Expected File or Blob, got:', file);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
|
||||
// Return object with cleanup function
|
||||
const result = { file, url };
|
||||
|
||||
|
||||
// Store cleanup function for later use
|
||||
(result as any)._cleanup = () => URL.revokeObjectURL(url);
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('useFileWithUrl: Failed to create object URL:', error, file);
|
||||
@@ -40,11 +40,11 @@ export function useFileWithUrlAndCleanup(file: File | null): {
|
||||
} {
|
||||
return useMemo(() => {
|
||||
if (!file) return { fileObj: null, cleanup: () => {} };
|
||||
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
const fileObj = { file, url };
|
||||
const cleanup = () => URL.revokeObjectURL(url);
|
||||
|
||||
|
||||
return { fileObj, cleanup };
|
||||
}, [file]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
||||
function calculateThumbnailScale(pageViewport: { width: number; height: number }): number {
|
||||
const maxWidth = 400; // Max thumbnail width
|
||||
const maxHeight = 600; // Max thumbnail height
|
||||
|
||||
|
||||
const scaleX = maxWidth / pageViewport.width;
|
||||
const scaleY = maxHeight / pageViewport.height;
|
||||
|
||||
|
||||
// Don't upscale, only downscale if needed
|
||||
return Math.min(scaleX, scaleY, 1.0);
|
||||
}
|
||||
@@ -22,16 +22,16 @@ function calculateThumbnailScale(pageViewport: { width: number; height: number }
|
||||
* Hook for IndexedDB-aware thumbnail loading
|
||||
* Handles thumbnail generation for files not in IndexedDB
|
||||
*/
|
||||
export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
thumbnail: string | null;
|
||||
isGenerating: boolean
|
||||
export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
thumbnail: string | null;
|
||||
isGenerating: boolean
|
||||
} {
|
||||
const [thumb, setThumb] = useState<string | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
|
||||
async function loadThumbnail() {
|
||||
if (!file) {
|
||||
setThumb(null);
|
||||
@@ -49,7 +49,7 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
setGenerating(true);
|
||||
try {
|
||||
let fileObject: File;
|
||||
|
||||
|
||||
// Handle IndexedDB files vs regular File objects
|
||||
if (file.storedInIndexedDB && file.id) {
|
||||
// For IndexedDB files, recreate File object from stored data
|
||||
@@ -61,9 +61,9 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
type: storedFile.type,
|
||||
lastModified: storedFile.lastModified
|
||||
});
|
||||
} else if (file.file) {
|
||||
} else if ((file as any /* Fix me */).file) {
|
||||
// For FileWithUrl objects that have a File object
|
||||
fileObject = file.file;
|
||||
fileObject = (file as any /* Fix me */).file;
|
||||
} else if (file.id) {
|
||||
// Fallback: try to get from IndexedDB even if storedInIndexedDB flag is missing
|
||||
const storedFile = await fileStorage.getFile(file.id);
|
||||
@@ -77,7 +77,7 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
} else {
|
||||
throw new Error('File object not available and no ID for IndexedDB lookup');
|
||||
}
|
||||
|
||||
|
||||
// Use the universal thumbnail generator
|
||||
const thumbnail = await generateThumbnailForFile(fileObject);
|
||||
if (!cancelled && thumbnail) {
|
||||
@@ -102,4 +102,4 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): {
|
||||
}, [file, file?.thumbnail, file?.id]);
|
||||
|
||||
return { thumbnail: thumb, isGenerating: generating };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user