mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Merge branch 'V2' into booklet
This commit is contained in:
commit
a660e332e3
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -139,5 +139,8 @@
|
||||
"app/core/src/main/java",
|
||||
"app/common/src/main/java",
|
||||
"app/proprietary/src/main/java"
|
||||
]
|
||||
],
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
@ -38,16 +38,18 @@
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "npm run generate-icons",
|
||||
"dev": "npx tsc --noEmit && vite",
|
||||
"dev": "npm run typecheck && vite",
|
||||
"prebuild": "npm run generate-icons",
|
||||
"lint": "npx eslint",
|
||||
"build": "npx tsc --noEmit && vite build",
|
||||
"lint": "eslint",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "npm run typecheck && npm run lint && npm run test:run",
|
||||
"generate-licenses": "node scripts/generate-licenses.js",
|
||||
"generate-icons": "node scripts/generate-icons.js",
|
||||
"generate-icons:verbose": "node scripts/generate-icons.js --verbose",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
|
@ -1584,7 +1584,29 @@
|
||||
"tags": "auto-detect,header-based,organize,relabel",
|
||||
"title": "Auto Rename",
|
||||
"header": "Auto Rename PDF",
|
||||
"submit": "Auto Rename"
|
||||
"description": "Automatically finds the title from your PDF content and uses it as the filename.",
|
||||
"submit": "Auto Rename",
|
||||
"files": {
|
||||
"placeholder": "Select a PDF file in the main view to get started"
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred whilst auto-renaming the PDF."
|
||||
},
|
||||
"results": {
|
||||
"title": "Auto-Rename Results"
|
||||
},
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "How Auto-Rename Works"
|
||||
},
|
||||
"howItWorks": {
|
||||
"title": "Smart Renaming",
|
||||
"text": "Automatically finds the title from your PDF content and uses it as the filename.",
|
||||
"bullet1": "Looks for text that appears to be a title or heading",
|
||||
"bullet2": "Creates a clean, valid filename from the detected title",
|
||||
"bullet3": "Keeps the original name if no suitable title is found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"adjust-contrast": {
|
||||
"tags": "color-correction,tune,modify,enhance,colour-correction"
|
||||
|
@ -1129,7 +1129,28 @@
|
||||
"tags": "auto-detect,header-based,organize,relabel",
|
||||
"title": "Auto Rename",
|
||||
"header": "Auto Rename PDF",
|
||||
"submit": "Auto Rename"
|
||||
"submit": "Auto Rename",
|
||||
"files": {
|
||||
"placeholder": "Select a PDF file in the main view to get started"
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while auto-renaming the PDF."
|
||||
},
|
||||
"results": {
|
||||
"title": "Auto-Rename Results"
|
||||
},
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "How Auto-Rename Works"
|
||||
},
|
||||
"howItWorks": {
|
||||
"title": "Smart Renaming",
|
||||
"text": "Automatically finds the best title from your PDF content and uses it as the filename.",
|
||||
"bullet1": "Looks for text that appears to be a title or heading",
|
||||
"bullet2": "Creates a clean, valid filename from the detected title",
|
||||
"bullet3": "Keeps the original name if no suitable title is found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"adjust-contrast": {
|
||||
"tags": "color-correction,tune,modify,enhance"
|
||||
|
@ -385,6 +385,13 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@posthog/core",
|
||||
"moduleUrl": "https://github.com/PostHog/posthog-js",
|
||||
"moduleVersion": "1.0.2",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "@tailwindcss/node",
|
||||
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
|
||||
@ -742,6 +749,13 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "core-js",
|
||||
"moduleUrl": "https://github.com/zloirock/core-js",
|
||||
"moduleVersion": "3.45.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "core-util-is",
|
||||
"moduleUrl": "https://github.com/isaacs/core-util-is",
|
||||
@ -924,6 +938,13 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "fflate",
|
||||
"moduleUrl": "https://github.com/101arrowz/fflate",
|
||||
"moduleVersion": "0.4.8",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "file-selector",
|
||||
"moduleUrl": "https://github.com/react-dropzone/file-selector",
|
||||
@ -1533,6 +1554,20 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "posthog-js",
|
||||
"moduleUrl": "https://github.com/PostHog/posthog-js",
|
||||
"moduleVersion": "1.261.0",
|
||||
"moduleLicense": "MIT*",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "preact",
|
||||
"moduleUrl": "https://github.com/preactjs/preact",
|
||||
"moduleVersion": "10.27.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
{
|
||||
"moduleName": "pretty-format",
|
||||
"moduleUrl": "https://github.com/facebook/jest",
|
||||
@ -1928,7 +1963,7 @@
|
||||
{
|
||||
"moduleName": "typescript",
|
||||
"moduleUrl": "https://github.com/microsoft/TypeScript",
|
||||
"moduleVersion": "5.8.3",
|
||||
"moduleVersion": "5.9.2",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
@ -1995,6 +2030,13 @@
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "web-vitals",
|
||||
"moduleUrl": "https://github.com/GoogleChrome/web-vitals",
|
||||
"moduleVersion": "4.2.4",
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": "webidl-conversions",
|
||||
"moduleUrl": "https://github.com/jsdom/webidl-conversions",
|
||||
|
@ -4,7 +4,7 @@ import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPa
|
||||
|
||||
interface AddPasswordSettingsProps {
|
||||
parameters: AddPasswordParameters;
|
||||
onParameterChange: (key: keyof AddPasswordParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof AddPasswordParameters>(key: K, value: AddPasswordParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AutoRenameParameters } from '../../../hooks/tools/autoRename/useAutoRenameParameters';
|
||||
|
||||
interface AutoRenameSettingsProps {
|
||||
parameters: AutoRenameParameters;
|
||||
onParameterChange: <K extends keyof AutoRenameParameters>(parameter: K, value: AutoRenameParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AutoRenameSettings: React.FC<AutoRenameSettingsProps> = (
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="auto-rename-settings">
|
||||
<p className="text-muted">
|
||||
{t('autoRename.description', 'This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text.')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoRenameSettings;
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Stack, Text, ScrollArea } from '@mantine/core';
|
||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||
@ -93,7 +93,7 @@ export default function ToolSelector({
|
||||
|
||||
const renderedTools = useMemo(() =>
|
||||
displayGroups.map((subcategory) =>
|
||||
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching)
|
||||
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true)
|
||||
), [displayGroups, handleToolSelect, isSearching, t]
|
||||
);
|
||||
|
||||
@ -150,7 +150,7 @@ export default function ToolSelector({
|
||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
||||
onSelect={()=>{}} rounded={true}></ToolButton>
|
||||
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
||||
</div>
|
||||
) : (
|
||||
// Show search input when no tool selected OR when dropdown is opened
|
||||
|
@ -4,7 +4,7 @@ import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissi
|
||||
|
||||
interface ChangePermissionsSettingsProps {
|
||||
parameters: ChangePermissionsParameters;
|
||||
onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void;
|
||||
onParameterChange: <K extends keyof ChangePermissionsParameters>(key: K, value: ChangePermissionsParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { CompressParameters } from "../../../hooks/tools/compress/useCompressPar
|
||||
|
||||
interface CompressSettingsProps {
|
||||
parameters: CompressParameters;
|
||||
onParameterChange: (key: keyof CompressParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof CompressParameters>(key: K, value: CompressParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -5,40 +5,40 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromEmailSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertFromEmailSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
const ConvertFromEmailSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: ConvertFromEmailSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm" data-testid="email-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.emailOptions", "Email to PDF Options")}:</Text>
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.includeAttachments", "Include email attachments")}
|
||||
checked={parameters.emailOptions.includeAttachments}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAttachments: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAttachments: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="include-attachments-checkbox"
|
||||
/>
|
||||
|
||||
|
||||
{parameters.emailOptions.includeAttachments && (
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.maxAttachmentSize", "Maximum attachment size (MB)")}:</Text>
|
||||
<NumberInput
|
||||
value={parameters.emailOptions.maxAttachmentSizeMB}
|
||||
onChange={(value) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
maxAttachmentSizeMB: Number(value) || 10
|
||||
onChange={(value) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
maxAttachmentSizeMB: Number(value) || 10
|
||||
})}
|
||||
min={1}
|
||||
max={100}
|
||||
@ -48,24 +48,24 @@ const ConvertFromEmailSettings = ({
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.includeAllRecipients", "Include CC and BCC recipients in header")}
|
||||
checked={parameters.emailOptions.includeAllRecipients}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAllRecipients: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
includeAllRecipients: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="include-all-recipients-checkbox"
|
||||
/>
|
||||
|
||||
|
||||
<Checkbox
|
||||
label={t("convert.downloadHtml", "Download HTML intermediate file instead of PDF")}
|
||||
checked={parameters.emailOptions.downloadHtml}
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
downloadHtml: event.currentTarget.checked
|
||||
onChange={(event) => onParameterChange('emailOptions', {
|
||||
...parameters.emailOptions,
|
||||
downloadHtml: event.currentTarget.checked
|
||||
})}
|
||||
disabled={disabled}
|
||||
data-testid="download-html-checkbox"
|
||||
@ -74,4 +74,4 @@ const ConvertFromEmailSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromEmailSettings;
|
||||
export default ConvertFromEmailSettings;
|
||||
|
@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromImageSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -5,28 +5,28 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertFromWebSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertFromWebSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
const ConvertFromWebSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: ConvertFromWebSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm" data-testid="web-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.webOptions", "Web to PDF Options")}:</Text>
|
||||
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
|
||||
<NumberInput
|
||||
value={parameters.htmlOptions.zoomLevel}
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: Number(value) || 1.0
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: Number(value) || 1.0
|
||||
})}
|
||||
min={0.1}
|
||||
max={3.0}
|
||||
@ -36,9 +36,9 @@ const ConvertFromWebSettings = ({
|
||||
/>
|
||||
<Slider
|
||||
value={parameters.htmlOptions.zoomLevel}
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: value
|
||||
onChange={(value) => onParameterChange('htmlOptions', {
|
||||
...parameters.htmlOptions,
|
||||
zoomLevel: value
|
||||
})}
|
||||
min={0.1}
|
||||
max={3.0}
|
||||
@ -51,4 +51,4 @@ const ConvertFromWebSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromWebSettings;
|
||||
export default ConvertFromWebSettings;
|
||||
|
@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext";
|
||||
|
||||
interface ConvertSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||
selectedFiles: StirlingFile[];
|
||||
disabled?: boolean;
|
||||
|
@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
|
||||
|
||||
interface ConvertToImageSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -7,16 +7,16 @@ import { StirlingFile } from '../../../types/fileContext';
|
||||
|
||||
interface ConvertToPdfaSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
selectedFiles: StirlingFile[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ConvertToPdfaSettings = ({
|
||||
parameters,
|
||||
const ConvertToPdfaSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
selectedFiles,
|
||||
disabled = false
|
||||
disabled = false
|
||||
}: ConvertToPdfaSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles);
|
||||
@ -29,7 +29,7 @@ const ConvertToPdfaSettings = ({
|
||||
return (
|
||||
<Stack gap="sm" data-testid="pdfa-settings">
|
||||
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
|
||||
|
||||
|
||||
{hasDigitalSignatures && (
|
||||
<Alert color="yellow">
|
||||
<Text size="sm">
|
||||
@ -37,14 +37,14 @@ const ConvertToPdfaSettings = ({
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text>
|
||||
<Select
|
||||
value={parameters.pdfaOptions.outputFormat}
|
||||
onChange={(value) => onParameterChange('pdfaOptions', {
|
||||
...parameters.pdfaOptions,
|
||||
outputFormat: value || 'pdfa-1'
|
||||
onChange={(value) => onParameterChange('pdfaOptions', {
|
||||
...parameters.pdfaOptions,
|
||||
outputFormat: value || 'pdfa-1'
|
||||
})}
|
||||
data={pdfaFormatOptions}
|
||||
disabled={disabled || isChecking}
|
||||
@ -58,4 +58,4 @@ const ConvertToPdfaSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertToPdfaSettings;
|
||||
export default ConvertToPdfaSettings;
|
||||
|
@ -16,7 +16,7 @@ interface AdvancedOption {
|
||||
interface AdvancedOCRSettingsProps {
|
||||
advancedOptions: string[];
|
||||
ocrRenderType?: string;
|
||||
onParameterChange: (key: keyof OCRParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
// Handle individual checkbox changes
|
||||
const handleCheckboxChange = (optionValue: string, checked: boolean) => {
|
||||
const option = advancedOptionsData.find(opt => opt.value === optionValue);
|
||||
|
||||
|
||||
if (option?.isSpecial) {
|
||||
// Handle special options (like compatibility mode) differently
|
||||
if (optionValue === 'compatibilityMode') {
|
||||
@ -69,7 +69,7 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
<Text size="sm" fw={500} mb="md">
|
||||
{t('ocr.settings.advancedOptions.label', 'Processing Options')}
|
||||
</Text>
|
||||
|
||||
|
||||
<Stack gap="sm">
|
||||
{advancedOptionsData.map((option) => (
|
||||
<Checkbox
|
||||
@ -87,4 +87,4 @@ const AdvancedOCRSettings: React.FC<AdvancedOCRSettingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedOCRSettings;
|
||||
export default AdvancedOCRSettings;
|
||||
|
@ -6,7 +6,7 @@ import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
|
||||
|
||||
interface OCRSettingsProps {
|
||||
parameters: OCRParameters;
|
||||
onParameterChange: (key: keyof OCRParameters, value: any) => void;
|
||||
onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/us
|
||||
|
||||
interface RemovePasswordSettingsProps {
|
||||
parameters: RemovePasswordParameters;
|
||||
onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void;
|
||||
onParameterChange: <K extends keyof RemovePasswordParameters>(key: K, value: RemovePasswordParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { SanitizeParameters, defaultParameters } from "../../../hooks/tools/sani
|
||||
|
||||
interface SanitizeSettingsProps {
|
||||
parameters: SanitizeParameters;
|
||||
onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void;
|
||||
onParameterChange: <K extends keyof SanitizeParameters>(key: K, value: SanitizeParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Tooltip } from '../../shared/Tooltip';
|
||||
|
||||
export interface ToolWorkflowTitleProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
tooltip?: {
|
||||
content?: React.ReactNode;
|
||||
tips?: any[];
|
||||
@ -15,10 +16,19 @@ export interface ToolWorkflowTitleProps {
|
||||
};
|
||||
}
|
||||
|
||||
export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<>
|
||||
export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowTitleProps) {
|
||||
const titleContent = (
|
||||
<Flex align="center" gap="xs" onClick={(e) => e.stopPropagation()}>
|
||||
<Text fw={500} size="lg" p="xs">
|
||||
{title}
|
||||
</Text>
|
||||
{tooltip && <LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tooltip ? (
|
||||
<Flex justify="center" w="100%">
|
||||
<Tooltip
|
||||
content={tooltip.content}
|
||||
@ -26,27 +36,17 @@ export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) {
|
||||
header={tooltip.header}
|
||||
sidebarTooltip={true}
|
||||
>
|
||||
<Flex align="center" gap="xs" onClick={(e) => e.stopPropagation()}>
|
||||
<Text fw={500} size="xl" p="md">
|
||||
{title}
|
||||
</Text>
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||
</Flex>
|
||||
{titleContent}
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
) : (
|
||||
titleContent
|
||||
)}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="center" w="100%">
|
||||
<Text fw={500} size="xl" p="md">
|
||||
{title}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<Text size="sm" mb="md" p="sm" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
|
||||
{description}
|
||||
</Text>
|
||||
<Divider mb="sm" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ export const renderToolButtons = (
|
||||
subcategory: SubcategoryGroup,
|
||||
selectedToolKey: string | null,
|
||||
onSelect: (id: string) => void,
|
||||
showSubcategoryHeader: boolean = true
|
||||
showSubcategoryHeader: boolean = true,
|
||||
disableNavigation: boolean = false
|
||||
) => (
|
||||
<Box key={subcategory.subcategoryId} w="100%">
|
||||
{showSubcategoryHeader && (
|
||||
@ -26,6 +27,7 @@ export const renderToolButtons = (
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
disableNavigation={disableNavigation}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isSplitMode, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants';
|
||||
import { isSplitMode, isSplitType, SPLIT_MODES, SPLIT_TYPES } from '../../../constants/splitConstants';
|
||||
import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters';
|
||||
|
||||
export interface SplitSettingsProps {
|
||||
parameters: SplitParameters;
|
||||
onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void;
|
||||
onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ const SplitSettings = ({
|
||||
<Select
|
||||
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||
value={parameters.splitType}
|
||||
onChange={(v) => v && onParameterChange('splitType', v)}
|
||||
onChange={(v) => isSplitType(v) && onParameterChange('splitType', v)}
|
||||
disabled={disabled}
|
||||
data={[
|
||||
{ value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") },
|
||||
|
@ -12,9 +12,10 @@ interface ToolButtonProps {
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
rounded?: boolean;
|
||||
disableNavigation?: boolean;
|
||||
}
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false }) => {
|
||||
const isUnavailable = !tool.component && !tool.link;
|
||||
const { getToolNavigation } = useToolNavigation();
|
||||
|
||||
@ -29,8 +30,8 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect
|
||||
onSelect(id);
|
||||
};
|
||||
|
||||
// Get navigation props for URL support
|
||||
const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null;
|
||||
// Get navigation props for URL support (only if navigation is not disabled)
|
||||
const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null;
|
||||
|
||||
const tooltipContent = isUnavailable
|
||||
? (<span><strong>Coming soon:</strong> {tool.description}</span>)
|
||||
|
22
frontend/src/components/tooltips/useAutoRenameTips.ts
Normal file
22
frontend/src/components/tooltips/useAutoRenameTips.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useAutoRenameTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("auto-rename.tooltip.header.title", "How Auto-Rename Works")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("auto-rename.tooltip.howItWorks.title", "Smart Renaming"),
|
||||
bullets: [
|
||||
t("auto-rename.tooltip.howItWorks.bullet1", "Looks for text that appears to be a title or heading"),
|
||||
t("auto-rename.tooltip.howItWorks.bullet2", "Creates a clean, valid filename from the detected title"),
|
||||
t("auto-rename.tooltip.howItWorks.bullet3", "Keeps the original name if no suitable title is found")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -12,6 +12,7 @@ import RemovePassword from "../tools/RemovePassword";
|
||||
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
|
||||
import AddWatermark from "../tools/AddWatermark";
|
||||
import Repair from "../tools/Repair";
|
||||
import AutoRename from "../tools/AutoRename";
|
||||
import SingleLargePage from "../tools/SingleLargePage";
|
||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||
@ -33,6 +34,7 @@ import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCerti
|
||||
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
|
||||
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
||||
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||
@ -492,7 +494,10 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
"auto-rename-pdf-file": {
|
||||
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.auto-rename.title", "Auto Rename PDF File"),
|
||||
component: null,
|
||||
component: AutoRename,
|
||||
maxFiles: -1,
|
||||
endpoints: ["remove-certificate-sign"],
|
||||
operationConfig: autoRenameOperationConfig,
|
||||
description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
|
||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||
subcategoryId: SubcategoryId.AUTOMATION,
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { AutoRenameParameters, defaultParameters } from './useAutoRenameParameters';
|
||||
|
||||
export const getFormData = ((parameters: AutoRenameParameters) =>
|
||||
Object.entries(parameters).map(([key, value]) =>
|
||||
[key, value.toString()]
|
||||
) as string[][]
|
||||
);
|
||||
|
||||
// Static function that can be used by both the hook and automation executor
|
||||
export const buildAutoRenameFormData = (parameters: AutoRenameParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
|
||||
// Add all permission parameters
|
||||
getFormData(parameters).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const autoRenameOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildAutoRenameFormData,
|
||||
operationType: 'autoRename',
|
||||
endpoint: '/api/v1/misc/auto-rename',
|
||||
filePrefix: 'autoRename_',
|
||||
preserveBackendFilename: true, // Use filename from backend response headers
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useAutoRenameOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation({
|
||||
...autoRenameOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('auto-rename.error.failed', 'An error occurred while auto-renaming the PDF.'))
|
||||
});
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface AutoRenameParameters extends BaseParameters {
|
||||
useFirstTextAsFallback: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: AutoRenameParameters = {
|
||||
useFirstTextAsFallback: false,
|
||||
};
|
||||
|
||||
export type AutoRenameParametersHook = BaseParametersHook<AutoRenameParameters>;
|
||||
|
||||
export const useAutoRenameParameters = (): AutoRenameParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'auto-rename',
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ export interface ApiCallsConfig<TParams = void> {
|
||||
buildFormData: (params: TParams, file: File) => FormData;
|
||||
filePrefix: string;
|
||||
responseHandler?: ResponseHandler;
|
||||
preserveBackendFilename?: boolean;
|
||||
}
|
||||
|
||||
export const useToolApiCalls = <TParams = void>() => {
|
||||
@ -46,7 +47,8 @@ export const useToolApiCalls = <TParams = void>() => {
|
||||
response.data,
|
||||
[file],
|
||||
config.filePrefix,
|
||||
config.responseHandler
|
||||
config.responseHandler,
|
||||
config.preserveBackendFilename ? response.headers : undefined
|
||||
);
|
||||
processedFiles.push(...responseFiles);
|
||||
|
||||
|
@ -33,6 +33,13 @@ interface BaseToolOperationConfig<TParams> {
|
||||
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
|
||||
filePrefix: string;
|
||||
|
||||
/**
|
||||
* Whether to preserve the filename provided by the backend in response headers.
|
||||
* When true, ignores filePrefix and uses the filename from Content-Disposition header.
|
||||
* Useful for tools like auto-rename where the backend determines the final filename.
|
||||
*/
|
||||
preserveBackendFilename?: boolean;
|
||||
|
||||
/** How to handle API responses (e.g., ZIP extraction, single file response) */
|
||||
responseHandler?: ResponseHandler;
|
||||
|
||||
@ -178,7 +185,8 @@ export const useToolOperation = <TParams>(
|
||||
endpoint: config.endpoint,
|
||||
buildFormData: config.buildFormData,
|
||||
filePrefix: config.filePrefix,
|
||||
responseHandler: config.responseHandler
|
||||
responseHandler: config.responseHandler,
|
||||
preserveBackendFilename: config.preserveBackendFilename
|
||||
};
|
||||
processedFiles = await processFiles(
|
||||
params,
|
||||
|
@ -59,6 +59,7 @@ i18n
|
||||
.init({
|
||||
fallbackLng: 'en-GB',
|
||||
supportedLngs: Object.keys(supportedLanguages),
|
||||
load: 'currentOnly',
|
||||
nonExplicitSupportedLngs: false,
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
|
44
frontend/src/tools/AutoRename.tsx
Normal file
44
frontend/src/tools/AutoRename.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps } from "../types/tool";
|
||||
|
||||
import { useAutoRenameParameters } from "../hooks/tools/autoRename/useAutoRenameParameters";
|
||||
import { useAutoRenameOperation } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
||||
import { useAutoRenameTips } from "../components/tooltips/useAutoRenameTips";
|
||||
|
||||
const AutoRename =(props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const base = useBaseTool(
|
||||
'"auto-rename-pdf-file',
|
||||
useAutoRenameParameters,
|
||||
useAutoRenameOperation,
|
||||
props
|
||||
);
|
||||
|
||||
return createToolFlow({
|
||||
title: { title:t("auto-rename.title", "Auto Rename PDF"), description: t("auto-rename.description", "Auto Rename PDF"), tooltip: useAutoRenameTips()},
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [],
|
||||
executeButton: {
|
||||
text: t("auto-rename.submit", "Auto Rename"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("auto-rename.results.title", "Auto-Rename Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default AutoRename;
|
@ -25,7 +25,8 @@ export type ModeType =
|
||||
| 'single-large-page'
|
||||
| 'repair'
|
||||
| 'unlockPdfForms'
|
||||
| 'removeCertificateSign';
|
||||
| 'removeCertificateSign'
|
||||
| 'auto-rename-pdf-file';
|
||||
|
||||
// Normalized state types
|
||||
export interface ProcessedFilePage {
|
||||
|
@ -2,8 +2,8 @@ import axios from 'axios';
|
||||
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||
import { AutomationFileProcessor } from './automationFileProcessor';
|
||||
import { ResourceManager } from './resourceManager';
|
||||
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
||||
import { processResponse } from './toolResponseProcessor';
|
||||
|
||||
|
||||
/**
|
||||
@ -68,12 +68,17 @@ export const executeToolOperationWithPrefix = async (
|
||||
let result;
|
||||
if (response.data.type === 'application/pdf' ||
|
||||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
|
||||
// Single PDF response (e.g. split with merge option) - use original filename
|
||||
const originalFileName = files[0]?.name || 'document.pdf';
|
||||
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||
// Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
|
||||
const processedFiles = await processResponse(
|
||||
response.data,
|
||||
files,
|
||||
filePrefix,
|
||||
undefined,
|
||||
config.preserveBackendFilename ? response.headers : undefined
|
||||
);
|
||||
result = {
|
||||
success: true,
|
||||
files: [singleFile],
|
||||
files: processedFiles,
|
||||
errors: []
|
||||
};
|
||||
} else {
|
||||
@ -85,7 +90,8 @@ export const executeToolOperationWithPrefix = async (
|
||||
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||
}
|
||||
// Apply prefix to files, replacing any existing prefix
|
||||
const processedFiles = filePrefix
|
||||
// Skip prefixing if preserveBackendFilename is true and backend provided a filename
|
||||
const processedFiles = filePrefix && !config.preserveBackendFilename
|
||||
? result.files.map(file => {
|
||||
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
|
||||
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type });
|
||||
@ -117,15 +123,16 @@ export const executeToolOperationWithPrefix = async (
|
||||
|
||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||
|
||||
// Create result file with automation prefix
|
||||
|
||||
const resultFile = ResourceManager.createResultFile(
|
||||
// Create result file using processResponse to respect preserveBackendFilename setting
|
||||
const processedFiles = await processResponse(
|
||||
response.data,
|
||||
file.name,
|
||||
filePrefix
|
||||
[file],
|
||||
filePrefix,
|
||||
undefined,
|
||||
config.preserveBackendFilename ? response.headers : undefined
|
||||
);
|
||||
resultFiles.push(resultFile);
|
||||
console.log(`✅ Created result file: ${resultFile.name}`);
|
||||
resultFiles.push(...processedFiles);
|
||||
console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`);
|
||||
|
@ -1,23 +1,40 @@
|
||||
// Note: This utility should be used with useToolResources for ZIP operations
|
||||
import { getFilenameFromHeaders } from './fileResponseUtils';
|
||||
|
||||
export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<File[]> | File[];
|
||||
|
||||
/**
|
||||
* Processes a blob response into File(s).
|
||||
* - If a tool-specific responseHandler is provided, it is used.
|
||||
* - If responseHeaders provided and contains Content-Disposition, uses that filename.
|
||||
* - Otherwise, create a single file using the filePrefix + original name.
|
||||
*/
|
||||
export async function processResponse(
|
||||
blob: Blob,
|
||||
originalFiles: File[],
|
||||
filePrefix: string,
|
||||
responseHandler?: ResponseHandler
|
||||
responseHandler?: ResponseHandler,
|
||||
responseHeaders?: Record<string, any>
|
||||
): Promise<File[]> {
|
||||
if (responseHandler) {
|
||||
const out = await responseHandler(blob, originalFiles);
|
||||
return Array.isArray(out) ? out : [out as unknown as File];
|
||||
}
|
||||
|
||||
// Check if we should use the backend-provided filename from headers
|
||||
// Only when responseHeaders are explicitly provided (indicating the operation requested this)
|
||||
if (responseHeaders) {
|
||||
const contentDisposition = responseHeaders['content-disposition'];
|
||||
const backendFilename = getFilenameFromHeaders(contentDisposition);
|
||||
if (backendFilename) {
|
||||
const type = blob.type || responseHeaders['content-type'] || 'application/octet-stream';
|
||||
return [new File([blob], backendFilename, { type })];
|
||||
}
|
||||
// If preserveBackendFilename was requested but no Content-Disposition header found,
|
||||
// fall back to default behavior (this handles cases where backend doesn't set the header)
|
||||
}
|
||||
|
||||
// Default behavior: use filePrefix + original name
|
||||
const original = originalFiles[0]?.name ?? 'result.pdf';
|
||||
const name = `${filePrefix}${original}`;
|
||||
const type = blob.type || 'application/octet-stream';
|
||||
|
Loading…
x
Reference in New Issue
Block a user