import { Download } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useTranslation } from "@/contexts/i18n-context"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; const PRESETS: { label: string; shadow: string; highlight: string }[] = [ { label: "Classic", shadow: "#1e3a8a", highlight: "#fbbf24" }, { label: "Noir", shadow: "#000000", highlight: "#ffffff" }, { label: "Sepia", shadow: "#2d1b00", highlight: "#ffe8c2" }, { label: "Midnight", shadow: "#0f2027", highlight: "#78ffd6" }, { label: "Sunset", shadow: "#3a1c71", highlight: "#ffaf7b" }, { label: "Cyber", shadow: "#0f0c29", highlight: "#f857a6" }, { label: "Ocean", shadow: "#000428", highlight: "#43cea2" }, { label: "Forest", shadow: "#022c12", highlight: "#9be15d" }, ]; interface DuotoneSettingsProps { onImageStyle?: (style: React.CSSProperties | null) => void; onImageOverlay?: (node: React.ReactNode) => void; } export function DuotoneSettings({ onImageStyle, onImageOverlay }: DuotoneSettingsProps) { const { t } = useTranslation(); const { files } = useFileStore(); const entry = useFileStore((s) => s.entries[s.selectedIndex]); const blobUrl = entry?.blobUrl; const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("duotone"); const [shadow, setShadow] = useState("#1e3a8a"); const [highlight, setHighlight] = useState("#fbbf24"); const [intensity, setIntensity] = useState(100); // Stable refs for preview callbacks to avoid stale closures const onImageStyleRef = useRef(onImageStyle); useEffect(() => { onImageStyleRef.current = onImageStyle; }); const onImageOverlayRef = useRef(onImageOverlay); useEffect(() => { onImageOverlayRef.current = onImageOverlay; }); // Live preview: a self-contained duotone overlay (its own grayscale copy of // the image plus lighten/darken color layers, isolated so the parent pane // filter can't re-gray it). Intensity fades toward the real color image // beneath, matching the backend blend. onImageStyle activates the wrapper // branch in image-viewer so the overlay children actually mount. useEffect(() => { if (!blobUrl) return; onImageStyleRef.current?.({}); onImageOverlayRef.current?.(
, ); return () => { onImageStyleRef.current?.(null); onImageOverlayRef.current?.(null); }; }, [shadow, highlight, intensity, blobUrl]); const handleProcess = () => { const settings = { shadow, highlight, intensity }; if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const hasFile = files.length > 0; const canProcess = hasFile && !processing; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (canProcess) handleProcess(); }; return (
{/* Presets */}

Presets

{PRESETS.map((p) => ( ))}
{/* Shadow Color */}
setShadow(e.target.value)} className="w-8 h-8 rounded border border-border shrink-0" /> setShadow(e.target.value)} className="flex-1 px-2 py-1 rounded border border-border bg-background text-xs text-foreground font-mono" />
{/* Highlight Color */}
setHighlight(e.target.value)} className="w-8 h-8 rounded border border-border shrink-0" /> setHighlight(e.target.value)} className="flex-1 px-2 py-1 rounded border border-border bg-background text-xs text-foreground font-mono" />
{/* Intensity Slider */}
{intensity}%
setIntensity(Number(e.target.value))} className="w-full mt-0.5" />
{error &&

{error}

} {processing ? ( ) : ( )} {downloadUrl && ( {t.common.download} )} ); }