refactor: rename Tool.alpha to Tool.experimental

This commit is contained in:
Siddharth Kumar Sah
2026-03-26 01:10:51 +08:00
parent ab370a74fe
commit 585d66f0c9
178 changed files with 5637 additions and 4082 deletions
@@ -1,6 +1,6 @@
import { Check, Copy, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Loader2, Copy, Check } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -83,9 +83,13 @@ export function BarcodeReadSettings() {
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80"
>
{copied ? (
<><Check className="h-3 w-3" /> Copied</>
<>
<Check className="h-3 w-3" /> Copied
</>
) : (
<><Copy className="h-3 w-3" /> Copy to clipboard</>
<>
<Copy className="h-3 w-3" /> Copy to clipboard
</>
)}
</button>
</>
@@ -1,8 +1,8 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { ProgressCard } from "@/components/common/progress-card";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function BlurFacesSettings() {
const { files } = useFileStore();
@@ -1,13 +1,21 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function BorderSettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("border");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("border");
const [borderWidth, setBorderWidth] = useState(10);
const [borderColor, setBorderColor] = useState("#000000");
@@ -33,12 +41,24 @@ export function BorderSettings() {
<label className="text-xs text-muted-foreground">Border Width</label>
<span className="text-xs font-mono text-foreground">{borderWidth}px</span>
</div>
<input type="range" min={0} max={100} value={borderWidth} onChange={(e) => setBorderWidth(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={borderWidth}
onChange={(e) => setBorderWidth(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Border Color</label>
<input type="color" value={borderColor} onChange={(e) => setBorderColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={borderColor}
onChange={(e) => setBorderColor(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
<div>
@@ -46,7 +66,14 @@ export function BorderSettings() {
<label className="text-xs text-muted-foreground">Corner Radius</label>
<span className="text-xs font-mono text-foreground">{cornerRadius}px</span>
</div>
<input type="range" min={0} max={200} value={cornerRadius} onChange={(e) => setCornerRadius(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={200}
value={cornerRadius}
onChange={(e) => setCornerRadius(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -54,7 +81,14 @@ export function BorderSettings() {
<label className="text-xs text-muted-foreground">Padding</label>
<span className="text-xs font-mono text-foreground">{padding}px</span>
</div>
<input type="range" min={0} max={100} value={padding} onChange={(e) => setPadding(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={padding}
onChange={(e) => setPadding(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -62,7 +96,14 @@ export function BorderSettings() {
<label className="text-xs text-muted-foreground">Shadow</label>
<span className="text-xs font-mono text-foreground">{shadowBlur}px</span>
</div>
<input type="range" min={0} max={50} value={shadowBlur} onChange={(e) => setShadowBlur(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={50}
value={shadowBlur}
onChange={(e) => setShadowBlur(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -94,7 +135,11 @@ export function BorderSettings() {
)}
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,6 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -60,10 +60,12 @@ export function BulkRenameSettings() {
const ext = f.name.includes(".") ? f.name.slice(f.name.lastIndexOf(".")) : "";
const idx = startIndex + i;
const padded = String(idx).padStart(String(files.length + startIndex).length, "0");
return pattern
.replace(/\{\{index\}\}/g, String(idx))
.replace(/\{\{padded\}\}/g, padded)
.replace(/\{\{original\}\}/g, f.name.replace(ext, "")) + ext;
return (
pattern
.replace(/\{\{index\}\}/g, String(idx))
.replace(/\{\{padded\}\}/g, padded)
.replace(/\{\{original\}\}/g, f.name.replace(ext, "")) + ext
);
})
: [];
@@ -84,8 +86,13 @@ export function BulkRenameSettings() {
<div>
<label className="text-xs text-muted-foreground">Start Index</label>
<input type="number" value={startIndex} onChange={(e) => setStartIndex(Number(e.target.value))} min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={startIndex}
onChange={(e) => setStartIndex(Number(e.target.value))}
min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
{previewNames.length > 0 && (
@@ -93,7 +100,10 @@ export function BulkRenameSettings() {
<label className="text-xs text-muted-foreground">Preview</label>
<div className="mt-1 space-y-0.5">
{previewNames.map((name, i) => (
<div key={i} className="text-xs font-mono text-foreground bg-muted px-2 py-0.5 rounded truncate">
<div
key={i}
className="text-xs font-mono text-foreground bg-muted px-2 py-0.5 rounded truncate"
>
{name}
</div>
))}
@@ -1,6 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -18,7 +18,8 @@ const LAYOUTS: { value: Layout; label: string }[] = [
];
export function CollageSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
const [layout, setLayout] = useState<Layout>("2x2");
const [gap, setGap] = useState(4);
const [backgroundColor, setBackgroundColor] = useState("#FFFFFF");
@@ -89,12 +90,24 @@ export function CollageSettings() {
<label className="text-xs text-muted-foreground">Gap</label>
<span className="text-xs font-mono text-foreground">{gap}px</span>
</div>
<input type="range" min={0} max={50} value={gap} onChange={(e) => setGap(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={50}
value={gap}
onChange={(e) => setGap(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Background Color</label>
<input type="color" value={backgroundColor} onChange={(e) => setBackgroundColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -116,7 +129,11 @@ export function CollageSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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 Collage
</a>
@@ -1,6 +1,6 @@
import { Check, Copy, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Loader2, Copy, Check } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -83,9 +83,7 @@ export function ColorPaletteSettings() {
className="w-6 h-6 rounded border border-border shrink-0"
style={{ backgroundColor: color }}
/>
<span className="text-xs font-mono text-foreground flex-1 text-left">
{color}
</span>
<span className="text-xs font-mono text-foreground flex-1 text-left">{color}</span>
{copiedIdx === i ? (
<Check className="h-3 w-3 text-green-500 shrink-0" />
) : (
@@ -1,8 +1,8 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
type Tab = "basic" | "channels" | "effects";
type Effect = "none" | "grayscale" | "sepia" | "invert";
@@ -14,8 +14,16 @@ interface ColorSettingsProps {
export function ColorSettings({ toolId }: ColorSettingsProps) {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor(toolId);
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor(toolId);
const [tab, setTab] = useState<Tab>(() => {
if (toolId === "color-channels") return "channels";
@@ -84,9 +92,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
key={t.id}
onClick={() => setTab(t.id)}
className={`flex-1 text-xs py-1.5 rounded ${
tab === t.id
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
tab === t.id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
}`}
>
{t.label}
@@ -1,6 +1,6 @@
import { useState, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -78,10 +78,7 @@ export function CompareSettings() {
Similarity: {similarity.toFixed(1)}%
</p>
<div className="mt-1 h-2 bg-background rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${similarity}%` }}
/>
<div className="h-full rounded-full bg-primary" style={{ width: `${similarity}%` }} />
</div>
</div>
)}
@@ -96,7 +93,11 @@ export function CompareSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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 Diff Image
</a>
@@ -1,13 +1,14 @@
import { useState, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function ComposeSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
const [overlayFile, setOverlayFile] = useState<File | null>(null);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
@@ -81,13 +82,23 @@ export function ComposeSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">X Position</label>
<input type="number" value={x} onChange={(e) => setX(Number(e.target.value))} min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={x}
onChange={(e) => setX(Number(e.target.value))}
min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Y Position</label>
<input type="number" value={y} onChange={(e) => setY(Number(e.target.value))} min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={y}
onChange={(e) => setY(Number(e.target.value))}
min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
</div>
@@ -96,7 +107,14 @@ export function ComposeSettings() {
<label className="text-xs text-muted-foreground">Opacity</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input type="range" min={0} max={100} value={opacity} onChange={(e) => setOpacity(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -138,7 +156,11 @@ export function ComposeSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,15 +1,23 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
type CompressMode = "quality" | "targetSize";
export function CompressSettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("compress");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("compress");
const [mode, setMode] = useState<CompressMode>("quality");
const [quality, setQuality] = useState(75);
@@ -30,8 +38,7 @@ export function CompressSettings() {
};
const hasFile = files.length > 0;
const canProcess =
mode === "quality" || (mode === "targetSize" && Number(targetSizeKb) > 0);
const canProcess = mode === "quality" || (mode === "targetSize" && Number(targetSizeKb) > 0);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -103,11 +110,7 @@ export function CompressSettings() {
<p>Original: {(originalSize / 1024).toFixed(1)} KB</p>
<p>Processed: {(processedSize / 1024).toFixed(1)} KB</p>
<p className="font-medium text-foreground">
Saved:{" "}
{originalSize > 0
? ((1 - processedSize / originalSize) * 100).toFixed(1)
: "0"}
%
Saved: {originalSize > 0 ? ((1 - processedSize / originalSize) * 100).toFixed(1) : "0"}%
</p>
</div>
)}
@@ -1,16 +1,24 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
const OUTPUT_FORMATS = ["jpg", "png", "webp", "avif", "tiff", "gif"] as const;
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif"]);
export function ConvertSettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("convert");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("convert");
const [format, setFormat] = useState<string>("png");
const [quality, setQuality] = useState(85);
@@ -98,10 +106,7 @@ export function ConvertSettings() {
<p>Processed: {(processedSize / 1024).toFixed(1)} KB</p>
<p>
Savings:{" "}
{originalSize > 0
? ((1 - processedSize / originalSize) * 100).toFixed(1)
: "0"}
%
{originalSize > 0 ? ((1 - processedSize / originalSize) * 100).toFixed(1) : "0"}%
</p>
</div>
)}
+5 -13
View File
@@ -1,4 +1,4 @@
import { useRef, useCallback, useEffect } from "react";
import { useCallback, useEffect, useRef } from "react";
import ReactCrop, { type Crop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
@@ -68,9 +68,7 @@ export function CropCanvas({
return;
} else if (e.key === "Enter") {
// Submit the crop form (find and submit closest form)
const form = document.querySelector<HTMLFormElement>(
'form[data-crop-form]',
);
const form = document.querySelector<HTMLFormElement>("form[data-crop-form]");
if (form) form.requestSubmit();
e.preventDefault();
return;
@@ -94,17 +92,11 @@ export function CropCanvas({
}, []);
// Calculate pixel dimensions for the badge
const pixelWidth =
imgDimensions ? Math.round((crop.width / 100) * imgDimensions.width) : 0;
const pixelHeight =
imgDimensions ? Math.round((crop.height / 100) * imgDimensions.height) : 0;
const pixelWidth = imgDimensions ? Math.round((crop.width / 100) * imgDimensions.width) : 0;
const pixelHeight = imgDimensions ? Math.round((crop.height / 100) * imgDimensions.height) : 0;
return (
<div
ref={containerRef}
className="flex flex-col w-full h-full max-w-4xl mx-auto outline-none"
tabIndex={0}
>
<div ref={containerRef} className="flex flex-col w-full h-full max-w-4xl mx-auto outline-none">
{/* Crop area */}
<div className="flex-1 flex items-center justify-center overflow-hidden bg-muted/20 p-4">
<ReactCrop
+12 -29
View File
@@ -1,9 +1,9 @@
import { ArrowLeftRight, Download, Grid3x3 } from "lucide-react";
import { useCallback } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download, ArrowLeftRight, Grid3x3 } from "lucide-react";
import { ProgressCard } from "@/components/common/progress-card";
import type { Crop } from "react-image-crop";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
const ASPECT_PRESETS = [
{ label: "Free", value: undefined as number | undefined },
@@ -35,14 +35,8 @@ export function CropSettings({
onGridToggle,
}: CropSettingsProps) {
const { files } = useFileStore();
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
progress,
} = useToolProcessor("crop");
const { processFiles, processAllFiles, processing, error, downloadUrl, progress } =
useToolProcessor("crop");
const { crop, aspect, showGrid, imgDimensions } = cropState;
@@ -66,10 +60,7 @@ export function CropSettings({
if (!imgDimensions) return;
const newCrop = { ...crop };
if (field === "left") {
newCrop.x = Math.max(
0,
Math.min((value / imgDimensions.width) * 100, 100 - newCrop.width),
);
newCrop.x = Math.max(0, Math.min((value / imgDimensions.width) * 100, 100 - newCrop.width));
} else if (field === "top") {
newCrop.y = Math.max(
0,
@@ -116,7 +107,7 @@ export function CropSettings({
} else {
// Desired ratio is taller than image — use full height, shrink width
newHeight = 100;
newWidth = (imgDimensions.height * value / imgDimensions.width) * 100;
newWidth = ((imgDimensions.height * value) / imgDimensions.width) * 100;
}
onCropChange({
unit: "%",
@@ -215,9 +206,7 @@ export function CropSettings({
<input
type="number"
value={pixels.left}
onChange={(e) =>
handlePixelChange("left", Number(e.target.value))
}
onChange={(e) => handlePixelChange("left", Number(e.target.value))}
min={0}
max={imgDimensions ? imgDimensions.width - 1 : undefined}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums"
@@ -230,9 +219,7 @@ export function CropSettings({
<input
type="number"
value={pixels.top}
onChange={(e) =>
handlePixelChange("top", Number(e.target.value))
}
onChange={(e) => handlePixelChange("top", Number(e.target.value))}
min={0}
max={imgDimensions ? imgDimensions.height - 1 : undefined}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums"
@@ -245,9 +232,7 @@ export function CropSettings({
<input
type="number"
value={pixels.width}
onChange={(e) =>
handlePixelChange("width", Number(e.target.value))
}
onChange={(e) => handlePixelChange("width", Number(e.target.value))}
min={1}
max={imgDimensions ? imgDimensions.width : undefined}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums"
@@ -260,9 +245,7 @@ export function CropSettings({
<input
type="number"
value={pixels.height}
onChange={(e) =>
handlePixelChange("height", Number(e.target.value))
}
onChange={(e) => handlePixelChange("height", Number(e.target.value))}
min={1}
max={imgDimensions ? imgDimensions.height : undefined}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums"
@@ -1,7 +1,7 @@
import { useState, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { ProgressCard } from "@/components/common/progress-card";
import { Download, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -116,19 +116,15 @@ export function EraseObjectSettings() {
<div>
<label className="text-sm font-medium text-muted-foreground">Mask Image</label>
<p className="text-[10px] text-muted-foreground mt-0.5 mb-1.5">
Upload a black &amp; white mask where white areas will be erased. Create the mask in any image editor.
Upload a black &amp; white mask where white areas will be erased. Create the mask in any
image editor.
</p>
<label className="flex items-center gap-2 px-3 py-2 rounded border border-dashed border-border cursor-pointer hover:border-primary">
<Upload className="h-4 w-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{maskFile ? maskFile.name : "Select mask image..."}
</span>
<input
type="file"
accept="image/*"
onChange={handleMaskSelect}
className="hidden"
/>
<input type="file" accept="image/*" onChange={handleMaskSelect} className="hidden" />
</label>
</div>
@@ -1,6 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -62,8 +62,8 @@ export function FaviconSettings() {
return (
<div className="space-y-4">
<p className="text-xs text-muted-foreground">
Upload a square image (recommended 512x512 or larger) to generate all
favicon and app icon sizes.
Upload a square image (recommended 512x512 or larger) to generate all favicon and app icon
sizes.
</p>
<div>
@@ -76,9 +76,7 @@ export function FaviconSettings() {
</div>
))}
</div>
<p className="text-[10px] text-muted-foreground mt-1">
+ manifest.json + HTML snippet
</p>
<p className="text-[10px] text-muted-foreground mt-1">+ manifest.json + HTML snippet</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -1,6 +1,6 @@
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -1,8 +1,8 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function GifToolsSettings() {
const { files } = useFileStore();
@@ -54,26 +54,46 @@ export function GifToolsSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<input type="number" value={width} onChange={(e) => setWidth(e.target.value)} placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={width}
onChange={(e) => setWidth(e.target.value)}
placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<input type="number" value={height} onChange={(e) => setHeight(e.target.value)} placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
</div>
<label className="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" checked={optimize} onChange={(e) => setOptimize(e.target.checked)} className="rounded" />
<input
type="checkbox"
checked={optimize}
onChange={(e) => setOptimize(e.target.checked)}
className="rounded"
/>
Optimize file size
</label>
</>
) : (
<div>
<label className="text-xs text-muted-foreground">Frame Number</label>
<input type="number" value={extractFrame} onChange={(e) => setExtractFrame(e.target.value)} min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={extractFrame}
onChange={(e) => setExtractFrame(e.target.value)}
min={0}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
<p className="text-[10px] text-muted-foreground mt-0.5">Frame 0 is the first frame</p>
</div>
)}
@@ -107,7 +127,11 @@ export function GifToolsSettings() {
)}
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,6 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -52,8 +52,8 @@ export function ImageToPdfSettings() {
return (
<div className="space-y-4">
<p className="text-xs text-muted-foreground">
{files.length} image{files.length !== 1 ? "s" : ""} will be combined
into a PDF, one image per page.
{files.length} image{files.length !== 1 ? "s" : ""} will be combined into a PDF, one image
per page.
</p>
<div>
@@ -93,7 +93,14 @@ export function ImageToPdfSettings() {
<label className="text-xs text-muted-foreground">Margin</label>
<span className="text-xs font-mono text-foreground">{margin}pt</span>
</div>
<input type="range" min={0} max={100} value={margin} onChange={(e) => setMargin(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={margin}
onChange={(e) => setMargin(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -108,7 +115,11 @@ export function ImageToPdfSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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 PDF
</a>
@@ -1,6 +1,6 @@
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -93,7 +93,9 @@ export function InfoSettings() {
<div className="space-y-3">
<div className="grid grid-cols-2 gap-1 text-xs">
<div className="text-muted-foreground">Dimensions</div>
<div className="text-foreground font-mono">{info.width} x {info.height}</div>
<div className="text-foreground font-mono">
{info.width} x {info.height}
</div>
<div className="text-muted-foreground">Format</div>
<div className="text-foreground font-mono">{info.format}</div>
<div className="text-muted-foreground">File Size</div>
@@ -125,7 +127,9 @@ export function InfoSettings() {
{info.histogram.map((ch) => (
<div key={ch.channel} className="space-y-0.5">
<div className="flex items-center gap-1.5">
<div className={`w-2 h-2 rounded-full ${channelColors[ch.channel] ?? "bg-gray-400"}`} />
<div
className={`w-2 h-2 rounded-full ${channelColors[ch.channel] ?? "bg-gray-400"}`}
/>
<span className="text-xs text-foreground capitalize">{ch.channel}</span>
</div>
<div className="flex gap-2 text-[10px] text-muted-foreground font-mono">
+5 -11
View File
@@ -1,7 +1,7 @@
import { useState, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { Check, Copy } from "lucide-react";
import { useRef, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { Copy, Check } from "lucide-react";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -204,11 +204,7 @@ export function OcrSettings() {
onClick={handleCopy}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{copied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? "Copied" : "Copy"}
</button>
</div>
@@ -219,9 +215,7 @@ export function OcrSettings() {
className="w-full px-2 py-1.5 rounded border border-border bg-muted text-xs text-foreground font-mono resize-y"
/>
{text.length > 0 && (
<p className="text-[10px] text-muted-foreground">
{text.length} characters extracted
</p>
<p className="text-[10px] text-muted-foreground">{text.length} characters extracted</p>
)}
</div>
)}
@@ -1,26 +1,25 @@
import { useState, useCallback } from "react";
import { TOOLS } from "@stirling-image/shared";
import * as icons from "lucide-react";
import {
Plus,
X,
ChevronUp,
ChevronDown,
ChevronRight,
ChevronUp,
Download,
FileImage,
Loader2,
Play,
Plus,
Save,
Upload,
Loader2,
FileImage,
Download,
X,
} from "lucide-react";
import * as icons from "lucide-react";
import { TOOLS } from "@stirling-image/shared";
import { useCallback, useState } from "react";
import { cn } from "@/lib/utils";
import { PipelineStepSettings } from "./pipeline-step-settings";
/** Tools that can be used as pipeline steps (excludes pipeline/batch/multi-file tools). */
const PIPELINE_TOOLS = TOOLS.filter(
(t) =>
!["pipeline", "batch", "compare", "find-duplicates", "collage", "compose"].includes(t.id)
(t) => !["pipeline", "batch", "compare", "find-duplicates", "collage", "compose"].includes(t.id),
);
export interface PipelineStep {
@@ -71,7 +70,7 @@ export function PipelineBuilder({
setShowToolPicker(false);
setExpandedStep(step.id);
},
[steps, onStepsChange]
[steps, onStepsChange],
);
const removeStep = useCallback(
@@ -79,7 +78,7 @@ export function PipelineBuilder({
onStepsChange(steps.filter((s) => s.id !== id));
if (expandedStep === id) setExpandedStep(null);
},
[steps, onStepsChange, expandedStep]
[steps, onStepsChange, expandedStep],
);
const moveStep = useCallback(
@@ -92,14 +91,14 @@ export function PipelineBuilder({
[newSteps[idx], newSteps[newIdx]] = [newSteps[newIdx], newSteps[idx]];
onStepsChange(newSteps);
},
[steps, onStepsChange]
[steps, onStepsChange],
);
const updateStepSettings = useCallback(
(id: string, newSettings: Record<string, unknown>) => {
onStepsChange(steps.map((s) => (s.id === id ? { ...s, settings: newSettings } : s)));
},
[steps, onStepsChange]
[steps, onStepsChange],
);
const handleFileSelect = useCallback(() => {
@@ -132,10 +131,7 @@ export function PipelineBuilder({
onExecute(file);
}, [file, onExecute]);
const iconsMap = icons as unknown as Record<
string,
React.ComponentType<{ className?: string }>
>;
const iconsMap = icons as unknown as Record<string, React.ComponentType<{ className?: string }>>;
return (
<div className="space-y-6">
@@ -147,7 +143,7 @@ export function PipelineBuilder({
"rounded-xl border-2 border-dashed p-6 text-center transition-colors",
file
? "border-primary/30 bg-primary/5"
: "border-border bg-muted/20 hover:border-primary/30"
: "border-border bg-muted/20 hover:border-primary/30",
)}
>
{file ? (
@@ -203,9 +199,7 @@ export function PipelineBuilder({
{/* Tool icon + name */}
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-sm font-medium text-foreground flex-1">
{tool.name}
</span>
<span className="text-sm font-medium text-foreground flex-1">{tool.name}</span>
{/* Controls */}
<div className="flex items-center gap-0.5 shrink-0">
@@ -247,9 +241,7 @@ export function PipelineBuilder({
{/* Expanded settings */}
{isExpanded && (
<div className="border-t border-border p-3 bg-muted/10 space-y-3">
<p className="text-xs text-muted-foreground">
{tool.description}
</p>
<p className="text-xs text-muted-foreground">{tool.description}</p>
<PipelineStepSettings
toolId={step.toolId}
settings={step.settings}
@@ -286,9 +278,7 @@ export function PipelineBuilder({
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium text-foreground">{tool.name}</div>
<div className="text-xs text-muted-foreground truncate">
{tool.description}
</div>
<div className="text-xs text-muted-foreground truncate">{tool.description}</div>
</div>
</button>
);
@@ -365,7 +355,6 @@ export function PipelineBuilder({
onChange={(e) => setSaveName(e.target.value)}
placeholder="Pipeline name"
className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground flex-1"
autoFocus
/>
<input
type="text"
@@ -17,7 +17,13 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
resize: [
{ key: "width", label: "Width (px)", type: "number", min: 1, placeholder: "Auto" },
{ key: "height", label: "Height (px)", type: "number", min: 1, placeholder: "Auto" },
{ key: "percentage", label: "Scale (%)", type: "number", min: 1, placeholder: "Use instead of width/height" },
{
key: "percentage",
label: "Scale (%)",
type: "number",
min: 1,
placeholder: "Use instead of width/height",
},
{
key: "fit",
label: "Fit Mode",
@@ -40,7 +46,15 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
],
rotate: [
{ key: "angle", label: "Angle (degrees)", type: "number", min: -360, max: 360, step: 90, defaultValue: 0 },
{
key: "angle",
label: "Angle (degrees)",
type: "number",
min: -360,
max: 360,
step: 90,
defaultValue: 0,
},
{ key: "horizontal", label: "Flip horizontal", type: "boolean", defaultValue: false },
{ key: "vertical", label: "Flip vertical", type: "boolean", defaultValue: false },
],
@@ -59,7 +73,14 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
{ value: "gif", label: "GIF" },
],
},
{ key: "quality", label: "Quality (1-100)", type: "number", min: 1, max: 100, placeholder: "Auto" },
{
key: "quality",
label: "Quality (1-100)",
type: "number",
min: 1,
max: 100,
placeholder: "Auto",
},
],
compress: [
@@ -125,12 +146,26 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
],
"brightness-contrast": [
{ key: "brightness", label: "Brightness", type: "number", min: -100, max: 100, defaultValue: 0 },
{
key: "brightness",
label: "Brightness",
type: "number",
min: -100,
max: 100,
defaultValue: 0,
},
{ key: "contrast", label: "Contrast", type: "number", min: -100, max: 100, defaultValue: 0 },
],
saturation: [
{ key: "saturation", label: "Saturation", type: "number", min: -100, max: 100, defaultValue: 0 },
{
key: "saturation",
label: "Saturation",
type: "number",
min: -100,
max: 100,
defaultValue: 0,
},
],
"color-channels": [
@@ -157,8 +192,20 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
"replace-color": [
{ key: "sourceColor", label: "Source color", type: "color", defaultValue: "#FF0000" },
{ key: "targetColor", label: "Target color", type: "color", defaultValue: "#00FF00" },
{ key: "makeTransparent", label: "Make transparent instead", type: "boolean", defaultValue: false },
{ key: "tolerance", label: "Tolerance (0-255)", type: "number", min: 0, max: 255, defaultValue: 30 },
{
key: "makeTransparent",
label: "Make transparent instead",
type: "boolean",
defaultValue: false,
},
{
key: "tolerance",
label: "Tolerance (0-255)",
type: "number",
min: 0,
max: 255,
defaultValue: 30,
},
],
"watermark-text": [
@@ -180,7 +227,14 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
{ value: "tiled", label: "Tiled" },
],
},
{ key: "rotation", label: "Rotation (degrees)", type: "number", min: -360, max: 360, defaultValue: 0 },
{
key: "rotation",
label: "Rotation (degrees)",
type: "number",
min: -360,
max: 360,
defaultValue: 0,
},
],
"watermark-image": [
@@ -230,7 +284,14 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
border: [
{ key: "borderWidth", label: "Width (px)", type: "number", min: 0, max: 200, defaultValue: 10 },
{ key: "borderColor", label: "Color", type: "color", defaultValue: "#000000" },
{ key: "cornerRadius", label: "Corner radius", type: "number", min: 0, max: 500, defaultValue: 0 },
{
key: "cornerRadius",
label: "Corner radius",
type: "number",
min: 0,
max: 500,
defaultValue: 0,
},
{ key: "padding", label: "Padding (px)", type: "number", min: 0, max: 200, defaultValue: 0 },
{ key: "shadowBlur", label: "Shadow blur", type: "number", min: 0, max: 50, defaultValue: 0 },
{ key: "shadowColor", label: "Shadow color", type: "color", defaultValue: "#00000080" },
@@ -243,7 +304,15 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
"blur-faces": [
{ key: "blurRadius", label: "Blur radius", type: "number", min: 1, max: 100, defaultValue: 30 },
{ key: "sensitivity", label: "Sensitivity (0-1)", type: "number", min: 0, max: 1, step: 0.1, defaultValue: 0.5 },
{
key: "sensitivity",
label: "Sensitivity (0-1)",
type: "number",
min: 0,
max: 1,
step: 0.1,
defaultValue: 0.5,
},
],
upscale: [
@@ -276,7 +345,14 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
{ value: "color", label: "Color" },
],
},
{ key: "threshold", label: "Threshold (0-255)", type: "number", min: 0, max: 255, defaultValue: 128 },
{
key: "threshold",
label: "Threshold (0-255)",
type: "number",
min: 0,
max: 255,
defaultValue: 128,
},
{
key: "detail",
label: "Detail",
@@ -309,7 +385,13 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
"gif-tools": [
{ key: "width", label: "Width (px)", type: "number", min: 1, max: 4096, placeholder: "Auto" },
{ key: "height", label: "Height (px)", type: "number", min: 1, max: 4096, placeholder: "Auto" },
{ key: "extractFrame", label: "Extract frame #", type: "number", min: 0, placeholder: "All frames" },
{
key: "extractFrame",
label: "Extract frame #",
type: "number",
min: 0,
placeholder: "All frames",
},
{ key: "optimize", label: "Optimize", type: "boolean", defaultValue: false },
],
@@ -340,7 +422,13 @@ const TOOL_FIELDS: Record<string, FieldDef[]> = {
],
"bulk-rename": [
{ key: "pattern", label: "Pattern", type: "text", placeholder: "image-{{index}}", defaultValue: "image-{{index}}" },
{
key: "pattern",
label: "Pattern",
type: "text",
placeholder: "image-{{index}}",
defaultValue: "image-{{index}}",
},
{ key: "startIndex", label: "Start index", type: "number", min: 0, defaultValue: 1 },
],
@@ -443,7 +531,10 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
type="number"
value={value != null && value !== "" ? Number(value) : ""}
onChange={(e) =>
updateField(field.key, e.target.value === "" ? undefined : Number(e.target.value))
updateField(
field.key,
e.target.value === "" ? undefined : Number(e.target.value),
)
}
min={field.min}
max={field.max}
@@ -515,7 +606,10 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
case "boolean":
return (
<label key={field.key} className="flex items-center gap-2 text-sm text-foreground cursor-pointer">
<label
key={field.key}
className="flex items-center gap-2 text-sm text-foreground cursor-pointer"
>
<input
type="checkbox"
checked={Boolean(value)}
@@ -1,5 +1,5 @@
import { useState } from "react";
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -67,7 +67,15 @@ export function QrGenerateSettings() {
<label className="text-xs text-muted-foreground">Size</label>
<span className="text-xs font-mono text-foreground">{size}px</span>
</div>
<input type="range" min={100} max={2000} step={50} value={size} onChange={(e) => setSize(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={100}
max={2000}
step={50}
value={size}
onChange={(e) => setSize(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -87,11 +95,21 @@ export function QrGenerateSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Foreground</label>
<input type="color" value={foreground} onChange={(e) => setForeground(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={foreground}
onChange={(e) => setForeground(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Background</label>
<input type="color" value={background} onChange={(e) => setBackground(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={background}
onChange={(e) => setBackground(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
</div>
@@ -108,8 +126,17 @@ export function QrGenerateSettings() {
{previewUrl && (
<div className="flex flex-col items-center gap-2">
<img src={previewUrl} alt="QR Code" className="max-w-full rounded border border-border" style={{ maxHeight: 200 }} />
<a href={downloadUrl!} 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">
<img
src={previewUrl}
alt="QR Code"
className="max-w-full rounded border border-border"
style={{ maxHeight: 200 }}
/>
<a
href={downloadUrl!}
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 QR Code
</a>
@@ -1,8 +1,8 @@
import { Download, ImageIcon, Package, User } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { ProgressCard } from "@/components/common/progress-card";
import { Download, User, Package, ImageIcon } from "lucide-react";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
type SubjectType = "people" | "products" | "general";
type Quality = "fast" | "balanced" | "best";
@@ -15,9 +15,9 @@ type BgModel =
| "u2net";
const MODEL_MAP: Record<SubjectType, Record<Quality, BgModel>> = {
people: { fast: "u2net", balanced: "birefnet-portrait", best: "birefnet-portrait" },
products: { fast: "u2net", balanced: "bria-rmbg", best: "birefnet-general" },
general: { fast: "u2net", balanced: "birefnet-general-lite", best: "birefnet-general" },
people: { fast: "u2net", balanced: "birefnet-portrait", best: "birefnet-portrait" },
products: { fast: "u2net", balanced: "bria-rmbg", best: "birefnet-general" },
general: { fast: "u2net", balanced: "birefnet-general-lite", best: "birefnet-general" },
};
const SUBJECT_OPTIONS: { value: SubjectType; label: string; icon: typeof User }[] = [
@@ -126,9 +126,7 @@ export function RemoveBgSettings() {
{/* Background color - intuitive preset buttons */}
<div>
<label className="text-sm font-medium text-muted-foreground">
Output Background
</label>
<label className="text-sm font-medium text-muted-foreground">Output Background</label>
<div className="flex gap-1.5 mt-1.5 flex-wrap">
{BG_PRESETS.map((preset) => (
<button
@@ -1,13 +1,21 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function ReplaceColorSettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("replace-color");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("replace-color");
const [sourceColor, setSourceColor] = useState("#FF0000");
const [targetColor, setTargetColor] = useState("#00FF00");
@@ -30,13 +38,23 @@ export function ReplaceColorSettings() {
<div>
<label className="text-xs text-muted-foreground">Source Color (to replace)</label>
<div className="flex items-center gap-2 mt-0.5">
<input type="color" value={sourceColor} onChange={(e) => setSourceColor(e.target.value)} className="w-10 h-8 rounded border border-border" />
<input
type="color"
value={sourceColor}
onChange={(e) => setSourceColor(e.target.value)}
className="w-10 h-8 rounded border border-border"
/>
<span className="text-xs font-mono text-foreground">{sourceColor}</span>
</div>
</div>
<label className="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" checked={makeTransparent} onChange={(e) => setMakeTransparent(e.target.checked)} className="rounded" />
<input
type="checkbox"
checked={makeTransparent}
onChange={(e) => setMakeTransparent(e.target.checked)}
className="rounded"
/>
Make transparent instead
</label>
@@ -44,7 +62,12 @@ export function ReplaceColorSettings() {
<div>
<label className="text-xs text-muted-foreground">Target Color (replacement)</label>
<div className="flex items-center gap-2 mt-0.5">
<input type="color" value={targetColor} onChange={(e) => setTargetColor(e.target.value)} className="w-10 h-8 rounded border border-border" />
<input
type="color"
value={targetColor}
onChange={(e) => setTargetColor(e.target.value)}
className="w-10 h-8 rounded border border-border"
/>
<span className="text-xs font-mono text-foreground">{targetColor}</span>
</div>
</div>
@@ -55,7 +78,14 @@ export function ReplaceColorSettings() {
<label className="text-xs text-muted-foreground">Tolerance</label>
<span className="text-xs font-mono text-foreground">{tolerance}</span>
</div>
<input type="range" min={0} max={255} value={tolerance} onChange={(e) => setTolerance(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={255}
value={tolerance}
onChange={(e) => setTolerance(Number(e.target.value))}
className="w-full mt-1"
/>
<div className="flex justify-between text-[10px] text-muted-foreground mt-0.5">
<span>Exact match</span>
<span>Wide range</span>
@@ -91,7 +121,11 @@ export function ReplaceColorSettings() {
)}
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,9 +1,9 @@
import { useState } from "react";
import { SOCIAL_MEDIA_PRESETS } from "@stirling-image/shared";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download, Link, Unlink } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
type ResizeTab = "presets" | "custom" | "scale";
type FitMode = "cover" | "contain" | "fill";
@@ -67,9 +67,7 @@ export function ResizeSettings() {
const canProcess =
hasFile &&
!processing &&
(tab === "scale"
? Number(percentage) > 0
: Boolean(width) || Boolean(height));
(tab === "scale" ? Number(percentage) > 0 : Boolean(width) || Boolean(height));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -1,13 +1,8 @@
import { useState, useEffect, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import {
RotateCcw,
RotateCw,
FlipHorizontal,
FlipVertical,
} from "lucide-react";
import { FlipHorizontal, FlipVertical, RotateCcw, RotateCw } 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";
export interface PreviewTransform {
rotate: number;
@@ -21,8 +16,7 @@ interface RotateSettingsProps {
export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, progress } =
useToolProcessor("rotate");
const { processFiles, processAllFiles, processing, error, progress } = useToolProcessor("rotate");
// Quick rotation in 90° steps: 0, 90, 180, 270
const [rotation, setRotation] = useState(0);
@@ -101,9 +95,7 @@ export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
Left
</button>
<div className="px-3 py-1.5 rounded-md bg-background border border-border text-center min-w-[4rem]">
<span className="text-sm font-mono font-medium tabular-nums">
{displayAngle}°
</span>
<span className="text-sm font-mono font-medium tabular-nums">{displayAngle}°</span>
</div>
<button
type="button"
@@ -1,8 +1,8 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
const ASPECT_PRESETS = [
{ label: "1:1 Square", w: 1080, h: 1080 },
@@ -92,7 +92,8 @@ export function SmartCropSettings() {
{/* Info */}
<p className="text-[10px] text-muted-foreground">
Uses entropy-based attention detection to find the most interesting region of the image and crops to it.
Uses entropy-based attention detection to find the most interesting region of the image and
crops to it.
</p>
{/* Error */}
@@ -1,6 +1,6 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -68,7 +68,10 @@ export function SplitSettings() {
{presets.map((p) => (
<button
key={p.label}
onClick={() => { setColumns(p.c); setRows(p.r); }}
onClick={() => {
setColumns(p.c);
setRows(p.r);
}}
className={`text-xs px-2 py-1 rounded ${columns === p.c && rows === p.r ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
{p.label}
@@ -80,19 +83,29 @@ export function SplitSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Columns</label>
<input type="number" value={columns} onChange={(e) => setColumns(Math.max(1, Number(e.target.value)))} min={1} max={10}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={columns}
onChange={(e) => setColumns(Math.max(1, Number(e.target.value)))}
min={1}
max={10}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Rows</label>
<input type="number" value={rows} onChange={(e) => setRows(Math.max(1, Number(e.target.value)))} min={1} max={10}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={rows}
onChange={(e) => setRows(Math.max(1, Number(e.target.value)))}
min={1}
max={10}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
</div>
<p className="text-xs text-muted-foreground">
Will produce {columns * rows} parts
</p>
<p className="text-xs text-muted-foreground">Will produce {columns * rows} parts</p>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -1,8 +1,8 @@
import { useState, useEffect } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download, ChevronDown, ChevronRight, Loader2, MapPin, AlertTriangle } from "lucide-react";
import { AlertTriangle, ChevronDown, ChevronRight, Download, Loader2, MapPin } from "lucide-react";
import { useEffect, useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
@@ -61,10 +61,19 @@ const EXIF_LABELS: Record<string, string> = {
/** Keys to skip in display (internal/binary/redundant) */
const SKIP_KEYS = new Set([
"ExifTag", "GPSTag", "InteroperabilityTag", "MakerNote",
"PrintImageMatching", "ComponentsConfiguration", "FlashpixVersion",
"ExifVersion", "FileSource", "SceneType", "UserComment",
"InteroperabilityIndex", "InteroperabilityVersion",
"ExifTag",
"GPSTag",
"InteroperabilityTag",
"MakerNote",
"PrintImageMatching",
"ComponentsConfiguration",
"FlashpixVersion",
"ExifVersion",
"FileSource",
"SceneType",
"UserComment",
"InteroperabilityIndex",
"InteroperabilityVersion",
]);
function formatExifValue(key: string, value: unknown): string {
@@ -110,7 +119,11 @@ function CollapsibleSection({
onClick={() => setOpen(!open)}
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-medium text-foreground hover:bg-muted/50 transition-colors"
>
{open ? <ChevronDown className="h-3 w-3 shrink-0" /> : <ChevronRight className="h-3 w-3 shrink-0" />}
{open ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span className="flex-1 text-left">{title}</span>
{warning && <AlertTriangle className="h-3 w-3 text-amber-500 shrink-0" />}
{badge && (
@@ -124,9 +137,16 @@ function CollapsibleSection({
);
}
function MetadataGrid({ data, labelMap }: { data: Record<string, unknown>; labelMap?: Record<string, string> }) {
function MetadataGrid({
data,
labelMap,
}: {
data: Record<string, unknown>;
labelMap?: Record<string, string>;
}) {
const entries = Object.entries(data).filter(
([k, v]) => !SKIP_KEYS.has(k) && !k.startsWith("_") && v !== undefined && v !== null && String(v) !== ""
([k, v]) =>
!SKIP_KEYS.has(k) && !k.startsWith("_") && v !== undefined && v !== null && String(v) !== "",
);
if (entries.length === 0) {
@@ -140,7 +160,10 @@ function MetadataGrid({ data, labelMap }: { data: Record<string, unknown>; label
<div className="text-[10px] text-muted-foreground truncate" title={k}>
{labelMap?.[k] ?? k}
</div>
<div className="text-[10px] text-foreground font-mono truncate" title={formatExifValue(k, v)}>
<div
className="text-[10px] text-foreground font-mono truncate"
title={formatExifValue(k, v)}
>
{formatExifValue(k, v)}
</div>
</div>
@@ -167,7 +190,9 @@ export function StripMetadataSettings() {
const [inspectError, setInspectError] = useState<string | null>(null);
const currentFile = entries[selectedIndex]?.file ?? null;
const fileKey = currentFile ? `${currentFile.name}-${currentFile.size}-${currentFile.lastModified}` : null;
const fileKey = currentFile
? `${currentFile.name}-${currentFile.size}-${currentFile.lastModified}`
: null;
// Auto-fetch metadata for the selected file (with per-file caching)
useEffect(() => {
@@ -214,7 +239,7 @@ export function StripMetadataSettings() {
})();
return () => controller.abort();
}, [currentFile, fileKey]);
}, [currentFile, fileKey, metadataCache.get]);
const handleStripAllChange = (checked: boolean) => {
setStripAll(checked);
@@ -245,8 +270,8 @@ export function StripMetadataSettings() {
const sectionCount = [hasExif, hasGps, hasIcc, hasXmp].filter(Boolean).length;
// GPS coordinates for display
const gpsLat = metadata?.gps?.["_latitude"] as number | null | undefined;
const gpsLon = metadata?.gps?.["_longitude"] as number | null | undefined;
const gpsLat = metadata?.gps?._latitude as number | null | undefined;
const gpsLon = metadata?.gps?._longitude as number | null | undefined;
return (
<form onSubmit={handleSubmit} className="space-y-4">
@@ -262,9 +287,7 @@ export function StripMetadataSettings() {
</div>
)}
{inspectError && (
<p className="text-[10px] text-red-500">{inspectError}</p>
)}
{inspectError && <p className="text-[10px] text-red-500">{inspectError}</p>}
{metadata && !hasAnyMetadata && !inspecting && (
<p className="text-xs text-muted-foreground italic py-1">
@@ -287,7 +310,7 @@ export function StripMetadataSettings() {
{hasExif && (
<CollapsibleSection
title="EXIF"
badge={`${Object.keys(metadata.exif!).filter(k => !SKIP_KEYS.has(k) && !k.startsWith("_")).length} fields`}
badge={`${Object.keys(metadata.exif!).filter((k) => !SKIP_KEYS.has(k) && !k.startsWith("_")).length} fields`}
defaultOpen
>
<MetadataGrid data={metadata.exif!} labelMap={EXIF_LABELS} />
@@ -299,19 +322,29 @@ export function StripMetadataSettings() {
)}
{hasGps && (
<CollapsibleSection title="GPS" warning badge={`${Object.keys(metadata.gps!).filter(k => !k.startsWith("_")).length} fields`}>
<CollapsibleSection
title="GPS"
warning
badge={`${Object.keys(metadata.gps!).filter((k) => !k.startsWith("_")).length} fields`}
>
<MetadataGrid data={metadata.gps!} />
</CollapsibleSection>
)}
{hasIcc && (
<CollapsibleSection title="ICC Profile" badge={`${Object.keys(metadata.icc!).length} fields`}>
<CollapsibleSection
title="ICC Profile"
badge={`${Object.keys(metadata.icc!).length} fields`}
>
<MetadataGrid data={metadata.icc!} />
</CollapsibleSection>
)}
{hasXmp && (
<CollapsibleSection title="XMP" badge={`${Object.keys(metadata.xmp!).length} fields`}>
<CollapsibleSection
title="XMP"
badge={`${Object.keys(metadata.xmp!).length} fields`}
>
<MetadataGrid data={metadata.xmp!} />
</CollapsibleSection>
)}
@@ -343,7 +376,9 @@ export function StripMetadataSettings() {
<div className="space-y-2">
<label className="text-xs text-muted-foreground">Or select specific metadata:</label>
<label className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}>
<label
className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}
>
<input
type="checkbox"
checked={stripExif}
@@ -353,11 +388,15 @@ export function StripMetadataSettings() {
/>
Strip EXIF (camera info, date, exposure)
{hasExif && !stripAll && (
<span className="ml-auto text-[10px] text-muted-foreground">{Object.keys(metadata!.exif!).filter(k => !SKIP_KEYS.has(k)).length} fields</span>
<span className="ml-auto text-[10px] text-muted-foreground">
{Object.keys(metadata?.exif!).filter((k) => !SKIP_KEYS.has(k)).length} fields
</span>
)}
</label>
<label className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}>
<label
className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}
>
<input
type="checkbox"
checked={stripGps}
@@ -371,7 +410,9 @@ export function StripMetadataSettings() {
)}
</label>
<label className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}>
<label
className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}
>
<input
type="checkbox"
checked={stripIcc}
@@ -382,7 +423,9 @@ export function StripMetadataSettings() {
Strip ICC (color profile)
</label>
<label className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}>
<label
className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}
>
<input
type="checkbox"
checked={stripXmp}
@@ -1,13 +1,14 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function SvgToRasterSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
const [width, setWidth] = useState(1024);
const [height, setHeight] = useState("");
const [backgroundColor, setBackgroundColor] = useState("#00000000");
@@ -67,13 +68,24 @@ export function SvgToRasterSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<input type="number" value={width} onChange={(e) => setWidth(Number(e.target.value))} min={1} max={8192}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
min={1}
max={8192}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<input type="number" value={height} onChange={(e) => setHeight(e.target.value)} placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
<input
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
placeholder="Auto"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
</div>
@@ -104,7 +116,12 @@ export function SvgToRasterSettings() {
{!transparent && (
<div>
<label className="text-xs text-muted-foreground">Background Color</label>
<input type="color" value={backgroundColor.slice(0, 7)} onChange={(e) => setBackgroundColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={backgroundColor.slice(0, 7)}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
)}
@@ -127,7 +144,11 @@ export function SvgToRasterSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,13 +1,21 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function TextOverlaySettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("text-overlay");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("text-overlay");
const [text, setText] = useState("Your Text Here");
const [fontSize, setFontSize] = useState(48);
@@ -45,12 +53,24 @@ export function TextOverlaySettings() {
<label className="text-xs text-muted-foreground">Font Size</label>
<span className="text-xs font-mono text-foreground">{fontSize}px</span>
</div>
<input type="range" min={8} max={200} value={fontSize} onChange={(e) => setFontSize(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={8}
max={200}
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Text Color</label>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
<div>
@@ -67,19 +87,34 @@ export function TextOverlaySettings() {
</div>
<label className="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" checked={shadow} onChange={(e) => setShadow(e.target.checked)} className="rounded" />
<input
type="checkbox"
checked={shadow}
onChange={(e) => setShadow(e.target.checked)}
className="rounded"
/>
Drop Shadow
</label>
<label className="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" checked={backgroundBox} onChange={(e) => setBackgroundBox(e.target.checked)} className="rounded" />
<input
type="checkbox"
checked={backgroundBox}
onChange={(e) => setBackgroundBox(e.target.checked)}
className="rounded"
/>
Background Box
</label>
{backgroundBox && (
<div>
<label className="text-xs text-muted-foreground">Box Color</label>
<input type="color" value={backgroundColor} onChange={(e) => setBackgroundColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-full mt-0.5 h-8 rounded border border-border"
/>
</div>
)}
@@ -112,7 +147,11 @@ export function TextOverlaySettings() {
)}
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,8 +1,8 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { ProgressCard } from "@/components/common/progress-card";
import { Download } from "lucide-react";
import { useState } from "react";
import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
export function UpscaleSettings() {
const { files } = useFileStore();
@@ -41,7 +41,8 @@ export function UpscaleSettings() {
{/* Info */}
<p className="text-[10px] text-muted-foreground">
Uses Real-ESRGAN for AI upscaling when available, otherwise falls back to high-quality Lanczos interpolation.
Uses Real-ESRGAN for AI upscaling when available, otherwise falls back to high-quality
Lanczos interpolation.
</p>
{/* Error */}
@@ -1,13 +1,14 @@
import { Download, Loader2 } from "lucide-react";
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2 } from "lucide-react";
function getToken(): string {
return localStorage.getItem("stirling-token") || "";
}
export function VectorizeSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
const [colorMode, setColorMode] = useState<"bw" | "color">("bw");
const [threshold, setThreshold] = useState(128);
const [detail, setDetail] = useState<"low" | "medium" | "high">("medium");
@@ -79,7 +80,14 @@ export function VectorizeSettings() {
<label className="text-xs text-muted-foreground">Threshold</label>
<span className="text-xs font-mono text-foreground">{threshold}</span>
</div>
<input type="range" min={0} max={255} value={threshold} onChange={(e) => setThreshold(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={255}
value={threshold}
onChange={(e) => setThreshold(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -114,7 +122,11 @@ export function VectorizeSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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 SVG
</a>
@@ -1,6 +1,6 @@
import { useState, useRef } from "react";
import { useFileStore } from "@/stores/file-store";
import { Download, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { useFileStore } from "@/stores/file-store";
type Position = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
@@ -9,7 +9,8 @@ function getToken(): string {
}
export function WatermarkImageSettings() {
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore();
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
useFileStore();
const [position, setPosition] = useState<Position>("bottom-right");
const [opacity, setOpacity] = useState(50);
const [scale, setScale] = useState(25);
@@ -99,7 +100,14 @@ export function WatermarkImageSettings() {
<label className="text-xs text-muted-foreground">Opacity</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input type="range" min={0} max={100} value={opacity} onChange={(e) => setOpacity(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
<div>
@@ -107,7 +115,14 @@ export function WatermarkImageSettings() {
<label className="text-xs text-muted-foreground">Scale</label>
<span className="text-xs font-mono text-foreground">{scale}%</span>
</div>
<input type="range" min={5} max={100} value={scale} onChange={(e) => setScale(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={5}
max={100}
value={scale}
onChange={(e) => setScale(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -129,7 +144,11 @@ export function WatermarkImageSettings() {
</button>
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>
@@ -1,15 +1,23 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download } from "lucide-react";
import { 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 function WatermarkTextSettings() {
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress } =
useToolProcessor("watermark-text");
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("watermark-text");
const [text, setText] = useState("Sample Watermark");
const [fontSize, setFontSize] = useState(48);
@@ -46,20 +54,39 @@ export function WatermarkTextSettings() {
<label className="text-xs text-muted-foreground">Font Size</label>
<span className="text-xs font-mono text-foreground">{fontSize}px</span>
</div>
<input type="range" min={8} max={200} value={fontSize} onChange={(e) => setFontSize(Number(e.target.value))} className="w-full mt-1" />
<input
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 className="text-xs text-muted-foreground">Color</label>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)} className="w-full mt-0.5 h-8 rounded border border-border" />
<input
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 className="text-xs text-muted-foreground">Opacity</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input type="range" min={0} max={100} value={opacity} onChange={(e) => setOpacity(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={0}
max={100}
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
</div>
@@ -84,7 +111,14 @@ export function WatermarkTextSettings() {
<label className="text-xs text-muted-foreground">Rotation</label>
<span className="text-xs font-mono text-foreground">{rotation}&deg;</span>
</div>
<input type="range" min={-180} max={180} value={rotation} onChange={(e) => setRotation(Number(e.target.value))} className="w-full mt-1" />
<input
type="range"
min={-180}
max={180}
value={rotation}
onChange={(e) => setRotation(Number(e.target.value))}
className="w-full mt-1"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
@@ -116,7 +150,11 @@ export function WatermarkTextSettings() {
)}
{downloadUrl && (
<a href={downloadUrl} 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">
<a
href={downloadUrl}
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>