467 lines
21 KiB
JavaScript
467 lines
21 KiB
JavaScript
/* mp_shared.js — MeowPanel 前端共享框架:全局状态 + API 客户端 + 公共工具 + 主题/弹窗/登录/滑条。
|
||
* 由 meowpanel.js 拆分而来(接口/行为不变)。作为经典脚本在 meowpanel.js 之前加载,暴露 window.MP。 */
|
||
(function () {
|
||
'use strict';
|
||
var MP = window.MP = window.MP || {};
|
||
const $ = (sel, root) => (root || document).querySelector(sel);
|
||
const $$ = (sel, root) => Array.prototype.slice.call((root || document).querySelectorAll(sel));
|
||
const sparkRms = [], sparkPeak = [];
|
||
const SPARK_MAX = 30;
|
||
const state = {
|
||
style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 <html> data-style/data-mode)
|
||
devStyle: null, devTheme: null, // 设备 settings.conf 的 ui.style / display.theme(默认来源)
|
||
conn: 'unknown',
|
||
filesRel: '/', filesAbs: null,
|
||
perfTimer: null, // 性能页轮询
|
||
route: 'perf',
|
||
unauth: false, // 鉴权未通过(弹登录页)
|
||
downloadNonce: 0
|
||
};
|
||
let toastTimer = null;
|
||
let authMode = 'login'; // 'login' | 'setup'
|
||
let volTimer = null;
|
||
const saveSetting = debounce(async function (key, value, type) {
|
||
try {
|
||
await api('PUT', '/api/settings/' + key, { value: value });
|
||
toast('已保存 ' + key + ' = ' + value);
|
||
} catch (e) { toast('保存失败:' + errText(e)); }
|
||
}, 400);
|
||
const LOG_GROUP_NAME = { all: '全部', system: '系统服务', app: 'App', audio_monitor: 'audio_monitor', power: 'power', net: 'net', crash: 'crash', ams: 'ams' };
|
||
|
||
function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||
|
||
function storageSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||
|
||
function storageDel(k) { try { localStorage.removeItem(k); } catch (e) {} }
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, m => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
}[m]));
|
||
}
|
||
|
||
function fmtBytes(n) {
|
||
if (n == null || isNaN(n)) return '—';
|
||
const u = ['B', 'KB', 'MB', 'GB', 'TB']; let v = Number(n), i = 0;
|
||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
||
return (i === 0 ? v : v.toFixed(1)) + ' ' + u[i];
|
||
}
|
||
|
||
function fmtUptime(sec) {
|
||
if (sec == null || isNaN(sec)) return '—';
|
||
sec = Math.floor(sec);
|
||
const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60);
|
||
return (d > 0 ? d + ' 天 ' : '') + h + ' 小时 ' + m + ' 分';
|
||
}
|
||
|
||
function fmtTime(ts) { return ts ? new Date(ts * 1000).toLocaleString() : '—'; }
|
||
|
||
function debounce(fn, ms) { let t = null; return function () { const a = arguments, c = this; clearTimeout(t); t = setTimeout(() => fn.apply(c, a), ms); }; }
|
||
|
||
function pct(p) { return Math.max(0, Math.min(100, p)); }
|
||
|
||
function deciToDb(v) { return (v == null || v <= -990) ? null : v / 10; }
|
||
|
||
function fmtDb(v) {
|
||
const db = deciToDb(v);
|
||
if (db == null) return '−∞';
|
||
return (db > 0 ? '+' : '') + db.toFixed(1);
|
||
}
|
||
|
||
function dbPct(v) { // -60..0 dBFS 映射到 0..100% 条宽
|
||
const db = deciToDb(v);
|
||
if (db == null) return 0;
|
||
return Math.max(0, Math.min(100, (db + 60) / 60 * 100));
|
||
}
|
||
|
||
function isAvail(b) { return !!(b && b.available !== false); }
|
||
|
||
function sparklineHtml() {
|
||
return '<svg class="lvl-spark" viewBox="0 0 100 46" preserveAspectRatio="none" aria-hidden="true">' +
|
||
'<line class="grid" x1="0" y1="46" x2="100" y2="46"/>' +
|
||
'<line class="grid" x1="0" y1="23" x2="100" y2="23"/>' +
|
||
'<polyline class="spk-rms" points="" fill="none" stroke="#00bfa5" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
|
||
'<polyline class="spk-peak" points="" fill="none" stroke="#ffb300" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
|
||
'</svg>';
|
||
}
|
||
|
||
function sparklinePoints(arr) {
|
||
const n = arr.length;
|
||
if (!n) return '';
|
||
return arr.map((v, i) => {
|
||
const x = (i / (SPARK_MAX - 1)) * 100;
|
||
return x.toFixed(1) + ',' + (46 - Math.max(0, Math.min(100, v)) / 100 * 40).toFixed(1);
|
||
}).join(' ');
|
||
}
|
||
|
||
function pushSpark(v, arr) { // deci-dB → 0..100 百分比入队(<=30 点)
|
||
arr.push(dbPct(v));
|
||
if (arr.length > SPARK_MAX) arr.shift();
|
||
}
|
||
|
||
function drawSpark(rootEl) {
|
||
const svg = rootEl && rootEl.querySelector('.lvl-spark');
|
||
if (!svg) return;
|
||
const r = svg.querySelector('.spk-rms'), p = svg.querySelector('.spk-peak');
|
||
if (r) r.setAttribute('points', sparklinePoints(sparkRms));
|
||
if (p) p.setAttribute('points', sparklinePoints(sparkPeak));
|
||
}
|
||
|
||
function resetSpark() { sparkRms.length = 0; sparkPeak.length = 0; }
|
||
|
||
function levelMeterHtml() {
|
||
return '<div class="lvl">' +
|
||
'<div class="lvl-row"><span class="lvl-lbl">RMS</span>' +
|
||
'<div class="lvl-meter"><div class="lvl-fill rms" style="width:0%"></div></div>' +
|
||
'<span class="lvl-val" data-val="rms">−∞</span></div>' +
|
||
'<div class="lvl-row"><span class="lvl-lbl">PEAK</span>' +
|
||
'<div class="lvl-meter"><div class="lvl-fill peak" style="width:0%"></div></div>' +
|
||
'<span class="lvl-val" data-val="peak">−∞</span></div>' +
|
||
'<div class="lvl-scale"><span>-60</span><span>-40</span><span>-20</span><span>0 dBFS</span></div>' +
|
||
sparklineHtml() + '</div>';
|
||
}
|
||
|
||
function updateLevelMeter(rootEl, d) {
|
||
if (!rootEl) return;
|
||
const rms = (d && d.available) ? d.rms : null;
|
||
const peak = (d && d.available) ? d.peak : null;
|
||
const fr = rootEl.querySelector('.lvl-fill.rms');
|
||
const fp = rootEl.querySelector('.lvl-fill.peak');
|
||
const vr = rootEl.querySelector('.lvl-val[data-val="rms"]');
|
||
const vp = rootEl.querySelector('.lvl-val[data-val="peak"]');
|
||
if (fr) fr.style.width = dbPct(rms) + '%';
|
||
if (fp) fp.style.width = dbPct(peak) + '%';
|
||
if (vr) vr.textContent = fmtDb(rms);
|
||
if (vp) vp.textContent = fmtDb(peak);
|
||
pushSpark(rms, sparkRms); pushSpark(peak, sparkPeak);
|
||
drawSpark(rootEl);
|
||
}
|
||
|
||
function homeKv(k, v) {
|
||
return '<div class="home-kv"><span class="k">' + esc(k) + '</span><span class="v">' + v + '</span></div>';
|
||
}
|
||
|
||
function homeUnavailable() { return '<div class="home-sub">— 数据不可用</div>'; }
|
||
|
||
function homeCard(title, body) { return '<div class="card"><h3 class="card-title">' + esc(title) + '</h3>' + body + '</div>'; }
|
||
|
||
function modePill(m) {
|
||
if (!m) return '<span class="home-chip mut">—</span>';
|
||
const cls = (m === 'meow') ? 'ok' : ((m === 'hiby') ? 'warn' : 'mut');
|
||
return '<span class="home-chip ' + cls + '">' + esc(m) + '</span>';
|
||
}
|
||
|
||
function setConn(ok) {
|
||
state.conn = ok ? 'ok' : 'bad';
|
||
const dot = $('#conn-dot');
|
||
dot.dataset.state = state.conn;
|
||
$('#conn-text').textContent = ok ? '在线' : '离线';
|
||
$('#offline-banner').hidden = !!ok;
|
||
}
|
||
|
||
function dbg() {
|
||
const a = Array.from(arguments);
|
||
console.log.apply(console, ['[mp]'].concat(a));
|
||
}
|
||
|
||
function dbgErr() {
|
||
const a = Array.from(arguments);
|
||
console.warn.apply(console, ['[mp][ERR]'].concat(a));
|
||
}
|
||
|
||
async function api(method, path, body) {
|
||
let res;
|
||
const headers = {};
|
||
const token = storageGet('meowpanel.token');
|
||
if (token) headers['X-MeowToken'] = token;
|
||
const init = { method: method || 'GET' };
|
||
if (body instanceof FormData) {
|
||
init.body = body;
|
||
} else if (body !== undefined && body !== null) {
|
||
headers['Content-Type'] = 'application/json';
|
||
init.body = JSON.stringify(body);
|
||
}
|
||
if (Object.keys(headers).length) init.headers = headers;
|
||
const t0 = performance.now();
|
||
try {
|
||
res = await fetch(path, init);
|
||
} catch (e) {
|
||
dbgErr(method || 'GET', path, '网络层失败:', e && e.message);
|
||
setConn(false);
|
||
throw { code: 'NETWORK', message: '后端未连接', status: 0 };
|
||
}
|
||
const ms = Math.round(performance.now() - t0);
|
||
/* 先读原文再解析:非 JSON 时能拿到原始内容前 300 字节进控制台 */
|
||
let text = '';
|
||
try { text = await res.text(); } catch (e) { /* 读体失败走 NOT_JSON */ }
|
||
let json = null;
|
||
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
||
if (!json || typeof json.ok !== 'boolean') {
|
||
// 静态服务器返回 HTML / 404 等
|
||
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '非 JSON 响应, len=' + text.length, '头:', text.slice(0, 200), '尾:', text.slice(-200));
|
||
try { fetch('/api/debug/report_bad_json', { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-MeowToken': storageGet('meowpanel.token') || '' }, body: text }); } catch (e2) {}
|
||
setConn(res.ok ? true : false);
|
||
throw { code: 'NOT_JSON', message: '后端返回了非 JSON 响应(HTTP ' + res.status + ')', status: res.status };
|
||
}
|
||
if (!json.ok) {
|
||
setConn(true);
|
||
const code = (json.error && json.error.code) || 'ERROR';
|
||
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '后端错误:', code, (json.error && json.error.message) || '');
|
||
if (code === 'UNAUTHORIZED') authRequired(); // 会话失效/未认证 → 弹登录页
|
||
throw { code: code, message: (json.error && json.error.message) || ('后端错误 ' + code), status: res.status };
|
||
}
|
||
if (ms > 800) dbg(method || 'GET', path, '慢响应', ms + 'ms');
|
||
setConn(true);
|
||
return json.data;
|
||
}
|
||
|
||
async function apiDownload(path) {
|
||
let res;
|
||
const headers = {};
|
||
const token = storageGet('meowpanel.token');
|
||
if (token) headers['X-MeowToken'] = token;
|
||
try { res = await fetch(path, Object.keys(headers).length ? { headers: headers } : undefined); }
|
||
catch (e) { setConn(false); throw { code: 'NETWORK', message: '后端未连接' }; }
|
||
if (!res.ok) { setConn(true); throw { code: 'HTTP_' + res.status, message: '下载失败(HTTP ' + res.status + ')' }; }
|
||
setConn(true);
|
||
return URL.createObjectURL(await res.blob());
|
||
}
|
||
|
||
function toast(msg, ms) {
|
||
const t = $('#toast');
|
||
t.textContent = msg; t.hidden = false;
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => { t.hidden = true; }, ms || 2600);
|
||
}
|
||
|
||
function confirmDlg(title, body, danger) {
|
||
return new Promise(res => {
|
||
$('#modal-title').textContent = title || '确认';
|
||
$('#modal-body').textContent = body || '';
|
||
const yes = $('#modal-yes'), no = $('#modal-no');
|
||
yes.className = 'btn btn-primary' + (danger ? ' btn-danger' : '');
|
||
const scrim = $('#modal'); scrim.hidden = false;
|
||
const done = v => { scrim.hidden = true; yes.onclick = no.onclick = scrim.onclick = null; res(v); };
|
||
yes.onclick = () => done(true);
|
||
no.onclick = () => done(false);
|
||
scrim.onclick = e => { if (e.target === scrim) done(false); };
|
||
});
|
||
}
|
||
|
||
function loginErr(msg) { const err = $('#login-err'); if (err) { err.textContent = msg; err.hidden = false; } }
|
||
|
||
function authRequired() { // 任一路由收到 401 → 弹登录页(会话失效/未认证)
|
||
state.unauth = true;
|
||
refreshAuthGate();
|
||
}
|
||
|
||
async function refreshAuthGate() { // 查状态决定「首次设置」还是「登录」
|
||
let st = null;
|
||
try { st = await api('GET', '/api/auth/status'); } catch (e) { st = null; }
|
||
if (st && st.session) { state.unauth = false; hideLogin(); return; }
|
||
if (st && !st.pin_set && !st.legacy) showLogin('setup');
|
||
else showLogin('login');
|
||
}
|
||
|
||
function showLogin(mode) {
|
||
state.unauth = true;
|
||
authMode = mode || 'login';
|
||
const lg = $('#login'); if (lg) lg.hidden = false;
|
||
const sub = $('#login-sub');
|
||
const pin2 = $('#login-pin2');
|
||
const btn = $('#login-btn');
|
||
if (authMode === 'setup') {
|
||
if (sub) sub.textContent = '首次使用:请设置一个 4 位数字 PIN 作为本面板访问口令。';
|
||
if (pin2) pin2.hidden = false;
|
||
if (btn) btn.textContent = '设 置';
|
||
} else {
|
||
if (sub) sub.textContent = '此面板已开启访问鉴权,请输入 4 位 PIN。';
|
||
if (pin2) pin2.hidden = true;
|
||
if (btn) btn.textContent = '登 录';
|
||
}
|
||
const ip = $('#login-pin'); if (ip) { ip.value = ''; ip.focus(); }
|
||
const ip2 = $('#login-pin2'); if (ip2 && authMode !== 'setup') ip2.value = '';
|
||
const err = $('#login-err'); if (err) err.hidden = true;
|
||
}
|
||
|
||
function hideLogin() { const lg = $('#login'); if (lg) lg.hidden = true; }
|
||
|
||
async function doLogin() {
|
||
const pin = ($('#login-pin').value || '').trim();
|
||
const err = $('#login-err'); if (err) err.hidden = true;
|
||
if (!pin) { loginErr('请输入 PIN'); return; }
|
||
if (authMode === 'setup') {
|
||
const pin2 = ($('#login-pin2').value || '').trim();
|
||
if (pin.length !== 4 || /[^0-9]/.test(pin)) { loginErr('PIN 必须为 4 位数字'); return; }
|
||
if (pin !== pin2) { loginErr('两次输入的 PIN 不一致'); return; }
|
||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||
try {
|
||
await api('POST', '/api/auth/setup', { pin: pin });
|
||
toast('PIN 已设置,请登录');
|
||
showLogin('login');
|
||
} catch (e2) { loginErr((e2 && e2.message) ? e2.message : '设置失败'); }
|
||
finally { if (btn) btn.disabled = false; }
|
||
return;
|
||
}
|
||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||
try {
|
||
const d = await api('POST', '/api/auth/login', { pin: pin });
|
||
storageSet('meowpanel.token', d.token);
|
||
state.unauth = false;
|
||
hideLogin();
|
||
toast('已登录');
|
||
if (started) route(); else startApp();
|
||
} catch (e) {
|
||
if (e && (e.code === 'LOCKED' || e.status === 429)) loginErr(e.message || '错误次数过多,已锁定,请稍后重试');
|
||
else loginErr((e && e.message) ? e.message : '登录失败');
|
||
} finally {
|
||
const b = $('#login-btn'); if (b) b.disabled = false;
|
||
}
|
||
}
|
||
|
||
function bindLogin() {
|
||
const ok = $('#login-btn'); if (ok) ok.addEventListener('click', doLogin);
|
||
const ip = $('#login-pin'); if (ip) ip.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
||
// PIN 输入:只允许数字,最长 4 位
|
||
[ip, $('#login-pin2')].forEach(el => { if (el) el.addEventListener('input', () => { el.value = el.value.replace(/[^0-9]/g, '').slice(0, 4); }); });
|
||
}
|
||
|
||
function passwordDlg(title, body) {
|
||
return new Promise(res => {
|
||
$('#pwdlg-title').textContent = title || '设置面板密码';
|
||
$('#pwdlg-body').textContent = body || '请输入新密码(留空则清除密码)。';
|
||
const inp = $('#pwdlg-input'); inp.value = ''; inp.focus();
|
||
const scrim = $('#pwdlg'); scrim.hidden = false;
|
||
const okb = $('#pwdlg-ok'), cancel = $('#pwdlg-cancel');
|
||
const done = v => { scrim.hidden = true; okb.onclick = cancel.onclick = scrim.onclick = inp.onkeydown = null; res(v); };
|
||
okb.onclick = () => done(inp.value);
|
||
cancel.onclick = () => done(null);
|
||
scrim.onclick = e => { if (e.target === scrim) done(null); };
|
||
inp.onkeydown = e => { if (e.key === 'Enter') done(inp.value); if (e.key === 'Escape') done(null); };
|
||
});
|
||
}
|
||
|
||
function bindPwDlg() { /* #content 点击委托在 bindSettings 里处理 pass 按钮;此处无需额外绑定 */ }
|
||
|
||
function browserMode() {
|
||
try { return (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; }
|
||
catch (e) { return 'dark'; }
|
||
}
|
||
|
||
function storedStyle() { const v = storageGet('meowpanel.style'); return (v === 'md3' || v === 'miuix') ? v : null; }
|
||
|
||
function storedMode() { const v = storageGet('meowpanel.mode'); return (v === 'system' || v === 'browser' || v === 'light' || v === 'dark') ? v : null; }
|
||
|
||
function resolveStyle() { return storedStyle() || state.devStyle || 'md3'; }
|
||
|
||
function resolveMode() {
|
||
const m = storedMode() || 'system';
|
||
if (m === 'light' || m === 'dark') return m;
|
||
if (m === 'system') return state.devTheme || browserMode(); // 设备未知时回退浏览器偏好
|
||
return browserMode();
|
||
}
|
||
|
||
function devStyleLabel() { return state.devStyle ? (state.devStyle === 'miuix' ? 'MIUIx' : 'Material You') : '未知(后端离线)'; }
|
||
|
||
function devThemeLabel() { return state.devTheme ? (state.devTheme === 'light' ? '浅色' : '深色') : '未知(后端离线)'; }
|
||
|
||
function applyTheme() {
|
||
const style = resolveStyle(), mode = resolveMode();
|
||
state.style = style; state.mode = mode;
|
||
const root = document.documentElement;
|
||
root.dataset.style = style; root.dataset.mode = mode;
|
||
$$('#style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
|
||
$$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
|
||
// 设置页「主题」卡(仅在该页渲染时存在)
|
||
$$('#set-style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
|
||
$$('#set-mode-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === (storedMode() || 'system')));
|
||
const rst = $('#set-style-reset'); if (rst) rst.disabled = !storedStyle();
|
||
}
|
||
|
||
async function initTheme() {
|
||
// 旧版键迁移:meowpanel.theme(dark/light)→ meowpanel.mode
|
||
const old = storageGet('meowpanel.theme');
|
||
if (old) {
|
||
if (!storedMode()) storageSet('meowpanel.mode', old === 'light' ? 'light' : 'dark');
|
||
storageDel('meowpanel.theme');
|
||
}
|
||
applyTheme();
|
||
// 浏览器日/夜切换:模式为 browser(或 system 且设备主题未知)时实时跟随
|
||
try {
|
||
if (window.matchMedia) {
|
||
const mq = matchMedia('(prefers-color-scheme: dark)');
|
||
const onChange = function () {
|
||
const m = storedMode() || 'system';
|
||
if (m === 'browser' || (m === 'system' && !state.devTheme)) applyTheme();
|
||
};
|
||
if (mq.addEventListener) mq.addEventListener('change', onChange);
|
||
else if (mq.addListener) mq.addListener(onChange);
|
||
}
|
||
} catch (e) {}
|
||
// 设备默认值(ui.style/display.theme)——未覆盖时的默认来源
|
||
try {
|
||
const t = await api('GET', '/api/theme');
|
||
state.devStyle = (t.style === 'md3' || t.style === 'miuix') ? t.style : null;
|
||
state.devTheme = (t.theme === 'light' || t.theme === 'dark') ? t.theme : null;
|
||
applyTheme();
|
||
if (state.route === 'settings') renderSettings(); // 刷新「主题」卡的设备当前值提示
|
||
} catch (e) { /* 后端离线,保持本地/浏览器偏好 */ }
|
||
}
|
||
|
||
function bindThemeUi() {
|
||
$('#style-seg').addEventListener('click', function (e) {
|
||
const b = e.target.closest('.seg-btn'); if (!b) return;
|
||
storageSet('meowpanel.style', b.dataset.style);
|
||
applyTheme();
|
||
sendTheme({ style: b.dataset.style });
|
||
});
|
||
$('#theme-seg').addEventListener('click', function (e) {
|
||
const b = e.target.closest('.seg-btn'); if (!b) return;
|
||
storageSet('meowpanel.mode', b.dataset.mode); // 顶栏=显式明/暗
|
||
applyTheme();
|
||
sendTheme({ theme: b.dataset.mode });
|
||
});
|
||
}
|
||
|
||
function emptyState(msg, code) {
|
||
const t = code === 'NO_CARD' ? '未识别到 TF 卡' : (code === 'NOT_IMPLEMENTED' ? '该功能后端尚未实现' : msg);
|
||
return '<div class="empty">' + esc(t) + '</div>';
|
||
}
|
||
|
||
function errText(e) {
|
||
if (e && e.code === 'NO_CARD') return '未识别到 TF 卡';
|
||
if (e && e.code === 'NOT_IMPLEMENTED') return '该功能后端尚未实现';
|
||
return (e && e.message) ? e.message : '未知错误';
|
||
}
|
||
|
||
function barHtml(p) {
|
||
p = pct(p);
|
||
const c = p >= 90 ? 'danger' : (p >= 70 ? 'warn' : '');
|
||
return '<div class="bar ' + c + '"><span style="width:' + p + '%"></span></div>';
|
||
}
|
||
|
||
function stat(label, valueHtml, sub, barPct) {
|
||
return '<div class="stat"><div class="stat-label">' + esc(label) + '</div>' + valueHtml +
|
||
(sub ? '<div class="stat-sub">' + sub + '</div>' : '') +
|
||
(barPct != null ? barHtml(barPct) : '') + '</div>';
|
||
}
|
||
|
||
function slur(step) { // step: {key,label,min,max,val}
|
||
return '<div class="card"><h3 class="card-title">' + esc(step.label) + '</h3>' +
|
||
'<div class="field-row"><span class="muted" style="width:44px">' + step.val + '</span>' +
|
||
'<input type="range" min="' + step.min + '" max="' + step.max + '" value="' + step.val + '" data-slider data-key="' + esc(step.key) + '">' +
|
||
'<span class="muted" style="width:44px;text-align:right">' + step.max + '</span></div></div>';
|
||
}
|
||
|
||
function bindSlider() {
|
||
$('#content').addEventListener('input', function (e) {
|
||
const r = e.target.closest('[data-slider]');
|
||
if (!r) return;
|
||
const row = r.closest('.field-row');
|
||
if (row) row.querySelector('.muted').textContent = r.value;
|
||
saveSetting(r.dataset.key, +r.value, 'int');
|
||
});
|
||
}
|
||
|
||
Object.assign(MP, { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, pushSpark, refreshAuthGate, resetSpark, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, updateLevelMeter, volTimer });
|
||
})(); |