199 lines
6.6 KiB
JavaScript
199 lines
6.6 KiB
JavaScript
// static/js/addons.js - Addon manager for manifest-based Odysseus addons
|
|
|
|
import uiModule from './ui.js';
|
|
|
|
let initialized = false;
|
|
let currentManifest = null;
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
function esc(value) {
|
|
return uiModule && typeof uiModule.esc === 'function'
|
|
? uiModule.esc(String(value ?? ''))
|
|
: String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(path, {
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
|
...options,
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);
|
|
return data;
|
|
}
|
|
|
|
function contributionSummary(addon) {
|
|
const items = Array.isArray(addon.contributions) ? addon.contributions : [];
|
|
if (!items.length) return '<span class="addon-muted">No contributions</span>';
|
|
const counts = new Map();
|
|
items.forEach(item => counts.set(item.type || 'unknown', (counts.get(item.type || 'unknown') || 0) + 1));
|
|
return Array.from(counts.entries())
|
|
.map(([type, count]) => `<span class="addon-chip">${esc(type)}${count > 1 ? ` x${count}` : ''}</span>`)
|
|
.join('');
|
|
}
|
|
|
|
function renderAddons(addons) {
|
|
const list = el('addons-list');
|
|
if (!list) return;
|
|
if (!addons.length) {
|
|
list.innerHTML = '<div class="admin-empty">No addons installed yet.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = addons.map(addon => `
|
|
<div class="addon-card" data-addon-id="${esc(addon.id)}">
|
|
<div class="addon-card-main">
|
|
<div class="addon-card-title">
|
|
<span class="addon-status ${addon.enabled ? 'enabled' : ''}"></span>
|
|
<strong>${esc(addon.name || addon.id)}</strong>
|
|
<span class="addon-version">${esc(addon.version || '0.1.0')}</span>
|
|
</div>
|
|
<div class="addon-desc">${esc(addon.description || 'No description')}</div>
|
|
<div class="addon-meta">
|
|
<span>${esc(addon.id)}</span>
|
|
${addon.author ? `<span>${esc(addon.author)}</span>` : ''}
|
|
${addon.error ? `<span class="addon-error">${esc(addon.error)}</span>` : ''}
|
|
</div>
|
|
<div class="addon-contribs">${contributionSummary(addon)}</div>
|
|
</div>
|
|
<div class="addon-card-actions">
|
|
<button class="admin-btn-sm addon-edit-btn" data-addon-id="${esc(addon.id)}">Edit</button>
|
|
<button class="admin-btn-sm addon-toggle-btn" data-addon-id="${esc(addon.id)}" data-enabled="${addon.enabled ? '0' : '1'}">${addon.enabled ? 'Disable' : 'Enable'}</button>
|
|
<button class="admin-btn-sm danger addon-delete-btn" data-addon-id="${esc(addon.id)}">Delete</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function setMessage(message, kind = '') {
|
|
const node = el('addons-msg');
|
|
if (!node) return;
|
|
node.textContent = message || '';
|
|
node.className = kind ? `addon-msg ${kind}` : 'addon-msg';
|
|
}
|
|
|
|
function setEditorManifest(manifest) {
|
|
currentManifest = manifest || null;
|
|
const editor = el('addon-manifest-editor');
|
|
if (!editor) return;
|
|
editor.value = JSON.stringify(manifest || {}, null, 2);
|
|
}
|
|
|
|
async function loadAddons() {
|
|
const list = el('addons-list');
|
|
if (list) list.innerHTML = '<div class="admin-empty">Loading addons...</div>';
|
|
try {
|
|
const data = await api('/api/addons');
|
|
renderAddons(Array.isArray(data.addons) ? data.addons : []);
|
|
setMessage('');
|
|
} catch (err) {
|
|
if (list) list.innerHTML = `<div class="admin-empty addon-error">${esc(err.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function loadTemplate() {
|
|
const idInput = el('addon-template-id');
|
|
const addonId = (idInput && idInput.value.trim()) || 'my-addon';
|
|
const data = await api(`/api/addons/template?addon_id=${encodeURIComponent(addonId)}`);
|
|
setEditorManifest(data.manifest);
|
|
setMessage('Template loaded. Edit the manifest and save it.', 'ok');
|
|
}
|
|
|
|
async function saveManifest() {
|
|
const editor = el('addon-manifest-editor');
|
|
if (!editor) return;
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(editor.value || '{}');
|
|
} catch (err) {
|
|
setMessage(`Invalid JSON: ${err.message}`, 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const data = await api('/api/addons', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ manifest }),
|
|
});
|
|
setEditorManifest(data.addon);
|
|
setMessage('Addon saved.', 'ok');
|
|
await loadAddons();
|
|
} catch (err) {
|
|
setMessage(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function editAddon(addonId) {
|
|
try {
|
|
const data = await api(`/api/addons/${encodeURIComponent(addonId)}`);
|
|
const addon = { ...(data.addon || {}) };
|
|
delete addon.path;
|
|
setEditorManifest(addon);
|
|
setMessage(`Editing ${addon.id}.`, 'ok');
|
|
} catch (err) {
|
|
setMessage(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function toggleAddon(addonId, enabled) {
|
|
try {
|
|
await api(`/api/addons/${encodeURIComponent(addonId)}/enabled`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
setMessage(enabled ? 'Addon enabled.' : 'Addon disabled.', 'ok');
|
|
await loadAddons();
|
|
} catch (err) {
|
|
setMessage(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteAddon(addonId) {
|
|
if (!confirm(`Delete addon "${addonId}"?`)) return;
|
|
try {
|
|
await api(`/api/addons/${encodeURIComponent(addonId)}`, { method: 'DELETE' });
|
|
if (currentManifest && currentManifest.id === addonId) setEditorManifest(null);
|
|
setMessage('Addon deleted.', 'ok');
|
|
await loadAddons();
|
|
} catch (err) {
|
|
setMessage(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
const refresh = el('addons-refresh-btn');
|
|
if (refresh) refresh.addEventListener('click', loadAddons);
|
|
const template = el('addon-template-btn');
|
|
if (template) template.addEventListener('click', () => loadTemplate().catch(err => setMessage(err.message, 'error')));
|
|
const save = el('addon-save-btn');
|
|
if (save) save.addEventListener('click', saveManifest);
|
|
const clear = el('addon-clear-btn');
|
|
if (clear) clear.addEventListener('click', () => {
|
|
setEditorManifest(null);
|
|
setMessage('');
|
|
});
|
|
const list = el('addons-list');
|
|
if (list) {
|
|
list.addEventListener('click', (event) => {
|
|
const edit = event.target.closest('.addon-edit-btn');
|
|
const toggle = event.target.closest('.addon-toggle-btn');
|
|
const del = event.target.closest('.addon-delete-btn');
|
|
if (edit) editAddon(edit.dataset.addonId);
|
|
if (toggle) toggleAddon(toggle.dataset.addonId, toggle.dataset.enabled === '1');
|
|
if (del) deleteAddon(del.dataset.addonId);
|
|
});
|
|
}
|
|
}
|
|
|
|
export function init() {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
bindEvents();
|
|
}
|
|
|
|
export function open() {
|
|
init();
|
|
loadAddons();
|
|
}
|
|
|
|
export default { init, open };
|