working with bad ui

This commit is contained in:
Connor Yoh 2025-08-15 14:44:46 +01:00
parent 85b612bf22
commit 7083ad9207
7 changed files with 392 additions and 32 deletions

View File

@ -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",

View File

@ -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 (
<Stack gap="md">
{/* Text Watermark Settings */}
{parameters.watermarkType === 'text' && (
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.text.label', 'Watermark Text')}</Text>
<TextInput
placeholder={t('watermark.settings.text.placeholder', 'Enter watermark text')}
value={parameters.watermarkText}
onChange={(e) => onParameterChange('watermarkText', e.target.value)}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.fontSize', 'Font Size')}</Text>
<NumberInput
value={parameters.fontSize}
onChange={(value) => onParameterChange('fontSize', value || 12)}
min={8}
max={72}
disabled={disabled}
/>
</Stack>
)}
{/* Image Watermark Settings */}
{parameters.watermarkType === 'image' && (
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.image.label', 'Watermark Image')}</Text>
<FileButton
resetRef={resetRef}
onChange={(file) => onParameterChange('watermarkImage', file)}
accept="image/*"
disabled={disabled}
>
{(props) => (
<Button {...props} variant="outline" fullWidth>
{parameters.watermarkImage ? parameters.watermarkImage.name : t('watermark.settings.image.choose', 'Choose Image')}
</Button>
)}
</FileButton>
{parameters.watermarkImage && (
<Text size="xs" c="dimmed">
{t('watermark.settings.image.selected', 'Selected: {{filename}}', { filename: parameters.watermarkImage.name })}
</Text>
)}
</Stack>
)}
</Stack>
);
};
export default WatermarkContentSettings;

View File

@ -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 (
<Stack gap="md">
{/* Text-specific settings */}
{parameters.watermarkType === 'text' && (
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.alphabet', 'Font/Language')}</Text>
<Select
value={parameters.alphabet}
onChange={(value) => value && onParameterChange('alphabet', value)}
data={alphabetOptions}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.color', 'Watermark Color')}</Text>
<ColorInput
value={parameters.customColor}
onChange={(value) => onParameterChange('customColor', value)}
disabled={disabled}
format="hex"
swatches={['#d3d3d3', '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff']}
/>
</Stack>
)}
{/* Appearance Settings */}
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
<NumberInput
value={parameters.rotation}
onChange={(value) => onParameterChange('rotation', value || 0)}
min={-360}
max={360}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
<NumberInput
value={parameters.opacity}
onChange={(value) => onParameterChange('opacity', value || 50)}
min={0}
max={100}
disabled={disabled}
/>
</Stack>
{/* Spacing Settings */}
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
<NumberInput
value={parameters.widthSpacer}
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
min={0}
max={200}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
<NumberInput
value={parameters.heightSpacer}
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
min={0}
max={200}
disabled={disabled}
/>
</Stack>
{/* Output Options */}
<Stack gap="sm">
<Checkbox
label={t('watermark.settings.convertToImage', 'Convert result to image-based PDF')}
description={t('watermark.settings.convertToImageDesc', 'Creates a PDF with images instead of text (more secure but larger file size)')}
checked={parameters.convertPDFToImage}
onChange={(event) => onParameterChange('convertPDFToImage', event.currentTarget.checked)}
disabled={disabled}
/>
</Stack>
</Stack>
);
};
export default WatermarkStyleSettings;

View File

@ -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 (
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.type', 'Watermark Type')}</Text>
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={watermarkType === 'text' ? 'filled' : 'outline'}
color={watermarkType === 'text' ? 'blue' : 'gray'}
onClick={() => onWatermarkTypeChange('text')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.text', 'Text')}
</div>
</Button>
<Button
variant={watermarkType === 'image' ? 'filled' : 'outline'}
color={watermarkType === 'image' ? 'blue' : 'gray'}
onClick={() => onWatermarkTypeChange('image')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.image', 'Image')}
</div>
</Button>
</div>
</Stack>
);
};
export default WatermarkTypeSettings;

View File

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

View File

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

View File

@ -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<Set<string>>(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) =>
/>
</ToolStep>
{/* Settings Step */}
{/* Watermark Type Step */}
<ToolStep
title="Settings"
title="Watermark Type"
isVisible={hasFiles}
isCollapsed={settingsCollapsed}
isCompleted={settingsCollapsed}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
completedMessage={settingsCollapsed ? "Watermark added" : undefined}
isCollapsed={typeStepCollapsed}
isCompleted={typeStepCompleted}
onCollapsedClick={handleTypeStepClick}
completedMessage={typeStepCompleted ?
`Type: ${watermarkParams.parameters.watermarkType === 'text' ? 'Text' : 'Image'}` : undefined}
>
<Stack gap="sm">
<AddWatermarkSettings
<WatermarkTypeSettings
watermarkType={watermarkParams.parameters.watermarkType}
onWatermarkTypeChange={(type) => watermarkParams.updateParameter('watermarkType', type)}
disabled={endpointLoading}
/>
</ToolStep>
{/* Content Step */}
<ToolStep
title={watermarkParams.parameters.watermarkType === 'text' ? "Text Content" : "Image Content"}
isVisible={typeStepCompleted}
isCollapsed={contentStepCollapsed}
isCompleted={contentStepCompleted}
onCollapsedClick={handleContentStepClick}
completedMessage={contentStepCompleted ?
(watermarkParams.parameters.watermarkType === 'text'
? `Text: "${watermarkParams.parameters.watermarkText}"`
: `Image: ${watermarkParams.parameters.watermarkImage?.name}`) : undefined}
>
<WatermarkContentSettings
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
</ToolStep>
{/* Style Step */}
<ToolStep
title="Style & Position (Optional)"
isVisible={contentStepCompleted}
isCollapsed={styleStepCollapsed}
isCompleted={styleStepCompleted}
onCollapsedClick={handleStyleStepClick}
completedMessage={styleStepCompleted ?
`Opacity: ${watermarkParams.parameters.opacity}%, Rotation: ${watermarkParams.parameters.rotation}°` : undefined}
>
<WatermarkStyleSettings
parameters={watermarkParams.parameters}
onParameterChange={watermarkParams.updateParameter}
disabled={endpointLoading}
/>
</ToolStep>
{/* Apply Button - Outside of settings steps */}
{styleStepCompleted && !hasResults && (
<Stack gap="sm" p="md">
<OperationButton
onClick={handleAddWatermark}
isLoading={watermarkOperation.isLoading}
@ -120,7 +220,7 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =>
submitText="Add Watermark and Review"
/>
</Stack>
</ToolStep>
)}
{/* Results Step */}
<ToolStep