feat: add replace-color tool and update tool page routing

- Add replace-color tool with pixel-level color replacement and tolerance
- Register all 19 new Phase 3 tools in routes/tools/index.ts
- Map all new tool IDs to settings components in tool-page.tsx
- Add PDF, ZIP, ICO, JSON content types to download route
- Install qrcode, jsqr, potrace, pdfkit dependencies
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 04:21:10 +08:00
parent b88d0d0af8
commit 9c55dd32c4
7 changed files with 1820 additions and 34 deletions
@@ -0,0 +1,85 @@
import { useState } from "react";
import { useFileStore } from "@/stores/file-store";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { Download, Loader2 } from "lucide-react";
export function ReplaceColorSettings() {
const { files } = useFileStore();
const { processFiles, processing, error, downloadUrl, originalSize, processedSize } =
useToolProcessor("replace-color");
const [sourceColor, setSourceColor] = useState("#FF0000");
const [targetColor, setTargetColor] = useState("#00FF00");
const [makeTransparent, setMakeTransparent] = useState(false);
const [tolerance, setTolerance] = useState(30);
const handleProcess = () => {
processFiles(files, { sourceColor, targetColor, makeTransparent, tolerance });
};
const hasFile = files.length > 0;
return (
<div className="space-y-4">
<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" />
<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" />
Make transparent instead
</label>
{!makeTransparent && (
<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" />
<span className="text-xs font-mono text-foreground">{targetColor}</span>
</div>
</div>
)}
<div>
<div className="flex justify-between items-center">
<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" />
<div className="flex justify-between text-[10px] text-muted-foreground mt-0.5">
<span>Exact match</span>
<span>Wide range</span>
</div>
</div>
{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>
)}
<button
onClick={handleProcess}
disabled={!hasFile || processing}
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"
>
{processing && <Loader2 className="h-4 w-4 animate-spin" />}
{processing ? "Processing..." : "Replace Color"}
</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">
<Download className="h-4 w-4" />
Download
</a>
)}
</div>
);
}
+94 -31
View File
@@ -12,6 +12,32 @@ import { ConvertSettings } from "@/components/tools/convert-settings";
import { CompressSettings } from "@/components/tools/compress-settings";
import { StripMetadataSettings } from "@/components/tools/strip-metadata-settings";
import { ColorSettings } from "@/components/tools/color-settings";
// Phase 3: Watermark & Overlay
import { WatermarkTextSettings } from "@/components/tools/watermark-text-settings";
import { WatermarkImageSettings } from "@/components/tools/watermark-image-settings";
import { TextOverlaySettings } from "@/components/tools/text-overlay-settings";
import { ComposeSettings } from "@/components/tools/compose-settings";
// Phase 3: Utilities
import { InfoSettings } from "@/components/tools/info-settings";
import { CompareSettings } from "@/components/tools/compare-settings";
import { FindDuplicatesSettings } from "@/components/tools/find-duplicates-settings";
import { ColorPaletteSettings } from "@/components/tools/color-palette-settings";
import { QrGenerateSettings } from "@/components/tools/qr-generate-settings";
import { BarcodeReadSettings } from "@/components/tools/barcode-read-settings";
// Phase 3: Layout & Composition
import { CollageSettings } from "@/components/tools/collage-settings";
import { SplitSettings } from "@/components/tools/split-settings";
import { BorderSettings } from "@/components/tools/border-settings";
// Phase 3: Format & Conversion
import { SvgToRasterSettings } from "@/components/tools/svg-to-raster-settings";
import { VectorizeSettings } from "@/components/tools/vectorize-settings";
import { GifToolsSettings } from "@/components/tools/gif-tools-settings";
// Phase 3: Optimization extras
import { BulkRenameSettings } from "@/components/tools/bulk-rename-settings";
import { FaviconSettings } from "@/components/tools/favicon-settings";
import { ImageToPdfSettings } from "@/components/tools/image-to-pdf-settings";
// Phase 3: Adjustments extra
import { ReplaceColorSettings } from "@/components/tools/replace-color-settings";
import * as icons from "lucide-react";
const COLOR_TOOL_IDS = new Set([
@@ -21,7 +47,11 @@ const COLOR_TOOL_IDS = new Set([
"color-effects",
]);
// Tools that don't need a file dropzone (they generate content or have custom UI)
const NO_DROPZONE_TOOLS = new Set(["qr-generate"]);
function ToolSettingsPanel({ toolId }: { toolId: string }) {
// Phase 2: Core tools
if (toolId === "resize") return <ResizeSettings />;
if (toolId === "crop") return <CropSettings />;
if (toolId === "rotate") return <RotateSettings />;
@@ -29,6 +59,32 @@ function ToolSettingsPanel({ toolId }: { toolId: string }) {
if (toolId === "compress") return <CompressSettings />;
if (toolId === "strip-metadata") return <StripMetadataSettings />;
if (COLOR_TOOL_IDS.has(toolId)) return <ColorSettings toolId={toolId} />;
// Phase 3: Watermark & Overlay
if (toolId === "watermark-text") return <WatermarkTextSettings />;
if (toolId === "watermark-image") return <WatermarkImageSettings />;
if (toolId === "text-overlay") return <TextOverlaySettings />;
if (toolId === "compose") return <ComposeSettings />;
// Phase 3: Utilities
if (toolId === "info") return <InfoSettings />;
if (toolId === "compare") return <CompareSettings />;
if (toolId === "find-duplicates") return <FindDuplicatesSettings />;
if (toolId === "color-palette") return <ColorPaletteSettings />;
if (toolId === "qr-generate") return <QrGenerateSettings />;
if (toolId === "barcode-read") return <BarcodeReadSettings />;
// Phase 3: Layout & Composition
if (toolId === "collage") return <CollageSettings />;
if (toolId === "split") return <SplitSettings />;
if (toolId === "border") return <BorderSettings />;
// Phase 3: Format & Conversion
if (toolId === "svg-to-raster") return <SvgToRasterSettings />;
if (toolId === "vectorize") return <VectorizeSettings />;
if (toolId === "gif-tools") return <GifToolsSettings />;
// Phase 3: Optimization extras
if (toolId === "bulk-rename") return <BulkRenameSettings />;
if (toolId === "favicon") return <FaviconSettings />;
if (toolId === "image-to-pdf") return <ImageToPdfSettings />;
// Phase 3: Adjustments extra
if (toolId === "replace-color") return <ReplaceColorSettings />;
return (
<p className="text-xs text-muted-foreground italic">
@@ -69,6 +125,7 @@ export function ToolPage() {
)[tool.icon] || icons.FileImage;
const hasFile = files.length > 0;
const isNoDropzone = NO_DROPZONE_TOOLS.has(tool.id);
return (
<AppLayout showToolPanel={false}>
@@ -84,37 +141,39 @@ export function ToolPage() {
</h2>
</div>
{/* File info */}
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">
Files
</h3>
{hasFile ? (
<div className="space-y-1">
{files.map((f, i) => (
<div
key={i}
className="flex items-center justify-between text-xs text-foreground bg-muted rounded px-2 py-1"
{/* File info - hidden for tools that don't need files */}
{!isNoDropzone && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">
Files
</h3>
{hasFile ? (
<div className="space-y-1">
{files.map((f, i) => (
<div
key={i}
className="flex items-center justify-between text-xs text-foreground bg-muted rounded px-2 py-1"
>
<span className="truncate">{f.name}</span>
<span className="text-muted-foreground shrink-0 ml-2">
{(f.size / 1024).toFixed(0)} KB
</span>
</div>
))}
<button
onClick={() => reset()}
className="text-xs text-muted-foreground hover:text-foreground"
>
<span className="truncate">{f.name}</span>
<span className="text-muted-foreground shrink-0 ml-2">
{(f.size / 1024).toFixed(0)} KB
</span>
</div>
))}
<button
onClick={() => reset()}
className="text-xs text-muted-foreground hover:text-foreground"
>
Clear
</button>
</div>
) : (
<p className="text-xs text-muted-foreground italic">
Drop or upload an image to get started
</p>
)}
</div>
Clear
</button>
</div>
) : (
<p className="text-xs text-muted-foreground italic">
Drop or upload an image to get started
</p>
)}
</div>
)}
<div className="border-t border-border" />
@@ -129,7 +188,11 @@ export function ToolPage() {
{/* Dropzone / Preview */}
<div className="flex-1 flex items-center justify-center p-6">
{processedUrl && originalBlobUrl ? (
{isNoDropzone ? (
<div className="text-center text-muted-foreground">
<p className="text-sm">Configure settings in the panel and generate.</p>
</div>
) : processedUrl && originalBlobUrl ? (
<BeforeAfterSlider
beforeSrc={originalBlobUrl}
afterSrc={processedUrl}