diff --git a/plugin/src/settings/inject.js b/plugin/src/settings/inject.js index fd3778d..36434b3 100644 --- a/plugin/src/settings/inject.js +++ b/plugin/src/settings/inject.js @@ -2,6 +2,9 @@ const { setIcon } = require("obsidian"); const generalTab = require("./general-tab"); const serverPluginsTab = require("./server-plugins-tab"); +// Tracks our own nav items in the "Ignis Core Plugins" group, keyed by plugin ID. +const ownedNavItems = new Map(); + function createNavEl(tab, setting) { const nav = document.createElement("div"); nav.className = "vertical-tab-nav-item tappable"; @@ -74,7 +77,141 @@ function createGroup(name) { return { group, items }; } +function findGroupByTitle(tabHeadersEl, title) { + const groups = tabHeadersEl.querySelectorAll(".vertical-tab-header-group"); + + for (const g of groups) { + const t = g.querySelector(".vertical-tab-header-group-title"); + + if (t?.textContent === title) { + return g; + } + } + + return null; +} + +function hideIgnisFromCommunityPlugins(setting) { + const cpTab = setting.settingTabs.find((t) => t.id === "community-plugins"); + + if (!cpTab || cpTab._ignisPatched) { + return; + } + + const origRender = cpTab.renderInstalledPlugin; + + cpTab.renderInstalledPlugin = function (manifest, ...rest) { + if (manifest.id.startsWith("ignis-")) { + return; + } + + return origRender.call(this, manifest, ...rest); + }; + + cpTab._ignisPatched = true; + cpTab._origRenderInstalledPlugin = origRender; +} + +function hideIgnisNavFromCommunityGroup(setting) { + const communityGroup = findGroupByTitle( + setting.tabHeadersEl, + "Community plugins", + ); + + if (!communityGroup) { + return; + } + + const items = communityGroup.querySelector(".vertical-tab-header-group-items"); + + if (!items) { + return; + } + + // Hide any ignis plugin nav items that Obsidian placed here. + for (const tab of setting.pluginTabs) { + if (tab.id.startsWith("ignis-") && tab.navEl?.parentElement === items) { + tab.navEl.style.display = "none"; + } + } + + // Hide the entire group if no visible items remain. + const hasVisible = Array.from(items.children).some( + (el) => el.style.display !== "none", + ); + + communityGroup.style.display = hasVisible ? "" : "none"; +} + +function addPluginNavItem(pluginId, setting, corePluginsItems) { + // Find the tab object Obsidian created for this plugin. + const tab = setting.pluginTabs.find((t) => t.id === pluginId); + + if (!tab) { + return; + } + + // Don't add if we already have one for this ID. + if (ownedNavItems.has(pluginId)) { + return; + } + + // Create our own nav item that delegates to Obsidian's tab. + const nav = document.createElement("div"); + nav.className = "vertical-tab-nav-item tappable"; + + if (tab.icon) { + const iconEl = document.createElement("div"); + iconEl.className = "vertical-tab-nav-item-icon"; + setIcon(iconEl, tab.icon); + nav.appendChild(iconEl); + } + + const title = document.createElement("div"); + title.className = "vertical-tab-nav-item-title"; + title.textContent = tab.name; + nav.appendChild(title); + + const chevron = document.createElement("div"); + chevron.className = "vertical-tab-nav-item-chevron"; + nav.appendChild(chevron); + + nav.addEventListener("click", () => { + setting.openTab(tab); + }); + + corePluginsItems.appendChild(nav); + ownedNavItems.set(pluginId, nav); +} + +function removePluginNavItem(pluginId) { + const nav = ownedNavItems.get(pluginId); + + if (nav) { + nav.remove(); + ownedNavItems.delete(pluginId); + } +} + +function removeExistingIgnisGroups(tabHeadersEl) { + const groups = tabHeadersEl.querySelectorAll(".vertical-tab-header-group"); + + for (const g of groups) { + const title = g.querySelector(".vertical-tab-header-group-title"); + + if ( + title?.textContent === "Ignis" || + title?.textContent === "Ignis Core Plugins" + ) { + g.remove(); + } + } +} + function injectIgnisSettings(setting, app) { + removeExistingIgnisGroups(setting.tabHeadersEl); + ownedNavItems.clear(); + const ignis = createGroup("Ignis"); const tabs = [ @@ -97,6 +234,121 @@ function injectIgnisSettings(setting, app) { const corePlugins = createGroup("Ignis Core Plugins"); setting.tabHeadersEl.appendChild(corePlugins.group); + + hideIgnisFromCommunityPlugins(setting); + + // Create our own nav items for ignis plugin tabs. + for (const tab of setting.pluginTabs) { + if (tab.id.startsWith("ignis-") && tab.id !== "ignis-bridge") { + addPluginNavItem(tab.id, setting, corePlugins.items); + } + } + + hideIgnisNavFromCommunityGroup(setting); + hideCorePluginsGroupIfEmpty(); + + // Watch the community group for changes. When Obsidian adds new ignis + // plugin nav items (async after enable), hide them and create our own. + const communityGroup = findGroupByTitle( + setting.tabHeadersEl, + "Community plugins", + ); + + if (communityGroup) { + const observer = new MutationObserver(() => { + // Re-check for new ignis plugin tabs and create nav items. + for (const tab of setting.pluginTabs) { + if (tab.id.startsWith("ignis-") && tab.id !== "ignis-bridge") { + addPluginNavItem(tab.id, setting, corePlugins.items); + } + } + + hideIgnisNavFromCommunityGroup(setting); + hideCorePluginsGroupIfEmpty(); + }); + + observer.observe(communityGroup, { childList: true, subtree: true }); + + const cleanupObserver = new MutationObserver(() => { + if (!setting.tabHeadersEl.isConnected) { + observer.disconnect(); + cleanupObserver.disconnect(); + } + }); + + cleanupObserver.observe(document.body, { + childList: true, + subtree: true, + }); + } +} + +function reconcilePluginTabs(setting) { + const corePluginsGroup = findGroupByTitle( + setting.tabHeadersEl, + "Ignis Core Plugins", + ); + + if (!corePluginsGroup) { + return; + } + + const corePluginsItems = corePluginsGroup.querySelector( + ".vertical-tab-header-group-items", + ); + + if (!corePluginsItems) { + return; + } + + // Get current set of ignis plugin IDs from pluginTabs. + const activeIds = new Set( + setting.pluginTabs + .filter((t) => t.id.startsWith("ignis-") && t.id !== "ignis-bridge") + .map((t) => t.id), + ); + + // Remove nav items for plugins that are no longer active. + for (const [id] of ownedNavItems) { + if (!activeIds.has(id)) { + removePluginNavItem(id); + } + } + + // Add nav items for newly active plugins. + for (const id of activeIds) { + addPluginNavItem(id, setting, corePluginsItems); + } + + hideIgnisNavFromCommunityGroup(setting); + hideCorePluginsGroupIfEmpty(); +} + +function hideCorePluginsGroupIfEmpty() { + for (const [, nav] of ownedNavItems) { + if (nav.isConnected) { + const group = nav.closest(".vertical-tab-header-group"); + + if (group) { + group.style.display = ""; + } + + return; + } + } + + // No items - find and hide the group by walking owned nav items' last known parent, + // or just search the DOM. + const groups = document.querySelectorAll(".vertical-tab-header-group"); + + for (const g of groups) { + const title = g.querySelector(".vertical-tab-header-group-title"); + + if (title?.textContent === "Ignis Core Plugins") { + g.style.display = ownedNavItems.size > 0 ? "" : "none"; + break; + } + } } function patchSettingsModal(plugin) { @@ -114,6 +366,20 @@ function unpatchSettingsModal(plugin) { if (plugin._originalOnOpen) { plugin.app.setting.onOpen = plugin._originalOnOpen; } + + const cpTab = plugin.app.setting.settingTabs.find( + (t) => t.id === "community-plugins", + ); + + if (cpTab?._origRenderInstalledPlugin) { + cpTab.renderInstalledPlugin = cpTab._origRenderInstalledPlugin; + delete cpTab._origRenderInstalledPlugin; + delete cpTab._ignisPatched; + } + + ownedNavItems.clear(); } -module.exports = { patchSettingsModal, unpatchSettingsModal }; +window.__ignisReconcilePluginTabs = reconcilePluginTabs; + +module.exports = { patchSettingsModal, unpatchSettingsModal, reconcilePluginTabs }; diff --git a/plugin/src/settings/server-plugins-tab.js b/plugin/src/settings/server-plugins-tab.js index d16b5a4..dbb940e 100644 --- a/plugin/src/settings/server-plugins-tab.js +++ b/plugin/src/settings/server-plugins-tab.js @@ -107,6 +107,14 @@ function display(containerEl, app) { new Notice( `${plugin.name} ${value ? "enabled" : "disabled"} for this vault.`, ); + + // Give Obsidian a moment to update its plugin tabs, + // then reconcile our sidebar groups. + setTimeout(() => { + if (typeof window.__ignisReconcilePluginTabs === "function") { + window.__ignisReconcilePluginTabs(app.setting); + } + }, 100); } catch (e) { new Notice(`Failed: ${e.message}`); toggle.setValue(!value);