// Frontend plugin registry. // Plugin modules call window.Naut.registerPlugin({...}) after they are imported. const registry = { plugins: [], views: [], sidebarSections: [], detailTabs: [], detailPanels: [], }; let sharedContext = {}; function normalizeList(value) { return Array.isArray(value) ? value : []; } function registerPlugin(plugin) { if (!plugin || !plugin.id || registry.plugins.some((p) => p.id === plugin.id)) return; registry.plugins.push(plugin); for (const view of normalizeList(plugin.views)) registry.views.push({ plugin, ...view }); for (const section of normalizeList(plugin.sidebarSections)) registry.sidebarSections.push({ plugin, ...section }); for (const tab of normalizeList(plugin.detailTabs)) registry.detailTabs.push({ plugin, ...tab }); for (const panel of normalizeList(plugin.detailPanels)) registry.detailPanels.push({ plugin, ...panel }); } window.Naut = { ...(window.Naut || {}), registerPlugin, plugins: registry, context: () => sharedContext, }; export async function loadPlugins(api, context) { sharedContext = context || {}; let manifest; try { manifest = await api.plugins(); } catch { manifest = { modules: [] }; } for (const modulePath of manifest.modules || []) { try { await import(modulePath); } catch (e) { console.error(`Failed to load plugin ${modulePath}`, e); } } return registry.plugins; } export function pluginViews() { return registry.views.filter((v) => v.id && v.label && typeof v.render === 'function'); } export function getPluginView(id) { return pluginViews().find((v) => v.id === id); } export function renderPluginView(id, host, context) { const view = getPluginView(id); if (!view) return false; view.render(host, context); return true; } export function renderPluginSidebarSections(context) { return registry.sidebarSections .filter((s) => s.id && s.title && typeof s.render === 'function') .map((s) => `
${context.f.esc(s.title)}
${s.render(context) || ''}
`) .join(''); } export function mountPluginSidebarSections(root, context) { for (const section of registry.sidebarSections) { const el = root.querySelector(`[data-plugin-sidebar="${CSS.escape(`${section.plugin.id}:${section.id}`)}"]`); if (el && typeof section.onMount === 'function') section.onMount(el, context); } } export function pluginDetailTabs() { return registry.detailTabs.filter((t) => t.id && t.label && typeof t.render === 'function'); } export async function renderPluginDetailTab(id, host, context) { const tab = pluginDetailTabs().find((t) => t.id === id); if (!tab) return false; await tab.render(host, context); return true; } export function renderPluginDetailPanels(torrent, context) { return registry.detailPanels .filter((p) => p.title && typeof p.render === 'function') .map((p) => { try { return `
${context.f.esc(p.title)}
${p.render(torrent, context) || ''}`; } catch (e) { console.error(`Failed to render plugin panel ${p.plugin.id}:${p.id || p.title}`, e); return ''; } }) .join(''); }