From a2aec783a77aca29ed3c10896144f68780ad3231 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 4 Sep 2026 13:09:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20M6.38=E6=89=B9=20=E2=80=94=20P1=20M?= =?UTF-8?q?D3=E7=BB=86=E5=8C=96(SDK=E5=AF=B9=E8=AF=9D=E6=A1=8628)+?= =?UTF-8?q?=E6=89=B9=E2=91=A1=E5=BA=95=E9=83=A8=E4=B8=8A=E6=BB=91=E5=9B=9E?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2+=E6=89=B96=20ncm=E9=A1=B6=E6=A0=8F=E5=9B=9E?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=A1=B5+=E6=89=B9=E2=91=A3=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=90=8E=E5=8F=B0=E5=88=97=E8=A1=A8+launcher=20UI?= =?UTF-8?q?=E5=BE=AE=E8=B0=83(=E6=AD=8C=E8=AF=8D=E9=A1=B5=E9=BB=91?= =?UTF-8?q?=E5=8D=8A=E9=80=8F=E6=98=8E=E5=BA=95/=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=A1=B5=E5=B8=83=E5=B1=80=E4=B8=8B=E7=A7=BB)+=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=A9=BAartwork=E4=BF=9D=E7=95=99=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P1: meow_ui_dialog.c 对话框底框 radius_card→MEOW_SHAPE_EXTRA_LARGE(28) 三处(:103填充/:104描边/:361 BottomSheet); meow_ui_widgets.h 新增 MEOW_SHAPE_EXTRA_LARGE 28 宏; TextField/FAB/Slider/Progress 按设计文档保留 - 批②: np_gesture.h 新增 NP_GEST_HOME+np_home_gesture(底部热区上滑,与左缘返回对称互斥); now_playing.c 新增 np_draw_home_bar(屏底左缘pill指示条)+手势衔接; test_now_playing_widgets G5b - 批6: ncm home.c 顶栏加回播放页按钮(MEOW_ICON_QUEUE_MUSIC)→push(ACT_PLAYER), 无音频也生效 - 批④: meowpanel /api/ams/bg 只读端点+前端后台列表页+杀/清复用kill通道 - UI微调: now_playing.c 播放页布局宏下移(NP_COVER_Y 84/NP_BTN_Y 612消底部留白)+歌词页背景统一黑色半透明叠层(弃封面取色底,删 np_draw_lyric_coverbg) - 封面保留: now_playing.c/notif_center.c 空artwork保留上一张已解码封面(防暂停/开歌单致META报空artwork后封面闪没); test_np_cover C2断言更新+mirror同步 0warning;宿主测试 test_np_cover ALL PASS;launcher/ncm/panel 宿主测试无回归。二进制留批量批次未入库 --- patch_v2/meow_launcherd/notif_center.c | 1 + patch_v2/meow_launcherd/now_playing.c | 71 +++++++---- patch_v2/meow_launcherd/np_gesture.h | 21 +++ .../meow_launcherd/test_now_playing_widgets.c | 44 +++++++ patch_v2/meow_launcherd/test_np_cover.c | 9 +- patch_v2/meowpanel/src/mp.h | 3 + patch_v2/meowpanel/src/mp_ams.c | 120 ++++++++++++++++++ patch_v2/meowpanel/src/mp_server.c | 31 +++++ patch_v2/meowpanel/web/index.html | 5 + patch_v2/meowpanel/web/meowpanel.css | 15 +++ patch_v2/meowpanel/web/meowpanel.js | 11 +- patch_v2/meowpanel/web/mp_pages.js | 101 ++++++++++++++- patch_v2/ncm_app/app/ui/home.c | 12 +- patch_v2/sdk/include/meow_ui_widgets.h | 15 ++- patch_v2/sdk/meow_ui_dialog.c | 6 +- 15 files changed, 418 insertions(+), 47 deletions(-) diff --git a/patch_v2/meow_launcherd/notif_center.c b/patch_v2/meow_launcherd/notif_center.c index 9af3bf9..863293c 100644 --- a/patch_v2/meow_launcherd/notif_center.c +++ b/patch_v2/meow_launcherd/notif_center.c @@ -900,6 +900,7 @@ static void ncp_media_cover_load(const meow_media_session_t *s) { if (!s) return; if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return; + if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork 保留上一张媒体卡封面(勿清,防暂停/开歌单后封面闪没) */ if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; } ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path); if (!s->artwork[0]) return; diff --git a/patch_v2/meow_launcherd/now_playing.c b/patch_v2/meow_launcherd/now_playing.c index 3cd6a7b..6add1a8 100644 --- a/patch_v2/meow_launcherd/now_playing.c +++ b/patch_v2/meow_launcherd/now_playing.c @@ -17,20 +17,28 @@ #define NP_COVER 300 /* 封面边长(方,居中) */ #define NP_COVER_X ((fb_xres - NP_COVER) / 2) -#define NP_COVER_Y 60 -#define NP_TITLE_Y 412 -#define NP_ARTIST_Y 450 -#define NP_ALBUM_Y 480 +#define NP_COVER_Y 84 /* 用户 2026-09-04:控件组整体下移,消除底部大留白 */ +#define NP_TITLE_Y 436 +#define NP_ARTIST_Y 476 +#define NP_ALBUM_Y 508 #define NP_PROG_X 40 #define NP_PROG_W (fb_xres - 80) -#define NP_PROG_Y 518 -#define NP_TIME_Y 492 -#define NP_BTN_Y 568 /* 控制按钮中心 y */ +#define NP_PROG_Y 548 +#define NP_TIME_Y 526 +#define NP_BTN_Y 612 /* 控制按钮中心 y */ #define NP_PLAY_CX (fb_xres / 2) #define NP_PREV_CX (NP_PLAY_CX - 78) #define NP_NEXT_CX (NP_PLAY_CX + 78) #define NP_PLAY_R 30 #define NP_PREV_R 22 +/* 批② home 指示条(底部上滑回桌面):位于屏底左缘、避开左缘返回区(NP_EDGE_W=24)。 + * 与 np_gesture.h np_home_gesture 的起滑区一致(down_x>=NP_EDGE_W 才认 home),保证 + * 「指示条可触发手势」自洽。y 距屏底 16px,高 6px(镜像 SDK meow_ui_swipe_hint 提示线高一档)。 + */ +#define NP_HOME_BAR_X (NP_EDGE_W + 8) /* 左缘 32:紧贴左缘返回区右侧 */ +#define NP_HOME_BAR_W 48 +#define NP_HOME_BAR_H 6 +#define NP_HOME_BAR_Y (fb_yres - 16) static meow_img *g_np_cover = NULL; static char g_np_cover_path[200] = ""; @@ -59,6 +67,8 @@ static void np_cover_load(const meow_media_session_t *s) { if (!s) return; if (g_np_cover && strcmp(g_np_cover_path, s->artwork) == 0) return; /* 已载且未变 → 跳过 */ + if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork(如打开歌单清 g_cover_path 致 META 报空) + * 时保留上一张已解码封面,勿据此清封面 → 播放页/歌词页封面不闪没 */ if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; } snprintf(g_np_cover_path, sizeof g_np_cover_path, "%s", s->artwork); if (!s->artwork[0]) return; @@ -326,20 +336,18 @@ static void np_lyric_refresh(const meow_media_session_t *s) g_np_lyric_last_valid = valid; } -/* 歌词页封面作整页背景:等比 cover 到 max(W, H-SB_BAR_H) 方块、居中铺满内容区(裁剪溢出 - * 部分),衬在 lyric_view 半透明 scrim 之下形成压暗封面;无封面不画(回退渐变/主题面)。 - * 批6:批5 只铺渐变、从不画封面 → 歌词页无封面;此处补上封面背景,取色只做附加。 */ -static void np_draw_lyric_coverbg(int W, int H) +/* 批② home 指示条:底部上滑回桌面的可见反馈(镜向左缘返回提示条;唯位置=屏底左缘)。 + * 主题用 on_surface_variant(淡灰指示,iOS home 指示同款语义);draw 在整页/空态/歌词页 + * 均调用一次(np_draw_page 播放分支/空态分支 + np_draw_lyric_page)。 */ +static void np_draw_home_bar(void) { - if (!g_np_cover) return; - int size = W > (H - SB_BAR_H) ? W : (H - SB_BAR_H); - int x = (W - size) / 2; - int y = SB_BAR_H + (H - SB_BAR_H - size) / 2; - if (y < SB_BAR_H) y = SB_BAR_H; - meow_ui_img_cover(x, y, size, g_np_cover); + const meow_ui_theme_t *th = s_ui_theme(); + meow_gfx_fill_round(NP_HOME_BAR_X, NP_HOME_BAR_Y, NP_HOME_BAR_W, NP_HOME_BAR_H, + NP_HOME_BAR_H / 2, th->on_surface_variant); } -/* 歌词页整页绘制:铺封面取色渐变(或 theme surface)底 → lyric_view(自绘 scrim + 歌词/空态)。 */ +/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底, + * 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */ static void np_draw_lyric_page(int redraw_sb) { const meow_ui_theme_t *th = s_ui_theme(); @@ -354,15 +362,13 @@ static void np_draw_lyric_page(int redraw_sb) np_cover_load(meow_media_current()); np_cover_theme_update(); np_apply_lyric_colors(); - if (g_np_cover) - np_draw_lyric_coverbg(W, H); - else if (g_np_cc_valid) - np_fill_gradient(0, SB_BAR_H, W, H - SB_BAR_H, g_np_cc.bg_top, g_np_cc.bg_bottom); - else - fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface); + /* 用户 2026-09-04:歌词页背景统一黑色半透明叠加层(弃封面取色/封面图半透明底)—— + * 部分高亮度封面对歌词显示不友好,黑底保证对比度。歌词行颜色/布局/滚动不变。 */ + meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176); np_lyric_refresh(meow_media_current()); if (g_np_lyric) meow_widget_draw(meow_lyric_view_widget(g_np_lyric)); + np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */ if (redraw_sb) draw_statusbar(); } @@ -388,6 +394,7 @@ static void np_draw_page(int redraw_sb) ui_draw((W - ui_width(t2, 18)) / 2, 348, t2, th->on_surface_variant, 18); const char *t3 = meow_str_zh("np.edge_back", "左缘右滑返回"); ui_draw((W - ui_width(t3, 16)) / 2, 396, t3, th->on_surface_variant, 16); + np_draw_home_bar(); /* 批②:空态也示 home 指示(底部上滑可回桌面) */ if (redraw_sb) draw_statusbar(); return; } @@ -424,8 +431,9 @@ static void np_draw_page(int redraw_sb) np_draw_progress(s, pos, th); /* 控制按钮(SDK meow_iconbtn,命中/状态走 SDK) */ np_draw_ctl(s); - /* 播放页底部干净(M6.33:歌词占位已移除——已有独立歌词页,占位多余; - * 歌词页无歌词时的空态由歌词页自身承载,播放页不再显示歌词占位文案)。 */ + /* 播放页底部(M6.33:歌词占位已移除——已有独立歌词页,占位多余;批②:底部仅示 + * home 指示条,非歌词占位)。 */ + np_draw_home_bar(); /* 批②:播放页示 home 指示(底部上滑可回桌面) */ if (redraw_sb) draw_statusbar(); } @@ -553,6 +561,11 @@ void run_now_playing(struct app *apps, int napps) * 的方向感知判定(真机定位「返回滑不出」根因曾依赖此日志; * 保留,键改为 ges=np_gesture_decide 返回值)。 */ np_gest_t gnp = np_gesture_decide(g_np_page, tdx, tdy, dx, dy); + /* 批②:底部上滑(home)优先——该手势竖向主导、起点在底部起滑区, + * 与左缘返回(横向+左缘起点)/顶部下滑(顶部热区)互斥,故先判 home + * 覆盖(np_gesture_decide 对纵滑返回 NONE,不冲突)。 */ + if (np_home_gesture(tdx, tdy, dx, dy, fb_yres)) + gnp = NP_GEST_HOME; logmsg("np up: page=%d down=(%d,%d) d=(%d,%d) adx=%d ady=%d ges=%d", g_np_page, tdx, tdy, dx, dy, adx, ady, (int)gnp); switch (gnp) { @@ -576,6 +589,12 @@ void run_now_playing(struct app *apps, int napps) logmsg("np: BACK break (exit, page=%d)", g_np_page); np_exit = 1; break; + case NP_GEST_HOME: + /* 批②:底部上滑 → 回桌面(后台)。媒体会话续播/进程保活归批① + * keepalive(本页退出不改保活核心),此处仅走回桌面的既有路径。 */ + logmsg("np: HOME bottom-swipe-up -> back to desktop (bg)"); + np_exit = 1; + break; case NP_GEST_NEXT: /* 播放页 右→左滑 → 进歌词页。 */ g_np_page = NP_PAGE_LYRIC; diff --git a/patch_v2/meow_launcherd/np_gesture.h b/patch_v2/meow_launcherd/np_gesture.h index 2074d6c..79a7c78 100644 --- a/patch_v2/meow_launcherd/np_gesture.h +++ b/patch_v2/meow_launcherd/np_gesture.h @@ -25,6 +25,7 @@ typedef enum { NP_GEST_NEXT, /* 播放页 右→左滑 → 进入歌词页 */ NP_GEST_PREV, /* 歌词页 左→右滑 → 回到播放页 */ NP_GEST_EXIT, /* 播放页 任意右滑 → 返回桌面 */ + NP_GEST_HOME, /* 批②:底部上滑 → 返回桌面(后台保留) */ NP_GEST_TAP, /* 近点按(按当前页分派:播放页=按钮/进度看点) */ } np_gest_t; @@ -47,6 +48,12 @@ typedef enum { #ifndef NP_EDGE_SWIPE #define NP_EDGE_SWIPE 65 /* 左缘右滑位移阈值(镜像 meow_touch.h MEOW_EDGE_SWIPE) */ #endif +#ifndef NP_HOME_EDGE_H +#define NP_HOME_EDGE_H 28 /* 底部起滑区高(自屏底向上;镜像顶部热区 NP_TOP_HOTZONE 同为 28) */ +#endif +#ifndef NP_HOME_SWIPE +#define NP_HOME_SWIPE 65 /* 底部上滑位移阈值(镜像 NP_SWIPE_THRESH / meow_edge_back 同口径) */ +#endif /* 左缘返回(镜像 meow_touch.c meow_edge_back:down_x∈[0,NP_EDGE_W) 且 dx>=NP_EDGE_SWIPE * 且横向主导)。为宿主测试免链 meow_touch.c,此处 static inline 复刻同口径。 */ @@ -58,6 +65,20 @@ static inline int np_edge_back(int down_x, int dx, int dy) return 0; } +/* 批② 底部上滑返回桌面(home,与左缘返回 np_edge_back 对称):原点在底部起滑区 + * (down_y∈[fb_h-NP_HOME_EDGE_H, fb_h))且**不在左缘**(down_x>=NP_EDGE_W,与 np_edge_back 按 + * 原点分界互斥——左缘 24px 列留给左缘返回)+ 向上位移≥NP_HOME_SWIPE 且竖向主导(adx<-dy)。 + * fb_h=屏高(调用方喂 fb_yres,宿主单测亦可喂固定值)。其余(顶部/中部起滑、横向主导、 + * 向下滑、未达阈值)一律 0,不与顶部下滑/横向切页/点按冲突。 */ +static inline int np_home_gesture(int down_x, int down_y, int dx, int dy, int fb_h) +{ + int adx = dx < 0 ? -dx : dx; + if (down_x >= NP_EDGE_W && down_y >= fb_h - NP_HOME_EDGE_H && down_y >= 0 && + dy <= -NP_HOME_SWIPE && adx < -dy) + return 1; + return 0; +} + /* 按当前页把手势几何 → 动作。down_x/down_y=抬手原点;dx/dy=抬手位移(释放-原点,即 tlx-tdx)。 * 优先级:顶部下滑 > 横向主导切页/返回 > 点按。横向反向或无主导 → NONE(调用方忽略,不误切)。 */ static inline np_gest_t np_gesture_decide(int page, int down_x, int down_y, int dx, int dy) diff --git a/patch_v2/meow_launcherd/test_now_playing_widgets.c b/patch_v2/meow_launcherd/test_now_playing_widgets.c index 7e57eac..0ebb851 100644 --- a/patch_v2/meow_launcherd/test_now_playing_widgets.c +++ b/patch_v2/meow_launcherd/test_now_playing_widgets.c @@ -308,6 +308,50 @@ int main(void) CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 300, 5, 80) == NP_GEST_NONE, "vertical dominant -> NONE"); + /* ============ G5b 批② 底部上滑(home)手势(np_gesture.h np_home_gesture,纯逻辑) ============ */ + printf("G5b bottom-swipe-up home gesture\n"); + const int FBRES = 720; /* 屏高(宿主 480x720;真机 fb_yres=720) */ + const int BZ = FBRES - NP_HOME_EDGE_H; /* 底部起滑区上界 692 */ + /* 底部起滑 + 向上主导 ≥阈值 → HOME */ + CHECK(np_home_gesture(200, BZ + 10, 0, -120, FBRES) == 1, + "home: bottom zone + up-swipe -> HOME"); + CHECK(np_home_gesture(100, FBRES - 1, 20, -80, FBRES) == 1, + "home: very bottom + slight right-drift up -> HOME"); + /* 与左缘返回互斥:左缘起点(down_x NOT home (reserved for edge-back)"); + /* 起点不在底部起滑区(顶部/中部)不认 home */ + CHECK(np_home_gesture(200, 10, 0, -120, FBRES) == 0, + "home: top origin -> NOT home"); + CHECK(np_home_gesture(200, 300, 0, -120, FBRES) == 0, + "home: mid origin -> NOT home"); + /* 横向主导(向右滑)不认 home(该判给左缘返回/切页) */ + CHECK(np_home_gesture(200, BZ + 10, 120, 0, FBRES) == 0, + "home: right-dominant -> NOT home"); + /* 向下滑 / 未达阈值 / 竖向不主导 不认 home */ + CHECK(np_home_gesture(200, BZ + 10, 0, 120, FBRES) == 0, + "home: downward -> NOT home"); + CHECK(np_home_gesture(200, BZ + 10, 0, -NP_HOME_SWIPE + 1, FBRES) == 0, + "home: below up-threshold -> NOT home"); + CHECK(np_home_gesture(200, BZ + 10, -120, -80, FBRES) == 0, + "home: horizontal-dominant up -> NOT home"); + /* 与 now_playing.c 集成顺序一致:np_gesture_decide 对底部纵滑返回 NONE,再由 home 覆盖为 HOME */ + { + int dx = 0, dy = -120, tdx = 200, tdy = BZ + 10; + np_gest_t gnp = np_gesture_decide(NP_PAGE_PLAY, tdx, tdy, dx, dy); + if (np_home_gesture(tdx, tdy, dx, dy, FBRES)) gnp = NP_GEST_HOME; + CHECK(gnp == NP_GEST_HOME, + "integration: bottom up-swipe -> HOME (overrides decide-NONE)"); + } + /* 集成:左缘起点底部上滑不被误判为 home(保留给左缘返回) */ + { + int tdx = 8, tdy = BZ + 10, dx = 0, dy = -120; /* 左缘 + 上滑(竖向主导) */ + np_gest_t gnp = np_gesture_decide(NP_PAGE_PLAY, tdx, tdy, dx, dy); + if (np_home_gesture(tdx, tdy, dx, dy, FBRES)) gnp = NP_GEST_HOME; + CHECK(gnp != NP_GEST_HOME, + "integration: left-edge origin up-swipe -> NOT home"); + } + /* ============ G6 歌词页:空态 + 渲染(批2 控件 + 批3 数据语义) ============ */ printf("G6 lyric view: empty + render\n"); meow_ui_activity_push(NP_SENTINEL); diff --git a/patch_v2/meow_launcherd/test_np_cover.c b/patch_v2/meow_launcherd/test_np_cover.c index 5927da1..c8478bb 100644 --- a/patch_v2/meow_launcherd/test_np_cover.c +++ b/patch_v2/meow_launcherd/test_np_cover.c @@ -48,6 +48,7 @@ static void np_cover_load(const meow_media_session_t *s) { if (!s) return; if (g_np_cover && strcmp(g_np_cover_path, s->artwork) == 0) return; /* 已载且未变 → 跳过 */ + if (!s->artwork[0]) return; /* 镜像 now_playing.c 2026-09-04:空 artwork 保留上一张封面(勿清) */ if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; } snprintf(g_np_cover_path, sizeof g_np_cover_path, "%s", s->artwork); if (!s->artwork[0]) return; @@ -135,12 +136,12 @@ int main(void) np_cover_load(&s); CHECK(g_np_cover == keep, "same path unchanged -> cache hit (no reload)"); - /* ============ C2 artwork 空 → 封面清空(回退主题占位) ============ */ - printf("C2 artwork empty -> cover cleared (theme placeholder)\n"); + /* ============ C2 artwork 空 → 封面保留(用户 2026-09-04:勿因空 artwork 清封面) ============ */ + printf("C2 artwork empty -> cover kept (NOT cleared)\n"); memset(s.artwork, 0, sizeof s.artwork); np_cover_load(&s); - CHECK(g_np_cover == NULL, "artwork empty -> cover NULL (theme placeholder)"); - CHECK(g_np_cover_path[0] == 0, "empty -> cached path reset to \"\""); + CHECK(g_np_cover == keep, "artwork empty -> cover kept (non-NULL, NOT cleared)"); + CHECK(strcmp(g_np_cover_path, pa) == 0, "empty -> cached path kept (still pa)"); /* ============ C3 artwork 从空复原(同路径)→ 封面重载(M6.33 修复) ============ */ printf("C3 artwork restored (same path) -> cover reloads (M6.33)\n"); diff --git a/patch_v2/meowpanel/src/mp.h b/patch_v2/meowpanel/src/mp.h index 063bcbb..5929473 100644 --- a/patch_v2/meowpanel/src/mp.h +++ b/patch_v2/meowpanel/src/mp.h @@ -292,7 +292,10 @@ int txt_tokens(char *buf, char **toks, int maxtoks); void join_tokens(char **toks, int from, int to, char *out, size_t cap); void ams_fill(sbuf_t *s); void ams_fg_fill(sbuf_t *s); +void ams_bg_fill(sbuf_t *s); +void ams_bg_parse(char *list_text, sbuf_t *s); /* 已 strip 的 am list 文本 → bg 列表 JSON(含 /proc 采样) */ int api_ams(int sock); +int api_ams_bg(int sock); int ams_pkg_ok(const char *pkg); void ams_history_parse(char *out, sbuf_t *s, const char *pkg); void ams_history_fill(sbuf_t *s, const char *pkg); diff --git a/patch_v2/meowpanel/src/mp_ams.c b/patch_v2/meowpanel/src/mp_ams.c index 8bc0ee2..f08ff35 100644 --- a/patch_v2/meowpanel/src/mp_ams.c +++ b/patch_v2/meowpanel/src/mp_ams.c @@ -194,3 +194,123 @@ int api_ams_ctl(int sock, const char *pkg, int kill) if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达"); return send_err(sock, 500, "INTERNAL", detail); } + +/* ---- 批④ 后台活动列表:/api/ams/bg(只读) ---- + * 数据源=amsd FG/BG 注册表(am list 的 `state=bg` 行)+ 每进程 pcpu/rss(读 /proc/, + * 与 launcher bg_page 的 bgm_proc_sample 同源同口径,单线程静态差分缓存取 CPU 占用)。 + * CPU/内存诚实原则:pid 读不到 / 首采样无差分基线 → 对应字段为 null(前端显示「—」),绝不编造。 */ +#define BG_CACHE_MAX 8 +static long g_bg_pid[BG_CACHE_MAX]; /* /proc//stat utime+stime 差分缓存(单线程串行持有) */ +static unsigned long long g_bg_tick[BG_CACHE_MAX]; + +/* 采样单进程 CPU/内存:*pcpu<0=未知(无基线/读不到)、*rss_kb<0=未知。 */ +static void bg_proc_sample(int pid, double *pcpu, long *rss_kb) +{ + *pcpu = -1.0; + *rss_kb = -1; + if (pid <= 0) return; + char path[64], line[1024]; + /* 内存:VmRSS(KB) */ + snprintf(path, sizeof path, "/proc/%d/status", pid); + if (read_small_file(path, line, sizeof line) >= 0) { + char *p = strstr(line, "VmRSS:"); + if (p) { + p = strchr(p, ' '); + if (p) { while (*p == ' ' || *p == '\t') p++; if (*p) *rss_kb = atol(p); } + } + } + /* CPU:/proc//stat 的 utime(14)+stime(15) 差分 / hz(%) */ + snprintf(path, sizeof path, "/proc/%d/stat", pid); + if (read_small_file(path, line, sizeof line) >= 0) { + char *rp = strrchr(line, ')'); + if (rp) { + char *p = rp + 2; + unsigned long long ut = 0, st = 0; + for (int f = 0; f < 14 && *p; f++) { + while (*p == ' ' || *p == '\t') p++; + if (!*p) break; + char *end; + unsigned long long v = strtoull(p, &end, 10); + if (end == p) { p++; continue; } + p = end; + if (f == 11) ut = v; /* utime */ + if (f == 12) st = v; /* stime */ + } + unsigned long long cur = ut + st; + static double hz = 0.0; + if (hz <= 0) { hz = (double)sysconf(_SC_CLK_TCK); if (hz <= 0) hz = 100.0; } + int slot = -1, had = 0; + unsigned long long prev = 0; + for (int i = 0; i < BG_CACHE_MAX; i++) + if (g_bg_pid[i] == pid) { had = 1; slot = i; prev = g_bg_tick[i]; break; } + if (!had) { + for (int i = 0; i < BG_CACHE_MAX; i++) if (g_bg_pid[i] == 0) { slot = i; break; } + if (slot < 0) slot = 0; + } + if (slot >= 0) { g_bg_pid[slot] = pid; g_bg_tick[slot] = cur; } + if (had) { *pcpu = 100.0 * (double)(cur - prev) / hz; if (*pcpu < 0) *pcpu = 0; } + } + } +} + +/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行)。 */ +void ams_bg_parse(char *list_text, sbuf_t *s) +{ + char *toks[256]; + sbuf_t apps = {0}; + int first = 1, count = 0; + char *ln = list_text; + while (*ln) { + char *nl = strchr(ln, '\n'); + if (nl) *nl = 0; + if (strncmp(ln, "APP ", 4) == 0) { + int n = txt_tokens(ln, toks, 256); + if (n >= 9) { + /* 自右向左固定(与 ams_fill 同布局):state=tok[n-6], pid=n-5;pkg=tok[1],name=join(tok[2..n-6)) */ + const char *st = toks[n - 6]; + if (strcmp(st, "bg") == 0) { + char nm[128]; + join_tokens(toks, 2, n - 6, nm, sizeof nm); + int pid = atoi(toks[n - 5]); + double pcpu; long rss; + bg_proc_sample(pid, &pcpu, &rss); + if (!first) sA(&apps, ","); + first = 0; count++; + sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]); + sA(&apps, ",\"name\":"); sA_str(&apps, nm); + sA(&apps, ",\"pid\":%d", pid); + sA(&apps, ",\"state\":"); sA_str(&apps, st); + sA(&apps, ",\"cpu_pct\":"); + if (pcpu < 0) sA(&apps, "null"); else sA(&apps, "%.1f", pcpu); + sA(&apps, ",\"rss_kb\":"); + if (rss < 0) sA(&apps, "null"); else sA(&apps, "%ld", rss); + sA(&apps, "}"); + } + } + } + if (nl) { *nl = '\n'; ln = nl + 1; } else break; + } + sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]}", + count, (long)time(NULL), apps.b ? apps.b : ""); + sbuf_free(&apps); +} + +void ams_bg_fill(sbuf_t *s) +{ + char out[16384]; + char cmd[300]; + snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin()); + int code = run_cmd(cmd, out, sizeof out); + if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; } + panel_strip_ansi(out); + ams_bg_parse(out, s); +} + +int api_ams_bg(int sock) +{ + sbuf_t s = {0}; + ams_bg_fill(&s); + int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); + sbuf_free(&s); + return rc; +} diff --git a/patch_v2/meowpanel/src/mp_server.c b/patch_v2/meowpanel/src/mp_server.c index 322fe30..8bf9d61 100644 --- a/patch_v2/meowpanel/src/mp_server.c +++ b/patch_v2/meowpanel/src/mp_server.c @@ -289,6 +289,7 @@ int handle(int sock, req_t *r) if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams/bg")) return api_ams_bg(sock); /* 批④ 后台活动列表(只读) */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock); if (!strcmp(r->method, "GET")) { @@ -536,6 +537,36 @@ int panel_selftest(void) sbuf_free(&h2); } + /* 6) ams_bg_parse:喂含 bg/fg 混合 state 的 am list 文本 → 仅 bg 条目、合法 JSON, + * cpu_pct/rss_kb 为 null(读不到/首采样)或数字——绝不编造不一致字段。 */ + { + char list[] = + "# amd register+state\n" + "APP os.meow.a 名A bg 1 1 0 clean 0\n" + "APP os.meow.b 名B fg 9999992 1 0 clean 0\n" + "APP os.meow.c 名C bg 9999991 2 1 exit 1\n"; + sbuf_t b = {0}; + ams_bg_parse(list, &b); + if (!b.b || !utf8_valid_n(b.b, b.n)) { printf("SELFTEST FAIL: ams_bg_parse 产物非法字节/为空\n"); sbuf_free(&b); return 1; } + jv_t *br = jv_parse(b.b, (int)b.n); + if (!br) { printf("SELFTEST FAIL: ams_bg_parse 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; } + jv_t *bapps = jv_obj_get(br, "apps"); + if (!bapps || bapps->type != J_ARR) { printf("SELFTEST FAIL: ams_bg_parse 缺 apps 数组\n"); jv_free(br); sbuf_free(&b); return 1; } + int bcnt = 0; + for (jv_t *e = bapps->child; e; e = e->next) { + bcnt++; + jv_t *st = jv_obj_get(e, "state"); + if (!st || st->type != J_STR || strcmp(st->str, "bg") != 0) { printf("SELFTEST FAIL: ams_bg_parse 条目 state 非 bg\n"); jv_free(br); sbuf_free(&b); return 1; } + jv_t *cp = jv_obj_get(e, "cpu_pct"); + jv_t *rs = jv_obj_get(e, "rss_kb"); + if (!cp || (cp->type != J_NULL && cp->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse cpu_pct 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; } + if (!rs || (rs->type != J_NULL && rs->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse rss_kb 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; } + } + if (bcnt != 2) { printf("SELFTEST FAIL: ams_bg_parse bg 条目应 2 个,实际 %d\n", bcnt); jv_free(br); sbuf_free(&b); return 1; } + jv_free(br); + sbuf_free(&b); + } + printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n"); return 0; } diff --git a/patch_v2/meowpanel/web/index.html b/patch_v2/meowpanel/web/index.html index 77817ab..071f532 100644 --- a/patch_v2/meowpanel/web/index.html +++ b/patch_v2/meowpanel/web/index.html @@ -69,6 +69,10 @@ 进程·服务 +

首页仪表盘

+ 检测中… diff --git a/patch_v2/meowpanel/web/meowpanel.css b/patch_v2/meowpanel/web/meowpanel.css index 80c5725..e7cd3a4 100644 --- a/patch_v2/meowpanel/web/meowpanel.css +++ b/patch_v2/meowpanel/web/meowpanel.css @@ -171,6 +171,8 @@ body{ .conn[data-state="bad"] .conn-dot{ background:var(--error); } .conn[data-state="ok"]{ color:var(--success); } .conn[data-state="bad"]{ color:var(--error); } +.topbar-actions{ display:flex; align-items:center; gap:8px; } +.topbar-btn{ padding:6px 12px; font-size:12px; } /* 内容区 */ .content{ flex:1; overflow-y:auto; padding:20px; } @@ -269,6 +271,19 @@ input[type=range]{ width:100%; accent-color:var(--primary); } .app-card .app-meta{ font-size:12px; color:var(--on-surface-variant); } .app-card .app-actions{ display:flex; gap:6px; margin-top:auto; flex-wrap:wrap; } +/* 后台活动列表(批④,与启动器后台管理页观感呼应:MD3 圆角卡 + 行右杀按钮) */ +.bg-list{ display:flex; flex-direction:column; gap:8px; } +.bg-row{ + display:flex; align-items:center; gap:12px; + padding:12px 14px; background:var(--surface-variant); + border-radius:var(--radius-l); border:var(--outline-w) solid var(--outline-variant); +} +.bg-row .bg-ico{ width:40px; height:40px; border-radius:var(--radius-m); background:var(--primary); color:var(--on-primary); display:flex; align-items:center; justify-content:center; font-weight:700; font-size:16px; flex:0 0 40px; } +.bg-row .bg-info{ flex:1; min-width:0; } +.bg-row .bg-name{ font-weight:600; } +.bg-row .bg-sub{ font-size:12px; color:var(--on-surface-variant); margin-top:2px; display:flex; align-items:center; gap:6px; flex-wrap:wrap; } +.bg-row .btn{ flex:0 0 auto; } + /* 实时屏幕(屏幕页) */ .screen-frame{ display:flex; justify-content:center; max-width:480px; margin:0 auto; } .screen-frame img{ diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js index e4f4ca4..3ac71eb 100644 --- a/patch_v2/meowpanel/web/meowpanel.js +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -3,7 +3,7 @@ (function () { 'use strict'; var MP = window.MP; -const { $, $$, 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, absFor, appInfo, appStatePill, appUninstall, applyScreenDeltaDom, audioCard, audioCtlHtml, audioFlowHtml, audioPortName, audioRouteLabel, batteryCard, batteryCardBody, bindApps, bindAudio, bindFiles, bindFonts, bindHome, bindLogs, bindNet, bindNotif, bindPin, bindPower, bindProc, bindScreen, bindSettings, bindSystem, breakdownCardBody, btCard, createImgFromBlob, crumbHtml, deviceCard, doDownload, ensureScreenCanvas, escSafeAttr, estimateCardBody, eventsBar, fetchAudioOnce, fetchHomeLevel, fetchHomeSystem, fetchNetConns, fetchNetLog, fetchNetOnce, fetchOverviewOnce, fetchPerfOnce, fetchPowerOnce, fetchProcOnce, fetchScreen, fmtDur, fmtFrames, groupName, handleRemoteGesture, healthCard, importFont, importFontPage, installFromFile, installFromPath, joinRel, loadFonts, loadFontsPage, loadPinStatus, logsCrashCard, mkdirFile, navFiles, netBadge, netBannerErr, netCard, netConnsBody, netDnsBody, netIfaceCards, netPolicyCard, netStateCls, netStatusCards, newestStdout, perfCard, pinEdit, pinEditDlg, powerEventsCard, powerHistoryCard, powerPolicyCard, powerProfileLabel, powerSummaryCard, pubBytesCard, remoteBtn, remotePadHtml, remotePointerDown, remotePointerUp, removeFile, renameFile, renderApps, renderAudio, renderBright, renderFiles, renderFontsPage, renderHome, renderHomeCards, renderLogs, renderLogsFiles, renderNet, renderNotif, renderPerf, renderPerfData, renderPower, renderPowerBody, renderProc, renderProcBody, renderScreen, renderSettings, renderSystem, restartScreenPoll, row, screenTap, seedScreenCanvas, segments, sendKey, sendKeyLabel, sendKeyRepeat, sendTheme, setBrightCtl, setPanelPassword, settingControl, srcPill, stopAudio, stopHome, stopNet, stopPerf, stopPower, stopProc, stopScreenPoll, storageCard, svcStatePill, uploadFiles, viewAppStdout, viewLogsCrashTail, volSteps } = MP; +const { $, $$, 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, absFor, appInfo, appStatePill, appUninstall, applyScreenDeltaDom, audioCard, audioCtlHtml, audioFlowHtml, audioPortName, audioRouteLabel, batteryCard, batteryCardBody, bindApps, bindAudio, bindBg, bindFiles, bindFonts, bindHome, bindLogs, bindNet, bindNotif, bindPin, bindPower, bindProc, bindScreen, bindSettings, bindSystem, breakdownCardBody, btCard, createImgFromBlob, crumbHtml, deviceCard, doDownload, ensureScreenCanvas, escSafeAttr, estimateCardBody, eventsBar, fetchAudioOnce, fetchHomeLevel, fetchHomeSystem, fetchNetConns, fetchNetLog, fetchNetOnce, fetchOverviewOnce, fetchPerfOnce, fetchPowerOnce, fetchProcOnce, fetchScreen, fmtDur, fmtFrames, groupName, handleRemoteGesture, healthCard, importFont, importFontPage, installFromFile, installFromPath, joinRel, loadFonts, loadFontsPage, loadPinStatus, logsCrashCard, mkdirFile, navFiles, netBadge, netBannerErr, netCard, netConnsBody, netDnsBody, netIfaceCards, netPolicyCard, netStateCls, netStatusCards, newestStdout, perfCard, pinEdit, pinEditDlg, powerEventsCard, powerHistoryCard, powerPolicyCard, powerProfileLabel, powerSummaryCard, pubBytesCard, remoteBtn, remotePadHtml, remotePointerDown, remotePointerUp, removeFile, renameFile, renderApps, renderAudio, renderBg, renderBgBody, renderBright, renderFiles, renderFontsPage, renderHome, renderHomeCards, renderLogs, renderLogsFiles, renderNet, renderNotif, renderPerf, renderPerfData, renderPower, renderPowerBody, renderProc, renderProcBody, renderScreen, renderSettings, renderSystem, restartScreenPoll, row, screenTap, seedScreenCanvas, segments, sendKey, sendKeyLabel, sendKeyRepeat, sendTheme, setBrightCtl, setPanelPassword, settingControl, srcPill, stopAudio, stopBg, stopHome, stopNet, stopPerf, stopPower, stopProc, stopScreenPoll, storageCard, svcStatePill, uploadFiles, viewAppStdout, viewLogsCrashTail, volSteps } = MP; const ROUTES = { home: { title: '首页仪表盘', render: renderHome, bind: bindHome }, @@ -20,7 +20,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode logs: { title: '系统日志', render: renderLogs, bind: bindLogs }, notif: { title: '通知中心', render: renderNotif, bind: bindNotif }, power: { title: '电源 / 功耗', render: renderPower, bind: bindPower }, - proc: { title: '进程 / 服务', render: renderProc, bind: bindProc } + proc: { title: '进程 / 服务', render: renderProc, bind: bindProc }, + bg: { title: '后台管理', render: renderBg, bind: bindBg } }; let started = false; @@ -33,7 +34,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode bindNav(); bindThemeUi(); bindSlider(); - bindFiles(); bindApps(); bindFonts(); bindSettings(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); bindHome(); bindProc(); bindPin(); bindAudio(); + bindFiles(); bindApps(); bindFonts(); bindSettings(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet(); bindHome(); bindProc(); bindBg(); bindPin(); bindAudio(); bindLogin(); bindPwDlg(); // 首次进入:访问级 PIN 鉴权——已登录直接进;未设 PIN(且无旧密码)→ 首次设置流程;已设 → 登录 @@ -58,8 +59,12 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode stopHome(); stopPower(); stopProc(); + stopBg(); $$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id)); $('#page-title').textContent = r.title; + // 顶栏「一键清后台」仅后台管理页显示(内容由 fetchBgOnce 按数据更新) + const bc = $('#bg-clear'); + if (bc) { bc.hidden = (id !== 'bg'); if (id !== 'bg') bc.disabled = false; } r.render(); } diff --git a/patch_v2/meowpanel/web/mp_pages.js b/patch_v2/meowpanel/web/mp_pages.js index 9e5d233..d959bcc 100644 --- a/patch_v2/meowpanel/web/mp_pages.js +++ b/patch_v2/meowpanel/web/mp_pages.js @@ -885,6 +885,105 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode }); } + /* ---- 批④ 后台活动页(#/bg):列表 + 行内「杀死」 + 顶栏「一键清后台」(复用 /api/ams/bg 只读 + /api/ams/:pkg/kill) ---- */ + let bgTimer = null; + let bgData = null; // {available,count,apps:[{pkg,name,pid,state,cpu_pct,rss_kb}]} + + function stopBg() { if (bgTimer) { clearInterval(bgTimer); bgTimer = null; } } + + function bgIconHtml(a) { + const ch = esc((a.name || a.pkg || '?').charAt(0).toUpperCase()); + return '' + ch + ''; + } + + function renderBgBody(d) { + let h = '
'; + h += '

后台活动 App(meow_amsd bg · 只读)

'; + if (!d || d.available === false) { + h += homeUnavailable(); + } else { + const apps = d.apps || []; + if (!apps.length) { + h += '
无后台 App
'; + } else { + h += '
'; + apps.forEach(a => { + const cpu = (a.cpu_pct == null) ? '—' : a.cpu_pct.toFixed(1) + '%'; + const mem = (a.rss_kb == null) ? '—' : fmtBytes(Number(a.rss_kb) * 1024); + const sub = 'CPU ' + cpu + ' · ' + mem + ' · 后台' + + (a.pid > 0 ? ' · pid ' + esc(a.pid) : ''); + h += '
' + bgIconHtml(a) + + '
' + esc(a.name || a.pkg || '未命名') + '
' + + '
' + sub + '' + esc(a.pkg) + '
' + + '
'; + }); + h += '
'; + } + h += '

占用数据读自 /proc/<pid>(与启动器后台管理页同源、同口径);「杀死」经 amsd(require_auth)。CPU 首采样为「—」,轮询后显示真实占用;数据不可得时如实显示「—」,绝不编造。

'; + } + h += '
'; + return h; + } + + async function fetchBgOnce() { + const el = $('#bg-page'); + if (!el || state.route !== 'bg') return; + let d = null; + try { d = await api('GET', '/api/ams/bg'); } + catch (e) { + if (state.route === 'bg') el.innerHTML = '
' + emptyState('加载失败:' + errText(e), e.code) + '
'; + const bc = $('#bg-clear'); if (bc) bc.disabled = true; + return; + } + if (state.route !== 'bg') return; + bgData = d; + el.innerHTML = renderBgBody(d); + const bc = $('#bg-clear'); + if (bc) { + const n = (d && d.apps && d.apps.length) ? d.apps.length : 0; + bc.disabled = !n; + bc.textContent = '一键清后台' + (n ? ' (' + n + ')' : ''); + } + } + + function renderBg() { + stopBg(); + $('#content').innerHTML = '

加载后台 App…

'; + fetchBgOnce(); + bgTimer = setInterval(fetchBgOnce, 4000); + } + + async function bgKill(pkg) { + const ok = await confirmDlg('结束 App', '确认结束后台 App ' + pkg + '?(SIGTERM 语义)', true); + if (!ok) return; + try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/kill'); toast('已请求结束 ' + pkg); } + catch (e) { toast('结束失败:' + errText(e)); } + fetchBgOnce(); + } + + async function bgClearAll() { + const list = (bgData && bgData.apps) ? bgData.apps : []; + if (!list.length) { toast('无后台 App 可清'); return; } + const ok = await confirmDlg('一键清除后台', '将结束 ' + list.length + ' 个后台 App,确认?(SIGTERM 语义)', true); + if (!ok) return; + let done = 0, fail = 0; + for (const a of list) { + try { await api('POST', '/api/ams/' + encodeURIComponent(a.pkg) + '/kill'); done++; } + catch (e) { fail++; } + } + toast('已清 ' + done + ' 个后台 App' + (fail ? ',' + fail + ' 个失败' : '')); + fetchBgOnce(); + } + + function bindBg() { + $('#content').addEventListener('click', async function (e) { + const kl = e.target.closest('[data-bgkill]'); + if (kl) { await bgKill(kl.dataset.bgkill); return; } + }); + const bc = $('#bg-clear'); + if (bc) bc.addEventListener('click', bgClearAll); + } + function stopNet() { if (netTimer) { clearInterval(netTimer); netTimer = null; } } function fmtDur(sec) { // 连接寿命(秒级精度) @@ -2376,5 +2475,5 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode }); } - Object.assign(MP, { absFor, appInfo, appStatePill, appUninstall, applyScreenDeltaDom, audioCard, audioCtlHtml, audioFlowHtml, audioPortName, audioRouteLabel, batteryCard, batteryCardBody, bindApps, bindAudio, bindFiles, bindFonts, bindHome, bindLogs, bindNet, bindNotif, bindPin, bindPower, bindProc, bindScreen, bindSettings, bindSystem, breakdownCardBody, btCard, createImgFromBlob, crumbHtml, deviceCard, doDownload, ensureScreenCanvas, escSafeAttr, estimateCardBody, eventsBar, fetchAudioOnce, fetchHomeLevel, fetchHomeSystem, fetchNetConns, fetchNetLog, fetchNetOnce, fetchOverviewOnce, fetchPerfOnce, fetchPowerOnce, fetchProcOnce, fetchScreen, fmtDur, fmtFrames, groupName, handleRemoteGesture, healthCard, importFont, importFontPage, installFromFile, installFromPath, joinRel, loadFonts, loadFontsPage, loadPinStatus, logsCrashCard, mkdirFile, navFiles, netBadge, netBannerErr, netCard, netConnsBody, netDnsBody, netIfaceCards, netPolicyCard, netStateCls, netStatusCards, newestStdout, perfCard, pinEdit, pinEditDlg, powerEventsCard, powerHistoryCard, powerPolicyCard, powerProfileLabel, powerSummaryCard, pubBytesCard, remoteBtn, remotePadHtml, remotePointerDown, remotePointerUp, removeFile, renameFile, renderApps, renderAudio, renderBright, renderFiles, renderFontsPage, renderHome, renderHomeCards, renderLogs, renderLogsFiles, renderNet, renderNotif, renderPerf, renderPerfData, renderPower, renderPowerBody, renderProc, renderProcBody, renderScreen, renderSettings, renderSystem, restartScreenPoll, row, screenTap, seedScreenCanvas, segments, sendKey, sendKeyLabel, sendKeyRepeat, sendTheme, setBrightCtl, setPanelPassword, settingControl, srcPill, stopAudio, stopHome, stopNet, stopPerf, stopPower, stopProc, stopScreenPoll, storageCard, svcStatePill, uploadFiles, viewAppStdout, viewLogsCrashTail, volSteps }); + Object.assign(MP, { absFor, appInfo, appStatePill, appUninstall, applyScreenDeltaDom, audioCard, audioCtlHtml, audioFlowHtml, audioPortName, audioRouteLabel, batteryCard, batteryCardBody, bgClearAll, bgIconHtml, bindBg, bindApps, bindAudio, bindFiles, bindFonts, bindHome, bindLogs, bindNet, bindNotif, bindPin, bindPower, bindProc, bindScreen, bindSettings, bindSystem, breakdownCardBody, btCard, createImgFromBlob, crumbHtml, deviceCard, doDownload, ensureScreenCanvas, escSafeAttr, estimateCardBody, eventsBar, fetchAudioOnce, fetchBgOnce, fetchHomeLevel, fetchHomeSystem, fetchNetConns, fetchNetLog, fetchNetOnce, fetchOverviewOnce, fetchPerfOnce, fetchPowerOnce, fetchProcOnce, fetchScreen, fmtDur, fmtFrames, groupName, handleRemoteGesture, healthCard, importFont, importFontPage, installFromFile, installFromPath, joinRel, loadFonts, loadFontsPage, loadPinStatus, logsCrashCard, mkdirFile, navFiles, netBadge, netBannerErr, netCard, netConnsBody, netDnsBody, netIfaceCards, netPolicyCard, netStateCls, netStatusCards, newestStdout, perfCard, pinEdit, pinEditDlg, powerEventsCard, powerHistoryCard, powerPolicyCard, powerProfileLabel, powerSummaryCard, pubBytesCard, remoteBtn, remotePadHtml, remotePointerDown, remotePointerUp, removeFile, renameFile, renderApps, renderAudio, renderBg, renderBgBody, renderBright, renderFiles, renderFontsPage, renderHome, renderHomeCards, renderLogs, renderLogsFiles, renderNet, renderNotif, renderPerf, renderPerfData, renderPower, renderPowerBody, renderProc, renderProcBody, renderScreen, renderSettings, renderSystem, restartScreenPoll, row, screenTap, seedScreenCanvas, segments, sendKey, sendKeyLabel, sendKeyRepeat, sendTheme, setBrightCtl, setPanelPassword, settingControl, srcPill, stopAudio, stopBg, stopHome, stopNet, stopPerf, stopPower, stopProc, stopScreenPoll, storageCard, svcStatePill, uploadFiles, viewAppStdout, viewLogsCrashTail, volSteps }); })(); \ No newline at end of file diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index 945418a..1fca710 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -1,8 +1,9 @@ /* ui/home.c — 我的音乐页(ACT_HOME,根页,已登录)。 * - * 网易云风格(NCM_UI_DESIGN §4.2):红系 topbar(标题「我的音乐」+搜索/刷新/设置动作)+ + * 网易云风格(NCM_UI_DESIGN §4.2):红系 topbar(标题「我的音乐」+搜索/刷新/设置/回播放页动作)+ * 个人区卡片(头像/昵称/ID)+ 双页签「我创建的/我收藏的」(meow_tabbar + 各自 meow_listview)+ * 组头红条 + 歌单行(rounded card + 红 mini-vinyl 缩略 + 红色按压竖条)。 + * 批6:顶栏「回播放页」按钮(sb_icons 位图,动作3 最右)——无活动音频也生效,按下 push(ACT_PLAYER)。 * * 品牌红走 ncm_palette(g_dark)(App 无权改 SDK MD3 蓝 primary),代码零硬编码散落: * 中性色读 meow_theme_get(),红读 ncm_palette()。SDK listview 的 MD3 蓝选中不可改, @@ -297,7 +298,8 @@ int home_on_tap(int x, int y) return 1; } if (ah == 1) { home_drag_reset(); settings_open(); return 1; } /* 设置 */ - if (ah >= 2) { home_drag_reset(); search_open(); return 1; } /* 搜索(最右) */ + if (ah == 2) { home_drag_reset(); search_open(); return 1; } /* 搜索 */ + if (ah == 3) { home_drag_reset(); meow_view_page_push(ACT_PLAYER); return 1; } /* 回播放页 */ } if (g_tabs && meow_widget_hit(g_tabs, x, y)) { int t = meow_tabbar_hit(g_tabs, x, y); @@ -373,12 +375,16 @@ static void home_page_create(meow_view *v, void *ctx) meow_ui_activity_push(ACT_HOME); /* owner=ACT_HOME */ g_tb = ncm_topbar_create("我的音乐", 0, "\xe2\x86\xbb"); /* 刷新(动作0) */ meow_topappbar_add_action(g_tb, "\xe2\x9a\x99"); /* 设置(动作1) */ - meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2, 最右) */ + meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2) */ + /* 批6:回播放页(动作3, 最右)。无活动音频也生效——按下即 push(ACT_PLAYER),播放页按 appstate + * 现有语义展示空态(暂无播放)/上次会话状态。 */ + meow_topappbar_add_action(g_tb, "\xe2\x99\xaa"); /* 回播放页(动作3, 最右) */ /* §307 topbar 图标迁移:三位码点(↻ ⚙ ⌕)在 4k 字库缺字形渲染成豆腐(WORK_STATE 310 前置 * 症状①),改为 sb_icons 位图(Material Symbols Rounded,字符标签仅作占位,绘制走位图)。 */ meow_topappbar_set_item_icon(g_tb, 0, MEOW_ICON_REFRESH); meow_topappbar_set_item_icon(g_tb, 1, MEOW_ICON_SETTINGS); meow_topappbar_set_item_icon(g_tb, 2, MEOW_ICON_SEARCH); + meow_topappbar_set_item_icon(g_tb, 3, MEOW_ICON_QUEUE_MUSIC); /* 回播放页 */ g_tabs = meow_tabbar_create(r_tabs.x, r_tabs.y, r_tabs.w, r_tabs.h); meow_tabbar_add_tab(g_tabs, tab_label(0)); meow_tabbar_add_tab(g_tabs, tab_label(1)); diff --git a/patch_v2/sdk/include/meow_ui_widgets.h b/patch_v2/sdk/include/meow_ui_widgets.h index fbbe544..0f654eb 100644 --- a/patch_v2/sdk/include/meow_ui_widgets.h +++ b/patch_v2/sdk/include/meow_ui_widgets.h @@ -147,13 +147,14 @@ meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *lab int meow_btn_set_variant(meow_widget *w, int variant); /* 越界 clamp;0=成功 */ /* ---- MD3 shape 层级(§5.8.6,Material You 圆角体系,单位 px)---- - * 组件按角色落到四档 + 胶囊:按钮=full(全高一半)、卡=large(16)、输入框=small(8)、 - * 勾选=extrasmall(4)。theme.radius/radius_card 为风格族默认;App 可经 opts.radius 逐控件覆盖。 - * 全高一半用 MEOW_SHAPE_FULL(h) 宏(控件的胶囊圆角)。 */ -#define MEOW_SHAPE_EXTRASMALL 4 /* 勾选 Checkbox/Radio */ -#define MEOW_SHAPE_SMALL 8 /* 输入框 TextField */ -#define MEOW_SHAPE_MEDIUM 12 /* 主题默认 radius(列表卡/通用控件) */ -#define MEOW_SHAPE_LARGE 16 /* 卡片/容器(theme.radius_card 默认) */ + * 组件按角色落到五档 + 胶囊:按钮=full(全高一半)、对话框=extra-large(28)、卡=large(16)、 + * 输入框=small(8)、勾选=extrasmall(4)。theme.radius/radius_card 为风格族默认;App 可经 + * opts.radius 逐控件覆盖。全高一半用 MEOW_SHAPE_FULL(h) 宏(控件的胶囊圆角)。 */ +#define MEOW_SHAPE_EXTRASMALL 4 /* 勾选 Checkbox/Radio */ +#define MEOW_SHAPE_SMALL 8 /* 输入框 TextField */ +#define MEOW_SHAPE_MEDIUM 12 /* 主题默认 radius(列表卡/通用控件) */ +#define MEOW_SHAPE_LARGE 16 /* 卡片/容器(theme.radius_card 默认) */ +#define MEOW_SHAPE_EXTRA_LARGE 28 /* 对话框 Dialog(MD3 标准 extra-large) */ #define MEOW_SHAPE_FULL(h) ((h) / 2) /* 全形=胶囊(按钮/导航等) */ /* ---- TextField(单行文本输入,接 IME §5.1)---- */ diff --git a/patch_v2/sdk/meow_ui_dialog.c b/patch_v2/sdk/meow_ui_dialog.c index ea70595..44dc4ec 100644 --- a/patch_v2/sdk/meow_ui_dialog.c +++ b/patch_v2/sdk/meow_ui_dialog.c @@ -100,8 +100,8 @@ static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g) static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g) { int pad = 24; - meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->surfaceContainerHighest); - meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->outline); + meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest); + meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->outline); meow_ui_text(g->dx + pad, g->dy + pad, d->title, th->font_title, th->onSurface); if (d->type == MEOW_DLG_SELECT) { @@ -358,7 +358,7 @@ int meow_sheet_open(meow_sheet *s) meow_ui_modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */ /* 首次绘制:遮罩 + 底部面板(顶部圆角) */ meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); - meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest); + meow_gfx_fill_round(0, py, W, ph, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest); meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface); for (int i = 0; i < s->nitems; i++) { int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW;