move bridge plugin to package

This commit is contained in:
Nystik
2026-05-20 22:26:58 +02:00
parent fe11f30c01
commit 4a65f142bc
18 changed files with 25 additions and 14 deletions

View File

@@ -0,0 +1,13 @@
const esbuild = require("esbuild");
const path = require("path");
module.exports = esbuild.build({
entryPoints: [path.join(__dirname, "src", "main.js")],
bundle: true,
outfile: path.join(__dirname, "main.js"),
format: "cjs",
platform: "browser",
target: ["chrome90"],
external: ["obsidian", "fs"],
logLevel: "info",
});

View File

@@ -0,0 +1,10 @@
{
"id": "ignis-bridge",
"name": "Ignis Bridge",
"version": "0.8.1",
"minAppVersion": "1.12.4",
"description": "Additional Ignis specific functionality and ignis plugin management.",
"author": "Nystik",
"authorUrl": "https://github.com/Nystik-gh/ignis",
"isDesktopOnly": false
}

View File

@@ -1,5 +1,11 @@
{
"name": "@ignis/bridge-plugin",
"version": "0.0.0-internal",
"private": true
"private": true,
"scripts": {
"build": "node build.js"
},
"devDependencies": {
"esbuild": "^0.20.0"
}
}

View File

@@ -0,0 +1,54 @@
// Demo-mode UX guards that run at the document level.
//
// Disable any email/password inputs to prevent users from entering credentials into a server they don't control.
const PLACEHOLDER =
"Disabled in demo. Don't enter credentials on a server you don't control.";
function isDemoMode() {
return document.body && document.body.dataset.demoMode === "true";
}
function disableInputs(root) {
const inputs = root.querySelectorAll(
'input[type="email"], input[type="password"]',
);
for (const input of inputs) {
if (input.dataset.ignisDemoDisabled === "1") {
continue;
}
input.disabled = true;
input.value = "";
input.placeholder = PLACEHOLDER;
input.dataset.ignisDemoDisabled = "1";
}
}
let observer = null;
function startDemoGuards() {
if (!isDemoMode() || observer) {
return;
}
// Walk what's already there.
disableInputs(document.body);
// And watch for anything added later (login modals, plugin dialogs, etc.).
observer = new MutationObserver(() => {
disableInputs(document.body);
});
observer.observe(document.body, { childList: true, subtree: true });
}
function stopDemoGuards() {
if (observer) {
observer.disconnect();
observer = null;
}
}
module.exports = { startDemoGuards, stopDemoGuards };

View File

@@ -0,0 +1,95 @@
const { Notice, TFile, TFolder } = require("obsidian");
function getVaultId() {
return window.__currentVaultId || "";
}
function triggerDownload(endpoint, filePath, downloadName) {
const vaultId = getVaultId();
const url =
`/api/fs/${endpoint}` +
`?vault=${encodeURIComponent(vaultId)}` +
`&path=${encodeURIComponent(filePath)}`;
const a = document.createElement("a");
a.href = url;
a.download = downloadName;
a.click();
}
function showFilePicker(app, targetFolder = null) {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.style.display = "none";
input.addEventListener("change", async () => {
const files = Array.from(input.files || []);
if (files.length === 0) return;
const folder = targetFolder || app.vault.getRoot();
const folderPath = folder.path;
new Notice(`Uploading ${files.length} file(s)...`);
let successCount = 0;
let errorCount = 0;
for (const file of files) {
try {
const arrayBuffer = await file.arrayBuffer();
const targetPath = folderPath
? `${folderPath}/${file.name}`
: file.name;
await app.vault.createBinary(targetPath, arrayBuffer);
successCount++;
} catch (e) {
console.error("[ignis-bridge] Upload failed:", file.name, e);
errorCount++;
}
}
if (successCount > 0) {
new Notice(`Uploaded ${successCount} file(s) successfully`);
}
if (errorCount > 0) {
new Notice(`Failed to upload ${errorCount} file(s)`, 5000);
}
input.remove();
});
document.body.appendChild(input);
input.click();
}
function addFileMenuItems(menu, file) {
menu.addItem((item) => {
item
.setTitle("Download")
.setIcon("download")
.onClick(() => triggerDownload("download", file.path, file.name));
});
}
function addFolderMenuItems(menu, folder, app) {
menu.addItem((item) => {
item
.setTitle("Download as ZIP")
.setIcon("download")
.onClick(() =>
triggerDownload("download-zip", folder.path, `${folder.name}.zip`),
);
});
menu.addItem((item) => {
item
.setTitle("Upload file")
.setIcon("upload")
.onClick(() => showFilePicker(app, folder));
});
}
module.exports = { showFilePicker, addFileMenuItems, addFolderMenuItems };

View File

@@ -0,0 +1,70 @@
const { Plugin, TFile, TFolder } = require("obsidian");
const {
showFilePicker,
addFileMenuItems,
addFolderMenuItems,
} = require("./file-actions");
const {
patchSettingsModal,
unpatchSettingsModal,
} = require("./settings/inject");
const pluginRegistry = require("./plugin-registry");
const { initStatusBar } = require("./status-bar");
const { WorkspacePickerModal } = require("./workspace-picker");
const { startDemoGuards, stopDemoGuards } = require("./demo-guards");
window.__obsidianAPI = require("obsidian");
class IgnisBridgePlugin extends Plugin {
async onload() {
if (!window.__ignis) {
console.log("[ignis-bridge] Not running in Ignis - plugin is a no-op.");
return;
}
console.log("[ignis-bridge] Plugin loaded");
await pluginRegistry.refresh();
patchSettingsModal(this);
startDemoGuards();
this._statusBarInterval = initStatusBar(this);
this.addRibbonIcon("upload", "Upload file", () => {
showFilePicker(this.app);
});
this.addCommand({
id: "open-workspace-in-new-tab",
name: "Open workspace in new tab",
callback: () => {
new WorkspacePickerModal(this.app).open();
},
});
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {
addFileMenuItems(menu, file);
} else if (file instanceof TFolder) {
addFolderMenuItems(menu, file, this.app);
}
}),
);
}
onunload() {
if (!window.__ignis) {
return;
}
if (this._statusBarInterval) {
clearInterval(this._statusBarInterval);
}
unpatchSettingsModal(this);
stopDemoGuards();
console.log("[ignis-bridge] Plugin unloaded");
}
}
module.exports = IgnisBridgePlugin;

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

@@ -0,0 +1,166 @@
const { Setting } = require("obsidian");
const GITHUB_URL = "https://github.com/Nystik-gh/ignis";
const GITHUB_API_LATEST =
"https://api.github.com/repos/Nystik-gh/ignis/releases/latest";
function getVersion(app) {
try {
const manifest = app.plugins.getPlugin("ignis-bridge")?.manifest;
return manifest?.version || "unknown";
} catch {
return "unknown";
}
}
// SemVer build metadata (`+xyz`) is informational and ignored for precedence.
function stripBuildMetadata(version) {
return (version || "").split("+")[0];
}
async function checkForUpdate(currentVersion) {
try {
const res = await fetch(GITHUB_API_LATEST);
if (!res.ok) {
return null;
}
const data = await res.json();
const latest = stripBuildMetadata(data.tag_name?.replace(/^v/, ""));
const current = stripBuildMetadata(currentVersion);
if (latest && latest !== current) {
return { version: latest, url: data.html_url };
}
return null;
} catch {
return null;
}
}
function display(containerEl, app) {
const version = getVersion(app);
const header = containerEl.createDiv("ignis-header");
const logo = header.createEl("img", {
cls: "ignis-header-logo",
attr: { src: "/assets/ignis.webp", alt: "Ignis" },
});
const info = header.createDiv("ignis-header-info");
info.createEl("div", { text: "Ignis", cls: "ignis-header-title" });
info.createEl("div", {
text: "Obsidian server bridge",
cls: "ignis-header-subtitle",
});
const right = header.createDiv("ignis-header-right");
const versionCol = right.createDiv("ignis-header-version-col");
versionCol.createEl("span", {
text: `Version ${version}`,
cls: "ignis-header-version",
});
const updateIndicator = versionCol.createEl("a", {
text: "Checking...",
cls: "ignis-update-indicator",
attr: { target: "_blank", rel: "noopener noreferrer" },
});
const githubLink = right.createEl("a", {
cls: "ignis-github-link",
href: GITHUB_URL,
attr: { target: "_blank", "aria-label": "GitHub" },
});
const githubIcon = githubLink.createEl("img", {
cls: "ignis-github-icon",
attr: { src: "/assets/github.svg", alt: "GitHub" },
});
checkForUpdate(version).then((latest) => {
if (latest) {
updateIndicator.textContent = `v${latest.version} available`;
updateIndicator.addClass("ignis-update-available");
updateIndicator.href = latest.url;
} else {
updateIndicator.textContent = "Up to date";
}
});
addServerStatus(containerEl);
}
function getWsStatus() {
const ws = window.__ignisWs;
if (!ws) {
return "disconnected";
}
switch (ws.readyState) {
case WebSocket.CONNECTING:
return "connecting";
case WebSocket.OPEN:
return "connected";
case WebSocket.CLOSING:
case WebSocket.CLOSED:
return "disconnected";
default:
return "disconnected";
}
}
function statusLabel(status) {
switch (status) {
case "connected":
return "Connected";
case "connecting":
return "Connecting...";
case "disconnected":
return "Disconnected";
default:
return "Unknown";
}
}
function addServerStatus(containerEl) {
const status = getWsStatus();
const setting = new Setting(containerEl).setName("Server status");
const dotEl = setting.controlEl.createEl("span", {
cls: `ignis-status-dot ignis-status-${status}`,
});
const labelEl = setting.controlEl.createEl("span", {
text: statusLabel(status),
cls: "ignis-status-label",
});
const update = () => {
const s = getWsStatus();
dotEl.className = `ignis-status-dot ignis-status-${s}`;
labelEl.textContent = statusLabel(s);
};
const pollInterval = setInterval(update, 3000);
const observer = new MutationObserver(() => {
if (!containerEl.isConnected) {
clearInterval(pollInterval);
observer.disconnect();
}
});
observer.observe(containerEl.parentElement || document.body, {
childList: true,
subtree: true,
});
}
module.exports = { display };

View File

@@ -0,0 +1,148 @@
const generalTab = require("./general-tab");
const serverPluginsTab = require("./server-plugins-tab");
const { createNavEl, createTab, createGroup } = require("./settings-ui");
const {
setupPluginTabs,
reconcilePluginTabs,
hideIgnisFromCommunityPlugins,
restoreCommunityPlugins,
clearOwnedPluginIds,
} = require("./plugin-tabs");
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();
}
}
}
// All ignis-managed nav elements (both Ignis group and Ignis Core Plugins group).
// Collected here so the openTab patch can manage is-active across all of them.
const allIgnisNavEls = new Map(); // tab id -> nav element
function replaceInstallerVersionRow(setting, ignisVersion) {
const container = setting.tabContentContainer || setting.contentEl;
if (!container) {
return;
}
const rows = container.querySelectorAll(".setting-item");
for (const row of rows) {
const desc = row.querySelector(".setting-item-description");
if (!desc || !desc.textContent.startsWith("Installer version:")) {
continue;
}
desc.empty();
desc.createEl("strong", { text: `Running in Ignis v${ignisVersion}` });
desc.createEl("br");
desc.appendText(
"Obsidian is served through Ignis. There's no installer to update.",
);
break;
}
}
function patchOpenTab(setting, plugin) {
if (setting._ignisOpenTabPatched) {
return;
}
const original = setting.openTab.bind(setting);
setting.openTab = function (tab) {
// Clear is-active from all ignis nav items.
for (const [, el] of allIgnisNavEls) {
el.removeClass("is-active");
}
original(tab);
// If the opened tab is one of ours, highlight it.
const navEl = allIgnisNavEls.get(tab.id);
if (navEl) {
navEl.addClass("is-active");
}
if (tab && tab.id === "about") {
replaceInstallerVersionRow(setting, plugin.manifest.version);
}
};
setting._ignisOpenTabPatched = true;
}
function injectIgnisSettings(setting, app, plugin) {
removeExistingIgnisGroups(setting.tabHeadersEl);
clearOwnedPluginIds();
allIgnisNavEls.clear();
patchOpenTab(setting, plugin);
replaceInstallerVersionRow(setting, plugin.manifest.version);
const ignis = createGroup("Ignis");
const tabs = [
createTab("ignis-general", "General", generalTab.display, app, "flame"),
createTab(
"ignis-core-plugins",
"Core plugins",
serverPluginsTab.display,
app,
"blocks",
),
];
for (const tab of tabs) {
tab.navEl = createNavEl(tab, setting);
ignis.items.appendChild(tab.navEl);
allIgnisNavEls.set(tab.id, tab.navEl);
}
setting.tabHeadersEl.appendChild(ignis.group);
const corePlugins = createGroup("Ignis Core Plugins");
setting.tabHeadersEl.appendChild(corePlugins.group);
hideIgnisFromCommunityPlugins(setting);
setupPluginTabs(setting, corePlugins.items, allIgnisNavEls);
}
function patchSettingsModal(plugin) {
const original = plugin.app.setting.onOpen;
const app = plugin.app;
plugin._originalOnOpen = original;
plugin.app.setting.onOpen = function () {
original.call(this);
injectIgnisSettings(this, app, plugin);
};
}
function unpatchSettingsModal(plugin) {
if (plugin._originalOnOpen) {
plugin.app.setting.onOpen = plugin._originalOnOpen;
}
delete plugin.app.setting._ignisOpenTabPatched;
restoreCommunityPlugins(plugin.app.setting);
clearOwnedPluginIds();
}
window.__ignisReconcilePluginTabs = (setting) =>
reconcilePluginTabs(setting, allIgnisNavEls);
module.exports = { patchSettingsModal, unpatchSettingsModal, reconcilePluginTabs };

View File

@@ -0,0 +1,237 @@
const { setIcon } = require("obsidian");
const { findGroupByTitle } = require("./settings-ui");
const { isIgnisPlugin } = require("../plugin-registry");
// Tracks which plugin IDs have nav items we created.
const ownedPluginIds = new Set();
function addPluginNavItem(pluginId, setting, corePluginsItems, ignisNavEls) {
const tab = setting.pluginTabs.find((t) => t.id === pluginId);
if (!tab) {
return;
}
if (ownedPluginIds.has(pluginId)) {
return;
}
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);
ownedPluginIds.add(pluginId);
ignisNavEls.set(pluginId, nav);
}
function removePluginNavItem(pluginId, ignisNavEls) {
const nav = ignisNavEls.get(pluginId);
if (nav && ownedPluginIds.has(pluginId)) {
nav.remove();
ownedPluginIds.delete(pluginId);
ignisNavEls.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;
}
for (const tab of setting.pluginTabs) {
if (isIgnisPlugin(tab.id) && tab.navEl?.parentElement === items) {
tab.navEl.style.display = "none";
}
}
const hasVisible = Array.from(items.children).some(
(el) => el.style.display !== "none",
);
communityGroup.style.display = hasVisible ? "" : "none";
}
function hideCorePluginsGroupIfEmpty(ignisNavEls) {
let hasConnected = false;
for (const id of ownedPluginIds) {
const nav = ignisNavEls.get(id);
if (nav?.isConnected) {
hasConnected = true;
break;
}
}
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 = hasConnected ? "" : "none";
break;
}
}
}
function setupPluginTabs(setting, corePluginsItems, ignisNavEls) {
for (const tab of setting.pluginTabs) {
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
addPluginNavItem(tab.id, setting, corePluginsItems, ignisNavEls);
}
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
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, ignisNavEls);
}
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
});
observer.observe(communityGroup, { childList: true, subtree: true });
const modalEl = setting.tabHeadersEl.closest(".modal");
if (modalEl && modalEl.parentElement) {
const cleanupObserver = new MutationObserver(() => {
if (!setting.tabHeadersEl.isConnected) {
observer.disconnect();
cleanupObserver.disconnect();
}
});
cleanupObserver.observe(modalEl.parentElement, {
childList: true,
});
}
}
}
function reconcilePluginTabs(setting, ignisNavEls) {
const corePluginsGroup = findGroupByTitle(
setting.tabHeadersEl,
"Ignis Core Plugins",
);
if (!corePluginsGroup) {
return;
}
const corePluginsItems = corePluginsGroup.querySelector(
".vertical-tab-header-group-items",
);
if (!corePluginsItems) {
return;
}
const activeIds = new Set(
setting.pluginTabs
.filter((t) => isIgnisPlugin(t.id) && t.id !== "ignis-bridge")
.map((t) => t.id),
);
for (const id of ownedPluginIds) {
if (!activeIds.has(id)) {
removePluginNavItem(id, ignisNavEls);
}
}
for (const id of activeIds) {
addPluginNavItem(id, setting, corePluginsItems, ignisNavEls);
}
hideIgnisNavFromCommunityGroup(setting);
hideCorePluginsGroupIfEmpty(ignisNavEls);
}
function clearOwnedPluginIds() {
ownedPluginIds.clear();
}
module.exports = {
setupPluginTabs,
reconcilePluginTabs,
hideIgnisFromCommunityPlugins,
restoreCommunityPlugins,
clearOwnedPluginIds,
};

View File

@@ -0,0 +1,132 @@
const { Setting, Notice } = require("obsidian");
function getVaultId() {
return window.__currentVaultId || "";
}
async function refreshPluginCache(bundledPluginId) {
const pluginPath = `.obsidian/plugins/${bundledPluginId}`;
const fs = require("fs");
if (fs._refreshSubtree) {
await fs._refreshSubtree(pluginPath);
}
}
async function fetchPlugins() {
const res = await fetch("/api/plugins");
if (!res.ok) {
throw new Error("Failed to fetch plugins");
}
return res.json();
}
async function togglePlugin(pluginId, enable, app) {
const action = enable ? "enable" : "disable";
const vaultId = getVaultId();
const res = await fetch(`/api/plugins/${pluginId}/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault: vaultId }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Failed to ${action} plugin`);
}
return res.json();
}
async function activateBundledPlugin(bundledPluginId, enable, app) {
if (!bundledPluginId) {
return;
}
const plugins = app.plugins;
if (enable) {
await plugins.loadManifests();
await plugins.enablePluginAndSave(bundledPluginId);
} else {
await plugins.disablePluginAndSave(bundledPluginId);
}
}
function display(containerEl, app) {
containerEl.createEl("h2", { text: "Ignis Core Plugins" });
const descEl = containerEl.createEl("p", {
text:
"Ignis plugins extend server functionality and run alongside your vaults. " +
"They are separate from Obsidian's built-in plugins.",
cls: "ignis-plugins-description",
});
const loadingEl = containerEl.createEl("p", { text: "Loading plugins..." });
fetchPlugins()
.then((plugins) => {
loadingEl.remove();
if (plugins.length === 0) {
containerEl.createEl("p", {
text: "No server plugins available.",
cls: "setting-item-description",
});
return;
}
const vaultId = getVaultId();
for (const plugin of plugins) {
const enabled = plugin.enabledVaults.includes(vaultId);
new Setting(containerEl)
.setName(plugin.name)
.setDesc(plugin.description || "")
.addToggle((toggle) => {
toggle.setValue(enabled);
toggle.onChange(async (value) => {
try {
await togglePlugin(plugin.id, value, app);
if (value && plugin.bundledPluginId) {
await refreshPluginCache(plugin.bundledPluginId);
}
await activateBundledPlugin(
plugin.bundledPluginId,
value,
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);
}
});
});
}
})
.catch((e) => {
loadingEl.setText("Failed to load plugins.");
console.error("[ignis-bridge] Server plugins error:", e);
});
}
module.exports = { display };

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

View File

@@ -0,0 +1,48 @@
function getWsStatus() {
const ws = window.__ignisWs;
if (!ws) {
return "disconnected";
}
switch (ws.readyState) {
case WebSocket.CONNECTING:
return "connecting";
case WebSocket.OPEN:
return "connected";
default:
return "disconnected";
}
}
const STATUS_LABELS = {
connected: "Ignis server: Connected",
connecting: "Ignis server: Connecting...",
disconnected: "Ignis server: Disconnected",
};
function initStatusBar(plugin) {
const item = plugin.addStatusBarItem();
item.addClass("ignis-statusbar-item");
const dot = item.createEl("span", {
cls: "ignis-statusbar-dot",
});
item.setAttribute("aria-label", "Ignis: Checking...");
item.setAttribute("data-tooltip-position", "top");
const update = () => {
const status = getWsStatus();
dot.className = `ignis-statusbar-dot ignis-statusbar-${status}`;
item.setAttribute("aria-label", STATUS_LABELS[status] || "Ignis: Unknown");
};
update();
const interval = setInterval(update, 3000);
return interval;
}
module.exports = { initStatusBar };

View File

@@ -0,0 +1,32 @@
const { FuzzySuggestModal } = require("obsidian");
class WorkspacePickerModal extends FuzzySuggestModal {
constructor(app) {
super(app);
this.setPlaceholder("Open workspace in new tab");
}
getItems() {
const plugin = this.app.internalPlugins.plugins.workspaces;
if (!plugin || !plugin.enabled || !plugin.instance) {
return [];
}
return Object.keys(plugin.instance.workspaces);
}
getItemText(item) {
return item;
}
onChooseItem(item) {
const url = new URL(window.location.href);
url.searchParams.set("workspace", item);
url.searchParams.set("load", "preset");
window.open(url.toString(), "_blank");
}
}
module.exports = { WorkspacePickerModal };

View File

@@ -0,0 +1,143 @@
.ignis-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid var(--background-modifier-border);
}
.ignis-header-logo {
width: 48px;
height: 48px;
flex-shrink: 0;
}
.ignis-header-info {
flex: 1;
min-width: 0;
}
.ignis-header-title {
font-size: var(--font-ui-large);
font-weight: var(--font-semibold);
line-height: 1.2;
margin: 0;
}
.ignis-header-subtitle {
font-size: var(--font-ui-small);
color: var(--text-muted);
line-height: 1.2;
margin: 4px 0 0;
}
.ignis-header-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.ignis-header-version-col {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.ignis-header-version {
font-size: var(--font-ui-small);
color: var(--text-muted);
}
.ignis-update-indicator {
font-size: var(--font-ui-smaller);
color: var(--text-faint);
text-decoration: none;
}
.ignis-update-indicator.ignis-update-available {
color: var(--text-accent);
}
.ignis-update-indicator.ignis-update-available:hover {
text-decoration: underline;
}
.ignis-github-link {
color: var(--text-muted);
display: flex;
align-items: center;
}
.ignis-github-link:hover {
color: var(--text-normal);
}
.ignis-github-link:hover .ignis-github-icon {
opacity: 1;
}
.ignis-github-icon {
width: 32px;
height: 32px;
opacity: 0.6;
}
.ignis-status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.ignis-status-connected {
background-color: var(--color-green);
}
.ignis-status-connecting {
background-color: var(--color-yellow);
}
.ignis-status-disconnected {
background-color: var(--color-red);
}
.ignis-status-label {
font-size: var(--font-ui-small);
color: var(--text-muted);
}
.ignis-statusbar-item {
display: flex;
align-items: center;
cursor: default;
}
.ignis-statusbar-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.ignis-statusbar-connected {
background-color: var(--color-green);
}
.ignis-statusbar-connecting {
background-color: var(--color-yellow);
}
.ignis-statusbar-disconnected {
background-color: var(--color-red);
}
.ignis-plugins-description {
padding: 0 16px;
color: var(--text-muted);
font-size: var(--font-ui-small);
margin-bottom: 16px;
}