+
+
+
+
+
+
+
+
+
Load plugin from file:
+
+
+
+
+
Click Refresh to fetch the plugin directory.
+
+
+
diff --git a/test/electron/renderer.js b/test/electron/renderer.js
index 77aaa20..a7ea8c2 100644
--- a/test/electron/renderer.js
+++ b/test/electron/renderer.js
@@ -211,6 +211,179 @@
refreshPluginList();
+ // ---------------------------------------------------------------------------
+ // Plugin Manager — tab switching
+ // ---------------------------------------------------------------------------
+ const tabInstalled = document.getElementById('tab-installed');
+ const tabDirectory = document.getElementById('tab-directory');
+ const paneInstalled = document.getElementById('pane-installed');
+ const paneDirectory = document.getElementById('pane-directory');
+
+ tabInstalled.addEventListener('click', () => {
+ tabInstalled.classList.add('active');
+ tabDirectory.classList.remove('active');
+ paneInstalled.classList.add('active');
+ paneDirectory.classList.remove('active');
+ });
+
+ tabDirectory.addEventListener('click', () => {
+ tabDirectory.classList.add('active');
+ tabInstalled.classList.remove('active');
+ paneDirectory.classList.add('active');
+ paneInstalled.classList.remove('active');
+ });
+
+ // ---------------------------------------------------------------------------
+ // Plugin Directory — fetch from remote index
+ // ---------------------------------------------------------------------------
+ const PLUGIN_INDEX_URL =
+ 'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/index.json';
+ const PLUGIN_BASE_URL =
+ 'https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/';
+
+ let directoryPlugins = [];
+ const dirStatusEl = document.getElementById('dir-status');
+ const dirListEl = document.getElementById('dir-list');
+ const dirSearchEl = document.getElementById('dir-search');
+
+ /** Renders marketplace cards, optionally filtered by a search query. */
+ function renderDirectoryList(query = '') {
+ const q = query.toLowerCase();
+ const filtered = directoryPlugins.filter(p =>
+ !q ||
+ p.name.toLowerCase().includes(q) ||
+ p.description.toLowerCase().includes(q) ||
+ (p.author ?? '').toLowerCase().includes(q) ||
+ (p.tags ?? []).some(t => t.toLowerCase().includes(q))
+ );
+
+ if (!filtered.length) {
+ dirListEl.innerHTML = '
No plugins found.
';
+ return;
+ }
+
+ const loadedIds = new Set(registry.getRegisteredPlugins().map(p => p.id));
+
+ dirListEl.innerHTML = filtered.map(p => {
+ const isLoaded = loadedIds.has(p.id);
+ const initial = (p.name ?? '?').charAt(0).toUpperCase();
+ return `
+
+
+
${p.description ?? ''}
+
+
+ `;
+ }).join('');
+
+ dirListEl.querySelectorAll('.market-install-btn:not([disabled])').forEach(btn => {
+ btn.addEventListener('click', () => downloadAndLoadPlugin(btn.dataset.id, btn.dataset.url));
+ });
+ }
+
+ /** Downloads a plugin's index.js from the marketplace and loads it into the registry. */
+ async function downloadAndLoadPlugin(pluginId, downloadUrl) {
+ if (!downloadUrl) {
+ addLog(`No download URL for plugin ${pluginId}`, 'error');
+ return;
+ }
+ try {
+ addLog(`Downloading plugin ${pluginId}…`, 'info');
+ const res = await fetch(downloadUrl);
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${downloadUrl}`);
+ const code = await res.text();
+
+ const m = { exports: {} };
+ // eslint-disable-next-line no-new-func
+ new Function('module', 'exports', code)(m, m.exports);
+ const p = m.exports;
+
+ if (!p || typeof p.onLoad !== 'function') {
+ 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');
+ };
+
+ registry.registerPlugin(id, p, ctx);
+ await p.onLoad(ctx);
+
+ addLog(`Plugin installed from directory: ${id}`, 'success');
+ refreshPluginList();
+ refreshCommandList();
+ renderSettings();
+ renderDirectoryList(dirSearchEl.value);
+ } catch (err) {
+ addLog(`Failed to install ${pluginId}: ${err.message}`, 'error');
+ }
+ }
+
+ /** Fetches the plugin index and all plugin metadata from the remote directory. */
+ async function fetchDirectory() {
+ dirStatusEl.textContent = 'Fetching plugin directory…';
+ dirListEl.innerHTML = '';
+ try {
+ const indexRes = await fetch(PLUGIN_INDEX_URL);
+ if (!indexRes.ok) throw new Error(`HTTP ${indexRes.status}`);
+ const index = await indexRes.json();
+
+ const metas = await Promise.all(
+ index.plugins.map(async (id) => {
+ try {
+ const r = await fetch(`${PLUGIN_BASE_URL}${id}.json`);
+ return r.ok ? r.json() : null;
+ } catch {
+ return null;
+ }
+ })
+ );
+
+ directoryPlugins = metas.filter(Boolean);
+ dirStatusEl.textContent = `${directoryPlugins.length} plugin${directoryPlugins.length !== 1 ? 's' : ''} in directory.`;
+ addLog(`Plugin directory fetched: ${directoryPlugins.length} entries`, 'success');
+ renderDirectoryList(dirSearchEl.value);
+ } catch (err) {
+ dirStatusEl.textContent = `Failed to fetch directory: ${err.message}`;
+ addLog(`Directory fetch failed: ${err.message}`, 'error');
+ }
+ }
+
+ document.getElementById('dir-refresh').addEventListener('click', fetchDirectory);
+
+ dirSearchEl.addEventListener('input', () => renderDirectoryList(dirSearchEl.value));
+
// Populate registered command list
function refreshCommandList() {
const cmdListEl = document.getElementById('cmd-list');