From f1cb8ce5b37ca5764e2bf6f37b441d7b064b0600 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:00:50 +0100 Subject: [PATCH 01/11] V2 Repair tool (#4234) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 13 ++- .../public/locales/en-US/translation.json | 13 ++- .../tools/repair/RepairSettings.tsx | 27 +++++++ .../src/data/useTranslatedToolRegistry.tsx | 7 +- .../hooks/tools/repair/useRepairOperation.ts | 23 ++++++ .../hooks/tools/repair/useRepairParameters.ts | 20 +++++ .../hooks/tools/shared/useBaseParameters.ts | 46 +++++++++++ frontend/src/tools/Repair.tsx | 80 +++++++++++++++++++ frontend/src/types/fileContext.ts | 3 +- frontend/src/types/parameters.ts | 7 ++ 10 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/tools/repair/RepairSettings.tsx create mode 100644 frontend/src/hooks/tools/repair/useRepairOperation.ts create mode 100644 frontend/src/hooks/tools/repair/useRepairParameters.ts create mode 100644 frontend/src/hooks/tools/shared/useBaseParameters.ts create mode 100644 frontend/src/tools/Repair.tsx create mode 100644 frontend/src/types/parameters.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 7d01af4f5..13d686f88 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1188,7 +1188,18 @@ "tags": "fix,restore,correction,recover", "title": "Repair", "header": "Repair PDFs", - "submit": "Repair" + "submit": "Repair", + "description": "This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.", + "filenamePrefix": "repaired", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst repairing the PDF." + }, + "results": { + "title": "Repair Results" + } }, "removeBlanks": { "tags": "cleanup,streamline,non-content,organize", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index af7188944..eb5d2e04e 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -915,7 +915,18 @@ "tags": "fix,restore,correction,recover", "title": "Repair", "header": "Repair PDFs", - "submit": "Repair" + "submit": "Repair", + "description": "This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.", + "filenamePrefix": "repaired", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while repairing the PDF." + }, + "results": { + "title": "Repair Results" + } }, "removeBlanks": { "tags": "cleanup,streamline,non-content,organize", diff --git a/frontend/src/components/tools/repair/RepairSettings.tsx b/frontend/src/components/tools/repair/RepairSettings.tsx new file mode 100644 index 000000000..15078defb --- /dev/null +++ b/frontend/src/components/tools/repair/RepairSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { RepairParameters } from '../../../hooks/tools/repair/useRepairParameters'; + +interface RepairSettingsProps { + parameters: RepairParameters; + onParameterChange: (parameter: K, value: RepairParameters[K]) => void; + disabled?: boolean; +} + +const RepairSettings: React.FC = ({ + parameters, + onParameterChange, + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('repair.description', 'This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.')} +

+
+ ); +}; + +export default RepairSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 5f6460217..1a53cf651 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -10,6 +10,7 @@ import ChangePermissions from '../tools/ChangePermissions'; import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; +import Repair from '../tools/Repair'; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -384,11 +385,13 @@ export function useFlatToolRegistry(): ToolRegistry { "repair": { icon: build, name: t("home.repair.title", "Repair"), - component: null, + component: Repair, view: "format", description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), category: ToolCategory.ADVANCED_TOOLS, - subcategory: SubcategoryId.ADVANCED_FORMATTING + subcategory: SubcategoryId.ADVANCED_FORMATTING, + maxFiles: -1, + endpoints: ["repair"] }, "detect-split-scanned-photos": { icon: scanner, diff --git a/frontend/src/hooks/tools/repair/useRepairOperation.ts b/frontend/src/hooks/tools/repair/useRepairOperation.ts new file mode 100644 index 000000000..b547bbd8f --- /dev/null +++ b/frontend/src/hooks/tools/repair/useRepairOperation.ts @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { RepairParameters } from './useRepairParameters'; + +export const useRepairOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: RepairParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + return formData; + }; + + return useToolOperation({ + operationType: 'repair', + endpoint: '/api/v1/misc/repair', + buildFormData, + filePrefix: t('repair.filenamePrefix', 'repaired') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/repair/useRepairParameters.ts b/frontend/src/hooks/tools/repair/useRepairParameters.ts new file mode 100644 index 000000000..5c924de93 --- /dev/null +++ b/frontend/src/hooks/tools/repair/useRepairParameters.ts @@ -0,0 +1,20 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface RepairParameters extends BaseParameters { + // Extends BaseParameters - ready for future parameter additions if needed +} + +export const defaultParameters: RepairParameters = { + // No parameters needed +}; + +export type RepairParametersHook = BaseParametersHook; + +export const useRepairParameters = (): RepairParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'repair', + // validateFn: optional custom validation if needed in future + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/shared/useBaseParameters.ts b/frontend/src/hooks/tools/shared/useBaseParameters.ts new file mode 100644 index 000000000..af244e6f9 --- /dev/null +++ b/frontend/src/hooks/tools/shared/useBaseParameters.ts @@ -0,0 +1,46 @@ +import { useState, useCallback } from 'react'; + +export interface BaseParametersHook { + parameters: T; + updateParameter: (parameter: K, value: T[K]) => void; + resetParameters: () => void; + validateParameters: () => boolean; + getEndpointName: () => string; +} + +export interface BaseParametersConfig { + defaultParameters: T; + endpointName: string; + validateFn?: (params: T) => boolean; +} + +export function useBaseParameters(config: BaseParametersConfig): BaseParametersHook { + const [parameters, setParameters] = useState(config.defaultParameters); + + const updateParameter = useCallback((parameter: K, value: T[K]) => { + setParameters(prev => ({ + ...prev, + [parameter]: value, + })); + }, []); + + const resetParameters = useCallback(() => { + setParameters(config.defaultParameters); + }, [config.defaultParameters]); + + const validateParameters = useCallback(() => { + return config.validateFn ? config.validateFn(parameters) : true; + }, [parameters, config.validateFn]); + + const getEndpointName = useCallback(() => { + return config.endpointName; + }, [config.endpointName]); + + return { + parameters, + updateParameter, + resetParameters, + validateParameters, + getEndpointName, + }; +} \ No newline at end of file diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx new file mode 100644 index 000000000..a3b63f591 --- /dev/null +++ b/frontend/src/tools/Repair.tsx @@ -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 { useRepairParameters } from "../hooks/tools/repair/useRepairParameters"; +import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation"; +import { BaseToolProps } from "../types/tool"; + +const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const repairParams = useRepairParameters(); + const repairOperation = useRepairOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(repairParams.getEndpointName()); + + useEffect(() => { + repairOperation.resetResults(); + onPreviewFile?.(null); + }, [repairParams.parameters]); + + const handleRepair = async () => { + try { + await repairOperation.executeOperation(repairParams.parameters, selectedFiles); + if (repairOperation.files && onComplete) { + onComplete(repairOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("repair.error.failed", "Repair operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "repair"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + repairOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("repair"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = repairOperation.files.length > 0 || repairOperation.downloadUrl !== null; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + placeholder: t("repair.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [], + executeButton: { + text: t("repair.submit", "Repair PDF"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleRepair, + disabled: !repairParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: repairOperation, + title: t("repair.results.title", "Repair Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default Repair; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 0b5552c4f..1c513b523 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -18,7 +18,8 @@ export type ModeType = | 'addPassword' | 'changePermissions' | 'watermark' - | 'removePassword'; + | 'removePassword' + | 'repair'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; diff --git a/frontend/src/types/parameters.ts b/frontend/src/types/parameters.ts new file mode 100644 index 000000000..6f8856a8b --- /dev/null +++ b/frontend/src/types/parameters.ts @@ -0,0 +1,7 @@ +// Base parameter interfaces for reusable patterns + +export interface BaseParameters { + // Base interface that all tool parameters should extend + // Provides a foundation for adding common properties across all tools + // Examples of future additions: userId, sessionId, commonFlags, etc. +} \ No newline at end of file From d74c3f4598860ed447ec682cd23edfc0acc01e81 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:30:57 +0100 Subject: [PATCH 02/11] V2 remove cert (#4239) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 13 ++- .../public/locales/en-US/translation.json | 13 ++- .../RemoveCertificateSignSettings.tsx | 27 +++++++ .../src/data/useTranslatedToolRegistry.tsx | 7 +- .../useRemoveCertificateSignOperation.ts | 23 ++++++ .../useRemoveCertificateSignParameters.ts | 19 +++++ frontend/src/tools/RemoveCertificateSign.tsx | 80 +++++++++++++++++++ frontend/src/types/fileContext.ts | 3 +- 8 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx create mode 100644 frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts create mode 100644 frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts create mode 100644 frontend/src/tools/RemoveCertificateSign.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 13d686f88..a9d38b142 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1268,7 +1268,18 @@ "title": "Remove Certificate Signature", "header": "Remove the digital certificate from the PDF", "selectPDF": "Select a PDF file:", - "submit": "Remove Signature" + "submit": "Remove Signature", + "description": "This tool will remove digital certificate signatures from your PDF document.", + "filenamePrefix": "unsigned", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst removing certificate signatures." + }, + "results": { + "title": "Certificate Removal Results" + } }, "pageLayout": { "tags": "merge,composite,single-view,organize", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index eb5d2e04e..43fbd72f1 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -995,7 +995,18 @@ "title": "Remove Certificate Signature", "header": "Remove the digital certificate from the PDF", "selectPDF": "Select a PDF file:", - "submit": "Remove Signature" + "submit": "Remove Signature", + "description": "This tool will remove digital certificate signatures from your PDF document.", + "filenamePrefix": "unsigned", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while removing certificate signatures." + }, + "results": { + "title": "Certificate Removal Results" + } }, "pageLayout": { "tags": "merge,composite,single-view,organize", diff --git a/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx b/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx new file mode 100644 index 000000000..f34e3f2e6 --- /dev/null +++ b/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { RemoveCertificateSignParameters } from '../../../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters'; + +interface RemoveCertificateSignSettingsProps { + parameters: RemoveCertificateSignParameters; + onParameterChange: (parameter: K, value: RemoveCertificateSignParameters[K]) => void; + disabled?: boolean; +} + +const RemoveCertificateSignSettings: React.FC = ({ + parameters, + onParameterChange, // Unused - kept for interface consistency and future extensibility + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('removeCertSign.description', 'This tool will remove digital certificate signatures from your PDF document.')} +

+
+ ); +}; + +export default RemoveCertificateSignSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 1a53cf651..364c54211 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -11,6 +11,7 @@ import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; +import RemoveCertificateSign from '../tools/RemoveCertificateSign'; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -323,11 +324,13 @@ export function useFlatToolRegistry(): ToolRegistry { "remove-certificate-sign": { icon: remove_moderator, name: t("home.removeCertSign.title", "Remove Certificate Signatures"), - component: null, + component: RemoveCertificateSign, view: "security", description: t("home.removeCertSign.desc", "Remove digital signatures from PDF documents"), category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + subcategory: SubcategoryId.REMOVAL, + maxFiles: -1, + endpoints: ["remove-certificate-sign"] }, diff --git a/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts new file mode 100644 index 000000000..5987944ec --- /dev/null +++ b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { RemoveCertificateSignParameters } from './useRemoveCertificateSignParameters'; + +export const useRemoveCertificateSignOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + return formData; + }; + + return useToolOperation({ + operationType: 'removeCertificateSign', + endpoint: '/api/v1/security/remove-cert-sign', + buildFormData, + filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts new file mode 100644 index 000000000..59903ccfc --- /dev/null +++ b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts @@ -0,0 +1,19 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface RemoveCertificateSignParameters extends BaseParameters { + // Extends BaseParameters - ready for future parameter additions if needed +} + +export const defaultParameters: RemoveCertificateSignParameters = { + // No parameters needed +}; + +export type RemoveCertificateSignParametersHook = BaseParametersHook; + +export const useRemoveCertificateSignParameters = (): RemoveCertificateSignParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'remove-certificate-sign', + }); +}; \ No newline at end of file diff --git a/frontend/src/tools/RemoveCertificateSign.tsx b/frontend/src/tools/RemoveCertificateSign.tsx new file mode 100644 index 000000000..e33675625 --- /dev/null +++ b/frontend/src/tools/RemoveCertificateSign.tsx @@ -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 { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters"; +import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; +import { BaseToolProps } from "../types/tool"; + +const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const removeCertificateSignParams = useRemoveCertificateSignParameters(); + const removeCertificateSignOperation = useRemoveCertificateSignOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removeCertificateSignParams.getEndpointName()); + + useEffect(() => { + removeCertificateSignOperation.resetResults(); + onPreviewFile?.(null); + }, [removeCertificateSignParams.parameters]); + + const handleRemoveSignature = async () => { + try { + await removeCertificateSignOperation.executeOperation(removeCertificateSignParams.parameters, selectedFiles); + if (removeCertificateSignOperation.files && onComplete) { + onComplete(removeCertificateSignOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("removeCertSign.error.failed", "Remove certificate signature operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "removeCertificateSign"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + removeCertificateSignOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("removeCertificateSign"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = removeCertificateSignOperation.files.length > 0 || removeCertificateSignOperation.downloadUrl !== null; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + placeholder: t("removeCertSign.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [], + executeButton: { + text: t("removeCertSign.submit", "Remove Signature"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleRemoveSignature, + disabled: !removeCertificateSignParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: removeCertificateSignOperation, + title: t("removeCertSign.results.title", "Certificate Removal Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default RemoveCertificateSign; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 1c513b523..b868bf0e6 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -19,7 +19,8 @@ export type ModeType = | 'changePermissions' | 'watermark' | 'removePassword' - | 'repair'; + | 'repair' + | 'removeCertificateSign'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; From 857b2b5c53cc1e3bf1b4a9bf8ece6d130bdceaa3 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:34:28 +0100 Subject: [PATCH 03/11] V2 Unlock forms (#4237) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 13 ++- .../public/locales/en-US/translation.json | 13 ++- .../unlockPdfForms/UnlockPdfFormsSettings.tsx | 27 +++++++ .../src/data/useTranslatedToolRegistry.tsx | 8 +- .../useUnlockPdfFormsOperation.ts | 23 ++++++ .../useUnlockPdfFormsParameters.ts | 19 +++++ frontend/src/tools/UnlockPdfForms.tsx | 80 +++++++++++++++++++ frontend/src/types/fileContext.ts | 1 + 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx create mode 100644 frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts create mode 100644 frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters.ts create mode 100644 frontend/src/tools/UnlockPdfForms.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index a9d38b142..e3bb3c0c6 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1020,7 +1020,18 @@ "tags": "remove,delete,form,field,readonly", "title": "Remove Read-Only from Form Fields", "header": "Unlock PDF Forms", - "submit": "Remove" + "submit": "Unlock Forms", + "description": "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.", + "filenamePrefix": "unlocked_forms", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst unlocking PDF forms." + }, + "results": { + "title": "Unlocked Forms Results" + } }, "changeMetadata": { "tags": "Title,author,date,creation,time,publisher,producer,stats", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 43fbd72f1..541c19fb5 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -747,7 +747,18 @@ "tags": "remove,delete,form,field,readonly", "title": "Remove Read-Only from Form Fields", "header": "Unlock PDF Forms", - "submit": "Remove" + "submit": "Unlock Forms", + "description": "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.", + "filenamePrefix": "unlocked_forms", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while unlocking PDF forms." + }, + "results": { + "title": "Unlocked Forms Results" + } }, "changeMetadata": { "tags": "Title,author,date,creation,time,publisher,producer,stats", diff --git a/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx b/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx new file mode 100644 index 000000000..cc8697d7a --- /dev/null +++ b/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { UnlockPdfFormsParameters } from '../../../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters'; + +interface UnlockPdfFormsSettingsProps { + parameters: UnlockPdfFormsParameters; + onParameterChange: (parameter: K, value: UnlockPdfFormsParameters[K]) => void; + disabled?: boolean; +} + +const UnlockPdfFormsSettings: React.FC = ({ + parameters, + onParameterChange, // Unused - kept for interface consistency and future extensibility + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('unlockPDFForms.description', 'This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.')} +

+
+ ); +}; + +export default UnlockPdfFormsSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 364c54211..db6aca089 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -11,8 +11,10 @@ import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; +import UnlockPdfForms from '../tools/UnlockPdfForms'; import RemoveCertificateSign from '../tools/RemoveCertificateSign'; + // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { const { t } = useTranslation(); @@ -96,11 +98,13 @@ export function useFlatToolRegistry(): ToolRegistry { "unlock-pdf-forms": { icon: preview_off, name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), - component: null, + component: UnlockPdfForms, view: "security", description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."), category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.DOCUMENT_SECURITY + subcategory: SubcategoryId.DOCUMENT_SECURITY, + maxFiles: -1, + endpoints: ["unlock-pdf-forms"] }, "manage-certificates": { icon: license, diff --git a/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts new file mode 100644 index 000000000..3b648762b --- /dev/null +++ b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { UnlockPdfFormsParameters } from './useUnlockPdfFormsParameters'; + +export const useUnlockPdfFormsOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + return formData; + }; + + return useToolOperation({ + operationType: 'unlockPdfForms', + endpoint: '/api/v1/misc/unlock-pdf-forms', + buildFormData, + filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters.ts b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters.ts new file mode 100644 index 000000000..ad2536643 --- /dev/null +++ b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters.ts @@ -0,0 +1,19 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface UnlockPdfFormsParameters extends BaseParameters { + // Extends BaseParameters - ready for future parameter additions if needed +} + +export const defaultParameters: UnlockPdfFormsParameters = { + // No parameters needed +}; + +export type UnlockPdfFormsParametersHook = BaseParametersHook; + +export const useUnlockPdfFormsParameters = (): UnlockPdfFormsParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'unlock-pdf-forms', + }); +}; \ No newline at end of file diff --git a/frontend/src/tools/UnlockPdfForms.tsx b/frontend/src/tools/UnlockPdfForms.tsx new file mode 100644 index 000000000..b8aee7894 --- /dev/null +++ b/frontend/src/tools/UnlockPdfForms.tsx @@ -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 { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters"; +import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; +import { BaseToolProps } from "../types/tool"; + +const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const unlockPdfFormsParams = useUnlockPdfFormsParameters(); + const unlockPdfFormsOperation = useUnlockPdfFormsOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(unlockPdfFormsParams.getEndpointName()); + + useEffect(() => { + unlockPdfFormsOperation.resetResults(); + onPreviewFile?.(null); + }, [unlockPdfFormsParams.parameters]); + + const handleUnlock = async () => { + try { + await unlockPdfFormsOperation.executeOperation(unlockPdfFormsParams.parameters, selectedFiles); + if (unlockPdfFormsOperation.files && onComplete) { + onComplete(unlockPdfFormsOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("unlockPDFForms.error.failed", "Unlock PDF forms operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "unlockPdfForms"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + unlockPdfFormsOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("unlockPdfForms"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = unlockPdfFormsOperation.files.length > 0 || unlockPdfFormsOperation.downloadUrl !== null; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + placeholder: t("unlockPDFForms.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [], + executeButton: { + text: t("unlockPDFForms.submit", "Unlock Forms"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleUnlock, + disabled: !unlockPdfFormsParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: unlockPdfFormsOperation, + title: t("unlockPDFForms.results.title", "Unlocked Forms Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default UnlockPdfForms; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index b868bf0e6..6e2a2ef93 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -20,6 +20,7 @@ export type ModeType = | 'watermark' | 'removePassword' | 'repair' + | 'unlockPdfForms' | 'removeCertificateSign'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; From d1cb3f0b300bbedbf786f82fbad6171b4e60f635 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:38:41 +0100 Subject: [PATCH 04/11] V2 PDF to large page (#4236) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 13 ++- .../public/locales/en-US/translation.json | 13 ++- .../SingleLargePageSettings.tsx | 27 +++++++ .../src/data/useTranslatedToolRegistry.tsx | 8 +- .../useSingleLargePageOperation.ts | 23 ++++++ .../useSingleLargePageParameters.ts | 19 +++++ frontend/src/tools/SingleLargePage.tsx | 80 +++++++++++++++++++ frontend/src/types/fileContext.ts | 1 + 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx create mode 100644 frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts create mode 100644 frontend/src/hooks/tools/singleLargePage/useSingleLargePageParameters.ts create mode 100644 frontend/src/tools/SingleLargePage.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e3bb3c0c6..333f6ca8b 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1618,7 +1618,18 @@ "pdfToSinglePage": { "title": "PDF To Single Page", "header": "PDF To Single Page", - "submit": "Convert To Single Page" + "submit": "Convert To Single Page", + "description": "This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.", + "filenamePrefix": "single_page", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst converting to single page." + }, + "results": { + "title": "Single Page Results" + } }, "pageExtracter": { "title": "Extract Pages", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 541c19fb5..0638d78d3 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1345,7 +1345,18 @@ "pdfToSinglePage": { "title": "PDF To Single Page", "header": "PDF To Single Page", - "submit": "Convert To Single Page" + "submit": "Convert To Single Page", + "description": "This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.", + "filenamePrefix": "single_page", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while converting to single page." + }, + "results": { + "title": "Single Page Results" + } }, "pageExtracter": { "title": "Extract Pages", diff --git a/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx b/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx new file mode 100644 index 000000000..87dfef926 --- /dev/null +++ b/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SingleLargePageParameters } from '../../../hooks/tools/singleLargePage/useSingleLargePageParameters'; + +interface SingleLargePageSettingsProps { + parameters: SingleLargePageParameters; + onParameterChange: (parameter: K, value: SingleLargePageParameters[K]) => void; + disabled?: boolean; +} + +const SingleLargePageSettings: React.FC = ({ + parameters, + onParameterChange, + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('pdfToSinglePage.description', 'This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.')} +

+
+ ); +}; + +export default SingleLargePageSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index db6aca089..359cf6d35 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -11,10 +11,12 @@ import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; +import SingleLargePage from '../tools/SingleLargePage'; import UnlockPdfForms from '../tools/UnlockPdfForms'; import RemoveCertificateSign from '../tools/RemoveCertificateSign'; + // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { const { t } = useTranslation(); @@ -236,11 +238,13 @@ export function useFlatToolRegistry(): ToolRegistry { "single-large-page": { icon: looks_one, name: t("home.PdfToSinglePage.title", "PDF to Single Large Page"), - component: null, + component: SingleLargePage, view: "format", description: t("home.PdfToSinglePage.desc", "Merges all PDF pages into one large single page"), category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.PAGE_FORMATTING + subcategory: SubcategoryId.PAGE_FORMATTING, + maxFiles: -1, + endpoints: ["pdf-to-single-page"] }, "add-attachments": { icon: attachment, diff --git a/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts new file mode 100644 index 000000000..e73944864 --- /dev/null +++ b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { SingleLargePageParameters } from './useSingleLargePageParameters'; + +export const useSingleLargePageOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: SingleLargePageParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + return formData; + }; + + return useToolOperation({ + operationType: 'singleLargePage', + endpoint: '/api/v1/general/pdf-to-single-page', + buildFormData, + filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/singleLargePage/useSingleLargePageParameters.ts b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageParameters.ts new file mode 100644 index 000000000..df401b1a4 --- /dev/null +++ b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageParameters.ts @@ -0,0 +1,19 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface SingleLargePageParameters extends BaseParameters { + // Extends BaseParameters - ready for future parameter additions if needed +} + +export const defaultParameters: SingleLargePageParameters = { + // No parameters needed +}; + +export type SingleLargePageParametersHook = BaseParametersHook; + +export const useSingleLargePageParameters = (): SingleLargePageParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'pdf-to-single-page', + }); +}; \ No newline at end of file diff --git a/frontend/src/tools/SingleLargePage.tsx b/frontend/src/tools/SingleLargePage.tsx new file mode 100644 index 000000000..0c4fb96db --- /dev/null +++ b/frontend/src/tools/SingleLargePage.tsx @@ -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 { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters"; +import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; +import { BaseToolProps } from "../types/tool"; + +const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const singleLargePageParams = useSingleLargePageParameters(); + const singleLargePageOperation = useSingleLargePageOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(singleLargePageParams.getEndpointName()); + + useEffect(() => { + singleLargePageOperation.resetResults(); + onPreviewFile?.(null); + }, [singleLargePageParams.parameters]); + + const handleConvert = async () => { + try { + await singleLargePageOperation.executeOperation(singleLargePageParams.parameters, selectedFiles); + if (singleLargePageOperation.files && onComplete) { + onComplete(singleLargePageOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("pdfToSinglePage.error.failed", "Single large page operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "single-large-page"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + singleLargePageOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("single-large-page"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = singleLargePageOperation.files.length > 0 || singleLargePageOperation.downloadUrl !== null; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + placeholder: t("pdfToSinglePage.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [], + executeButton: { + text: t("pdfToSinglePage.submit", "Convert To Single Page"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleConvert, + disabled: !singleLargePageParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: singleLargePageOperation, + title: t("pdfToSinglePage.results.title", "Single Page Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default SingleLargePage; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 6e2a2ef93..d9dde75b7 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -19,6 +19,7 @@ export type ModeType = | 'changePermissions' | 'watermark' | 'removePassword' + | 'single-large-page' | 'repair' | 'unlockPdfForms' | 'removeCertificateSign'; From eada9e43ec6e6cf57f238d72f4b3e273ad08008a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:12:12 +0100 Subject: [PATCH 05/11] tooltip (#4242) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 44 +++++++++++- .../public/locales/en-US/translation.json | 67 ++++++++++++++++++- .../tooltips/usePageSelectionTips.tsx | 62 +++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/tooltips/usePageSelectionTips.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 333f6ca8b..64cd9408d 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1011,7 +1011,49 @@ "submit": "Change" }, "removePages": { - "tags": "Remove pages,delete pages" + "tags": "Remove pages,delete pages", + "title": "Remove Pages", + "pageNumbers": "Pages to Remove", + "pageNumbersPlaceholder": "e.g. 1,3,5-7", + "pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7", + "filenamePrefix": "pages_removed", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "settings": { + "title": "Page Selection" + }, + "error": { + "failed": "An error occurred whilst removing pages." + }, + "results": { + "title": "Page Removal Results" + }, + "submit": "Remove Pages" + }, + "pageSelection": { + "tooltip": { + "header": { + "title": "Page Selection Guide" + }, + "basic": { + "title": "Basic Usage", + "text": "Select specific pages from your PDF document using simple syntax.", + "bullet1": "Individual pages: 1,3,5", + "bullet2": "Page ranges: 3-6 or 10-15", + "bullet3": "All pages: all" + }, + "advanced": { + "title": "Advanced Features" + }, + "tips": { + "title": "Tips", + "text": "Keep these guidelines in mind:", + "bullet1": "Page numbers start from 1 (not 0)", + "bullet2": "Spaces are automatically removed", + "bullet3": "Invalid expressions are ignored" + } + } }, "compressPdfs": { "tags": "squish,small,tiny" diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 0638d78d3..358ccd53a 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -738,7 +738,72 @@ "submit": "Change" }, "removePages": { - "tags": "Remove pages,delete pages" + "tags": "Remove pages,delete pages", + "title": "Remove Pages", + "pageNumbers": "Pages to Remove", + "pageNumbersPlaceholder": "e.g. 1,3,5-7", + "pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7", + "filenamePrefix": "pages_removed", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "settings": { + "title": "Page Selection" + }, + "error": { + "failed": "An error occurred while removing pages." + }, + "results": { + "title": "Page Removal Results" + }, + "submit": "Remove Pages" + }, + "pageSelection": { + "tooltip": { + "header": { + "title": "Page Selection Guide" + }, + "basic": { + "title": "Basic Usage", + "text": "Select specific pages from your PDF document using simple syntax.", + "bullet1": "Individual pages: 1,3,5", + "bullet2": "Page ranges: 3-6 or 10-15", + "bullet3": "All pages: all" + }, + "advanced": { + "title": "Advanced Features", + "expandText": "▶ Show advanced options", + "collapseText": "▼ Hide advanced options", + "mathematical": { + "title": "Mathematical Functions", + "text": "Use mathematical expressions to select page patterns:", + "bullet1": "2n - all even pages (2, 4, 6, 8...)", + "bullet2": "2n+1 - all odd pages (1, 3, 5, 7...)", + "bullet3": "3n - every 3rd page (3, 6, 9, 12...)", + "bullet4": "4n-1 - pages 3, 7, 11, 15..." + }, + "ranges": { + "title": "Open-ended Ranges", + "text": "Select from a starting point to the end:", + "bullet1": "5- selects pages 5 to end of document", + "bullet2": "10- selects pages 10 to end" + }, + "combinations": { + "title": "Complex Combinations", + "text": "Combine different selection methods:", + "bullet1": "1,3-5,8,2n - pages 1, 3-5, 8, and all even pages", + "bullet2": "10-,2n+1 - pages 10 to end plus all odd pages", + "bullet3": "1-5,15-,3n - pages 1-5, 15 to end, and every 3rd page" + } + }, + "tips": { + "title": "Tips", + "text": "Keep these guidelines in mind:", + "bullet1": "Page numbers start from 1 (not 0)", + "bullet2": "Spaces are automatically removed", + "bullet3": "Invalid expressions are ignored" + } + } }, "compressPdfs": { "tags": "squish,small,tiny" diff --git a/frontend/src/components/tooltips/usePageSelectionTips.tsx b/frontend/src/components/tooltips/usePageSelectionTips.tsx new file mode 100644 index 000000000..d51034992 --- /dev/null +++ b/frontend/src/components/tooltips/usePageSelectionTips.tsx @@ -0,0 +1,62 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +/** + * Reusable tooltip for page selection functionality. + * Can be used by any tool that uses the GeneralUtils.parsePageList syntax. + */ +export const usePageSelectionTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("pageSelection.tooltip.header.title", "Page Selection Guide") + }, + tips: [ + { + description: t("pageSelection.tooltip.description", "Choose which pages to use for the operation. Supports single pages, ranges, formulas, and the all keyword.") + }, + { + title: t("pageSelection.tooltip.individual.title", "Individual Pages"), + description: t("pageSelection.tooltip.individual.description", "Enter numbers separated by commas."), + bullets: [ + t("pageSelection.tooltip.individual.bullet1", "1,3,5 → selects pages 1, 3, 5"), + t("pageSelection.tooltip.individual.bullet2", "2,7,12 → selects pages 2, 7, 12") + ] + }, + { + title: t("pageSelection.tooltip.ranges.title", "Page Ranges"), + description: t("pageSelection.tooltip.ranges.description", "Use - for consecutive pages."), + bullets: [ + t("pageSelection.tooltip.ranges.bullet1", "3-6 → selects pages 3–6"), + t("pageSelection.tooltip.ranges.bullet2", "10-15 → selects pages 10–15"), + t("pageSelection.tooltip.ranges.bullet3", "5- → selects pages 5 to end") + ] + }, + { + title: t("pageSelection.tooltip.mathematical.title", "Mathematical Functions"), + description: t("pageSelection.tooltip.mathematical.description", "Use n in formulas for patterns."), + bullets: [ + t("pageSelection.tooltip.mathematical.bullet2", "2n-1 → all odd pages (1, 3, 5…)"), + t("pageSelection.tooltip.mathematical.bullet1", "2n → all even pages (2, 4, 6…)"), + t("pageSelection.tooltip.mathematical.bullet3", "3n → every 3rd page (3, 6, 9…)"), + t("pageSelection.tooltip.mathematical.bullet4", "4n-1 → pages 3, 7, 11, 15…") + ] + }, + { + title: t("pageSelection.tooltip.special.title", "Special Keywords"), + bullets: [ + t("pageSelection.tooltip.special.bullet1", "all → selects all pages"), + ] + }, + { + title: t("pageSelection.tooltip.complex.title", "Complex Combinations"), + description: t("pageSelection.tooltip.complex.description", "Mix different types."), + bullets: [ + t("pageSelection.tooltip.complex.bullet1", "1,3-5,8,2n → pages 1, 3–5, 8, plus evens"), + t("pageSelection.tooltip.complex.bullet2", "10-,2n-1 → from page 10 to end + odd pages") + ] + } + ] + }; +}; \ No newline at end of file From cd2b82d614b57b6024a6c2ee1f8602ea7f2634b2 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:51:55 +0100 Subject: [PATCH 06/11] Feature/v2/filemanagerimprovements (#4243) - Select all/deselect all - Delete Selected - Download Selected - Recent file delete -> menu button with drop down for delete and download - Shift click selection added {330DF96D-7040-4CCB-B089-523F370E3185} {2D2F4876-7D35-45C3-B0CD-3127EEEEF7B5} --------- Co-authored-by: Connor Yoh --- .../public/locales/en-GB/translation.json | 9 +- .../components/fileManager/DesktopLayout.tsx | 63 ++++---- .../components/fileManager/FileActions.tsx | 115 +++++++++++++ .../components/fileManager/FileListArea.tsx | 16 +- .../components/fileManager/FileListItem.tsx | 107 ++++++++---- .../components/fileManager/MobileLayout.tsx | 26 ++- frontend/src/contexts/FileManagerContext.tsx | 148 ++++++++++++++--- frontend/src/theme/mantineTheme.ts | 13 ++ frontend/src/utils/downloadUtils.ts | 152 ++++++++++++++++++ 9 files changed, 558 insertions(+), 91 deletions(-) create mode 100644 frontend/src/components/fileManager/FileActions.tsx create mode 100644 frontend/src/utils/downloadUtils.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 64cd9408d..efcc3de0c 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -2031,7 +2031,14 @@ "fileSize": "Size", "fileVersion": "Version", "totalSelected": "Total Selected", - "dropFilesHere": "Drop files here" + "dropFilesHere": "Drop files here", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "deleteSelected": "Delete Selected", + "downloadSelected": "Download Selected", + "selectedCount": "{{count}} selected", + "download": "Download", + "delete": "Delete" }, "storage": { "temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically", diff --git a/frontend/src/components/fileManager/DesktopLayout.tsx b/frontend/src/components/fileManager/DesktopLayout.tsx index be701ff20..8d1e32ffc 100644 --- a/frontend/src/components/fileManager/DesktopLayout.tsx +++ b/frontend/src/components/fileManager/DesktopLayout.tsx @@ -4,6 +4,7 @@ import FileSourceButtons from './FileSourceButtons'; import FileDetails from './FileDetails'; import SearchInput from './SearchInput'; import FileListArea from './FileListArea'; +import FileActions from './FileActions'; import HiddenFileInput from './HiddenFileInput'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; @@ -17,27 +18,27 @@ const DesktopLayout: React.FC = () => { return ( {/* Column 1: File Sources */} - - + {/* Column 2: File List */} - -
{ overflow: 'hidden' }}> {activeSource === 'recent' && ( -
- -
+ <> +
+ +
+
+ +
+ )} - +
0 ? modalHeight : '100%', backgroundColor: 'transparent', border: 'none', @@ -66,12 +75,12 @@ const DesktopLayout: React.FC = () => {
- + {/* Column 3: File Details */} - @@ -79,11 +88,11 @@ const DesktopLayout: React.FC = () => { - + {/* Hidden file input for local file selection */}
); }; -export default DesktopLayout; \ No newline at end of file +export default DesktopLayout; diff --git a/frontend/src/components/fileManager/FileActions.tsx b/frontend/src/components/fileManager/FileActions.tsx new file mode 100644 index 000000000..7bc8d27bc --- /dev/null +++ b/frontend/src/components/fileManager/FileActions.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Group, Text, ActionIcon, Tooltip } from "@mantine/core"; +import SelectAllIcon from "@mui/icons-material/SelectAll"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DownloadIcon from "@mui/icons-material/Download"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "../../contexts/FileManagerContext"; + +const FileActions: React.FC = () => { + const { t } = useTranslation(); + const { recentFiles, selectedFileIds, filteredFiles, onSelectAll, onDeleteSelected, onDownloadSelected } = + useFileManagerContext(); + + const handleSelectAll = () => { + onSelectAll(); + }; + + const handleDeleteSelected = () => { + if (selectedFileIds.length > 0) { + onDeleteSelected(); + } + }; + + const handleDownloadSelected = () => { + if (selectedFileIds.length > 0) { + onDownloadSelected(); + } + }; + + // Only show actions if there are files + if (recentFiles.length === 0) { + return null; + } + + const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length; + const hasSelection = selectedFileIds.length > 0; + + return ( +
+ {/* Left: Select All */} +
+ + + + + +
+ + {/* Center: Selected count */} +
+ {hasSelection && ( + + {t("fileManager.selectedCount", "{{count}} selected", { count: selectedFileIds.length })} + + )} +
+ + {/* Right: Delete and Download */} + + + + + + + + + + + + + +
+ ); +}; + +export default FileActions; diff --git a/frontend/src/components/fileManager/FileListArea.tsx b/frontend/src/components/fileManager/FileListArea.tsx index 8e1975137..fd9357a94 100644 --- a/frontend/src/components/fileManager/FileListArea.tsx +++ b/frontend/src/components/fileManager/FileListArea.tsx @@ -19,22 +19,23 @@ const FileListArea: React.FC = ({ activeSource, recentFiles, filteredFiles, - selectedFileIds, + selectedFilesSet, onFileSelect, onFileRemove, onFileDoubleClick, + onDownloadSingle, isFileSupported, } = useFileManagerContext(); const { t } = useTranslation(); if (activeSource === 'recent') { return ( - @@ -53,10 +54,11 @@ const FileListArea: React.FC = ({ onFileSelect(file)} + onSelect={(shiftKey) => onFileSelect(file, index, shiftKey)} onRemove={() => onFileRemove(index)} + onDownload={() => onDownloadSingle(file)} onDoubleClick={() => onFileDoubleClick(file)} /> )) @@ -77,4 +79,4 @@ const FileListArea: React.FC = ({ ); }; -export default FileListArea; \ No newline at end of file +export default FileListArea; diff --git a/frontend/src/components/fileManager/FileListItem.tsx b/frontend/src/components/fileManager/FileListItem.tsx index 147133009..4b0e408d1 100644 --- a/frontend/src/components/fileManager/FileListItem.tsx +++ b/frontend/src/components/fileManager/FileListItem.tsx @@ -1,6 +1,9 @@ import React, { useState } from 'react'; -import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core'; +import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu } from '@mantine/core'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import DeleteIcon from '@mui/icons-material/Delete'; +import DownloadIcon from '@mui/icons-material/Download'; +import { useTranslation } from 'react-i18next'; import { getFileSize, getFileDate } from '../../utils/fileUtils'; import { FileWithUrl } from '../../types/file'; @@ -8,33 +11,44 @@ interface FileListItemProps { file: FileWithUrl; isSelected: boolean; isSupported: boolean; - onSelect: () => void; + onSelect: (shiftKey?: boolean) => void; onRemove: () => void; + onDownload?: () => void; onDoubleClick?: () => void; isLast?: boolean; } -const FileListItem: React.FC = ({ - file, - isSelected, - isSupported, - onSelect, - onRemove, +const FileListItem: React.FC = ({ + file, + isSelected, + isSupported, + onSelect, + onRemove, + onDownload, onDoubleClick }) => { const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { t } = useTranslation(); + + // Keep item in hovered state if menu is open + const shouldShowHovered = isHovered || isMenuOpen; return ( <> - onSelect(e.shiftKey)} onDoubleClick={onDoubleClick} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -54,26 +68,59 @@ const FileListItem: React.FC = ({ }} /> - + {file.name} {getFileSize(file)} • {getFileDate(file)} - {/* Delete button - fades in/out on hover */} - { e.stopPropagation(); onRemove(); }} - style={{ - opacity: isHovered ? 1 : 0, - transform: isHovered ? 'scale(1)' : 'scale(0.8)', - transition: 'opacity 0.3s ease, transform 0.3s ease', - pointerEvents: isHovered ? 'auto' : 'none' - }} + + {/* Three dots menu - fades in/out on hover */} + setIsMenuOpen(true)} + onClose={() => setIsMenuOpen(false)} > - - + + e.stopPropagation()} + style={{ + opacity: shouldShowHovered ? 1 : 0, + transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', + transition: 'opacity 0.3s ease, transform 0.3s ease', + pointerEvents: shouldShowHovered ? 'auto' : 'none' + }} + > + + + + + + {onDownload && ( + } + onClick={(e) => { + e.stopPropagation(); + onDownload(); + }} + > + {t('fileManager.download', 'Download')} + + )} + } + onClick={(e) => { + e.stopPropagation(); + onRemove(); + }} + > + {t('fileManager.delete', 'Delete')} + + + { } @@ -81,4 +128,4 @@ const FileListItem: React.FC = ({ ); }; -export default FileListItem; \ No newline at end of file +export default FileListItem; diff --git a/frontend/src/components/fileManager/MobileLayout.tsx b/frontend/src/components/fileManager/MobileLayout.tsx index 30d1ad6b9..5201aafb4 100644 --- a/frontend/src/components/fileManager/MobileLayout.tsx +++ b/frontend/src/components/fileManager/MobileLayout.tsx @@ -4,6 +4,7 @@ import FileSourceButtons from './FileSourceButtons'; import FileDetails from './FileDetails'; import SearchInput from './SearchInput'; import FileListArea from './FileListArea'; +import FileActions from './FileActions'; import HiddenFileInput from './HiddenFileInput'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; @@ -22,10 +23,11 @@ const MobileLayout: React.FC = () => { // Estimate heights of fixed components const fileSourceHeight = '3rem'; // FileSourceButtons height const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height + const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom) const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height - const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps + const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps - return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`; + return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`; }; return ( @@ -51,12 +53,20 @@ const MobileLayout: React.FC = () => { minHeight: 0 }}> {activeSource === 'recent' && ( - - - + <> + + + + + + + )} diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 98b93cf0b..4115e873d 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react'; import { FileWithUrl } from '../types/file'; -import { StoredFile } from '../services/fileStorage'; +import { StoredFile, fileStorage } from '../services/fileStorage'; +import { downloadFiles } from '../utils/downloadUtils'; // Type for the context value - now contains everything directly interface FileManagerContextValue { @@ -11,16 +12,21 @@ interface FileManagerContextValue { selectedFiles: FileWithUrl[]; filteredFiles: FileWithUrl[]; fileInputRef: React.RefObject; + selectedFilesSet: Set; // Handlers onSourceChange: (source: 'recent' | 'local' | 'drive') => void; onLocalFileClick: () => void; - onFileSelect: (file: FileWithUrl) => void; + onFileSelect: (file: FileWithUrl, index: number, shiftKey?: boolean) => void; onFileRemove: (index: number) => void; onFileDoubleClick: (file: FileWithUrl) => void; onOpenFiles: () => void; onSearchChange: (value: string) => void; onFileInputChange: (event: React.ChangeEvent) => void; + onSelectAll: () => void; + onDeleteSelected: () => void; + onDownloadSelected: () => void; + onDownloadSingle: (file: FileWithUrl) => void; // External props recentFiles: FileWithUrl[]; @@ -60,22 +66,29 @@ export const FileManagerProvider: React.FC = ({ const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent'); const [selectedFileIds, setSelectedFileIds] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const [lastClickedIndex, setLastClickedIndex] = useState(null); const fileInputRef = useRef(null); // Track blob URLs for cleanup const createdBlobUrls = useRef>(new Set()); // Computed values (with null safety) - const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name)); - const filteredFiles = (recentFiles || []).filter(file => - file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const selectedFilesSet = new Set(selectedFileIds); + + const selectedFiles = selectedFileIds.length === 0 ? [] : + (recentFiles || []).filter(file => selectedFilesSet.has(file.id || file.name)); + + const filteredFiles = !searchTerm ? recentFiles || [] : + (recentFiles || []).filter(file => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => { setActiveSource(source); if (source !== 'recent') { setSelectedFileIds([]); setSearchTerm(''); + setLastClickedIndex(null); } }, []); @@ -83,19 +96,46 @@ export const FileManagerProvider: React.FC = ({ fileInputRef.current?.click(); }, []); - const handleFileSelect = useCallback((file: FileWithUrl) => { - setSelectedFileIds(prev => { - if (file.id) { - if (prev.includes(file.id)) { - return prev.filter(id => id !== file.id); - } else { - return [...prev, file.id]; + const handleFileSelect = useCallback((file: FileWithUrl, currentIndex: number, shiftKey?: boolean) => { + const fileId = file.id || file.name; + if (!fileId) return; + + if (shiftKey && lastClickedIndex !== null) { + // Range selection with shift-click + const startIndex = Math.min(lastClickedIndex, currentIndex); + const endIndex = Math.max(lastClickedIndex, currentIndex); + + setSelectedFileIds(prev => { + const selectedSet = new Set(prev); + + // Add all files in the range to selection + for (let i = startIndex; i <= endIndex; i++) { + const rangeFileId = filteredFiles[i]?.id || filteredFiles[i]?.name; + if (rangeFileId) { + selectedSet.add(rangeFileId); + } } - } else { - return prev; - } - }); - }, []); + + return Array.from(selectedSet); + }); + } else { + // Normal click behavior - optimized with Set for O(1) lookup + setSelectedFileIds(prev => { + const selectedSet = new Set(prev); + + if (selectedSet.has(fileId)) { + selectedSet.delete(fileId); + } else { + selectedSet.add(fileId); + } + + return Array.from(selectedSet); + }); + + // Update last clicked index for future range selections + setLastClickedIndex(currentIndex); + } + }, [filteredFiles, lastClickedIndex]); const handleFileRemove = useCallback((index: number) => { const fileToRemove = filteredFiles[index]; @@ -152,6 +192,72 @@ export const FileManagerProvider: React.FC = ({ event.target.value = ''; }, [storeFile, onFilesSelected, refreshRecentFiles, onClose]); + const handleSelectAll = useCallback(() => { + const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length; + if (allFilesSelected) { + // Deselect all + setSelectedFileIds([]); + setLastClickedIndex(null); + } else { + // Select all filtered files + setSelectedFileIds(filteredFiles.map(file => file.id || file.name)); + setLastClickedIndex(null); + } + }, [filteredFiles, selectedFileIds]); + + const handleDeleteSelected = useCallback(async () => { + if (selectedFileIds.length === 0) return; + + try { + // Get files to delete based on current filtered view + const filesToDelete = filteredFiles.filter(file => + selectedFileIds.includes(file.id || file.name) + ); + + // Delete files from storage + for (const file of filesToDelete) { + const lookupKey = file.id || file.name; + await fileStorage.deleteFile(lookupKey); + } + + // Clear selection + setSelectedFileIds([]); + + // Refresh the file list + await refreshRecentFiles(); + } catch (error) { + console.error('Failed to delete selected files:', error); + } + }, [selectedFileIds, filteredFiles, refreshRecentFiles]); + + + const handleDownloadSelected = useCallback(async () => { + if (selectedFileIds.length === 0) return; + + try { + // Get selected files + const selectedFilesToDownload = filteredFiles.filter(file => + selectedFileIds.includes(file.id || file.name) + ); + + // Use generic download utility + await downloadFiles(selectedFilesToDownload, { + zipFilename: `selected-files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip` + }); + } catch (error) { + console.error('Failed to download selected files:', error); + } + }, [selectedFileIds, filteredFiles]); + + const handleDownloadSingle = useCallback(async (file: FileWithUrl) => { + try { + await downloadFiles([file]); + } catch (error) { + console.error('Failed to download file:', error); + } + }, []); + + // Cleanup blob URLs when component unmounts useEffect(() => { return () => { @@ -169,6 +275,7 @@ export const FileManagerProvider: React.FC = ({ setActiveSource('recent'); setSelectedFileIds([]); setSearchTerm(''); + setLastClickedIndex(null); } }, [isOpen]); @@ -180,6 +287,7 @@ export const FileManagerProvider: React.FC = ({ selectedFiles, filteredFiles, fileInputRef, + selectedFilesSet, // Handlers onSourceChange: handleSourceChange, @@ -190,6 +298,10 @@ export const FileManagerProvider: React.FC = ({ onOpenFiles: handleOpenFiles, onSearchChange: handleSearchChange, onFileInputChange: handleFileInputChange, + onSelectAll: handleSelectAll, + onDeleteSelected: handleDeleteSelected, + onDownloadSelected: handleDownloadSelected, + onDownloadSingle: handleDownloadSingle, // External props recentFiles, diff --git a/frontend/src/theme/mantineTheme.ts b/frontend/src/theme/mantineTheme.ts index 27e70d461..a8552b2bb 100644 --- a/frontend/src/theme/mantineTheme.ts +++ b/frontend/src/theme/mantineTheme.ts @@ -191,6 +191,19 @@ export const mantineTheme = createTheme({ }, }, }, + Tooltip: { + styles: { + tooltip: { + backgroundColor: 'var( --tooltip-title-bg)', + color: 'var( --tooltip-title-color)', + border: '1px solid var(--tooltip-borderp)', + fontSize: '0.75rem', + fontWeight: '500', + boxShadow: 'var(--shadow-md)', + borderRadius: 'var(--radius-sm)', + }, + }, + }, Checkbox: { styles: { diff --git a/frontend/src/utils/downloadUtils.ts b/frontend/src/utils/downloadUtils.ts new file mode 100644 index 000000000..1c411c87d --- /dev/null +++ b/frontend/src/utils/downloadUtils.ts @@ -0,0 +1,152 @@ +import { FileWithUrl } from '../types/file'; +import { fileStorage } from '../services/fileStorage'; +import { zipFileService } from '../services/zipFileService'; + +/** + * Downloads a blob as a file using browser download API + * @param blob - The blob to download + * @param filename - The filename for the download + */ +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up the blob URL + URL.revokeObjectURL(url); +} + +/** + * Downloads a single file from IndexedDB storage + * @param file - The file object with storage information + * @throws Error if file cannot be retrieved from storage + */ +export async function downloadFileFromStorage(file: FileWithUrl): Promise { + const lookupKey = file.id || file.name; + const storedFile = await fileStorage.getFile(lookupKey); + + if (!storedFile) { + throw new Error(`File "${file.name}" not found in storage`); + } + + const blob = new Blob([storedFile.data], { type: storedFile.type }); + downloadBlob(blob, storedFile.name); +} + +/** + * Downloads multiple files as individual downloads + * @param files - Array of files to download + */ +export async function downloadMultipleFiles(files: FileWithUrl[]): Promise { + for (const file of files) { + await downloadFileFromStorage(file); + } +} + +/** + * Downloads multiple files as a single ZIP archive + * @param files - Array of files to include in ZIP + * @param zipFilename - Optional custom ZIP filename (defaults to timestamped name) + */ +export async function downloadFilesAsZip(files: FileWithUrl[], zipFilename?: string): Promise { + if (files.length === 0) { + throw new Error('No files provided for ZIP download'); + } + + // Convert stored files to File objects + const fileObjects: File[] = []; + for (const fileWithUrl of files) { + const lookupKey = fileWithUrl.id || fileWithUrl.name; + const storedFile = await fileStorage.getFile(lookupKey); + + if (storedFile) { + const file = new File([storedFile.data], storedFile.name, { + type: storedFile.type, + lastModified: storedFile.lastModified + }); + fileObjects.push(file); + } + } + + if (fileObjects.length === 0) { + throw new Error('No valid files found in storage for ZIP download'); + } + + // Generate default filename if not provided + const finalZipFilename = zipFilename || + `files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`; + + // Create and download ZIP + const { zipFile } = await zipFileService.createZipFromFiles(fileObjects, finalZipFilename); + downloadBlob(zipFile, finalZipFilename); +} + +/** + * Smart download function that handles single or multiple files appropriately + * - Single file: Downloads directly + * - Multiple files: Downloads as ZIP + * @param files - Array of files to download + * @param options - Download options + */ +export async function downloadFiles( + files: FileWithUrl[], + options: { + forceZip?: boolean; + zipFilename?: string; + multipleAsIndividual?: boolean; + } = {} +): Promise { + if (files.length === 0) { + throw new Error('No files provided for download'); + } + + if (files.length === 1 && !options.forceZip) { + // Single file download + await downloadFileFromStorage(files[0]); + } else if (options.multipleAsIndividual) { + // Multiple individual downloads + await downloadMultipleFiles(files); + } else { + // ZIP download (default for multiple files) + await downloadFilesAsZip(files, options.zipFilename); + } +} + +/** + * Downloads a File object directly (for files already in memory) + * @param file - The File object to download + * @param filename - Optional custom filename + */ +export function downloadFileObject(file: File, filename?: string): void { + downloadBlob(file, filename || file.name); +} + +/** + * Downloads text content as a file + * @param content - Text content to download + * @param filename - Filename for the download + * @param mimeType - MIME type (defaults to text/plain) + */ +export function downloadTextAsFile( + content: string, + filename: string, + mimeType: string = 'text/plain' +): void { + const blob = new Blob([content], { type: mimeType }); + downloadBlob(blob, filename); +} + +/** + * Downloads JSON data as a file + * @param data - Data to serialize and download + * @param filename - Filename for the download + */ +export function downloadJsonAsFile(data: any, filename: string): void { + const content = JSON.stringify(data, null, 2); + downloadTextAsFile(content, filename, 'application/json'); +} \ No newline at end of file From d06cbcaa91359cbd9efc6b0f2218b6125bd34920 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:54:30 +0100 Subject: [PATCH 07/11] V2 Files Selected indicator in toolstep (#4241) Added back our files selected indicator in tools step --------- Co-authored-by: Connor Yoh --- .../tools/shared/FileStatusIndicator.tsx | 22 ++++++++++++------- frontend/src/tools/AddPassword.tsx | 2 +- frontend/src/tools/AddWatermark.tsx | 2 +- frontend/src/tools/ChangePermissions.tsx | 2 +- frontend/src/tools/Compress.tsx | 2 +- frontend/src/tools/Convert.tsx | 3 +-- frontend/src/tools/OCR.tsx | 2 +- frontend/src/tools/RemovePassword.tsx | 2 +- frontend/src/tools/Repair.tsx | 4 ++-- frontend/src/tools/Sanitize.tsx | 3 +-- frontend/src/tools/Split.tsx | 3 +-- 11 files changed, 25 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index e6f0b2e2f..22a25e627 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -1,5 +1,6 @@ -import React from 'react'; -import { Text } from '@mantine/core'; +import React from "react"; +import { Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; export interface FileStatusIndicatorProps { selectedFiles?: File[]; @@ -8,20 +9,25 @@ export interface FileStatusIndicatorProps { const FileStatusIndicator = ({ selectedFiles = [], - placeholder = "Select a PDF file in the main view to get started" + placeholder, }: FileStatusIndicatorProps) => { + const { t } = useTranslation(); + const defaultPlaceholder = placeholder || t("files.placeholder", "Select a PDF file in the main view to get started"); // Only show content when no files are selected if (selectedFiles.length === 0) { return ( - {placeholder} + {defaultPlaceholder} ); } - // Return nothing when files are selected - return null; -} + return ( + + ✓ {selectedFiles.length === 1 ? t("fileSelected", "Selected: {{filename}}", { filename: selectedFiles[0]?.name }) : t("filesSelected", "{{count}} files selected", { count: selectedFiles.length })} + + ); +}; -export default FileStatusIndicator; \ No newline at end of file +export default FileStatusIndicator; diff --git a/frontend/src/tools/AddPassword.tsx b/frontend/src/tools/AddPassword.tsx index 35e97e1b4..20b603044 100644 --- a/frontend/src/tools/AddPassword.tsx +++ b/frontend/src/tools/AddPassword.tsx @@ -68,7 +68,7 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, }, steps: [ { diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx index 3f2357a7e..01306e6f0 100644 --- a/frontend/src/tools/AddWatermark.tsx +++ b/frontend/src/tools/AddWatermark.tsx @@ -189,7 +189,7 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, }, steps: getSteps(), executeButton: { diff --git a/frontend/src/tools/ChangePermissions.tsx b/frontend/src/tools/ChangePermissions.tsx index 9a9647759..928422059 100644 --- a/frontend/src/tools/ChangePermissions.tsx +++ b/frontend/src/tools/ChangePermissions.tsx @@ -64,7 +64,7 @@ const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, }, steps: [ { diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index ace479b8e..d7e0ebc2c 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -62,7 +62,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles && !hasResults, + isCollapsed: hasResults, }, steps: [ { diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx index 0bef140ba..e2616f4a4 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -34,7 +34,6 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const hasFiles = selectedFiles.length > 0; const hasResults = convertOperation.downloadUrl !== null; - const filesCollapsed = hasFiles; const settingsCollapsed = hasResults; useEffect(() => { @@ -97,7 +96,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { return createToolFlow({ files: { selectedFiles, - isCollapsed: filesCollapsed, + isCollapsed: hasResults, placeholder: t("convert.selectFilesPlaceholder", "Select files in the main view to get started"), }, steps: [ diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx index b44b349f7..72fac0b37 100644 --- a/frontend/src/tools/OCR.tsx +++ b/frontend/src/tools/OCR.tsx @@ -80,7 +80,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, }, steps: [ { diff --git a/frontend/src/tools/RemovePassword.tsx b/frontend/src/tools/RemovePassword.tsx index 42f22ba9c..31744186b 100644 --- a/frontend/src/tools/RemovePassword.tsx +++ b/frontend/src/tools/RemovePassword.tsx @@ -62,7 +62,7 @@ const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, }, steps: [ { diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx index a3b63f591..fc30b9b95 100644 --- a/frontend/src/tools/Repair.tsx +++ b/frontend/src/tools/Repair.tsx @@ -57,7 +57,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { return createToolFlow({ files: { selectedFiles, - isCollapsed: hasFiles || hasResults, + isCollapsed: hasResults, placeholder: t("repair.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [], @@ -77,4 +77,4 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }); }; -export default Repair; \ No newline at end of file +export default Repair; diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx index 2874e54e4..258f0f930 100644 --- a/frontend/src/tools/Sanitize.tsx +++ b/frontend/src/tools/Sanitize.tsx @@ -55,13 +55,12 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const hasFiles = selectedFiles.length > 0; const hasResults = sanitizeOperation.files.length > 0; - const filesCollapsed = hasFiles || hasResults; const settingsCollapsed = !hasFiles || hasResults; return createToolFlow({ files: { selectedFiles, - isCollapsed: filesCollapsed, + isCollapsed: hasResults, placeholder: t("sanitize.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [ diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index f0e35dfb0..ea68404f0 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -54,13 +54,12 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const hasFiles = selectedFiles.length > 0; const hasResults = splitOperation.downloadUrl !== null; - const filesCollapsed = hasFiles || hasResults; const settingsCollapsed = !hasFiles || hasResults; return createToolFlow({ files: { selectedFiles, - isCollapsed: filesCollapsed, + isCollapsed: hasResults, placeholder: "Select a PDF file in the main view to get started", }, steps: [ From a6706fcb0c1c8e0b7ebbfc6f37cad7ce981d56b2 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 21 Aug 2025 08:48:25 +0100 Subject: [PATCH 08/11] V2 reduce boilerplate in param hooks (#4246) # Description of Changes Extend the base params in all tools param hooks, reducing boilerplate code. --- .../tools/compress/CompressSettings.tsx | 2 +- .../tools/ocr/AdvancedOCRSettings.tsx | 2 +- .../src/components/tools/ocr/OCRSettings.tsx | 12 +- .../src/constants/addWatermarkConstants.ts | 15 -- .../addPassword/useAddPasswordParameters.ts | 49 ++--- .../addWatermark/useAddWatermarkParameters.ts | 66 +++---- .../useChangePermissionsParameters.ts | 49 +---- .../tools/compress/useCompressOperation.ts | 12 +- .../tools/compress/useCompressParameters.ts | 58 ++---- .../convert/useConvertParameters.test.ts | 85 ++++---- .../tools/convert/useConvertParameters.ts | 184 ++++++++---------- .../src/hooks/tools/ocr/useOCROperation.ts | 2 +- .../src/hooks/tools/ocr/useOCRParameters.ts | 50 ++--- .../useRemovePasswordParameters.ts | 49 ++--- .../tools/sanitize/useSanitizeParameters.ts | 45 ++--- .../hooks/tools/shared/useBaseParameters.ts | 22 ++- .../hooks/tools/split/useSplitParameters.ts | 75 +++---- .../ConvertSmartDetectionIntegration.test.tsx | 9 +- 18 files changed, 308 insertions(+), 478 deletions(-) diff --git a/frontend/src/components/tools/compress/CompressSettings.tsx b/frontend/src/components/tools/compress/CompressSettings.tsx index 47ff4189e..d82a99453 100644 --- a/frontend/src/components/tools/compress/CompressSettings.tsx +++ b/frontend/src/components/tools/compress/CompressSettings.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core"; import { useTranslation } from "react-i18next"; -import { CompressParameters } from "../../../hooks/tools/compress/useCompressOperation"; +import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters"; interface CompressSettingsProps { parameters: CompressParameters; diff --git a/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx b/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx index 3bd8c1569..a38926edf 100644 --- a/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx +++ b/frontend/src/components/tools/ocr/AdvancedOCRSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Stack, Text, Checkbox } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import { OCRParameters } from './OCRSettings'; +import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters'; export interface AdvancedOCRParameters { advancedOptions: string[]; diff --git a/frontend/src/components/tools/ocr/OCRSettings.tsx b/frontend/src/components/tools/ocr/OCRSettings.tsx index 588884889..b4d9aa766 100644 --- a/frontend/src/components/tools/ocr/OCRSettings.tsx +++ b/frontend/src/components/tools/ocr/OCRSettings.tsx @@ -2,13 +2,7 @@ import React from 'react'; import { Stack, Select, Text, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import LanguagePicker from './LanguagePicker'; - -export interface OCRParameters { - languages: string[]; - ocrType: string; - ocrRenderType: string; - additionalOptions: string[]; -} +import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters'; interface OCRSettingsProps { parameters: OCRParameters; @@ -25,7 +19,7 @@ const OCRSettings: React.FC = ({ return ( - +