mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
merge: resolve conflict with main branch in tool-registry.tsx
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ const OUTPUT_FORMATS: { value: OutputFormat; label: string }[] = [
|
||||
{ value: "jpeg", label: "JPEG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
{ value: "jxl", label: "JXL" },
|
||||
];
|
||||
|
||||
const BG_PRESETS = [
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
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 SIMULATION_TYPES = [
|
||||
{
|
||||
group: "Red-Green",
|
||||
types: [
|
||||
{
|
||||
value: "protanopia",
|
||||
label: "Protanopia",
|
||||
description: "No red cones. Reds appear dark. ~1% of males.",
|
||||
},
|
||||
{
|
||||
value: "protanomaly",
|
||||
label: "Protanomaly",
|
||||
description: "Reduced red sensitivity. ~1% of males.",
|
||||
},
|
||||
{
|
||||
value: "deuteranopia",
|
||||
label: "Deuteranopia",
|
||||
description: "No green cones. Reds and greens look similar. ~1% of males.",
|
||||
},
|
||||
{
|
||||
value: "deuteranomaly",
|
||||
label: "Deuteranomaly",
|
||||
description: "Reduced green sensitivity. Most common type. ~5% of males.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Blue-Yellow",
|
||||
types: [
|
||||
{
|
||||
value: "tritanopia",
|
||||
label: "Tritanopia",
|
||||
description: "No blue cones. Blues and greens look similar. Very rare.",
|
||||
},
|
||||
{
|
||||
value: "tritanomaly",
|
||||
label: "Tritanomaly",
|
||||
description: "Reduced blue sensitivity. Very rare.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Monochromatic",
|
||||
types: [
|
||||
{
|
||||
value: "achromatopsia",
|
||||
label: "Achromatopsia",
|
||||
description: "Complete color blindness. Sees only luminance. Very rare.",
|
||||
},
|
||||
{
|
||||
value: "blueConeMonochromacy",
|
||||
label: "Blue Cone Monochromacy",
|
||||
description: "Only blue cones functional. Very limited color. Very rare.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const TYPE_MAP = new Map(SIMULATION_TYPES.flatMap((g) => g.types.map((t) => [t.value, t])));
|
||||
|
||||
export function ColorBlindnessSettings() {
|
||||
const { files } = useFileStore();
|
||||
const {
|
||||
processFiles,
|
||||
processAllFiles,
|
||||
processing,
|
||||
error,
|
||||
downloadUrl,
|
||||
originalSize,
|
||||
processedSize,
|
||||
progress,
|
||||
} = useToolProcessor("color-blindness");
|
||||
|
||||
const [simulationType, setSimulationType] = useState("deuteranomaly");
|
||||
const selectedInfo = TYPE_MAP.get(simulationType);
|
||||
|
||||
const handleProcess = () => {
|
||||
const settings = { simulationType };
|
||||
if (files.length > 1) {
|
||||
processAllFiles(files, settings);
|
||||
} else {
|
||||
processFiles(files, settings);
|
||||
}
|
||||
};
|
||||
|
||||
const hasFile = files.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="cb-simulation-type" className="text-xs text-muted-foreground">
|
||||
Simulation Type
|
||||
</label>
|
||||
<select
|
||||
id="cb-simulation-type"
|
||||
value={simulationType}
|
||||
onChange={(e) => setSimulationType(e.target.value)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
{SIMULATION_TYPES.map((group) => (
|
||||
<optgroup key={group.group} label={group.group}>
|
||||
{group.types.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
{selectedInfo && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{selectedInfo.description}</p>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{processing ? (
|
||||
<ProgressCard
|
||||
active={processing}
|
||||
phase={progress.phase === "idle" ? "uploading" : progress.phase}
|
||||
label="Simulating color blindness"
|
||||
stage={progress.stage}
|
||||
percent={progress.percent}
|
||||
elapsed={progress.elapsed}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="color-blindness-submit"
|
||||
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"
|
||||
>
|
||||
{files.length > 1 ? `Simulate (${files.length} files)` : "Simulate"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{downloadUrl && (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
data-testid="color-blindness-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>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,38 @@ 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", "heic", "heif"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
|
||||
const OUTPUT_FORMATS = [
|
||||
"jpg",
|
||||
"png",
|
||||
"webp",
|
||||
"avif",
|
||||
"tiff",
|
||||
"gif",
|
||||
"heic",
|
||||
"heif",
|
||||
"jxl",
|
||||
"bmp",
|
||||
"ico",
|
||||
"jp2",
|
||||
"qoi",
|
||||
] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif", "jxl", "jp2"];
|
||||
|
||||
const FORMAT_LABELS: Record<string, string> = {
|
||||
jpg: "JPG",
|
||||
png: "PNG",
|
||||
webp: "WebP",
|
||||
avif: "AVIF",
|
||||
tiff: "TIFF",
|
||||
gif: "GIF",
|
||||
heic: "HEIC",
|
||||
heif: "HEIF",
|
||||
jxl: "JXL",
|
||||
bmp: "BMP",
|
||||
ico: "ICO",
|
||||
jp2: "JP2",
|
||||
qoi: "QOI",
|
||||
};
|
||||
|
||||
export interface ConvertControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
@@ -54,7 +84,7 @@ export function ConvertControls({ settings: initialSettings, onChange }: Convert
|
||||
>
|
||||
{OUTPUT_FORMATS.map((f) => (
|
||||
<option key={f} value={f}>
|
||||
{f.toUpperCase()}
|
||||
{FORMAT_LABELS[f] ?? f.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -6,8 +6,18 @@ import { generateId } from "@/lib/utils";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
import type { EraserCanvasRef } from "./eraser-canvas";
|
||||
|
||||
const OUTPUT_FORMATS = ["png", "jpg", "webp", "avif", "tiff", "gif", "heic", "heif"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
|
||||
const OUTPUT_FORMATS = [
|
||||
"png",
|
||||
"jpg",
|
||||
"webp",
|
||||
"avif",
|
||||
"tiff",
|
||||
"gif",
|
||||
"heic",
|
||||
"heif",
|
||||
"jxl",
|
||||
] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif", "jxl"];
|
||||
|
||||
interface EraseObjectSettingsProps {
|
||||
eraserRef: React.RefObject<EraserCanvasRef | null>;
|
||||
|
||||
@@ -10,6 +10,7 @@ const OUTPUT_FORMATS = [
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
{ value: "jxl", label: "JXL" },
|
||||
] as const;
|
||||
|
||||
export function ImageToBase64Settings() {
|
||||
@@ -69,7 +70,11 @@ export function ImageToBase64Settings() {
|
||||
};
|
||||
|
||||
const hasFiles = files.length > 0;
|
||||
const showQuality = outputFormat === "jpeg" || outputFormat === "webp" || outputFormat === "avif";
|
||||
const showQuality =
|
||||
outputFormat === "jpeg" ||
|
||||
outputFormat === "webp" ||
|
||||
outputFormat === "avif" ||
|
||||
outputFormat === "jxl";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -13,7 +13,7 @@ const TIERS: { id: Tier; label: string; desc: string }[] = [
|
||||
{ id: "maximum", label: "Maximum", desc: "Best AI model, slowest" },
|
||||
];
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif"]);
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif", "jxl"]);
|
||||
|
||||
export interface NoiseRemovalControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
@@ -28,9 +28,9 @@ export function NoiseRemovalControls({
|
||||
const [strength, setStrength] = useState(50);
|
||||
const [detailPreservation, setDetailPreservation] = useState(50);
|
||||
const [colorNoise, setColorNoise] = useState(30);
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp" | "avif">(
|
||||
"original",
|
||||
);
|
||||
const [outputFormat, setOutputFormat] = useState<
|
||||
"original" | "png" | "jpeg" | "webp" | "avif" | "jxl"
|
||||
>("original");
|
||||
const [quality, setQuality] = useState(90);
|
||||
|
||||
// One-time init from pipeline settings
|
||||
@@ -44,7 +44,9 @@ export function NoiseRemovalControls({
|
||||
setDetailPreservation(Number(initialSettings.detailPreservation));
|
||||
if (initialSettings.colorNoise != null) setColorNoise(Number(initialSettings.colorNoise));
|
||||
if (initialSettings.format != null)
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif");
|
||||
setOutputFormat(
|
||||
initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif" | "jxl",
|
||||
);
|
||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||
}, [initialSettings]);
|
||||
|
||||
@@ -164,8 +166,8 @@ export function NoiseRemovalControls({
|
||||
{/* Output format */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Output Format</p>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif", "jxl"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type WebFormat = "webp" | "jpeg" | "avif" | "png";
|
||||
type WebFormat = "webp" | "jpeg" | "avif" | "png" | "jxl";
|
||||
|
||||
interface PreviewState {
|
||||
loading: boolean;
|
||||
@@ -19,6 +19,7 @@ const FORMAT_LABELS: Record<WebFormat, string> = {
|
||||
jpeg: "JPEG",
|
||||
avif: "AVIF",
|
||||
png: "PNG",
|
||||
jxl: "JXL",
|
||||
};
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
@@ -200,8 +201,8 @@ export function OptimizeForWebSettings() {
|
||||
{/* Format selector */}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Output Format</p>
|
||||
<div className="grid grid-cols-4 gap-1 mt-1">
|
||||
{(["webp", "jpeg", "avif", "png"] as const).map((f) => (
|
||||
<div className="grid grid-cols-5 gap-1 mt-1">
|
||||
{(["webp", "jpeg", "avif", "png", "jxl"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
|
||||
@@ -11,6 +11,7 @@ const FORMAT_OPTIONS = [
|
||||
{ value: "gif", label: "GIF" },
|
||||
{ value: "heic", label: "HEIC" },
|
||||
{ value: "heif", label: "HEIF" },
|
||||
{ value: "jxl", label: "JXL" },
|
||||
];
|
||||
|
||||
const DPI_PRESETS = [
|
||||
@@ -33,7 +34,7 @@ const COLOR_MODE_OPTIONS = [
|
||||
{ value: "bw", label: "B&W" },
|
||||
] as const;
|
||||
|
||||
const LOSSY_FORMATS = ["jpg", "webp", "avif", "heic", "heif"];
|
||||
const LOSSY_FORMATS = ["jpg", "webp", "avif", "heic", "heif", "jxl"];
|
||||
|
||||
export function PdfToImageSettings() {
|
||||
const store = usePdfToImageStore();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif"]);
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif", "jxl"]);
|
||||
|
||||
export interface RedEyeRemovalControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
@@ -17,9 +17,9 @@ export function RedEyeRemovalControls({
|
||||
}: RedEyeRemovalControlsProps) {
|
||||
const [sensitivity, setSensitivity] = useState(50);
|
||||
const [strength, setStrength] = useState(70);
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp" | "avif">(
|
||||
"original",
|
||||
);
|
||||
const [outputFormat, setOutputFormat] = useState<
|
||||
"original" | "png" | "jpeg" | "webp" | "avif" | "jxl"
|
||||
>("original");
|
||||
const [quality, setQuality] = useState(90);
|
||||
|
||||
// One-time init from pipeline settings
|
||||
@@ -30,7 +30,9 @@ export function RedEyeRemovalControls({
|
||||
if (initialSettings.sensitivity != null) setSensitivity(Number(initialSettings.sensitivity));
|
||||
if (initialSettings.strength != null) setStrength(Number(initialSettings.strength));
|
||||
if (initialSettings.format != null)
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif");
|
||||
setOutputFormat(
|
||||
initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif" | "jxl",
|
||||
);
|
||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||
}, [initialSettings]);
|
||||
|
||||
@@ -101,8 +103,8 @@ export function RedEyeRemovalControls({
|
||||
{/* Output format */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Output Format</p>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif", "jxl"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
|
||||
@@ -29,9 +29,10 @@ const OUTPUT_FORMATS = [
|
||||
{ value: "jpg", label: "JPG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
{ value: "jxl", label: "JXL" },
|
||||
] as const;
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif"]);
|
||||
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif", "jxl"]);
|
||||
|
||||
export function SplitSettings() {
|
||||
const { files, processing: fileStoreProcessing } = useFileStore();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useFileStore } from "@/stores/file-store";
|
||||
type Direction = "horizontal" | "vertical" | "grid";
|
||||
type ResizeMode = "fit" | "original" | "stretch" | "crop";
|
||||
type Alignment = "start" | "center" | "end";
|
||||
type OutputFormat = "png" | "jpeg" | "webp" | "avif";
|
||||
type OutputFormat = "png" | "jpeg" | "webp" | "avif" | "jxl";
|
||||
|
||||
export function StitchSettings() {
|
||||
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
|
||||
@@ -227,7 +227,7 @@ export function StitchSettings() {
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Format</p>
|
||||
<div className="grid grid-cols-3 gap-1 mt-1">
|
||||
{(["png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
{(["png", "jpeg", "webp", "avif", "jxl"] as const).map((f) => (
|
||||
<button
|
||||
type="button"
|
||||
key={f}
|
||||
@@ -240,7 +240,7 @@ export function StitchSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(format === "jpeg" || format === "webp" || format === "avif") && (
|
||||
{(format === "jpeg" || format === "webp" || format === "avif" || format === "jxl") && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<label htmlFor="stitch-quality" className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -4,12 +4,12 @@ import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
type OutputFormat = "png" | "jpg" | "webp" | "avif" | "tiff" | "gif" | "heif";
|
||||
type OutputFormat = "png" | "jpg" | "webp" | "avif" | "tiff" | "gif" | "heif" | "jxl";
|
||||
type SizingMode = "scale" | "custom";
|
||||
type BgMode = "transparent" | "color";
|
||||
|
||||
const FORMATS: OutputFormat[] = ["png", "jpg", "webp", "avif", "tiff", "gif", "heif"];
|
||||
const LOSSY_FORMATS: OutputFormat[] = ["jpg", "webp", "avif", "heif"];
|
||||
const FORMATS: OutputFormat[] = ["png", "jpg", "webp", "avif", "tiff", "gif", "heif", "jxl"];
|
||||
const LOSSY_FORMATS: OutputFormat[] = ["jpg", "webp", "avif", "heif", "jxl"];
|
||||
const NO_TRANSPARENCY_FORMATS: OutputFormat[] = ["jpg", "tiff"];
|
||||
|
||||
const SCALE_PRESETS = [0.5, 1, 2, 3, 4];
|
||||
|
||||
@@ -10,8 +10,18 @@ const MODEL_OPTIONS = [
|
||||
{ value: "auto", label: "Balanced" },
|
||||
{ value: "realesrgan", label: "Best" },
|
||||
] as const;
|
||||
const OUTPUT_FORMATS = ["png", "jpg", "webp", "avif", "tiff", "gif", "heic", "heif"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
|
||||
const OUTPUT_FORMATS = [
|
||||
"png",
|
||||
"jpg",
|
||||
"webp",
|
||||
"avif",
|
||||
"tiff",
|
||||
"gif",
|
||||
"heic",
|
||||
"heif",
|
||||
"jxl",
|
||||
] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif", "jxl"];
|
||||
|
||||
export interface UpscaleControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user