更改第三方插件名字的插件

还可以改图标
还可以改中文名

const { Plugin, PluginSettingTab, Setting, FuzzySuggestModal, setIcon, getIconIds } = require('obsidian');

const DEFAULT_SETTINGS = {
    names: {},
    icons: {}
}

// --- 图标搜索选择弹窗 ---
class IconPickerModal extends FuzzySuggestModal {
    constructor(app, pluginId, plugin, onChoose) {
        super(app);
        this.pluginId = pluginId;
        this.plugin = plugin;
        this.onChoose = onChoose;
        this.setPlaceholder("搜索图标 (输入英文, 如 'star', 'settings', 'image')...");
    }

    getItems() {
        return ["恢复默认状态", ...getIconIds()];
    }

    getItemText(item) {
        return item;
    }

    renderSuggestion(match, el) {
        const iconName = match.item;

        el.style.display = "flex";
        el.style.alignItems = "center";
        el.style.gap = "10px";
        
        if (iconName === "恢复默认状态") {
            el.createSpan({ text: "🔄 恢复默认 (核心插件恢复原版 / 第三方恢复拼图)" });
            return;
        }

        const iconContainer = el.createDiv();
        setIcon(iconContainer, iconName);
        el.createSpan({ text: iconName });
    }

    onChooseItem(item, evt) {
        this.onChoose(item === "恢复默认状态" ? null : item);
    }
}

// --- 主插件逻辑 ---
class PluginRenamer extends Plugin {
    async onload() {
        await this.loadSettings();
        
        // 核心解法:监听设置面板打开,注入纯 DOM 级的 MutationObserver 监控器
        this.patchSettingOpen();
        
        this.app.workspace.onLayoutReady(() => {
            // 兜底:如果重载时面板本来就是开着的,立刻应用一次
            setTimeout(() => {
                if (document.querySelector('.vertical-tab-header')) {
                    this.applyToExistingTabs();
                    this.setupMutationObserver();
                }
            }, 500);
        });

        this.addSettingTab(new PluginRenamerSettingTab(this.app, this));
    }

    onunload() {
        this.unpatchSettingOpen();
        this.restoreExistingTabs();
        if (this.mutationObserver) {
            this.mutationObserver.disconnect();
            this.mutationObserver = null;
        }
    }

    async loadSettings() {
        this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
    }

    async saveSettings() {
        await this.saveData(this.settings);
    }

    // 劫持设置面板的打开动作,借机挂载雷达
    patchSettingOpen() {
        if (!this.app.setting) return;
        this.originalSettingOpen = this.app.setting.open;
        const self = this;
        
        this.app.setting.open = function() {
            const result = self.originalSettingOpen.apply(this, arguments);
            // 留出 50ms 等待 React 初次把 HTML 画出来
            setTimeout(() => {
                self.applyToExistingTabs();
                self.setupMutationObserver();
            }, 50);
            return result;
        };
    }

    unpatchSettingOpen() {
        if (this.app.setting && this.originalSettingOpen) {
            this.app.setting.open = this.originalSettingOpen;
        }
    }

    // 致敬参考代码:纯 DOM 级别 MutationObserver 监听器 (彻底解决“有的有没有的没”和闪烁问题)
    setupMutationObserver() {
        if (this.mutationObserver) {
            this.mutationObserver.disconnect();
        }

        const header = document.querySelector('.vertical-tab-header');
        if (!header) return;

        this.mutationObserver = new MutationObserver((mutations) => {
            mutations.forEach(m => {
                // 只要 React 敢插入新节点,我们就立刻处理
                m.addedNodes.forEach(node => {
                    if (node instanceof HTMLElement) {
                        if (node.classList.contains("vertical-tab-nav-item") && node.hasAttribute("data-setting-id")) {
                            this.applyIconToNavItem(node);
                        } else {
                            // 防范未来 Obsidian 可能会包裹一个外层 div 的情况
                            node.querySelectorAll?.(".vertical-tab-nav-item[data-setting-id]").forEach(tab => {
                                this.applyIconToNavItem(tab);
                            });
                        }
                    }
                });
            });
        });

        // 监听整个侧边栏的所有子树变动
        this.mutationObserver.observe(header, { childList: true, subtree: true });
    }

    // 扫描屏幕上现存的节点
    applyToExistingTabs() {
        const header = document.querySelector('.vertical-tab-header');
        if (!header) return;

        header.querySelectorAll(".vertical-tab-nav-item[data-setting-id]").forEach(tabEl => {
            this.applyIconToNavItem(tabEl);
        });
    }

    // 精确的纯 DOM 修改器
    applyIconToNavItem(tabEl) {
        const pluginId = tabEl.getAttribute("data-setting-id");
        if (!pluginId) return;

        // 判断是核心还是第三方
        const manifests = this.app.plugins.manifests;
        const isThirdParty = !!manifests[pluginId];

        const customName = this.settings.names[pluginId];
        let targetIcon = this.settings.icons[pluginId];

        // 默认规则:没设置的,第三方用拼图,核心为空(保留原生图标)
        if (targetIcon === undefined) {
            targetIcon = isThirdParty ? 'puzzle' : null;
        }

        // ---------------- 1. 处理名称 ----------------
        if (customName && customName.trim() !== "") {
            // 在修改前备份原名
            if (!tabEl.dataset.originalName) {
                const walker = document.createTreeWalker(tabEl, NodeFilter.SHOW_TEXT, null, false);
                let node;
                while ((node = walker.nextNode())) {
                    if (node.nodeValue.trim() !== "") {
                        tabEl.dataset.originalName = node.nodeValue;
                        break;
                    }
                }
            }
            if (tabEl.hasAttribute('aria-label') && tabEl.getAttribute('aria-label') !== customName) {
                tabEl.setAttribute('aria-label', customName);
            }
            const walker = document.createTreeWalker(tabEl, NodeFilter.SHOW_TEXT, null, false);
            let node;
            while ((node = walker.nextNode())) {
                if (node.nodeValue.trim() !== "") {
                    if (node.nodeValue !== customName) node.nodeValue = customName;
                    break;
                }
            }
        } else {
            // 清除时还原名称
            if (tabEl.dataset.originalName) {
                const walker = document.createTreeWalker(tabEl, NodeFilter.SHOW_TEXT, null, false);
                let node;
                while ((node = walker.nextNode())) {
                    if (node.nodeValue.trim() !== "") {
                        node.nodeValue = tabEl.dataset.originalName;
                        break;
                    }
                }
                if (tabEl.hasAttribute('aria-label')) {
                    tabEl.setAttribute('aria-label', tabEl.dataset.originalName);
                }
                delete tabEl.dataset.originalName;
            }
        }

        // ---------------- 2. 处理图标 ----------------
        // 强制为纯文本结构的第三方插件添加 Flex 布局,保证图文并排
        if (isThirdParty) {
            tabEl.style.display = "flex";
            tabEl.style.alignItems = "center";
        }

        // 找到我们自己创建的图标容器 和 原生可能自带的容器
        let customIconEl = tabEl.querySelector('.vertical-tab-nav-item-icon.custom-icon');
        let nativeIconEl = tabEl.querySelector('.vertical-tab-nav-item-icon:not(.custom-icon)');

        if (!targetIcon) {
            // 需要恢复为默认状态:删除我们的自定义容器,把原生容器重新显示出来
            if (customIconEl) customIconEl.remove();
            if (nativeIconEl) nativeIconEl.style.display = '';
            return;
        }

        // 如果还没有我们的自定义图标容器,就造一个进去
        if (!customIconEl) {
            if (nativeIconEl) nativeIconEl.style.display = 'none'; // 把原生的藏起来

            customIconEl = document.createElement('div');
            customIconEl.classList.add("vertical-tab-nav-item-icon", "custom-icon");

            // 同样只针对第三方微调一下边距
            if (isThirdParty) {
                customIconEl.style.marginRight = '8px';
                customIconEl.style.display = 'flex';
                customIconEl.style.alignItems = 'center';
                customIconEl.style.justifyContent = 'center';
            }

            let firstNode = tabEl.firstChild;
            firstNode ? tabEl.insertBefore(customIconEl, firstNode) : tabEl.appendChild(customIconEl);
        } else {
            // 如果容器已经在了,确保原生图标一直藏着
            if (nativeIconEl) nativeIconEl.style.display = 'none';
        }

        // 只有图案不同的时候才重绘 SVG,性能极高!
        if (customIconEl.dataset.icon !== targetIcon) {
            customIconEl.innerHTML = '';
            setIcon(customIconEl, targetIcon);
            customIconEl.dataset.icon = targetIcon;
        }
    }

    restoreExistingTabs() {
        const header = document.querySelector('.vertical-tab-header');
        if (!header) return;

        header.querySelectorAll(".vertical-tab-nav-item[data-setting-id]").forEach(tabEl => {
            // 恢复原版名称
            if (tabEl.dataset.originalName) {
                const walker = document.createTreeWalker(tabEl, NodeFilter.SHOW_TEXT, null, false);
                let node;
                while ((node = walker.nextNode())) {
                    if (node.nodeValue.trim() !== "") {
                        node.nodeValue = tabEl.dataset.originalName;
                        break;
                    }
                }
                if (tabEl.hasAttribute('aria-label')) {
                    tabEl.setAttribute('aria-label', tabEl.dataset.originalName);
                }
                delete tabEl.dataset.originalName;
            }

            // 移除自定义图标,放回原生图标
            let customIconEl = tabEl.querySelector('.vertical-tab-nav-item-icon.custom-icon');
            if (customIconEl) customIconEl.remove();

            let nativeIconEl = tabEl.querySelector('.vertical-tab-nav-item-icon:not(.custom-icon)');
            if (nativeIconEl) nativeIconEl.style.display = '';

            // 清理强制附加的第三方样式
            const pluginId = tabEl.getAttribute("data-setting-id");
            if (pluginId && this.app.plugins.manifests && this.app.plugins.manifests[pluginId]) {
                tabEl.style.display = '';
                tabEl.style.alignItems = '';
            }
        });
    }
}

// --- 设置页面 ---
class PluginRenamerSettingTab extends PluginSettingTab {
    constructor(app, plugin) {
        super(app, plugin);
        this.plugin = plugin;
    }

    display() {
        const { containerEl } = this;
        containerEl.empty();

        containerEl.createEl('h2', { text: '⚙️ 插件名称与图标自定义' });
        containerEl.createEl('p', { text: '点击左侧按钮更改图标,右侧更改名称。支持修改所有核心和第三方插件,即改即生效!', cls: "setting-item-description" });

        // 获取并合并所有设置菜单(兼容核心 + 社区插件)
        const pluginTabs = this.app.setting.pluginTabs || [];
        const settingTabs = this.app.setting.settingTabs ||[];
        const manifests = this.app.plugins.manifests;
        
        let allTabs = [...settingTabs, ...pluginTabs];

        const sortedTabs = allTabs.sort((a, b) => {
            const nameA = manifests[a.id]?.name || a.name || a.id;
            const nameB = manifests[b.id]?.name || b.name || b.id;
            return nameA.localeCompare(nameB);
        });

        sortedTabs.forEach((tab) => {
            if (tab.id === this.plugin.manifest.id) return; // 隐藏自己

            const pluginId = tab.id;
            const isThirdParty = manifests && !!manifests[pluginId];
            const originalName = manifests[pluginId]?.name || tab.name || pluginId;
            const fallbackIcon = isThirdParty ? 'puzzle' : (tab.icon || 'box'); 

            const setting = new Setting(containerEl)
                .setDesc(`ID: ${pluginId}`)
                .addExtraButton(btn => {
                    const currentIcon = this.plugin.settings.icons[pluginId];
                    const displayIcon = currentIcon !== undefined ? currentIcon : fallbackIcon; 

                    btn.setIcon(displayIcon || 'image')
                       .setTooltip("更改侧边栏图标")
                       .onClick(() => {
                           new IconPickerModal(this.app, pluginId, this.plugin, async (selectedIcon) => {
                               if (selectedIcon === null) {
                                   delete this.plugin.settings.icons[pluginId];
                                   btn.setIcon(fallbackIcon || 'image'); 
                                   setIcon(iconEl, fallbackIcon || 'image'); 
                               } else {
                                   this.plugin.settings.icons[pluginId] = selectedIcon;
                                   btn.setIcon(selectedIcon);
                                   setIcon(iconEl, selectedIcon);
                               }
                               await this.plugin.saveSettings();
                               // ★ 核心:只需触发遍历一次屏幕元素,它立刻自己修补! ★
                               this.plugin.applyToExistingTabs();
                           }).open();
                       });
                });
                
            const currentIcon = this.plugin.settings.icons[pluginId];
            const displayIcon = currentIcon !== undefined ? currentIcon : fallbackIcon;

            const iconEl = document.createElement('span');
            iconEl.style.marginRight = '8px';
            iconEl.style.display = 'inline-flex';
            iconEl.style.alignItems = 'center';
            setIcon(iconEl, displayIcon || 'image');
            
            const nameSpan = document.createElement('span');
            nameSpan.textContent = this.plugin.settings.names[pluginId] || originalName;

            setting.nameEl.appendChild(iconEl);
            setting.nameEl.appendChild(nameSpan);
            
            setting.addText(text => text
                .setPlaceholder('输入想显示的中文...')
                .setValue(this.plugin.settings.names[pluginId] || '')
                .onChange(async (value) => {
                    this.plugin.settings.names[pluginId] = value;
                    await this.plugin.saveSettings();

                    const targetName = value.trim() !== "" ? value : originalName;
                    nameSpan.textContent = targetName;
                    this.plugin.applyToExistingTabs();
                })
            );
        });
    }
}

module.exports = PluginRenamer;
3 个赞