diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index a12c1f41..9b4b8406 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -297,23 +297,14 @@ export default function VideoEditor() { }); } }, [status]); - useEffect(() => { -const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (file) { - e.preventDefault(); - e.returnValue = ""; - } -}; - -window.addEventListener("beforeunload", handleBeforeUnload); -return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); -}; -}, [file]); const isProcessing = status === "loading-engine" || status === "exporting"; - const isMac = typeof navigator !== "undefined" && /Mac/i.test(navigator.platform); + const [isMac, setIsMac] = useState(false); + + useEffect(() => { + setIsMac(typeof navigator !== "undefined" && /Mac/i.test(navigator.platform)); + }, []); const intervalSeconds = useMemo(() => { if (duration <= 30) return 2; @@ -717,7 +708,7 @@ return () => { onClick={resetSettings} 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 + Clear saved session diff --git a/src/hooks/useVideoEditor.ts b/src/hooks/useVideoEditor.ts index f2edb45c..03191fec 100644 --- a/src/hooks/useVideoEditor.ts +++ b/src/hooks/useVideoEditor.ts @@ -24,6 +24,7 @@ import { RECIPE_STORAGE_KEY, LEGACY_SETTINGS_KEY, } from "@/lib/editorPersistence"; +import { saveSessionFile, loadSessionFile, clearSessionFile } from "@/lib/sessionDB"; const DEFAULT_TITLE = "Reframe — Resize, trim, and export videos in your browser"; @@ -155,20 +156,7 @@ export function useVideoEditor() { height: number; duration: number; } | null>(null); - const [recipe, setRecipe] = useState(() => { - if (typeof window === "undefined") return { ...DEFAULT_RECIPE }; - const params = new URLSearchParams(window.location.search); - const encoded = params.get("settings"); - if (encoded) { - const decoded = decodeRecipe(encoded); - if (decoded) { - return migratePersistedRecipe(decoded); - } - } - return loadPersistedRecipe(localStorage, migratePersistedRecipe({ - soundOnCompletion: getStoredSoundPreference(localStorage), - })); - }); + const [recipe, setRecipe] = useState({ ...DEFAULT_RECIPE }); const [status, setStatus] = useState("idle"); const [progress, setProgress] = useState(0); const [result, setResult] = useState(null); @@ -256,8 +244,33 @@ export function useVideoEditor() { useEffect(() => { if (typeof window === "undefined") return; + + // Auto-restore saved video session + loadSessionFile().then(async (savedFile) => { + if (savedFile) { + try { + const { width, height, duration: dur } = await extractMetadata(savedFile); + setDuration(dur); + setVideoMetadata({ width, height, duration: dur }); + setFile(savedFile); + } catch (e) { + console.error("Failed to restore video session:", e); + clearSessionFile().catch(console.error); + } + } + }).catch(console.error); + try { const params = new URLSearchParams(window.location.search); + const encoded = params.get("settings"); + if (encoded) { + const decoded = decodeRecipe(encoded); + if (decoded) { + setRecipe(migratePersistedRecipe(decoded)); + return; + } + } + const recipeKeys = Object.keys(DEFAULT_RECIPE) as Array; const hasRecipeParams = recipeKeys.some(key => params.has(key)); @@ -290,7 +303,10 @@ export function useVideoEditor() { })); } } else { - setRecipe((current) => loadPersistedRecipe(localStorage, current)); + setRecipe((current) => loadPersistedRecipe(localStorage, migratePersistedRecipe({ + ...current, + soundOnCompletion: getStoredSoundPreference(localStorage), + }))); } } catch (e) { // ignore @@ -412,6 +428,7 @@ export function useVideoEditor() { setDuration(dur); setVideoMetadata({ width, height, duration: dur }); setFile(selectedFile); + saveSessionFile(selectedFile).catch(console.error); if (dimensionCheck === "warning") { console.warn(`[Reframe] High resolution video detected (${width}×${height}). Export may be slow.`); @@ -626,6 +643,7 @@ export function useVideoEditor() { try { localStorage.removeItem(RECIPE_STORAGE_KEY); localStorage.removeItem(LEGACY_SETTINGS_KEY); + clearSessionFile().catch(console.error); } catch { // ignore } @@ -657,6 +675,7 @@ export function useVideoEditor() { try { localStorage.removeItem(RECIPE_STORAGE_KEY); localStorage.removeItem(LEGACY_SETTINGS_KEY); + clearSessionFile().catch(console.error); } catch { // ignore } diff --git a/src/lib/sessionDB.ts b/src/lib/sessionDB.ts new file mode 100644 index 00000000..57f0a1d7 --- /dev/null +++ b/src/lib/sessionDB.ts @@ -0,0 +1,82 @@ +const DB_NAME = "reframe-session"; +const STORE_NAME = "files"; + +export async function saveSessionFile(file: File): Promise { + return new Promise((resolve, reject) => { + try { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME); + } + }; + request.onsuccess = () => { + const db = request.result; + const tx = db.transaction(STORE_NAME, "readwrite"); + tx.objectStore(STORE_NAME).put(file, "currentVideo"); + tx.oncomplete = () => { db.close(); resolve(); }; + tx.onerror = () => reject(tx.error); + }; + request.onerror = () => reject(request.error); + } catch (e) { + reject(e); + } + }); +} + +export async function loadSessionFile(): Promise { + return new Promise((resolve, reject) => { + try { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME); + } + }; + request.onsuccess = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.close(); + return resolve(null); + } + const tx = db.transaction(STORE_NAME, "readonly"); + const getReq = tx.objectStore(STORE_NAME).get("currentVideo"); + getReq.onsuccess = () => { + db.close(); + resolve(getReq.result || null); + }; + getReq.onerror = () => reject(getReq.error); + }; + request.onerror = () => reject(request.error); + } catch (e) { + reject(e); + } + }); +} + +export async function clearSessionFile(): Promise { + return new Promise((resolve, reject) => { + try { + const request = indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME); + } + }; + request.onsuccess = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.close(); + return resolve(); + } + const tx = db.transaction(STORE_NAME, "readwrite"); + tx.objectStore(STORE_NAME).clear(); + tx.oncomplete = () => { db.close(); resolve(); }; + tx.onerror = () => reject(tx.error); + }; + request.onerror = () => reject(request.error); + } catch (e) { + reject(e); + } + }); +}