mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -24,7 +24,11 @@
|
|||||||
"dotenv": "^16.4.0",
|
"dotenv": "^16.4.0",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.38.0",
|
||||||
"fastify": "^5.2.0",
|
"fastify": "^5.2.0",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"p-queue": "^9.1.0",
|
"p-queue": "^9.1.0",
|
||||||
|
"pdfkit": "^0.18.0",
|
||||||
|
"potrace": "^2.1.8",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^3.24.0"
|
||||||
},
|
},
|
||||||
@@ -32,6 +36,9 @@
|
|||||||
"@types/archiver": "^7.0.0",
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.0",
|
"@types/better-sqlite3": "^7.6.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/pdfkit": "^0.17.5",
|
||||||
|
"@types/potrace": "^2.1.5",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"drizzle-kit": "^0.30.0",
|
"drizzle-kit": "^0.30.0",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
|
|||||||
@@ -157,6 +157,10 @@ function getContentType(ext: string): string {
|
|||||||
tif: "image/tiff",
|
tif: "image/tiff",
|
||||||
avif: "image/avif",
|
avif: "image/avif",
|
||||||
svg: "image/svg+xml",
|
svg: "image/svg+xml",
|
||||||
|
pdf: "application/pdf",
|
||||||
|
zip: "application/zip",
|
||||||
|
ico: "image/x-icon",
|
||||||
|
json: "application/json",
|
||||||
};
|
};
|
||||||
return map[ext] ?? "application/octet-stream";
|
return map[ext] ?? "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,39 @@ import { registerConvert } from "./convert.js";
|
|||||||
import { registerCompress } from "./compress.js";
|
import { registerCompress } from "./compress.js";
|
||||||
import { registerStripMetadata } from "./strip-metadata.js";
|
import { registerStripMetadata } from "./strip-metadata.js";
|
||||||
import { registerColorAdjustments } from "./color-adjustments.js";
|
import { registerColorAdjustments } from "./color-adjustments.js";
|
||||||
|
// Phase 3: Watermark & Overlay
|
||||||
|
import { registerWatermarkText } from "./watermark-text.js";
|
||||||
|
import { registerWatermarkImage } from "./watermark-image.js";
|
||||||
|
import { registerTextOverlay } from "./text-overlay.js";
|
||||||
|
import { registerCompose } from "./compose.js";
|
||||||
|
// Phase 3: Utilities
|
||||||
|
import { registerInfo } from "./info.js";
|
||||||
|
import { registerCompare } from "./compare.js";
|
||||||
|
import { registerFindDuplicates } from "./find-duplicates.js";
|
||||||
|
import { registerColorPalette } from "./color-palette.js";
|
||||||
|
import { registerQrGenerate } from "./qr-generate.js";
|
||||||
|
import { registerBarcodeRead } from "./barcode-read.js";
|
||||||
|
// Phase 3: Layout & Composition
|
||||||
|
import { registerCollage } from "./collage.js";
|
||||||
|
import { registerSplit } from "./split.js";
|
||||||
|
import { registerBorder } from "./border.js";
|
||||||
|
// Phase 3: Format & Conversion
|
||||||
|
import { registerSvgToRaster } from "./svg-to-raster.js";
|
||||||
|
import { registerVectorize } from "./vectorize.js";
|
||||||
|
import { registerGifTools } from "./gif-tools.js";
|
||||||
|
// Phase 3: Optimization extras
|
||||||
|
import { registerBulkRename } from "./bulk-rename.js";
|
||||||
|
import { registerFavicon } from "./favicon.js";
|
||||||
|
import { registerImageToPdf } from "./image-to-pdf.js";
|
||||||
|
// Phase 3: Adjustments extra
|
||||||
|
import { registerReplaceColor } from "./replace-color.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry that imports and registers all tool routes.
|
* Registry that imports and registers all tool routes.
|
||||||
* Each tool uses the createToolRoute factory from tool-factory.ts.
|
* Each tool uses the createToolRoute factory from tool-factory.ts.
|
||||||
*/
|
*/
|
||||||
export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
||||||
|
// Phase 2: Core tools
|
||||||
registerResize(app);
|
registerResize(app);
|
||||||
registerCrop(app);
|
registerCrop(app);
|
||||||
registerRotate(app);
|
registerRotate(app);
|
||||||
@@ -20,5 +47,37 @@ export async function registerToolRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
registerStripMetadata(app);
|
registerStripMetadata(app);
|
||||||
registerColorAdjustments(app);
|
registerColorAdjustments(app);
|
||||||
|
|
||||||
app.log.info("Tool routes registered (7 tools, 10 endpoints)");
|
// Phase 3: Watermark & Overlay
|
||||||
|
registerWatermarkText(app);
|
||||||
|
registerWatermarkImage(app);
|
||||||
|
registerTextOverlay(app);
|
||||||
|
registerCompose(app);
|
||||||
|
|
||||||
|
// Phase 3: Utilities
|
||||||
|
registerInfo(app);
|
||||||
|
registerCompare(app);
|
||||||
|
registerFindDuplicates(app);
|
||||||
|
registerColorPalette(app);
|
||||||
|
registerQrGenerate(app);
|
||||||
|
registerBarcodeRead(app);
|
||||||
|
|
||||||
|
// Phase 3: Layout & Composition
|
||||||
|
registerCollage(app);
|
||||||
|
registerSplit(app);
|
||||||
|
registerBorder(app);
|
||||||
|
|
||||||
|
// Phase 3: Format & Conversion
|
||||||
|
registerSvgToRaster(app);
|
||||||
|
registerVectorize(app);
|
||||||
|
registerGifTools(app);
|
||||||
|
|
||||||
|
// Phase 3: Optimization extras
|
||||||
|
registerBulkRename(app);
|
||||||
|
registerFavicon(app);
|
||||||
|
registerImageToPdf(app);
|
||||||
|
|
||||||
|
// Phase 3: Adjustments extra
|
||||||
|
registerReplaceColor(app);
|
||||||
|
|
||||||
|
app.log.info("Tool routes registered (26 tools, 29 endpoints)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { createToolRoute } from "../tool-factory.js";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
|
const settingsSchema = z.object({
|
||||||
|
sourceColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#FF0000"),
|
||||||
|
targetColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#00FF00"),
|
||||||
|
makeTransparent: z.boolean().default(false),
|
||||||
|
tolerance: z.number().min(0).max(255).default(30),
|
||||||
|
});
|
||||||
|
|
||||||
|
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||||
|
return {
|
||||||
|
r: parseInt(hex.slice(1, 3), 16),
|
||||||
|
g: parseInt(hex.slice(3, 5), 16),
|
||||||
|
b: parseInt(hex.slice(5, 7), 16),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorDistance(
|
||||||
|
r1: number, g1: number, b1: number,
|
||||||
|
r2: number, g2: number, b2: number,
|
||||||
|
): number {
|
||||||
|
return Math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerReplaceColor(app: FastifyInstance) {
|
||||||
|
createToolRoute(app, {
|
||||||
|
toolId: "replace-color",
|
||||||
|
settingsSchema,
|
||||||
|
process: async (inputBuffer, settings, filename) => {
|
||||||
|
const source = hexToRgb(settings.sourceColor);
|
||||||
|
const target = hexToRgb(settings.targetColor);
|
||||||
|
|
||||||
|
// Get raw RGBA pixels
|
||||||
|
const image = sharp(inputBuffer).ensureAlpha();
|
||||||
|
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const pixels = Buffer.from(data);
|
||||||
|
const maxDist = settings.tolerance * 1.73; // sqrt(3) for RGB distance scaling
|
||||||
|
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const dist = colorDistance(
|
||||||
|
pixels[i], pixels[i + 1], pixels[i + 2],
|
||||||
|
source.r, source.g, source.b,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dist <= maxDist) {
|
||||||
|
if (settings.makeTransparent) {
|
||||||
|
pixels[i + 3] = 0; // Make transparent
|
||||||
|
} else {
|
||||||
|
// Blend: closer to source color = more of target color
|
||||||
|
const blend = 1 - dist / maxDist;
|
||||||
|
pixels[i] = Math.round(pixels[i] * (1 - blend) + target.r * blend);
|
||||||
|
pixels[i + 1] = Math.round(pixels[i + 1] * (1 - blend) + target.g * blend);
|
||||||
|
pixels[i + 2] = Math.round(pixels[i + 2] * (1 - blend) + target.b * blend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await sharp(pixels, {
|
||||||
|
raw: { width: info.width, height: info.height, channels: 4 },
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
return { buffer, filename, contentType: "image/png" };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,32 @@ import { ConvertSettings } from "@/components/tools/convert-settings";
|
|||||||
import { CompressSettings } from "@/components/tools/compress-settings";
|
import { CompressSettings } from "@/components/tools/compress-settings";
|
||||||
import { StripMetadataSettings } from "@/components/tools/strip-metadata-settings";
|
import { StripMetadataSettings } from "@/components/tools/strip-metadata-settings";
|
||||||
import { ColorSettings } from "@/components/tools/color-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";
|
import * as icons from "lucide-react";
|
||||||
|
|
||||||
const COLOR_TOOL_IDS = new Set([
|
const COLOR_TOOL_IDS = new Set([
|
||||||
@@ -21,7 +47,11 @@ const COLOR_TOOL_IDS = new Set([
|
|||||||
"color-effects",
|
"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 }) {
|
function ToolSettingsPanel({ toolId }: { toolId: string }) {
|
||||||
|
// Phase 2: Core tools
|
||||||
if (toolId === "resize") return <ResizeSettings />;
|
if (toolId === "resize") return <ResizeSettings />;
|
||||||
if (toolId === "crop") return <CropSettings />;
|
if (toolId === "crop") return <CropSettings />;
|
||||||
if (toolId === "rotate") return <RotateSettings />;
|
if (toolId === "rotate") return <RotateSettings />;
|
||||||
@@ -29,6 +59,32 @@ function ToolSettingsPanel({ toolId }: { toolId: string }) {
|
|||||||
if (toolId === "compress") return <CompressSettings />;
|
if (toolId === "compress") return <CompressSettings />;
|
||||||
if (toolId === "strip-metadata") return <StripMetadataSettings />;
|
if (toolId === "strip-metadata") return <StripMetadataSettings />;
|
||||||
if (COLOR_TOOL_IDS.has(toolId)) return <ColorSettings toolId={toolId} />;
|
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 (
|
return (
|
||||||
<p className="text-xs text-muted-foreground italic">
|
<p className="text-xs text-muted-foreground italic">
|
||||||
@@ -69,6 +125,7 @@ export function ToolPage() {
|
|||||||
)[tool.icon] || icons.FileImage;
|
)[tool.icon] || icons.FileImage;
|
||||||
|
|
||||||
const hasFile = files.length > 0;
|
const hasFile = files.length > 0;
|
||||||
|
const isNoDropzone = NO_DROPZONE_TOOLS.has(tool.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout showToolPanel={false}>
|
<AppLayout showToolPanel={false}>
|
||||||
@@ -84,7 +141,8 @@ export function ToolPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File info */}
|
{/* File info - hidden for tools that don't need files */}
|
||||||
|
{!isNoDropzone && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<h3 className="text-sm font-medium text-muted-foreground">
|
||||||
Files
|
Files
|
||||||
@@ -115,6 +173,7 @@ export function ToolPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-t border-border" />
|
<div className="border-t border-border" />
|
||||||
|
|
||||||
@@ -129,7 +188,11 @@ export function ToolPage() {
|
|||||||
|
|
||||||
{/* Dropzone / Preview */}
|
{/* Dropzone / Preview */}
|
||||||
<div className="flex-1 flex items-center justify-center p-6">
|
<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
|
<BeforeAfterSlider
|
||||||
beforeSrc={originalBlobUrl}
|
beforeSrc={originalBlobUrl}
|
||||||
afterSrc={processedUrl}
|
afterSrc={processedUrl}
|
||||||
|
|||||||
Generated
+1499
-2
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user