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 };