diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 00cfe7265..c3bf98080 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1761,13 +1761,24 @@ "selected": "Selected: {{filename}}" }, "fontSize": "Font Size", - "position": "Position", + "alphabet": "Font/Language", + "color": "Watermark Color", "rotation": "Rotation (degrees)", "opacity": "Opacity (%)", "spacing": { "width": "Width Spacing", "height": "Height Spacing" - } + }, + "convertToImage": "Convert result to image-based PDF", + "convertToImageDesc": "Creates a PDF with images instead of text (more secure but larger file size)" + }, + "alphabet": { + "roman": "Roman/Latin", + "arabic": "Arabic", + "japanese": "Japanese", + "korean": "Korean", + "chinese": "Chinese", + "thai": "Thai" }, "positions": { "topLeft": "Top Left", diff --git a/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx new file mode 100644 index 000000000..872bc1d23 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx @@ -0,0 +1,80 @@ +import React, { useRef } from "react"; +import { Stack, Text, TextInput, FileButton, Button, NumberInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +interface AddWatermarkParameters { + watermarkType?: 'text' | 'image'; + watermarkText: string; + watermarkImage?: File; + fontSize: number; + rotation: number; + opacity: number; + widthSpacer: number; + heightSpacer: number; + position: string; + overrideX?: number; + overrideY?: number; +} + +interface WatermarkContentSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkContentSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkContentSettingsProps) => { + const { t } = useTranslation(); + const resetRef = useRef<() => void>(null); + + return ( + + {/* Text Watermark Settings */} + {parameters.watermarkType === 'text' && ( + + {t('watermark.settings.text.label', 'Watermark Text')} + onParameterChange('watermarkText', e.target.value)} + disabled={disabled} + /> + + {t('watermark.settings.fontSize', 'Font Size')} + onParameterChange('fontSize', value || 12)} + min={8} + max={72} + disabled={disabled} + /> + + )} + + {/* Image Watermark Settings */} + {parameters.watermarkType === 'image' && ( + + {t('watermark.settings.image.label', 'Watermark Image')} + onParameterChange('watermarkImage', file)} + accept="image/*" + disabled={disabled} + > + {(props) => ( + + {parameters.watermarkImage ? parameters.watermarkImage.name : t('watermark.settings.image.choose', 'Choose Image')} + + )} + + {parameters.watermarkImage && ( + + {t('watermark.settings.image.selected', 'Selected: {{filename}}', { filename: parameters.watermarkImage.name })} + + )} + + )} + + ); +}; + +export default WatermarkContentSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx new file mode 100644 index 000000000..95aa7e4a3 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { Stack, Text, NumberInput, Select, ColorInput, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +interface AddWatermarkParameters { + watermarkType?: 'text' | 'image'; + watermarkText: string; + watermarkImage?: File; + fontSize: number; + rotation: number; + opacity: number; + widthSpacer: number; + heightSpacer: number; + alphabet: string; + customColor: string; + convertPDFToImage: boolean; +} + +interface WatermarkStyleSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkStyleSettingsProps) => { + const { t } = useTranslation(); + + const alphabetOptions = [ + { value: 'roman', label: t('watermark.alphabet.roman', 'Roman/Latin') }, + { value: 'arabic', label: t('watermark.alphabet.arabic', 'Arabic') }, + { value: 'japanese', label: t('watermark.alphabet.japanese', 'Japanese') }, + { value: 'korean', label: t('watermark.alphabet.korean', 'Korean') }, + { value: 'chinese', label: t('watermark.alphabet.chinese', 'Chinese') }, + { value: 'thai', label: t('watermark.alphabet.thai', 'Thai') } + ]; + + return ( + + {/* Text-specific settings */} + {parameters.watermarkType === 'text' && ( + + {t('watermark.settings.alphabet', 'Font/Language')} + value && onParameterChange('alphabet', value)} + data={alphabetOptions} + disabled={disabled} + /> + + {t('watermark.settings.color', 'Watermark Color')} + onParameterChange('customColor', value)} + disabled={disabled} + format="hex" + swatches={['#d3d3d3', '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff']} + /> + + )} + + {/* Appearance Settings */} + + {t('watermark.settings.rotation', 'Rotation (degrees)')} + onParameterChange('rotation', value || 0)} + min={-360} + max={360} + disabled={disabled} + /> + + {t('watermark.settings.opacity', 'Opacity (%)')} + onParameterChange('opacity', value || 50)} + min={0} + max={100} + disabled={disabled} + /> + + + {/* Spacing Settings */} + + {t('watermark.settings.spacing.width', 'Width Spacing')} + onParameterChange('widthSpacer', value || 50)} + min={0} + max={200} + disabled={disabled} + /> + + {t('watermark.settings.spacing.height', 'Height Spacing')} + onParameterChange('heightSpacer', value || 50)} + min={0} + max={200} + disabled={disabled} + /> + + + {/* Output Options */} + + onParameterChange('convertPDFToImage', event.currentTarget.checked)} + disabled={disabled} + /> + + + ); +}; + +export default WatermarkStyleSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx new file mode 100644 index 000000000..438d781a6 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Button, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +interface WatermarkTypeSettingsProps { + watermarkType?: 'text' | 'image'; + onWatermarkTypeChange: (type: 'text' | 'image') => void; + disabled?: boolean; +} + +const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t('watermark.settings.type', 'Watermark Type')} + + onWatermarkTypeChange('text')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('watermark.watermarkType.text', 'Text')} + + + onWatermarkTypeChange('image')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('watermark.watermarkType.image', 'Image')} + + + + + ); +}; + +export default WatermarkTypeSettings; \ No newline at end of file diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts index 9251d91d5..a5f28bc06 100644 --- a/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts +++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts @@ -7,25 +7,27 @@ const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData const formData = new FormData(); formData.append("fileInput", file); + // Required: watermarkType as string + formData.append("watermarkType", parameters.watermarkType || "text"); + + // Add watermark content based on type if (parameters.watermarkType === 'text') { formData.append("watermarkText", parameters.watermarkText); - } else if (parameters.watermarkImage) { + } else if (parameters.watermarkType === 'image' && parameters.watermarkImage) { formData.append("watermarkImage", parameters.watermarkImage); } + // Required parameters with correct formatting formData.append("fontSize", parameters.fontSize.toString()); formData.append("rotation", parameters.rotation.toString()); formData.append("opacity", (parameters.opacity / 100).toString()); // Convert percentage to decimal formData.append("widthSpacer", parameters.widthSpacer.toString()); formData.append("heightSpacer", parameters.heightSpacer.toString()); - formData.append("position", parameters.position); - if (parameters.overrideX !== undefined) { - formData.append("overrideX", parameters.overrideX.toString()); - } - if (parameters.overrideY !== undefined) { - formData.append("overrideY", parameters.overrideY.toString()); - } + // Backend-expected parameters from user input + formData.append("alphabet", parameters.alphabet); + formData.append("customColor", parameters.customColor); + formData.append("convertPDFToImage", parameters.convertPDFToImage.toString()); return formData; }; diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts index 4f3405221..68f31ff8a 100644 --- a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts +++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react'; export interface AddWatermarkParameters { - watermarkType: 'text' | 'image'; + watermarkType?: 'text' | 'image'; watermarkText: string; watermarkImage?: File; fontSize: number; @@ -9,20 +9,22 @@ export interface AddWatermarkParameters { opacity: number; widthSpacer: number; heightSpacer: number; - position: string; - overrideX?: number; - overrideY?: number; + alphabet: string; + customColor: string; + convertPDFToImage: boolean; } const defaultParameters: AddWatermarkParameters = { - watermarkType: 'text', + watermarkType: undefined, watermarkText: '', fontSize: 12, rotation: 0, opacity: 50, widthSpacer: 50, heightSpacer: 50, - position: 'center' + alphabet: 'roman', + customColor: '#d3d3d3', + convertPDFToImage: false }; export const useAddWatermarkParameters = () => { @@ -40,6 +42,9 @@ export const useAddWatermarkParameters = () => { }, []); const validateParameters = useCallback((): boolean => { + if (!parameters.watermarkType) { + return false; + } if (parameters.watermarkType === 'text') { return parameters.watermarkText.trim().length > 0; } else { diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx index dc549745d..4ef37625d 100644 --- a/frontend/src/tools/AddWatermark.tsx +++ b/frontend/src/tools/AddWatermark.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Button, Stack, Text } from "@mantine/core"; import { useTranslation } from "react-i18next"; import DownloadIcon from "@mui/icons-material/Download"; @@ -12,7 +12,9 @@ import ErrorNotification from "../components/tools/shared/ErrorNotification"; import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; import ResultsPreview from "../components/tools/shared/ResultsPreview"; -import AddWatermarkSettings from "../components/tools/addWatermark/AddWatermarkSettings"; +import WatermarkTypeSettings from "../components/tools/addWatermark/WatermarkTypeSettings"; +import WatermarkContentSettings from "../components/tools/addWatermark/WatermarkContentSettings"; +import WatermarkStyleSettings from "../components/tools/addWatermark/WatermarkStyleSettings"; import { useAddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters"; import { useAddWatermarkOperation } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; @@ -65,7 +67,65 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => const hasFiles = selectedFiles.length > 0; const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null; const filesCollapsed = hasFiles; - const settingsCollapsed = hasResults; + + // Step completion logic + const typeStepCompleted = hasFiles && !!watermarkParams.parameters.watermarkType; + const contentStepCompleted = typeStepCompleted && ( + (watermarkParams.parameters.watermarkType === 'text' && watermarkParams.parameters.watermarkText.trim().length > 0) || + (watermarkParams.parameters.watermarkType === 'image' && watermarkParams.parameters.watermarkImage !== undefined) + ); + const styleStepCompleted = contentStepCompleted; // Style step has defaults, so completed when content is done + + // Track which steps have been manually opened + const [manuallyOpenedSteps, setManuallyOpenedSteps] = useState>(new Set()); + + // Auto-collapse logic with manual override + const typeStepCollapsed = typeStepCompleted && !hasResults && !manuallyOpenedSteps.has('type'); + const contentStepCollapsed = contentStepCompleted && !hasResults && !manuallyOpenedSteps.has('content'); + const styleStepCollapsed = !manuallyOpenedSteps.has('style'); // Style starts collapsed, only opens when clicked + + // Click handlers to manage step visibility and reset results + const handleTypeStepClick = () => { + setManuallyOpenedSteps(prev => { + const newSet = new Set(prev); + if (newSet.has('type')) { + newSet.delete('type'); // Close if already open + } else { + newSet.add('type'); // Open if closed + } + return newSet; + }); + watermarkOperation.resetResults(); + onPreviewFile?.(null); + }; + + const handleContentStepClick = () => { + setManuallyOpenedSteps(prev => { + const newSet = new Set(prev); + if (newSet.has('content')) { + newSet.delete('content'); // Close if already open + } else { + newSet.add('content'); // Open if closed + } + return newSet; + }); + watermarkOperation.resetResults(); + onPreviewFile?.(null); + }; + + const handleStyleStepClick = () => { + setManuallyOpenedSteps(prev => { + const newSet = new Set(prev); + if (newSet.has('style')) { + newSet.delete('style'); // Close if already open + } else { + newSet.add('style'); // Open if closed + } + return newSet; + }); + watermarkOperation.resetResults(); + onPreviewFile?.(null); + }; const previewResults = useMemo(() => watermarkOperation.files?.map((file, index) => ({ @@ -96,22 +156,62 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => /> - {/* Settings Step */} + {/* Watermark Type Step */} - - + watermarkParams.updateParameter('watermarkType', type)} + disabled={endpointLoading} + /> + + {/* Content Step */} + + + + + {/* Style Step */} + + + + + {/* Apply Button - Outside of settings steps */} + {styleStepCompleted && !hasResults && ( + submitText="Add Watermark and Review" /> - + )} {/* Results Step */}