meowpanel: 新增 /api/audio(audiod MSG_AUDIO_QUERY→STATE 透传, 不在时 available:false)+ 前端「音频」页(5s轮询只读)与「字体」页(列表/上传进度态/导出/409冲突确认force重试); 后端 md5 015f9cf1

This commit is contained in:
MeowOS committed 2026-09-01 03:40:28 +08:00
1 parent f149e24f28
commit 7cb1bdace4
6 files changed
+253 -7

No files matched your search

+8
View File
@@ -29,6 +29,10 @@
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
<span class="nav-label">亮度·音量</span>
</button>
<button class="nav-item" data-route="audio" title="音频服务状态(meow_audiod)">
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 10v4h3l5 4V6l-5 4H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5M18 7a7 7 0 0 1 0 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
<span class="nav-label">音频</span>
</button>
<button class="nav-item" data-route="files" title="文件管理">
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
<span class="nav-label">文件</span>
@@ -37,6 +41,10 @@
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="13" y="4" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="4" y="13" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="13" y="13" width="7" height="7" rx="4" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
<span class="nav-label">应用</span>
</button>
<button class="nav-item" data-route="fonts" title="字体库管理">
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M5 18L10 5h1.4L16.5 18h-2.2l-1.2-3.2H8.3L7.1 18H5zm4-5h3.4l-1.7-4.7L9 13z" fill="currentColor"/><path d="M17 19h4v1.6h-4z" fill="currentColor"/></svg></span>
<span class="nav-label">字体</span>
</button>
<button class="nav-item" data-route="settings" title="设置镜像">
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2" fill="var(--bg)"/><circle cx="15" cy="12" r="2" fill="var(--bg)"/><circle cx="7" cy="18" r="2" fill="var(--bg)"/></svg></span>
<span class="nav-label">设置</span>
+139 -1
View File
@@ -328,6 +328,61 @@
});
}
/* ---------------- 2b. 音频页(meow_audiod 状态快照,5s 轮询,只读) ---------------- */
let audioTimer = null;
function stopAudio() { if (audioTimer) { clearInterval(audioTimer); audioTimer = null; } }
function audioPortName(p) { // audiod numid=9 路由口语义(AUDIOD_DESIGN §5)
const m = { 0: '空闲', 1: 'Line Out', 2: '3.5mm', 3: '4.4mm 平衡', 4: 'Type-C SPDIF' };
if (p == null || p < 0) return '—';
return m[p] || ('端口 ' + p);
}
function fmtFrames(n) { return (n == null) ? '—' : Number(n).toLocaleString(); }
function renderAudioData(page, d) {
let h = '';
if (!d || !d.available) {
h += '<div class="card"><h3 class="card-title">音频服务(meow_audiod)</h3>' +
'<p class="muted">audiod 不可用(服务未运行或 IPC 不可达)。MeowOS 模式下 audiod 恒在;HibyOS 模式下按门控不启动。</p></div>';
} else {
h += '<div class="card"><h3 class="card-title">当前流</h3>';
if (d.active) {
h += '<table class="table"><tbody>' +
row('应用', esc(d.app || '—') + ' <span class="muted">(sid ' + esc(d.sid) + ')</span>') +
row('采样率', esc(d.rate) + ' Hz') +
row('声道', esc(d.ch) + ' ch') +
row('格式', esc(d.fmt || '—')) +
row('已写帧', fmtFrames(d.written_frames)) +
row('已播放帧', fmtFrames(d.played_frames)) +
row('underrun', '<span class="' + (d.underrun > 0 ? 'urgency-hi' : '') + '">' + esc(d.underrun) + '</span>') +
row('状态', d.paused ? '已暂停' : '播放中') +
'</tbody></table>';
} else {
h += '<p class="muted">当前无活动音频流。</p>';
}
h += '</div>';
h += '<div class="card"><h3 class="card-title">路由与音量</h3><table class="table"><tbody>' +
row('路由模式', esc(d.route_mode || '—') + (d.route_mode === 'manual' ? '(手动覆盖)' : '(跟随插孔)')) +
row('输出口', audioPortName(d.port)) +
row('音量', (d.vol != null && d.vol >= 0 ? esc(d.vol) + ' %' : '—')) +
'</tbody></table></div>';
}
h += '<p class="muted">每 5 秒自动刷新 · 只读展示(测试音请用设备 CLI <code>meow_audio tone</code>,面板不放声)。</p>';
page.innerHTML = h;
}
async function fetchAudioOnce() {
const page = $('#audio-page');
if (!page || state.route !== 'audio') return;
let d;
try { d = await api('GET', '/api/audio'); }
catch (e) { if (page && state.route === 'audio') page.innerHTML = emptyState('加载失败:' + errText(e), e.code); return; }
if (state.route === 'audio') renderAudioData(page, d);
}
function renderAudio() {
$('#content').innerHTML = '<div class="page" id="audio-page"><p class="placeholder">加载音频状态…</p></div>';
stopAudio();
fetchAudioOnce();
audioTimer = setInterval(fetchAudioOnce, 5000);
}
/* ---------------- 3. 文件管理页 ---------------- */
// 返回带索引的 rel 显示面包屑
function joinRel(a, b) {
@@ -556,6 +611,86 @@
} catch (e) { toast('获取详情失败:' + errText(e)); }
}
/* ---------------- 4b. 字体管理页(/api/fonts* 专属页) ---------------- */
function renderFontsPage() {
$('#content').innerHTML = '<div class="page" id="fonts-page">' +
'<div class="card"><h3 class="card-title">字体库</h3>' +
'<div class="field-row" style="margin-bottom:14px">' +
'<button class="btn btn-primary" id="fpage-import">上传字体库 .tar.gz</button>' +
'<button class="btn" id="fpage-refresh">刷新</button>' +
'<input type="file" id="fpage-file" accept=".tar.gz,.tgz,application/gzip" hidden></div>' +
'<div id="fpage-list"><p class="muted">加载字体库列表…</p></div>' +
'<p class="muted" style="margin-top:8px">上传 tar.gz(内含 font.png + font.idx + font.json,平铺三件套)→ 校验后解压到 ' +
'<code>MeowOS/fonts/&lt;family&gt;/&lt;weight&gt;/</code>;同 family/weight 冲突时会要求确认覆盖。' +
'导入后进设备「设置 → 字体」页,列表自动重扫生效。</p></div></div>';
loadFontsPage();
}
async function loadFontsPage() {
const box = $('#fpage-list'); if (!box) return;
let d;
try { d = await api('GET', '/api/fonts'); }
catch (e) { box.innerHTML = '<p class="muted">字体库列表加载失败:' + esc(errText(e)) + '</p>'; return; }
const fs = (d && d.fonts) || [];
if (!fs.length) { box.innerHTML = '<div class="empty">暂无字库(TF 卡 MeowOS/fonts 下无完整三件套)</div>'; return; }
let h = '<table class="table"><thead><tr><th>family</th><th>weight</th><th>名称</th><th>tier</th><th>glyph 数</th><th style="text-align:right">操作</th></tr></thead><tbody>';
fs.forEach(f => {
h += '<tr><td class="mono">' + esc(f.family || '') + '</td><td class="mono">' + esc(f.weight || '') + '</td>' +
'<td>' + esc(f.name || '—') + '</td><td class="muted">' + esc(f.tier || '—') + '</td>' +
'<td class="muted">' + (f.glyph_count ? Number(f.glyph_count).toLocaleString() : '—') + '</td>' +
'<td><div class="row-actions"><button class="btn btn-ghost" data-fpexport="' + esc(f.family || '') +
'" data-fpweight="' + esc(f.weight || '') + '">导出</button></div></td></tr>';
});
box.innerHTML = h + '</tbody></table>';
}
async function importFontPage(files) {
if (!files || !files.length) return;
const f = files[0];
const btn = $('#fpage-import');
if (btn && btn.disabled) return; // 上传中防连点(进度态)
const fd = new FormData(); fd.append('file', f);
if (btn) { btn.disabled = true; btn.textContent = '导入中…'; }
toast('正在导入 ' + f.name + '…');
let res = null;
try { res = await api('POST', '/api/fonts/import', fd); }
catch (err) {
if (err.code === 'CONFLICT') {
const ok = await confirmDlg('覆盖字库', '目标字库(同 family/weight)已存在,覆盖导入吗?', true);
if (ok) {
try { res = await api('POST', '/api/fonts/import?force=1', fd); }
catch (e2) { toast('导入失败:' + errText(e2)); }
}
} else toast('导入失败:' + errText(err));
}
if (btn) { btn.disabled = false; btn.textContent = '上传字体库 .tar.gz'; }
if (res) {
toast('已导入 ' + (res.family || '') + '/' + (res.weight || ''));
if (res.warning) toast(res.warning);
else if (res.hint) toast(res.hint);
loadFontsPage();
}
}
function bindFonts() {
const c = $('#content');
c.addEventListener('click', function (e) {
if (e.target.closest('#fpage-import')) { const inp = $('#fpage-file'); if (inp) { inp.value = ''; inp.click(); } return; }
if (e.target.closest('#fpage-refresh')) { loadFontsPage(); return; }
const fe = e.target.closest('[data-fpexport]');
if (fe) {
if (fe.dataset.busy) return; // 打包期间防连点
fe.dataset.busy = '1';
const old = fe.textContent; fe.textContent = '打包中…';
const f = fe.dataset.fpexport, w = fe.dataset.fpweight;
doDownload('/api/fonts/export?family=' + encodeURIComponent(f) + '&weight=' + encodeURIComponent(w),
(f || 'font') + '-' + (w || 'regular') + '.font.tar.gz')
.finally(() => { delete fe.dataset.busy; fe.textContent = old; });
return;
}
});
c.addEventListener('change', function (e) {
if (e.target.id === 'fpage-file') importFontPage(e.target.files);
});
}
/* ---------------- 5. 设置镜像页 ---------------- */
function settingControl(it) {
const key = it.key;
@@ -1050,8 +1185,10 @@
const ROUTES = {
perf: { title: '实时性能', render: renderPerf, bind: null },
bright: { title: '亮度 · 音量', render: renderBright, bind: null },
audio: { title: '音频状态', render: renderAudio, bind: null },
files: { title: '文件管理', render: renderFiles, bind: bindFiles },
apps: { title: 'MeowAPPs 管理', render: renderApps, bind: bindApps },
fonts: { title: '字体管理', render: renderFontsPage, bind: bindFonts },
settings: { title: '设置镜像', render: renderSettings, bind: bindSettings },
remote: { title: '远程控制', render: renderRemote, bind: bindRemote },
screen: { title: '实时屏幕', render: renderScreen, bind: bindScreen },
@@ -1070,6 +1207,7 @@
const r = ROUTES[id] || ROUTES.perf;
state.route = id;
stopPerf();
stopAudio();
stopScreenPoll();
$$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id));
$('#page-title').textContent = r.title;
@@ -1089,7 +1227,7 @@
bindNav();
bindThemeUi();
bindSlider();
bindFiles(); bindApps(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif();
bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif();
bindLogin();
bindPwDlg();
// 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用