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,