diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index c3bf98080..4f79f9d4d 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1780,6 +1780,98 @@ "chinese": "Chinese", "thai": "Thai" }, + "steps": { + "type": "Watermark Type", + "textContent": "Text Content", + "imageContent": "Image Content", + "style": "Style & Position", + "advanced": "Advanced Options" + }, + "results": { + "title": "Watermark Results" + }, + "tooltip": { + "type": { + "header": { + "title": "Watermark Type Selection" + }, + "description": { + "title": "Choose Your Watermark", + "text": "Select between text or image watermarks based on your needs." + }, + "text": { + "title": "Text Watermarks", + "text": "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors.", + "bullet1": "Customizable fonts and languages", + "bullet2": "Adjustable colors and transparency", + "bullet3": "Ideal for legal or branding text" + }, + "image": { + "title": "Image Watermarks", + "text": "Use logos, stamps, or any image as a watermark. Great for branding and visual identification.", + "bullet1": "Upload any image format", + "bullet2": "Maintains image quality", + "bullet3": "Perfect for logos and stamps" + } + }, + "content": { + "header": { + "title": "Content Configuration" + }, + "text": { + "title": "Text Settings", + "text": "Configure your text watermark appearance and language support.", + "bullet1": "Enter your watermark text", + "bullet2": "Adjust font size (8-72pt)", + "bullet3": "Select language/script support", + "bullet4": "Choose custom colors" + }, + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering for your text.", + "bullet1": "Roman/Latin for Western languages", + "bullet2": "Arabic for Arabic script", + "bullet3": "Japanese, Korean, Chinese for Asian languages", + "bullet4": "Thai for Thai script" + } + }, + "style": { + "header": { + "title": "Style & Positioning" + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Width spacing: Horizontal distance between watermarks", + "bullet2": "Height spacing: Vertical distance between watermarks", + "bullet3": "Higher values create more spread out patterns" + } + }, + "advanced": { + "header": { + "title": "Advanced Options" + }, + "conversion": { + "title": "PDF to Image Conversion", + "text": "Convert the final PDF to an image-based format for enhanced security.", + "bullet1": "Prevents text selection and copying", + "bullet2": "Makes watermarks harder to remove", + "bullet3": "Results in larger file sizes", + "bullet4": "Best for sensitive or copyrighted content" + }, + "security": { + "title": "Security Considerations", + "text": "Image-based PDFs provide additional protection against unauthorized editing and content extraction." + } + } + }, "positions": { "topLeft": "Top Left", "topCenter": "Top Center", diff --git a/frontend/src/components/tools/addWatermark/WatermarkAdvancedSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkAdvancedSettings.tsx new file mode 100644 index 000000000..a691471a0 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkAdvancedSettings.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Stack, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "./types"; + +interface WatermarkAdvancedSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkAdvancedSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkAdvancedSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Output Options */} + onParameterChange('convertPDFToImage', event.currentTarget.checked)} + disabled={disabled} + /> + + ); +}; + +export default WatermarkAdvancedSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx index 872bc1d23..5bab1ab83 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkContentSettings.tsx @@ -1,20 +1,7 @@ import React, { useRef } from "react"; -import { Stack, Text, TextInput, FileButton, Button, NumberInput } from "@mantine/core"; +import { Stack, Text, TextInput, FileButton, Button, NumberInput, Select, ColorInput } 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; -} +import { AddWatermarkParameters } from "./types"; interface WatermarkContentSettingsProps { parameters: AddWatermarkParameters; @@ -26,6 +13,15 @@ const WatermarkContentSettings = ({ parameters, onParameterChange, disabled = fa const { t } = useTranslation(); const resetRef = useRef<() => void>(null); + 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 Watermark Settings */} @@ -47,6 +43,23 @@ const WatermarkContentSettings = ({ parameters, onParameterChange, disabled = fa max={72} disabled={disabled} /> + + {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)')} @@ -100,16 +56,6 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals /> - {/* Output Options */} - - onParameterChange('convertPDFToImage', event.currentTarget.checked)} - disabled={disabled} - /> - ); }; diff --git a/frontend/src/components/tools/addWatermark/types.ts b/frontend/src/components/tools/addWatermark/types.ts new file mode 100644 index 000000000..610212fea --- /dev/null +++ b/frontend/src/components/tools/addWatermark/types.ts @@ -0,0 +1,13 @@ +export 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; +} \ No newline at end of file diff --git a/frontend/src/components/tooltips/useWatermarkTips.ts b/frontend/src/components/tooltips/useWatermarkTips.ts new file mode 100644 index 000000000..408d5de2f --- /dev/null +++ b/frontend/src/components/tooltips/useWatermarkTips.ts @@ -0,0 +1,124 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useWatermarkTypeTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.type.header.title", "Watermark Type Selection") + }, + tips: [ + { + title: t("watermark.tooltip.type.description.title", "Choose Your Watermark"), + description: t("watermark.tooltip.type.description.text", "Select between text or image watermarks based on your needs.") + }, + { + title: t("watermark.tooltip.type.text.title", "Text Watermarks"), + description: t("watermark.tooltip.type.text.text", "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors."), + bullets: [ + t("watermark.tooltip.type.text.bullet1", "Customizable fonts and languages"), + t("watermark.tooltip.type.text.bullet2", "Adjustable colors and transparency"), + t("watermark.tooltip.type.text.bullet3", "Ideal for legal or branding text") + ] + }, + { + title: t("watermark.tooltip.type.image.title", "Image Watermarks"), + description: t("watermark.tooltip.type.image.text", "Use logos, stamps, or any image as a watermark. Great for branding and visual identification."), + bullets: [ + t("watermark.tooltip.type.image.bullet1", "Upload any image format"), + t("watermark.tooltip.type.image.bullet2", "Maintains image quality"), + t("watermark.tooltip.type.image.bullet3", "Perfect for logos and stamps") + ] + } + ] + }; +}; + +export const useWatermarkContentTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.content.header.title", "Content Configuration") + }, + tips: [ + { + title: t("watermark.tooltip.content.text.title", "Text Settings"), + description: t("watermark.tooltip.content.text.text", "Configure your text watermark appearance and language support."), + bullets: [ + t("watermark.tooltip.content.text.bullet1", "Enter your watermark text"), + t("watermark.tooltip.content.text.bullet2", "Adjust font size (8-72pt)"), + t("watermark.tooltip.content.text.bullet3", "Select language/script support"), + t("watermark.tooltip.content.text.bullet4", "Choose custom colors") + ] + }, + { + title: t("watermark.tooltip.content.language.title", "Language Support"), + description: t("watermark.tooltip.content.language.text", "Choose the appropriate language setting to ensure proper font rendering for your text."), + bullets: [ + t("watermark.tooltip.content.language.bullet1", "Roman/Latin for Western languages"), + t("watermark.tooltip.content.language.bullet2", "Arabic for Arabic script"), + t("watermark.tooltip.content.language.bullet3", "Japanese, Korean, Chinese for Asian languages"), + t("watermark.tooltip.content.language.bullet4", "Thai for Thai script") + ] + } + ] + }; +}; + +export const useWatermarkStyleTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.style.header.title", "Style & Positioning") + }, + tips: [ + { + title: t("watermark.tooltip.style.appearance.title", "Appearance Settings"), + description: t("watermark.tooltip.style.appearance.text", "Control how your watermark looks and blends with the document."), + bullets: [ + t("watermark.tooltip.style.appearance.bullet1", "Rotation: -360° to 360° for angled watermarks"), + t("watermark.tooltip.style.appearance.bullet2", "Opacity: 0-100% for transparency control"), + t("watermark.tooltip.style.appearance.bullet3", "Lower opacity creates subtle watermarks") + ] + }, + { + title: t("watermark.tooltip.style.spacing.title", "Spacing Control"), + description: t("watermark.tooltip.style.spacing.text", "Adjust the spacing between repeated watermarks across the page."), + bullets: [ + t("watermark.tooltip.style.spacing.bullet1", "Width spacing: Horizontal distance between watermarks"), + t("watermark.tooltip.style.spacing.bullet2", "Height spacing: Vertical distance between watermarks"), + t("watermark.tooltip.style.spacing.bullet3", "Higher values create more spread out patterns") + ] + } + ] + }; +}; + +export const useWatermarkAdvancedTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.advanced.header.title", "Advanced Options") + }, + tips: [ + { + title: t("watermark.tooltip.advanced.conversion.title", "PDF to Image Conversion"), + description: t("watermark.tooltip.advanced.conversion.text", "Convert the final PDF to an image-based format for enhanced security."), + bullets: [ + t("watermark.tooltip.advanced.conversion.bullet1", "Prevents text selection and copying"), + t("watermark.tooltip.advanced.conversion.bullet2", "Makes watermarks harder to remove"), + t("watermark.tooltip.advanced.conversion.bullet3", "Results in larger file sizes"), + t("watermark.tooltip.advanced.conversion.bullet4", "Best for sensitive or copyrighted content") + ] + }, + { + title: t("watermark.tooltip.advanced.security.title", "Security Considerations"), + description: t("watermark.tooltip.advanced.security.text", "Image-based PDFs provide additional protection against unauthorized editing and content extraction.") + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx index 4ef37625d..1e30f948e 100644 --- a/frontend/src/tools/AddWatermark.tsx +++ b/frontend/src/tools/AddWatermark.tsx @@ -1,23 +1,19 @@ -import React, { useEffect, useMemo, useState } from "react"; -import { Button, Stack, Text } from "@mantine/core"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import DownloadIcon from "@mui/icons-material/Download"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useFileContext } from "../contexts/FileContext"; import { useToolFileSelection } from "../contexts/FileSelectionContext"; -import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep"; -import OperationButton from "../components/tools/shared/OperationButton"; -import ErrorNotification from "../components/tools/shared/ErrorNotification"; -import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; -import ResultsPreview from "../components/tools/shared/ResultsPreview"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; import WatermarkTypeSettings from "../components/tools/addWatermark/WatermarkTypeSettings"; import WatermarkContentSettings from "../components/tools/addWatermark/WatermarkContentSettings"; import WatermarkStyleSettings from "../components/tools/addWatermark/WatermarkStyleSettings"; +import WatermarkAdvancedSettings from "../components/tools/addWatermark/WatermarkAdvancedSettings"; import { useAddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters"; import { useAddWatermarkOperation } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; +import { useWatermarkTypeTips, useWatermarkContentTips, useWatermarkStyleTips, useWatermarkAdvancedTips } from "../components/tooltips/useWatermarkTips"; import { BaseToolProps } from "../types/tool"; const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -25,8 +21,16 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => const { setCurrentMode } = useFileContext(); const { selectedFiles } = useToolFileSelection(); + const [collapsedType, setCollapsedType] = useState(false); + const [collapsedStyle, setCollapsedStyle] = useState(true); + const [collapsedAdvanced, setCollapsedAdvanced] = useState(true); + const watermarkParams = useAddWatermarkParameters(); const watermarkOperation = useAddWatermarkOperation(); + const watermarkTypeTips = useWatermarkTypeTips(); + const watermarkContentTips = useWatermarkContentTips(); + const watermarkStyleTips = useWatermarkStyleTips(); + const watermarkAdvancedTips = useWatermarkAdvancedTips(); // Endpoint validation const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-watermark"); @@ -34,234 +38,129 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => useEffect(() => { watermarkOperation.resetResults(); onPreviewFile?.(null); - }, [watermarkParams.parameters, selectedFiles]); + }, [watermarkParams.parameters]); + + // Auto-collapse type step after selection + useEffect(() => { + if (watermarkParams.parameters.watermarkType && !collapsedType) { + setCollapsedType(true); + } + }, [watermarkParams.parameters.watermarkType]); const handleAddWatermark = async () => { try { - await watermarkOperation.executeOperation( - watermarkParams.parameters, - selectedFiles - ); + await watermarkOperation.executeOperation(watermarkParams.parameters, selectedFiles); if (watermarkOperation.files && onComplete) { onComplete(watermarkOperation.files); } } catch (error) { if (onError) { - onError(error instanceof Error ? error.message : 'Add watermark operation failed'); + onError(error instanceof Error ? error.message : t("watermark.error.failed", "Add watermark operation failed")); } } }; const handleThumbnailClick = (file: File) => { onPreviewFile?.(file); - sessionStorage.setItem('previousMode', 'watermark'); - setCurrentMode('viewer'); + sessionStorage.setItem("previousMode", "watermark"); + setCurrentMode("viewer"); }; const handleSettingsReset = () => { watermarkOperation.resetResults(); onPreviewFile?.(null); - setCurrentMode('watermark'); + setCurrentMode("watermark"); }; const hasFiles = selectedFiles.length > 0; const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null; - const filesCollapsed = hasFiles; - - // Step completion logic + const settingsCollapsed = !hasFiles || 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) => ({ - file, - thumbnail: watermarkOperation.thumbnails[index] - })) || [], - [watermarkOperation.files, watermarkOperation.thumbnails] - ); + // Step visibility logic - all steps always visible once files are selected + const styleCollapsed = collapsedStyle || hasResults; + const advancedCollapsed = collapsedAdvanced || hasResults; - return ( - - - {/* Files Step */} - - - - - {/* Watermark Type Step */} - + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + }, + steps: [ + { + title: t("watermark.steps.type", "Watermark Type"), + isCollapsed: settingsCollapsed? true : collapsedType, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedType(!collapsedType), + tooltip: watermarkTypeTips, + content: ( watermarkParams.updateParameter('watermarkType', type)} disabled={endpointLoading} /> - - - {/* Content Step */} - + ), + }, + { + title: watermarkParams.parameters.watermarkType === 'text' + ? t("watermark.steps.textContent", "Text Content") + : t("watermark.steps.imageContent", "Image Content"), + isCollapsed: settingsCollapsed? true : contentStepCompleted, + tooltip: watermarkContentTips, + content: ( - - - {/* Style Step */} - + ), + }, + { + title: t("watermark.steps.style", "Style & Position"), + isCollapsed: settingsCollapsed? true : styleCollapsed, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedStyle(!collapsedStyle), + tooltip: watermarkStyleTips, + content: ( - - - {/* Apply Button - Outside of settings steps */} - {styleStepCompleted && !hasResults && ( - - - - )} - - {/* Results Step */} - - - {watermarkOperation.status && ( - {watermarkOperation.status} - )} - - - - {watermarkOperation.downloadUrl && ( - - )} - - - - - - - ); + ), + }, + { + title: t("watermark.steps.advanced", "Advanced Options"), + isCollapsed: settingsCollapsed? true : advancedCollapsed, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedAdvanced(!collapsedAdvanced), + tooltip: watermarkAdvancedTips, + content: ( + + ), + }, + ], + executeButton: { + text: t("watermark.submit", "Add Watermark"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleAddWatermark, + disabled: !watermarkParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: watermarkOperation, + title: t("watermark.results.title", "Watermark Results"), + onFileClick: handleThumbnailClick, + }, + }); } -export default AddWatermark; \ No newline at end of file +export default AddWatermark;