From d6471fd077de280c10fcbc8a59618d74d83cc8c2 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sun, 29 Mar 2026 17:44:41 +0200 Subject: [PATCH] refactor bridge plugin, minor improvement to plugin tracking. --- plugin/src/main.js | 2 + plugin/src/plugin-registry.js | 37 ++++ plugin/src/settings/inject.js | 329 ++--------------------------- plugin/src/settings/plugin-tabs.js | 246 +++++++++++++++++++++ plugin/src/settings/settings-ui.js | 89 ++++++++ 5 files changed, 386 insertions(+), 317 deletions(-) create mode 100644 plugin/src/plugin-registry.js create mode 100644 plugin/src/settings/plugin-tabs.js create mode 100644 plugin/src/settings/settings-ui.js diff --git a/plugin/src/main.js b/plugin/src/main.js index 4cd1d35..cb10573 100644 --- a/plugin/src/main.js +++ b/plugin/src/main.js @@ -1,6 +1,7 @@ const { Plugin, TFile, TFolder } = require("obsidian"); const { showFilePicker, addFileMenuItems, addFolderMenuItems } = require("./file-actions"); const { patchSettingsModal, unpatchSettingsModal } = require("./settings/inject"); +const pluginRegistry = require("./plugin-registry"); window.__obsidianAPI = require("obsidian"); @@ -8,6 +9,7 @@ class IgnisBridgePlugin extends Plugin { async onload() { console.log("[ignis-bridge] Plugin loaded"); + await pluginRegistry.refresh(); patchSettingsModal(this); this.addRibbonIcon("upload", "Upload file", () => { diff --git a/plugin/src/plugin-registry.js b/plugin/src/plugin-registry.js new file mode 100644 index 0000000..77f5fcc --- /dev/null +++ b/plugin/src/plugin-registry.js @@ -0,0 +1,37 @@ +// Maintains a set of known ignis plugin IDs for filtering. +// Populated on bridge plugin load and updated when plugins are enabled/disabled. + +const knownIds = new Set(["ignis-bridge"]); + +async function refresh() { + try { + const res = await fetch("/api/plugins"); + const plugins = await res.json(); + + // Keep ignis-bridge, add all bundled plugin IDs. + knownIds.clear(); + knownIds.add("ignis-bridge"); + + for (const plugin of plugins) { + if (plugin.bundledPluginId) { + knownIds.add(plugin.bundledPluginId); + } + } + } catch { + // Keep whatever we had. + } +} + +function isIgnisPlugin(pluginId) { + return knownIds.has(pluginId); +} + +function addId(pluginId) { + knownIds.add(pluginId); +} + +function getKnownIds() { + return knownIds; +} + +module.exports = { refresh, isIgnisPlugin, addId, getKnownIds }; diff --git a/plugin/src/settings/inject.js b/plugin/src/settings/inject.js index 36434b3..d20e0dc 100644 --- a/plugin/src/settings/inject.js +++ b/plugin/src/settings/inject.js @@ -1,197 +1,13 @@ -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"; - - if (tab.icon) { - const iconEl = document.createElement("div"); - iconEl.className = "vertical-tab-nav-item-icon"; - - if (tab.icon.startsWith("`; - } else { - 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); - }); - - return nav; -} - -function createTab(id, name, displayFn, app, icon) { - const tab = { - id, - name, - icon: icon || null, - containerEl: createDiv("vertical-tab-content"), - navEl: null, - - display() { - this.containerEl.empty(); - displayFn(this.containerEl, app); - }, - - hide() { - this.containerEl.empty(); - }, - }; - - return tab; -} - -function createGroup(name) { - const group = document.createElement("div"); - group.className = "vertical-tab-header-group"; - - const title = document.createElement("div"); - title.className = "vertical-tab-header-group-title"; - title.textContent = name; - group.appendChild(title); - - const items = document.createElement("div"); - items.className = "vertical-tab-header-group-items"; - group.appendChild(items); - - 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); - } -} +const { createNavEl, createTab, createGroup } = require("./settings-ui"); +const { + setupPluginTabs, + reconcilePluginTabs, + hideIgnisFromCommunityPlugins, + restoreCommunityPlugins, + clearOwnedNavItems, +} = require("./plugin-tabs"); function removeExistingIgnisGroups(tabHeadersEl) { const groups = tabHeadersEl.querySelectorAll(".vertical-tab-header-group"); @@ -210,7 +26,7 @@ function removeExistingIgnisGroups(tabHeadersEl) { function injectIgnisSettings(setting, app) { removeExistingIgnisGroups(setting.tabHeadersEl); - ownedNavItems.clear(); + clearOwnedNavItems(); const ignis = createGroup("Ignis"); @@ -236,119 +52,7 @@ function injectIgnisSettings(setting, app) { 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; - } - } + setupPluginTabs(setting, corePlugins.items); } function patchSettingsModal(plugin) { @@ -367,17 +71,8 @@ function unpatchSettingsModal(plugin) { 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(); + restoreCommunityPlugins(plugin.app.setting); + clearOwnedNavItems(); } window.__ignisReconcilePluginTabs = reconcilePluginTabs; diff --git a/plugin/src/settings/plugin-tabs.js b/plugin/src/settings/plugin-tabs.js new file mode 100644 index 0000000..b4b13bf --- /dev/null +++ b/plugin/src/settings/plugin-tabs.js @@ -0,0 +1,246 @@ +const { setIcon } = require("obsidian"); +const { findGroupByTitle } = require("./settings-ui"); +const { isIgnisPlugin } = require("../plugin-registry"); + +// Tracks our own nav items in the "Ignis Core Plugins" group, keyed by plugin ID. +const ownedNavItems = new Map(); + +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 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 (isIgnisPlugin(manifest.id)) { + return; + } + + return origRender.call(this, manifest, ...rest); + }; + + cpTab._ignisPatched = true; + cpTab._origRenderInstalledPlugin = origRender; +} + +function restoreCommunityPlugins(setting) { + const cpTab = setting.settingTabs.find( + (t) => t.id === "community-plugins", + ); + + if (cpTab?._origRenderInstalledPlugin) { + cpTab.renderInstalledPlugin = cpTab._origRenderInstalledPlugin; + delete cpTab._origRenderInstalledPlugin; + delete cpTab._ignisPatched; + } +} + +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 (isIgnisPlugin(tab.id) && 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 hideCorePluginsGroupIfEmpty() { + for (const [, nav] of ownedNavItems) { + if (nav.isConnected) { + const group = nav.closest(".vertical-tab-header-group"); + + if (group) { + group.style.display = ""; + } + + return; + } + } + + // No connected items -- find and hide the group. + 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 setupPluginTabs(setting, corePluginsItems) { + // Create our own nav items for ignis plugin tabs. + for (const tab of setting.pluginTabs) { + if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") { + addPluginNavItem(tab.id, setting, corePluginsItems); + } + } + + 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(() => { + for (const tab of setting.pluginTabs) { + if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") { + addPluginNavItem(tab.id, setting, corePluginsItems); + } + } + + // Re-evaluate visibility since non-ignis items may have appeared. + 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) => isIgnisPlugin(t.id) && 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 clearOwnedNavItems() { + ownedNavItems.clear(); +} + +module.exports = { + setupPluginTabs, + reconcilePluginTabs, + hideIgnisFromCommunityPlugins, + restoreCommunityPlugins, + clearOwnedNavItems, +}; diff --git a/plugin/src/settings/settings-ui.js b/plugin/src/settings/settings-ui.js new file mode 100644 index 0000000..041c634 --- /dev/null +++ b/plugin/src/settings/settings-ui.js @@ -0,0 +1,89 @@ +const { setIcon } = require("obsidian"); + +function createNavEl(tab, setting) { + 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"; + + if (tab.icon.startsWith("`; + } else { + 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); + }); + + return nav; +} + +function createTab(id, name, displayFn, app, icon) { + const tab = { + id, + name, + icon: icon || null, + containerEl: createDiv("vertical-tab-content"), + navEl: null, + + display() { + this.containerEl.empty(); + displayFn(this.containerEl, app); + }, + + hide() { + this.containerEl.empty(); + }, + }; + + return tab; +} + +function createGroup(name) { + const group = document.createElement("div"); + group.className = "vertical-tab-header-group"; + + const title = document.createElement("div"); + title.className = "vertical-tab-header-group-title"; + title.textContent = name; + group.appendChild(title); + + const items = document.createElement("div"); + items.className = "vertical-tab-header-group-items"; + group.appendChild(items); + + 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; +} + +module.exports = { createNavEl, createTab, createGroup, findGroupByTitle };