ai-chatbot/hooks/use-video-canvas-persistence.ts

82 lines
3.2 KiB
TypeScript

"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<VideoCanvasSaveQueue | null>(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 };
}