// ── 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);