mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
working with bad ui
This commit is contained in:
parent
85b612bf22
commit
7083ad9207
@ -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",
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
parameters={watermarkParams.parameters}
|
||||
onParameterChange={watermarkParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
<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
|
||||
|
Loading…
x
Reference in New Issue
Block a user