Stirling-PDF/frontend/src/hooks/tools/convert/useConvertParameters.ts
ConnorYoh 23d86deae7
Feature/v2/automate (#4248)
* automate feature
* Moved all providers to app level to simplify homepage 
* Circular dependency fixes
* You will see that now toolRegistry gets a tool config and a tool
settings object. These enable automate to run the tools using as much
static code as possible.

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-22 14:40:27 +01:00

357 lines
12 KiB
TypeScript

import {
COLOR_TYPES,
OUTPUT_OPTIONS,
FIT_OPTIONS,
TO_FORMAT_OPTIONS,
CONVERSION_MATRIX,
type ColorType,
type OutputOption,
type FitOption
} from '../../../constants/convertConstants';
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat, getAvailableToExtensions as getAvailableToExtensionsUtil } from '../../../utils/convertUtils';
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
import { useCallback, useMemo } from 'react';
export interface ConvertParameters extends BaseParameters {
fromExtension: string;
toExtension: string;
imageOptions: {
colorType: ColorType;
dpi: number;
singleOrMultiple: OutputOption;
fitOption: FitOption;
autoRotate: boolean;
combineImages: boolean;
};
htmlOptions: {
zoomLevel: number;
};
emailOptions: {
includeAttachments: boolean;
maxAttachmentSizeMB: number;
downloadHtml: boolean;
includeAllRecipients: boolean;
};
pdfaOptions: {
outputFormat: string;
};
isSmartDetection: boolean;
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
export interface ConvertParametersHook extends BaseParametersHook<ConvertParameters> {
getEndpoint: () => string;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
analyzeFileTypes: (files: Array<{name: string}>) => void;
}
export const defaultParameters: ConvertParameters = {
fromExtension: '',
toExtension: '',
imageOptions: {
colorType: COLOR_TYPES.COLOR,
dpi: 300,
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
fitOption: FIT_OPTIONS.MAINTAIN_ASPECT,
autoRotate: true,
combineImages: true,
},
htmlOptions: {
zoomLevel: 1.0,
},
emailOptions: {
includeAttachments: true,
maxAttachmentSizeMB: 10,
downloadHtml: false,
includeAllRecipients: false,
},
pdfaOptions: {
outputFormat: 'pdfa-1',
},
isSmartDetection: false,
smartDetectionType: 'none',
};
const validateParameters = (params: ConvertParameters): boolean => {
const { fromExtension, toExtension } = params;
if (!fromExtension || !toExtension) return false;
// Handle dynamic format identifiers (file-<extension>)
let supportedToExtensions: string[] = [];
if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options
supportedToExtensions = CONVERSION_MATRIX['any'] || [];
} else {
// Regular format - check conversion matrix
supportedToExtensions = CONVERSION_MATRIX[fromExtension] || [];
}
if (!supportedToExtensions.includes(toExtension)) {
return false;
}
return true;
};
const getEndpointName = (params: ConvertParameters): string => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = params;
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint
return 'file-to-pdf';
} else if (smartDetectionType === 'images') {
// All images -> PDF using img-to-pdf endpoint
return 'img-to-pdf';
} else if (smartDetectionType === 'web') {
// All web files -> PDF using html-to-pdf endpoint
return 'html-to-pdf';
}
}
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return 'file-to-pdf';
}
return getEndpointNameUtil(fromExtension, toExtension);
};
export const useConvertParameters = (): ConvertParametersHook => {
const config = useMemo(() => ({
defaultParameters,
endpointName: getEndpointName,
validateFn: validateParameters,
}), []);
const baseHook = useBaseParameters(config);
const getEndpoint = () => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = baseHook.parameters;
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint
return '/api/v1/convert/file/pdf';
} else if (smartDetectionType === 'images') {
// All images -> PDF using img-to-pdf endpoint
return '/api/v1/convert/img/pdf';
} else if (smartDetectionType === 'web') {
// All web files -> PDF using html-to-pdf endpoint
return '/api/v1/convert/html/pdf';
}
}
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return '/api/v1/convert/file/pdf';
}
return getEndpointUrl(fromExtension, toExtension);
};
const getAvailableToExtensions = getAvailableToExtensionsUtil;
const analyzeFileTypes = useCallback((files: Array<{name: string}>) => {
if (files.length === 0) {
// No files - only reset smart detection, keep user's format choices
baseHook.setParameters(prev => {
// Only update if something actually changed
if (prev.isSmartDetection === false && prev.smartDetectionType === 'none') {
return prev; // No change needed
}
return {
...prev,
isSmartDetection: false,
smartDetectionType: 'none'
// Don't reset fromExtension and toExtension - let user keep their choices
};
});
return;
}
if (files.length === 1) {
// Single file - use regular detection with smart target selection
const detectedExt = detectFileExtensionUtil(files[0].name);
let fromExt = detectedExt;
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
// If no explicit conversion exists for this file type, create a dynamic format entry
// and fall back to 'any' conversion logic for the actual endpoint
if (availableTargets.length === 0 && detectedExt) {
fromExt = `file-${detectedExt}`; // Create dynamic format identifier
availableTargets = CONVERSION_MATRIX['any'] || [];
} else if (availableTargets.length === 0) {
// No extension detected - fall back to 'any'
fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || [];
}
baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt);
// Auto-select target only if:
// 1. No current target is set, OR
// 2. Current target is invalid for new source type, OR
// 3. There's only one possible target (forced conversion)
let newToExtension = currentToExt;
if (!currentToExt || !isCurrentToExtValid) {
newToExtension = availableTargets.length === 1 ? availableTargets[0] : '';
}
const newState = {
...prev,
isSmartDetection: false,
smartDetectionType: 'none' as const,
fromExtension: fromExt,
toExtension: newToExtension
};
// Only update if something actually changed
if (
prev.isSmartDetection === newState.isSmartDetection &&
prev.smartDetectionType === newState.smartDetectionType &&
prev.fromExtension === newState.fromExtension &&
prev.toExtension === newState.toExtension
) {
return prev; // Return the same object to prevent re-render
}
return newState;
});
return;
}
// Multiple files - analyze file types
const extensions = files.map(file => detectFileExtensionUtil(file.name));
const uniqueExtensions = [...new Set(extensions)];
if (uniqueExtensions.length === 1) {
// All files are the same type - use regular detection with smart target selection
const detectedExt = uniqueExtensions[0];
let fromExt = detectedExt;
let availableTargets = CONVERSION_MATRIX[detectedExt] || [];
// If no explicit conversion exists for this file type, fall back to 'any'
if (availableTargets.length === 0) {
fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || [];
}
baseHook.setParameters(prev => {
// Check if current toExtension is still valid for the new fromExtension
const currentToExt = prev.toExtension;
const isCurrentToExtValid = availableTargets.includes(currentToExt);
// Auto-select target only if:
// 1. No current target is set, OR
// 2. Current target is invalid for new source type, OR
// 3. There's only one possible target (forced conversion)
let newToExtension = currentToExt;
if (!currentToExt || !isCurrentToExtValid) {
newToExtension = availableTargets.length === 1 ? availableTargets[0] : '';
}
const newState = {
...prev,
isSmartDetection: false,
smartDetectionType: 'none' as const,
fromExtension: fromExt,
toExtension: newToExtension
};
// Only update if something actually changed
if (
prev.isSmartDetection === newState.isSmartDetection &&
prev.smartDetectionType === newState.smartDetectionType &&
prev.fromExtension === newState.fromExtension &&
prev.toExtension === newState.toExtension
) {
return prev; // Return the same object to prevent re-render
}
return newState;
});
} else {
// Mixed file types
const allImages = uniqueExtensions.every(ext => isImageFormat(ext));
const allWeb = uniqueExtensions.every(ext => isWebFormat(ext));
if (allImages) {
// All files are images - use image-to-pdf conversion
baseHook.setParameters(prev => {
// Only update if something actually changed
if (prev.isSmartDetection === true &&
prev.smartDetectionType === 'images' &&
prev.fromExtension === 'image' &&
prev.toExtension === 'pdf') {
return prev; // No change needed
}
return {
...prev,
isSmartDetection: true,
smartDetectionType: 'images',
fromExtension: 'image',
toExtension: 'pdf'
};
});
} else if (allWeb) {
// All files are web files - use html-to-pdf conversion
baseHook.setParameters(prev => {
// Only update if something actually changed
if (prev.isSmartDetection === true &&
prev.smartDetectionType === 'web' &&
prev.fromExtension === 'html' &&
prev.toExtension === 'pdf') {
return prev; // No change needed
}
return {
...prev,
isSmartDetection: true,
smartDetectionType: 'web',
fromExtension: 'html',
toExtension: 'pdf'
};
});
} else {
// Mixed non-image types - use file-to-pdf conversion
baseHook.setParameters(prev => {
// Only update if something actually changed
if (prev.isSmartDetection === true &&
prev.smartDetectionType === 'mixed' &&
prev.fromExtension === 'any' &&
prev.toExtension === 'pdf') {
return prev; // No change needed
}
return {
...prev,
isSmartDetection: true,
smartDetectionType: 'mixed',
fromExtension: 'any',
toExtension: 'pdf'
};
});
}
}
}, [baseHook.setParameters]);
return {
...baseHook,
getEndpoint,
getAvailableToExtensions,
analyzeFileTypes,
};
};