mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: full HEIF/HEIC support, content-aware resize performance fix, UI improvements
- Add bidirectional HEIF support: decode (input) and encode (output) via system heif-convert/heif-enc - Add server-side WebP preview generation for non-browser-previewable formats (HEIC, TIFF) - Fix content-aware resize failing on HEIF input (decode before passing to caire) - Fix content-aware resize timeout on large images by downscaling to max 1200px and using JPEG intermediate - Add HEIF as target format in convert tool - Add loading spinner for HEIF preview decode in file store - Fix file picker not accepting HEIF files (explicit .heic,.heif,.hif extensions) - Extend frontend timeout for medium tools to 180s with 45s progress animation - Redesign rotate controls with preset buttons and compact flip section - Remove misleading savings percentage from convert tool
This commit is contained in:
@@ -4,8 +4,8 @@ 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"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic"];
|
||||
const OUTPUT_FORMATS = ["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
|
||||
|
||||
export interface ConvertControlsProps {
|
||||
onChange?: (settings: Record<string, unknown>) => void;
|
||||
@@ -132,10 +132,6 @@ export function ConvertSettings() {
|
||||
<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>
|
||||
Savings:{" "}
|
||||
{originalSize > 0 ? ((1 - processedSize / originalSize) * 100).toFixed(1) : "0"}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export function PipelineBuilder({
|
||||
const handleFileSelect = useCallback(() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.accept = "image/*,.heic,.heif,.hif";
|
||||
input.onchange = (e) => {
|
||||
const f = (e.target as HTMLInputElement).files?.[0];
|
||||
if (f) setFile(f);
|
||||
|
||||
@@ -87,20 +87,45 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Quick rotate */}
|
||||
{/* Quick rotate presets */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Rotate</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rotate-left"
|
||||
onClick={rotateLeft}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
|
||||
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
|
||||
title="Rotate 90° counter-clockwise"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Left
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
-90°
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRotation((r) => r + 180)}
|
||||
className="flex-1 flex items-center justify-center py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
|
||||
title="Rotate 180°"
|
||||
>
|
||||
180°
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rotate-right"
|
||||
onClick={rotateRight}
|
||||
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
|
||||
title="Rotate 90° clockwise"
|
||||
>
|
||||
+90°
|
||||
<RotateCw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom angle */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Angle</p>
|
||||
<div className="flex items-center justify-center gap-1.5 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRotation((r) => r - 1)}
|
||||
@@ -122,7 +147,7 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
|
||||
commitAngleInput();
|
||||
}
|
||||
}}
|
||||
className="w-14 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 pr-4"
|
||||
className="w-16 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 pr-4"
|
||||
/>
|
||||
<span className="absolute right-2 text-sm font-mono text-muted-foreground pointer-events-none">
|
||||
°
|
||||
@@ -136,16 +161,6 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rotate-right"
|
||||
onClick={rotateRight}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
|
||||
title="Rotate 90° clockwise"
|
||||
>
|
||||
Right
|
||||
<RotateCw className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,26 +200,26 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
|
||||
type="button"
|
||||
data-testid="rotate-flip-h"
|
||||
onClick={() => setFlipH(!flipH)}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||
flipH
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-primary/10"
|
||||
}`}
|
||||
>
|
||||
<FlipHorizontal className="h-4 w-4" />
|
||||
<FlipHorizontal className="h-3.5 w-3.5" />
|
||||
Horizontal
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rotate-flip-v"
|
||||
onClick={() => setFlipV(!flipV)}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||
flipV
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-primary/10"
|
||||
}`}
|
||||
>
|
||||
<FlipVertical className="h-4 w-4" />
|
||||
<FlipVertical className="h-3.5 w-3.5" />
|
||||
Vertical
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user