auto rename

This commit is contained in:
Anthony Stirling 2025-08-20 14:02:39 +01:00
parent d1cb3f0b30
commit 3994914c4b
12 changed files with 235 additions and 7 deletions

View File

@ -1318,6 +1318,25 @@
"header": "Auto Rename PDF", "header": "Auto Rename PDF",
"submit": "Auto Rename" "submit": "Auto Rename"
}, },
"autoRename": {
"title": "Auto Rename PDF File",
"description": "This tool will automatically rename PDF files based on their content. It analyses the document to find the most suitable title from the text.",
"submit": "Auto Rename",
"settings": {
"title": "Auto-Rename Settings",
"useFirstTextAsFallback": "Use first text as fallback",
"useFirstTextAsFallbackDesc": "If no suitable title is found, use the first text in the document"
},
"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"
}
},
"adjust-contrast": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance,colour-correction" "tags": "color-correction,tune,modify,enhance,colour-correction"
}, },

View File

@ -1045,6 +1045,25 @@
"header": "Auto Rename PDF", "header": "Auto Rename PDF",
"submit": "Auto Rename" "submit": "Auto Rename"
}, },
"autoRename": {
"title": "Auto Rename PDF File",
"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.",
"submit": "Auto Rename",
"settings": {
"title": "Auto-Rename Settings",
"useFirstTextAsFallback": "Use first text as fallback",
"useFirstTextAsFallbackDesc": "If no suitable title is found, use the first text in the document"
},
"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"
}
},
"adjust-contrast": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance" "tags": "color-correction,tune,modify,enhance"
}, },

View File

@ -0,0 +1,27 @@
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> = ({
parameters,
onParameterChange, // Used for parameter changes
disabled = false
}) => {
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

@ -14,6 +14,7 @@ import Repair from '../tools/Repair';
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';
import AutoRename from '../tools/AutoRename';
@ -356,11 +357,13 @@ export function useFlatToolRegistry(): ToolRegistry {
"auto-rename-pdf-file": { "auto-rename-pdf-file": {
icon: <span className="material-symbols-rounded">match_word</span>, icon: <span className="material-symbols-rounded">match_word</span>,
name: t("home.auto-rename.title", "Auto Rename PDF File"), name: t("home.auto-rename.title", "Auto Rename PDF File"),
component: null, component: AutoRename,
view: "format", view: "format",
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"),
category: ToolCategory.ADVANCED_TOOLS, category: ToolCategory.ADVANCED_TOOLS,
subcategory: SubcategoryId.AUTOMATION subcategory: SubcategoryId.AUTOMATION,
maxFiles: -1,
endpoints: ["auto-rename"]
}, },
"auto-split-pages": { "auto-split-pages": {
icon: <span className="material-symbols-rounded">split_scene_right</span>, icon: <span className="material-symbols-rounded">split_scene_right</span>,

View File

@ -0,0 +1,25 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { AutoRenameParameters } from './useAutoRenameParameters';
export const useAutoRenameOperation = () => {
const { t } = useTranslation();
const buildFormData = (parameters: AutoRenameParameters, file: File): FormData => {
const formData = new FormData();
formData.append("fileInput", file);
formData.append("useFirstTextAsFallback", parameters.useFirstTextAsFallback.toString());
return formData;
};
return useToolOperation<AutoRenameParameters>({
operationType: 'autoRename',
endpoint: '/api/v1/misc/auto-rename',
buildFormData,
filePrefix: '', // Not used when preserveBackendFilename is true
multiFileEndpoint: false,
preserveBackendFilename: true, // Use filename from backend response headers
getErrorMessage: createStandardErrorHandler(t('autoRename.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

@ -49,6 +49,13 @@ export interface ToolOperationConfig<TParams = void> {
*/ */
multiFileEndpoint?: boolean; multiFileEndpoint?: boolean;
/**
* 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;
@ -172,7 +179,8 @@ export const useToolOperation = <TParams = void>(
endpoint: config.endpoint, endpoint: config.endpoint,
buildFormData: config.buildFormData as (params: TParams, file: File) => FormData, buildFormData: config.buildFormData as (params: TParams, file: File) => FormData,
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

@ -0,0 +1,80 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { useAutoRenameParameters } from "../hooks/tools/autoRename/useAutoRenameParameters";
import { useAutoRenameOperation } from "../hooks/tools/autoRename/useAutoRenameOperation";
import { BaseToolProps } from "../types/tool";
const AutoRename = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { setCurrentMode } = useFileContext();
const { selectedFiles } = useToolFileSelection();
const autoRenameParams = useAutoRenameParameters();
const autoRenameOperation = useAutoRenameOperation();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(autoRenameParams.getEndpointName());
useEffect(() => {
autoRenameOperation.resetResults();
onPreviewFile?.(null);
}, [autoRenameParams.parameters]);
const handleAutoRename = async () => {
try {
await autoRenameOperation.executeOperation(autoRenameParams.parameters, selectedFiles);
if (autoRenameOperation.files && onComplete) {
onComplete(autoRenameOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : t("autoRename.error.failed", "Auto-rename operation failed"));
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "auto-rename-pdf-file");
setCurrentMode("viewer");
};
const handleSettingsReset = () => {
autoRenameOperation.resetResults();
onPreviewFile?.(null);
setCurrentMode("auto-rename-pdf-file");
};
const hasFiles = selectedFiles.length > 0;
const hasResults = autoRenameOperation.files.length > 0 || autoRenameOperation.downloadUrl !== null;
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasFiles || hasResults,
placeholder: t("autoRename.files.placeholder", "Select a PDF file in the main view to get started"),
},
steps: [],
executeButton: {
text: t("autoRename.submit", "Auto-Rename"),
isVisible: !hasResults,
loadingText: t("loading"),
onClick: handleAutoRename,
disabled: !autoRenameParams.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: autoRenameOperation,
title: t("autoRename.results.title", "Auto-Rename Results"),
onFileClick: handleThumbnailClick,
},
});
};
export default AutoRename;

View File

@ -22,7 +22,8 @@ export type ModeType =
| 'single-large-page' | 'single-large-page'
| 'repair' | 'repair'
| 'unlockPdfForms' | 'unlockPdfForms'
| 'removeCertificateSign'; | 'removeCertificateSign'
| 'auto-rename-pdf-file';
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';

View File

@ -10,7 +10,15 @@
export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => { export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (match && match[1]) { if (match && match[1]) {
return match[1].replace(/['"]/g, ''); const filename = match[1].replace(/['"]/g, '');
// Decode URL-encoded characters (e.g., %20 for spaces)
try {
return decodeURIComponent(filename);
} catch (error) {
// If decoding fails, return the original filename
console.warn('Failed to decode filename:', filename, error);
return filename;
}
} }
return null; return null;
}; };

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