Files
matrix-screen-controller/核桃派软件源代码/app/static/views/layers.js
T

122 lines
8.7 KiB
JavaScript

import { announce, apiJson } from "../core.js";
import { runControlAction, setControlReady, setControlUnavailable } from "../interaction-feedback.js";
import { layerToStorage, sceneToStorageValue } from "../scene-model.js";
import { getScene, getSelectedLayer, subscribeScene, selectLayer, changeLayer, setLayerBackground,
animationEditState, templateEditState, enterTemplateEdit, enterAnimationEdit, markTemplateSaved,
markAnimationFrameSaved } from "../scene-board.js";
import { initializeTextWorkspace } from "./text.js";
import { refreshTemplates } from "./templates.js";
import { refreshAnimations } from "./animations.js";
const byId=id=>document.getElementById(id);
const list=byId("layer-list"), background=byId("layer-background-enabled"), color=byId("background-color");
const dialog=byId("layer-copy-dialog"),target=byId("layer-copy-target"),feedback=byId("layer-copy-feedback");
const submit=byId("layer-copy-submit"),frames=byId("layer-copy-frame-list");
let source=null,targets=new Map(),targetDetail=null,loadSequence=0,busy=false;
function render({scene}) {
const selected=getSelectedLayer(),brush=selected.type==="brush";
byId("brush-layer-tools").hidden=!brush;byId("text-layer-tools").hidden=brush;
background.checked=Boolean(selected.background);
if(brush){color.value=selected.backgroundColor||selected.background||"#000000";color.dispatchEvent(new Event("input"));}
list.replaceChildren(...[...scene.layers].reverse().map(l=>{
const button=document.createElement("button");button.type="button";button.className="layer-item";
button.textContent=`${l.name} · ${l.type==="brush"?"画笔":"文字"}`;button.setAttribute("aria-pressed",String(l.id===selected.id));
button.addEventListener("click",()=>selectLayer(l.id));return button;
}));
const index=scene.layers.indexOf(selected);
for(const [id,allowed,reason] of [["delete-layer",!brush||scene.layers.filter(l=>l.type==="brush").length>1,"必须保留一个画笔图层。"],
["raise-layer",index<scene.layers.length-1,"已经在最上方。"],["lower-layer",index>0,"已经在最下方。"]]) {
if(allowed)setControlReady(byId(id));else setControlUnavailable(byId(id),reason);
}
if(!brush)initializeTextWorkspace().catch(e=>announce(e.message,true));
}
subscribeScene(render);
for(const [id,action,value] of [["new-brush-layer","create","brush"],["new-text-layer","create","text"],
["delete-layer","delete"],["raise-layer","up"],["lower-layer","down"]])byId(id).addEventListener("click",()=>changeLayer(action,value));
byId("rename-layer").addEventListener("click",()=>{
const name=window.prompt("图层名称(1..80个字符)",getSelectedLayer().name);
if(name!==null)try{changeLayer("rename",name);}catch(e){announce(e.message,true);}
});
background.addEventListener("change",()=>setLayerBackground(background.checked,color.value));
color.addEventListener("input",()=>{if(getSelectedLayer().type==="brush"&&getSelectedLayer().backgroundColor!==color.value)setLayerBackground(background.checked,color.value);});
function dirtyChoice(){
const d=document.createElement("dialog");d.className="layer-copy-dialog";
const title=document.createElement("h3");title.textContent="目标画面正在编辑,存在未保存修改";d.append(title);
for(const [value,label] of [["save","保存后复制"],["discard","放弃修改后复制"],["cancel","取消"]]){
const b=document.createElement("button");b.type="button";b.textContent=label;b.addEventListener("click",()=>d.close(value));d.append(b);
}
document.body.append(d);d.showModal();return new Promise(resolve=>d.addEventListener("close",()=>{const choice=d.returnValue||"cancel";d.remove();resolve(choice);},{once:true}));
}
async function loadFrames(){
const sequence=++loadSequence;targetDetail=null;frames.replaceChildren();byId("layer-copy-frames").hidden=true;
const ref=targets.get(target.value);
if(!ref){setControlReady(submit);return;}
setControlUnavailable(submit,"正在读取目标帧。");feedback.textContent="正在读取目标帧…";
try{
const detail=await apiJson(ref.type==="template"?`/api/templates/${ref.id}`:`/api/animations/${ref.id}/frames`);if(sequence!==loadSequence)return;
targetDetail=detail;
if(ref.type==="template"){feedback.textContent="副本默认置顶。";setControlReady(submit);return;}
byId("layer-copy-frames").hidden=false;
for(const [i,frame] of detail.frames.entries()) {
const label=document.createElement("label"),input=document.createElement("input");input.type="checkbox";input.value=frame.id;
const name=document.createElement("span");name.textContent=`${i+1}. ${frame.name||"未命名帧"}`;
label.append(input,name);frames.append(label);
}
feedback.textContent=detail.frames.length?"选择一个或多个目标帧,复制后置顶。":"目标动图没有帧,请先添加帧。";
if(detail.frames.length)setControlReady(submit);else setControlUnavailable(submit,"目标动图没有帧。");
}catch(e){if(sequence===loadSequence){feedback.textContent=e.message;setControlUnavailable(submit,"读取失败,请重新选择目标。");}}
}
byId("copy-layer").addEventListener("click",async()=>{
const sequence=++loadSequence;
source=layerToStorage(getSelectedLayer());targets=new Map();target.replaceChildren(new Option("当前画板","current"));
feedback.textContent="正在读取可复制目标…";dialog.showModal();setControlUnavailable(submit,"正在读取目标。");
try{
const [templates,animations]=await Promise.all([apiJson("/api/templates"),apiJson("/api/animations")]);
if(sequence!==loadSequence)return;
for(const [type,items] of [["template",templates.templates],["animation",animations.animations]])for(const item of items.filter(i=>!i.is_demo&&!i.read_only)){
const key=`${type}:${item.id}`;targets.set(key,{...item,type});target.append(new Option(`${type==="template"?"静态":"动图"} · ${item.name}`,key));
}
feedback.textContent="副本默认置顶,不改变原图层。";await loadFrames();
}catch(e){feedback.textContent=e.message;setControlReady(submit);}
});
target.addEventListener("change",loadFrames);
for(const [id,checked] of [["layer-copy-all",true],["layer-copy-none",false]])byId(id).addEventListener("click",()=>frames.querySelectorAll("input").forEach(input=>input.checked=checked));
byId("layer-copy-cancel").addEventListener("click",()=>{if(!busy){loadSequence++;dialog.close();}});
dialog.addEventListener("cancel",e=>{if(busy)e.preventDefault();else loadSequence++;});
submit.addEventListener("click",()=>runControlAction(submit,{message:"正在复制图层…"},async()=>{
busy=true;target.disabled=true;frames.inert=true;
try{
if(target.value==="current"){changeLayer("duplicate");dialog.close();announce("图层已复制到当前画板最上方");return;}
const ref=targets.get(target.value);if(!ref)throw Error("请选择有效目标");
const edit=ref.type==="template"?templateEditState():animationEditState();
// An ordinary template parked behind animation editing is not the active board.
const editing=edit&&(ref.type==="template"?edit.id===ref.id&&!animationEditState():edit.animationId===ref.id);
let savedRevision=null;
if(editing&&edit.dirty){
const choice=await dirtyChoice();if(choice==="cancel")return;
if(choice==="save"){
const path=ref.type==="template"?`/api/templates/${ref.id}`:`/api/animations/${ref.id}/frames/${edit.frameId}`;
const saved=await apiJson(path,{method:"PUT",headers:{"If-Match":`"${edit.revision}"`},body:JSON.stringify({scene:sceneToStorageValue(getScene())})});
if(ref.type==="template")markTemplateSaved(saved);else markAnimationFrameSaved(saved);
savedRevision=saved.revision;
}
}
const current=targetDetail;
if(!current)throw Error("目标状态已失效,请重新选择");
// Preserve the revision from frame selection: intervening writes must conflict.
const frameIds=Array.from(frames.querySelectorAll("input:checked"),input=>input.value);
if(ref.type==="animation"&&!frameIds.length)throw Error("请至少选择一个目标帧");
const revision=savedRevision||current.revision;
const result=await apiJson(ref.type==="template"?`/api/templates/${ref.id}/layers`:`/api/animations/${ref.id}/frames/layers`,{
method:"POST",headers:{"If-Match":`"${revision}"`},body:JSON.stringify({layer:source,...(ref.type==="animation"?{frame_ids:frameIds}:{})})});
if(editing){if(ref.type==="template")enterTemplateEdit(result);else{
const frame=await apiJson(`/api/animations/${ref.id}/frames/${edit.frameId}`);
enterAnimationEdit({...frame,animation_id:ref.id,animation_name:result.name,animation_revision:result.revision});
}}
dialog.close();announce("图层已复制到目标最上方");
await Promise.all([refreshTemplates(),refreshAnimations()]);
}catch(e){feedback.textContent=`复制失败:${e.message}`;announce(feedback.textContent,true);}
finally{busy=false;target.disabled=false;frames.inert=false;}
}));