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
@@ -28,7 +28,7 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
|
||||
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
|
||||
- 产出(本次构建实测):
|
||||
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
||||
体积约 **218 KB**(固件阈值内)。md5:`51ffc183403aa20e0d19d0f811c300b7`(本次含字体库 tar.gz 导入/导出 + 排序修复)。
|
||||
体积约 **218 KB**(固件阈值内)。md5:`015f9cf1c12e36d133b01cf6b6648edd`(本次含 `/api/audio` 音频状态端点 + 前端「音频」「字体」页)。
|
||||
- `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **148 KB**。
|
||||
- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自
|
||||
`meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。
|
||||
@@ -112,6 +112,7 @@ http://<host>.local:8080/
|
||||
| GET | `/api/fonts` | 字体库列表(扫描 `MeowOS/fonts/<family>/<weight>/` 三件套齐) | ✅(无卡→503) |
|
||||
| POST | `/api/fonts/import` | 上传字体库 `.tar.gz`(multipart `file`,可带 `?force=1` 覆盖) | ✅(缺件/坏json/穿越→400,冲突→409,超限→413) |
|
||||
| GET | `/api/fonts/export` | 导出 `<family>/<weight>/` 三件套为 `.tar.gz`(`?family=&weight=`) | ✅(缺件→404) |
|
||||
| GET | `/api/audio` | 音频状态快照:meow_audiod IPC `MSG_AUDIO_QUERY→STATE` 透传(当前流/路由/音量/underrun,见 §5.12) | ✅(audiod 不在→`available:false`,不报错) |
|
||||
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
|
||||
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
|
||||
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
|
||||
@@ -306,6 +307,27 @@ http://<host>.local:8080/
|
||||
再导入→逐字节一致;GNU tar/`tarfile` 均能读;目录穿越 `../`/绝对路径/软链成员/缺件/坏 font.json/多出条目/
|
||||
子目录包裹/非 gzip——全部拒绝;打包文件名 `<family>-<weight>.font.tar.gz`。见 §7 宿主测试备注。
|
||||
|
||||
### 5.12 音频状态(`/api/audio`,新增,AUDIOD_DESIGN §8 面板对接)
|
||||
- **数据源**:`meow_audiod` 独占音频守护(板载进固件,svcd 托管;HibyOS 模式按门控不启动)。
|
||||
后端连 `/usr/data/meow_audio.sock`(可被 `--sock-audio` / env `MEOW_AUDIO_SOCK` 覆盖,宿主/qemu 测试用,
|
||||
与 audiod CLI 同 env),发 `MSG_AUDIO_QUERY`(0x00C7) 空负载,收 `MSG_AUDIO_STATE`(0x00C8) 文本帧
|
||||
(`active=.. app=.. sid=.. rate=.. ch=.. fmt=.. written=.. played=.. underrun=.. paused=.. route_mode=.. port=.. vol=..`,
|
||||
空格分隔 k=v、NUL 结尾)。**audiod 无 MSG_HELLO 握手**(见 meow_audio.c CLI),与 perf/power/launcher 路径不同。
|
||||
- **响应**:解析 k=v 为 JSON——`{available:true, active, app, sid, rate, ch, fmt, written_frames, played_frames,
|
||||
underrun, paused, route_mode("auto"/"manual"), port, vol}`;`port` 为 amixer numid=9 路由口
|
||||
(0=空闲 1=lineout 2=3.5 3=4.4 4=Type-C SPDIF;宿主无 amixer 时为 -1)。
|
||||
- **audiod 不在**(socket 缺失/连接拒/超时/非预期应答)→ HTTP 200 `{"available":false}`,**不报错**(面板音频页显示
|
||||
「audiod 不可用」)。只读端点;不放测试音(真机乱响风险,tone 留给 CLI `meow_audio tone`)。
|
||||
- **前端**:「音频」页(`web/#/audio`)5s 轮询展示——当前流(app/rate/ch/fmt/已写帧/已播放帧/underrun/暂停态)+
|
||||
路由口 + 音量 + audiod 可用性;切页即停轮询。
|
||||
|
||||
### 5.13 字体管理页(前端 `web/#/fonts`,新增)
|
||||
- 后端 `/api/fonts*` 三端点(§5.11,agent-243 落地)的专属页面:表格列出 family/weight/name/tier/glyph_count,
|
||||
行内「导出」(busy 防连点「打包中…」)+ 顶部「上传字体库 .tar.gz」(导入中禁用按钮=进度态)+「刷新」。
|
||||
- 上传遇 **409 CONFLICT**(同 family/weight 已存在)→ 弹确认框,确认后带 `?force=1` 重试;成功 toast 带
|
||||
`family/weight` 与 `warning`(sha256 不符仅提示)/`hint`。
|
||||
- 设置页原「字体库」卡保留(同一后端,互不影响)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)
|
||||
@@ -345,6 +367,10 @@ http://<host>.local:8080/
|
||||
上限 32MB,`?force=1` 覆盖)/`/api/fonts/export`(导出,GET,`<family>-<weight>.font.tar.gz`)。前两者由前端设置页「字体库」卡接入。
|
||||
还修正了 `/api/files` 排序:**目录优先 + 名称自然排序**(数字感知 track2<track10,中文按无符号字节序,见 §5.4)。
|
||||
**导入不触发 launcher 重扫**(扫描时机=启动/进设置页),响应用 `hint` 提示。
|
||||
14. **`/api/audio`(新增,§5.12)**:协议 §2 无此端点。按 AUDIOD_DESIGN §8「MSG_AUDIO_STATE 供 MeowPanel 系统/音频页展示」落地——
|
||||
连 audiod sock 发 `MSG_AUDIO_QUERY`(0x00C7)、收 `MSG_AUDIO_STATE`(0x00C8) k=v 文本解析为 JSON 透传;**audiod 无握手**、
|
||||
不在时回 `{"available":false}`(200,非错误)。前端新增「音频」页(5s 轮询只读)。同批新增前端「字体」页(§5.13,
|
||||
`/api/fonts*` 的专属页;设置页字体库卡保留)。
|
||||
|
||||
---
|
||||
|
||||
@@ -392,17 +418,17 @@ http://<host>.local:8080/
|
||||
|
||||
```
|
||||
patch_v2/meowpanel/
|
||||
├── meowpanel.c # 后端源码(约 4560 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序)
|
||||
├── meowpanel.c # 后端源码(约 4650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序/audiod 状态快照)
|
||||
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning)
|
||||
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
|
||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;meowpanel.c + mp_jpeg.c)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 51ffc183…)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 015f9cf1…)
|
||||
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
||||
├── services.d/
|
||||
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
||||
├── web/ # 前端(已存在;部署时拷到后端同目录)
|
||||
│ ├── index.html # 含登录页 overlay + 设置密码对话框
|
||||
│ ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航
|
||||
│ ├── meowpanel.css # 含登录页样式
|
||||
│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片
|
||||
│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片、音频页(5s轮询)/字体页
|
||||
└── README.md # 本文件
|
||||
```
|
||||
Binary file not shown.
@@ -92,6 +92,7 @@ static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notificat
|
||||
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
||||
#define POWER_SOCK "/usr/data/meow_power.sock"
|
||||
#define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
|
||||
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
|
||||
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
||||
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
|
||||
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
|
||||
@@ -123,6 +124,7 @@ static const char *g_sensitive_names[] = {
|
||||
static const char *g_sock_perf = PERF_SOCK;
|
||||
static const char *g_sock_power = POWER_SOCK;
|
||||
static const char *g_sock_launcher = LAUNCHER_SOCK;
|
||||
static const char *g_sock_audio = AUDIO_SOCK;
|
||||
|
||||
/* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ----------------
|
||||
* 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。
|
||||
@@ -908,6 +910,72 @@ done:
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- 音频状态:meow_audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE) ----------------
|
||||
* audiod 无 MSG_HELLO 握手(见 patch_v2/meow_audiod/meow_audio.c CLI:直连发空负载 QUERY,
|
||||
* 收 STATE 文本帧——"k=v" 空格分隔、NUL 结尾)。audiod 不在/超时/应答异常一律回
|
||||
* {"available":false}(HTTP 200,不报错)——面板音频页据此显示「audiod 不可用」。
|
||||
* socket 可被 env MEOW_AUDIO_SOCK / --sock-audio 覆盖(宿主/qemu 测试用,与 audiod CLI 同 env)。 */
|
||||
static const char *audio_kv(const char *txt, const char *key, char *out, size_t cap)
|
||||
{
|
||||
size_t kl = strlen(key);
|
||||
const char *p = txt;
|
||||
while (p && *p) {
|
||||
if (!strncmp(p, key, kl) && p[kl] == '=') {
|
||||
p += kl + 1;
|
||||
size_t n = strcspn(p, " \t\n");
|
||||
if (n >= cap) n = cap - 1;
|
||||
memcpy(out, p, n);
|
||||
out[n] = 0;
|
||||
return out;
|
||||
}
|
||||
p = strchr(p, ' ');
|
||||
if (p) p++;
|
||||
}
|
||||
out[0] = 0;
|
||||
return NULL;
|
||||
}
|
||||
static long long audio_kv_ll(const char *txt, const char *key, long long defv)
|
||||
{
|
||||
char b[64];
|
||||
if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv;
|
||||
return strtoll(b, NULL, 10);
|
||||
}
|
||||
|
||||
static int api_audio(int sock)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return send_json(sock, 200, "{\"available\":false}");
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type = 0; uint32_t rid = 0;
|
||||
int n = -1;
|
||||
if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0)
|
||||
n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
if (n <= 0 || type != MSG_AUDIO_STATE)
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
buf[n] = 0;
|
||||
const char *txt = (const char *)buf;
|
||||
char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16];
|
||||
audio_kv(txt, "app", app, sizeof app);
|
||||
audio_kv(txt, "fmt", fmt, sizeof fmt);
|
||||
audio_kv(txt, "route_mode", rmode, sizeof rmode);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
|
||||
sA_str(&s, app);
|
||||
sA(&s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
|
||||
audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0));
|
||||
sA_str(&s, fmt);
|
||||
sA(&s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
|
||||
audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0),
|
||||
audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0));
|
||||
sA_str(&s, rmode);
|
||||
sA(&s, ",\"port\":%lld,\"vol\":%lld}",
|
||||
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- 路径安全 ---------------- */
|
||||
static int tf_present(void)
|
||||
{
|
||||
@@ -4385,6 +4453,9 @@ static int handle(int sock, req_t *r)
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r);
|
||||
|
||||
/* ---- 音频状态(meow_audiod 快照,只读) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
|
||||
|
||||
/* ---- 实时画面(阶段 C) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
|
||||
return api_screen_jpeg(sock, r);
|
||||
@@ -4513,10 +4584,11 @@ int main(int argc, char **argv)
|
||||
else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; }
|
||||
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||||
printf("MeowPanel backend %s\n", PANEL_VERSION);
|
||||
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P]\n");
|
||||
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P]\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -4527,6 +4599,8 @@ int main(int argc, char **argv)
|
||||
if (env && env[0]) g_sock_power = env;
|
||||
env = getenv("MEOW_IPC_LAUNCHER_SOCK");
|
||||
if (env && env[0]) g_sock_launcher = env;
|
||||
env = getenv("MEOW_AUDIO_SOCK");
|
||||
if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */
|
||||
env = getenv("MEOW_SETTINGS_FILE");
|
||||
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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