mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Extract a *Controls subcomponent from all 16 pipeline-compatible tool settings components. Each Controls component holds the UI state and settings controls, accepts an onChange callback, and uses useRef to prevent infinite re-render loops. The standalone *Settings components become thin wrappers that add useToolProcessor, useFileStore, and action buttons. pipeline-step-settings.tsx is rewritten from ~675 lines to ~55 lines: the entire TOOL_FIELDS declarative map and generic renderer are deleted and replaced with direct imports of the Controls components. Pipeline steps now render the exact same UI as standalone tool pages. Special cases: - CropControls: numeric inputs for pipeline (standalone uses canvas) - RotateControls: resetSignal prop for post-processing reset - ColorControls: accepts toolId for tab selection - StripMetadataControls: checkboxes only (no file inspection) - RemoveBgControls: already extracted, unchanged
208 lines
6.7 KiB
TypeScript
208 lines
6.7 KiB
TypeScript
import { Download } from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { ProgressCard } from "@/components/common/progress-card";
|
|
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
|
import { useFileStore } from "@/stores/file-store";
|
|
|
|
type Position = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "tiled";
|
|
|
|
export interface WatermarkTextControlsProps {
|
|
onChange?: (settings: Record<string, unknown>) => void;
|
|
}
|
|
|
|
export function WatermarkTextControls({ onChange }: WatermarkTextControlsProps) {
|
|
const [text, setText] = useState("Sample Watermark");
|
|
const [fontSize, setFontSize] = useState(48);
|
|
const [color, setColor] = useState("#000000");
|
|
const [opacity, setOpacity] = useState(50);
|
|
const [position, setPosition] = useState<Position>("center");
|
|
const [rotation, setRotation] = useState(0);
|
|
|
|
const onChangeRef = useRef(onChange);
|
|
useEffect(() => {
|
|
onChangeRef.current = onChange;
|
|
});
|
|
|
|
useEffect(() => {
|
|
onChangeRef.current?.({ text, fontSize, color, opacity, position, rotation });
|
|
}, [text, fontSize, color, opacity, position, rotation]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label htmlFor="watermark-text-text" className="text-xs text-muted-foreground">
|
|
Watermark Text
|
|
</label>
|
|
<input
|
|
id="watermark-text-text"
|
|
type="text"
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between items-center">
|
|
<label htmlFor="watermark-text-font-size" className="text-xs text-muted-foreground">
|
|
Font Size
|
|
</label>
|
|
<span className="text-xs font-mono text-foreground">{fontSize}px</span>
|
|
</div>
|
|
<input
|
|
id="watermark-text-font-size"
|
|
type="range"
|
|
min={8}
|
|
max={200}
|
|
value={fontSize}
|
|
onChange={(e) => setFontSize(Number(e.target.value))}
|
|
className="w-full mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<label htmlFor="watermark-text-color" className="text-xs text-muted-foreground">
|
|
Color
|
|
</label>
|
|
<input
|
|
id="watermark-text-color"
|
|
type="color"
|
|
value={color}
|
|
onChange={(e) => setColor(e.target.value)}
|
|
className="w-full mt-0.5 h-8 rounded border border-border"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex justify-between items-center">
|
|
<label htmlFor="watermark-text-opacity" className="text-xs text-muted-foreground">
|
|
Opacity
|
|
</label>
|
|
<span className="text-xs font-mono text-foreground">{opacity}%</span>
|
|
</div>
|
|
<input
|
|
id="watermark-text-opacity"
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={opacity}
|
|
onChange={(e) => setOpacity(Number(e.target.value))}
|
|
className="w-full mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="watermark-text-position" className="text-xs text-muted-foreground">
|
|
Position
|
|
</label>
|
|
<select
|
|
id="watermark-text-position"
|
|
value={position}
|
|
onChange={(e) => setPosition(e.target.value as Position)}
|
|
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
|
>
|
|
<option value="center">Center</option>
|
|
<option value="top-left">Top Left</option>
|
|
<option value="top-right">Top Right</option>
|
|
<option value="bottom-left">Bottom Left</option>
|
|
<option value="bottom-right">Bottom Right</option>
|
|
<option value="tiled">Tiled (Repeating)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between items-center">
|
|
<label htmlFor="watermark-text-rotation" className="text-xs text-muted-foreground">
|
|
Rotation
|
|
</label>
|
|
<span className="text-xs font-mono text-foreground">{rotation}°</span>
|
|
</div>
|
|
<input
|
|
id="watermark-text-rotation"
|
|
type="range"
|
|
min={-180}
|
|
max={180}
|
|
value={rotation}
|
|
onChange={(e) => setRotation(Number(e.target.value))}
|
|
className="w-full mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function WatermarkTextSettings() {
|
|
const { files } = useFileStore();
|
|
const {
|
|
processFiles,
|
|
processAllFiles,
|
|
processing,
|
|
error,
|
|
downloadUrl,
|
|
originalSize,
|
|
processedSize,
|
|
progress,
|
|
} = useToolProcessor("watermark-text");
|
|
|
|
const [settings, setSettings] = useState<Record<string, unknown>>({});
|
|
|
|
const handleProcess = () => {
|
|
if (files.length > 1) {
|
|
processAllFiles(files, settings);
|
|
} else {
|
|
processFiles(files, settings);
|
|
}
|
|
};
|
|
|
|
const hasFile = files.length > 0;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<WatermarkTextControls onChange={setSettings} />
|
|
|
|
{error && <p className="text-xs text-red-500">{error}</p>}
|
|
|
|
{originalSize != null && processedSize != null && (
|
|
<div className="text-xs text-muted-foreground space-y-0.5">
|
|
<p>Original: {(originalSize / 1024).toFixed(1)} KB</p>
|
|
<p>Processed: {(processedSize / 1024).toFixed(1)} KB</p>
|
|
</div>
|
|
)}
|
|
|
|
{processing ? (
|
|
<ProgressCard
|
|
active={processing}
|
|
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
|
label="Adding watermark"
|
|
stage={progress.stage}
|
|
percent={progress.percent}
|
|
elapsed={progress.elapsed}
|
|
/>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
data-testid="watermark-text-submit"
|
|
onClick={handleProcess}
|
|
disabled={!hasFile || processing || !settings.text}
|
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{files.length > 1 ? `Apply Watermark (${files.length} files)` : "Apply Watermark"}
|
|
</button>
|
|
)}
|
|
|
|
{downloadUrl && (
|
|
<a
|
|
href={downloadUrl}
|
|
download
|
|
data-testid="watermark-text-download"
|
|
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Download
|
|
</a>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|