122 lines
8.7 KiB
JavaScript
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;}
|
|
}));
|