meowpanel: 新增 /api/audio(audiod MSG_AUDIO_QUERY→STATE 透传, 不在时 available:false)+ 前端「音频」页(5s轮询只读)与「字体」页(列表/上传进度态/导出/409冲突确认force重试); 后端 md5 015f9cf1
This commit is contained in:
1 parent
f149e24f28
commit
7cb1bdace4
6 files changed
+253
-7
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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/<family>/<weight>/</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();
|
||||
// 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用
|
||||
|
||||
Reference in new issue
Block a user