Files
SnapOtter/apps/web/src/components/tools/compress-settings.tsx
T
SnapOtter 5b868886ba fix: make target size the primary compression mode
Move Target Size button to the left and default to it on load, since
it is the more useful mode for most users.
2026-05-11 16:33:10 +08:00

192 lines
6.2 KiB
TypeScript

import { Download } 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";
type CompressMode = "quality" | "targetSize";
export interface CompressControlsProps {
settings?: Record<string, unknown>;
onChange?: (settings: Record<string, unknown>) => void;
}
export function CompressControls({ settings: initialSettings, onChange }: CompressControlsProps) {
const [mode, setMode] = useState<CompressMode>("targetSize");
const [quality, setQuality] = useState(75);
const [targetSizeKb, setTargetSizeKb] = useState("");
const initializedRef = useRef(false);
useEffect(() => {
if (!initialSettings || initializedRef.current) return;
initializedRef.current = true;
if (initialSettings.mode != null) setMode(initialSettings.mode as CompressMode);
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
if (initialSettings.targetSizeKb != null) setTargetSizeKb(String(initialSettings.targetSizeKb));
}, [initialSettings]);
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
});
useEffect(() => {
if (mode === "quality") {
onChangeRef.current?.({ mode, quality });
} else {
onChangeRef.current?.({ mode, targetSizeKb: Number(targetSizeKb) });
}
}, [mode, quality, targetSizeKb]);
return (
<div className="space-y-4">
{/* Mode toggle */}
<div>
<p className="text-sm font-medium text-muted-foreground">Compression Mode</p>
<div className="flex gap-1 mt-1">
<button
type="button"
onClick={() => setMode("targetSize")}
className={`flex-1 text-xs py-1.5 rounded ${mode === "targetSize" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
Target Size
</button>
<button
type="button"
onClick={() => setMode("quality")}
className={`flex-1 text-xs py-1.5 rounded ${mode === "quality" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
Quality
</button>
</div>
</div>
{mode === "targetSize" ? (
<div>
<label htmlFor="compress-target-size" className="text-xs text-muted-foreground">
Target Size (KB)
</label>
<input
id="compress-target-size"
type="number"
value={targetSizeKb}
onChange={(e) => setTargetSizeKb(e.target.value)}
min={1}
placeholder="e.g. 200"
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
/>
</div>
) : (
<div>
<div className="flex justify-between items-center">
<label htmlFor="compress-quality" className="text-xs text-muted-foreground">
Quality
</label>
<span className="text-xs font-mono text-foreground">{quality}</span>
</div>
<input
id="compress-quality"
type="range"
min={1}
max={100}
value={quality}
onChange={(e) => setQuality(Number(e.target.value))}
className="w-full mt-1"
/>
<div className="flex justify-between text-[10px] text-muted-foreground mt-0.5">
<span>Smallest file</span>
<span>Best quality</span>
</div>
</div>
)}
</div>
);
}
export function CompressSettings() {
const { files } = useFileStore();
const {
processFiles,
processAllFiles,
processing,
error,
downloadUrl,
originalSize,
processedSize,
progress,
} = useToolProcessor("compress");
const [settings, setSettings] = useState<Record<string, unknown>>({});
const handleProcess = () => {
if (files.length > 1) {
processAllFiles(files, settings);
} else {
processFiles(files, settings);
}
};
const hasFile = files.length > 0;
const canProcess =
settings.mode === "quality" ||
(settings.mode === "targetSize" && Number(settings.targetSizeKb) > 0);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (hasFile && canProcess && !processing) handleProcess();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<CompressControls onChange={setSettings} />
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{/* Size info */}
{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>
<p className="font-medium text-foreground">
Saved: {originalSize > 0 ? ((1 - processedSize / originalSize) * 100).toFixed(1) : "0"}%
</p>
</div>
)}
{/* Process */}
{processing ? (
<ProgressCard
active={processing}
phase={progress.phase === "idle" ? "uploading" : progress.phase}
label="Compressing"
stage={progress.stage}
percent={progress.percent}
elapsed={progress.elapsed}
/>
) : (
<button
type="submit"
data-testid="compress-submit"
disabled={!hasFile || !canProcess || 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 ? `Compress (${files.length} files)` : "Compress"}
</button>
)}
{/* Download */}
{downloadUrl && (
<a
href={downloadUrl}
download
data-testid="compress-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>
)}
</form>
);
}