import { state as appState } from "/assets/js/state.js"; import { tr } from "/assets/js/i18n.js"; import { $, esc } from "/assets/js/shared.js"; import { ownerName, rgbName } from "./schema.js"; import { get } from "/assets/js/navigation.js"; import { tokenHeaders } from "./services.js"; import { ensureHosts, loadConfig } from "/assets/js/settings.js"; export function pluginText(zh, en) { return appState.LANG === "zh" ? zh : en; } export function renderPlugins() { return `

${pluginText("仅发现插件不会接管插件的设备能力。接入后发送监控状态;暂停或断开联动不会停止插件的本地功能。", "Discovery does not take control of plugin device capabilities. Connecting sends monitoring state; pausing or disconnecting the link does not stop the plugin’s local functions.")}

`; } export function fanMetric(plugins) { const fans = Object.entries(plugins) .filter( ([, p]) => p.online && p.capabilities?.fan && p.status?.available !== false, ) .map(([id, p]) => { const st = p.status; let value = ""; if ( p.capabilities.fan.rpm && typeof st.fan_rpm === "number" && Number.isFinite(st.fan_rpm) && st.fan_rpm >= 0 ) value = st.fan_rpm + " RPM"; else if (st.fan_name && st.fan_name !== "UNAVAILABLE") value = { OFF: pluginText("关闭", "Off"), L1: pluginText("低速", "Low"), L2: pluginText("中低速", "Medium-low"), L3: pluginText("中速", "Medium"), L4: pluginText("高速", "High"), MAX: pluginText("全速", "Full speed"), }[st.fan_name] || st.fan_name; return value ? { name: p.name || id, value } : null; }) .filter(Boolean); return fans.length ? ` · ${fans.map((f) => esc(pluginText("风扇", "Fan") + (fans.length > 1 ? " " + f.name : "") + " " + f.value)).join(" · ")}` : ""; } export function pluginSummary(plugins, host) { if (!Object.keys(plugins).length) return `
${pluginText("风扇与硬件插件", "Fan and hardware plugins")}

${pluginText("尚未接入硬件插件,风扇状态不可用。独立安装并启动插件后,可在插件管理中接入。", "No hardware plugin connected; fan status is unavailable. Install and start a plugin independently, then connect it in plugin management.")}

`; return Object.entries(plugins) .map(([id, p]) => { const st = p.status || {}, cap = p.capabilities || {}, details = []; if (p.online) { if (cap.display) details.push("OLED " + esc(ownerName(st.oled_page || ""))); if (cap.rgb) details.push("RGB " + esc(rgbName(st.rgb_mode || ""))); } return `
${esc(p.name || id)} · ${p.online ? tr("在线") : tr("离线")}${details.length ? " · " + details.join(" · ") : ""}
`; }) .join(""); } export async function loadPlugins() { const list = $("pluginList"); if (!list) return; const button = $("refreshPlugins"), feedback = $("pluginRefreshState"); if (button?.disabled) return; if (button) button.disabled = true; if (feedback) feedback.textContent = pluginText("正在刷新…", "Refreshing…"); list.textContent = pluginText( "正在发现本机插件…", "Discovering local plugins…", ); try { const data = await get( "/api/v1/plugins?host=" + encodeURIComponent(appState.currentConfigHost), ); appState.pluginRows = data.plugins; if (feedback) feedback.textContent = pluginText( "刷新完成,发现 " + appState.pluginRows.length + " 个插件 · ", "Refreshed, " + appState.pluginRows.length + " plugins found · ", ) + new Date().toLocaleTimeString(); list.innerHTML = appState.pluginRows .map( (p) => `

${esc(p.descriptor?.name || p.id)}

${esc(p.socket)} · ${p.online ? tr("在线") : tr("离线")} · ${p.linked ? (p.paused ? pluginText("联动已暂停", "Link paused") : pluginText("已接入", "Connected")) : pluginText("尚未接入", "Not connected")}
${p.error ? `

${esc(p.error)}

` : ""}${p.status?.errors && Object.keys(p.status.errors).length ? `

${esc(JSON.stringify(p.status.errors))}

` : ""}

${Object.keys( p.descriptor?.capabilities || {}, ) .map((k) => esc( { fan: pluginText("风扇", "Fan"), display: pluginText("屏幕", "Display"), rgb: pluginText("灯光", "Lighting"), }[k] || k, ), ) .join( " · ", )}

${p.linked ? `` : ``}
`, ) .join("") || `

${pluginText("未发现正在运行的硬件插件。刷新只查询插件,不会安装或启动服务。若已安装,请先在目标机器启动插件,再刷新并接入;未接入不影响监控。", "No running hardware plugins found. Refresh only queries plugins; it does not install or start services. Start the installed plugin on the target host, then refresh and connect. Monitoring works independently.")}

`; list .querySelectorAll("[data-plugin-op]") .forEach( (b) => (b.onclick = () => pluginAction(b.dataset.pluginId, b.dataset.pluginOp)), ); list .querySelectorAll("[data-plugin-edit]") .forEach((b) => (b.onclick = () => editPlugin(b.dataset.pluginEdit))); } catch (e) { list.textContent = e.message; if (feedback) feedback.textContent = pluginText("刷新失败", "Refresh failed"); } finally { if (button) button.disabled = false; } } export async function pluginPut(path, payload) { return get(path, { method: "PUT", headers: tokenHeaders(), body: JSON.stringify({ ...payload, host: appState.currentConfigHost }), }); } export async function pluginAction(id, operation) { try { await pluginPut("/api/v1/plugins", { id, operation }); await loadPlugins(); } catch (e) { alert(e.message); } } export function capabilityField(section, key, value, spec) { const unit = appState.LANG === "zh" ? { s: "秒", ms: "毫秒" }[spec.unit] || spec.unit : spec.unit, label = spec.label?.[appState.LANG] || spec.label?.en || key, id = `${section}.${key}`, vis = spec.visible ? ` data-cap-visible="${esc(JSON.stringify(spec.visible))}"` : ""; return ``; } export function pluginChoice(x) { const zh = { off: "全灭", solid: "自定义常亮", flash: "自定义闪烁(实验性)", breathe: "内置呼吸", flow: "流水灯", marquee: "跑马灯", rainbow: "彩虹灯", colorful: "炫彩灯", red: "红色", green: "绿色", blue: "蓝色", yellow: "黄色", purple: "紫色", cyan: "青色", white: "白色", }; return appState.LANG === "zh" ? zh[x] || x : x; } export function capabilityFields(section, fields, values) { const seen = new Set(); return Object.entries(fields) .map(([key, spec]) => { if (!spec.color_group) return capabilityField(section, key, values[key], spec); if (seen.has(spec.color_group)) return ""; seen.add(spec.color_group); const members = Object.entries(fields).filter( ([, v]) => v.color_group === spec.color_group, ), hex = "#" + ["r", "g", "b"] .map((c) => Number(values[members.find(([, v]) => v.channel === c)[0]]) .toString(16) .padStart(2, "0"), ) .join(""); return `
${members.map(([k, v]) => capabilityField(section, k, values[k], { ...v, visible: null })).join("")}
`; }) .join(""); } export async function editPlugin(id) { try { appState.pluginSelected = id; appState.pluginDraft = await get( "/api/v1/plugin/config?host=" + encodeURIComponent(appState.currentConfigHost) + "&id=" + encodeURIComponent(id), ); const data = appState.pluginDraft, editor = $("pluginEditor"); editor.innerHTML = `

${esc(id)}

${pluginText("配置由插件保存并立即应用,不重启监控。", "Settings are saved and applied by the plugin without restarting monitoring.")}

${(data.notes || []).map((n) => `

${esc(n[appState.LANG] || n.en || "")}

`).join("")}${Object.entries( data.schema, ) .map( ([section, fields]) => `

${{ fan: pluginText("风扇", "Fan"), oled: pluginText("屏幕", "Display"), rgb: pluginText("灯光", "Lighting") }[section] || esc(section)}

${capabilityFields(section, fields, data.config[section])}
`, ) .join( "", )}
`; editor.oninput = (e) => { if (e.target.dataset.capColor) { const group = e.target.dataset.capColor, hex = e.target.value; ["r", "g", "b"].forEach((c, i) => { editor.querySelector(`[data-cap-key="${group}_${c}"]`).value = parseInt(hex.slice(1 + 2 * i, 3 + 2 * i), 16); }); } for (const picker of editor.querySelectorAll("[data-cap-color]")) { picker.value = "#" + ["r", "g", "b"] .map((c) => Math.max( 0, Math.min( 255, Number( editor.querySelector( `[data-cap-key="${picker.dataset.capColor}_${c}"]`, ).value, ), ), ) .toString(16) .padStart(2, "0"), ) .join(""); } updatePluginVisibility(); }; updatePluginVisibility(); $("savePlugin").onclick = savePluginConfig; } catch (e) { $("pluginEditor").textContent = e.message; } } export function updatePluginVisibility() { document.querySelectorAll("[data-cap-visible]").forEach((el) => { const rules = JSON.parse(el.dataset.capVisible); el.classList.toggle( "hidden", !Object.entries(rules).every(([key, values]) => values.includes( document.querySelector(`[data-cap-key="${key}"]`)?.value, ), ), ); }); } export async function savePluginConfig() { const updates = {}; for (const el of document.querySelectorAll("[data-cap-key]")) { if (!el.reportValidity()) return; const s = el.dataset.capSection, k = el.dataset.capKey; (updates[s] ??= {})[k] = el.value; } try { await pluginPut("/api/v1/plugin/config", { id: appState.pluginSelected, updates, }); $("pluginSaveState").textContent = pluginText("保存成功", "Saved"); } catch (e) { $("pluginSaveState").textContent = e.message; } } document.addEventListener("click", async (e) => { const b = e.target.closest("[data-manage-plugin-host]"); if (!b) return; appState.currentConfigHost = b.dataset.managePluginHost; await ensureHosts(); $("configHost").value = appState.currentConfigHost; document.querySelector('[data-tab="config"]').click(); await loadConfig(); document.querySelector('[data-config-section="plugins"]')?.click(); });