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
|
// 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').textContent = 'example-plugin loaded';
|
||||||
document.getElementById('status-badge').classList.add('loaded');
|
document.getElementById('status-badge').classList.add('loaded');
|
||||||
@@ -109,39 +133,67 @@
|
|||||||
// Plugin Manager panel
|
// Plugin Manager panel
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** Renders the plugin directory card list. */
|
/** Renders the installed-plugins card list, including unloaded entries. */
|
||||||
function refreshPluginList() {
|
function refreshPluginList() {
|
||||||
const listEl = document.getElementById('plugin-list');
|
const listEl = document.getElementById('plugin-list');
|
||||||
const plugins = registry.getRegisteredPlugins();
|
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>';
|
listEl.innerHTML = '<div id="no-plugins-msg" style="color:#555;font-size:12px;text-align:center;padding:20px 0;">No plugins loaded</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
listEl.innerHTML = plugins.map(p => `
|
listEl.innerHTML = Array.from(knownPlugins.entries()).map(([id, known]) => {
|
||||||
<div class="plugin-card" data-id="${p.id}">
|
const live = loadedMap.get(id);
|
||||||
<div class="plugin-card-header">
|
const loaded = !!live;
|
||||||
<span class="plugin-name">${p.name}</span>
|
const statusBadge = loaded
|
||||||
<span class="plugin-version">v${p.version}</span>
|
? '<span class="badge green">loaded</span>'
|
||||||
</div>
|
: '<span class="badge red">unloaded</span>';
|
||||||
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${p.id}</div>
|
const featureBadges = live ? `
|
||||||
<div class="plugin-badges">
|
${live.commandCount ? `<span class="badge">${live.commandCount} cmd${live.commandCount !== 1 ? 's' : ''}</span>` : ''}
|
||||||
<span class="badge green">loaded</span>
|
${live.interceptorCount ? `<span class="badge">${live.interceptorCount} interceptor${live.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
|
||||||
${p.commandCount ? `<span class="badge">${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}</span>` : ''}
|
${live.themeCount ? `<span class="badge amber">${live.themeCount} theme${live.themeCount !== 1 ? 's' : ''}</span>` : ''}
|
||||||
${p.interceptorCount ? `<span class="badge">${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
|
${live.hasSettings ? '<span class="badge">has settings</span>' : ''}
|
||||||
${p.themeCount ? `<span class="badge amber">${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}</span>` : ''}
|
` : '';
|
||||||
${p.hasSettings ? `<span class="badge">has settings</span>` : ''}
|
const actionBtn = loaded
|
||||||
</div>
|
? `<button class="plugin-unload-btn" data-action="unload" data-id="${id}">Unload</button>`
|
||||||
<button class="plugin-unload-btn" data-id="${p.id}">Unload</button>
|
: `<button class="plugin-unload-btn" style="background:#1a4a2e;color:#4caf50;" data-action="reload" data-id="${id}">Reload</button>`;
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
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 () => {
|
btn.addEventListener('click', async () => {
|
||||||
const id = btn.dataset.id;
|
const id = btn.dataset.id;
|
||||||
await registry.unregisterPlugin(id);
|
const action = btn.dataset.action;
|
||||||
addLog(`Plugin unloaded: ${id}`, 'warn');
|
|
||||||
|
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();
|
refreshPluginList();
|
||||||
refreshCommandList();
|
refreshCommandList();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
@@ -167,27 +219,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = p.id ?? path.basename(path.dirname(filePath));
|
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`);
|
throw new Error(`Plugin with id "${id}" is already loaded`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = createPluginContext(
|
await registerAndLoad(id, p, { filePath });
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
addLog(`Plugin loaded from path: ${id}`, 'success');
|
addLog(`Plugin loaded from path: ${id}`, 'success');
|
||||||
refreshPluginList();
|
refreshPluginList();
|
||||||
@@ -321,26 +357,9 @@
|
|||||||
throw new Error(`Plugin ${pluginId} does not export a valid plugin object`);
|
throw new Error(`Plugin ${pluginId} does not export a valid plugin object`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = pluginId;
|
await registerAndLoad(pluginId, p, { downloadUrl });
|
||||||
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);
|
addLog(`Plugin installed from directory: ${pluginId}`, 'success');
|
||||||
await p.onLoad(ctx);
|
|
||||||
|
|
||||||
addLog(`Plugin installed from directory: ${id}`, 'success');
|
|
||||||
refreshPluginList();
|
refreshPluginList();
|
||||||
refreshCommandList();
|
refreshCommandList();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user