Stirling-PDF/frontend/src/hooks/tools/convert/useConvertOperation.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

250 lines
8.1 KiB
TypeScript
Raw Normal View History

2025-07-23 17:23:25 +01:00
import { useCallback, useState, useEffect } from 'react';
2025-07-23 11:25:55 +01:00
import axios from 'axios';
import { useTranslation } from 'react-i18next';
import { useFileContext } from '../../../contexts/FileContext';
import { FileOperation } from '../../../types/fileContext';
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
import { makeApiUrl } from '../../../utils/api';
import { ConvertParameters } from './useConvertParameters';
import {
CONVERSION_ENDPOINTS,
ENDPOINT_NAMES,
EXTENSION_TO_ENDPOINT
} from '../../../constants/convertConstants';
2025-07-25 11:36:57 +01:00
import { getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
2025-07-23 11:25:55 +01:00
export interface ConvertOperationHook {
executeOperation: (
parameters: ConvertParameters,
selectedFiles: File[]
) => Promise<void>;
// Flattened result properties for cleaner access
files: File[];
thumbnails: string[];
isGeneratingThumbnails: boolean;
downloadUrl: string | null;
downloadFilename: string;
status: string;
errorMessage: string | null;
isLoading: boolean;
// Result management functions
resetResults: () => void;
clearError: () => void;
}
export const useConvertOperation = (): ConvertOperationHook => {
const { t } = useTranslation();
const {
recordOperation,
markOperationApplied,
markOperationFailed,
addFiles
} = useFileContext();
// Internal state management
const [files, setFiles] = useState<File[]>([]);
const [thumbnails, setThumbnails] = useState<string[]>([]);
const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [downloadFilename, setDownloadFilename] = useState('');
const [status, setStatus] = useState('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const buildFormData = useCallback((
parameters: ConvertParameters,
selectedFiles: File[]
) => {
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append("fileInput", file);
});
const { fromExtension, toExtension, imageOptions } = parameters;
// Add conversion-specific parameters
2025-07-25 11:36:57 +01:00
if (isImageFormat(toExtension)) {
formData.append("imageFormat", toExtension);
2025-07-23 11:25:55 +01:00
formData.append("colorType", imageOptions.colorType);
formData.append("dpi", imageOptions.dpi.toString());
formData.append("singleOrMultiple", imageOptions.singleOrMultiple);
} else if (fromExtension === 'pdf' && ['docx', 'odt'].includes(toExtension)) {
formData.append("outputFormat", toExtension);
} else if (fromExtension === 'pdf' && ['pptx', 'odp'].includes(toExtension)) {
formData.append("outputFormat", toExtension);
} else if (fromExtension === 'pdf' && ['txt', 'rtf'].includes(toExtension)) {
formData.append("outputFormat", toExtension);
2025-07-25 11:36:57 +01:00
} else if (isImageFormat(fromExtension) && toExtension === 'pdf') {
2025-07-23 11:25:55 +01:00
formData.append("fitOption", "fillPage");
formData.append("colorType", imageOptions.colorType);
formData.append("autoRotate", "true");
2025-07-28 13:58:43 +01:00
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
// CSV extraction requires page numbers parameter
formData.append("pageNumbers", parameters.pageNumbers || "all");
2025-07-23 11:25:55 +01:00
}
return formData;
}, []);
const createOperation = useCallback((
parameters: ConvertParameters,
selectedFiles: File[]
): { operation: FileOperation; operationId: string; fileId: string } => {
const operationId = `convert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const fileId = selectedFiles[0].name;
const operation: FileOperation = {
id: operationId,
type: 'convert',
timestamp: Date.now(),
fileIds: selectedFiles.map(f => f.name),
status: 'pending',
metadata: {
originalFileName: selectedFiles[0].name,
parameters: {
fromExtension: parameters.fromExtension,
toExtension: parameters.toExtension,
2025-07-28 13:58:43 +01:00
pageNumbers: parameters.pageNumbers,
2025-07-23 11:25:55 +01:00
imageOptions: parameters.imageOptions,
},
fileSize: selectedFiles[0].size
}
};
return { operation, operationId, fileId };
}, []);
const processResults = useCallback(async (blob: Blob, filename: string) => {
try {
// For single file conversions, create a file directly
const convertedFile = new File([blob], filename, { type: blob.type });
// Set local state for preview
setFiles([convertedFile]);
setThumbnails([]);
setIsGeneratingThumbnails(true);
// Add converted file to FileContext for future use
await addFiles([convertedFile]);
// Generate thumbnail for preview
try {
const thumbnail = await generateThumbnailForFile(convertedFile);
setThumbnails([thumbnail]);
} catch (error) {
console.warn(`Failed to generate thumbnail for ${filename}:`, error);
setThumbnails(['']);
}
setIsGeneratingThumbnails(false);
} catch (error) {
console.warn('Failed to process conversion result:', error);
}
}, [addFiles]);
const executeOperation = useCallback(async (
parameters: ConvertParameters,
selectedFiles: File[]
) => {
if (selectedFiles.length === 0) {
setStatus(t("noFileSelected"));
return;
}
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
const formData = buildFormData(parameters, selectedFiles);
2025-07-23 17:23:25 +01:00
// Get endpoint using utility function
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
2025-07-23 11:25:55 +01:00
if (!endpoint) {
setErrorMessage(t("convert.errorNotSupported", { from: parameters.fromExtension, to: parameters.toExtension }));
return;
}
recordOperation(fileId, operation);
setStatus(t("loading"));
setIsLoading(true);
setErrorMessage(null);
try {
const response = await axios.post(endpoint, formData, { responseType: "blob" });
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
// Generate filename based on conversion
const originalName = selectedFiles[0].name.split('.')[0];
const filename = `${originalName}_converted.${parameters.toExtension}`;
setDownloadUrl(url);
setDownloadFilename(filename);
setStatus(t("downloadComplete"));
await processResults(blob, filename);
markOperationApplied(fileId, operationId);
} catch (error: any) {
console.error(error);
let errorMsg = t("convert.errorConversion", "An error occurred while converting the file.");
if (error.response?.data && typeof error.response.data === 'string') {
errorMsg = error.response.data;
} else if (error.message) {
errorMsg = error.message;
}
setErrorMessage(errorMsg);
setStatus(t("error._value", "Conversion failed."));
markOperationFailed(fileId, operationId, errorMsg);
} finally {
setIsLoading(false);
}
}, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, processResults]);
const resetResults = useCallback(() => {
2025-07-23 17:23:25 +01:00
// Clean up blob URLs to prevent memory leaks
if (downloadUrl) {
window.URL.revokeObjectURL(downloadUrl);
}
2025-07-23 11:25:55 +01:00
setFiles([]);
setThumbnails([]);
setIsGeneratingThumbnails(false);
setDownloadUrl(null);
setDownloadFilename('');
setStatus('');
setErrorMessage(null);
setIsLoading(false);
2025-07-23 17:23:25 +01:00
}, [downloadUrl]);
2025-07-23 11:25:55 +01:00
const clearError = useCallback(() => {
setErrorMessage(null);
}, []);
2025-07-23 17:23:25 +01:00
// Cleanup blob URLs on unmount to prevent memory leaks
useEffect(() => {
return () => {
if (downloadUrl) {
window.URL.revokeObjectURL(downloadUrl);
}
};
}, [downloadUrl]);
2025-07-23 11:25:55 +01:00
return {
executeOperation,
// Flattened result properties for cleaner access
files,
thumbnails,
isGeneratingThumbnails,
downloadUrl,
downloadFilename,
status,
errorMessage,
isLoading,
// Result management functions
resetResults,
clearError,
};
};