Keep unloaded plugins in list with Reload button
This commit is contained in:
@@ -75,31 +75,55 @@
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin context
|
||||
// ---------------------------------------------------------------------------
|
||||
const context = createPluginContext(
|
||||
{
|
||||
pluginId: 'example-plugin',
|
||||
eventClient: eventEmitter,
|
||||
onNotify: (opts) =>
|
||||
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
|
||||
},
|
||||
registry
|
||||
);
|
||||
|
||||
// Intercept context logging so it appears in the UI panel
|
||||
context.log = (...args) => {
|
||||
console.log('[Plugin example-plugin]', ...args);
|
||||
registry.addLog('example-plugin', 'log', args);
|
||||
addLog(args.map(String).join(' '), 'log');
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register + load
|
||||
// ---------------------------------------------------------------------------
|
||||
registry.registerPlugin('example-plugin', plugin, context);
|
||||
await plugin.onLoad(context);
|
||||
|
||||
/**
|
||||
* Persistent record of every plugin we have ever seen, keyed by plugin id.
|
||||
* Survives unload so the Installed list can show a Reload button.
|
||||
*
|
||||
* @type {Map<string, { name: string, version: string, pluginModule: object, filePath?: string, downloadUrl?: string }>}
|
||||
*/
|
||||
const knownPlugins = new Map();
|
||||
|
||||
/**
|
||||
* Creates a plugin context wired to the shared registry and event emitter,
|
||||
* registers and loads the plugin, then records it in knownPlugins.
|
||||
* @param {string} id
|
||||
* @param {object} pluginModule
|
||||
* @param {{ filePath?: string, downloadUrl?: string }} [source]
|
||||
*/
|
||||
async function registerAndLoad(id, pluginModule, source = {}) {
|
||||
const ctx = createPluginContext(
|
||||
{
|
||||
pluginId: id,
|
||||
eventClient: eventEmitter,
|
||||
onNotify: (opts) =>
|
||||
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
|
||||
},
|
||||
registry
|
||||
);
|
||||
ctx.log = (...args) => {
|
||||
console.log(`[Plugin ${id}]`, ...args);
|
||||
registry.addLog(id, 'log', args);
|
||||
addLog(args.map(String).join(' '), 'log');
|
||||
};
|
||||
|
||||
registry.registerPlugin(id, pluginModule, ctx);
|
||||
await pluginModule.onLoad(ctx);
|
||||
|
||||
knownPlugins.set(id, {
|
||||
name: pluginModule.name ?? id,
|
||||
version: pluginModule.version ?? '?',
|
||||
pluginModule,
|
||||
...source,
|
||||
});
|
||||
}
|
||||
|
||||
await registerAndLoad('example-plugin', plugin, {
|
||||
filePath: pluginPath,
|
||||
});
|
||||
|
||||
document.getElementById('status-badge').textContent = 'example-plugin loaded';
|
||||
document.getElementById('status-badge').classList.add('loaded');
|
||||
@@ -109,39 +133,67 @@
|
||||
// Plugin Manager panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Renders the plugin directory card list. */
|
||||
/** Renders the installed-plugins card list, including unloaded entries. */
|
||||
function refreshPluginList() {
|
||||
const listEl = document.getElementById('plugin-list');
|
||||
const plugins = registry.getRegisteredPlugins();
|
||||
const listEl = document.getElementById('plugin-list');
|
||||
const loadedMap = new Map(registry.getRegisteredPlugins().map(p => [p.id, p]));
|
||||
|
||||
if (!plugins.length) {
|
||||
if (!knownPlugins.size) {
|
||||
listEl.innerHTML = '<div id="no-plugins-msg" style="color:#555;font-size:12px;text-align:center;padding:20px 0;">No plugins loaded</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = plugins.map(p => `
|
||||
<div class="plugin-card" data-id="${p.id}">
|
||||
<div class="plugin-card-header">
|
||||
<span class="plugin-name">${p.name}</span>
|
||||
<span class="plugin-version">v${p.version}</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${p.id}</div>
|
||||
<div class="plugin-badges">
|
||||
<span class="badge green">loaded</span>
|
||||
${p.commandCount ? `<span class="badge">${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.interceptorCount ? `<span class="badge">${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.themeCount ? `<span class="badge amber">${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${p.hasSettings ? `<span class="badge">has settings</span>` : ''}
|
||||
</div>
|
||||
<button class="plugin-unload-btn" data-id="${p.id}">Unload</button>
|
||||
</div>
|
||||
`).join('');
|
||||
listEl.innerHTML = Array.from(knownPlugins.entries()).map(([id, known]) => {
|
||||
const live = loadedMap.get(id);
|
||||
const loaded = !!live;
|
||||
const statusBadge = loaded
|
||||
? '<span class="badge green">loaded</span>'
|
||||
: '<span class="badge red">unloaded</span>';
|
||||
const featureBadges = live ? `
|
||||
${live.commandCount ? `<span class="badge">${live.commandCount} cmd${live.commandCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${live.interceptorCount ? `<span class="badge">${live.interceptorCount} interceptor${live.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${live.themeCount ? `<span class="badge amber">${live.themeCount} theme${live.themeCount !== 1 ? 's' : ''}</span>` : ''}
|
||||
${live.hasSettings ? '<span class="badge">has settings</span>' : ''}
|
||||
` : '';
|
||||
const actionBtn = loaded
|
||||
? `<button class="plugin-unload-btn" data-action="unload" data-id="${id}">Unload</button>`
|
||||
: `<button class="plugin-unload-btn" style="background:#1a4a2e;color:#4caf50;" data-action="reload" data-id="${id}">Reload</button>`;
|
||||
|
||||
listEl.querySelectorAll('.plugin-unload-btn').forEach(btn => {
|
||||
return `
|
||||
<div class="plugin-card" data-id="${id}">
|
||||
<div class="plugin-card-header">
|
||||
<span class="plugin-name">${known.name}</span>
|
||||
<span class="plugin-version">v${known.version}</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${id}</div>
|
||||
<div class="plugin-badges">${statusBadge}${featureBadges}</div>
|
||||
${actionBtn}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
listEl.querySelectorAll('[data-action]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
await registry.unregisterPlugin(id);
|
||||
addLog(`Plugin unloaded: ${id}`, 'warn');
|
||||
const id = btn.dataset.id;
|
||||
const action = btn.dataset.action;
|
||||
|
||||
if (action === 'unload') {
|
||||
await registry.unregisterPlugin(id);
|
||||
addLog(`Plugin unloaded: ${id}`, 'warn');
|
||||
} else if (action === 'reload') {
|
||||
const known = knownPlugins.get(id);
|
||||
if (!known) return;
|
||||
try {
|
||||
await registerAndLoad(id, known.pluginModule, {
|
||||
filePath: known.filePath,
|
||||
downloadUrl: known.downloadUrl,
|
||||
});
|
||||
addLog(`Plugin reloaded: ${id}`, 'success');
|
||||
} catch (err) {
|
||||
addLog(`Failed to reload ${id}: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
refreshPluginList();
|
||||
refreshCommandList();
|
||||
renderSettings();
|
||||
@@ -167,27 +219,11 @@
|
||||
}
|
||||
|
||||
const id = p.id ?? path.basename(path.dirname(filePath));
|
||||
if (registry.getRegisteredPlugins().some(r => r.id === id)) {
|
||||
if (knownPlugins.has(id) && registry.getRegisteredPlugins().some(r => r.id === id)) {
|
||||
throw new Error(`Plugin with id "${id}" is already loaded`);
|
||||
}
|
||||
|
||||
const ctx = createPluginContext(
|
||||
{
|
||||
pluginId: id,
|
||||
eventClient: eventEmitter,
|
||||
onNotify: (opts) =>
|
||||
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
|
||||
},
|
||||
registry
|
||||
);
|
||||
ctx.log = (...args) => {
|
||||
console.log(`[Plugin ${id}]`, ...args);
|
||||
registry.addLog(id, 'log', args);
|
||||
addLog(args.map(String).join(' '), 'log');
|
||||
};
|
||||
|
||||
registry.registerPlugin(id, p, ctx);
|
||||
await p.onLoad(ctx);
|
||||
await registerAndLoad(id, p, { filePath });
|
||||
|
||||
addLog(`Plugin loaded from path: ${id}`, 'success');
|
||||
refreshPluginList();
|
||||
@@ -321,26 +357,9 @@
|
||||
throw new Error(`Plugin ${pluginId} does not export a valid plugin object`);
|
||||
}
|
||||
|
||||
const id = pluginId;
|
||||
const ctx = createPluginContext(
|
||||
{
|
||||
pluginId: id,
|
||||
eventClient: eventEmitter,
|
||||
onNotify: (opts) =>
|
||||
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
|
||||
},
|
||||
registry
|
||||
);
|
||||
ctx.log = (...args) => {
|
||||
console.log(`[Plugin ${id}]`, ...args);
|
||||
registry.addLog(id, 'log', args);
|
||||
addLog(args.map(String).join(' '), 'log');
|
||||
};
|
||||
await registerAndLoad(pluginId, p, { downloadUrl });
|
||||
|
||||
registry.registerPlugin(id, p, ctx);
|
||||
await p.onLoad(ctx);
|
||||
|
||||
addLog(`Plugin installed from directory: ${id}`, 'success');
|
||||
addLog(`Plugin installed from directory: ${pluginId}`, 'success');
|
||||
refreshPluginList();
|
||||
refreshCommandList();
|
||||
renderSettings();
|
||||
|
||||
Reference in New Issue
Block a user