From d8b48d9f481873cf30f7fd0539a1db242767a389 Mon Sep 17 00:00:00 2001 From: Akshar Sawhney Date: Sat, 16 May 2026 11:05:21 +0530 Subject: [PATCH 1/2] Update VideoEditor.tsx --- src/components/VideoEditor.tsx | 98 ++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 917c9611..e7fc5212 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -49,6 +49,7 @@ export default function VideoEditor() { handleFileSelect, handleExport, cancelExport, reset, } = useVideoEditor(); + const isProcessing = status === "loading-engine" || status === "exporting"; return ( @@ -92,6 +93,11 @@ export default function VideoEditor() { )} + {file && file.size > 100 * 1024 * 1024 && ( +

+ ⚠️ Large file — processing may take several minutes +

+ )} {file && (
} title="Audio & Speed" delay={150}> + +
} title="Adjustments" delay={175}> +
+ {/* Brightness */} +
+
+ Brightness + +
+ updateRecipe({ brightness: Number(e.target.value) })} + className="w-full" + /> +
+ + {/* Contrast */} +
+
+ Contrast + +
+ updateRecipe({ contrast: Number(e.target.value) })} + className="w-full" + /> +
+ + {/* Saturation */} +
+
+ Saturation + +
+ updateRecipe({ saturation: Number(e.target.value) })} + className="w-full" + /> +
+
+
+
} title="Export quality" delay={200}> - +
@@ -122,10 +206,18 @@ export default function VideoEditor() { className="flex items-start gap-3 p-4 bg-film-50 border border-film-200 rounded-xl text-film-800 text-sm animate-fade-in" > -
+

Error

{error}

+ {!error.includes("Validation Failed") && ( + + )}
)} @@ -181,7 +273,7 @@ export default function VideoEditor() { href="https://github.com/magic-peach/reframe" target="_blank" rel="noopener noreferrer" - className="flex items-center gap-1.5 text-[11px] font-heading font-medium text-[var(--muted)] hover:text-film-600 transition-colors" + className="min-h-[44px] min-w-[44px] flex items-center gap-1.5 px-2 text-[11px] font-heading font-medium text-[var(--muted)] hover:text-film-600 transition-colors" > Source on GitHub From 9498c72a1d1b3bc39800ed091e144aebd327e3ec Mon Sep 17 00:00:00 2001 From: aksharsawhney74-rgb Date: Tue, 19 May 2026 23:24:42 +0530 Subject: [PATCH 2/2] feat: add ability to save and delete custom dimensions as local presets --- src/components/PresetSelector.tsx | 189 +++++++++++++++++++++++++----- 1 file changed, 158 insertions(+), 31 deletions(-) diff --git a/src/components/PresetSelector.tsx b/src/components/PresetSelector.tsx index 14f1481f..e3debef9 100644 --- a/src/components/PresetSelector.tsx +++ b/src/components/PresetSelector.tsx @@ -1,10 +1,19 @@ "use client"; +import { useState, useEffect } from "react"; import { PRESETS } from "@/lib/presets"; import { EditRecipe } from "@/lib/types"; -import { Settings2 } from "lucide-react"; +import { Settings2, Save, X } from "lucide-react"; import { cn } from "@/lib/utils"; +// 1. Define the shape of our saved custom presets +interface SavedPreset { + id: string; + label: string; + width: number; + height: number; +} + interface Props { recipe: EditRecipe; onChange: (patch: Partial) => void; @@ -37,16 +46,70 @@ function RatioBox({ width, height, active }: { width: number; height: number; ac } export default function PresetSelector({ recipe, onChange }: Props) { + // 2. Setup state for saved presets and the new preset name input + const [savedPresets, setSavedPresets] = useState([]); + const [newPresetName, setNewPresetName] = useState(""); + + // 3. Load from localStorage on mount + useEffect(() => { + const stored = localStorage.getItem("reframe_custom_presets"); + if (stored) { + try { + setSavedPresets(JSON.parse(stored)); + } catch (e) { + console.error("Failed to parse saved presets", e); + } + } + }, []); + + // 4. Handle saving a new preset + const handleSavePreset = () => { + if (!newPresetName.trim()) return; + + const newPreset: SavedPreset = { + id: `custom-${Date.now()}`, + label: newPresetName.trim(), + width: recipe.customWidth, + height: recipe.customHeight, + }; + + const updated = [...savedPresets, newPreset]; + setSavedPresets(updated); + localStorage.setItem("reframe_custom_presets", JSON.stringify(updated)); + setNewPresetName(""); + + // Automatically select the newly saved preset + onChange({ + preset: newPreset.id, + customWidth: newPreset.width, + customHeight: newPreset.height + }); + }; + + // 5. Handle deleting a preset + const handleDeletePreset = (id: string, e: React.MouseEvent) => { + e.stopPropagation(); // Prevent the button click from also selecting the preset + const updated = savedPresets.filter(p => p.id !== id); + setSavedPresets(updated); + localStorage.setItem("reframe_custom_presets", JSON.stringify(updated)); + + // If they deleted the preset they currently have selected, revert to custom + if (recipe.preset === id) { + onChange({ preset: "custom" }); + } + }; + return (
+ {/* Render built-in presets */} {PRESETS.filter((p) => p.id !== "custom").map((preset) => { const active = recipe.preset === preset.id; return ( + ); + })} + + {/* The Custom Trigger Button */}
+ {/* The Custom Configuration Panel */} {recipe.preset === "custom" && ( -
-
- - onChange({ customWidth: Number(e.target.value) })} - className="w-full text-sm px-3 py-1.5 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 transition-shadow" - /> +
+
+
+ + onChange({ customWidth: Number(e.target.value) })} + className="w-full text-sm px-3 py-1.5 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 transition-shadow" + /> +
+ x +
+ + onChange({ customHeight: Number(e.target.value) })} + className="w-full text-sm px-3 py-1.5 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 transition-shadow" + /> +
- x -
- + + {/* New Save Preset Row */} +
onChange({ customHeight: Number(e.target.value) })} - className="w-full text-sm px-3 py-1.5 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 transition-shadow" + type="text" + placeholder="Name your preset..." + value={newPresetName} + onChange={(e) => setNewPresetName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSavePreset()} + className="flex-1 text-sm px-3 py-1.5 border border-[var(--border)] rounded-md bg-[var(--bg)] focus:outline-none focus:ring-2 focus:ring-film-400" /> +
)}
); -} +} \ No newline at end of file