mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-06 04:25:22 +00:00
Working tool
This commit is contained in:
parent
abd35ae790
commit
fabf2c5310
@ -45,6 +45,13 @@ files,
|
|||||||
onPreviewFile={onPreviewFile}
|
onPreviewFile={onPreviewFile}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "convert":
|
||||||
|
return (
|
||||||
|
<ToolComponent
|
||||||
|
selectedFiles={toolSelectedFiles}
|
||||||
|
onPreviewFile={onPreviewFile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "merge":
|
case "merge":
|
||||||
return (
|
return (
|
||||||
<ToolComponent
|
<ToolComponent
|
||||||
|
165
frontend/src/components/tools/convert/ConvertSettings.tsx
Normal file
165
frontend/src/components/tools/convert/ConvertSettings.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack, Text, Select, NumberInput, Group, Divider } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
|
import {
|
||||||
|
FROM_FORMAT_OPTIONS,
|
||||||
|
TO_FORMAT_OPTIONS,
|
||||||
|
COLOR_TYPES,
|
||||||
|
OUTPUT_OPTIONS,
|
||||||
|
} from "../../../constants/convertConstants";
|
||||||
|
|
||||||
|
interface ConvertSettingsProps {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||||
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConvertSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
getAvailableToExtensions,
|
||||||
|
disabled = false
|
||||||
|
}: ConvertSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleFromExtensionChange = (value: string | null) => {
|
||||||
|
if (value) {
|
||||||
|
onParameterChange('fromExtension', value);
|
||||||
|
// Reset to extension when from extension changes
|
||||||
|
onParameterChange('toExtension', '');
|
||||||
|
// Reset format-specific options
|
||||||
|
onParameterChange('imageOptions', {
|
||||||
|
colorType: COLOR_TYPES.COLOR,
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToExtensionChange = (value: string | null) => {
|
||||||
|
if (value) {
|
||||||
|
onParameterChange('toExtension', value);
|
||||||
|
// Reset format-specific options when target extension changes
|
||||||
|
onParameterChange('imageOptions', {
|
||||||
|
colorType: COLOR_TYPES.COLOR,
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Format Selection */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("convert.convertFrom", "Convert from")}:
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={parameters.fromExtension}
|
||||||
|
onChange={handleFromExtensionChange}
|
||||||
|
data={FROM_FORMAT_OPTIONS.map(option => ({ value: option.value, label: option.label }))}
|
||||||
|
disabled={disabled}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
placeholder="Select source file format"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("convert.convertTo", "Convert to")}:
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={parameters.toExtension}
|
||||||
|
onChange={handleToExtensionChange}
|
||||||
|
data={(getAvailableToExtensions(parameters.fromExtension) || []).map(option => ({ value: option.value, label: option.label }))}
|
||||||
|
disabled={!parameters.fromExtension || disabled}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
placeholder="Select target file format"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Format-specific options */}
|
||||||
|
{['png', 'jpg'].includes(parameters.toExtension) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.imageOptions", "Image Options")}:</Text>
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
label={t("convert.colorType", "Color Type")}
|
||||||
|
value={parameters.imageOptions.colorType}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
colorType: val as typeof COLOR_TYPES[keyof typeof COLOR_TYPES]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||||
|
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||||
|
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t("convert.dpi", "DPI")}
|
||||||
|
value={parameters.imageOptions.dpi}
|
||||||
|
onChange={(val) => typeof val === 'number' && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
dpi: val
|
||||||
|
})}
|
||||||
|
min={72}
|
||||||
|
max={600}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Select
|
||||||
|
label={t("convert.output", "Output")}
|
||||||
|
value={parameters.imageOptions.singleOrMultiple}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
singleOrMultiple: val as typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: OUTPUT_OPTIONS.SINGLE, label: t("convert.single", "Single") },
|
||||||
|
{ value: OUTPUT_OPTIONS.MULTIPLE, label: t("convert.multiple", "Multiple") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Color options for image to PDF conversion */}
|
||||||
|
{['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'].includes(parameters.fromExtension) && parameters.toExtension === 'pdf' && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
||||||
|
<Select
|
||||||
|
label={t("convert.colorType", "Color Type")}
|
||||||
|
value={parameters.imageOptions.colorType}
|
||||||
|
onChange={(val) => val && onParameterChange('imageOptions', {
|
||||||
|
...parameters.imageOptions,
|
||||||
|
colorType: val as typeof COLOR_TYPES[keyof typeof COLOR_TYPES]
|
||||||
|
})}
|
||||||
|
data={[
|
||||||
|
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||||
|
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||||
|
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertSettings;
|
@ -33,14 +33,16 @@ const ToolStep = ({
|
|||||||
}: ToolStepProps) => {
|
}: ToolStepProps) => {
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
// Get context at the top level
|
||||||
|
const parent = useContext(ToolStepContext);
|
||||||
|
|
||||||
// Auto-detect if we should show numbers based on sibling count
|
// Auto-detect if we should show numbers based on sibling count
|
||||||
const shouldShowNumber = useMemo(() => {
|
const shouldShowNumber = useMemo(() => {
|
||||||
if (showNumber !== undefined) return showNumber;
|
if (showNumber !== undefined) return showNumber;
|
||||||
const parent = useContext(ToolStepContext);
|
|
||||||
return parent ? parent.visibleStepCount >= 3 : false;
|
return parent ? parent.visibleStepCount >= 3 : false;
|
||||||
}, [showNumber]);
|
}, [showNumber, parent]);
|
||||||
|
|
||||||
const stepNumber = useContext(ToolStepContext)?.getStepNumber?.() || 1;
|
const stepNumber = parent?.getStepNumber?.() || 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
|
159
frontend/src/constants/convertConstants.ts
Normal file
159
frontend/src/constants/convertConstants.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
export const FROM_FORMATS = {
|
||||||
|
PDF: 'pdf',
|
||||||
|
OFFICE: 'office',
|
||||||
|
IMAGE: 'image',
|
||||||
|
HTML: 'html',
|
||||||
|
MARKDOWN: 'markdown',
|
||||||
|
TEXT: 'text'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const TO_FORMATS = {
|
||||||
|
PDF: 'pdf',
|
||||||
|
IMAGE: 'image',
|
||||||
|
OFFICE_WORD: 'office-word',
|
||||||
|
OFFICE_PRESENTATION: 'office-presentation',
|
||||||
|
OFFICE_TEXT: 'office-text',
|
||||||
|
HTML: 'html',
|
||||||
|
XML: 'xml'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const COLOR_TYPES = {
|
||||||
|
COLOR: 'color',
|
||||||
|
GREYSCALE: 'greyscale',
|
||||||
|
BLACK_WHITE: 'blackwhite'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const OUTPUT_OPTIONS = {
|
||||||
|
SINGLE: 'single',
|
||||||
|
MULTIPLE: 'multiple'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const OFFICE_FORMATS = {
|
||||||
|
DOCX: 'docx',
|
||||||
|
ODT: 'odt',
|
||||||
|
PPTX: 'pptx',
|
||||||
|
ODP: 'odp',
|
||||||
|
TXT: 'txt',
|
||||||
|
RTF: 'rtf'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CONVERSION_ENDPOINTS = {
|
||||||
|
'office-pdf': '/api/v1/convert/file/pdf',
|
||||||
|
'pdf-image': '/api/v1/convert/pdf/img',
|
||||||
|
'image-pdf': '/api/v1/convert/img/pdf',
|
||||||
|
'pdf-office-word': '/api/v1/convert/pdf/word',
|
||||||
|
'pdf-office-presentation': '/api/v1/convert/pdf/presentation',
|
||||||
|
'pdf-office-text': '/api/v1/convert/pdf/text',
|
||||||
|
'pdf-html': '/api/v1/convert/pdf/html',
|
||||||
|
'pdf-xml': '/api/v1/convert/pdf/xml',
|
||||||
|
'html-pdf': '/api/v1/convert/html/pdf',
|
||||||
|
'markdown-pdf': '/api/v1/convert/markdown/pdf'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ENDPOINT_NAMES = {
|
||||||
|
'office-pdf': 'file-to-pdf',
|
||||||
|
'pdf-image': 'pdf-to-img',
|
||||||
|
'image-pdf': 'img-to-pdf',
|
||||||
|
'pdf-office-word': 'pdf-to-word',
|
||||||
|
'pdf-office-presentation': 'pdf-to-presentation',
|
||||||
|
'pdf-office-text': 'pdf-to-text',
|
||||||
|
'pdf-html': 'pdf-to-html',
|
||||||
|
'pdf-xml': 'pdf-to-xml',
|
||||||
|
'html-pdf': 'html-to-pdf',
|
||||||
|
'markdown-pdf': 'markdown-to-pdf'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SUPPORTED_CONVERSIONS: Record<string, string[]> = {
|
||||||
|
[FROM_FORMATS.PDF]: [TO_FORMATS.IMAGE, TO_FORMATS.OFFICE_WORD, TO_FORMATS.OFFICE_PRESENTATION, TO_FORMATS.OFFICE_TEXT, TO_FORMATS.HTML, TO_FORMATS.XML],
|
||||||
|
[FROM_FORMATS.OFFICE]: [TO_FORMATS.PDF],
|
||||||
|
[FROM_FORMATS.IMAGE]: [TO_FORMATS.PDF],
|
||||||
|
[FROM_FORMATS.HTML]: [TO_FORMATS.PDF],
|
||||||
|
[FROM_FORMATS.MARKDOWN]: [TO_FORMATS.PDF],
|
||||||
|
[FROM_FORMATS.TEXT]: [TO_FORMATS.PDF]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FILE_EXTENSIONS = {
|
||||||
|
[FROM_FORMATS.PDF]: ['pdf'],
|
||||||
|
[FROM_FORMATS.OFFICE]: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'],
|
||||||
|
[FROM_FORMATS.IMAGE]: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'],
|
||||||
|
[FROM_FORMATS.HTML]: ['html', 'htm'],
|
||||||
|
[FROM_FORMATS.MARKDOWN]: ['md'],
|
||||||
|
[FROM_FORMATS.TEXT]: ['txt', 'rtf']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grouped file extensions for dropdowns
|
||||||
|
export const FROM_FORMAT_OPTIONS = [
|
||||||
|
{ value: 'pdf', label: 'PDF', group: 'Document' },
|
||||||
|
{ value: 'docx', label: 'Word Document (.docx)', group: 'Office Documents' },
|
||||||
|
{ value: 'doc', label: 'Word Document (.doc)', group: 'Office Documents' },
|
||||||
|
{ value: 'xlsx', label: 'Excel Spreadsheet (.xlsx)', group: 'Office Documents' },
|
||||||
|
{ value: 'xls', label: 'Excel Spreadsheet (.xls)', group: 'Office Documents' },
|
||||||
|
{ value: 'pptx', label: 'PowerPoint (.pptx)', group: 'Office Documents' },
|
||||||
|
{ value: 'ppt', label: 'PowerPoint (.ppt)', group: 'Office Documents' },
|
||||||
|
{ value: 'odt', label: 'OpenDocument Text (.odt)', group: 'Office Documents' },
|
||||||
|
{ value: 'ods', label: 'OpenDocument Spreadsheet (.ods)', group: 'Office Documents' },
|
||||||
|
{ value: 'odp', label: 'OpenDocument Presentation (.odp)', group: 'Office Documents' },
|
||||||
|
{ value: 'jpg', label: 'JPEG Image (.jpg)', group: 'Images' },
|
||||||
|
{ value: 'jpeg', label: 'JPEG Image (.jpeg)', group: 'Images' },
|
||||||
|
{ value: 'png', label: 'PNG Image (.png)', group: 'Images' },
|
||||||
|
{ value: 'gif', label: 'GIF Image (.gif)', group: 'Images' },
|
||||||
|
{ value: 'bmp', label: 'BMP Image (.bmp)', group: 'Images' },
|
||||||
|
{ value: 'tiff', label: 'TIFF Image (.tiff)', group: 'Images' },
|
||||||
|
{ value: 'webp', label: 'WebP Image (.webp)', group: 'Images' },
|
||||||
|
{ value: 'html', label: 'HTML (.html)', group: 'Web' },
|
||||||
|
{ value: 'htm', label: 'HTML (.htm)', group: 'Web' },
|
||||||
|
{ value: 'md', label: 'Markdown (.md)', group: 'Text' },
|
||||||
|
{ value: 'txt', label: 'Text File (.txt)', group: 'Text' },
|
||||||
|
{ value: 'rtf', label: 'Rich Text Format (.rtf)', group: 'Text' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TO_FORMAT_OPTIONS = [
|
||||||
|
{ value: 'pdf', label: 'PDF', group: 'Document' },
|
||||||
|
{ value: 'docx', label: 'Word Document (.docx)', group: 'Office Documents' },
|
||||||
|
{ value: 'odt', label: 'OpenDocument Text (.odt)', group: 'Office Documents' },
|
||||||
|
{ value: 'pptx', label: 'PowerPoint (.pptx)', group: 'Office Documents' },
|
||||||
|
{ value: 'odp', label: 'OpenDocument Presentation (.odp)', group: 'Office Documents' },
|
||||||
|
{ value: 'txt', label: 'Text File (.txt)', group: 'Text' },
|
||||||
|
{ value: 'rtf', label: 'Rich Text Format (.rtf)', group: 'Text' },
|
||||||
|
{ value: 'png', label: 'PNG Image (.png)', group: 'Images' },
|
||||||
|
{ value: 'jpg', label: 'JPEG Image (.jpg)', group: 'Images' },
|
||||||
|
{ value: 'html', label: 'HTML (.html)', group: 'Web' },
|
||||||
|
{ value: 'xml', label: 'XML (.xml)', group: 'Web' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Conversion matrix - what each source format can convert to
|
||||||
|
export const CONVERSION_MATRIX: Record<string, string[]> = {
|
||||||
|
'pdf': ['png', 'jpg', 'docx', 'odt', 'pptx', 'odp', 'txt', 'rtf', 'html', 'xml'],
|
||||||
|
'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'],
|
||||||
|
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
||||||
|
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'],
|
||||||
|
'jpg': ['pdf'], 'jpeg': ['pdf'], 'png': ['pdf'], 'gif': ['pdf'], 'bmp': ['pdf'], 'tiff': ['pdf'], 'webp': ['pdf'],
|
||||||
|
'html': ['pdf'], 'htm': ['pdf'],
|
||||||
|
'md': ['pdf'],
|
||||||
|
'txt': ['pdf'], 'rtf': ['pdf']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map extensions to endpoint keys
|
||||||
|
export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
||||||
|
'pdf': {
|
||||||
|
'png': 'pdf-to-img', 'jpg': 'pdf-to-img',
|
||||||
|
'docx': 'pdf-to-word', 'odt': 'pdf-to-word',
|
||||||
|
'pptx': 'pdf-to-presentation', 'odp': 'pdf-to-presentation',
|
||||||
|
'txt': 'pdf-to-text', 'rtf': 'pdf-to-text',
|
||||||
|
'html': 'pdf-to-html', 'xml': 'pdf-to-xml'
|
||||||
|
},
|
||||||
|
'docx': { 'pdf': 'file-to-pdf' }, 'doc': { 'pdf': 'file-to-pdf' }, 'odt': { 'pdf': 'file-to-pdf' },
|
||||||
|
'xlsx': { 'pdf': 'file-to-pdf' }, 'xls': { 'pdf': 'file-to-pdf' }, 'ods': { 'pdf': 'file-to-pdf' },
|
||||||
|
'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' },
|
||||||
|
'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' },
|
||||||
|
'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' },
|
||||||
|
'html': { 'pdf': 'html-to-pdf' }, 'htm': { 'pdf': 'html-to-pdf' },
|
||||||
|
'md': { 'pdf': 'markdown-to-pdf' },
|
||||||
|
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FromFormat = typeof FROM_FORMATS[keyof typeof FROM_FORMATS];
|
||||||
|
export type ToFormat = typeof TO_FORMATS[keyof typeof TO_FORMATS];
|
||||||
|
export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES];
|
||||||
|
export type OutputOption = typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS];
|
||||||
|
export type OfficeFormat = typeof OFFICE_FORMATS[keyof typeof OFFICE_FORMATS];
|
245
frontend/src/hooks/tools/convert/useConvertOperation.ts
Normal file
245
frontend/src/hooks/tools/convert/useConvertOperation.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
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
|
||||||
|
if (['png', 'jpg'].includes(toExtension)) {
|
||||||
|
formData.append("imageFormat", toExtension === 'jpg' ? 'jpg' : 'png');
|
||||||
|
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);
|
||||||
|
} else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'].includes(fromExtension) && toExtension === 'pdf') {
|
||||||
|
formData.append("fitOption", "fillPage");
|
||||||
|
formData.append("colorType", imageOptions.colorType);
|
||||||
|
formData.append("autoRotate", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Get endpoint using constants
|
||||||
|
const getEndpoint = () => {
|
||||||
|
const { fromExtension, toExtension } = parameters;
|
||||||
|
const endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
|
||||||
|
if (!endpointKey) return '';
|
||||||
|
|
||||||
|
// Find the endpoint URL from CONVERSION_ENDPOINTS using the endpoint name
|
||||||
|
for (const [key, endpoint] of Object.entries(CONVERSION_ENDPOINTS)) {
|
||||||
|
if (ENDPOINT_NAMES[key as keyof typeof ENDPOINT_NAMES] === endpointKey) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getEndpoint();
|
||||||
|
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(() => {
|
||||||
|
setFiles([]);
|
||||||
|
setThumbnails([]);
|
||||||
|
setIsGeneratingThumbnails(false);
|
||||||
|
setDownloadUrl(null);
|
||||||
|
setDownloadFilename('');
|
||||||
|
setStatus('');
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeOperation,
|
||||||
|
|
||||||
|
// Flattened result properties for cleaner access
|
||||||
|
files,
|
||||||
|
thumbnails,
|
||||||
|
isGeneratingThumbnails,
|
||||||
|
downloadUrl,
|
||||||
|
downloadFilename,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
|
// Result management functions
|
||||||
|
resetResults,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
};
|
129
frontend/src/hooks/tools/convert/useConvertParameters.ts
Normal file
129
frontend/src/hooks/tools/convert/useConvertParameters.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
FROM_FORMATS,
|
||||||
|
TO_FORMATS,
|
||||||
|
COLOR_TYPES,
|
||||||
|
OUTPUT_OPTIONS,
|
||||||
|
OFFICE_FORMATS,
|
||||||
|
CONVERSION_ENDPOINTS,
|
||||||
|
ENDPOINT_NAMES,
|
||||||
|
SUPPORTED_CONVERSIONS,
|
||||||
|
FILE_EXTENSIONS,
|
||||||
|
FROM_FORMAT_OPTIONS,
|
||||||
|
TO_FORMAT_OPTIONS,
|
||||||
|
CONVERSION_MATRIX,
|
||||||
|
EXTENSION_TO_ENDPOINT,
|
||||||
|
type FromFormat,
|
||||||
|
type ToFormat,
|
||||||
|
type ColorType,
|
||||||
|
type OutputOption,
|
||||||
|
type OfficeFormat
|
||||||
|
} from '../../../constants/convertConstants';
|
||||||
|
|
||||||
|
export interface ConvertParameters {
|
||||||
|
fromExtension: string;
|
||||||
|
toExtension: string;
|
||||||
|
imageOptions: {
|
||||||
|
colorType: ColorType;
|
||||||
|
dpi: number;
|
||||||
|
singleOrMultiple: OutputOption;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConvertParametersHook {
|
||||||
|
parameters: ConvertParameters;
|
||||||
|
updateParameter: (parameter: keyof ConvertParameters, value: any) => void;
|
||||||
|
resetParameters: () => void;
|
||||||
|
validateParameters: () => boolean;
|
||||||
|
getEndpointName: () => string;
|
||||||
|
getEndpoint: () => string;
|
||||||
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
|
detectFileExtension: (filename: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialParameters: ConvertParameters = {
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: '',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: COLOR_TYPES.COLOR,
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: OUTPUT_OPTIONS.MULTIPLE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConvertParameters = (): ConvertParametersHook => {
|
||||||
|
const [parameters, setParameters] = useState<ConvertParameters>(initialParameters);
|
||||||
|
|
||||||
|
const updateParameter = (parameter: keyof ConvertParameters, value: any) => {
|
||||||
|
setParameters(prev => ({ ...prev, [parameter]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetParameters = () => {
|
||||||
|
setParameters(initialParameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateParameters = () => {
|
||||||
|
const { fromExtension, toExtension } = parameters;
|
||||||
|
|
||||||
|
if (!fromExtension || !toExtension) return false;
|
||||||
|
|
||||||
|
// Check if conversion is supported
|
||||||
|
const supportedToExtensions = CONVERSION_MATRIX[fromExtension];
|
||||||
|
if (!supportedToExtensions || !supportedToExtensions.includes(toExtension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional validation for image conversions
|
||||||
|
if (['png', 'jpg'].includes(toExtension)) {
|
||||||
|
return parameters.imageOptions.dpi >= 72 && parameters.imageOptions.dpi <= 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEndpointName = () => {
|
||||||
|
const { fromExtension, toExtension } = parameters;
|
||||||
|
if (!fromExtension || !toExtension) return '';
|
||||||
|
|
||||||
|
const endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
|
||||||
|
return endpointKey || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEndpoint = () => {
|
||||||
|
const endpointName = getEndpointName();
|
||||||
|
if (!endpointName) return '';
|
||||||
|
|
||||||
|
// Find the endpoint URL from CONVERSION_ENDPOINTS using the endpoint name
|
||||||
|
for (const [key, endpoint] of Object.entries(CONVERSION_ENDPOINTS)) {
|
||||||
|
if (ENDPOINT_NAMES[key as keyof typeof ENDPOINT_NAMES] === endpointName) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableToExtensions = (fromExtension: string) => {
|
||||||
|
if (!fromExtension) return [];
|
||||||
|
|
||||||
|
const supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
|
||||||
|
return TO_FORMAT_OPTIONS.filter(option =>
|
||||||
|
supportedExtensions.includes(option.value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectFileExtension = (filename: string): string => {
|
||||||
|
const extension = filename.split('.').pop()?.toLowerCase();
|
||||||
|
return extension || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters,
|
||||||
|
getEndpointName,
|
||||||
|
getEndpoint,
|
||||||
|
getAvailableToExtensions,
|
||||||
|
detectFileExtension,
|
||||||
|
};
|
||||||
|
};
|
@ -3,9 +3,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||||
|
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
||||||
import SplitPdfPanel from "../tools/Split";
|
import SplitPdfPanel from "../tools/Split";
|
||||||
import CompressPdfPanel from "../tools/Compress";
|
import CompressPdfPanel from "../tools/Compress";
|
||||||
import MergePdfPanel from "../tools/Merge";
|
import MergePdfPanel from "../tools/Merge";
|
||||||
|
import ConvertPanel from "../tools/Convert";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
|
|
||||||
type ToolRegistryEntry = {
|
type ToolRegistryEntry = {
|
||||||
@ -23,6 +25,7 @@ const baseToolRegistry = {
|
|||||||
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "split" },
|
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "split" },
|
||||||
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "compress" },
|
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "compress" },
|
||||||
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" },
|
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" },
|
||||||
|
convert: { icon: <SwapHorizIcon />, component: ConvertPanel, view: "convert" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tool endpoint mappings
|
// Tool endpoint mappings
|
||||||
@ -30,6 +33,7 @@ const toolEndpoints: Record<string, string[]> = {
|
|||||||
split: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"],
|
split: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"],
|
||||||
compress: ["compress-pdf"],
|
compress: ["compress-pdf"],
|
||||||
merge: ["merge-pdfs"],
|
merge: ["merge-pdfs"],
|
||||||
|
convert: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -224,6 +224,11 @@ export default function HomePage() {
|
|||||||
setCurrentView('compress');
|
setCurrentView('compress');
|
||||||
setLeftPanelView('toolContent');
|
setLeftPanelView('toolContent');
|
||||||
sessionStorage.removeItem('previousMode');
|
sessionStorage.removeItem('previousMode');
|
||||||
|
} else if (previousMode === 'convert') {
|
||||||
|
selectTool('convert');
|
||||||
|
setCurrentView('convert');
|
||||||
|
setLeftPanelView('toolContent');
|
||||||
|
sessionStorage.removeItem('previousMode');
|
||||||
} else {
|
} else {
|
||||||
setCurrentView('fileEditor');
|
setCurrentView('fileEditor');
|
||||||
}
|
}
|
||||||
@ -273,6 +278,16 @@ export default function HomePage() {
|
|||||||
setToolSelectedFiles(files);
|
setToolSelectedFiles(files);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : currentView === "convert" ? (
|
||||||
|
<FileEditor
|
||||||
|
toolMode={true}
|
||||||
|
multiSelect={false}
|
||||||
|
showUpload={true}
|
||||||
|
showBulkActions={true}
|
||||||
|
onFileSelect={(files) => {
|
||||||
|
setToolSelectedFiles(files);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : selectedToolKey && selectedTool ? (
|
) : selectedToolKey && selectedTool ? (
|
||||||
<ToolRenderer
|
<ToolRenderer
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
|
@ -1,418 +1,172 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import {
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
Alert,
|
|
||||||
Divider,
|
|
||||||
Select,
|
|
||||||
NumberInput,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { ArrowDownward } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FileWithUrl } from "../types/file";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
|
||||||
export interface ConvertPanelProps {
|
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||||
files: FileWithUrl[];
|
import OperationButton from "../components/tools/shared/OperationButton";
|
||||||
setDownloadUrl: (url: string) => void;
|
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||||
params: {
|
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||||
fromFormat: string;
|
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||||
toFormat: string;
|
|
||||||
imageOptions?: {
|
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||||
colorType: string;
|
|
||||||
dpi: number;
|
import { useConvertParameters } from "../hooks/tools/convert/useConvertParameters";
|
||||||
singleOrMultiple: string;
|
import { useConvertOperation } from "../hooks/tools/convert/useConvertOperation";
|
||||||
};
|
|
||||||
officeOptions?: {
|
interface ConvertProps {
|
||||||
outputFormat: string;
|
selectedFiles?: File[];
|
||||||
};
|
onPreviewFile?: (file: File | null) => void;
|
||||||
};
|
|
||||||
updateParams: (newParams: Partial<ConvertPanelProps["params"]>) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConvertPanel: React.FC<ConvertPanelProps> = ({ files, setDownloadUrl, params, updateParams }) => {
|
const Convert = ({ selectedFiles = [], onPreviewFile }: ConvertProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
const { setCurrentMode } = useFileContext();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [fromFormat, setFromFormat] = useState(params.fromFormat || "");
|
|
||||||
const [toFormat, setToFormat] = useState(params.toFormat || "");
|
|
||||||
const [colorType, setColorType] = useState(params.imageOptions?.colorType || "color");
|
|
||||||
const [dpi, setDpi] = useState(params.imageOptions?.dpi || 300);
|
|
||||||
const [singleOrMultiple, setSingleOrMultiple] = useState(params.imageOptions?.singleOrMultiple || "multiple");
|
|
||||||
const [outputFormat, setOutputFormat] = useState(params.officeOptions?.outputFormat || "");
|
|
||||||
|
|
||||||
useEffect(() => {
|
const convertParams = useConvertParameters();
|
||||||
if (files.length > 0 && !fromFormat) {
|
const convertOperation = useConvertOperation();
|
||||||
const firstFile = files[0];
|
|
||||||
const detectedFormat = detectFileFormat(firstFile.name);
|
|
||||||
setFromFormat(detectedFormat);
|
|
||||||
updateParams({ fromFormat: detectedFormat });
|
|
||||||
}
|
|
||||||
}, [files, fromFormat]);
|
|
||||||
|
|
||||||
const detectFileFormat = (filename: string): string => {
|
|
||||||
const extension = filename.split('.').pop()?.toLowerCase();
|
|
||||||
switch (extension) {
|
|
||||||
case 'pdf': return 'pdf';
|
|
||||||
case 'doc': case 'docx': return 'office';
|
|
||||||
case 'xls': case 'xlsx': return 'office';
|
|
||||||
case 'ppt': case 'pptx': return 'office';
|
|
||||||
case 'odt': case 'ods': case 'odp': return 'office';
|
|
||||||
case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': case 'tiff': case 'webp': return 'image';
|
|
||||||
case 'html': case 'htm': return 'html';
|
|
||||||
case 'md': return 'markdown';
|
|
||||||
case 'txt': case 'rtf': return 'text';
|
|
||||||
default: return 'unknown';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailableToFormats = (from: string): string[] => {
|
|
||||||
switch (from) {
|
|
||||||
case 'pdf':
|
|
||||||
return ['image', 'office-word', 'office-presentation', 'office-text', 'html', 'xml'];
|
|
||||||
case 'office':
|
|
||||||
return ['pdf'];
|
|
||||||
case 'image':
|
|
||||||
return ['pdf'];
|
|
||||||
case 'html':
|
|
||||||
return ['pdf'];
|
|
||||||
case 'markdown':
|
|
||||||
return ['pdf'];
|
|
||||||
case 'text':
|
|
||||||
return ['pdf'];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getApiEndpoint = (from: string, to: string): string => {
|
|
||||||
if (from === 'office' && to === 'pdf') {
|
|
||||||
return '/api/v1/convert/file/pdf';
|
|
||||||
} else if (from === 'pdf' && to === 'image') {
|
|
||||||
return '/api/v1/convert/pdf/img';
|
|
||||||
} else if (from === 'image' && to === 'pdf') {
|
|
||||||
return '/api/v1/convert/img/pdf';
|
|
||||||
} else if (from === 'pdf' && to === 'office-word') {
|
|
||||||
return '/api/v1/convert/pdf/word';
|
|
||||||
} else if (from === 'pdf' && to === 'office-presentation') {
|
|
||||||
return '/api/v1/convert/pdf/presentation';
|
|
||||||
} else if (from === 'pdf' && to === 'office-text') {
|
|
||||||
return '/api/v1/convert/pdf/text';
|
|
||||||
} else if (from === 'pdf' && to === 'html') {
|
|
||||||
return '/api/v1/convert/pdf/html';
|
|
||||||
} else if (from === 'pdf' && to === 'xml') {
|
|
||||||
return '/api/v1/convert/pdf/xml';
|
|
||||||
} else if (from === 'html' && to === 'pdf') {
|
|
||||||
return '/api/v1/convert/html/pdf';
|
|
||||||
} else if (from === 'markdown' && to === 'pdf') {
|
|
||||||
return '/api/v1/convert/markdown/pdf';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConvert = async () => {
|
// Endpoint validation
|
||||||
if (files.length === 0) {
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||||
setErrorMessage(t("convert.errorNoFiles", "Please select at least one file to convert."));
|
convertParams.getEndpointName()
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (!fromFormat || !toFormat) {
|
|
||||||
setErrorMessage(t("convert.errorNoFormat", "Please select both source and target formats."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = getApiEndpoint(fromFormat, toFormat);
|
|
||||||
if (!endpoint) {
|
|
||||||
setErrorMessage(
|
|
||||||
t("convert.errorNotSupported", { from: fromFormat, to: toFormat, defaultValue: `Conversion from ${fromFormat} to ${toFormat} is not supported.` })
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
// Auto-detect extension when files change
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFiles.length > 0 && !convertParams.parameters.fromExtension) {
|
||||||
|
const firstFile = selectedFiles[0];
|
||||||
|
const detectedExtension = convertParams.detectFileExtension(firstFile.name);
|
||||||
|
if (detectedExtension) {
|
||||||
|
convertParams.updateParameter('fromExtension', detectedExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedFiles, convertParams.parameters.fromExtension]);
|
||||||
|
|
||||||
// Handle IndexedDB files
|
useEffect(() => {
|
||||||
for (const file of files) {
|
convertOperation.resetResults();
|
||||||
if (!file.id) {
|
onPreviewFile?.(null);
|
||||||
console.warn("File without ID found, skipping:", file.name);
|
}, [convertParams.parameters, selectedFiles]);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const storedFile = await fileStorage.getFile(file.id);
|
|
||||||
if (!storedFile) {
|
|
||||||
console.warn("Stored file not found in IndexedDB for ID:", file.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
|
||||||
const actualFile = new File([blob], storedFile.name, {
|
|
||||||
type: storedFile.type,
|
|
||||||
lastModified: storedFile.lastModified,
|
|
||||||
});
|
|
||||||
formData.append("fileInput", actualFile);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add conversion-specific parameters
|
|
||||||
if (toFormat === 'image') {
|
|
||||||
formData.append("imageFormat", "png");
|
|
||||||
formData.append("colorType", colorType);
|
|
||||||
formData.append("dpi", dpi.toString());
|
|
||||||
formData.append("singleOrMultiple", singleOrMultiple);
|
|
||||||
} else if (fromFormat === 'pdf' && toFormat.startsWith('office')) {
|
|
||||||
if (toFormat === 'office-word') {
|
|
||||||
formData.append("outputFormat", outputFormat || "docx");
|
|
||||||
} else if (toFormat === 'office-presentation') {
|
|
||||||
formData.append("outputFormat", outputFormat || "pptx");
|
|
||||||
} else if (toFormat === 'office-text') {
|
|
||||||
formData.append("outputFormat", outputFormat || "txt");
|
|
||||||
}
|
|
||||||
} else if (fromFormat === 'image' && toFormat === 'pdf') {
|
|
||||||
formData.append("fitOption", "fillPage");
|
|
||||||
formData.append("colorType", colorType);
|
|
||||||
formData.append("autoRotate", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
const handleConvert = async () => {
|
||||||
setErrorMessage(null);
|
await convertOperation.executeOperation(
|
||||||
|
convertParams.parameters,
|
||||||
try {
|
selectedFiles
|
||||||
console.log("Converting files from", fromFormat, "to", toFormat, "using endpoint:", endpoint);
|
|
||||||
console.log("Form data:", Array.from(formData.entries()).map(([key, value]) => `${key}: ${value}`));
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`Conversion failed: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
setDownloadUrl(url);
|
|
||||||
setLocalDownloadUrl(url);
|
|
||||||
} catch (error: any) {
|
|
||||||
setErrorMessage(error.message || "Unknown error occurred.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFromFormatChange = (value: string | null) => {
|
|
||||||
if (value) {
|
|
||||||
setFromFormat(value);
|
|
||||||
setToFormat("");
|
|
||||||
// Reset all format-specific options when source format changes
|
|
||||||
setColorType("color");
|
|
||||||
setDpi(300);
|
|
||||||
setSingleOrMultiple("multiple");
|
|
||||||
setOutputFormat("");
|
|
||||||
updateParams({
|
|
||||||
fromFormat: value,
|
|
||||||
toFormat: "",
|
|
||||||
imageOptions: { colorType: "color", dpi: 300, singleOrMultiple: "multiple" },
|
|
||||||
officeOptions: { outputFormat: "" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToFormatChange = (value: string | null) => {
|
|
||||||
if (value) {
|
|
||||||
setToFormat(value);
|
|
||||||
// Reset format-specific options when target format changes
|
|
||||||
setColorType("color");
|
|
||||||
setDpi(300);
|
|
||||||
setSingleOrMultiple("multiple");
|
|
||||||
setOutputFormat("");
|
|
||||||
updateParams({
|
|
||||||
fromFormat,
|
|
||||||
toFormat: value,
|
|
||||||
imageOptions: { colorType: "color", dpi: 300, singleOrMultiple: "multiple" },
|
|
||||||
officeOptions: { outputFormat: "" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Text size="sm">
|
|
||||||
{t("convert.desc", "Convert files between different formats")}
|
|
||||||
</Text>
|
|
||||||
<Divider my="sm" />
|
|
||||||
|
|
||||||
<Stack gap="md">
|
|
||||||
<Stack gap="sm" align="center">
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
<Text size="sm" fw={500} mb="xs">
|
|
||||||
{t("convert.convertFrom", "Convert from")}:
|
|
||||||
</Text>
|
|
||||||
<Select
|
|
||||||
value={fromFormat}
|
|
||||||
onChange={handleFromFormatChange}
|
|
||||||
data={[
|
|
||||||
{ value: 'pdf', label: 'PDF' },
|
|
||||||
{ value: 'office', label: t("convert.officeDocs", "Office Documents (Word, Excel, PowerPoint)") },
|
|
||||||
{ value: 'image', label: t("convert.imagesExt", "Images (JPG, PNG, etc.)") },
|
|
||||||
{ value: 'html', label: 'HTML' },
|
|
||||||
{ value: 'markdown', label: t("convert.markdown", "Markdown") },
|
|
||||||
{ value: 'text', label: t("convert.textRtf", "Text/RTF") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
<Text size="sm" fw={500} mb="xs">
|
|
||||||
{t("convert.convertTo", "Convert to")}:
|
|
||||||
</Text>
|
|
||||||
<Select
|
|
||||||
value={toFormat}
|
|
||||||
onChange={handleToFormatChange}
|
|
||||||
data={getAvailableToFormats(fromFormat).map(format => ({
|
|
||||||
value: format,
|
|
||||||
label: format === 'office-word' ? t("convert.wordDoc") :
|
|
||||||
format === 'office-presentation' ? t("convert.powerPointPresentation", "PowerPoint Presentation") :
|
|
||||||
format === 'office-text' ? t("convert.textRtf") :
|
|
||||||
format === 'image' ? t("convert.images") :
|
|
||||||
format === 'pdf' ? 'PDF' :
|
|
||||||
format.charAt(0).toUpperCase() + format.slice(1)
|
|
||||||
}))}
|
|
||||||
disabled={!fromFormat}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{(toFormat === 'image' || (fromFormat === 'pdf' && toFormat?.startsWith('office')) || (fromFormat === 'image' && toFormat === 'pdf')) && (
|
|
||||||
<Divider />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toFormat === 'image' && (
|
|
||||||
<Stack gap="sm">
|
|
||||||
<Text size="sm" fw={500}>{t("convert.imageOptions", "Image Options")}:</Text>
|
|
||||||
<Group grow>
|
|
||||||
<Select
|
|
||||||
label={t("convert.colorType", "Color Type")}
|
|
||||||
value={colorType}
|
|
||||||
onChange={(val) => val && setColorType(val)}
|
|
||||||
data={[
|
|
||||||
{ value: 'color', label: t("convert.color", "Color") },
|
|
||||||
{ value: 'greyscale', label: t("convert.greyscale", "Greyscale") },
|
|
||||||
{ value: 'blackwhite', label: t("convert.blackwhite", "Black & White") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
label={t("convert.dpi", "DPI")}
|
|
||||||
value={dpi}
|
|
||||||
onChange={(val) => typeof val === 'number' && setDpi(val)}
|
|
||||||
min={72}
|
|
||||||
max={600}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Select
|
|
||||||
label={t("convert.output", "Output")}
|
|
||||||
value={singleOrMultiple}
|
|
||||||
onChange={(val) => val && setSingleOrMultiple(val)}
|
|
||||||
data={[
|
|
||||||
{ value: 'single', label: t("convert.single") },
|
|
||||||
{ value: 'multiple', label: t("convert.multiple") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fromFormat === 'pdf' && toFormat?.startsWith('office') && (
|
|
||||||
<Stack gap="sm">
|
|
||||||
<Text size="sm" fw={500}>{t("convert.outputOptions", "Output Options")}:</Text>
|
|
||||||
<Select
|
|
||||||
label={t("convert.fileFormat")}
|
|
||||||
value={outputFormat}
|
|
||||||
onChange={(val) => val && setOutputFormat(val)}
|
|
||||||
data={
|
|
||||||
toFormat === 'office-word' ? [
|
|
||||||
{ value: 'docx', label: t("convert.wordDocExt") },
|
|
||||||
{ value: 'odt', label: t("convert.odtExt") },
|
|
||||||
] :
|
|
||||||
toFormat === 'office-presentation' ? [
|
|
||||||
{ value: 'pptx', label: t("convert.pptExt") },
|
|
||||||
{ value: 'odp', label: t("convert.odpExt") },
|
|
||||||
] :
|
|
||||||
toFormat === 'office-text' ? [
|
|
||||||
{ value: 'txt', label: t("convert.txtExt") },
|
|
||||||
{ value: 'rtf', label: t("convert.rtfExt") },
|
|
||||||
] : []
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fromFormat === 'image' && toFormat === 'pdf' && (
|
|
||||||
<Stack gap="sm">
|
|
||||||
<Text size="sm" fw={500}>{t("convert.pdfOptions", "PDF Options")}:</Text>
|
|
||||||
<Select
|
|
||||||
label={t("convert.colorType")}
|
|
||||||
value={colorType}
|
|
||||||
onChange={(val) => val && setColorType(val)}
|
|
||||||
data={[
|
|
||||||
{ value: 'color', label: t("convert.color") },
|
|
||||||
{ value: 'greyscale', label: t("convert.greyscale") },
|
|
||||||
{ value: 'blackwhite', label: t("convert.blackwhite") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider my="sm" />
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">
|
|
||||||
{t("convert.selectedFiles", "Selected files")}: ({files.length}):
|
|
||||||
</Text>
|
|
||||||
<Stack gap={4}>
|
|
||||||
{files.map((file, index) => (
|
|
||||||
<Group key={index} gap="xs">
|
|
||||||
<Text size="sm">{file.name}</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
{files.length === 0 && (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t("convert.noFileSelected", "No files selected for conversion. Please add files to convert.")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleConvert}
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={files.length === 0 || !fromFormat || !toFormat || isLoading}
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
{isLoading ? t("convert.converting", "Converting...") : t("convert.convertFiles", "Convert Files")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<Alert color="red">
|
|
||||||
{errorMessage}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{downloadUrl && (
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
href={downloadUrl}
|
|
||||||
download
|
|
||||||
color="green"
|
|
||||||
variant="light"
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
{t("convert.downloadConverted", "Download Converted File")} <ArrowDownward />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem('previousMode', 'convert');
|
||||||
|
setCurrentMode('viewer');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
convertOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode('convert');
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = convertOperation.downloadUrl !== null;
|
||||||
|
const filesCollapsed = hasFiles;
|
||||||
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
|
const previewResults = useMemo(() =>
|
||||||
|
convertOperation.files?.map((file, index) => ({
|
||||||
|
file,
|
||||||
|
thumbnail: convertOperation.thumbnails[index]
|
||||||
|
})) || [],
|
||||||
|
[convertOperation.files, convertOperation.thumbnails]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolStepContainer>
|
||||||
|
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||||
|
{/* Files Step */}
|
||||||
|
<ToolStep
|
||||||
|
title="Files"
|
||||||
|
isVisible={true}
|
||||||
|
isCollapsed={filesCollapsed}
|
||||||
|
isCompleted={filesCollapsed}
|
||||||
|
completedMessage={hasFiles ? `Selected: ${selectedFiles[0]?.name}` : undefined}
|
||||||
|
>
|
||||||
|
<FileStatusIndicator
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
|
placeholder="Select a file in the main view to get started"
|
||||||
|
/>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{/* Settings Step */}
|
||||||
|
<ToolStep
|
||||||
|
title="Settings"
|
||||||
|
isVisible={hasFiles}
|
||||||
|
isCollapsed={settingsCollapsed}
|
||||||
|
isCompleted={settingsCollapsed}
|
||||||
|
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||||
|
completedMessage={settingsCollapsed ? "Conversion completed" : undefined}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<ConvertSettings
|
||||||
|
parameters={convertParams.parameters}
|
||||||
|
onParameterChange={convertParams.updateParameter}
|
||||||
|
getAvailableToExtensions={convertParams.getAvailableToExtensions}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{convertParams.parameters.fromExtension && convertParams.parameters.toExtension && (
|
||||||
|
<OperationButton
|
||||||
|
onClick={handleConvert}
|
||||||
|
isLoading={convertOperation.isLoading}
|
||||||
|
disabled={!convertParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||||
|
loadingText={t("convert.converting", "Converting...")}
|
||||||
|
submitText={t("convert.convertFiles", "Convert Files")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{/* Results Step */}
|
||||||
|
<ToolStep
|
||||||
|
title="Results"
|
||||||
|
isVisible={hasResults}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{convertOperation.status && (
|
||||||
|
<Text size="sm" c="dimmed">{convertOperation.status}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ErrorNotification
|
||||||
|
error={convertOperation.errorMessage}
|
||||||
|
onClose={convertOperation.clearError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{convertOperation.downloadUrl && (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={convertOperation.downloadUrl}
|
||||||
|
download={convertOperation.downloadFilename || "converted_file"}
|
||||||
|
leftSection={<DownloadIcon />}
|
||||||
|
color="green"
|
||||||
|
fullWidth
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
|
{t("convert.downloadConverted", "Download Converted File")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ResultsPreview
|
||||||
|
files={previewResults}
|
||||||
|
onFileClick={handleThumbnailClick}
|
||||||
|
isGeneratingThumbnails={convertOperation.isGeneratingThumbnails}
|
||||||
|
title="Conversion Results"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</ToolStep>
|
||||||
|
</Stack>
|
||||||
|
</ToolStepContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConvertPanel;
|
export default Convert;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user