82 lines
3.2 KiB
TypeScript
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 };
|
|
}
|