// ── Plugin Settings UI ── // Toggles collect pending changes client-side and commit together via // the Apply button. Only reloads once all changes are committed. import { $, esc, api } from './util.js'; let pluginsCache = null; let pending = new Map(); // name -> desired enabled state async function fetchPlugins() { const res = await fetch('api/plugins'); if (!res.ok) throw new Error('failed to load plugins'); pluginsCache = await res.json(); return pluginsCache; } function render(list) { const container = $('plugins-list'); if (!container) return; if (!list.length) { container.innerHTML = '

No plugins installed. Drop a plugin folder into public/plugins/ to add one.

'; return; } container.innerHTML = list.map(p => { const effectiveEnabled = pending.has(p.name) ? pending.get(p.name) : p.enabled; const isPending = pending.has(p.name); return `
${esc(p.name)} v${esc(p.version || '?')} ${p.hasBackend ? 'php' : ''} ${isPending ? `${effectiveEnabled ? 'will enable' : 'will disable'}` : ''}
${p.description ? `
${esc(p.description)}
` : ''}
`; }).join(''); container.querySelectorAll('.plugin-toggle').forEach(input => { input.addEventListener('change', onToggle); }); updateApplyBar(); } function onToggle(e) { const input = e.currentTarget; const name = input.dataset.name; const original = input.dataset.original === '1'; const desired = input.checked; if (desired === original) { pending.delete(name); } else { pending.set(name, desired); } render(pluginsCache); // re-render to update badge and border } function updateApplyBar() { const bar = $('plugins-apply-bar'); if (!bar) return; if (pending.size === 0) { bar.classList.add('hidden'); return; } bar.classList.remove('hidden'); const count = pending.size; const label = $('plugins-apply-count'); if (label) label.textContent = `${count} pending change${count !== 1 ? 's' : ''}`; } async function applyChanges() { if (pending.size === 0) return; const btn = $('plugins-apply-btn'); if (btn) { btn.disabled = true; btn.textContent = 'applying...'; } try { for (const [name, enabled] of pending) { const res = await api('api/plugins/toggle', { method: 'POST', body: { name, enabled } }); if (res.error) throw new Error(`${name}: ${res.error}`); } pending.clear(); // Reload so the new plugin set takes effect location.reload(); } catch (err) { if (btn) { btn.disabled = false; btn.textContent = 'Apply'; } alert('Failed to apply: ' + err.message); } } function discardChanges() { pending.clear(); render(pluginsCache); } export async function openPluginSettings() { const container = $('plugins-list'); if (!container) return; container.innerHTML = '

Loading...

'; try { const list = await fetchPlugins(); render(list); } catch (err) { container.innerHTML = `

Failed to load plugins: ${esc(err.message)}

`; } } $('plugins-apply-btn')?.addEventListener('click', applyChanges); $('plugins-discard-btn')?.addEventListener('click', discardChanges);