Add plugin system with sample plugin and settings UI

Introduces a plugin architecture, including a PluginManager, plugin loading in main and renderer processes, and a sample plugin demonstrating menu, IPC, and context menu contributions. Adds a Plugins tab to the settings UI for managing plugins (enable/disable, reload), and updates preload.js to load renderer preloads from plugins. Documentation for plugin development is included in README-PLUGINS.md.
This commit is contained in:
2025-09-08 19:10:05 +12:00
parent 62810fcb89
commit e228ca6317
11 changed files with 576 additions and 2 deletions
+9
View File
@@ -1101,6 +1101,15 @@ function updateZoomUI() {
function zoomIn() { ipcRenderer.invoke('zoom-in').then(updateZoomUI); }
function zoomOut() { ipcRenderer.invoke('zoom-out').then(updateZoomUI); }
// Optional: sample plugin demo hook (safe if plugin missing)
try {
if (window.sampleHello && typeof window.sampleHello.onHello === 'function') {
window.sampleHello.onHello((payload) => {
console.log('[Sample Plugin] Hello message:', payload);
});
}
} catch {}
// Utility: close the menu when interacting with a given element (e.g., webview)
function attachCloseMenuOnInteract(el) {
if (!el) return;
+10
View File
@@ -1,3 +1,13 @@
/* existing styles */
/* Plugins panel */
.plugins-list { display: grid; gap: 10px; }
.plugin-item { display:flex; justify-content:space-between; align-items:center; border:1px solid rgba(255,255,255,0.12); padding:10px; border-radius:8px; background: rgba(255,255,255,0.03); }
.plugin-meta { display:flex; flex-direction:column; gap:2px; min-width:0; }
.plugin-title { font-weight:600; }
.plugin-desc { opacity:.8; font-size:.9em; }
.plugin-actions { display:flex; gap:8px; align-items:center; }
.plugin-actions .spacer { width:8px; }
:root {
--bg: #121418;
--dark-blue: #0B1C2B;
+16
View File
@@ -15,6 +15,7 @@
<button class="tab-link active" role="tab" aria-selected="true" aria-controls="panel-general" id="tab-general" data-tab="general">General</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-appearance" id="tab-appearance" data-tab="appearance">Appearance</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-history" id="tab-history" data-tab="history">History</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-plugins" id="tab-plugins" data-tab="plugins">Plugins</button>
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-about" id="tab-about" data-tab="about">About</button>
</nav>
</aside>
@@ -196,6 +197,21 @@
</div>
</section>
<!-- Plugins Panel -->
<section class="tab-panel" id="panel-plugins" role="tabpanel" aria-labelledby="tab-plugins">
<h2>Plugins</h2>
<div class="customization-group">
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<button id="plugins-reload-all">Reload Plugins</button>
<span class="note">Changes to renderer preloads may require app restart.</span>
</div>
</div>
<div class="customization-group">
<h3>Installed</h3>
<div id="plugins-list" class="plugins-list" role="list"></div>
</div>
</section>
<!-- About Panel -->
<section class="tab-panel" id="panel-about" role="tabpanel" aria-labelledby="tab-about">
<h2>About</h2>
+87
View File
@@ -332,3 +332,90 @@ window.addEventListener('DOMContentLoaded', () => {
});
}
});
// -----------------------------
// Plugins management (Settings)
// -----------------------------
async function loadPluginsUI() {
const listEl = document.getElementById('plugins-list');
const reloadAllBtn = document.getElementById('plugins-reload-all');
if (!listEl) return;
// Load list
let items = [];
try {
items = (ipc ? await ipc.invoke('plugins-list') : []) || [];
} catch (e) {
console.warn('plugins-list failed', e);
}
listEl.innerHTML = '';
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'plugin-item';
empty.textContent = 'No plugins found';
listEl.appendChild(empty);
} else {
for (const p of items) {
const row = document.createElement('div');
row.className = 'plugin-item';
row.setAttribute('role', 'listitem');
row.innerHTML = `
<div class="plugin-meta">
<div class="plugin-title">${escapeHtml(p.name)} <span style="opacity:.7;font-weight:400">v${escapeHtml(p.version)}</span></div>
<div class="plugin-desc">${escapeHtml(p.description || '')}</div>
<div class="plugin-desc" style="opacity:.6; font-size:.85em;">${escapeHtml(p.dir)}</div>
</div>
<div class="plugin-actions">
<label style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" class="plugin-enable" ${p.enabled ? 'checked' : ''}>
<span>${p.enabled ? 'Enabled' : 'Disabled'}</span>
</label>
<span class="spacer"></span>
<button class="plugin-reload">Reload</button>
</div>`;
// Wire actions
const enableInput = row.querySelector('input.plugin-enable');
const labelSpan = row.querySelector('label span');
enableInput.addEventListener('change', async () => {
const enabled = enableInput.checked;
try {
if (ipc) await ipc.invoke('plugins-set-enabled', { id: p.id, enabled });
labelSpan.textContent = enabled ? 'Enabled' : 'Disabled';
showStatus(`${p.name}: ${enabled ? 'Enabled' : 'Disabled'}.`);
} catch (e) {
console.error('Failed to toggle plugin', p.id, e);
enableInput.checked = !enabled;
labelSpan.textContent = enableInput.checked ? 'Enabled' : 'Disabled';
showStatus('Failed updating plugin');
}
});
const reloadBtn = row.querySelector('button.plugin-reload');
reloadBtn.addEventListener('click', async () => {
try {
if (ipc) await ipc.invoke('plugins-reload', { id: p.id });
showStatus(`${p.name} reloaded.`);
} catch (e) {
console.error('Plugin reload failed', e);
showStatus('Reload failed');
}
});
listEl.appendChild(row);
}
}
if (reloadAllBtn) reloadAllBtn.onclick = async () => {
try { if (ipc) await ipc.invoke('plugins-reload', {}); showStatus('Plugins reloaded.'); } catch { showStatus('Reload failed'); }
};
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[c]));
}
// Load when settings page shows Plugins tab for the first time
window.addEventListener('DOMContentLoaded', () => {
const tabBtn = document.getElementById('tab-plugins');
if (!tabBtn) return;
let loaded = false;
const ensureLoad = () => { if (!loaded) { loaded = true; loadPluginsUI(); } };
tabBtn.addEventListener('click', ensureLoad);
if (location.hash === '#plugins') ensureLoad();
});