Merge remote-tracking branch 'origin/V2' into feature/v2/automate

This commit is contained in:
Connor Yoh 2025-08-21 12:03:17 +01:00
commit 018d1e57ff
22 changed files with 416 additions and 594 deletions

View File

@ -366,14 +366,6 @@
"title": "Convert",
"desc": "Convert files between different formats"
},
"imageToPDF": {
"title": "Image to PDF",
"desc": "Convert a image (PNG, JPEG, GIF) to PDF."
},
"pdfToImage": {
"title": "PDF to Image",
"desc": "Convert a PDF to a image. (PNG, JPEG, GIF)"
},
"pdfOrganiser": {
"title": "Organise",
"desc": "Remove/Rearrange pages in any order"
@ -390,14 +382,6 @@
"title": "Add Watermark",
"desc": "Add a custom watermark to your PDF document."
},
"permissions": {
"title": "Change Permissions",
"desc": "Change the permissions of your PDF document"
},
"pageRemover": {
"title": "Remove",
"desc": "Delete unwanted pages from your PDF document."
},
"removePassword": {
"title": "Remove Password",
"desc": "Remove password protection from your PDF document."
@ -414,10 +398,6 @@
"title": "Change Metadata",
"desc": "Change/Remove/Add metadata from a PDF document"
},
"fileToPDF": {
"title": "Convert file to PDF",
"desc": "Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more)"
},
"ocr": {
"title": "OCR / Cleanup scans",
"desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text."
@ -426,30 +406,6 @@
"title": "Extract Images",
"desc": "Extracts all images from a PDF and saves them to zip"
},
"pdfToPDFA": {
"title": "PDF to PDF/A",
"desc": "Convert PDF to PDF/A for long-term storage"
},
"PDFToWord": {
"title": "PDF to Word",
"desc": "Convert PDF to Word formats (DOC, DOCX and ODT)"
},
"PDFToPresentation": {
"title": "PDF to Presentation",
"desc": "Convert PDF to Presentation formats (PPT, PPTX and ODP)"
},
"PDFToText": {
"title": "PDF to RTF (Text)",
"desc": "Convert PDF to Text or RTF format"
},
"PDFToHTML": {
"title": "PDF to HTML",
"desc": "Convert PDF to HTML format"
},
"PDFToXML": {
"title": "PDF to XML",
"desc": "Convert PDF to XML format"
},
"ScannerImageSplit": {
"title": "Detect/Split Scanned photos",
"desc": "Splits multiple photos from within a photo/PDF"
@ -518,34 +474,14 @@
"title": "Auto Split Pages",
"desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code"
},
"sanitizePdf": {
"sanitize": {
"title": "Sanitise",
"desc": "Remove scripts and other elements from PDF files"
},
"URLToPDF": {
"title": "URL/Website To PDF",
"desc": "Converts any http(s)URL to PDF"
},
"HTMLToPDF": {
"title": "HTML to PDF",
"desc": "Converts any HTML file or zip to PDF"
},
"MarkdownToPDF": {
"title": "Markdown to PDF",
"desc": "Converts any Markdown file to PDF"
},
"PDFToMarkdown": {
"title": "PDF to Markdown",
"desc": "Converts any PDF to Markdown"
"desc": "Remove potentially harmful elements from PDF files"
},
"getPdfInfo": {
"title": "Get ALL Info on PDF",
"desc": "Grabs any and all information possible on PDFs"
},
"pageExtracter": {
"title": "Extract page(s)",
"desc": "Extracts select pages from PDF"
},
"pdfToSinglePage": {
"title": "PDF to Single Large Page",
"desc": "Merges all PDF pages into one large single page"
@ -562,14 +498,6 @@
"title": "Manual Redaction",
"desc": "Redacts a PDF based on selected text, drawn shapes and/or selected page(s)"
},
"PDFToCSV": {
"title": "PDF to CSV",
"desc": "Extracts Tables from a PDF converting it to CSV"
},
"split-by-size-or-count": {
"title": "Auto Split by Size/Count",
"desc": "Split a single PDF into multiple documents based on size, page count, or document count"
},
"overlay-pdfs": {
"title": "Overlay PDFs",
"desc": "Overlays PDFs on-top of another PDF"
@ -625,6 +553,54 @@
"reorganizePages": {
"title": "Reorganize Pages",
"desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
},
"extractPages": {
"title": "Extract Pages",
"desc": "Extract specific pages from a PDF document"
},
"removePages": {
"title": "Remove Pages",
"desc": "Remove specific pages from a PDF document"
},
"removeImagePdf": {
"title": "Remove Image",
"desc": "Remove images from PDF documents"
},
"autoSizeSplitPDF": {
"title": "Auto Split by Size/Count",
"desc": "Automatically split PDFs by file size or page count"
},
"adjust-contrast": {
"title": "Adjust Colours/Contrast",
"desc": "Adjust colours and contrast of PDF documents"
},
"replaceColorPdf": {
"title": "Replace & Invert Colour",
"desc": "Replace or invert colours in PDF documents"
},
"devApi": {
"title": "API",
"desc": "Link to API documentation"
},
"devFolderScanning": {
"title": "Automated Folder Scanning",
"desc": "Link to automated folder scanning guide"
},
"devSsoGuide": {
"title": "SSO Guide",
"desc": "Link to SSO guide"
},
"devAirgapped": {
"title": "Air-gapped Setup",
"desc": "Link to air-gapped setup guide"
},
"addPassword": {
"title": "Add Password",
"desc": "Add password protection and restrictions to PDF files"
},
"changePermissions": {
"title": "Change Permissions",
"desc": "Change document restrictions and permissions"
}
},
"viewPdf": {

View File

@ -1,7 +1,7 @@
import React, { useState } from "react";
import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressOperation";
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
interface CompressSettingsProps {
parameters: CompressParameters;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Stack, Text, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { OCRParameters } from './OCRSettings';
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
export interface AdvancedOCRParameters {
advancedOptions: string[];

View File

@ -2,13 +2,7 @@ import React from 'react';
import { Stack, Select, Text, Divider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LanguagePicker from './LanguagePicker';
export interface OCRParameters {
languages: string[];
ocrType: string;
ocrRenderType: string;
additionalOptions: string[];
}
import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
interface OCRSettingsProps {
parameters: OCRParameters;
@ -25,7 +19,7 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
return (
<Stack gap="md">
<Select
label={t('ocr.settings.ocrMode.label', 'OCR Mode')}
value={parameters.ocrType}
@ -51,4 +45,4 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
);
};
export default OCRSettings;
export default OCRSettings;

View File

@ -1,17 +1,3 @@
export interface AddWatermarkParameters {
watermarkType?: 'text' | 'image';
watermarkText: string;
watermarkImage?: File;
fontSize: number; // Used for both text size and image size
rotation: number;
opacity: number;
widthSpacer: number;
heightSpacer: number;
alphabet: string;
customColor: string;
convertPDFToImage: boolean;
}
export interface AlphabetOption {
value: string;
label: string;
@ -25,16 +11,3 @@ export const alphabetOptions: AlphabetOption[] = [
{ value: "chinese", label: "简体中文" },
{ value: "thai", label: "ไทย" },
];
export const defaultWatermarkParameters: AddWatermarkParameters = {
watermarkType: undefined,
watermarkText: '',
fontSize: 12,
rotation: 0,
opacity: 50,
widthSpacer: 50,
heightSpacer: 50,
alphabet: 'roman',
customColor: '#d3d3d3',
convertPDFToImage: false
};

View File

@ -1,6 +1,7 @@
import { type TFunction } from 'i18next';
import React from 'react';
import { ToolOperationHook } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
export enum SubcategoryId {
SIGNING = 'signing',
@ -25,7 +26,7 @@ export enum ToolCategory {
export type ToolRegistryEntry = {
icon: React.ReactNode;
name: string;
component: React.ComponentType<any> | null;
component: React.ComponentType<BaseToolProps> | null;
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
description: string;
category: ToolCategory;

View File

@ -15,13 +15,13 @@ import SingleLargePage from '../tools/SingleLargePage';
import UnlockPdfForms from '../tools/UnlockPdfForms';
import RemoveCertificateSign from '../tools/RemoveCertificateSign';
const showPlaceholderTools = false; // For development purposes. Allows seeing the full list of tools, even if they're unimplemented
// Hook to get the translated tool registry
export function useFlatToolRegistry(): ToolRegistry {
const { t } = useTranslation();
return useMemo(() => ({
const { t } = useTranslation();
const allTools: ToolRegistry = {
// Signing
"certSign": {
@ -56,8 +56,8 @@ export function useFlatToolRegistry(): ToolRegistry {
subcategory: SubcategoryId.DOCUMENT_SECURITY,
maxFiles: -1,
endpoints: ["add-password"]
},
"add-watermark": {
},
"watermark": {
icon: <span className="material-symbols-rounded">branding_watermark</span>,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
@ -148,8 +148,8 @@ export function useFlatToolRegistry(): ToolRegistry {
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.VERIFICATION
},
// Document Review
"read": {
@ -205,7 +205,7 @@ export function useFlatToolRegistry(): ToolRegistry {
component: null,
view: "pageEditor",
description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."),
category: ToolCategory.STANDARD_TOOLS,
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING
},
"adjust-page-size-scale": {
@ -217,12 +217,12 @@ export function useFlatToolRegistry(): ToolRegistry {
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING
},
"add-page-numbers": {
"addPageNumbers": {
icon: <span className="material-symbols-rounded">123</span>,
name: t("home.add-page-numbers.title", "Add Page Numbers"),
name: t("home.addPageNumbers.title", "Add Page Numbers"),
component: null,
view: "format",
description: t("home.add-page-numbers.desc", "Add Page numbers throughout a document in a set location"),
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING
},
@ -237,10 +237,10 @@ export function useFlatToolRegistry(): ToolRegistry {
},
"single-large-page": {
icon: <span className="material-symbols-rounded">looks_one</span>,
name: t("home.PdfToSinglePage.title", "PDF to Single Large Page"),
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
component: SingleLargePage,
view: "format",
description: t("home.PdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
@ -259,12 +259,12 @@ export function useFlatToolRegistry(): ToolRegistry {
// Extraction
"extract-pages": {
"extractPages": {
icon: <span className="material-symbols-rounded">upload</span>,
name: t("home.extractPage.title", "Extract Pages"),
name: t("home.extractPages.title", "Extract Pages"),
component: null,
view: "extract",
description: t("home.extractPage.desc", "Extract specific pages from a PDF document"),
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.EXTRACTION
},
@ -281,7 +281,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Removal
"remove": {
"removePages": {
icon: <span className="material-symbols-rounded">delete</span>,
name: t("home.removePages.title", "Remove Pages"),
component: null,
@ -331,10 +331,10 @@ export function useFlatToolRegistry(): ToolRegistry {
},
"remove-certificate-sign": {
icon: <span className="material-symbols-rounded">remove_moderator</span>,
name: t("home.removeCertSign.title", "Remove Certificate Signatures"),
name: t("home.removeCertSign.title", "Remove Certificate Sign"),
component: RemoveCertificateSign,
view: "security",
description: t("home.removeCertSign.desc", "Remove digital signatures from PDF documents"),
description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"),
category: ToolCategory.STANDARD_TOOLS,
subcategory: SubcategoryId.REMOVAL,
maxFiles: -1,
@ -386,12 +386,12 @@ export function useFlatToolRegistry(): ToolRegistry {
// Advanced Formatting
"adjust-colors-contrast": {
"adjustContrast": {
icon: <span className="material-symbols-rounded">palette</span>,
name: t("home.adjust-contrast.title", "Adjust Colors/Contrast"),
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null,
view: "format",
description: t("home.adjust-contrast.desc", "Adjust colors and contrast of PDF documents"),
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
category: ToolCategory.ADVANCED_TOOLS,
subcategory: SubcategoryId.ADVANCED_FORMATTING
},
@ -525,22 +525,22 @@ export function useFlatToolRegistry(): ToolRegistry {
category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL
},
"compressPdfs": {
"compress": {
icon: <span className="material-symbols-rounded">zoom_in_map</span>,
name: t("home.compressPdfs.title", "Compress"),
name: t("home.compress.title", "Compress"),
component: CompressPdfPanel,
view: "compress",
description: t("home.compressPdfs.desc", "Compress PDFs to reduce their file size."),
description: t("home.compress.desc", "Compress PDFs to reduce their file size."),
category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL,
maxFiles: -1
},
"convert": {
icon: <span className="material-symbols-rounded">sync_alt</span>,
name: t("home.fileToPDF.title", "Convert"),
name: t("home.convert.title", "Convert"),
component: ConvertPanel,
view: "convert",
description: t("home.fileToPDF.desc", "Convert files to and from PDF format"),
description: t("home.convert.desc", "Convert files to and from PDF format"),
category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL,
maxFiles: -1,
@ -618,5 +618,17 @@ export function useFlatToolRegistry(): ToolRegistry {
category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL
},
}), [t]);
}
};
if (showPlaceholderTools) {
return allTools;
} else {
const filteredTools = Object.keys(allTools)
.filter(key => allTools[key].component !== null || allTools[key].link)
.reduce((obj, key) => {
obj[key] = allTools[key];
return obj;
}, {} as ToolRegistry);
return filteredTools;
}
}

View File

@ -1,7 +1,8 @@
import { useState } from 'react';
import { ChangePermissionsParameters, ChangePermissionsParametersHook, useChangePermissionsParameters } from '../changePermissions/useChangePermissionsParameters';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface AddPasswordParameters {
export interface AddPasswordParameters extends BaseParameters {
password: string;
ownerPassword: string;
keyLength: number;
@ -11,14 +12,9 @@ export interface AddPasswordFullParameters extends AddPasswordParameters {
permissions: ChangePermissionsParameters;
}
export interface AddPasswordParametersHook {
export interface AddPasswordParametersHook extends BaseParametersHook<AddPasswordParameters> {
fullParameters: AddPasswordFullParameters;
parameters: AddPasswordParameters;
permissions: ChangePermissionsParametersHook;
updateParameter: <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: AddPasswordParameters = {
@ -28,42 +24,31 @@ export const defaultParameters: AddPasswordParameters = {
};
export const useAddPasswordParameters = (): AddPasswordParametersHook => {
const [parameters, setParameters] = useState<AddPasswordParameters>(defaultParameters);
const permissions = useChangePermissionsParameters();
const baseHook = useBaseParameters({
defaultParameters,
endpointName: 'add-password',
validateFn: () => {
// No required parameters for Add Password. Defer to permissions validation.
return permissions.validateParameters();
},
});
const fullParameters: AddPasswordFullParameters = {
...parameters,
...baseHook.parameters,
permissions: permissions.parameters,
};
const updateParameter = <K extends keyof AddPasswordParameters>(parameter: K, value: AddPasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
baseHook.resetParameters();
permissions.resetParameters();
};
const validateParameters = () => {
// No required parameters for Add Password. Defer to permissions validation.
return permissions.validateParameters();
};
const getEndpointName = () => {
return 'add-password';
};
return {
...baseHook,
fullParameters,
parameters,
permissions,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
};

View File

@ -1,35 +1,48 @@
import { useState, useCallback } from 'react';
import { defaultWatermarkParameters, AddWatermarkParameters } from '../../../constants/addWatermarkConstants';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export const useAddWatermarkParameters = () => {
const [parameters, setParameters] = useState<AddWatermarkParameters>(defaultWatermarkParameters);
export interface AddWatermarkParameters extends BaseParameters {
watermarkType?: 'text' | 'image';
watermarkText: string;
watermarkImage?: File;
fontSize: number; // Used for both text size and image size
rotation: number;
opacity: number;
widthSpacer: number;
heightSpacer: number;
alphabet: string;
customColor: string;
convertPDFToImage: boolean;
}
const updateParameter = useCallback(<K extends keyof AddWatermarkParameters>(
key: K,
value: AddWatermarkParameters[K]
) => {
setParameters(prev => ({ ...prev, [key]: value }));
}, []);
export const defaultParameters: AddWatermarkParameters = {
watermarkType: undefined,
watermarkText: '',
fontSize: 12,
rotation: 0,
opacity: 50,
widthSpacer: 50,
heightSpacer: 50,
alphabet: 'roman',
customColor: '#d3d3d3',
convertPDFToImage: false
};
const resetParameters = useCallback(() => {
setParameters(defaultWatermarkParameters);
}, []);
export type AddWatermarkParametersHook = BaseParametersHook<AddWatermarkParameters>;
const validateParameters = useCallback((): boolean => {
if (!parameters.watermarkType) {
return false;
}
if (parameters.watermarkType === 'text') {
return parameters.watermarkText.trim().length > 0;
} else {
return parameters.watermarkImage !== undefined;
}
}, [parameters]);
return {
parameters,
updateParameter,
resetParameters,
validateParameters
};
};
export const useAddWatermarkParameters = (): AddWatermarkParametersHook => {
return useBaseParameters({
defaultParameters: defaultParameters,
endpointName: 'add-watermark',
validateFn: (params): boolean => {
if (!params.watermarkType) {
return false;
}
if (params.watermarkType === 'text') {
return params.watermarkText.trim().length > 0;
} else {
return params.watermarkImage !== undefined;
}
},
});
};

View File

@ -1,6 +1,7 @@
import { useState } from 'react';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ChangePermissionsParameters {
export interface ChangePermissionsParameters extends BaseParameters {
preventAssembly: boolean;
preventExtractContent: boolean;
preventExtractForAccessibility: boolean;
@ -11,14 +12,6 @@ export interface ChangePermissionsParameters {
preventPrintingFaithful: boolean;
}
export interface ChangePermissionsParametersHook {
parameters: ChangePermissionsParameters;
updateParameter: (parameter: keyof ChangePermissionsParameters, value: boolean) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: ChangePermissionsParameters = {
preventAssembly: false,
preventExtractContent: false,
@ -30,35 +23,11 @@ export const defaultParameters: ChangePermissionsParameters = {
preventPrintingFaithful: false,
};
export type ChangePermissionsParametersHook = BaseParametersHook<ChangePermissionsParameters>;
export const useChangePermissionsParameters = (): ChangePermissionsParametersHook => {
const [parameters, setParameters] = useState<ChangePermissionsParameters>(defaultParameters);
const updateParameter = <K extends keyof ChangePermissionsParameters>(parameter: K, value: ChangePermissionsParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
// Always valid - any combination of permissions is allowed
return true;
};
const getEndpointName = () => {
return 'add-password'; // Change Permissions is a fake endpoint for the Add Password tool
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
return useBaseParameters({
defaultParameters,
endpointName: 'add-password', // Change Permissions is a fake endpoint for the Add Password tool
});
};

View File

@ -1,15 +1,7 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
export interface CompressParameters {
compressionLevel: number;
grayscale: boolean;
expectedSize: string;
compressionMethod: 'quality' | 'filesize';
fileSizeValue: string;
fileSizeUnit: 'KB' | 'MB';
}
import { CompressParameters } from './useCompressParameters';
const buildFormData = (parameters: CompressParameters, file: File): FormData => {
const formData = new FormData();

View File

@ -1,15 +1,16 @@
import { useState } from 'react';
import { CompressParameters } from './useCompressOperation';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface CompressParametersHook {
parameters: CompressParameters;
updateParameter: (parameter: keyof CompressParameters, value: string | boolean | number) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
export interface CompressParameters extends BaseParameters {
compressionLevel: number;
grayscale: boolean;
expectedSize: string;
compressionMethod: 'quality' | 'filesize';
fileSizeValue: string;
fileSizeUnit: 'KB' | 'MB';
}
export const initialParameters: CompressParameters = {
const defaultParameters: CompressParameters = {
compressionLevel: 5,
grayscale: false,
expectedSize: '',
@ -18,32 +19,15 @@ export const initialParameters: CompressParameters = {
fileSizeUnit: 'MB',
};
export type CompressParametersHook = BaseParametersHook<CompressParameters>;
export const useCompressParameters = (): CompressParametersHook => {
const [parameters, setParameters] = useState<CompressParameters>(initialParameters);
const updateParameter = (parameter: keyof CompressParameters, value: string | boolean | number) => {
setParameters(prev => ({ ...prev, [parameter]: value }));
};
const resetParameters = () => {
setParameters(initialParameters);
};
const validateParameters = () => {
// For compression, we only need to validate that compression level is within range
// and that at least one file is selected (at least, I think that's all we need to do here)
return parameters.compressionLevel >= 1 && parameters.compressionLevel <= 9;
};
const getEndpointName = () => {
return 'compress-pdf';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
};
return useBaseParameters({
defaultParameters,
endpointName: 'compress-pdf',
validateFn: (params) => {
// For compression, we only need to validate that compression level is within range
return params.compressionLevel >= 1 && params.compressionLevel <= 9;
},
});
};

View File

@ -5,14 +5,15 @@
import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from './useConvertParameters';
import { FIT_OPTIONS } from '../../../constants/convertConstants';
describe('useConvertParameters', () => {
describe('Parameter Management', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useConvertParameters());
expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe('');
expect(result.current.parameters.imageOptions.colorType).toBe('color');
@ -28,46 +29,52 @@ describe('useConvertParameters', () => {
test('should update individual parameters', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
});
expect(result.current.parameters.fromExtension).toBe('pdf');
expect(result.current.parameters.toExtension).toBe(''); // Should not affect other params
});
test('should update nested image options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('imageOptions', {
colorType: 'grayscale',
dpi: 150,
singleOrMultiple: 'single'
singleOrMultiple: 'single',
fitOption: FIT_OPTIONS.FILL_PAGE,
autoRotate: false,
combineImages: false,
});
});
expect(result.current.parameters.imageOptions.colorType).toBe('grayscale');
expect(result.current.parameters.imageOptions.dpi).toBe(150);
expect(result.current.parameters.imageOptions.singleOrMultiple).toBe('single');
expect(result.current.parameters.imageOptions.fitOption).toBe(FIT_OPTIONS.FILL_PAGE);
expect(result.current.parameters.imageOptions.autoRotate).toBe(false);
expect(result.current.parameters.imageOptions.combineImages).toBe(false);
});
test('should update nested HTML options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('htmlOptions', {
zoomLevel: 1.5
});
});
expect(result.current.parameters.htmlOptions.zoomLevel).toBe(1.5);
});
test('should update nested email options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('emailOptions', {
includeAttachments: false,
@ -76,7 +83,7 @@ describe('useConvertParameters', () => {
includeAllRecipients: true
});
});
expect(result.current.parameters.emailOptions.includeAttachments).toBe(false);
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(20);
expect(result.current.parameters.emailOptions.downloadHtml).toBe(true);
@ -85,49 +92,49 @@ describe('useConvertParameters', () => {
test('should update nested PDF/A options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa'
});
});
expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa');
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png');
});
expect(result.current.parameters.fromExtension).toBe('pdf');
act(() => {
result.current.resetParameters();
});
expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe('');
});
});
describe('Parameter Validation', () => {
test('should validate parameters correctly', () => {
const { result } = renderHook(() => useConvertParameters());
// No parameters - should be invalid
expect(result.current.validateParameters()).toBe(false);
// Only fromExtension - should be invalid
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
});
expect(result.current.validateParameters()).toBe(false);
// Both extensions with supported conversion - should be valid
act(() => {
result.current.updateParameter('toExtension', 'png');
@ -137,63 +144,63 @@ describe('useConvertParameters', () => {
test('should validate unsupported conversions', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'unsupported');
});
expect(result.current.validateParameters()).toBe(false);
});
});
describe('Endpoint Generation', () => {
test('should generate correct endpoint names', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png');
});
const endpointName = result.current.getEndpointName();
expect(endpointName).toBe('pdf-to-img');
});
test('should generate correct endpoint URLs', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
result.current.updateParameter('toExtension', 'png');
});
const endpoint = result.current.getEndpoint();
expect(endpoint).toBe('/api/v1/convert/pdf/img');
});
test('should return empty strings for invalid conversions', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'invalid');
result.current.updateParameter('toExtension', 'invalid');
});
expect(result.current.getEndpointName()).toBe('');
expect(result.current.getEndpoint()).toBe('');
});
});
describe('Available Extensions', () => {
test('should return available extensions for valid source format', () => {
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('pdf');
expect(availableExtensions.length).toBeGreaterThan(0);
expect(availableExtensions.some(ext => ext.value === 'png')).toBe(true);
expect(availableExtensions.some(ext => ext.value === 'jpg')).toBe(true);
@ -201,9 +208,9 @@ describe('useConvertParameters', () => {
test('should return empty array for invalid source format', () => {
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('invalid');
expect(availableExtensions).toEqual([{
"group": "Document",
"label": "PDF",
@ -213,11 +220,11 @@ describe('useConvertParameters', () => {
test('should return empty array for empty source format', () => {
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('');
expect(availableExtensions).toEqual([]);
});
});
});
});

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react';
import {
COLOR_TYPES,
import {
COLOR_TYPES,
OUTPUT_OPTIONS,
FIT_OPTIONS,
TO_FORMAT_OPTIONS,
@ -11,8 +10,10 @@ import {
} from '../../../constants/convertConstants';
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ConvertParameters {
export interface ConvertParameters extends BaseParameters {
fromExtension: string;
toExtension: string;
imageOptions: {
@ -39,18 +40,13 @@ export interface ConvertParameters {
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
export interface ConvertParametersHook {
parameters: ConvertParameters;
updateParameter: (parameter: keyof ConvertParameters, value: any) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
export interface ConvertParametersHook extends BaseParametersHook<ConvertParameters> {
getEndpoint: () => string;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
analyzeFileTypes: (files: Array<{name: string}>) => void;
}
const initialParameters: ConvertParameters = {
const defaultParameters: ConvertParameters = {
fromExtension: '',
toExtension: '',
imageOptions: {
@ -77,67 +73,63 @@ const initialParameters: ConvertParameters = {
smartDetectionType: 'none',
};
export const useConvertParameters = (): ConvertParametersHook => {
const [parameters, setParameters] = useState<ConvertParameters>(initialParameters);
const validateParameters = (params: ConvertParameters): boolean => {
const { fromExtension, toExtension } = params;
const updateParameter = (parameter: keyof ConvertParameters, value: any) => {
setParameters(prev => ({ ...prev, [parameter]: value }));
};
if (!fromExtension || !toExtension) return false;
const resetParameters = () => {
setParameters(initialParameters);
};
// 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] || [];
}
const validateParameters = () => {
const { fromExtension, toExtension } = parameters;
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;
};
if (!supportedToExtensions.includes(toExtension)) {
return false;
}
const getEndpointName = () => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
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 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';
}
return getEndpointNameUtil(fromExtension, toExtension);
};
}
// 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 baseHook = useBaseParameters({
defaultParameters,
endpointName: getEndpointName,
validateFn: validateParameters,
});
const getEndpoint = () => {
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = parameters;
const { fromExtension, toExtension, isSmartDetection, smartDetectionType } = baseHook.parameters;
if (isSmartDetection) {
if (smartDetectionType === 'mixed') {
// Mixed file types -> PDF using file-to-pdf endpoint
@ -150,37 +142,37 @@ export const useConvertParameters = (): ConvertParametersHook => {
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 = (fromExtension: string) => {
if (!fromExtension) return [];
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options (file-to-pdf)
const supportedExtensions = CONVERSION_MATRIX['any'] || [];
return TO_FORMAT_OPTIONS.filter(option =>
return TO_FORMAT_OPTIONS.filter(option =>
supportedExtensions.includes(option.value)
);
}
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
// If no explicit conversion exists, but file-to-pdf might be available,
// If no explicit conversion exists, but file-to-pdf might be available,
// fall back to 'any' conversion (which converts unknown files to PDF via file-to-pdf)
if (supportedExtensions.length === 0 && fromExtension !== 'any') {
supportedExtensions = CONVERSION_MATRIX['any'] || [];
}
return TO_FORMAT_OPTIONS.filter(option =>
return TO_FORMAT_OPTIONS.filter(option =>
supportedExtensions.includes(option.value)
);
};
@ -189,7 +181,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
const analyzeFileTypes = (files: Array<{name: string}>) => {
if (files.length === 0) {
// No files - only reset smart detection, keep user's format choices
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: false,
smartDetectionType: 'none'
@ -197,13 +189,13 @@ export const useConvertParameters = (): ConvertParametersHook => {
}));
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) {
@ -214,21 +206,21 @@ export const useConvertParameters = (): ConvertParametersHook => {
fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || [];
}
setParameters(prev => {
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
// 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] : '';
}
return {
...prev,
isSmartDetection: false,
@ -249,27 +241,27 @@ export const useConvertParameters = (): ConvertParametersHook => {
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'] || [];
}
setParameters(prev => {
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
// 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] : '';
}
return {
...prev,
isSmartDetection: false,
@ -282,10 +274,10 @@ export const useConvertParameters = (): ConvertParametersHook => {
// 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
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'images',
@ -294,7 +286,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
}));
} else if (allWeb) {
// All files are web files - use html-to-pdf conversion
setParameters(prev => ({
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'web',
@ -302,8 +294,8 @@ export const useConvertParameters = (): ConvertParametersHook => {
toExtension: 'pdf'
}));
} else {
// Mixed non-image types - use file-to-pdf conversion
setParameters(prev => ({
// Mixed non-image types - use file-to-pdf conversion
baseHook.setParameters(prev => ({
...prev,
isSmartDetection: true,
smartDetectionType: 'mixed',
@ -315,13 +307,9 @@ export const useConvertParameters = (): ConvertParametersHook => {
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
...baseHook,
getEndpoint,
getAvailableToExtensions,
analyzeFileTypes,
};
};
};

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
import { OCRParameters } from './useOCRParameters';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { useToolResources } from '../shared/useToolResources';

View File

@ -1,13 +1,15 @@
import { useState } from 'react';
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface OCRParametersHook {
parameters: OCRParameters;
updateParameter: (key: keyof OCRParameters, value: any) => void;
resetParameters: () => void;
validateParameters: () => boolean;
export interface OCRParameters extends BaseParameters {
languages: string[];
ocrType: string;
ocrRenderType: string;
additionalOptions: string[];
}
export type OCRParametersHook = BaseParametersHook<OCRParameters>;
const defaultParameters: OCRParameters = {
languages: [],
ocrType: 'skip-text',
@ -16,28 +18,12 @@ const defaultParameters: OCRParameters = {
};
export const useOCRParameters = (): OCRParametersHook => {
const [parameters, setParameters] = useState<OCRParameters>(defaultParameters);
const updateParameter = (key: keyof OCRParameters, value: any) => {
setParameters(prev => ({
...prev,
[key]: value
}));
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
// At minimum, we need at least one language selected
return parameters.languages.length > 0;
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
};
};
return useBaseParameters({
defaultParameters,
endpointName: 'ocr-pdf',
validateFn: (params) => {
// At minimum, we need at least one language selected
return params.languages.length > 0;
},
});
};

View File

@ -1,49 +1,22 @@
import { useState } from 'react';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface RemovePasswordParameters {
export interface RemovePasswordParameters extends BaseParameters {
password: string;
}
export interface RemovePasswordParametersHook {
parameters: RemovePasswordParameters;
updateParameter: <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export type RemovePasswordParametersHook = BaseParametersHook<RemovePasswordParameters>;
export const defaultParameters: RemovePasswordParameters = {
password: '',
};
export const useRemovePasswordParameters = (): RemovePasswordParametersHook => {
const [parameters, setParameters] = useState<RemovePasswordParameters>(defaultParameters);
const updateParameter = <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
return parameters.password !== '';
};
const getEndpointName = () => {
return 'remove-password';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
return useBaseParameters({
defaultParameters,
endpointName: 'remove-password',
validateFn: (params) => {
return params.password !== '';
},
});
};

View File

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface SanitizeParameters {
export interface SanitizeParameters extends BaseParameters {
removeJavaScript: boolean;
removeEmbeddedFiles: boolean;
removeXMPMetadata: boolean;
@ -18,36 +19,14 @@ export const defaultParameters: SanitizeParameters = {
removeFonts: false,
};
export const useSanitizeParameters = () => {
const [parameters, setParameters] = useState<SanitizeParameters>(defaultParameters);
export type SanitizeParametersHook = BaseParametersHook<SanitizeParameters>;
const updateParameter = useCallback(<K extends keyof SanitizeParameters>(
key: K,
value: SanitizeParameters[K]
) => {
setParameters(prev => ({
...prev,
[key]: value
}));
}, []);
const resetParameters = useCallback(() => {
setParameters(defaultParameters);
}, []);
const validateParameters = useCallback(() => {
return Object.values(parameters).some(value => value === true);
}, [parameters]);
const getEndpointName = () => {
return 'sanitize-pdf'
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
export const useSanitizeParameters = (): SanitizeParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'sanitize-pdf',
validateFn: (params) => {
return Object.values(params).some(value => value === true);
},
});
};

View File

@ -1,7 +1,8 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface BaseParametersHook<T> {
parameters: T;
setParameters: Dispatch<SetStateAction<T>>;
updateParameter: <K extends keyof T>(parameter: K, value: T[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
@ -10,7 +11,7 @@ export interface BaseParametersHook<T> {
export interface BaseParametersConfig<T> {
defaultParameters: T;
endpointName: string;
endpointName: string | ((params: T) => string);
validateFn?: (params: T) => boolean;
}
@ -32,15 +33,24 @@ export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParam
return config.validateFn ? config.validateFn(parameters) : true;
}, [parameters, config.validateFn]);
const getEndpointName = useCallback(() => {
return config.endpointName;
}, [config.endpointName]);
const endpointName = config.endpointName;
let getEndpointName: () => string;
if (typeof endpointName === "string") {
getEndpointName = useCallback(() => {
return endpointName;
}, []);
} else {
getEndpointName = useCallback(() => {
return endpointName(parameters);
}, [parameters]);
}
return {
parameters,
setParameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
}
}

View File

@ -1,7 +1,8 @@
import { useState } from 'react';
import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, SplitType } from '../../../constants/splitConstants';
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface SplitParameters {
export interface SplitParameters extends BaseParameters {
mode: SplitMode | '';
pages: string;
hDiv: string;
@ -14,15 +15,9 @@ export interface SplitParameters {
allowDuplicates: boolean;
}
export interface SplitParametersHook {
parameters: SplitParameters;
updateParameter: (parameter: keyof SplitParameters, value: string | boolean) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export type SplitParametersHook = BaseParametersHook<SplitParameters>;
const initialParameters: SplitParameters = {
const defaultParameters: SplitParameters = {
mode: '',
pages: '',
hDiv: '2',
@ -36,43 +31,27 @@ const initialParameters: SplitParameters = {
};
export const useSplitParameters = (): SplitParametersHook => {
const [parameters, setParameters] = useState<SplitParameters>(initialParameters);
return useBaseParameters({
defaultParameters,
endpointName: (params) => {
if (!params.mode) return ENDPOINTS[SPLIT_MODES.BY_PAGES];
return ENDPOINTS[params.mode as SplitMode];
},
validateFn: (params) => {
if (!params.mode) return false;
const updateParameter = (parameter: keyof SplitParameters, value: string | boolean) => {
setParameters(prev => ({ ...prev, [parameter]: value }));
};
const resetParameters = () => {
setParameters(initialParameters);
};
const validateParameters = () => {
if (!parameters.mode) return false;
switch (parameters.mode) {
case SPLIT_MODES.BY_PAGES:
return parameters.pages.trim() !== "";
case SPLIT_MODES.BY_SECTIONS:
return parameters.hDiv !== "" && parameters.vDiv !== "";
case SPLIT_MODES.BY_SIZE_OR_COUNT:
return parameters.splitValue.trim() !== "";
case SPLIT_MODES.BY_CHAPTERS:
return parameters.bookmarkLevel !== "";
default:
return false;
}
};
const getEndpointName = () => {
if (!parameters.mode) return ENDPOINTS[SPLIT_MODES.BY_PAGES];
return ENDPOINTS[parameters.mode as SplitMode];
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
switch (params.mode) {
case SPLIT_MODES.BY_PAGES:
return params.pages.trim() !== "";
case SPLIT_MODES.BY_SECTIONS:
return params.hDiv !== "" && params.vDiv !== "";
case SPLIT_MODES.BY_SIZE_OR_COUNT:
return params.splitValue.trim() !== "";
case SPLIT_MODES.BY_CHAPTERS:
return params.bookmarkLevel !== "";
default:
return false;
}
},
});
};

View File

@ -48,8 +48,8 @@ export const useToolManagement = (): ToolManagementResult => {
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
availableToolRegistry[toolKey] = {
...baseTool,
name: t(baseTool.name),
description: t(baseTool.description)
name: baseTool.name,
description: baseTool.description,
};
}
});

View File

@ -13,6 +13,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../../i18n/config';
import axios from 'axios';
import { detectFileExtension } from '../../utils/fileUtils';
import { FIT_OPTIONS } from '../../constants/convertConstants';
// Mock axios
vi.mock('axios');
@ -23,8 +24,8 @@ vi.mock('../../services/fileStorage', () => ({
fileStorage: {
init: vi.fn().mockResolvedValue(undefined),
storeFile: vi.fn().mockImplementation((file, thumbnail) => {
return Promise.resolve({
id: `mock-id-${file.name}`,
return Promise.resolve({
id: `mock-id-${file.name}`,
name: file.name,
size: file.size,
type: file.type,
@ -403,7 +404,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
colorType: 'grayscale',
dpi: 150,
singleOrMultiple: 'single',
fitOption: 'fitToPage',
fitOption: FIT_OPTIONS.FIT_PAGE,
autoRotate: false,
combineImages: true
});
@ -417,7 +418,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('fitOption')).toBe('fitToPage');
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
expect(formData.get('colorType')).toBe('grayscale');
expect(formData.get('autoRotate')).toBe('false');
});