Files
SnapOtter/apps/web/src/lib/tool-registry.tsx
T
58cdbe50b4 feat: SOTA sharpening tool with 3 methods and 7 presets (#56)
* feat(sharpening): add SharpenAdvancedOptions type for multi-method sharpening

* feat(sharpening): implement 3-method sharpen engine (adaptive, USM, high-pass)

* feat(sharpening): add dedicated API route with Zod validation

* feat(sharpening): register tool in constants, i18n, and suggested tools

* feat(sharpening): add settings UI with presets, methods, and advanced controls

* feat(sharpening): register tool in frontend registry with before-after display

---------

Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
2026-04-13 19:12:09 +08:00

380 lines
14 KiB
TypeScript

/**
* Tool UI registry.
*
* Maps each toolId to its settings component, display mode, and capabilities.
* Adding a new tool means adding one entry here instead of editing a 750-line file.
*/
import type React from "react";
import { lazy } from "react";
import type { Crop } from "react-image-crop";
import type { BgPreviewState } from "@/components/common/image-viewer";
import type { EraserCanvasRef } from "@/components/tools/eraser-canvas";
import type { PreviewTransform } from "@/components/tools/rotate-settings";
// ── Display modes ──────────────────────────────────────────────────
export type DisplayMode =
| "side-by-side"
| "before-after"
| "live-preview"
| "no-comparison"
| "interactive-crop"
| "interactive-eraser"
| "interactive-split"
| "no-dropzone"
| "custom-results";
// ── Crop and eraser prop types ─────────────────────────────────────
export interface CropProps {
cropState: {
crop: Crop;
aspect: number | undefined;
showGrid: boolean;
imgDimensions: { width: number; height: number } | null;
};
onCropChange: (crop: Crop) => void;
onAspectChange: (aspect: number | undefined) => void;
onGridToggle: (show: boolean) => void;
}
export interface EraserProps {
eraserRef: React.RefObject<EraserCanvasRef | null>;
hasStrokes: boolean;
brushSize: number;
onBrushSizeChange: (size: number) => void;
}
// ── Registry entry ─────────────────────────────────────────────────
export interface ToolRegistryEntry {
/** The display mode for this tool's image viewer. */
displayMode: DisplayMode;
/** Whether this tool supports live preview transforms (rotate, color). */
livePreview?: boolean;
/** The settings component for this tool. */
Settings: React.ComponentType<{
onPreviewTransform?: (t: PreviewTransform) => void;
onPreviewFilter?: (filter: string) => void;
onBgPreview?: (state: BgPreviewState | null) => void;
onImageStyle?: (style: React.CSSProperties | null) => void;
cropProps?: CropProps;
eraserProps?: EraserProps;
}>;
/** Optional panel for tools that render custom content in the main area. */
ResultsPanel?: React.ComponentType;
}
// ── Lazy-loaded settings components ────────────────────────────────
// Using dynamic imports so the bundle only loads what's needed.
const ResizeSettings = lazy(() =>
import("@/components/tools/resize-settings").then((m) => ({ default: m.ResizeSettings })),
);
const CropSettings = lazy(() =>
import("@/components/tools/crop-settings").then((m) => ({ default: m.CropSettings })),
);
const RotateSettings = lazy(() =>
import("@/components/tools/rotate-settings").then((m) => ({ default: m.RotateSettings })),
);
const ConvertSettings = lazy(() =>
import("@/components/tools/convert-settings").then((m) => ({ default: m.ConvertSettings })),
);
const CompressSettings = lazy(() =>
import("@/components/tools/compress-settings").then((m) => ({ default: m.CompressSettings })),
);
const StripMetadataSettings = lazy(() =>
import("@/components/tools/strip-metadata-settings").then((m) => ({
default: m.StripMetadataSettings,
})),
);
const EditMetadataSettings = lazy(() =>
import("@/components/tools/edit-metadata-settings").then((m) => ({
default: m.EditMetadataSettings,
})),
);
const ColorSettings = lazy(() =>
import("@/components/tools/color-settings").then((m) => ({ default: m.ColorSettings })),
);
const SharpeningSettings = lazy(() =>
import("@/components/tools/sharpening-settings").then((m) => ({
default: m.SharpeningSettings,
})),
);
const WatermarkTextSettings = lazy(() =>
import("@/components/tools/watermark-text-settings").then((m) => ({
default: m.WatermarkTextSettings,
})),
);
const WatermarkImageSettings = lazy(() =>
import("@/components/tools/watermark-image-settings").then((m) => ({
default: m.WatermarkImageSettings,
})),
);
const TextOverlaySettings = lazy(() =>
import("@/components/tools/text-overlay-settings").then((m) => ({
default: m.TextOverlaySettings,
})),
);
const ComposeSettings = lazy(() =>
import("@/components/tools/compose-settings").then((m) => ({ default: m.ComposeSettings })),
);
const InfoSettings = lazy(() =>
import("@/components/tools/info-settings").then((m) => ({ default: m.InfoSettings })),
);
const CompareSettings = lazy(() =>
import("@/components/tools/compare-settings").then((m) => ({ default: m.CompareSettings })),
);
const FindDuplicatesSettings = lazy(() =>
import("@/components/tools/find-duplicates-settings").then((m) => ({
default: m.FindDuplicatesSettings,
})),
);
const FindDuplicatesResults = lazy(() =>
import("@/components/tools/find-duplicates-results").then((m) => ({
default: m.FindDuplicatesResults,
})),
);
const ColorPaletteSettings = lazy(() =>
import("@/components/tools/color-palette-settings").then((m) => ({
default: m.ColorPaletteSettings,
})),
);
const QrGenerateSettings = lazy(() =>
import("@/components/tools/qr-generate-settings").then((m) => ({
default: m.QrGenerateSettings,
})),
);
const QrGeneratePreview = lazy(() =>
import("@/components/tools/qr-generate-preview").then((m) => ({
default: m.QrGeneratePreview,
})),
);
const BarcodeReadSettings = lazy(() =>
import("@/components/tools/barcode-read-settings").then((m) => ({
default: m.BarcodeReadSettings,
})),
);
const CollageSettings = lazy(() =>
import("@/components/tools/collage-settings").then((m) => ({ default: m.CollageSettings })),
);
const StitchSettings = lazy(() =>
import("@/components/tools/stitch-settings").then((m) => ({ default: m.StitchSettings })),
);
const SplitSettings = lazy(() =>
import("@/components/tools/split-settings").then((m) => ({ default: m.SplitSettings })),
);
const SplitCanvas = lazy(() =>
import("@/components/tools/split-canvas").then((m) => ({ default: m.SplitCanvas })),
);
const BorderSettings = lazy(() =>
import("@/components/tools/border-settings").then((m) => ({ default: m.BorderSettings })),
);
const SvgToRasterSettings = lazy(() =>
import("@/components/tools/svg-to-raster-settings").then((m) => ({
default: m.SvgToRasterSettings,
})),
);
const VectorizeSettings = lazy(() =>
import("@/components/tools/vectorize-settings").then((m) => ({
default: m.VectorizeSettings,
})),
);
const GifToolsSettings = lazy(() =>
import("@/components/tools/gif-tools-settings").then((m) => ({
default: m.GifToolsSettings,
})),
);
const BulkRenameSettings = lazy(() =>
import("@/components/tools/bulk-rename-settings").then((m) => ({
default: m.BulkRenameSettings,
})),
);
const FaviconSettings = lazy(() =>
import("@/components/tools/favicon-settings").then((m) => ({ default: m.FaviconSettings })),
);
const ImageToPdfSettings = lazy(() =>
import("@/components/tools/image-to-pdf-settings").then((m) => ({
default: m.ImageToPdfSettings,
})),
);
const PdfToImageSettings = lazy(() =>
import("@/components/tools/pdf-to-image-settings").then((m) => ({
default: m.PdfToImageSettings,
})),
);
const PdfToImagePreview = lazy(() =>
import("@/components/tools/pdf-to-image-preview").then((m) => ({
default: m.PdfToImagePreview,
})),
);
const ReplaceColorSettings = lazy(() =>
import("@/components/tools/replace-color-settings").then((m) => ({
default: m.ReplaceColorSettings,
})),
);
const RemoveBgSettings = lazy(() =>
import("@/components/tools/remove-bg-settings").then((m) => ({
default: m.RemoveBgSettings,
})),
);
const UpscaleSettings = lazy(() =>
import("@/components/tools/upscale-settings").then((m) => ({ default: m.UpscaleSettings })),
);
const OcrSettings = lazy(() =>
import("@/components/tools/ocr-settings").then((m) => ({ default: m.OcrSettings })),
);
const BlurFacesSettings = lazy(() =>
import("@/components/tools/blur-faces-settings").then((m) => ({
default: m.BlurFacesSettings,
})),
);
const EraseObjectSettings = lazy(() =>
import("@/components/tools/erase-object-settings").then((m) => ({
default: m.EraseObjectSettings,
})),
);
const SmartCropSettings = lazy(() =>
import("@/components/tools/smart-crop-settings").then((m) => ({
default: m.SmartCropSettings,
})),
);
const ImageEnhancementSettings = lazy(() =>
import("@/components/tools/image-enhancement-settings").then((m) => ({
default: m.ImageEnhancementSettings,
})),
);
// ── Color tool wrapper ─────────────────────────────────────────────
// Color tools share a single component but differ by toolId.
function makeColorSettingsComponent(
toolId: string,
): React.ComponentType<{ onPreviewFilter?: (filter: string) => void }> {
return function ColorSettingsForTool(props: { onPreviewFilter?: (filter: string) => void }) {
return <ColorSettings toolId={toolId} onPreviewFilter={props.onPreviewFilter} />;
};
}
// ── Crop/Eraser wrappers ───────────────────────────────────────────
// These tools need special props that are passed through the registry.
function CropSettingsWrapper(props: { cropProps?: CropProps }) {
if (!props.cropProps) return null;
return <CropSettings {...props.cropProps} />;
}
function EraseObjectSettingsWrapper(props: { eraserProps?: EraserProps }) {
if (!props.eraserProps) return null;
return <EraseObjectSettings {...props.eraserProps} />;
}
// ── The registry ───────────────────────────────────────────────────
export const toolRegistry = new Map<string, ToolRegistryEntry>([
// Essentials
["resize", { displayMode: "side-by-side", Settings: ResizeSettings }],
["crop", { displayMode: "interactive-crop", Settings: CropSettingsWrapper as never }],
[
"rotate",
{
displayMode: "side-by-side",
livePreview: true,
Settings: RotateSettings as never,
},
],
["convert", { displayMode: "no-comparison", Settings: ConvertSettings }],
["compress", { displayMode: "before-after", Settings: CompressSettings }],
["strip-metadata", { displayMode: "no-comparison", Settings: StripMetadataSettings }],
["edit-metadata", { displayMode: "no-comparison", Settings: EditMetadataSettings }],
// Color adjustments (consolidated)
[
"adjust-colors",
{
displayMode: "live-preview" as DisplayMode,
livePreview: true,
Settings: makeColorSettingsComponent("adjust-colors") as never,
},
],
// Sharpening
["sharpening", { displayMode: "before-after", Settings: SharpeningSettings }],
// Watermark & Overlay
["watermark-text", { displayMode: "before-after", Settings: WatermarkTextSettings }],
["watermark-image", { displayMode: "before-after", Settings: WatermarkImageSettings }],
["text-overlay", { displayMode: "before-after", Settings: TextOverlaySettings }],
["compose", { displayMode: "before-after", Settings: ComposeSettings }],
// Utilities
["info", { displayMode: "before-after", Settings: InfoSettings }],
["compare", { displayMode: "before-after", Settings: CompareSettings }],
[
"find-duplicates",
{
displayMode: "custom-results",
Settings: FindDuplicatesSettings,
ResultsPanel: FindDuplicatesResults,
},
],
["color-palette", { displayMode: "before-after", Settings: ColorPaletteSettings }],
[
"qr-generate",
{ displayMode: "no-dropzone", Settings: QrGenerateSettings, ResultsPanel: QrGeneratePreview },
],
["barcode-read", { displayMode: "before-after", Settings: BarcodeReadSettings }],
// Layout & Composition
["collage", { displayMode: "before-after", Settings: CollageSettings }],
["stitch", { displayMode: "no-comparison", Settings: StitchSettings }],
[
"split",
{ displayMode: "interactive-split", Settings: SplitSettings, ResultsPanel: SplitCanvas },
],
["border", { displayMode: "live-preview", livePreview: true, Settings: BorderSettings as never }],
// Format & Conversion
["svg-to-raster", { displayMode: "before-after", Settings: SvgToRasterSettings }],
["vectorize", { displayMode: "before-after", Settings: VectorizeSettings }],
["gif-tools", { displayMode: "before-after", Settings: GifToolsSettings }],
// Optimization extras
["bulk-rename", { displayMode: "before-after", Settings: BulkRenameSettings }],
["favicon", { displayMode: "before-after", Settings: FaviconSettings }],
["image-to-pdf", { displayMode: "before-after", Settings: ImageToPdfSettings }],
[
"pdf-to-image",
{ displayMode: "no-dropzone", Settings: PdfToImageSettings, ResultsPanel: PdfToImagePreview },
],
// Adjustments extra
["replace-color", { displayMode: "before-after", Settings: ReplaceColorSettings }],
// AI Tools
["remove-background", { displayMode: "before-after", Settings: RemoveBgSettings }],
["upscale", { displayMode: "before-after", Settings: UpscaleSettings }],
["ocr", { displayMode: "before-after", Settings: OcrSettings }],
["blur-faces", { displayMode: "before-after", Settings: BlurFacesSettings }],
[
"erase-object",
{
displayMode: "interactive-eraser",
Settings: EraseObjectSettingsWrapper as never,
},
],
["smart-crop", { displayMode: "before-after", Settings: SmartCropSettings }],
[
"image-enhancement",
{
displayMode: "live-preview" as DisplayMode,
livePreview: true,
Settings: ImageEnhancementSettings as never,
},
],
]);
export function getToolRegistryEntry(toolId: string): ToolRegistryEntry | undefined {
return toolRegistry.get(toolId);
}