Files
AskaEth a2aec783a7 feat(ui): M6.38批 — P1 MD3细化(SDK对话框28)+批②底部上滑回桌面+批6 ncm顶栏回播放页+批④面板后台列表+launcher UI微调(歌词页黑半透明底/播放页布局下移)+封面空artwork保留修复
- 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 宿主测试无回归。二进制留批量批次未入库
2026-09-04 13:09:03 +08:00

725 lines
41 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* now_playing.c — 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回)。
*
* 定案(用户):Now Playing = 通知中心媒体卡片 + 主界面横划进入的播放页,两者都做。
* 本文件实现后者:封面大图 + 元数据 + 进度 + 控制按钮 + 歌词占位;返回手势回主界面。
* 数据源:meow_media 会话(最近活跃媒体源);空态=占位。参照 SDK §5.2 横向分页模板的
* 手势分流(左缘返回 / 顶部下滑通知中心 / 中部横滑内容)——本页只接管「中部/右缘向左滑进、
* 左缘右滑返回」,与全局手势不冲突。
*
* 性能:封面解码缓存(按 artwork 路径变化重载,不每帧解码);整页 1s 刷新进度(墙钟推进),
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
*/
#include "meow_launcher_internal.h"
#include "np_gesture.h" /* 批4:双页方向感知手势决策(header-only,宿主可单测) */
#include "meow_lyric_view.h" /* 批2控件:歌词页渲染(批4接入,数据批3 meow_media_lyric_*) */
#include "meow_img_color.h" /* 批5:封面双色取色原语(独立源码,不并入 meow_ui.a) */
#define NP_COVER 300 /* 封面边长(方,居中) */
#define NP_COVER_X ((fb_xres - NP_COVER) / 2)
#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 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] = "";
/* ---- 批4:Now Playing 双页状态 + 歌词页控件 ----
* 同一容器两页:NP_PAGE_PLAY(播放页,原有内容)+ NP_PAGE_LYRIC(歌词页,批2 meow_lyric_view)。
* 页面切换靠方向感知手势(np_gesture.h);歌词数据批3(meow_media_lyric_*),主题先走 meow_theme
* 默认;meow_lyric_view_set_colors 为批5 封面取色动态主题预留(本批不取色,见 np_lyric_refresh)。 */
static int g_np_page = NP_PAGE_PLAY;
static meow_lyric_view *g_np_lyric = NULL; /* 歌词视图(lazy 建一次,跨进入复用) */
static uint32_t g_np_lyric_seq = 0xFFFFFFFFu; /* 上次解析的歌曲代际(新 seq 才重解析) */
static char g_np_lyric_app[MEOW_APP_ID_MAX] = ""; /* 上次解析的源 app_id */
static int g_np_lyric_last_valid = -1; /* -1=未初始化 0=无词 1=有词(防重复 clear) */
/* 批4:歌词页宽度测量(launcher ui_width 包装成 meow_lyric_width_fn;meow_lyric_view 需要)。 */
static int np_lyric_width(const char *u, int size, void *ctx)
{
(void)ctx;
return ui_width(u, size);
}
/* 封面解码缓存(artwork 路径变化才重载;路径未变但上次加载失败→允许重试)。
* M6.33 修「artwork 空→复原」封面不重载:原「路径未变即跳过」在「空 artwork 清空封面后再
* 以同路径复原」或「首载失败同路径重报」时封面永不恢复(g_np_cover==NULL 时即使路径未变也重载)。 */
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;
char path[240]; /* 相对 artwork 加前缀最坏 191+19=210 < 240,防 -Wformat-truncation 实际截断 */
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
g_np_cover = meow_img_load_file(path);
}
/* ==================================================================
* 批5:封面双色取色(主色+次色)→ 播放页/歌词页动态主题。
* - 何时取:进入 Now Playing 页 / 封面路径变化时(np_cover_theme_update 幂等,按路径缓存)。
* - 取色成本:meow_img_duocolor 内部自适副采样(等效缩略图,≤~3.6k 样本),不额外解码/分配。
* - 缓存:按 artwork 路径;路径未变不重取(g_np_cc_valid + g_np_cc_path)。
* - 回退:无封面/取色失败(g_np_cc_valid=0)→ 歌词页 set_colors(NULL) 用 meow_theme 默认;
* 播放页背景/文字走 s_ui_theme() 主题面。
* ================================================================== */
static meow_img_color_theme g_np_cc;
static int g_np_cc_valid = 0;
static char g_np_cc_path[200] = "";
/* 校验/刷新取色缓存(封面路径变化才重取;幂等)。 */
static void np_cover_theme_update(void)
{
const meow_media_session_t *s = meow_media_current();
const char *art = s ? s->artwork : "";
if (g_np_cc_valid && strcmp(g_np_cc_path, art) == 0) return; /* 缓存命中 */
snprintf(g_np_cc_path, sizeof g_np_cc_path, "%s", art);
g_np_cc_valid = 0;
if (!g_np_cover || !art[0]) return; /* 无封面 → 回退主题 */
uint32_t mainc = 0, secondc = 0;
if (meow_img_duocolor(g_np_cover, &mainc, &secondc) != 0) return; /* 取色失败 → 回退主题 */
meow_img_color_derive(mainc, secondc, meow_theme_get(), &g_np_cc);
g_np_cc_valid = 1;
}
/* 把封面取色注入歌词页(无取色 → NULL 回退 meow_theme 默认)。 */
static void np_apply_lyric_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_set_colors(g_np_lyric, g_np_cc_valid ? &g_np_cc.lyr : NULL);
}
/* 渐变插值(无 GPU,逐行整数;封面主题背景用)。返回 [y0, y0+h) 带内屏幕行 y 的颜色。 */
static uint32_t np_grad_row(uint32_t top, uint32_t bottom, int y0, int h, int y)
{
int t = (h <= 1) ? 0 : (y - y0) * 255 / (h - 1);
if (t < 0) t = 0; else if (t > 255) t = 255;
int tr = (top >> 16) & 255, tg = (top >> 8) & 255, tb = top & 255;
int br = (bottom >> 16) & 255, bg = (bottom >> 8) & 255, bb = bottom & 255;
int r = tr + ((br - tr) * t + 128) / 255;
int g = tg + ((bg - tg) * t + 128) / 255;
int b = tb + ((bb - tb) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 竖向双色渐变填充(封面主题背景用;整页/局部共用 np_grad_row,保证局部重绘与整页渐变无缝)。 */
static void np_fill_gradient(int x, int y, int w, int h, uint32_t top, uint32_t bottom)
{
if (w <= 0 || h <= 0) return;
for (int yy = 0; yy < h; yy++)
fill_rect(x, y + yy, w, 1, np_grad_row(top, bottom, y, h, y + yy));
}
/* 播放页当前主题 · 页背景行色:内容区(状态栏下)屏幕行 y 的背景色。
* 取色有效 → 取色渐变(按整页 span 插值:局部重绘进度带逐行取色与整页渐变无缝衔接);
* 取色无效 → 返回 0(调用方回退 meow_theme 面)。 */
static uint32_t np_page_bg_row(int y)
{
if (!g_np_cc_valid) return 0;
return np_grad_row(g_np_cc.bg_top, g_np_cc.bg_bottom, SB_BAR_H, fb_yres - SB_BAR_H, y);
}
#if 0 /* ================================================================
* M7.0b 退役:Now Playing 手绘控制钮/进度条改走 SDK 控件(meow_iconbtn +
* meow_progress),命中/状态/进度全由 SDK 接管;保留作回退位(删除 #if 0 行恢复旧手绘)。
* ================================================================ */
/* 实心三角(dir=+1 右向 / -1 左向)。 */
static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy);
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 圆形控制按钮(kind:0=prev 1=play 2=next 3=pause)。 */
static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) {
np_tri(cx + 5, cy, r - 5, -1, fg);
fill_rect(cx - 4, cy - (r - 7), 4, (r - 7) * 2, fg);
} else if (kind == 1) {
np_tri(cx + 3, cy, r - 3, 1, fg);
} else if (kind == 2) {
np_tri(cx - 5, cy, r - 5, 1, fg);
fill_rect(cx + 2, cy - (r - 7), 4, (r - 7) * 2, fg);
} else {
int bw = (r - 8) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 8, cy - (r - 7), bw, (r - 7) * 2, fg);
fill_rect(cx + 8 - bw, cy - (r - 7), bw, (r - 7) * 2, fg);
}
}
#endif /* M7.0b 退役:Now Playing 旧手绘控制钮 */
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。(M7.0b 保留:封面/歌名/艺术家仍 SD) */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* ==================================================================
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
* - 播放控制三钮 → meow_iconbtn(PREV / PLAY_ARROW|PAUSE / NEXT;按下=PAUSE、暂停=PLAY_ARROW,
* 同位置叠两钮、按当前状态只画其一);
* - 命中走 meow_widget_hit(圆形,owner==哨兵 + enabled 闸门)→ meow_media_playctl 分发;
* - 封面(meow_img contain)与歌名/艺术家(meow_font)不动(本就 SD),仅文案走 meow_str。
* 控件 lazy 创建一次(owner=NP_SENTINEL_ACT),跨多次进入复用;run_now_playing() 进入 push
* 哨兵、退出 pop(owner 闸门使控件命中生效)。
* ================================================================== */
#define NP_SENTINEL_ACT 0x51 /* Now Playing 哨兵 activity */
static meow_widget *g_np_prog = NULL;
static meow_widget *g_np_prev = NULL, *g_np_play = NULL, *g_np_pause = NULL, *g_np_next = NULL;
static int g_np_init = 0;
static void np_widgets_create(void)
{
meow_ui_activity_push(NP_SENTINEL_ACT);
g_np_prog = meow_progress_create(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, 0);
g_np_prev = meow_iconbtn_create(NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_np_play = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PLAY_ARROW);
g_np_pause = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PAUSE);
g_np_next = meow_iconbtn_create(NP_NEXT_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感:底色=primary、图标=onPrimary;其余两钮默认 surfaceContainerLow。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
/* 批4:歌词页视图(覆盖状态栏下整页;宽测量走 launcher ui_width 包装)。
* set_colors 留作批5 封面取色覆盖入口;本批用 meow_theme 默认(不调用)。 */
g_np_lyric = meow_lyric_view_create(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H,
np_lyric_width, NULL);
if (g_np_lyric)
meow_lyric_view_set_empty(g_np_lyric, meow_str_zh("np.no_lyric", "暂无歌词"));
meow_ui_activity_pop();
g_np_init = 1;
}
/* 播放页当前主题 → 控件修饰(整页与局部重绘共用同一次派生,避免两处取色逻辑漂移)。
* 进度条 meow_progress:opts.bg=轨道、opts.fg=填充(批5b W_PROGRESS 新增支持);
* 控制钮 meow_iconbtn:opts.bg=圆底、opts.fg=图标墨色。
* 取色有效(g_np_cc_valid):进度条轨道=fg_dim/填充=accent,三钮圆底=accent、图标=accent_fg;
* 取色无效:全部清 opts 回主题默认(进度条=surfaceVariant/primary、play/pause=primary/onPrimary、
* prev/next=surfaceContainerLow/onSurface——与创建时一致,不回归)。
* 每次绘制前调用(np_draw_page 播放分支 / np_repaint_progress),保证主题跟手。 */
static void np_theme_widgets(void)
{
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
if (g_np_cc_valid) {
op.bg = g_np_cc.fg_dim; op.fg = g_np_cc.accent; /* 进度条:track=fg_dim / fill=accent */
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
op.bg = g_np_cc.accent; op.fg = g_np_cc.accent_fg; /* 三钮:圆底=accent / 图标=accent_fg */
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
return;
}
/* 取色无效 → 默认主题(回退):清 prog/prev/next 的 opts,play/pause 回主题 primary。 */
memset(&op, 0, sizeof op);
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
if (g_np_play || g_np_pause) {
op.bg = th->primary; op.fg = th->onPrimary; /* 主播放钮:强调动作 */
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
}
}
/* 绘制播放控制三钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时
* 上一帧残留被新画圆底盖掉)。控件配色在调用前由 np_theme_widgets 统一挂载取色主题。 */
static void np_draw_ctl(const meow_media_session_t *s)
{
if (g_np_prev) { meow_widget_set_selected(g_np_prev, 0); meow_widget_draw(g_np_prev); }
if (g_np_next) { meow_widget_set_selected(g_np_next, 0); meow_widget_draw(g_np_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_np_pause : g_np_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 进度带(进度条 + 时间)绘制;pos = 当前显示位置。进度条走 SDK meow_progress。 */
static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const meow_ui_theme_t *th)
{
int pct = (s->dur_ms > 0) ? (int)((uint64_t)pos * 100 / s->dur_ms) : 0;
if (pct > 100) pct = 100;
if (g_np_prog) { meow_progress_set(g_np_prog, pct); meow_widget_draw(g_np_prog); }
/* 时间(左=当前,右=总长;MM:SS) */
char l[16], rbuf[16];
unsigned lm = pos / 60000u, ls = (pos % 60000u) / 1000u;
unsigned dm = (s->dur_ms ? s->dur_ms : 0) / 60000u, ds = (s->dur_ms ? s->dur_ms : 0) % 60000u / 1000u;
snprintf(l, sizeof l, "%u:%02u", lm, ls);
snprintf(rbuf, sizeof rbuf, "%u:%02u", dm, ds);
uint32_t tcol = g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant;
ui_draw(NP_PROG_X, NP_TIME_Y, l, tcol, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, tcol, 16);
}
/* ==================================================================
* 批4:歌词页渲染(批2 meow_lyric_view + 数据批3 meow_media_lyric_*)。
* - 有词:meow_lyric_parse(lrc,tlrc)→meow_lyric_view_set_pos(display_pos) 实时滚动,
* 当前行由 meow_lyric_view 内部 meow_lyric_line_at(pos_ms) 计算(与绘制/点跳同源)。
* - 无词/无源:清空并置 pos=0,视图内建空态文案(np.no_lyric→"暂无歌词")。
* 主题:先走 meow_theme 默认(视图自绘 scrim/活动行纯白/灰阶递减);批5 封面取色动态主题的可
* 接入点为 meow_lyric_view_set_colors(本批不取色,故不调用;接入只需在下方填 meow_lyric_view_colors
* 后调用一次,字段 0=回主题默认)。
* ================================================================== */
static void np_lyric_refresh(const meow_media_session_t *s)
{
if (!g_np_lyric) return;
int valid = meow_media_lyric_valid(s);
if (valid) {
/* 换歌/换源(新 lyric_seq/app_id)才重解析;否则沿用,只推进位置。 */
if (g_np_lyric_seq != s->lyric_seq || strcmp(g_np_lyric_app, s->app_id) != 0) {
meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s));
g_np_lyric_seq = s->lyric_seq;
snprintf(g_np_lyric_app, sizeof g_np_lyric_app, "%s", s->app_id);
}
meow_lyric_view_set_pos(g_np_lyric, meow_media_display_pos(s));
} else {
/* 无词/无源:仅当上次仍有词才清空(省冗余 memset),置 pos=0。 */
if (g_np_lyric_last_valid != 0) {
meow_lyric_clear();
g_np_lyric_seq = 0xFFFFFFFFu;
g_np_lyric_app[0] = 0;
}
meow_lyric_view_set_pos(g_np_lyric, 0);
}
g_np_lyric_last_valid = valid;
}
/* 批② home 指示条:底部上滑回桌面的可见反馈(镜向左缘返回提示条;唯位置=屏底左缘)。
* 主题用 on_surface_variant(淡灰指示,iOS home 指示同款语义);draw 在整页/空态/歌词页
* 均调用一次(np_draw_page 播放分支/空态分支 + np_draw_lyric_page)。 */
static void np_draw_home_bar(void)
{
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);
}
/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底,
* 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */
static void np_draw_lyric_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create();
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。
* 批6:有封面时以封面作整页背景(衬在 lyric_view 半透明 scrim 之下),取色只做附加;
* 无封面回退渐变/主题面。
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
np_cover_load(meow_media_current());
np_cover_theme_update();
np_apply_lyric_colors();
/* 用户 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();
}
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
static void np_draw_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
/* 批4:歌词页整页走 lyric_view(无源/无词空态由视图内建)。 */
if (g_np_page == NP_PAGE_LYRIC) {
np_draw_lyric_page(redraw_sb);
return;
}
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) {
/* 空态占位(M7.0b 文案走 meow_str,未知 id 回退中文) */
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const char *t1 = meow_str_zh("np.not_playing", "未在播放");
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
const char *t2 = meow_str_zh("np.open_app_hint", "打开播放 App 以显示正在播放");
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;
}
np_cover_load(s);
/* 批5:封面取色(主/次色)+ 背景渐变或主题面;并注入歌词页 set_colors。
* np_cover_theme_update 幂等(封面路径不变不重取);无封面/取色失败回退 meow_theme。 */
np_cover_theme_update();
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);
np_apply_lyric_colors();
/* 封面:解码缓存 + 圆角裁方;无封面回退主题色占位 + 首字 */
if (g_np_cover) {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
meow_ui_img_cover(NP_COVER_X, NP_COVER_Y, NP_COVER, g_np_cover);
} else {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
meow_gfx_fill_round(NP_COVER_X, NP_COVER_Y, NP_COVER, NP_COVER, 14, th->primary);
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 22);
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
t, meow_theme_get()->onPrimary, 22);
}
/* 元数据(批5:封面取色时文字随 fg/fg_dim,否则主题面) */
np_center_text(NP_TITLE_Y, s->title, 26, g_np_cc_valid ? g_np_cc.fg : th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 60);
if (s->album[0])
np_center_text(NP_ALBUM_Y, s->album, 16, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 80);
/* 批5b:控件随播放页当前主题(进度条轨道/填充 + 三钮)——整页与局部重绘共用同一次派生。 */
np_theme_widgets();
/* 进度(SDK meow_progress) */
uint32_t pos = meow_media_display_pos(s);
np_draw_progress(s, pos, th);
/* 控制按钮(SDK meow_iconbtn,命中/状态走 SDK) */
np_draw_ctl(s);
/* 播放页底部(M6.33:歌词占位已移除——已有独立歌词页,占位多余;批②:底部仅示
* home 指示条,非歌词占位)。 */
np_draw_home_bar(); /* 批②:播放页示 home 指示(底部上滑可回桌面) */
if (redraw_sb) draw_statusbar();
}
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。
* 批5b:局部重绘也走播放页当前主题——进度带/按钮带底色用取色渐变(np_page_bg_row 与整页同 span
* 无缝衔接),进度条/三钮经 np_theme_widgets 挂取色色;无取色回退 meow_theme 面/默认控件色。 */
static void np_repaint_progress(void)
{
const meow_ui_theme_t *th = s_ui_theme();
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
np_cover_theme_update(); /* 取色当前主题(幂等,封面路径未变不重取) */
uint32_t pos = meow_media_display_pos(s);
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏);底色随取色渐变或主题面。 */
if (g_np_cc_valid) {
for (int yy = NP_TIME_Y; yy < NP_BTN_Y + NP_PLAY_R; yy++)
fill_rect(0, yy, fb_xres, 1, np_page_bg_row(yy));
} else {
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
}
np_theme_widgets(); /* 进度条/按钮随取色主题(或回退默认) */
np_draw_progress(s, pos, th);
np_draw_ctl(s);
meow_dirty_add(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y);
meow_ui_flush();
}
/* 控制按钮命中:返回 cmd(1-4),0=未命中。走 SDK 圆形命中(owner==哨兵 + enabled 闸门)。
* 播放/暂停两钮同位:命中任一(同矩形)→ 按会话状态分派 play(1)/pause(2)。 */
static int np_btn_tap(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
if (g_np_prev && meow_widget_hit(g_np_prev, tx, ty)) return 3;
if (g_np_play && meow_widget_hit(g_np_play, tx, ty))
return (s && s->state == MEOW_MEDIA_PLAYING) ? 2 : 1;
if (g_np_next && meow_widget_hit(g_np_next, tx, ty)) return 4;
return 0;
}
/* 进度 seek 命中(拖动/点按进度带):返回 seek_ms(>0),0=未中(或 dur=0)。 */
static int64_t np_progress_seek(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
if (!s || !s->dur_ms) return 0;
if (tx >= NP_PROG_X && tx <= NP_PROG_X + NP_PROG_W &&
ty >= NP_PROG_Y - 14 && ty <= NP_PROG_Y + 20) {
int frac = (tx - NP_PROG_X) * 1000 / NP_PROG_W;
return (int64_t)s->dur_ms * frac / 1000;
}
return 0;
}
void run_now_playing(struct app *apps, int napps)
{
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
struct keypoll kp_keys;
kp_open(&kp_keys, DEV_KEYS);
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
kp_open(&kp_power, DEV_POWER);
struct kd_state kd_play;
memset(&kd_play, 0, sizeof kd_play);
/* 读播放键按住态(用于边沿) */
int m_prev_play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
int np_prev_power = 0; /* 电源键边沿跟踪 */
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
unsigned long long last_prog = now_ms();
unsigned long long np_last_status_tick = now_ms(); /* M6.20:播放页状态栏时钟 1s 刷新(主界面同语义) */
int need_redraw = 1;
int np_exit = 0; /* M6.22:返回手势命中 → 置位退出外层主循环。触摸内的 break 只跳出内层
* 读事件 while,不退出外层 while(!sigterm_flag),故必须靠标志跳出(根因:返回「没反应」)。 */
char last_title[128] = "";
uint32_t last_state = 0xFFFFFFFFu;
char last_app[MEOW_APP_ID_MAX] = "";
char last_art[200] = ""; /* M6.33:上次已刷新封面 artwork 路径(artwork 变化也触发整页重绘) */
meow_ui_activity_push(NP_SENTINEL_ACT); /* M7.0b:进入 push 哨兵(与函数尾 pop 配对)。
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
* owner==active 闸门——漏 push 则 active()!=0x51,
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
g_np_lyric_app[0] = 0;
np_draw_page(1);
meow_dirty_full();
meow_ui_flush();
while (!sigterm_flag) {
if (np_exit) break; /* M6.22:返回手势已命中 → 退出本页,走尾部收尾回主界面 */
meow_ipc_process();
meow_media_tick();
unsigned long long now = now_ms();
/* 触摸 */
if (tfd >= 0 && meow_screen_on) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) { tdx = tlx; tdy = tly; td_ok = 1; }
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1;
np_last_input = now_ms(); }
} else if (!val && td) {
td = 0;
if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* M6.21 诊断 + 批4 页感知:打印该次抬手手势的完整输入与按当前页
* 的方向感知判定(真机定位「返回滑不出」根因曾依赖此日志;
* 保留,键改为 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) {
case NP_GEST_TOP:
/* 顶部热区下滑 → 打开通知中心(与主界面同语义;mnp_top_swipe_down
* 内部已排除左缘 down_x<24,与下方返回手势互斥)。run_notif_center
* 为阻塞模态,返回后整页重画。 */
{
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 0, 0, 0);
need_redraw = 1;
meow_dirty_full();
meow_ui_flush();
}
break;
case NP_GEST_EXIT:
/* 播放页左缘右滑 → 回桌面(保留 M6.26 全局手势语义)。 */
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;
need_redraw = 1;
logmsg("np: -> lyric page");
break;
case NP_GEST_PREV:
/* 歌词页 左→右滑 → 回播放页(含左缘;不接桌面,避免误退)。 */
g_np_page = NP_PAGE_PLAY;
need_redraw = 1;
logmsg("np: -> play page");
break;
case NP_GEST_TAP:
/* 近点按(按页分派):播放页=三钮/进度看点;歌词页预留点跳
* (本批不启用,见 batch5/后续:借 meow_lyric_view_hit_line + on_seek)。 */
if (g_np_page == NP_PAGE_PLAY) {
int cmd = np_btn_tap(tlx, tly);
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
else {
int64_t seek = np_progress_seek(tlx, tly);
if (seek > 0) meow_media_playctl(5, seek);
}
}
break;
default:
break; /* NP_GEST_NONE:反向/纵向主导,忽略不误切 */
}
if (np_exit) break; /* M6.22:返回手势已命中 → 跳出内层读事件 while */
tdx = tdy = -1;
} else { tdx = tdy = -1; }
}
}
}
}
}
/* 播放键边沿 → 播放/暂停切换 */
kp_poll(&kp_keys, NULL, NULL);
int play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (play && !m_prev_play) {
const meow_media_session_t *s = meow_media_current();
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
np_last_input = now_ms();
}
m_prev_play = play;
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
* NP 退出后生效(可接受偏差,记日志)。 */
kp_poll(&kp_power, NULL, NULL);
int pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
if (pwr && !np_prev_power) {
np_last_input = now_ms();
if (meow_screen_on) { meow_panel_off(); logmsg("nowplaying: power tap -> panel off"); }
else { meow_panel_on(); need_redraw = 1; logmsg("nowplaying: power tap -> panel on"); }
}
np_prev_power = pwr;
/* M2.92:NP 页自动息屏(与主界面同超时;页面无触摸/按键计时器不走主循环) */
if (screen_off_timeout > 0 && meow_screen_on &&
now_ms() - np_last_input >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
logmsg("nowplaying: idle %ds -> auto panel off", screen_off_timeout);
}
/* 息屏期只低频轮询按键(唤醒用),不重画不读触摸(FBIOBLANK 已挂起 goodix) */
if (!meow_screen_on) { usleep(100 * 1000); continue; }
/* 媒体变化检测(元数据/源/状态/artwork):立即整页重画。
* M6.33 补 artwork:ncm 状态切换可能先报空 artwork 再复原(标题/源/状态未变),
* 原检测漏 artwork → 封面清空后不再触发整页重绘 → 播放页/歌词页封面永不恢复。 */
const meow_media_session_t *s = meow_media_current();
int meta_changed = !s || strcmp(last_app, s->app_id) ||
strcmp(last_title, s->title) || strcmp(last_art, s->artwork) ||
last_state != s->state;
if (meta_changed) {
if (s) {
snprintf(last_app, sizeof last_app, "%s", s->app_id);
snprintf(last_title, sizeof last_title, "%s", s->title);
snprintf(last_art, sizeof last_art, "%s", s->artwork);
last_state = s->state;
} else { last_app[0] = 0; last_title[0] = 0; last_art[0] = 0; last_state = 0xFFFFFFFFu; }
need_redraw = 1;
}
/* M2.92 修死循环:空态/停止态只在「变化」时画一次(meta_changed 已覆盖
* NULL↔会话切换),不再每 10ms 整页重画(旧行为 = 100% CPU 忙刷 → 卡死观感)。 */
if (need_redraw) {
np_draw_page(0);
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
meow_ui_flush();
need_redraw = 0;
last_prog = now;
} else if (s && (now - last_prog >= 1000)) {
/* 进度推进(按页分流):播放页=进度带+按钮;歌词页=歌词实时滚动重绘(当前行随 pos)。 */
last_prog = now;
if (g_np_page == NP_PAGE_LYRIC) {
/* 批6:整页重绘(封面背景+scrim+歌词),避免 lyric_view 半透明 scrim 每 1s
* 叠涂把封面/背景逐秒压黑(批4 既有「只重绘控件不重铺背景」结构修正)。 */
np_draw_lyric_page(0);
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
meow_ui_flush();
} else {
np_repaint_progress();
}
}
/* M6.20:播放页状态栏时钟 1s 刷新(与主界面/子页同语义;draw_clock 喂 SDK 全局
* 状态栏 + 标脏顶条,SDK present() 重绘——否则时间冻结在进入播放页那一刻)。 */
if (now - np_last_status_tick >= 1000) {
np_last_status_tick = now;
draw_clock();
}
usleep(10000);
}
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
if (tfd >= 0) close(tfd);
kp_close(&kp_keys);
kp_close(&kp_power);
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
g_np_cover_path[0] = 0;
g_np_cc_valid = 0; /* 批5:退出清取色缓存,下次进入重新取 */
g_np_cc_path[0] = 0;
}