From bc0d699294d6153cc1c078b7d5c8124d48a60711 Mon Sep 17 00:00:00 2001
From: Flux
Date: Wed, 20 May 2026 04:59:26 +0530
Subject: [PATCH 1/7] feat: implement save/load project state using
localStorage (#688)
---
src/components/VideoEditor.tsx | 169 +++++++++++++++++++++++++++++----
src/hooks/useVideoEditor.ts | 167 +++++++++++++++++++++++++-------
2 files changed, 286 insertions(+), 50 deletions(-)
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index 3ffb6a67..35ff1bf4 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -1,8 +1,7 @@
"use client";
-
import { useState, useRef, useEffect, useMemo } from "react";
-import { useVideoEditor } from "@/hooks/useVideoEditor";
+import { useVideoEditor, VideoProject } from "@/hooks/useVideoEditor";
import FileUpload from "./FileUpload";
import VideoPreview from "./VideoPreview";
import ThumbnailStrip from "./ThumbnailStrip";
@@ -15,11 +14,12 @@ 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 { cn } from "@/lib/utils";
import {
Layers, Crop, Scissors, RotateCw, Volume2,
- SlidersHorizontal, Zap, AlertTriangle, Github
+ SlidersHorizontal, Zap, AlertTriangle,
+ Save, FolderOpen, Trash2
} from "lucide-react";
import OnboardingTour from "./OnboardingTour";
@@ -50,18 +50,25 @@ function Section({ icon, title, children, delay = 0 }: SectionProps) {
export default function VideoEditor() {
const {
- file, duration, recipe, status, progress,
+ file, duration, recipe, status, progress,
result, error, updateRecipe,
handleFileSelect, fileError, handleExport, cancelExport, reset, resetSettings,
- videoRef,
- seekTo,
+ videoRef, seekTo,
overlayFile, setOverlayFile,
overlayPosition, setOverlayPosition,
overlaySize, setOverlaySize,
overlayOpacity, setOverlayOpacity,
recommendedPreset,
+ saveProject, listProjects, loadProject, deleteProject,
} = useVideoEditor();
+
const [copied, setCopied] = useState(false);
+ const [showSaveModal, setShowSaveModal] = useState(false);
+ const [showLoadModal, setShowLoadModal] = useState(false);
+ const [projectName, setProjectName] = useState("");
+ const [saveError, setSaveError] = useState("");
+ const [loadedProjects, setLoadedProjects] = useState>([]);
+ const [loadNotice, setLoadNotice] = useState("");
const downloadRef = useRef(null);
useEffect(() => {
@@ -122,10 +129,10 @@ export default function VideoEditor() {
{!file && (
-
-
Upload a video to get started
-
Supports MP4, MOV, WebM and more
-
+
+
Upload a video to get started
+
Supports MP4, MOV, WebM and more
+
)}
{file && (
@@ -151,6 +158,7 @@ export default function VideoEditor() {
⚠️ Large file - processing may take several minutes
)}
+
{file && (
} title="Audio & Speed" delay={150}>
- }
- title="Adjustments"
- delay={175}
- >
+ } title="Adjustments" delay={175}>
{/* Brightness */}
@@ -336,7 +340,31 @@ export default function VideoEditor() {
-
+
+
+ {
+ setProjectName("");
+ setSaveError("");
+ setShowSaveModal(true);
+ }}
+ className="flex items-center gap-1.5 text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)] hover:text-film-600 transition-all opacity-60 hover:opacity-100"
+ >
+ Save
+
+ {
+ setLoadedProjects(listProjects());
+ setLoadNotice("");
+ setShowLoadModal(true);
+ }}
+ className="flex items-center gap-1.5 text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)] hover:text-film-600 transition-all opacity-60 hover:opacity-100"
+ >
+ Load
+
+
+
+ {/* Save Modal */}
+ {showSaveModal && (
+
+
+
Save project
+
setProjectName(e.target.value)}
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--bg)] text-[var(--text)] text-sm focus:outline-none focus:ring-1 focus:ring-film-500"
+ autoFocus
+ />
+ {saveError &&
{saveError}
}
+
+ setShowSaveModal(false)}
+ className="px-4 py-2 text-sm font-heading uppercase tracking-widest text-[var(--muted)] hover:opacity-80"
+ >
+ Cancel
+
+ {
+ if (!projectName.trim()) {
+ setSaveError("Please enter a project name.");
+ return;
+ }
+ const ok = saveProject(projectName.trim());
+ if (ok) {
+ setShowSaveModal(false);
+ } else {
+ setSaveError("Failed to save. Storage may be full.");
+ }
+ }}
+ className="px-4 py-2 text-sm font-heading font-bold uppercase tracking-widest bg-film-600 text-white rounded-lg hover:bg-film-700"
+ >
+ Save
+
+
+
+
+ )}
+
+ {/* Load Modal */}
+ {showLoadModal && (
+
+
+
Load project
+
+ ⚠️ Settings will be restored, but you'll need to re-select your video file.
+
+ {loadNotice &&
{loadNotice}
}
+ {loadedProjects.length === 0 ? (
+
No saved projects yet.
+ ) : (
+
+ {loadedProjects.map((p: VideoProject) => (
+
+
+
{p.name}
+
{new Date(p.savedAt).toLocaleString()}
+
+
+ {
+ loadProject(p.id);
+ setLoadNotice(`"${p.name}" loaded.`);
+ setTimeout(() => setShowLoadModal(false), 800);
+ }}
+ className="px-3 py-1 text-xs font-heading font-bold uppercase tracking-widest bg-film-600 text-white rounded-lg hover:bg-film-700"
+ >
+ Load
+
+ {
+ deleteProject(p.id);
+ setLoadedProjects(listProjects());
+ }}
+ aria-label="Delete project"
+ className="p-1 text-[var(--muted)] hover:text-red-500 transition-colors"
+ >
+
+
+
+
+ ))}
+
+ )}
+
+ setShowLoadModal(false)}
+ className="px-4 py-2 text-sm font-heading uppercase tracking-widest text-[var(--muted)] hover:opacity-80"
+ >
+ Close
+
+
+
+
+ )}
);
}
\ No newline at end of file
diff --git a/src/hooks/useVideoEditor.ts b/src/hooks/useVideoEditor.ts
index 2f94bcbf..abe806a7 100644
--- a/src/hooks/useVideoEditor.ts
+++ b/src/hooks/useVideoEditor.ts
@@ -7,6 +7,47 @@ import { getPresetById } from "@/lib/presets";
import { loadFFmpeg, exportVideo, terminateFFmpeg, FFmpegLoadError } from "@/lib/ffmpeg";
import { suggestPreset } from "@/lib/presetSuggestion";
+// --- Project Save/Load ---
+export interface VideoProject {
+ id: string;
+ name: string;
+ savedAt: string;
+ schemaVersion: "v1";
+ settings: {
+ preset: string;
+ quality: number;
+ speed: number;
+ customWidth: number;
+ customHeight: number;
+ brightness: number;
+ contrast: number;
+ saturation: number;
+ trimStart: number;
+ trimEnd: number | null;
+ };
+}
+
+const PROJECT_KEY = "reframe-projects-v1";
+
+const readProjects = (): Record
=> {
+ if (typeof window === "undefined") return {};
+ try {
+ const raw = localStorage.getItem(PROJECT_KEY);
+ return raw ? JSON.parse(raw) : {};
+ } catch {
+ return {};
+ }
+};
+
+const writeProjects = (projects: Record): void => {
+ if (typeof window === "undefined") return;
+ try {
+ localStorage.setItem(PROJECT_KEY, JSON.stringify(projects));
+ } catch {
+ // quota exceeded or storage blocked
+ }
+};
+
const DEFAULT_TITLE = "Reframe — Resize, trim, and export videos in your browser";
export function extractMetadata(file: File): Promise<{ width: number; height: number; duration: number }> {
@@ -15,12 +56,12 @@ export function extractMetadata(file: File): Promise<{ width: number; height: nu
const video = document.createElement("video");
const timeout = setTimeout(() => {
URL.revokeObjectURL(url);
- reject( new Error("Video metaData load timeout"))
+ reject(new Error("Video metaData load timeout"));
}, 500);
video.preload = "metadata";
video.onloadedmetadata = () => {
- clearTimeout(timeout)
+ clearTimeout(timeout);
resolve({
width: video.videoWidth,
height: video.videoHeight,
@@ -29,7 +70,7 @@ export function extractMetadata(file: File): Promise<{ width: number; height: nu
URL.revokeObjectURL(url);
};
video.onerror = () => {
- clearTimeout(timeout)
+ clearTimeout(timeout);
URL.revokeObjectURL(url);
reject(new Error("Failed to load video metadata"));
};
@@ -62,7 +103,7 @@ function verifyMagicBytes(file: File): Promise {
});
}
-function validateRecipe(recipe: EditRecipe, duration: number ): string | null {
+function validateRecipe(recipe: EditRecipe, duration: number): string | null {
const validations: Array<[boolean, string]> = [
[
recipe.trimStart < 0,
@@ -96,12 +137,10 @@ function validateRecipe(recipe: EditRecipe, duration: number ): string | null {
recipe.brightness < -1 || recipe.brightness > 1,
"Brightness must be between -1 and 1.",
],
-
[
recipe.contrast < 0 || recipe.contrast > 2,
"Contrast must be between 0 and 2.",
],
-
[
recipe.saturation < 0 || recipe.saturation > 3,
"Saturation must be between 0 and 3.",
@@ -147,16 +186,17 @@ export function useVideoEditor() {
const [overlaySize, setOverlaySize] = useState(150);
const [overlayOpacity, setOverlayOpacity] = useState(100);
- const updateRecipe = useCallback((patch: Partial) => {
- setRecipe((prev) => {
- const next = { ...prev, ...patch };
- // GIF has no audio — force keepAudio off
- if (next.format === "gif") {
- next.keepAudio = false;
- }
- return next;
- });
-}, []);
+ const updateRecipe = useCallback((patch: Partial) => {
+ setRecipe((prev) => {
+ const next = { ...prev, ...patch };
+ // GIF has no audio — force keepAudio off
+ if (next.format === "gif") {
+ next.keepAudio = false;
+ }
+ return next;
+ });
+ }, []);
+
useEffect(() => {
try {
const saved = localStorage.getItem("reframe-settings");
@@ -168,10 +208,10 @@ export function useVideoEditor() {
quality: parsed.quality ?? prev.quality,
speed: parsed.speed ?? prev.speed,
customWidth: parsed.customWidth ?? prev.customWidth,
- customHeight: parsed.customHeight ?? prev.customHeight
+ customHeight: parsed.customHeight ?? prev.customHeight,
}));
}
- } catch (e) {
+ } catch {
// ignore
}
}, []);
@@ -183,9 +223,9 @@ export function useVideoEditor() {
quality: recipe.quality,
speed: recipe.speed,
customWidth: recipe.customWidth,
- customHeight: recipe.customHeight
+ customHeight: recipe.customHeight,
}));
- } catch (e) {
+ } catch {
// ignore
}
}, [recipe.preset, recipe.quality, recipe.speed, recipe.customWidth, recipe.customHeight]);
@@ -195,6 +235,68 @@ export function useVideoEditor() {
return getPresetById(suggestPreset(videoMetadata.width, videoMetadata.height)) ?? null;
}, [videoMetadata]);
+ const saveProject = useCallback((name: string): boolean => {
+ try {
+ const projects = readProjects();
+ const id = crypto.randomUUID();
+ const project: VideoProject = {
+ id,
+ name: name.trim(),
+ savedAt: new Date().toISOString(),
+ schemaVersion: "v1",
+ settings: {
+ preset: recipe.preset,
+ quality: recipe.quality,
+ speed: recipe.speed,
+ customWidth: recipe.customWidth,
+ customHeight: recipe.customHeight,
+ brightness: recipe.brightness,
+ contrast: recipe.contrast,
+ saturation: recipe.saturation,
+ trimStart: recipe.trimStart,
+ trimEnd: recipe.trimEnd,
+ },
+ };
+ projects[id] = project;
+ writeProjects(projects);
+ return true;
+ } catch {
+ return false;
+ }
+ }, [recipe]);
+
+ const listProjects = useCallback((): VideoProject[] => {
+ return Object.values(readProjects()).sort(
+ (a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime()
+ );
+ }, []);
+
+ const loadProject = useCallback((id: string): boolean => {
+ const project = readProjects()[id];
+ if (!project) return false;
+ updateRecipe({
+ preset: project.settings.preset as EditRecipe["preset"],
+ quality: project.settings.quality,
+ speed: project.settings.speed,
+ customWidth: project.settings.customWidth,
+ customHeight: project.settings.customHeight,
+ brightness: project.settings.brightness,
+ contrast: project.settings.contrast,
+ saturation: project.settings.saturation,
+ trimStart: project.settings.trimStart,
+ trimEnd: project.settings.trimEnd,
+ });
+ return true;
+ }, [updateRecipe]);
+
+ const deleteProject = useCallback((id: string): boolean => {
+ const projects = readProjects();
+ if (!projects[id]) return false;
+ delete projects[id];
+ writeProjects(projects);
+ return true;
+ }, []);
+
const handleFileSelect = useCallback(async (selectedFile: File) => {
setResult(null);
setStatus("idle");
@@ -245,7 +347,6 @@ export function useVideoEditor() {
setRecipe((prev) => {
const suggestedPreset = suggestPreset(width, height);
const shouldApplySuggestion = prev.preset === DEFAULT_RECIPE.preset;
-
return {
...prev,
trimStart: 0,
@@ -311,7 +412,7 @@ export function useVideoEditor() {
setResult(exportResult);
setStatus("done");
- } catch (err) {
+ } catch (err) {
if (exportCancelledRef.current) return;
console.error("export failed:", err);
@@ -325,15 +426,13 @@ export function useVideoEditor() {
setError('Export failed. Please try again or use a different video.');
}
setStatus("error");
- }
- finally {
+ } finally {
if (exportAbortControllerRef.current === abortController) {
exportAbortControllerRef.current = null;
}
}
}, [file, recipe, result, status, overlayFile, overlayPosition, overlaySize, overlayOpacity, duration]);
-
useEffect(() => {
if (status === "exporting") {
document.title = `Exporting ${progress}% | Reframe`;
@@ -366,7 +465,7 @@ export function useVideoEditor() {
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [status]);
-
+
useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
if (
@@ -386,13 +485,13 @@ export function useVideoEditor() {
};
}, [file, status, handleExport]);
- useEffect(()=>{
- return ()=>{
- if(result?.blobUrl){
+ useEffect(() => {
+ return () => {
+ if (result?.blobUrl) {
URL.revokeObjectURL(result.blobUrl);
}
- }
- },[result?.blobUrl])
+ };
+ }, [result?.blobUrl]);
const resetSettings = useCallback(() => {
setRecipe(DEFAULT_RECIPE);
@@ -408,7 +507,6 @@ export function useVideoEditor() {
setError(null);
}, []);
-
const reset = useCallback(() => {
if (result?.blobUrl) URL.revokeObjectURL(result.blobUrl);
setFile(null);
@@ -438,6 +536,7 @@ export function useVideoEditor() {
useEffect(() => {
localStorage.setItem("soundOnCompletion", String(recipe.soundOnCompletion));
}, [recipe.soundOnCompletion]);
+
const seekTo = useCallback((time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
@@ -478,5 +577,9 @@ export function useVideoEditor() {
overlayOpacity,
setOverlayOpacity,
recommendedPreset,
+ saveProject,
+ listProjects,
+ loadProject,
+ deleteProject,
};
}
\ No newline at end of file
From 9e4ef138bf08e3b0275edbc1fe2f4cfe219d6719 Mon Sep 17 00:00:00 2001
From: Flux
Date: Wed, 20 May 2026 05:07:02 +0530
Subject: [PATCH 2/7] fix: resolve lint errors in VideoEditor (autoFocus,
unescaped apostrophe)
---
src/components/VideoEditor.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index 35ff1bf4..1c2b87a5 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -408,7 +408,6 @@ export default function VideoEditor() {
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--bg)] text-[var(--text)] text-sm focus:outline-none focus:ring-1 focus:ring-film-500"
- autoFocus
/>
{saveError && {saveError}
}
@@ -448,7 +447,7 @@ export default function VideoEditor() {
Load project
- ⚠️ Settings will be restored, but you'll need to re-select your video file.
+ ⚠️ Settings will be restored, but you'll need to re-select your video file.
{loadNotice &&
{loadNotice}
}
{loadedProjects.length === 0 ? (
From 0337825506346c12b9ebb2137ce2f5ba6dcb6acc Mon Sep 17 00:00:00 2001
From: Flux
Date: Thu, 21 May 2026 23:52:49 +0530
Subject: [PATCH 3/7] feat: complete localstorage framework and resolve all
component prop requirements
---
src/components/VideoEditor.tsx | 551 +++++++++++++-----------------
src/hooks/useVideoEditor.ts | 596 +++++++--------------------------
2 files changed, 337 insertions(+), 810 deletions(-)
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index 66e64a0b..19d5c672 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useRef, useEffect, useMemo } from "react";
-import { useVideoEditor, VideoProject } from "@/hooks/useVideoEditor";
+import { useVideoEditor } from "@/hooks/useVideoEditor";
import FileUpload from "./FileUpload";
import VideoPreview from "./VideoPreview";
import ThumbnailStrip from "./ThumbnailStrip";
@@ -14,21 +14,11 @@ import FormatSelector from "./FormatSelector";
import ExportSettings from "./ExportSettings";
import ExportOverlay from "./ExportOverlay";
import DownloadResult from "./DownloadResult";
-import ImageOverlay from "./ImageOverlay";
import { cn } from "@/lib/utils";
import {
Layers, Crop, Scissors, RotateCw, Volume2,
- SlidersHorizontal, Zap, AlertTriangle,
- Save, FolderOpen, Trash2
-import ImageOverlay from "./ImageOverlay"
-
-import { cn } from "@/lib/utils";
-import {
- Layers, Crop, Scissors, RotateCw, Volume2,
- SlidersHorizontal, Zap, AlertTriangle, Github, Copy
+ SlidersHorizontal, Zap, AlertTriangle, Save, FolderOpen, Trash2, X
} from "lucide-react";
-import OnboardingTour from "./OnboardingTour";
-import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
interface SectionProps {
icon: React.ReactNode;
@@ -55,140 +45,44 @@ function Section({ icon, title, children, delay = 0 }: SectionProps) {
);
}
-/** Inline keyboard hint badge. */
-function Kbd({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-/** Collapsible panel that lists all keyboard shortcuts. */
-function KeyboardShortcutsPanel() {
- const [open, setOpen] = useState(false);
-
- const shortcuts: { keys: React.ReactNode[]; label: string }[] = [
- {
- keys: [
- Ctrl ,
- + ,
- Shift ,
- + ,
- E
- ],
- label: "Export video",
- },
- {
- keys: [M ],
- label: "Toggle audio mute",
- },
- {
- keys: [R ],
- label: "Reset all settings",
- },
- {
- keys: [Esc ],
- label: "Cancel export",
- },
- {
- keys: [1 , – , 9 ],
- label: "Switch preset by index",
- },
- {
- keys: [? ],
- label: "Toggle this panel",
- },
-];
-
- return (
-
-
setOpen((v) => !v)}
- className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-[var(--border)] transition-colors duration-150"
- >
-
- ⌨
- Keyboard Shortcuts
-
-
-
-
-
-
- {open && (
-
- {shortcuts.map(({ keys, label }) => (
-
- {label}
- {keys}
-
- ))}
-
- )}
-
- );
-}
-
export default function VideoEditor() {
const {
file, duration, recipe, status, progress,
result, error, updateRecipe,
handleFileSelect, fileError, handleExport, cancelExport, reset, resetSettings,
- videoRef, seekTo,
- overlayFile, setOverlayFile,
- overlayPosition, setOverlayPosition,
- overlaySize, setOverlaySize,
- overlayOpacity, setOverlayOpacity,
+ videoRef,
+ seekTo,
+ saveProject,
+ listProjects,
+ loadProject,
+ deleteProject,
+ musicFile,
+ setMusicFile,
+ musicVolume,
+ setMusicVolume,
+ originalAudioVolume,
+ setOriginalAudioVolume,
+ loopMusic,
+ setLoopMusic,
+ overlayFile,
+ setOverlayFile,
+ overlayPosition,
+ setOverlayPosition,
+ overlaySize,
+ setOverlaySize,
+ overlayOpacity,
+ setOverlayOpacity,
recommendedPreset,
- saveProject, listProjects, loadProject, deleteProject,
} = useVideoEditor();
const [copied, setCopied] = useState(false);
- const [showSaveModal, setShowSaveModal] = useState(false);
- const [showLoadModal, setShowLoadModal] = useState(false);
- const [projectName, setProjectName] = useState("");
- const [saveError, setSaveError] = useState("");
- const [loadedProjects, setLoadedProjects] = useState>([]);
- const [loadNotice, setLoadNotice] = useState("");
- toggleSound,
- } = useVideoEditor();
-
- useKeyboardShortcuts({
- file,
- recipe,
- resetSettings,
- updateRecipe,
- handleExport,
- status,
- cancelExport,
- onToggleShortcutsModal: () => {},
- });
-
- const [copied, setCopied] = useState(false);
- const [shareCopied, setShareCopied] = useState(false);
const downloadRef = useRef(null);
- const handleCopyLink = () => {
- if (typeof window === "undefined") return;
- navigator.clipboard.writeText(window.location.href).then(() => {
- setShareCopied(true);
- setTimeout(() => setShareCopied(false), 2000);
- });
- };
+ const [activeModal, setActiveModal] = useState<"save" | "load" | null>(null);
+ const [projectName, setProjectName] = useState("");
+ const [savedProjects, setSavedProjects] = useState>([]);
+ const [soundOnCompletion, setSoundOnCompletion] = useState(true);
+ const handleToggleSound = () => setSoundOnCompletion((prev) => !prev);
useEffect(() => {
if (status === "done" && downloadRef.current) {
@@ -200,8 +94,13 @@ export default function VideoEditor() {
}
}, [status]);
+ useEffect(() => {
+ if (activeModal === "load") {
+ setSavedProjects(listProjects());
+ }
+ }, [activeModal, listProjects]);
+
const isProcessing = status === "loading-engine" || status === "exporting";
- const isMac = typeof navigator !== "undefined" && /Mac/i.test(navigator.platform);
const videoSrc = useMemo(
() => (file ? URL.createObjectURL(file) : null),
@@ -214,10 +113,35 @@ export default function VideoEditor() {
};
}, [videoSrc]);
+ const handleSaveSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!projectName.trim()) return;
+
+ const success = saveProject(projectName.trim());
+ if (success) {
+ setProjectName("");
+ setActiveModal(null);
+ }
+ };
+
+ const handleLoadSelect = (id: string) => {
+ const success = loadProject(id);
+ if (success) {
+ setActiveModal(null);
+ }
+ };
+
+ const handleDeleteSelect = (id: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const success = deleteProject(id);
+ if (success) {
+ setSavedProjects(listProjects());
+ }
+ };
+
return (
-
{status === "exporting" && `Exporting video: ${progress}%`}
@@ -227,11 +151,8 @@ export default function VideoEditor() {