*/
var locales = {
zh: {
notice_select: "请先选择至少一个元素",
ui_panel_title: "✨ 阴影图层管理器",
ui_static_title: "🌙 静态长亮阴影",
ui_hover_title: "🖱️ 鼠标悬停特效",
ui_hover_pointer: "👆 悬停时显示手指光标",
ui_layer: "图层",
ui_add_layer: "+ 添加新图层",
ui_done: "完成",
ui_editor_add: "✨ 新增",
ui_editor_edit: "✏️ 编辑",
ui_editor_layer_type: "图层",
ui_editor_static: "静态",
ui_editor_hover: "Hover",
ui_editor_color: "🎨 颜色/亮度差",
ui_editor_blur: "🌫️ 模糊 (Blur)",
ui_editor_x: "↔️ 水平偏移 (X)",
ui_editor_y: "↕️ 垂直偏移 (Y)",
ui_editor_tip: "<b>💡 格式提示:</b><br>• 负数(如 <code>-20</code>)表示基于原色加深。<br>• <code>0.1</code> 表示相对尺寸,<code>10px</code> 表示绝对像素。",
ui_editor_cancel: "取消",
ui_editor_save: "确认保存"
},
en: {
notice_select: "Please select at least one element",
ui_panel_title: "✨ Shadow Layer Manager",
ui_static_title: "🌙 Static Shadow",
ui_hover_title: "🖱️ Hover Effects",
ui_hover_pointer: "👆 Pointer cursor on hover",
ui_layer: "Layer",
ui_add_layer: "+ Add New Layer",
ui_done: "Done",
ui_editor_add: "✨ Add",
ui_editor_edit: "✏️ Edit",
ui_editor_layer_type: "Layer",
ui_editor_static: "Static",
ui_editor_hover: "Hover",
ui_editor_color: "🎨 Color/Brightness",
ui_editor_blur: "🌫️ Blur",
ui_editor_x: "↔️ Offset X",
ui_editor_y: "↕️ Offset Y",
ui_editor_tip: "<b>💡 Format Tips:</b><br>• Negative (e.g., <code>-20</code>) darkens base color.<br>• <code>0.1</code> for relative, <code>10px</code> for absolute.",
ui_editor_cancel: "Cancel",
ui_editor_save: "Save"
}
};
const { Notice } = ea.obsidian;
// ====================== 1. 数据初始化与解析 ======================
const selectedEls = ea.getViewSelectedElements();
if (selectedEls.length === 0) {
new Notice(t("notice_select"));
return;
}
const target = selectedEls[0];
const DEFAULT_SHADOW = { shadowColor: "rgba(0,0,0,0.3)", shadowBlur: "0.1", shadowOffsetX: "0", shadowOffsetY: "0.1" };
// 内部状态管理器 (不污染全局 window)
let state = {
staticEnabled: !!target.customData?.shadow,
staticShadows: [],
hoverEnabled: !!target.customData?.hover,
hoverPointer: target.customData?.hover?.pointer !== false,
hoverShadows: []
};
// 解析静态阴影
if (target.customData?.shadow) {
state.staticShadows = Array.isArray(target.customData.shadow)
? JSON.parse(JSON.stringify(target.customData.shadow))
: [JSON.parse(JSON.stringify(target.customData.shadow))];
}
// 解析 Hover 阴影
if (target.customData?.hover?.shadow) {
state.hoverShadows = Array.isArray(target.customData.hover.shadow)
? JSON.parse(JSON.stringify(target.customData.hover.shadow))
: [JSON.parse(JSON.stringify(target.customData.hover.shadow))];
}
// ====================== 2. 核心应用逻辑 ======================
async function applyToSelected() {
if (selectedEls.length === 0) return;
const api = ea.getExcalidrawAPI();
selectedEls.forEach((el) => {
api.ShapeCache?.cache?.delete(el);
let customData = { ...(el.customData || {}) };
// 应用静态阴影
if (state.staticEnabled && state.staticShadows.length > 0) {
customData.shadow = JSON.parse(JSON.stringify(state.staticShadows));
} else {
delete customData.shadow;
}
// 应用 Hover 阴影
if (state.hoverEnabled) {
customData.hover = {
...(customData.hover || {}),
id: customData.hover?.id || [el.id],
pointer: state.hoverPointer
};
if (state.hoverShadows.length > 0) {
customData.hover.shadow = JSON.parse(JSON.stringify(state.hoverShadows));
} else {
delete customData.hover.shadow;
}
} else {
if (customData.hover) {
delete customData.hover.shadow;
delete customData.hover.pointer;
if (Object.keys(customData.hover).filter(k => k !== 'id').length === 0) {
delete customData.hover;
}
}
}
el.customData = customData;
});
ea.copyViewElementsToEAforEditing(selectedEls);
await ea.addElementsToView(false, false, true);
api.updateScene({ appState: { zoom: { value: api.getAppState().zoom.value + 0.0001 } } });
}
// ====================== 3. UI 界面构建 ======================
function createFloatingPanel() {
if (document.getElementById("ea-multi-shadow-panel")) return;
// --- 主面板容器 ---
const panel = document.createElement('div');
panel.id = "ea-multi-shadow-panel";
panel.style.cssText = `position:fixed; top:100px; right:80px; width:340px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 12px 32px rgba(0,0,0,0.3); border-radius:10px; z-index:9999; display:flex; flex-direction:column; max-height:80vh; overflow:hidden; font-family: var(--font-interface);`;
// 头部
const header = document.createElement('div');
header.style.cssText = `padding:14px 16px; background:var(--background-secondary); cursor:move; border-bottom:1px solid var(--background-modifier-border); display:flex; justify-content:space-between; align-items:center; user-select:none; font-weight:bold; color:var(--text-normal);`;
header.innerHTML = `<span>${t("ui_panel_title")}</span><button id="close-btn" style="background:none; border:none; cursor:pointer; color:var(--text-muted); font-size:16px; padding:0;">✕</button>`;
panel.appendChild(header);
// 滚动内容区
const content = document.createElement('div');
content.style.cssText = `padding:16px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:20px; position:relative;`;
panel.appendChild(content);
// --- 动态渲染图层列表的工厂函数 ---
const renderSection = (title, modeKey) => {
const wrapper = document.createElement('div');
wrapper.id = `section-${modeKey}`;
wrapper.style.cssText = `border: 1px solid var(--background-modifier-border); border-radius: 8px; background:var(--background-primary-alt); overflow:hidden;`;
const shadowsArray = modeKey === 'static' ? state.staticShadows : state.hoverShadows;
const isEnabled = modeKey === 'static' ? state.staticEnabled : state.hoverEnabled;
let html = `
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px 12px; background:var(--background-secondary-alt); border-bottom: ${isEnabled ? '1px solid var(--background-modifier-border)' : 'none'};">
<strong style="color:var(--text-accent); font-size:13px;">${title}</strong>
<input type="checkbox" id="toggle-${modeKey}" ${isEnabled ? 'checked' : ''} style="cursor:pointer;">
</div>
`;
if (isEnabled) {
html += `<div style="padding:10px 12px; display:flex; flex-direction:column; gap:8px;">`;
// Hover 专属的 Pointer 开关
if (modeKey === 'hover') {
html += `
<label style="display:flex; justify-content:space-between; align-items:center; font-size:12px; color:var(--text-muted); background:var(--background-modifier-form-field); padding:6px 10px; border-radius:6px; cursor:pointer;">
<span>${t("ui_hover_pointer")}</span>
<input type="checkbox" id="toggle-pointer" ${state.hoverPointer ? 'checked' : ''}>
</label>
`;
}
// 图层列表
shadowsArray.forEach((sh, idx) => {
const colorDisp = String(sh.shadowColor).startsWith('#') || String(sh.shadowColor).startsWith('rgb')
? `<span style="display:inline-block; width:12px; height:12px; border-radius:50%; background:${sh.shadowColor}; border:1px solid var(--background-modifier-border);"></span>`
: `⚙️`; // 动态亮度计算
html += `
<div style="display:flex; justify-content:space-between; align-items:center; background:var(--background-primary); border:1px solid var(--background-modifier-border); border-radius:6px; padding:6px 10px; font-size:12px;">
<div style="display:flex; align-items:center; gap:8px;">
${colorDisp}
<span style="color:var(--text-normal);">${t("ui_layer")} ${idx + 1}</span>
</div>
<div style="display:flex; gap:6px;">
<button data-action="edit" data-mode="${modeKey}" data-idx="${idx}" style="background:none; border:none; cursor:pointer; color:var(--text-muted); padding:2px;">✏️</button>
<button data-action="delete" data-mode="${modeKey}" data-idx="${idx}" style="background:none; border:none; cursor:pointer; color:var(--text-error); padding:2px;">🗑️</button>
</div>
</div>
`;
});
// 添加按钮
html += `
<button data-action="add" data-mode="${modeKey}" style="width:100%; padding:6px; margin-top:4px; background:transparent; border:1px dashed var(--text-muted); color:var(--text-muted); border-radius:6px; cursor:pointer; font-size:12px; transition:all 0.2s;">
${t("ui_add_layer")}
</button>
</div>`;
}
wrapper.innerHTML = html;
return wrapper;
};
// --- 刷新内容区 ---
const refreshUI = () => {
content.innerHTML = '';
content.appendChild(renderSection(t("ui_static_title"), "static"));
content.appendChild(renderSection(t("ui_hover_title"), "hover"));
// 绑定事件 (事件委托)
content.onchange = (e) => {
if (e.target.id === 'toggle-static') { state.staticEnabled = e.target.checked; refreshUI(); applyToSelected(); }
if (e.target.id === 'toggle-hover') { state.hoverEnabled = e.target.checked; refreshUI(); applyToSelected(); }
if (e.target.id === 'toggle-pointer') { state.hoverPointer = e.target.checked; applyToSelected(); }
};
content.onclick = (e) => {
const btn = e.target.closest('button');
if (!btn) return;
const mode = btn.dataset.mode;
const action = btn.dataset.action;
const idx = parseInt(btn.dataset.idx);
if (action === 'delete') {
if (mode === 'static') state.staticShadows.splice(idx, 1);
else state.hoverShadows.splice(idx, 1);
refreshUI(); applyToSelected();
} else if (action === 'edit') {
showEditor(mode, idx);
} else if (action === 'add') {
showEditor(mode, -1);
}
};
};
// --- 弹出编辑器 UI ---
const showEditor = (mode, index) => {
const isNew = index === -1;
let shadowData = isNew
? { ...DEFAULT_SHADOW }
: (mode === 'static' ? { ...state.staticShadows[index] } : { ...state.hoverShadows[index] });
const editorOverlay = document.createElement('div');
editorOverlay.style.cssText = `position:absolute; top:0; left:0; width:100%; height:100%; background:var(--background-primary); z-index:20; padding:16px; display:flex; flex-direction:column; gap:15px; animation: slideIn 0.2s ease-out; box-sizing: border-box; border-top:1px solid var(--background-modifier-border);`;
// 动态注入动画
if (!document.getElementById('ea-shadow-anim')) {
const style = document.createElement('style');
style.id = 'ea-shadow-anim';
style.innerHTML = `@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }`;
document.head.appendChild(style);
}
const buildInputRow = (label, key) => `
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="font-size:12px; color:var(--text-muted);">${label}</label>
<input type="text" id="edit-${key}" value="${shadowData[key]}" style="width:120px; background:var(--background-modifier-form-field); border:1px solid var(--background-modifier-border); border-radius:4px; padding:4px 8px; font-size:12px; color:var(--text-normal);">
</div>
`;
editorOverlay.innerHTML = `
<div style="font-weight:bold; color:var(--text-accent); border-bottom:1px dashed var(--background-modifier-border); padding-bottom:8px;">
${isNew ? t("ui_editor_add") : t("ui_editor_edit")}${t("ui_editor_layer_type")} (${mode === 'static' ? t("ui_editor_static") : t("ui_editor_hover")})
</div>
<div style="display:flex; flex-direction:column; gap:12px; flex:1;">
${buildInputRow(t("ui_editor_color"), "shadowColor")}
${buildInputRow(t("ui_editor_blur"), "shadowBlur")}
${buildInputRow(t("ui_editor_x"), "shadowOffsetX")}
${buildInputRow(t("ui_editor_y"), "shadowOffsetY")}
<div style="padding:10px; background:var(--background-secondary-alt); border-radius:6px; font-size:11px; color:var(--text-muted); line-height:1.4; margin-top:auto;">
${t("ui_editor_tip")}
</div>
</div>
<div style="display:flex; gap:10px; margin-top:10px;">
<button id="edit-cancel" style="flex:1; padding:6px; background:var(--background-secondary); border:1px solid var(--background-modifier-border); border-radius:6px; cursor:pointer;">${t("ui_editor_cancel")}</button>
<button id="edit-save" style="flex:1; padding:6px; background:var(--interactive-accent); color:var(--text-on-accent); border:none; border-radius:6px; cursor:pointer;">${t("ui_editor_save")}</button>
</div>
`;
content.appendChild(editorOverlay);
// 绑定编辑器按钮
editorOverlay.querySelector('#edit-cancel').onclick = () => editorOverlay.remove();
editorOverlay.querySelector('#edit-save').onclick = () => {
shadowData.shadowColor = editorOverlay.querySelector('#edit-shadowColor').value;
shadowData.shadowBlur = editorOverlay.querySelector('#edit-shadowBlur').value;
shadowData.shadowOffsetX = editorOverlay.querySelector('#edit-shadowOffsetX').value;
shadowData.shadowOffsetY = editorOverlay.querySelector('#edit-shadowOffsetY').value;
const targetArray = mode === 'static' ? state.staticShadows : state.hoverShadows;
if (isNew) targetArray.push(shadowData);
else targetArray[index] = shadowData;
editorOverlay.remove();
refreshUI();
applyToSelected();
};
};
refreshUI();
// 底部完成按钮
const footer = document.createElement('div');
footer.style.cssText = `padding:12px 16px; border-top:1px solid var(--background-modifier-border); display:flex; justify-content:flex-end; background:var(--background-secondary);`;
const btnClose = document.createElement('button');
btnClose.innerText = t("ui_done");
btnClose.style.cssText = `background:var(--interactive-accent); color:var(--text-on-accent); border:none; padding:6px 16px; border-radius:6px; cursor:pointer; font-weight:bold;`;
btnClose.onclick = () => panel.remove();
footer.appendChild(btnClose);
panel.appendChild(footer);
document.body.appendChild(panel);
header.querySelector('#close-btn').onclick = () => panel.remove();
// 拖拽逻辑
let isDragging = false, startX, startY, initLeft, initTop;
header.onmousedown = (e) => {
if(e.target.tagName === 'BUTTON') return;
isDragging = true; startX = e.clientX; startY = e.clientY;
const rect = panel.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top;
e.preventDefault();
document.onmousemove = (e) => {
if(!isDragging) return;
panel.style.left = (initLeft + e.clientX - startX) + 'px';
panel.style.top = (initTop + e.clientY - startY) + 'px';
};
document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; };
};
}
createFloatingPanel();