Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions src/components/UndoToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// src/components/UndoToast.tsx
"use client";

import { useEffect, useState } from "react";

interface UndoToastProps {
visible: boolean;
onUndo: () => void;
onDismiss: () => void;
duration?: number; // ms, default 5000
}

export default function UndoToast({
visible,
onUndo,
onDismiss,
duration = 5000,
}: UndoToastProps) {
const [progress, setProgress] = useState(100);

useEffect(() => {
if (!visible) {
setProgress(100);
return;
}

const interval = 50; // update every 50ms
const steps = duration / interval;
const decrement = 100 / steps;

const timer = setInterval(() => {
setProgress((prev) => {
if (prev <= 0) {
clearInterval(timer);
onDismiss();
return 0;
}
return prev - decrement;
});
}, interval);

return () => clearInterval(timer);
}, [visible, duration, onDismiss]);

if (!visible) return null;

return (
<div
role="status"
aria-live="polite"
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex flex-col gap-1
bg-zinc-900 text-white rounded-xl shadow-lg px-4 pt-3 pb-2 min-w-[260px]"
>
<div className="flex items-center justify-between gap-6">
<span className="text-sm">Settings reset.</span>
<button
onClick={() => {
onUndo();
onDismiss();
}}
className="text-sm font-semibold text-yellow-400 hover:text-yellow-300
transition-colors focus:outline-none focus:ring-2
focus:ring-yellow-400 rounded"
>
Undo
</button>
</div>
{/* shrinking progress bar */}
<div className="h-0.5 w-full bg-zinc-700 rounded-full overflow-hidden mt-1">
<div
className="h-full bg-yellow-400 transition-none"
style={{ width: `${progress}%` }}
/>
</div>
</div>
);
}
14 changes: 13 additions & 1 deletion src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import ImageOverlay from "./ImageOverlay"
import { getPresetById } from "@/lib/presets";

import { cn } from "@/lib/utils";
import UndoToast from "./UndoToast";

import {
Layers, Crop, Scissors, RotateCw, Volume2, Type,
SlidersHorizontal, Zap, AlertTriangle, Github, Copy
Expand Down Expand Up @@ -208,6 +210,10 @@ export default function VideoEditor() {
overlaySize, setOverlaySize,
overlayOpacity, setOverlayOpacity,
recommendedPreset,
resetSettings: handleReset,
handleUndo,
showUndoToast,
handleToastDismiss,
currentTime,
toggleSound,
} = useVideoEditor();
Expand Down Expand Up @@ -733,7 +739,7 @@ export default function VideoEditor() {
</button>
<button
type="button"
onClick={resetSettings}
onClick={handleReset}
className="text-sm font-heading font-bold uppercase tracking-widest text-[var(--muted)] hover:text-film-600 transition-all opacity-60 hover:opacity-100"
>
Reset all settings
Expand Down Expand Up @@ -777,6 +783,12 @@ export default function VideoEditor() {
</div>
</div>
</div>
<UndoToast
visible={showUndoToast}
onUndo={handleUndo}
onDismiss={handleToastDismiss}
duration={5000}
/>
</div>
);
}
26 changes: 26 additions & 0 deletions src/hooks/useVideoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export function extractMetadata(file: File): Promise<{ width: number; height: nu
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const video = document.createElement("video");
const videoRef = useRef<HTMLVideoElement>(null);


const timeout = setTimeout(() => {
URL.revokeObjectURL(url);
reject( new Error("Video metaData load timeout — the file may be too large or the device too slow. Please try again.") );
Expand Down Expand Up @@ -178,6 +181,9 @@ export function useVideoEditor() {
const exportCancelledRef = useRef(false);
const videoRef = useRef<HTMLVideoElement>(null);

const [previousRecipe, setPreviousRecipe] = useState<typeof DEFAULT_RECIPE | null>(null);
const [showUndoToast, setShowUndoToast] = useState(false);

const [musicFile, setMusicFile] = useState<File | null>(null);
const [musicVolume, setMusicVolume] = useState(70);
const [originalAudioVolume, setOriginalAudioVolume] = useState(40);
Expand Down Expand Up @@ -626,14 +632,30 @@ export function useVideoEditor() {
}
},[result?.blobUrl])

// ADD THIS INSTEAD:
useEffect(() => {
return () => {
terminateFFmpeg();
};
}, []);

const resetSettings = useCallback(() => {
setPreviousRecipe({ ...recipe });
setRecipe(DEFAULT_RECIPE);
setShowUndoToast(true);
}, [recipe]);

const handleUndo = useCallback(() => {
if (previousRecipe) {
setRecipe(previousRecipe);
setPreviousRecipe(null);
}
setShowUndoToast(false);
}, [previousRecipe]);

const handleToastDismiss = useCallback(() => {
setShowUndoToast(false);
setPreviousRecipe(null);
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
Expand Down Expand Up @@ -693,6 +715,10 @@ export function useVideoEditor() {
}, [recipe.soundOnCompletion, updateRecipe]);

return {

showUndoToast,
handleUndo,
handleToastDismiss,
file,
duration,
recipe,
Expand Down