CI / lint (push) Failing after 30s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 35s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
568 lines
24 KiB
JavaScript
568 lines
24 KiB
JavaScript
/* ObsiGate — Plugin Manager (Frontend)
|
|
* Handles plugin lifecycle, UI, and sandboxed execution via Web Worker.
|
|
*/
|
|
|
|
import { api } from './auth.js';
|
|
import { showToast } from './ui.js';
|
|
import { t } from './i18n.js';
|
|
import { safeCreateIcons } from './utils.js';
|
|
|
|
// ── Plugin Registry (in-memory cache) ─────────────────────────────────────
|
|
let _pluginsCache = new Map();
|
|
let _worker = null;
|
|
let _pendingCalls = new Map();
|
|
let _callId = 0;
|
|
|
|
// ── Worker Management ────────────────────────────────────────────────────
|
|
|
|
function getOrCreateWorker() {
|
|
if (!_worker) {
|
|
// Create worker from blob to avoid separate file (simpler deployment)
|
|
// Using string concatenation to avoid nested template literal issues
|
|
const workerCode =
|
|
"const pluginSandbox = {};\n" +
|
|
"const handlers = {};\n\n" +
|
|
"// Safe globals for plugins\n" +
|
|
"const safeGlobals = {\n" +
|
|
" console: {\n" +
|
|
" log: (...args) => self.postMessage({ type: 'log', args }),\n" +
|
|
" warn: (...args) => self.postMessage({ type: 'warn', args }),\n" +
|
|
" error: (...args) => self.postMessage({ type: 'error', args })\n" +
|
|
" },\n" +
|
|
" fetch: async (url, options) => {\n" +
|
|
" const response = await fetch(url, options);\n" +
|
|
" const data = await response.json();\n" +
|
|
" return data;\n" +
|
|
" },\n" +
|
|
" setTimeout,\n" +
|
|
" clearTimeout,\n" +
|
|
" Promise,\n" +
|
|
" JSON,\n" +
|
|
" Math,\n" +
|
|
" Date,\n" +
|
|
" Object,\n" +
|
|
" Array,\n" +
|
|
" String,\n" +
|
|
" Number,\n" +
|
|
" Boolean,\n" +
|
|
" RegExp,\n" +
|
|
" Error,\n" +
|
|
" Map,\n" +
|
|
" Set,\n" +
|
|
" WeakMap,\n" +
|
|
" WeakSet,\n" +
|
|
" URL,\n" +
|
|
" URLSearchParams,\n" +
|
|
" Headers,\n" +
|
|
" Request,\n" +
|
|
" Response,\n" +
|
|
" FormData,\n" +
|
|
" Blob,\n" +
|
|
" File,\n" +
|
|
" atob,\n" +
|
|
" btoa,\n" +
|
|
" encodeURIComponent,\n" +
|
|
" decodeURIComponent,\n" +
|
|
" parseInt,\n" +
|
|
" parseFloat,\n" +
|
|
" isNaN,\n" +
|
|
" isFinite\n" +
|
|
"};\n\n" +
|
|
"// Message handler\n" +
|
|
"self.onmessage = async (e) => {\n" +
|
|
" const { type, payload, callId } = e.data;\n\n" +
|
|
" try {\n" +
|
|
" if (type === 'loadPlugin') {\n" +
|
|
" const { code, name, permissions } = payload;\n\n" +
|
|
" const sandbox = { ...safeGlobals };\n" +
|
|
" const pluginModule = { exports: {} };\n\n" +
|
|
" const fn = new Function(\n" +
|
|
" 'module', 'exports', 'require', 'globals',\n" +
|
|
" code\n" +
|
|
" );\n" +
|
|
" fn(pluginModule, pluginModule.exports, (mod) => {\n" +
|
|
" throw new Error('require() not allowed in plugins');\n" +
|
|
" }, sandbox);\n\n" +
|
|
" for (const [hook, handlerName] of Object.entries(sandbox.handlers || {})) {\n" +
|
|
" if (!handlers[hook]) handlers[hook] = [];\n" +
|
|
" handlers[hook].push({ name, handler: sandbox[handlerName], permissions });\n" +
|
|
" }\n\n" +
|
|
" self.postMessage({ type: 'loaded', callId, plugin: name });\n" +
|
|
" }\n" +
|
|
" else if (type === 'executeHook') {\n" +
|
|
" const { hook, context, plugins: pluginList } = payload;\n" +
|
|
" const results = [];\n\n" +
|
|
" for (const p of pluginList) {\n" +
|
|
" const hookHandlers = handlers[hook]?.filter(h => h.name === p.name) || [];\n" +
|
|
" for (const h of hookHandlers) {\n" +
|
|
" try {\n" +
|
|
" const missing = h.permissions?.filter(p => !payload.permissions?.includes(p)) || [];\n" +
|
|
" if (missing.length > 0) {\n" +
|
|
" results.push({ plugin: p.name, success: false, error: 'Missing permissions: ' + missing.join(', ') });\n" +
|
|
" continue;\n" +
|
|
" }\n\n" +
|
|
" const result = await h.handler(context);\n" +
|
|
" results.push({ plugin: p.name, success: true, data: result });\n" +
|
|
" } catch (err) {\n" +
|
|
" results.push({ plugin: p.name, success: false, error: err.message });\n" +
|
|
" }\n" +
|
|
" }\n" +
|
|
" }\n\n" +
|
|
" self.postMessage({ type: 'hookResult', callId, results });\n" +
|
|
" }\n" +
|
|
" else if (type === 'unloadPlugin') {\n" +
|
|
" const { name } = payload;\n" +
|
|
" for (const hook of Object.keys(handlers)) {\n" +
|
|
" handlers[hook] = handlers[hook].filter(h => h.name !== name);\n" +
|
|
" }\n" +
|
|
" self.postMessage({ type: 'unloaded', callId });\n" +
|
|
" }\n" +
|
|
" } catch (err) {\n" +
|
|
" self.postMessage({ type: 'error', callId, error: err.message });\n" +
|
|
" }\n" +
|
|
"};\n";
|
|
|
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
_worker = new Worker(URL.createObjectURL(blob));
|
|
|
|
_worker.onmessage = (e) => {
|
|
const { type, callId, plugin, result, results, error } = e.data;
|
|
|
|
const pending = _pendingCalls.get(callId);
|
|
if (!pending) return;
|
|
|
|
clearTimeout(pending.timeout);
|
|
_pendingCalls.delete(callId);
|
|
|
|
if (type === 'error' || type === 'loaded' || type === 'unloaded') {
|
|
if (error) pending.reject(new Error(error));
|
|
else pending.resolve({ success: true });
|
|
} else if (type === 'hookResult') {
|
|
pending.resolve(results);
|
|
}
|
|
};
|
|
|
|
_worker.onerror = (err) => {
|
|
console.error('[PluginWorker] Error:', err);
|
|
for (const [, pending] of _pendingCalls) {
|
|
clearTimeout(pending.timeout);
|
|
pending.reject(new Error('Worker error: ' + err.message));
|
|
}
|
|
_pendingCalls.clear();
|
|
_worker = null;
|
|
};
|
|
}
|
|
return _worker;
|
|
}
|
|
|
|
function callWorker(type, payload, timeoutMs = 30000) {
|
|
return new Promise((resolve, reject) => {
|
|
const worker = getOrCreateWorker();
|
|
const id = ++_callId;
|
|
|
|
const timeout = setTimeout(() => {
|
|
_pendingCalls.delete(id);
|
|
reject(new Error('Plugin worker timeout'));
|
|
}, timeoutMs);
|
|
|
|
_pendingCalls.set(id, { resolve, reject, timeout });
|
|
worker.postMessage({ type, payload, callId: id });
|
|
});
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
|
|
export async function loadPlugins(vault) {
|
|
try {
|
|
const plugins = await api('/api/plugins?vault=' + encodeURIComponent(vault));
|
|
_pluginsCache.set(vault, plugins);
|
|
return plugins;
|
|
} catch (err) {
|
|
console.error('Failed to load plugins:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function getCachedPlugins(vault) {
|
|
return _pluginsCache.get(vault) || [];
|
|
}
|
|
|
|
export async function installPlugin(vault, manifest, code) {
|
|
const plugin = await api('/api/plugins?vault=' + encodeURIComponent(vault), {
|
|
method: 'POST',
|
|
body: JSON.stringify({ manifest, code })
|
|
});
|
|
await loadPlugins(vault);
|
|
return plugin;
|
|
}
|
|
|
|
export async function uninstallPlugin(vault, name) {
|
|
await api('/api/plugins/' + encodeURIComponent(name) + '?vault=' + encodeURIComponent(vault), {
|
|
method: 'DELETE'
|
|
});
|
|
await loadPlugins(vault);
|
|
}
|
|
|
|
export async function enablePlugin(vault, name) {
|
|
const plugin = await api('/api/plugins/' + encodeURIComponent(name) + '/enable?vault=' + encodeURIComponent(vault), {
|
|
method: 'POST'
|
|
});
|
|
await loadPlugins(vault);
|
|
return plugin;
|
|
}
|
|
|
|
export async function disablePlugin(vault, name) {
|
|
const plugin = await api('/api/plugins/' + encodeURIComponent(name) + '/disable?vault=' + encodeURIComponent(vault), {
|
|
method: 'POST'
|
|
});
|
|
await loadPlugins(vault);
|
|
return plugin;
|
|
}
|
|
|
|
export async function getPluginCode(vault, name) {
|
|
const { code } = await api('/api/plugins/' + encodeURIComponent(name) + '/code?vault=' + encodeURIComponent(vault));
|
|
return code;
|
|
}
|
|
|
|
export async function validateManifest(manifest) {
|
|
return api('/api/plugins/validate-manifest', {
|
|
method: 'POST',
|
|
body: JSON.stringify(manifest)
|
|
});
|
|
}
|
|
|
|
export async function getPluginTemplate() {
|
|
return api('/api/plugins/template');
|
|
}
|
|
|
|
// ── Hook Execution (for other modules to call) ────────────────────────────
|
|
|
|
export async function executeHook(vault, hook, context) {
|
|
const plugins = getCachedPlugins(vault).filter(p => p.enabled && p.hooks?.includes(hook));
|
|
|
|
if (plugins.length === 0) return [];
|
|
|
|
const pluginList = plugins.map(p => ({
|
|
name: p.name,
|
|
handler: p.hooks?.[hook] || '',
|
|
permissions: p.permissions || []
|
|
})).filter(p => p.handler);
|
|
|
|
if (pluginList.length === 0) return [];
|
|
|
|
try {
|
|
const results = await callWorker('executeHook', {
|
|
hook,
|
|
context,
|
|
plugins: pluginList,
|
|
permissions: pluginList.flatMap(p => p.permissions)
|
|
});
|
|
return results;
|
|
} catch (err) {
|
|
console.error('Hook ' + hook + ' execution failed:', err);
|
|
return pluginList.map(p => ({
|
|
plugin: p.name,
|
|
success: false,
|
|
error: err.message
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Convenience methods for specific hooks
|
|
export async function onFileRender(vault, fileContent, filePath) {
|
|
const results = await executeHook(vault, 'onFileRender', { fileContent, filePath, vault });
|
|
let replaced = null;
|
|
const append = [];
|
|
for (const r of results) {
|
|
if (!r.success) continue;
|
|
const d = r.data;
|
|
if (typeof d === 'string') {
|
|
replaced = d;
|
|
} else if (d && typeof d === 'object') {
|
|
if (typeof d.content === 'string') replaced = d.content;
|
|
if (typeof d.append === 'string') append.push(d.append);
|
|
if (typeof d.html === 'string') append.push(d.html);
|
|
}
|
|
}
|
|
return { replace: replaced, append };
|
|
}
|
|
|
|
export async function onSearchFilter(vault, query, results) {
|
|
const hookResults = await executeHook(vault, 'onSearchFilter', { query, results, vault });
|
|
for (const r of hookResults) {
|
|
if (r.success && Array.isArray(r.data)) return r.data;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export async function onEditorAction(vault, action, editorState) {
|
|
const results = await executeHook(vault, 'onEditorAction', { action, editorState, vault });
|
|
return results.some(r => r.success && r.data?.handled === true);
|
|
}
|
|
|
|
export async function onSidebarItem(vault, itemType, itemPath) {
|
|
const results = await executeHook(vault, 'onSidebarItem', { itemType, itemPath, vault });
|
|
const items = [];
|
|
for (const r of results) {
|
|
if (r.success && Array.isArray(r.data)) items.push(...r.data);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
export async function onFileCreate(vault, filePath, content) {
|
|
await executeHook(vault, 'onFileCreate', { filePath, content, vault });
|
|
}
|
|
|
|
export async function onFileDelete(vault, filePath) {
|
|
const results = await executeHook(vault, 'onFileDelete', { filePath, vault });
|
|
return results.every(r => r.success && r.data?.allow !== false);
|
|
}
|
|
|
|
export async function onVaultMount(vault) {
|
|
await executeHook(vault, 'onVaultMount', { vault });
|
|
}
|
|
|
|
// ── UI: Plugins Settings Page ────────────────────────────────────────────
|
|
|
|
export async function renderPluginsSettings(vault, container) {
|
|
const plugins = await loadPlugins(vault);
|
|
|
|
container.innerHTML =
|
|
'<div class="plugins-settings">' +
|
|
'<div class="plugins-header">' +
|
|
'<h2 data-i18n="plugins.title">🧩 Plugins</h2>' +
|
|
'<p class="config-description" data-i18n="plugins.description">' +
|
|
'Extend ObsiGate with custom renderers, search filters, and editor actions.' +
|
|
'</p>' +
|
|
'</div>' +
|
|
'<div class="plugins-toolbar">' +
|
|
'<button class="btn btn-primary" id="plugin-install-btn" data-i18n="plugins.install">Install Plugin</button>' +
|
|
'<button class="btn btn-secondary" id="plugin-template-btn" data-i18n="plugins.template">View Template</button>' +
|
|
'</div>' +
|
|
'<div id="plugins-list" class="plugins-list">' +
|
|
(plugins.length === 0
|
|
? '<div class="config-description" data-i18n="plugins.none">No plugins installed.</div>'
|
|
: plugins.map(p =>
|
|
'<div class="plugin-item ' + (p.enabled ? '' : 'disabled') + '" data-name="' + p.name + '">' +
|
|
'<div class="plugin-info">' +
|
|
'<div class="plugin-name">' +
|
|
'<strong>' + p.name + '</strong> v' + p.version +
|
|
'<span class="plugin-author">by ' + p.author + '</span>' +
|
|
'</div>' +
|
|
'<div class="plugin-description">' + p.description + '</div>' +
|
|
'<div class="plugin-meta">' +
|
|
'<span class="plugin-hooks">' + (p.hooks?.join(', ') || 'no hooks') + '</span>' +
|
|
'<span class="plugin-perms">' + (p.permissions?.join(', ') || 'no permissions') + '</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="plugin-actions">' +
|
|
'<button class="btn btn-sm ' + (p.enabled ? 'btn-warning' : 'btn-success') + '"' +
|
|
' data-action="' + (p.enabled ? 'disable' : 'enable') + '"' +
|
|
' data-name="' + p.name + '"' +
|
|
' data-i18n="' + (p.enabled ? 'plugins.disable' : 'plugins.enable') + '">' +
|
|
(p.enabled ? 'Disable' : 'Enable') +
|
|
'</button>' +
|
|
'<button class="btn btn-sm btn-secondary" data-action="code" data-name="' + p.name + '" data-i18n="plugins.view_code">Code</button>' +
|
|
'<button class="btn btn-sm btn-danger" data-action="uninstall" data-name="' + p.name + '" data-i18n="plugins.uninstall">Uninstall</button>' +
|
|
'</div>' +
|
|
'</div>'
|
|
).join('')) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
container.querySelector('#plugin-install-btn')?.addEventListener('click', () => openInstallModal(vault));
|
|
container.querySelector('#plugin-template-btn')?.addEventListener('click', () => openTemplateModal());
|
|
|
|
container.querySelectorAll('.plugin-actions button').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
const action = btn.dataset.action;
|
|
const name = btn.dataset.name;
|
|
try {
|
|
if (action === 'enable') {
|
|
await enablePlugin(vault, name);
|
|
showToast(t('plugins.enabled', { name }), 'success');
|
|
} else if (action === 'disable') {
|
|
await disablePlugin(vault, name);
|
|
showToast(t('plugins.disabled', { name }), 'success');
|
|
} else if (action === 'uninstall') {
|
|
if (confirm(t('plugins.confirm_uninstall', { name }))) {
|
|
await uninstallPlugin(vault, name);
|
|
showToast(t('plugins.uninstalled', { name }), 'success');
|
|
}
|
|
} else if (action === 'code') {
|
|
const code = await getPluginCode(vault, name);
|
|
openCodeModal(name, code);
|
|
}
|
|
await renderPluginsSettings(vault, container);
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
safeCreateIcons();
|
|
}
|
|
|
|
function openInstallModal(vault) {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML =
|
|
'<div class="modal" style="max-width: 700px;">' +
|
|
'<div class="modal-header">' +
|
|
'<h3 data-i18n="plugins.install">Install Plugin</h3>' +
|
|
'<button class="modal-close" data-i18n="common.close">✕</button>' +
|
|
'</div>' +
|
|
'<div class="modal-body">' +
|
|
'<div class="form-group">' +
|
|
'<label data-i18n="plugins.manifest_json">Manifest (plugin.json)</label>' +
|
|
'<textarea id="plugin-manifest-json" class="modal-textarea" rows="15" spellcheck="false"' +
|
|
' placeholder=\'{"name":"my-plugin","version":"1.0.0","description":"...","author":"You","main":"index.js","hooks":{"onFileRender":"renderFile"},"permissions":["read_files"]}\'></textarea>' +
|
|
'</div>' +
|
|
'<div class="form-group">' +
|
|
'<label data-i18n="plugins.code">Plugin Code (main file)</label>' +
|
|
'<textarea id="plugin-code" class="modal-textarea" rows="20" spellcheck="false"' +
|
|
' placeholder="// Your plugin code here\\nexport async function renderFile(context) { ... }"></textarea>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="modal-footer">' +
|
|
'<button class="btn btn-secondary modal-cancel" data-i18n="common.cancel">Cancel</button>' +
|
|
'<button class="btn btn-primary" id="plugin-install-confirm" data-i18n="plugins.install">Install</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('.modal-close')?.addEventListener('click', () => modal.remove());
|
|
modal.querySelector('.modal-cancel')?.addEventListener('click', () => modal.remove());
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
|
|
modal.querySelector('#plugin-install-confirm')?.addEventListener('click', async () => {
|
|
const manifestText = modal.querySelector('#plugin-manifest-json').value;
|
|
const code = modal.querySelector('#plugin-code').value;
|
|
|
|
try {
|
|
const manifest = JSON.parse(manifestText);
|
|
const validation = await validateManifest(manifest);
|
|
if (!validation.valid) throw new Error(validation.error || 'Invalid manifest');
|
|
|
|
await installPlugin(vault, manifest, code);
|
|
showToast(t('plugins.installed', { name: manifest.name }), 'success');
|
|
modal.remove();
|
|
|
|
const container = document.getElementById('cfg-plugins');
|
|
if (container) await renderPluginsSettings(vault, container);
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function openTemplateModal() {
|
|
getPluginTemplate().then(template => {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML =
|
|
'<div class="modal" style="max-width: 700px;">' +
|
|
'<div class="modal-header">' +
|
|
'<h3 data-i18n="plugins.template">Plugin Template</h3>' +
|
|
'<button class="modal-close" data-i18n="common.close">✕</button>' +
|
|
'</div>' +
|
|
'<div class="modal-body">' +
|
|
'<div class="form-group">' +
|
|
'<label data-i18n="plugins.manifest_json">Manifest</label>' +
|
|
'<pre class="code-block" style="max-height: 200px; overflow: auto;">' + JSON.stringify(template.manifest, null, 2) + '</pre>' +
|
|
'</div>' +
|
|
'<div class="form-group">' +
|
|
'<label data-i18n="plugins.code">Code</label>' +
|
|
'<pre class="code-block" style="max-height: 300px; overflow: auto;">' + template.code + '</pre>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="modal-footer">' +
|
|
'<button class="btn btn-primary" data-i18n="common.close" onclick="this.closest(\'.modal-overlay\').remove()">Close</button>' +
|
|
'<button class="btn btn-secondary" onclick="copyTemplate()">Copy to Clipboard</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('.modal-close')?.addEventListener('click', () => modal.remove());
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
|
|
window.copyTemplate = () => {
|
|
navigator.clipboard.writeText(JSON.stringify(template, null, 2));
|
|
showToast('Template copied to clipboard', 'success');
|
|
};
|
|
});
|
|
}
|
|
|
|
function openCodeModal(name, code) {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML =
|
|
'<div class="modal" style="max-width: 800px;">' +
|
|
'<div class="modal-header">' +
|
|
'<h3>' + name + ' — Source Code</h3>' +
|
|
'<button class="modal-close" data-i18n="common.close">✕</button>' +
|
|
'</div>' +
|
|
'<div class="modal-body">' +
|
|
'<pre class="code-block" style="max-height: 500px; overflow: auto;">' + escapeHtml(code) + '</pre>' +
|
|
'</div>' +
|
|
'<div class="modal-footer">' +
|
|
'<button class="btn btn-primary" data-i18n="common.close" onclick="this.closest(\'.modal-overlay\').remove()">Close</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(modal);
|
|
modal.querySelector('.modal-close')?.addEventListener('click', () => modal.remove());
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ── Auto-init when settings page loads ────────────────────────────────────
|
|
document.addEventListener('click', (e) => {
|
|
const link = e.target.closest('[href="#cfg-plugins"]');
|
|
if (link) {
|
|
setTimeout(() => {
|
|
const container = document.getElementById('cfg-plugins');
|
|
if (container && !container.dataset.pluginsLoaded) {
|
|
container.dataset.pluginsLoaded = 'true';
|
|
const vault = document.querySelector('[data-current-vault]')?.dataset.currentVault;
|
|
if (vault) renderPluginsSettings(vault, container);
|
|
}
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
// Export for other modules
|
|
export const PluginManager = {
|
|
loadPlugins,
|
|
getCachedPlugins,
|
|
installPlugin,
|
|
uninstallPlugin,
|
|
enablePlugin,
|
|
disablePlugin,
|
|
getPluginCode,
|
|
validateManifest,
|
|
getPluginTemplate,
|
|
executeHook,
|
|
onFileRender,
|
|
onSearchFilter,
|
|
onEditorAction,
|
|
onSidebarItem,
|
|
onFileCreate,
|
|
onFileDelete,
|
|
onVaultMount,
|
|
renderPluginsSettings,
|
|
};
|
|
|
|
// Initialize plugins system
|
|
export async function initPlugins() {
|
|
// Pre-load plugins for current vault if available
|
|
const vault = document.querySelector('[data-current-vault]')?.dataset.currentVault;
|
|
if (vault) {
|
|
await loadPlugins(vault);
|
|
}
|
|
} |