"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { CanvasRevisionConflict, VideoCanvasSaveQueue, type VideoCanvasSaveStatus } from "@/lib/video-canvas/save-queue"; export function useVideoCanvasPersistence( projectId: string, loaded: { id: string; revision: string } | null, snapshot: string, ) { const queueRef = useRef(null); const latestSnapshot = useRef(snapshot); latestSnapshot.current = snapshot; const [state, setState] = useState<{ status: VideoCanvasSaveStatus; message?: string }>({ status: "saved" }); 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) { throw new CanvasRevisionConflict("此画布已在其他页面更新。请先保留当前内容,再刷新加载最新版本。"); } 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 }); }); queueRef.current = queue; setState({ status: "saved" }); // Fixed cadence also saves while generation progress is continuously changing. const interval = window.setInterval(() => { if (queue.dirty && !queue.failed) void queue.flush(); }, 1500); const onHidden = () => { if (document.visibilityState === "hidden") void queue.flush(); }; const onBeforeUnload = (event: BeforeUnloadEvent) => { if (!queue.dirty) return; 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); void queue.flush(); if (queueRef.current === queue) queueRef.current = null; }; }, [projectId, ready, loaded]); useEffect(() => { if (ready) queueRef.current?.update(snapshot); }, [snapshot, ready]); const save = useCallback(() => { if (!ready) return; queueRef.current?.update(latestSnapshot.current); void queueRef.current?.flush(); }, [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]); return { ...state, save, ready }; }