From 0a26ecccd5bf9f90d5c50486c21bd6d722a18c8c Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Tue, 9 Sep 2025 21:13:27 +1200 Subject: [PATCH] Add categories and authors metadata to plugins Introduces optional 'categories' and 'authors' fields to plugin manifests, updates plugin-manager.js to normalize and expose these fields, and enhances the settings UI to display plugin tags and authors. Also updates documentation and an example plugin manifest to demonstrate the new fields. --- README-PLUGINS.md | 4 ++++ bookmarks.backup.json | 7 +------ plugin-manager.js | 28 +++++++++++++++++++++++++++- plugins/nebot/plugin.json | 7 ++++++- renderer/settings.css | 4 ++++ renderer/settings.js | 6 ++++++ 6 files changed, 48 insertions(+), 8 deletions(-) diff --git a/README-PLUGINS.md b/README-PLUGINS.md index 8291e57..97a809b 100644 --- a/README-PLUGINS.md +++ b/README-PLUGINS.md @@ -22,6 +22,8 @@ Example: "description": "What it does", "main": "main.js", "rendererPreload": "renderer-preload.js", + "categories": ["Search", "Productivity"], + "authors": ["Jane Doe", { "name": "Acme Labs", "email": "oss@acme.example" }], "enabled": true } ``` @@ -30,6 +32,8 @@ Fields: - id: Unique id. Defaults to folder name if omitted. - main: Optional entry for main process integration. - rendererPreload: Optional file injected into the preload. Use it to expose limited APIs. +- categories: Optional string or array of strings used for organizing/filtering plugins in UI and APIs. Example: ["AI", "Utilities"]. +- authors: Optional string or array of strings/objects describing authors. Objects support { name, email, url }. In APIs/UI, names are displayed. - enabled: Defaults to true. ## Main process API (activate) diff --git a/bookmarks.backup.json b/bookmarks.backup.json index 321638d..c44dc44 100644 --- a/bookmarks.backup.json +++ b/bookmarks.backup.json @@ -1,8 +1,3 @@ [ - { - "title": "Discord", - "url": "discord.gg", - "icon": "data:image/svg+xml;utf8,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EDiscord%3C%2Ftitle%3E%3Cpath%20d%3D%22M20.317%204.3698a19.7913%2019.7913%200%2000-4.8851-1.5152.0741.0741%200%2000-.0785.0371c-.211.3753-.4447.8648-.6083%201.2495-1.8447-.2762-3.68-.2762-5.4868%200-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077%200%2000-.0785-.037%2019.7363%2019.7363%200%2000-4.8852%201.515.0699.0699%200%2000-.0321.0277C.5334%209.0458-.319%2013.5799.0992%2018.0578a.0824.0824%200%2000.0312.0561c2.0528%201.5076%204.0413%202.4228%205.9929%203.0294a.0777.0777%200%2000.0842-.0276c.4616-.6304.8731-1.2952%201.226-1.9942a.076.076%200%2000-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077%200%2001-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743%200%2001.0776-.0105c3.9278%201.7933%208.18%201.7933%2012.0614%200a.0739.0739%200%2001.0785.0095c.1202.099.246.1981.3728.2924a.077.077%200%2001-.0066.1276%2012.2986%2012.2986%200%2001-1.873.8914.0766.0766%200%2000-.0407.1067c.3604.698.7719%201.3628%201.225%201.9932a.076.076%200%2000.0842.0286c1.961-.6067%203.9495-1.5219%206.0023-3.0294a.077.077%200%2000.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061%200%2000-.0312-.0286zM8.02%2015.3312c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9555-2.4189%202.157-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.9555%202.4189-2.1569%202.4189zm7.9748%200c-1.1825%200-2.1569-1.0857-2.1569-2.419%200-1.3332.9554-2.4189%202.1569-2.4189%201.2108%200%202.1757%201.0952%202.1568%202.419%200%201.3332-.946%202.4189-2.1568%202.4189Z%22%2F%3E%3C%2Fsvg%3E", - "iconSet": "simple" - } + ] \ No newline at end of file diff --git a/plugin-manager.js b/plugin-manager.js index f0c9a05..2ceb9d3 100644 --- a/plugin-manager.js +++ b/plugin-manager.js @@ -42,7 +42,19 @@ class PluginManager { const dir = path.join(root, ent.name); const manifestPath = path.join(dir, 'plugin.json'); let manifest; - try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { continue; } + try { + manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + // Normalize optional fields + const cats = manifest.categories; + if (typeof cats === 'string') manifest.categories = [cats]; + else if (Array.isArray(cats)) manifest.categories = cats.filter(x => typeof x === 'string'); + else if (cats == null) manifest.categories = []; + + const au = manifest.authors; + if (typeof au === 'string') manifest.authors = [au]; + else if (Array.isArray(au)) manifest.authors = au.filter(x => (typeof x === 'string') || (x && typeof x === 'object' && typeof x.name === 'string')); + else if (au == null) manifest.authors = []; + } catch { continue; } const enabled = manifest.enabled !== false; // default true const id = manifest.id || ent.name; const record = { id, dir, manifest, enabled, mod: null, mainPath: null }; @@ -171,6 +183,10 @@ class PluginManager { name: p.manifest.name || p.id, version: p.manifest.version || '0.0.0', description: p.manifest.description || '', + categories: Array.isArray(p.manifest.categories) ? p.manifest.categories : [], + authors: Array.isArray(p.manifest.authors) + ? p.manifest.authors.map(x => (typeof x === 'string' ? x : (x && x.name) || '')).filter(Boolean) + : [], enabled: !!p.enabled, hasMain: !!p.manifest.main, hasRendererPreload: !!p.manifest.rendererPreload, @@ -190,11 +206,21 @@ class PluginManager { const manifestPath = path.join(dir, 'plugin.json'); try { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const cats = manifest.categories; + const categories = typeof cats === 'string' ? [cats] : Array.isArray(cats) ? cats.filter(x => typeof x === 'string') : []; + const au = manifest.authors; + const authors = typeof au === 'string' + ? [au] + : Array.isArray(au) + ? au.map(x => (typeof x === 'string' ? x : (x && x.name) || null)).filter(Boolean) + : []; out.push({ id: manifest.id || ent.name, name: manifest.name || ent.name, version: manifest.version || '0.0.0', description: manifest.description || '', + categories, + authors, enabled: manifest.enabled !== false, hasMain: !!manifest.main, hasRendererPreload: !!manifest.rendererPreload, diff --git a/plugins/nebot/plugin.json b/plugins/nebot/plugin.json index 2dbe9ec..4c3f7d4 100644 --- a/plugins/nebot/plugin.json +++ b/plugins/nebot/plugin.json @@ -1,9 +1,14 @@ { - "id": "ollama-chat", + "id": "nebot-chat", "name": "Nebot", "version": "0.1.0", "description": "Nebot: a floating chat panel that talks to a local/remote Ollama server and saves chats in the plugin folder.", "main": "main.js", "rendererPreload": "renderer-preload.js", + "categories": ["AI", "Chat", "Utilities"], + "authors": [ + { "name": "Nebula Team", "email": "andrewzambazos@gmail.com" }, + "Bobbybear007" + ], "enabled": true } \ No newline at end of file diff --git a/renderer/settings.css b/renderer/settings.css index 70bc51b..ba83138 100644 --- a/renderer/settings.css +++ b/renderer/settings.css @@ -8,6 +8,10 @@ .plugin-desc { opacity:.8; font-size:.9em; } .plugin-actions { display:flex; gap:8px; align-items:center; } .plugin-actions .spacer { width:8px; } +.plugin-tags { display:flex; flex-wrap: wrap; gap:6px; margin-top: 4px; } +.plugin-tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:999px; font-size:.75em; opacity:.9; border:1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.06); } +.plugin-authors { margin-top: 4px; font-size:.85em; opacity:.85; } +.plugin-authors .muted { opacity:.7; margin-right: 6px; } :root { --bg: #121418; --dark-blue: #0B1C2B; diff --git a/renderer/settings.js b/renderer/settings.js index 12b1ade..d14e02d 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -355,6 +355,10 @@ async function loadPluginsUI() { listEl.appendChild(empty); } else { for (const p of items) { + const categories = Array.isArray(p.categories) ? p.categories.filter(x => x && typeof x === 'string') : []; + const authors = Array.isArray(p.authors) ? p.authors.filter(x => x && typeof x === 'string') : []; + const tagsHtml = categories.length ? `
${categories.map(c => `${escapeHtml(c)}`).join('')}
` : ''; + const authorsHtml = authors.length ? `
Authors: ${authors.map(a => `${escapeHtml(a)}`).join(', ')}
` : ''; const row = document.createElement('div'); row.className = 'plugin-item'; row.setAttribute('role', 'listitem'); @@ -362,6 +366,8 @@ async function loadPluginsUI() {
${escapeHtml(p.name)} v${escapeHtml(p.version)}
${escapeHtml(p.description || '')}
+ ${tagsHtml} + ${authorsHtml}
${escapeHtml(p.dir)}