---
name: 管理自定义调色板 (i18n 增强版)
description: 弹出一个可视化的色彩管理面板,支持创建色组、添加/修改/删除颜色,并将配置自动保存为标准的国际化 JSON 格式。
author: ymjr
version: 2.1.0
license: MIT
usage: 直接运行此脚本以呼出调色板管理器悬浮窗口。
features:
- 完美支持多分组,每组可容纳任意数量的颜色
- 支持中英双语自适应显示与编辑,不破坏异国语言数据
- 组内支持颜色名即时输入、可视化颜色拾取器更改 Hex、单色删除
- 自动序列化为标准的国际化 JSON 结构保存至插件后台
dependencies:
- 依赖 扩展调色板核心引擎 提供底层解析支持
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
ui_title: "🎨 自定义调色板管理器",
ui_btn_add_group: "➕ 新建色组",
ui_btn_save: "💾 保存调色板配置",
ui_prompt_group_name: "请输入新色组的名称:",
ui_prompt_rename: "请输入色组的新名称:",
ui_confirm_delete_group: "确定要删除整个色组 \"{name}\" 及其内部的所有颜色吗?",
ui_default_group_name: "未命名色组",
ui_default_color_name: "新颜色",
ui_add_color: "✨ 添加颜色",
ui_empty_tip: "暂无自定义色组,请点击下方按钮创建",
ui_tooltip_rename: "重命名色组",
ui_tooltip_delete: "删除色组",
notice_saved: "✅ 调色板配置已成功保存!重启或刷新画板即可看到最新效果。",
setting_desc: "⚠️ 请不要在此处直接修改 JSON 代码。请运行 '管理自定义调色板' 脚本通过可视化面板进行增删改查。"
},
en: {
ui_title: "🎨 Custom Palette Manager",
ui_btn_add_group: "➕ New Group",
ui_btn_save: "💾 Save Configuration",
ui_prompt_group_name: "Enter new group name:",
ui_prompt_rename: "Enter new name for the group:",
ui_confirm_delete_group: "Are you sure you want to delete group \"{name}\" and all its colors?",
ui_default_group_name: "Unnamed Group",
ui_default_color_name: "New Color",
ui_add_color: "✨ Add Color",
ui_empty_tip: "No custom color groups found. Click the button below to create one.",
ui_tooltip_rename: "Rename Group",
ui_tooltip_delete: "Delete Group",
notice_saved: "✅ Palette saved successfully! Refresh Excalidraw to apply.",
setting_desc: "⚠️ Do not modify JSON directly. Use 'Manage Custom Palette' script UI to update."
}
};
const { Notice } = ea.obsidian;
// 🌐 动态获取当前 Obsidian 系统的语言环境
function getCurrentLanguage() {
let lang = "en";
if (typeof window !== "undefined") {
const obsidianLang = window.localStorage.getItem("language");
lang = obsidianLang || (navigator.language && navigator.language.startsWith("zh") ? "zh" : "en");
}
return lang.startsWith("zh") ? "zh" : "en";
}
// 🗺️ 国际化文本提取工具
function getLocalizedText(field, currentLang) {
if (!field) return "";
if (typeof field === "string") return field;
if (typeof field === "object") {
return field[currentLang] || field["en"] || Object.values(field)[0] || "";
}
return String(field);
}
// ✍️ 国际化文本安全写入/转换工具
function updateLocalizedField(field, text, currentLang) {
if (typeof field === "object" && field !== null) {
field[currentLang] = text;
return field;
} else {
// 如果原本是纯字符串老数据,平滑升级为多语言对象
const obj = { zh: String(field || text), en: String(field || text) };
obj[currentLang] = text;
return obj;
}
}
// 1. 读取并解析现有的配置状态
let engineSettings = ExcalidrawAutomate.plugin.settings.scriptEngineSettings["extra colors"] || {};
let paletteData = [];
try {
if (engineSettings["Palette JSON Config"] && engineSettings["Palette JSON Config"].value) {
paletteData = JSON.parse(engineSettings["Palette JSON Config"].value);
} else {
// 向下兼容读取老版本的配置串
const oldStr = engineSettings["Palette List"]?.value;
if(oldStr) {
paletteData = [{
"groupName": { "zh": "默认色组", "en": "Default Group" },
"colors": oldStr.split(",").map(i => {
const p = i.split(":");
return p.length >= 2 ? { name: p[0].trim(), value: p[1].trim() } : null;
}).filter(Boolean)
}];
} else {
paletteData = [];
}
}
} catch (e) {
paletteData = [];
}
// 2. UI 辅助函数
function createIconButton(text, onClick, title="", isCritical=false) {
const btn = document.createElement('button');
btn.innerHTML = text;
btn.title = title;
const activeColor = isCritical ? "var(--text-error)" : "var(--text-normal)";
btn.style.cssText = `background:var(--interactive-normal); border:1px solid var(--background-modifier-border); color:${activeColor}; border-radius:4px; cursor:pointer; padding:2px 6px; font-size:12px; display:inline-flex; align-items:center; justify-content:center;`;
btn.onmouseover = () => btn.style.background = "var(--interactive-hover)";
btn.onmouseout = () => btn.style.background = "var(--interactive-normal)";
btn.onclick = onClick;
return btn;
}
// 3. 主控制面板创建
function createPaletteManagerPanel() {
if (document.getElementById("ea-palette-manager-adv-panel")) return;
const currentLang = getCurrentLanguage(); // 获取当前环境语言
const panel = document.createElement('div');
panel.id = "ea-palette-manager-adv-panel";
panel.style.cssText = `position:fixed; top:120px; right:80px; width:380px; max-height:80vh; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 10px 30px rgba(0,0,0,0.3); border-radius:8px; z-index:9999; display:flex; flex-direction:column; overflow:hidden; color:var(--text-normal);`;
// 头部区域与拖拽
const header = document.createElement('div');
header.style.cssText = `padding:12px 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;`;
header.innerHTML = `${t("ui_title")}`;
panel.appendChild(header);
const content = document.createElement('div');
content.style.cssText = `padding:16px; display:flex; flex-direction:column; gap:16px; overflow-y:auto; flex:1;`;
panel.appendChild(content);
// 渲染色组核心区域
const renderGroups = () => {
content.innerHTML = "";
if (paletteData.length === 0) {
const emptyTip = document.createElement('div');
emptyTip.style.cssText = "text-align:center; color:var(--text-muted); font-size:12px; padding:20px 0;";
emptyTip.innerText = t("ui_empty_tip");
content.appendChild(emptyTip);
}
paletteData.forEach((group, gIndex) => {
const groupWrap = document.createElement('div');
groupWrap.style.cssText = `background:var(--background-secondary-alt); border:1px solid var(--background-modifier-border); border-radius:6px; padding:12px; display:flex; flex-direction:column; gap:10px;`;
// 组标题及工具栏
const titleRow = document.createElement('div');
titleRow.style.cssText = "display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--background-modifier-border); padding-bottom:6px;";
const titleText = document.createElement('span');
const currentGroupName = getLocalizedText(group.groupName, currentLang); // 🚀 I18n 读取组名
titleText.innerText = currentGroupName;
titleText.style.cssText = "font-weight:bold; font-size:13px; color:var(--text-accent);";
titleRow.appendChild(titleText);
const groupTools = document.createElement('div');
groupTools.style.cssText = "display:flex; gap:4px;";
// 重命名组
groupTools.appendChild(createIconButton("✏️", () => {
const currentName = getLocalizedText(group.groupName, currentLang);
const newName = prompt(t("ui_prompt_rename"), currentName);
if (newName && newName.trim()) {
// 🚀 I18n 安全写入组名
group.groupName = updateLocalizedField(group.groupName, newName.trim(), currentLang);
renderGroups();
}
}, t("ui_tooltip_rename")));
// 删除组
groupTools.appendChild(createIconButton("🗑️", () => {
const currentName = getLocalizedText(group.groupName, currentLang);
if (confirm(t("ui_confirm_delete_group", { name: currentName }))) {
paletteData.splice(gIndex, 1);
renderGroups();
}
}, t("ui_tooltip_delete"), true));
titleRow.appendChild(groupTools);
groupWrap.appendChild(titleRow);
// 颜色项列表
const colorsContainer = document.createElement('div');
colorsContainer.style.cssText = "display:flex; flex-direction:column; gap:6px;";
group.colors.forEach((colorItem, cIndex) => {
const colorRow = document.createElement('div');
colorRow.style.cssText = "display:flex; align-items:center; gap:8px; background:var(--background-primary); padding:4px 8px; border-radius:4px; border:1px solid var(--background-modifier-border);";
// 颜色选择器
const picker = document.createElement('input');
picker.type = "color";
picker.value = colorItem.value.startsWith("#") ? colorItem.value : "#ffffff";
picker.style.cssText = "width:24px; height:24px; padding:0; border:none; background:none; cursor:pointer; flex-shrink:0;";
picker.onchange = (e) => {
group.colors[cIndex].value = e.target.value.toUpperCase();
};
colorRow.appendChild(picker);
// 颜色名称输入框
const nameInput = document.createElement('input');
nameInput.type = "text";
nameInput.value = getLocalizedText(colorItem.name, currentLang); // 🚀 I18n 读取颜色名
nameInput.style.cssText = "flex:1; background:transparent; border:none; color:var(--text-normal); font-size:12px; padding:2px 4px;";
nameInput.onchange = (e) => {
const targetVal = e.target.value.trim() || t("ui_default_color_name");
// 🚀 I18n 安全写入颜色名
group.colors[cIndex].name = updateLocalizedField(group.colors[cIndex].name, targetVal, currentLang);
};
colorRow.appendChild(nameInput);
// 单色删除按钮
const delColorBtn = document.createElement('button');
delColorBtn.innerHTML = "✕";
delColorBtn.style.cssText = "background:none; border:none; cursor:pointer; color:var(--text-muted); font-size:11px; padding:2px 4px;";
delColorBtn.onmouseover = () => delColorBtn.style.color = "var(--text-error)";
delColorBtn.onmouseout = () => delColorBtn.style.color = "var(--text-muted)";
delColorBtn.onclick = () => {
group.colors.splice(cIndex, 1);
renderGroups();
};
colorRow.appendChild(delColorBtn);
colorsContainer.appendChild(colorRow);
});
groupWrap.appendChild(colorsContainer);
// 组内添加颜色按钮
const btnAddColor = document.createElement('button');
btnAddColor.innerText = t("ui_add_color");
btnAddColor.style.cssText = "align-self:flex-start; background:none; border:1px dashed var(--background-modifier-border); color:var(--text-muted); font-size:11px; padding:2px 8px; border-radius:4px; cursor:pointer;";
btnAddColor.onclick = () => {
// 🚀 创建标准的双语颜色对象
group.colors.push({
name: { zh: locales.zh.ui_default_color_name, en: locales.en.ui_default_color_name },
value: "#3B82F6"
});
renderGroups();
};
groupWrap.appendChild(btnAddColor);
content.appendChild(groupWrap);
});
};
// 底部控制按钮
const footer = document.createElement('div');
footer.style.cssText = `padding:12px 16px; border-top:1px solid var(--background-modifier-border); display:flex; justify-content:space-between; background:var(--background-secondary); border-radius: 0 0 8px 8px;`;
const btnAddGroup = document.createElement('button');
btnAddGroup.innerText = t("ui_btn_add_group");
btnAddGroup.onclick = () => {
const gName = prompt(t("ui_prompt_group_name"), t("ui_default_group_name"));
if(gName && gName.trim()) {
// 🚀 创建标准的双语色组对象
const newGroup = {
groupName: { zh: gName.trim(), en: gName.trim() },
colors: [{
name: { zh: locales.zh.ui_default_color_name, en: locales.en.ui_default_color_name },
value: "#EF4444"
}]
};
newGroup.groupName[currentLang] = gName.trim();
paletteData.push(newGroup);
renderGroups();
}
};
footer.appendChild(btnAddGroup);
const btnSave = document.createElement('button');
btnSave.innerText = t("ui_btn_save");
btnSave.className = "mod-cta";
btnSave.onclick = async () => {
let targetEngineSettings = ExcalidrawAutomate.plugin.settings.scriptEngineSettings["extra colors"] || {};
targetEngineSettings["Palette JSON Config"] = {
value: JSON.stringify(paletteData, null, 2),
description: t("setting_desc")
};
ExcalidrawAutomate.plugin.settings.scriptEngineSettings["extra colors"] = targetEngineSettings;
await ExcalidrawAutomate.plugin.saveSettings();
new Notice(t("notice_saved"));
panel.remove();
};
footer.appendChild(btnSave);
panel.appendChild(footer);
document.body.appendChild(panel);
renderGroups();
// 关闭及拖拽事件绑定
header.querySelector('#close-btn').onclick = () => panel.remove();
let isDragging = false, startX, startY, initLeft, initTop;
header.onmousedown = (e) => {
if(e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') 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; };
};
}
createPaletteManagerPanel();