--- name: 动态素材库管理器 description: 动态加载或卸载 Excalidraw 素材库 (跨窗口持久化记录,智能重建防误删)。 author: ymjr version: 1.0.0 license: MIT usage: 运行脚本呼出面板。全键盘操作:输入过滤 -> 上下键选择 -> 回车智能切换(加载/卸载) -> Esc退出。 features: - 利用全局 Map 跨窗口持久化记录当前会话已加载的素材库 - 去除危险的 Replace 模式,采用安全重组内存的方式实现卸载 - 支持回车或双击智能 Toggle (加载/卸载) --- /* ```javascript */ const SCRIPT_ID = "ymjr.feature.dynamic-library-manager"; var locales = { zh: { settings_desc: "个人扩展素材库的所在文件夹路径", modal_title: "📚 动态素材库管理器", placeholder_search: "输入名称模糊搜索...", btn_cancel: "关闭 (Esc)", btn_unload: "卸载 (Del)", btn_load: "加载 (Enter)", empty_list: "未找到匹配的素材库", tag_loaded: "🟢 已加载", notice_load_success: "🎉 成功加载: {name}", notice_unload_success: "🗑️ 成功卸载: {name}", notice_error: "❌ 操作失败,请检查文件格式" }, en: { settings_desc: "Extra Personal library folder path", modal_title: "📚 Dynamic Library Manager", placeholder_search: "Type to search...", btn_cancel: "Close (Esc)", btn_unload: "Unload (Del)", btn_load: "Load (Enter)", empty_list: "No matching library files", tag_loaded: "🟢 Loaded", notice_load_success: "🎉 Successfully loaded: {name}", notice_unload_success: "🗑️ Successfully unloaded: {name}", notice_error: "❌ Operation failed. Check file format." } }; // 1. 初始化命名空间与全局持久化状态 if (!ExcalidrawAutomate.plugin._ymjr_dynLib) { ExcalidrawAutomate.plugin._ymjr_dynLib = { isPatched: false, originalSetLibrary: null, dynamicItemsMap: new Map() // 【核心】跨窗口持久化存储动态加载的素材项 }; } function fuzzyMatch(text, query) { text = text.toLowerCase(); query = query.toLowerCase(); let textIdx = 0; let queryIdx = 0; while (textIdx < text.length && queryIdx < query.length) { if (text[textIdx] === query[queryIdx]) { queryIdx++; } textIdx++; } return queryIdx === query.length; } // 2. 核心功能类封装 class DynamicLibraryManager { constructor(ea, api, app) { this.ea = ea; this.api = api; this.app = app; this.state = ExcalidrawAutomate.plugin._ymjr_dynLib; // 引用全局状态 this.libraryPath = ""; this.files = []; } async init() { await this.ensureSettings(); this.setupInterceptor(); this.fetchLibraryFiles(); this.renderUI(); } async ensureSettings() { let settings = this.ea.getScriptSettings(); if (!settings["extra library path"]) { settings = { "extra library path": { value: "Excalidraw/Libraries", description: t("settings_desc") } }; this.ea.setScriptSettings(settings); } this.libraryPath = settings["extra library path"].value.replace(/\\/g, '/'); } // 拦截器:防止动态注入的库被错误写入持久化配置文件中 setupInterceptor() { if (!this.state.isPatched) { this.state.originalSetLibrary = ExcalidrawAutomate.plugin.setStencilLibrary.bind(ExcalidrawAutomate.plugin); ExcalidrawAutomate.plugin.setStencilLibrary = async (library) => { try { const libraryItems = library.libraryItems.filter(item => ["unpublished", "published"].includes(item.status) ); const saveLib = { ...library, libraryItems: libraryItems }; return this.state.originalSetLibrary(saveLib); } catch (e) { console.error(`[${SCRIPT_ID}] Intercept Error:`, e); return this.state.originalSetLibrary(library); } }; this.state.isPatched = true; } } fetchLibraryFiles() { let allFiles = this.app.vault.getFiles(); this.files = allFiles.filter(f => f.path.startsWith(this.libraryPath) && ['json', 'excalidrawlib'].includes(f.extension) ); } // 核心重组逻辑:合并“永久素材”与“仍在Map中的动态素材”覆写画布内存 async rebuildLibrary() { // 1. 获取用户本地真正的永久素材库 let permanentItems = []; const lib2 = ExcalidrawAutomate.plugin.settings.library2; if (lib2 && lib2.libraryItems) { permanentItems = lib2.libraryItems.filter(item => ["unpublished", "published"].includes(item.status)); } else if (ExcalidrawAutomate.plugin.settings.library && ExcalidrawAutomate.plugin.settings.library.libraryItems) { permanentItems = ExcalidrawAutomate.plugin.settings.library.libraryItems.filter(item => ["unpublished", "published"].includes(item.status)); } // 2. 获取当前会话下所有活跃的动态素材 let dynamicItems = []; for (const items of this.state.dynamicItemsMap.values()) { dynamicItems.push(...items); } // 3. 组合并强行覆盖画布内存 (实现无痕添加和精准删除) const assembledItems = [...permanentItems, ...dynamicItems]; this.api.updateLibrary({ libraryItems: assembledItems, prompt: false, merge: false, // 必须是 false,才能剔除被删掉的库 openLibraryMenu: true }); } async executeLoad(file) { try { const content = await this.app.vault.read(file); const lib = JSON.parse(content); let libraryItems = lib.libraryItems || lib.library || []; if (!libraryItems.length) throw new Error("Empty library structure"); libraryItems.forEach(el => el.status = file.basename); // 存入全局 Map this.state.dynamicItemsMap.set(file.basename, libraryItems); await this.rebuildLibrary(); new Notice(t("notice_load_success", { name: file.basename })); this.renderListUIOnly(); } catch (error) { console.error(`[${SCRIPT_ID}] Load Error:`, error); new Notice(t("notice_error")); } } async executeUnload(file) { if (!this.state.dynamicItemsMap.has(file.basename)) return; try { // 从全局 Map 移除 this.state.dynamicItemsMap.delete(file.basename); await this.rebuildLibrary(); new Notice(t("notice_unload_success", { name: file.basename })); this.renderListUIOnly(); } catch (error) { console.error(`[${SCRIPT_ID}] Unload Error:`, error); new Notice(t("notice_error")); } } renderUI() { const existingPanel = document.getElementById("ymjr-dynlib-container"); if (existingPanel) existingPanel.remove(); const container = document.createElement("div"); container.id = "ymjr-dynlib-container"; this.container = container; const style = ` #ymjr-dynlib-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px); display: flex; justify-content: center; align-items: flex-start; padding-top: 10vh; z-index: 9999; } .ymjr-modal { background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: 8px; box-shadow: var(--shadow-l); width: 450px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; font-family: var(--font-interface); } .ymjr-header { padding: 16px; border-bottom: 1px solid var(--background-modifier-border); font-size: 1.2em; font-weight: 600; color: var(--text-normal); } .ymjr-search-box { padding: 12px 16px; border-bottom: 1px solid var(--background-modifier-border); background: var(--background-primary-alt); } .ymjr-search-input { width: 100%; padding: 8px 12px; border-radius: 6px; background: var(--background-modifier-form-field); border: 1px solid var(--background-modifier-border); color: var(--text-normal); font-size: 1em; outline: none; transition: box-shadow 0.2s, border-color 0.2s; } .ymjr-search-input:focus { border-color: var(--interactive-accent); box-shadow: 0 0 0 2px var(--background-modifier-active-hover); } .ymjr-file-list { flex-grow: 1; overflow-y: auto; padding: 8px; max-height: 350px; scroll-behavior: smooth; } .ymjr-file-item { padding: 10px 12px; margin: 2px 0; border-radius: 6px; cursor: pointer; transition: background 0.1s; color: var(--text-normal); border: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; } .ymjr-file-item.selected { background: var(--interactive-accent); color: var(--text-on-accent); font-weight: 500; } .ymjr-tag-loaded { font-size: 0.8em; padding: 2px 6px; border-radius: 4px; background: rgba(40, 200, 100, 0.2); color: #28c864; font-weight: bold; } .ymjr-file-item.selected .ymjr-tag-loaded { background: rgba(255, 255, 255, 0.2); color: white; } .ymjr-footer { padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--background-modifier-border); background: var(--background-primary); } .ymjr-footer-right { display: flex; gap: 8px; } .ymjr-btn { padding: 6px 14px; border-radius: 4px; cursor: pointer; border: none; font-weight: 500; } .ymjr-btn-cancel { background: transparent; color: var(--text-muted); border: 1px solid var(--background-modifier-border); } .ymjr-btn-cancel:hover { background: var(--background-modifier-hover); color: var(--text-normal); } .ymjr-btn-danger { background: rgba(220, 50, 50, 0.1); color: #e63c3c; border: 1px solid rgba(220, 50, 50, 0.3); } .ymjr-btn-danger:hover { background: rgba(220, 50, 50, 0.2); } .ymjr-btn-confirm { background: var(--interactive-accent); color: var(--text-on-accent); } .ymjr-btn-confirm:hover { background: var(--interactive-accent-hover); } .ymjr-btn:disabled { opacity: 0.4; cursor: not-allowed; } `; container.innerHTML = `