Restore default template binding across browser sessions

This commit is contained in:
2026-10-10 10:51:59 +08:00
parent 608229a497
commit 5db283c05a
20 changed files with 334 additions and 8 deletions
@@ -1,5 +1,6 @@
import { attachScene, createLayer, cloneLayer, cloneScene, composeScene, clipTextPixels, translatedOverflow, clipBrushPixels, textClipAt, normalizeTextElement, layerToStorage } from "./scene-model.js";
import { announce, apiBlob, isAppStale } from "./core.js";
import { announce, apiBlob, apiJson, isAppStale } from "./core.js";
import { readBoardSession, writeBoardSession, loadDefaultBoard, initializeDefaultBoard } from "./board-startup-model.js";
import { renderUiCopy, renderUiValue } from "./ui-copy-runtime.js";
import {
RGB_BYTE_LENGTH,
@@ -102,10 +103,61 @@ let lastExactRgb = null;
let sceneSource = null;
let draftConflict = false;
let externalDraftValue = null;
let externalDraftContext = null;
let draftPersistenceBlocked = !draftLoad.canPersist;
let draftProtectionNotice = (
draftPersistenceBlocked || draftLoad.status === "cleanup-warning"
) ? draftLoad.notice : "";
const deviceBootId = document.querySelector('meta[name="device-boot-id"]')?.content || "";
let boardBootId = "";
let boardMutation = 0;
let startupBoardPromise = null;
if (draftLoad.canPersist && deviceBootId) {
try {
const session = readBoardSession(draftStorage, serializeScene(scene), deviceBootId);
boardBootId = session.bootId || "";
templateEdit = session.context;
if (templateEdit && !deserializeScene(templateEdit.baseline)) throw new Error("画板保存基线无效;原草稿已保留。");
if (templateEdit && !sceneNeedsTemplateSave(templateEdit, serializeScene(scene)) && templateEdit.type === "template") {
sceneSource = { type: "template", id: templateEdit.id, revision: templateEdit.revision };
}
} catch (error) {
templateEdit = null;
draftPersistenceBlocked = true;
draftProtectionNotice = error.message;
}
}
export function initializeStartupBoard() {
if (startupBoardPromise) return startupBoardPromise;
const mutation = boardMutation;
const canLoad = () => Boolean(deviceBootId) && boardBootId !== deviceBootId
&& !draftPersistenceBlocked && !draftConflict && !animationEdit && boardMutation === mutation;
const retain = () => {
if (draftPersistenceBlocked || draftConflict || animationEdit) return;
boardBootId = deviceBootId;
persistScene();
};
startupBoardPromise = initializeDefaultBoard({
canLoad,
load: () => loadDefaultBoard(apiJson),
apply: (content) => {
// Validate before changing either the session marker or the current draft.
if (!deserializeScene(content.scene)) throw new Error("默认画面场景无效");
boardBootId = deviceBootId;
replaceSceneFromStorage(content.scene, {
reason: "startup-default-load",
source: content.type === "template" ? { type: "template", id: content.id, revision: content.revision } : null,
templateEditContext: content,
});
},
retain,
}).catch((error) => {
announce(`默认画面读取失败,已保留当前草稿:${error.message}`, true);
return false;
});
return startupBoardPromise;
}
const draftConflictNotice = document.getElementById("draft-conflict-notice");
const loadExternalDraftButton = document.getElementById("load-external-draft");
@@ -122,6 +174,7 @@ function blockDraftPersistence(message) {
}
export function persistScene() {
boardMutation += 1;
if (animationEdit) {
animationEdit.dirty = serializeScene(scene) !== animationEdit.baseline;
updateBoardUi();
@@ -131,7 +184,10 @@ export function persistScene() {
if (draftConflict || draftPersistenceBlocked) return false;
try {
if (!draftStorage) throw new Error("browser storage unavailable");
draftStorage.setItem(SCENE_STORAGE_KEY, serializeScene(scene));
const draft = serializeScene(scene);
// Commit metadata first: a torn pair never restores the wrong binding.
writeBoardSession(draftStorage, boardBootId, draft, templateEdit);
draftStorage.setItem(SCENE_STORAGE_KEY, draft);
return true;
} catch (_error) {
blockDraftPersistence("浏览器无法保存统一画板草稿;原草稿已保留,本页不会继续覆盖或应用它。");
@@ -157,6 +213,12 @@ window.addEventListener("storage", (event) => {
return;
}
if (external.status !== "valid") return;
try {
externalDraftContext = readBoardSession(draftStorage, event.newValue, deviceBootId).context;
} catch (error) {
blockDraftPersistence(error.message);
return;
}
externalDraftValue = event.newValue;
setDraftConflict(true);
announce("另一标签页修改了画板草稿,请选择要保留的版本", true);
@@ -164,11 +226,12 @@ window.addEventListener("storage", (event) => {
loadExternalDraftButton.addEventListener("click", async () => {
const value = externalDraftValue;
const context = externalDraftContext;
if (!value) return;
if (animationEdit && !await sceneReplacementGuard()) return;
externalDraftValue = null;
setDraftConflict(false);
replaceSceneFromStorage(value, { persist: false, reason: "tab-sync" });
replaceSceneFromStorage(value, { persist: false, reason: "tab-sync", templateEditContext: context });
announce("已载入另一标签页的最新画板草稿");
});
@@ -670,6 +733,7 @@ export function replaceSceneFromStorage(
templateEditContext = null,
} = {},
) {
boardMutation += 1;
const restored = deserializeScene(value);
if (!restored) throw new Error("模板场景无效,无法载入");
textLayers.forEach((entry) => {
@@ -719,6 +783,8 @@ export function markTemplateSaved(template) {
clearEditHistory();
templateEdit = createTemplateEditContext(template, serializeScene(scene));
sceneSource = { type: "template", id: template.id, revision: template.revision };
boardBootId = deviceBootId;
persistScene();
updateBoardUi();
document.dispatchEvent(new CustomEvent("matrix:template-edit-change", { detail: templateEditState() }));
return true;