diff --git a/src/components/ExportHistory.tsx b/src/components/ExportHistory.tsx
new file mode 100644
index 00000000..eb56df8d
--- /dev/null
+++ b/src/components/ExportHistory.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useState } from "react";
+
+interface ExportHistoryItem {
+ id: string;
+ blobUrl: string;
+ filename: string;
+ format: string;
+ size: number;
+ width: number;
+ height: number;
+ exportedAt: number;
+}
+
+interface ExportHistoryProps {
+ history: ExportHistoryItem[];
+ onClear: () => void;
+}
+
+function formatFileSize(bytes: number) {
+ if (bytes < 1024) return `${bytes} B`;
+
+ if (bytes < 1024 * 1024) {
+ return `${(bytes / 1024).toFixed(1)} KB`;
+ }
+
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+export default function ExportHistory({
+ history,
+ onClear,
+}: ExportHistoryProps) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ if (!history.length) return null;
+
+ const handleDownload = (item: ExportHistoryItem) => {
+ const link = document.createElement("a");
+
+ link.href = item.blobUrl;
+ link.download = item.filename;
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ History is cleared when you close or refresh the page.
+
+
+ {isOpen && (
+
+ {history.map((item) => (
+
+
+
+ {item.filename}
+
+
+
+ {item.format.toUpperCase()}
+
+ •
+
+
+ {item.width}×{item.height}
+
+
+ •
+
+ {formatFileSize(item.size)}
+
+
+
+ Exported{" "}
+ {new Date(item.exportedAt).toLocaleString()}
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index f89872d5..a3beeed3 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -22,6 +22,7 @@ import {
SlidersHorizontal, Zap, AlertTriangle, Github
} from "lucide-react";
import OnboardingTour from "./OnboardingTour";
+import ExportHistory from "@/components/ExportHistory";
interface SectionProps {
icon: React.ReactNode;
@@ -51,7 +52,7 @@ function Section({ icon, title, children, delay = 0 }: SectionProps) {
export default function VideoEditor() {
const {
file, duration, recipe, status, progress,
- result, error, updateRecipe,
+ result, exportHistory, clearExportHistory, error, updateRecipe,
handleFileSelect, fileError, handleExport, cancelExport, reset, resetSettings,
videoRef,
seekTo,
@@ -305,11 +306,20 @@ export default function VideoEditor() {
)}
- {status === "done" && result && (
-
-
-
- )}
+ {status === "done" && result && (
+
+
+
+)}
+
+
{
@@ -131,6 +137,7 @@ export function useVideoEditor() {
const [status, setStatus] = useState
("idle");
const [progress, setProgress] = useState(0);
const [result, setResult] = useState(null);
+ const [exportHistory, setExportHistory] = useState([]);
const [error, setError] = useState(null);
const [fileError, setFileError] = useState("");
const exportAbortControllerRef = useRef(null);
@@ -274,7 +281,6 @@ export function useVideoEditor() {
setStatus("loading-engine");
setProgress(0);
setError(null);
- if (result?.blobUrl) URL.revokeObjectURL(result.blobUrl);
setResult(null);
const ffmpeg = await loadFFmpeg(abortController.signal);
@@ -304,7 +310,29 @@ export function useVideoEditor() {
if (exportCancelledRef.current) return;
setResult(exportResult);
- setStatus("done");
+
+const historyItem: ExportHistoryItem = {
+ ...exportResult,
+ id: crypto.randomUUID(),
+ filename: file.name,
+ exportedAt: Date.now(),
+};
+
+setExportHistory((prev) => {
+ const updated = [historyItem, ...prev];
+
+ if (updated.length > 5) {
+ const removed = updated.pop();
+
+ if (removed?.blobUrl) {
+ URL.revokeObjectURL(removed.blobUrl);
+ }
+ }
+
+ return updated;
+});
+
+setStatus("done");
} catch (err) {
if (exportCancelledRef.current) return;
@@ -325,9 +353,21 @@ export function useVideoEditor() {
exportAbortControllerRef.current = null;
}
}
- }, [file, recipe, result, status, overlayFile, overlayPosition, overlaySize, overlayOpacity, duration]);
-
-
+ }, [
+ file,
+ recipe,
+ status,
+ overlayFile,
+ overlayPosition,
+ overlaySize,
+ overlayOpacity,
+ duration,
+ musicFile,
+ musicVolume,
+ originalAudioVolume,
+ loopMusic,
+ ]);
+
useEffect(() => {
if (status === "exporting") {
document.title = `Exporting ${progress}% | Reframe`;
@@ -380,13 +420,15 @@ export function useVideoEditor() {
};
}, [file, status, handleExport]);
- useEffect(()=>{
- return ()=>{
- if(result?.blobUrl){
- URL.revokeObjectURL(result.blobUrl);
+ const clearExportHistory = useCallback(() => {
+ exportHistory.forEach((item) => {
+ if (item.blobUrl) {
+ URL.revokeObjectURL(item.blobUrl);
}
- }
- },[result?.blobUrl])
+ });
+
+ setExportHistory([]);
+ }, [exportHistory]);
const resetSettings = useCallback(() => {
setRecipe(DEFAULT_RECIPE);
@@ -404,7 +446,6 @@ export function useVideoEditor() {
const reset = useCallback(() => {
- if (result?.blobUrl) URL.revokeObjectURL(result.blobUrl);
setFile(null);
setVideoMetadata(null);
setDuration(0);
@@ -413,7 +454,7 @@ export function useVideoEditor() {
setProgress(0);
setResult(null);
setError(null);
- }, [result]);
+ }, []);
useEffect(() => {
if (process.env.NODE_ENV !== "development") return;
@@ -445,6 +486,8 @@ export function useVideoEditor() {
status,
progress,
result,
+ exportHistory,
+ clearExportHistory,
error,
videoRef,
seekTo,