"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { mergeCanvasSnapshots, type CanvasConflictChoice } from "@/lib/video-canvas/merge-snapshot"; import { CanvasRevisionConflict, VideoCanvasSaveQueue, type VideoCanvasSaveStatus, } from "@/lib/video-canvas/save-queue"; export function useVideoCanvasPersistence( projectId: string, loaded: { id: string; revision: string } | null, snapshot: string, applySnapshot?: (snapshot: string) => void, ) { const queueRef = useRef(null); const latestSnapshot = useRef(snapshot); const observedSnapshot = useRef(snapshot); latestSnapshot.current = snapshot; const [state, setState] = useState<{ status: VideoCanvasSaveStatus; message?: string }>({ status: "saved", }); const [conflicts, setConflicts] = useState([]); const applyRef = useRef(applySnapshot); applyRef.current = applySnapshot; const ready = loaded?.id === projectId; useEffect(() => { if (!ready || !loaded) return; let active = true; const queue = new VideoCanvasSaveQueue( latestSnapshot.current, loaded.revision, async (value, revision) => { const response = await fetch(`/api/canvas/${encodeURIComponent(projectId)}`, { method: "PUT", signal: AbortSignal.timeout(45_000), headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...JSON.parse(value), workspaceType: "video", baseCanvasUpdatedAt: revision, }), }); const result = await response.json(); if (response.status === 409) { const latest = await fetch(`/api/canvas/${encodeURIComponent(projectId)}`, { cache: "no-store", signal: AbortSignal.timeout(15000), }); const remote = await latest.json(); if (!latest.ok || !remote.success || !remote.data?.updatedAt) throw new Error("读取其他页面的修改失败,当前草稿已保留,请重试"); throw new CanvasRevisionConflict( "其他页面修改了相同内容,请选择冲突部分保留哪一版", JSON.stringify({ title: remote.data.title, canvasData: remote.data.canvasData }), String(remote.data.updatedAt), ); } if (!response.ok || !result.success || !result.data?.updatedAt) { throw new Error(result.error || "保存失败,请点击重试"); } return String(result.data.updatedAt); }, (status, message) => { if (active) setState({ status, message }); }, applyRef.current ? { merge: mergeCanvasSnapshots, apply: (value) => { if (active) applyRef.current?.(value); }, conflict: (fields) => { if (active) setConflicts(fields); }, } : undefined, ); queueRef.current = queue; observedSnapshot.current = latestSnapshot.current; setConflicts([]); setState({ status: "saved" }); const backupKey = `video-canvas-draft:${projectId}`; try { const backup = JSON.parse(sessionStorage.getItem(backupKey) || "null"); if (backup?.base && backup?.snapshot) queue.restoreBackup(backup.base, backup.snapshot); } catch { /* Ignore corrupt draft cache; server content is intact. */ } const retainDraft = () => { try { if (queue.dirty) sessionStorage.setItem(backupKey, JSON.stringify(queue.backup)); else sessionStorage.removeItem(backupKey); } catch { /* The unsaved-change warning and JSON export remain available when browser storage is full. */ } }; // Fixed cadence also saves while generation progress is continuously changing. const interval = window.setInterval(() => { retainDraft(); if (queue.dirty && !queue.failed) void queue.flush().then(retainDraft); }, 1500); const onHidden = () => { if (document.visibilityState === "hidden") { retainDraft(); void queue.flush().then(retainDraft); } }; const onBeforeUnload = (event: BeforeUnloadEvent) => { if (!queue.dirty) return; retainDraft(); void queue.flush(); event.preventDefault(); event.returnValue = ""; }; document.addEventListener("visibilitychange", onHidden); window.addEventListener("beforeunload", onBeforeUnload); return () => { active = false; clearInterval(interval); document.removeEventListener("visibilitychange", onHidden); window.removeEventListener("beforeunload", onBeforeUnload); retainDraft(); void queue.flush().then(retainDraft); if (queueRef.current === queue) queueRef.current = null; }; }, [projectId, ready, loaded]); useEffect(() => { if (ready && observedSnapshot.current !== snapshot) { observedSnapshot.current = snapshot; queueRef.current?.update(snapshot); } }, [snapshot, ready]); const save = useCallback(() => { if (!ready) return; queueRef.current?.update(latestSnapshot.current); void queueRef.current?.flush(); }, [ready]); const saveAndConfirm = useCallback(async () => { if (!ready || !queueRef.current) return false; const queue = queueRef.current; queue.update(latestSnapshot.current); await queue.flush(); return !queue.dirty && !queue.failed; }, [ready]); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") { event.preventDefault(); save(); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [save]); const resolveConflict = useCallback( (choice: CanvasConflictChoice) => queueRef.current?.resolveConflict(choice), [], ); const downloadDraft = useCallback(() => { const data = queueRef.current?.backup.snapshot || latestSnapshot.current; const url = URL.createObjectURL(new Blob([data], { type: "application/json" })); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `video-canvas-${projectId}-draft.json`; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }, [projectId]); return { ...state, save, saveAndConfirm, ready, conflicts, resolveConflict, downloadDraft }; }