refactor bridge plugin, minor improvement to plugin tracking.

This commit is contained in:
Nystik
2026-03-29 17:44:41 +02:00
parent 10bd06bd94
commit d6471fd077
5 changed files with 386 additions and 317 deletions

View File

@@ -1,6 +1,7 @@
const { Plugin, TFile, TFolder } = require("obsidian"); const { Plugin, TFile, TFolder } = require("obsidian");
const { showFilePicker, addFileMenuItems, addFolderMenuItems } = require("./file-actions"); const { showFilePicker, addFileMenuItems, addFolderMenuItems } = require("./file-actions");
const { patchSettingsModal, unpatchSettingsModal } = require("./settings/inject"); const { patchSettingsModal, unpatchSettingsModal } = require("./settings/inject");
const pluginRegistry = require("./plugin-registry");
window.__obsidianAPI = require("obsidian"); window.__obsidianAPI = require("obsidian");
@@ -8,6 +9,7 @@ class IgnisBridgePlugin extends Plugin {
async onload() { async onload() {
console.log("[ignis-bridge] Plugin loaded"); console.log("[ignis-bridge] Plugin loaded");
await pluginRegistry.refresh();
patchSettingsModal(this); patchSettingsModal(this);
this.addRibbonIcon("upload", "Upload file", () => { this.addRibbonIcon("upload", "Upload file", () => {

View File

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

View File

@@ -1,197 +1,13 @@
const { setIcon } = require("obsidian");
const generalTab = require("./general-tab"); const generalTab = require("./general-tab");
const serverPluginsTab = require("./server-plugins-tab"); const serverPluginsTab = require("./server-plugins-tab");
const { createNavEl, createTab, createGroup } = require("./settings-ui");
// Tracks our own nav items in the "Ignis Core Plugins" group, keyed by plugin ID. const {
const ownedNavItems = new Map(); setupPluginTabs,
reconcilePluginTabs,
function createNavEl(tab, setting) { hideIgnisFromCommunityPlugins,
const nav = document.createElement("div"); restoreCommunityPlugins,
nav.className = "vertical-tab-nav-item tappable"; clearOwnedNavItems,
} = require("./plugin-tabs");
if (tab.icon) {
const iconEl = document.createElement("div");
iconEl.className = "vertical-tab-nav-item-icon";
if (tab.icon.startsWith("<svg") || tab.icon.startsWith("<img")) {
iconEl.innerHTML = tab.icon;
} else if (tab.icon.endsWith(".svg") || tab.icon.endsWith(".webp") || tab.icon.endsWith(".png")) {
iconEl.innerHTML = `<img src="${tab.icon}" class="svg-icon" width="24" height="24" />`;
} 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);
}
}
function removeExistingIgnisGroups(tabHeadersEl) { function removeExistingIgnisGroups(tabHeadersEl) {
const groups = tabHeadersEl.querySelectorAll(".vertical-tab-header-group"); const groups = tabHeadersEl.querySelectorAll(".vertical-tab-header-group");
@@ -210,7 +26,7 @@ function removeExistingIgnisGroups(tabHeadersEl) {
function injectIgnisSettings(setting, app) { function injectIgnisSettings(setting, app) {
removeExistingIgnisGroups(setting.tabHeadersEl); removeExistingIgnisGroups(setting.tabHeadersEl);
ownedNavItems.clear(); clearOwnedNavItems();
const ignis = createGroup("Ignis"); const ignis = createGroup("Ignis");
@@ -236,119 +52,7 @@ function injectIgnisSettings(setting, app) {
setting.tabHeadersEl.appendChild(corePlugins.group); setting.tabHeadersEl.appendChild(corePlugins.group);
hideIgnisFromCommunityPlugins(setting); hideIgnisFromCommunityPlugins(setting);
setupPluginTabs(setting, corePlugins.items);
// 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) { function patchSettingsModal(plugin) {
@@ -367,17 +71,8 @@ function unpatchSettingsModal(plugin) {
plugin.app.setting.onOpen = plugin._originalOnOpen; plugin.app.setting.onOpen = plugin._originalOnOpen;
} }
const cpTab = plugin.app.setting.settingTabs.find( restoreCommunityPlugins(plugin.app.setting);
(t) => t.id === "community-plugins", clearOwnedNavItems();
);
if (cpTab?._origRenderInstalledPlugin) {
cpTab.renderInstalledPlugin = cpTab._origRenderInstalledPlugin;
delete cpTab._origRenderInstalledPlugin;
delete cpTab._ignisPatched;
}
ownedNavItems.clear();
} }
window.__ignisReconcilePluginTabs = reconcilePluginTabs; window.__ignisReconcilePluginTabs = reconcilePluginTabs;

View File

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

View File

@@ -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("<svg") || tab.icon.startsWith("<img")) {
iconEl.innerHTML = tab.icon;
} else if (tab.icon.endsWith(".svg") || tab.icon.endsWith(".webp") || tab.icon.endsWith(".png")) {
iconEl.innerHTML = `<img src="${tab.icon}" class="svg-icon" width="24" height="24" />`;
} 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 };