diff --git a/test/electron/index.html b/test/electron/index.html index a1e0070..c70aa44 100644 --- a/test/electron/index.html +++ b/test/electron/index.html @@ -210,6 +210,75 @@ #load-plugin-row button { white-space: nowrap; font-size: 11px; padding: 4px 10px; } #no-plugins-msg { color: #555; font-size: 12px; text-align: center; padding: 20px 0; } + + /* Manager tabs */ + .tab-bar { + display: flex; + gap: 1px; + background: #0f3460; + flex-shrink: 0; + } + .tab-bar button { + flex: 1; + padding: 7px 4px; + border-radius: 0; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + background: #16213e; + color: #555; + border: none; + border-bottom: 2px solid transparent; + transition: color 0.15s, border-color 0.15s; + } + .tab-bar button.active { + color: #7986cb; + border-bottom-color: #7986cb; + background: #1a1a2e; + } + .tab-bar button:hover:not(.active) { color: #90a4ae; background: #1a1a2e; } + + .tab-pane { display: none; flex-direction: column; gap: 10px; } + .tab-pane.active { display: flex; } + + /* Marketplace cards */ + .market-card { + background: #0f3460; + border: 1px solid #1e3a6e; + border-radius: 6px; + padding: 10px; + display: flex; + flex-direction: column; + gap: 5px; + } + .market-card-header { display: flex; align-items: center; gap: 8px; } + .market-card-thumb { + width: 36px; height: 36px; border-radius: 6px; + background: linear-gradient(135deg, #233a6e 0%, #0f3460 100%); + flex-shrink: 0; object-fit: cover; + } + .market-card-thumb-fallback { + width: 36px; height: 36px; border-radius: 6px; + background: linear-gradient(135deg, #1e3a7e 0%, #e94560 100%); + flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + font-weight: 700; font-size: 15px; color: #fff; + } + .market-info { flex: 1; min-width: 0; } + .market-name { font-weight: 700; font-size: 12px; color: #e0e0e0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .market-author { font-size: 10px; color: #7986cb; } + .market-desc { font-size: 11px; color: #90a4ae; line-height: 1.4; } + .market-footer { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } + .market-install-btn { + font-size: 10px; padding: 3px 10px; background: #e94560; color: #fff; + border-radius: 4px; margin-left: auto; + } + .market-install-btn:hover { background: #c73652; } + .market-install-btn:disabled { background: #333; color: #555; cursor: default; } + .market-tag { font-size: 9px; padding: 1px 5px; border-radius: 6px; background: #162250; color: #7986cb; } + #dir-search { margin-bottom: 4px; } + #dir-status { font-size: 11px; color: #7986cb; text-align: center; padding: 6px 0; } @@ -220,9 +289,17 @@
-
-
Plugin Manager
-
+
+
+ Plugin Manager +
+
+ + +
+ + +
Load plugin from file:
@@ -231,6 +308,14 @@
Registered plugins:
No plugins loaded
+ + +
+ +
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.name} + +
+
${p.name}
+
by ${p.author ?? 'unknown'}  ·  v${p.version ?? '?'}
+
+
+
${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');