diff --git a/src/components/DownloadResult.tsx b/src/components/DownloadResult.tsx index 9cb04286..e5e49845 100644 --- a/src/components/DownloadResult.tsx +++ b/src/components/DownloadResult.tsx @@ -1,9 +1,9 @@ "use client"; import { useState, useEffect } from "react"; -import { ExportResult } from "@/lib/types"; +import { ExportHistoryItem, ExportResult } from "@/lib/types"; import { formatBytes } from "@/lib/utils"; -import { Download, RotateCcw, Share2, AlertCircle, Volume2, VolumeX } from "lucide-react"; +import { Clock3, Download, RotateCcw, Share2, AlertCircle, Volume2, VolumeX } from "lucide-react"; import LottiePlayer from "./LottiePlayer"; import { NativeShareButton } from "./NativeShareButton"; import successAnim from "@/lib/lottie/success.json"; @@ -24,12 +24,22 @@ function formatExportDuration(ms: number): string { interface Props { result: ExportResult; + exportHistory: ExportHistoryItem[]; onReset: () => void; soundOnCompletion: boolean; onToggleSound: () => void; } -export default function DownloadResult({ result, onReset, soundOnCompletion, onToggleSound }: Props) { +function formatExportedAt(timestamp: number): string { + return new Date(timestamp).toLocaleString(undefined, { + day: "2-digit", + month: "short", + hour: "2-digit", + minute: "2-digit", + }); +} + +export default function DownloadResult({ result, exportHistory, onReset, soundOnCompletion, onToggleSound }: Props) { const defaultName = `reframe_${result.width}x${result.height}`; const [name, setName] = useState(defaultName); @@ -186,6 +196,53 @@ export default function DownloadResult({ result, onReset, soundOnCompletion, onT Share on X + + {exportHistory.length > 0 && ( +
+
+
+ + +
+ )} ); } diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index b1d2e574..849ec21a 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -199,7 +199,7 @@ function KeyboardShortcutsPanel() { export default function VideoEditor() { const { file, duration, recipe, status, progress, - result, error, exportStartedAt, updateRecipe, + result, exportHistory, error, exportStartedAt, updateRecipe, handleFileSelect, fileError, handleExport, cancelExport, reset, resetSettings, videoRef, seekTo, @@ -681,7 +681,13 @@ export default function VideoEditor() { {status === "done" && result && (
- +
)} diff --git a/src/hooks/useVideoEditor.ts b/src/hooks/useVideoEditor.ts index b3ea8283..461e734d 100644 --- a/src/hooks/useVideoEditor.ts +++ b/src/hooks/useVideoEditor.ts @@ -1,7 +1,7 @@ "use client"; import { useState, useCallback, useEffect, useRef, useMemo } from "react"; -import { EditRecipe, ExportResult, ExportStatus, MAX_FILE_SIZE, OverlayPosition, isValidRecipe } from "@/lib/types"; +import { EditRecipe, ExportHistoryItem, ExportResult, ExportStatus, MAX_FILE_SIZE, OverlayPosition, isValidRecipe } from "@/lib/types"; import { DEFAULT_RECIPE, SPEED_STEPS } from "@/lib/constants"; import { getPresetById } from "@/lib/presets"; import { loadFFmpeg, exportVideo, terminateFFmpeg, FFmpegLoadError } from "@/lib/ffmpeg"; @@ -9,7 +9,12 @@ import { suggestPreset } from "@/lib/presetSuggestion"; import { validateDimensions, getDownscaledDimensions } from "@/utils/video-validation"; const DEFAULT_TITLE = "Reframe — Resize, trim, and export videos in your browser"; - const STORAGE_KEY = "reframe:recipe"; +const STORAGE_KEY = "reframe:recipe"; +const MAX_EXPORT_HISTORY = 5; + +function getExportFilename(result: ExportResult): string { + return `reframe_${result.width}x${result.height}.${result.format}`; +} export function extractMetadata(file: File): Promise<{ width: number; height: number; duration: number }> { return new Promise((resolve, reject) => { @@ -171,11 +176,13 @@ 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 [exportStartedAt, setExportStartedAt] = useState(null); const exportAbortControllerRef = useRef(null); const exportCancelledRef = useRef(false); + const exportHistoryRef = useRef([]); const videoRef = useRef(null); const [musicFile, setMusicFile] = useState(null); @@ -498,9 +505,32 @@ export function useVideoEditor() { ); if (exportCancelledRef.current) return; - setResult({ + const completedResult = { ...exportResult, exportDurationMs: Date.now() - startedAt, + }; + + setResult(completedResult); + setExportHistory((previous) => { + const historyResult = { + ...completedResult, + blobUrl: URL.createObjectURL(completedResult.blob), + }; + const next = [ + { + id: `${Date.now()}-${completedResult.width}x${completedResult.height}-${completedResult.format}`, + result: historyResult, + filename: getExportFilename(completedResult), + createdAt: Date.now(), + }, + ...previous, + ]; + + next.slice(MAX_EXPORT_HISTORY).forEach((item) => { + URL.revokeObjectURL(item.result.blobUrl); + }); + + return next.slice(0, MAX_EXPORT_HISTORY); }); setStatus("done"); } catch (err) { @@ -573,6 +603,10 @@ export function useVideoEditor() { window.addEventListener("beforeunload", handler); return () => window.removeEventListener("beforeunload", handler); }, [status]); + + useEffect(() => { + exportHistoryRef.current = exportHistory; + }, [exportHistory]); useEffect(() => { const handleKeydown = (e: KeyboardEvent) => { @@ -628,6 +662,9 @@ export function useVideoEditor() { useEffect(() => { return () => { + exportHistoryRef.current.forEach((item) => { + URL.revokeObjectURL(item.result.blobUrl); + }); terminateFFmpeg(); }; }, []); @@ -700,6 +737,7 @@ export function useVideoEditor() { progress, exportStartedAt, result, + exportHistory, error, videoRef, seekTo, diff --git a/src/lib/types.ts b/src/lib/types.ts index deb816b8..5593edd1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -68,6 +68,13 @@ export interface ExportResult { exportDurationMs?: number; } +export interface ExportHistoryItem { + id: string; + result: ExportResult; + filename: string; + createdAt: number; +} + export type ExportStatus = | "idle" | "loading-engine"