diff --git a/test/electron/renderer.js b/test/electron/renderer.js index a7ea8c2..d662a22 100644 --- a/test/electron/renderer.js +++ b/test/electron/renderer.js @@ -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} + */ + 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 = '
No plugins loaded
'; return; } - listEl.innerHTML = plugins.map(p => ` -
-
- ${p.name} - v${p.version} -
-
${p.id}
-
- loaded - ${p.commandCount ? `${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}` : ''} - ${p.interceptorCount ? `${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}` : ''} - ${p.themeCount ? `${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}` : ''} - ${p.hasSettings ? `has settings` : ''} -
- -
- `).join(''); + listEl.innerHTML = Array.from(knownPlugins.entries()).map(([id, known]) => { + const live = loadedMap.get(id); + const loaded = !!live; + const statusBadge = loaded + ? 'loaded' + : 'unloaded'; + const featureBadges = live ? ` + ${live.commandCount ? `${live.commandCount} cmd${live.commandCount !== 1 ? 's' : ''}` : ''} + ${live.interceptorCount ? `${live.interceptorCount} interceptor${live.interceptorCount !== 1 ? 's' : ''}` : ''} + ${live.themeCount ? `${live.themeCount} theme${live.themeCount !== 1 ? 's' : ''}` : ''} + ${live.hasSettings ? 'has settings' : ''} + ` : ''; + const actionBtn = loaded + ? `` + : ``; - listEl.querySelectorAll('.plugin-unload-btn').forEach(btn => { + return ` +
+
+ ${known.name} + v${known.version} +
+
${id}
+
${statusBadge}${featureBadges}
+ ${actionBtn} +
+ `; + }).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();