Merge branch 'V2' into booklet

This commit is contained in:
Anthony Stirling 2025-09-10 10:50:44 +01:00 committed by GitHub
commit a660e332e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 401 additions and 115 deletions

View File

@ -139,5 +139,8 @@
"app/core/src/main/java", "app/core/src/main/java",
"app/common/src/main/java", "app/common/src/main/java",
"app/proprietary/src/main/java" "app/proprietary/src/main/java"
] ],
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
} }

View File

@ -38,16 +38,18 @@
}, },
"scripts": { "scripts": {
"predev": "npm run generate-icons", "predev": "npm run generate-icons",
"dev": "npx tsc --noEmit && vite", "dev": "npm run typecheck && vite",
"prebuild": "npm run generate-icons", "prebuild": "npm run generate-icons",
"lint": "npx eslint", "lint": "eslint",
"build": "npx tsc --noEmit && vite build", "build": "npm run typecheck && vite build",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"check": "npm run typecheck && npm run lint && npm run test:run",
"generate-licenses": "node scripts/generate-licenses.js", "generate-licenses": "node scripts/generate-licenses.js",
"generate-icons": "node scripts/generate-icons.js", "generate-icons": "node scripts/generate-icons.js",
"generate-icons:verbose": "node scripts/generate-icons.js --verbose", "generate-icons:verbose": "node scripts/generate-icons.js --verbose",
"test": "vitest", "test": "vitest",
"test:run": "vitest run",
"test:watch": "vitest --watch", "test:watch": "vitest --watch",
"test:coverage": "vitest --coverage", "test:coverage": "vitest --coverage",
"test:e2e": "playwright test", "test:e2e": "playwright test",

View File

@ -1584,7 +1584,29 @@
"tags": "auto-detect,header-based,organize,relabel", "tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename", "title": "Auto Rename",
"header": "Auto Rename PDF", "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": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance,colour-correction" "tags": "color-correction,tune,modify,enhance,colour-correction"

View File

@ -1129,7 +1129,28 @@
"tags": "auto-detect,header-based,organize,relabel", "tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename", "title": "Auto Rename",
"header": "Auto Rename PDF", "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": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance" "tags": "color-correction,tune,modify,enhance"

View File

@ -385,6 +385,13 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/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", "moduleName": "@tailwindcss/node",
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss", "moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
@ -742,6 +749,13 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/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", "moduleName": "core-util-is",
"moduleUrl": "https://github.com/isaacs/core-util-is", "moduleUrl": "https://github.com/isaacs/core-util-is",
@ -924,6 +938,13 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/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", "moduleName": "file-selector",
"moduleUrl": "https://github.com/react-dropzone/file-selector", "moduleUrl": "https://github.com/react-dropzone/file-selector",
@ -1533,6 +1554,20 @@
"moduleLicense": "MIT", "moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/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", "moduleName": "pretty-format",
"moduleUrl": "https://github.com/facebook/jest", "moduleUrl": "https://github.com/facebook/jest",
@ -1928,7 +1963,7 @@
{ {
"moduleName": "typescript", "moduleName": "typescript",
"moduleUrl": "https://github.com/microsoft/TypeScript", "moduleUrl": "https://github.com/microsoft/TypeScript",
"moduleVersion": "5.8.3", "moduleVersion": "5.9.2",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
@ -1995,6 +2030,13 @@
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-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", "moduleName": "webidl-conversions",
"moduleUrl": "https://github.com/jsdom/webidl-conversions", "moduleUrl": "https://github.com/jsdom/webidl-conversions",

View File

@ -4,7 +4,7 @@ import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPa
interface AddPasswordSettingsProps { interface AddPasswordSettingsProps {
parameters: AddPasswordParameters; parameters: AddPasswordParameters;
onParameterChange: (key: keyof AddPasswordParameters, value: any) => void; onParameterChange: <K extends keyof AddPasswordParameters>(key: K, value: AddPasswordParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -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;

View File

@ -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 { useTranslation } from 'react-i18next';
import { Stack, Text, ScrollArea } from '@mantine/core'; import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
@ -93,7 +93,7 @@ export default function ToolSelector({
const renderedTools = useMemo(() => const renderedTools = useMemo(() =>
displayGroups.map((subcategory) => displayGroups.map((subcategory) =>
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching) renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true)
), [displayGroups, handleToolSelect, isSearching, t] ), [displayGroups, handleToolSelect, isSearching, t]
); );
@ -150,7 +150,7 @@ export default function ToolSelector({
<div onClick={handleSearchFocus} style={{ cursor: 'pointer', <div onClick={handleSearchFocus} style={{ cursor: 'pointer',
borderRadius: "var(--mantine-radius-lg)" }}> borderRadius: "var(--mantine-radius-lg)" }}>
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false} <ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
onSelect={()=>{}} rounded={true}></ToolButton> onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
</div> </div>
) : ( ) : (
// Show search input when no tool selected OR when dropdown is opened // Show search input when no tool selected OR when dropdown is opened

View File

@ -4,7 +4,7 @@ import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissi
interface ChangePermissionsSettingsProps { interface ChangePermissionsSettingsProps {
parameters: ChangePermissionsParameters; parameters: ChangePermissionsParameters;
onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void; onParameterChange: <K extends keyof ChangePermissionsParameters>(key: K, value: ChangePermissionsParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -5,7 +5,7 @@ import { CompressParameters } from "../../../hooks/tools/compress/useCompressPar
interface CompressSettingsProps { interface CompressSettingsProps {
parameters: CompressParameters; parameters: CompressParameters;
onParameterChange: (key: keyof CompressParameters, value: any) => void; onParameterChange: <K extends keyof CompressParameters>(key: K, value: CompressParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -5,7 +5,7 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
interface ConvertFromEmailSettingsProps { interface ConvertFromEmailSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
interface ConvertFromImageSettingsProps { interface ConvertFromImageSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -5,7 +5,7 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame
interface ConvertFromWebSettingsProps { interface ConvertFromWebSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext";
interface ConvertSettingsProps { interface ConvertSettingsProps {
parameters: ConvertParameters; 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}>; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
selectedFiles: StirlingFile[]; selectedFiles: StirlingFile[];
disabled?: boolean; disabled?: boolean;

View File

@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame
interface ConvertToImageSettingsProps { interface ConvertToImageSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -7,7 +7,7 @@ import { StirlingFile } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps { interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters; parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void; onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
selectedFiles: StirlingFile[]; selectedFiles: StirlingFile[];
disabled?: boolean; disabled?: boolean;
} }

View File

@ -16,7 +16,7 @@ interface AdvancedOption {
interface AdvancedOCRSettingsProps { interface AdvancedOCRSettingsProps {
advancedOptions: string[]; advancedOptions: string[];
ocrRenderType?: string; ocrRenderType?: string;
onParameterChange: (key: keyof OCRParameters, value: any) => void; onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -6,7 +6,7 @@ import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters';
interface OCRSettingsProps { interface OCRSettingsProps {
parameters: OCRParameters; parameters: OCRParameters;
onParameterChange: (key: keyof OCRParameters, value: any) => void; onParameterChange: <K extends keyof OCRParameters>(key: K, value: OCRParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -4,7 +4,7 @@ import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/us
interface RemovePasswordSettingsProps { interface RemovePasswordSettingsProps {
parameters: RemovePasswordParameters; parameters: RemovePasswordParameters;
onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void; onParameterChange: <K extends keyof RemovePasswordParameters>(key: K, value: RemovePasswordParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -4,7 +4,7 @@ import { SanitizeParameters, defaultParameters } from "../../../hooks/tools/sani
interface SanitizeSettingsProps { interface SanitizeSettingsProps {
parameters: SanitizeParameters; parameters: SanitizeParameters;
onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void; onParameterChange: <K extends keyof SanitizeParameters>(key: K, value: SanitizeParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }

View File

@ -5,6 +5,7 @@ import { Tooltip } from '../../shared/Tooltip';
export interface ToolWorkflowTitleProps { export interface ToolWorkflowTitleProps {
title: string; title: string;
description?: string;
tooltip?: { tooltip?: {
content?: React.ReactNode; content?: React.ReactNode;
tips?: any[]; tips?: any[];
@ -15,10 +16,19 @@ export interface ToolWorkflowTitleProps {
}; };
} }
export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) { export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowTitleProps) {
if (tooltip) { 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 ( return (
<> <>
{tooltip ? (
<Flex justify="center" w="100%"> <Flex justify="center" w="100%">
<Tooltip <Tooltip
content={tooltip.content} content={tooltip.content}
@ -26,27 +36,17 @@ export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) {
header={tooltip.header} header={tooltip.header}
sidebarTooltip={true} sidebarTooltip={true}
> >
<Flex align="center" gap="xs" onClick={(e) => e.stopPropagation()}> {titleContent}
<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>
</Tooltip> </Tooltip>
</Flex> </Flex>
<Divider /> ) : (
</> titleContent
); )}
}
return ( <Text size="sm" mb="md" p="sm" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
<> {description}
<Flex justify="center" w="100%">
<Text fw={500} size="xl" p="md">
{title}
</Text> </Text>
</Flex> <Divider mb="sm" />
<Divider />
</> </>
); );
} }

View File

@ -12,7 +12,8 @@ export const renderToolButtons = (
subcategory: SubcategoryGroup, subcategory: SubcategoryGroup,
selectedToolKey: string | null, selectedToolKey: string | null,
onSelect: (id: string) => void, onSelect: (id: string) => void,
showSubcategoryHeader: boolean = true showSubcategoryHeader: boolean = true,
disableNavigation: boolean = false
) => ( ) => (
<Box key={subcategory.subcategoryId} w="100%"> <Box key={subcategory.subcategoryId} w="100%">
{showSubcategoryHeader && ( {showSubcategoryHeader && (
@ -26,6 +27,7 @@ export const renderToolButtons = (
tool={tool} tool={tool}
isSelected={selectedToolKey === id} isSelected={selectedToolKey === id}
onSelect={onSelect} onSelect={onSelect}
disableNavigation={disableNavigation}
/> />
))} ))}
</div> </div>

View File

@ -1,11 +1,11 @@
import { Stack, TextInput, Select, Checkbox } from '@mantine/core'; import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
import { useTranslation } from 'react-i18next'; 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'; import { SplitParameters } from '../../../hooks/tools/split/useSplitParameters';
export interface SplitSettingsProps { export interface SplitSettingsProps {
parameters: SplitParameters; parameters: SplitParameters;
onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void; onParameterChange: <K extends keyof SplitParameters>(key: K, value: SplitParameters[K]) => void;
disabled?: boolean; disabled?: boolean;
} }
@ -62,7 +62,7 @@ const SplitSettings = ({
<Select <Select
label={t("split-by-size-or-count.type.label", "Split Type")} label={t("split-by-size-or-count.type.label", "Split Type")}
value={parameters.splitType} value={parameters.splitType}
onChange={(v) => v && onParameterChange('splitType', v)} onChange={(v) => isSplitType(v) && onParameterChange('splitType', v)}
disabled={disabled} disabled={disabled}
data={[ data={[
{ value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") }, { value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") },

View File

@ -12,9 +12,10 @@ interface ToolButtonProps {
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
rounded?: boolean; 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 isUnavailable = !tool.component && !tool.link;
const { getToolNavigation } = useToolNavigation(); const { getToolNavigation } = useToolNavigation();
@ -29,8 +30,8 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect
onSelect(id); onSelect(id);
}; };
// Get navigation props for URL support // Get navigation props for URL support (only if navigation is not disabled)
const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null; const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null;
const tooltipContent = isUnavailable const tooltipContent = isUnavailable
? (<span><strong>Coming soon:</strong> {tool.description}</span>) ? (<span><strong>Coming soon:</strong> {tool.description}</span>)

View 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")
]
}
]
};
};

View File

@ -12,6 +12,7 @@ import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import AddWatermark from "../tools/AddWatermark"; import AddWatermark from "../tools/AddWatermark";
import Repair from "../tools/Repair"; import Repair from "../tools/Repair";
import AutoRename from "../tools/AutoRename";
import SingleLargePage from "../tools/SingleLargePage"; import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms"; import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign"; import RemoveCertificateSign from "../tools/RemoveCertificateSign";
@ -33,6 +34,7 @@ import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCerti
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation"; import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation"; import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation"; import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
import CompressSettings from "../components/tools/compress/CompressSettings"; import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from "../components/tools/split/SplitSettings"; import SplitSettings from "../components/tools/split/SplitSettings";
@ -492,7 +494,10 @@ export function useFlatToolRegistry(): ToolRegistry {
"auto-rename-pdf-file": { "auto-rename-pdf-file": {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.auto-rename.title", "Auto Rename PDF File"), 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"), description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,

View File

@ -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.'))
});
};

View File

@ -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',
});
};

View File

@ -8,6 +8,7 @@ export interface ApiCallsConfig<TParams = void> {
buildFormData: (params: TParams, file: File) => FormData; buildFormData: (params: TParams, file: File) => FormData;
filePrefix: string; filePrefix: string;
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
preserveBackendFilename?: boolean;
} }
export const useToolApiCalls = <TParams = void>() => { export const useToolApiCalls = <TParams = void>() => {
@ -46,7 +47,8 @@ export const useToolApiCalls = <TParams = void>() => {
response.data, response.data,
[file], [file],
config.filePrefix, config.filePrefix,
config.responseHandler config.responseHandler,
config.preserveBackendFilename ? response.headers : undefined
); );
processedFiles.push(...responseFiles); processedFiles.push(...responseFiles);

View File

@ -33,6 +33,13 @@ interface BaseToolOperationConfig<TParams> {
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */ /** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string; 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) */ /** How to handle API responses (e.g., ZIP extraction, single file response) */
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
@ -178,7 +185,8 @@ export const useToolOperation = <TParams>(
endpoint: config.endpoint, endpoint: config.endpoint,
buildFormData: config.buildFormData, buildFormData: config.buildFormData,
filePrefix: config.filePrefix, filePrefix: config.filePrefix,
responseHandler: config.responseHandler responseHandler: config.responseHandler,
preserveBackendFilename: config.preserveBackendFilename
}; };
processedFiles = await processFiles( processedFiles = await processFiles(
params, params,

View File

@ -59,6 +59,7 @@ i18n
.init({ .init({
fallbackLng: 'en-GB', fallbackLng: 'en-GB',
supportedLngs: Object.keys(supportedLanguages), supportedLngs: Object.keys(supportedLanguages),
load: 'currentOnly',
nonExplicitSupportedLngs: false, nonExplicitSupportedLngs: false,
debug: process.env.NODE_ENV === 'development', debug: process.env.NODE_ENV === 'development',

View 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;

View File

@ -25,7 +25,8 @@ export type ModeType =
| 'single-large-page' | 'single-large-page'
| 'repair' | 'repair'
| 'unlockPdfForms' | 'unlockPdfForms'
| 'removeCertificateSign'; | 'removeCertificateSign'
| 'auto-rename-pdf-file';
// Normalized state types // Normalized state types
export interface ProcessedFilePage { export interface ProcessedFilePage {

View File

@ -2,8 +2,8 @@ import axios from 'axios';
import { ToolRegistry } from '../data/toolsTaxonomy'; import { ToolRegistry } from '../data/toolsTaxonomy';
import { AUTOMATION_CONSTANTS } from '../constants/automation'; import { AUTOMATION_CONSTANTS } from '../constants/automation';
import { AutomationFileProcessor } from './automationFileProcessor'; import { AutomationFileProcessor } from './automationFileProcessor';
import { ResourceManager } from './resourceManager';
import { ToolType } from '../hooks/tools/shared/useToolOperation'; import { ToolType } from '../hooks/tools/shared/useToolOperation';
import { processResponse } from './toolResponseProcessor';
/** /**
@ -68,12 +68,17 @@ export const executeToolOperationWithPrefix = async (
let result; let result;
if (response.data.type === 'application/pdf' || if (response.data.type === 'application/pdf' ||
(response.headers && response.headers['content-type'] === 'application/pdf')) { (response.headers && response.headers['content-type'] === 'application/pdf')) {
// Single PDF response (e.g. split with merge option) - use original filename // Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
const originalFileName = files[0]?.name || 'document.pdf'; const processedFiles = await processResponse(
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' }); response.data,
files,
filePrefix,
undefined,
config.preserveBackendFilename ? response.headers : undefined
);
result = { result = {
success: true, success: true,
files: [singleFile], files: processedFiles,
errors: [] errors: []
}; };
} else { } else {
@ -85,7 +90,8 @@ export const executeToolOperationWithPrefix = async (
console.warn(`⚠️ File processing warnings:`, result.errors); console.warn(`⚠️ File processing warnings:`, result.errors);
} }
// Apply prefix to files, replacing any existing prefix // 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 => { ? result.files.map(file => {
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, ''); const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type }); 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`); console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
// Create result file with automation prefix // Create result file using processResponse to respect preserveBackendFilename setting
const processedFiles = await processResponse(
const resultFile = ResourceManager.createResultFile(
response.data, response.data,
file.name, [file],
filePrefix filePrefix,
undefined,
config.preserveBackendFilename ? response.headers : undefined
); );
resultFiles.push(resultFile); resultFiles.push(...processedFiles);
console.log(`✅ Created result file: ${resultFile.name}`); console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
} }
console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`); console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`);

View File

@ -1,23 +1,40 @@
// Note: This utility should be used with useToolResources for ZIP operations // 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[]; export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<File[]> | File[];
/** /**
* Processes a blob response into File(s). * Processes a blob response into File(s).
* - If a tool-specific responseHandler is provided, it is used. * - 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. * - Otherwise, create a single file using the filePrefix + original name.
*/ */
export async function processResponse( export async function processResponse(
blob: Blob, blob: Blob,
originalFiles: File[], originalFiles: File[],
filePrefix: string, filePrefix: string,
responseHandler?: ResponseHandler responseHandler?: ResponseHandler,
responseHeaders?: Record<string, any>
): Promise<File[]> { ): Promise<File[]> {
if (responseHandler) { if (responseHandler) {
const out = await responseHandler(blob, originalFiles); const out = await responseHandler(blob, originalFiles);
return Array.isArray(out) ? out : [out as unknown as File]; 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 original = originalFiles[0]?.name ?? 'result.pdf';
const name = `${filePrefix}${original}`; const name = `${filePrefix}${original}`;
const type = blob.type || 'application/octet-stream'; const type = blob.type || 'application/octet-stream';