webui: automation settings panel, set-location, responsive layout

- Automation tab: settings form (define_settings) beside the editor,
  stacking above it when narrow; script/settings save split.
- Set location modal: current location + reset checkbox.
- saveScriptSettings/setSavePath API wiring; plugins panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-21 23:22:00 -04:00
parent 5a5b5f4330
commit 9a1d67acd4
11 changed files with 741 additions and 13 deletions

99
public/js/plugins.js Normal file
View file

@ -0,0 +1,99 @@
// 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) => `<div class="side-group plugin-side" data-plugin-sidebar="${s.plugin.id}:${s.id}">
<div class="side-head">${context.f.esc(s.title)}</div>${s.render(context) || ''}
</div>`)
.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 `<div class="section-h">${context.f.esc(p.title)}</div>${p.render(torrent, context) || ''}`;
} catch (e) {
console.error(`Failed to render plugin panel ${p.plugin.id}:${p.id || p.title}`, e);
return '';
}
})
.join('');
}