+ {templates.length === 0 && (
+
+ No saved templates
+
+ )}
+
+ {templates.map((template) => (
+
+
+ {template.name}
+
+
+
+ onApplyTemplate(template.recipe)}
+ className="text-xs px-2 py-1 border rounded"
+ >
+ Apply
+
+
+ {
+ const name = prompt(
+ "Rename template",
+ template.name
+ );
+
+ if (!name) return;
+
+ renameTemplate(template.id, name);
+ refreshTemplates();
+ }}
+ className="text-xs px-2 py-1 border rounded"
+ >
+ Rename
+
+
+ {
+ deleteTemplate(template.id);
+ refreshTemplates();
+ }}
+ className="text-xs px-2 py-1 border rounded"
+ >
+ Delete
+
+
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index a12c1f41..69285c97 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -16,7 +16,7 @@ import FormatSelector from "./FormatSelector";
import ExportSettings from "./ExportSettings";
import ExportOverlay from "./ExportOverlay";
import DownloadResult from "./DownloadResult";
-import ImageOverlay from "./ImageOverlay"
+import ImageOverlay from "./ImageOverlay";
import { getPresetById } from "@/lib/presets";
import { cn } from "@/lib/utils";
@@ -27,6 +27,9 @@ import {
import OnboardingTour from "./OnboardingTour";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
import { loadOverlayState, persistOverlayState } from "@/lib/editorPersistence";
+import CustomTemplateManager from "./CustomTemplateManager";
+
+import { saveTemplate, } from "@/lib/templateStorage";
interface SectionProps {
icon: React.ReactNode;
@@ -123,37 +126,37 @@ function KeyboardShortcutsPanel() {
const [open, setOpen] = useState(false);
const shortcuts: { keys: React.ReactNode[]; label: string }[] = [
- {
- keys: [
-
@@ -221,7 +224,7 @@ export default function VideoEditor() {
handleExport,
status,
cancelExport,
- onToggleShortcutsModal: () => {},
+ onToggleShortcutsModal: () => { },
});
const [copied, setCopied] = useState(false);
@@ -289,7 +292,7 @@ export default function VideoEditor() {
useEffect(() => {
if (status === "done" && downloadRef.current) {
-
+
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
downloadRef.current.scrollIntoView({
behavior: prefersReducedMotion ? "instant" : "smooth",
@@ -298,19 +301,19 @@ export default function VideoEditor() {
}
}, [status]);
useEffect(() => {
-const handleBeforeUnload = (e: BeforeUnloadEvent) => {
- if (file) {
- e.preventDefault();
- e.returnValue = "";
- }
-};
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (file) {
+ e.preventDefault();
+ e.returnValue = "";
+ }
+ };
-window.addEventListener("beforeunload", handleBeforeUnload);
+ window.addEventListener("beforeunload", handleBeforeUnload);
-return () => {
- window.removeEventListener("beforeunload", handleBeforeUnload);
-};
-}, [file]);
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ };
+ }, [file]);
const isProcessing = status === "loading-engine" || status === "exporting";
const isMac = typeof navigator !== "undefined" && /Mac/i.test(navigator.platform);
@@ -337,8 +340,8 @@ return () => {
const qualityLabel = recipe.quality <= 21
? "High"
: recipe.quality <= 25
- ? "Balanced"
- : "Small file";
+ ? "Balanced"
+ : "Small file";
return `Exporting to ${width}×${height} ${recipe.format.toUpperCase()} • ${framingLabel} • ${speedLabel} • Quality: ${qualityLabel}`;
}, [recipe]);
@@ -348,6 +351,23 @@ return () => {
if (videoSrc) URL.revokeObjectURL(videoSrc);
};
}, [videoSrc]);
+ const handleSaveTemplate = () => {
+ const name = prompt("Template name");
+
+ if (!name?.trim()) return;
+
+ saveTemplate({
+ id:
+ typeof crypto !== "undefined" &&
+ crypto.randomUUID
+ ? crypto.randomUUID()
+ : Date.now().toString(),
+ name: name.trim(),
+ recipe: { ...recipe },
+ });
+
+ alert("Template saved!");
+ };
return (
@@ -367,46 +387,46 @@ return () => {
-
-
- REFRAME
-
-
- Your video, any format
-
-
-
- No login. No ads. 100% private.
-
-
-
-
- No login. No ads. 100% private - your video never leaves your device.
-
-
+
+
+ REFRAME
+
+
+ Your video, any format
+
+
+
+ No login. No ads. 100% private.
+
+
+
+
+ No login. No ads. 100% private - your video never leaves your device.
+
+
@@ -667,10 +687,12 @@ return () => {
)}
-
+
{!file && (
@@ -681,83 +703,84 @@ return () => {
)}
-
-
}
- title="Resize & Aspect Ratio"
- isOpen={openSections.resize}
- onToggle={() => toggleSection("resize")}
- delay={50}
- >
- {recommendedPreset && (
-
-
- We detected a {recommendedPreset.label.replace(/\s/g, "")} video → Recommended: {(recommendedPreset.platform.split("·")[0] ?? "").trim()} ({recommendedPreset.label.replace(/\s/g, "")})
-
-
- )}
-
-
-
-
-
- {shareCopied ? "Copied!" : "Copy Link"}
-
-
- Reset all settings
-
+ {/* Resize INFO ONLY (no duplicate controls) */}
+
+
+
+
+
+ Resize & Aspect Ratio
+
+
+
+ {recommendedPreset && (
+
+
+ We detected a {recommendedPreset.label.replace(/\s/g, "")} video →
+ Recommended: {(recommendedPreset.platform.split("·")[0] ?? "").trim()} (
+ {recommendedPreset.label.replace(/\s/g, "")})
+
+
+ )}
+
+
+ Resize controls are available in the left panel.
+
+ {/* Copy + Reset */}
+
+
+
+ {shareCopied ? "Copied!" : "Copy Link"}
+
+
+
+ Reset all settings
+
+
+
+ {/* Keyboard shortcuts */}
+ {/* Export summary */}
{file && (
{exportSummary}
)}
-
-
- {isProcessing ? "PROCESSING" : "EXPORT"}
-
+ {/* Templates */}
+
+ } title="Templates">
+
+ Save Current Template
+
+
+ updateRecipe(savedRecipe)}
+ />
+
+
- {file && !isProcessing && (
-
- {isMac ? "⌘" : "Ctrl"} + Enter to export
-
- )}
+
);
-}
+}
\ No newline at end of file
diff --git a/src/lib/templateStorage.tsx b/src/lib/templateStorage.tsx
new file mode 100644
index 00000000..88d8a2f9
--- /dev/null
+++ b/src/lib/templateStorage.tsx
@@ -0,0 +1,59 @@
+export interface CustomTemplate {
+ id: string;
+ name: string;
+ recipe: any;
+}
+
+const STORAGE_KEY = "reframe-custom-templates";
+
+export const getTemplates = (): CustomTemplate[] => {
+ if (typeof window === "undefined") return [];
+
+ try {
+ const data = localStorage.getItem(STORAGE_KEY);
+ return data ? JSON.parse(data) : [];
+ } catch {
+ return [];
+ }
+};
+
+export const saveTemplate = (template: CustomTemplate) => {
+ const templates = getTemplates();
+
+ templates.push(template);
+
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify(templates)
+ );
+
+ console.log("Saved templates:", templates);
+};
+
+export const deleteTemplate = (id: string) => {
+ const updated = getTemplates().filter(
+ (template) => template.id !== id
+ );
+
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify(updated)
+ );
+};
+
+export const renameTemplate = (
+ id: string,
+ name: string
+) => {
+ const updated = getTemplates().map(
+ (template) =>
+ template.id === id
+ ? { ...template, name }
+ : template
+ );
+
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify(updated)
+ );
+};
\ No newline at end of file