Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/now_playing.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

836 lines
47 KiB
C
Raw 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);
}
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
if (ui_width(s, size) <= maxw) {
ui_draw(x, y, s, color, size);
return;
}
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(x, y, buf, color, size);
}
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
static void np_draw_lyric_title(const meow_media_session_t *s)
{
if (!s) return;
if (!s->title[0] && !s->artist[0]) return;
int pad = 24;
int y = SB_BAR_H + 20;
int maxw = fb_xres - 2 * pad;
if (s->title[0])
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
if (s->artist[0])
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
}
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
static void np_set_lyric_ncm_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_colors c;
memset(&c, 0, sizeof c);
c.scrim = 0x000000;
c.cur = 0xFFFFFF;
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
meow_lyric_view_set_colors(g_np_lyric, &c);
}
/* ==================================================================
* 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-05):有封面 → 等比居中放大铺满内容区(cover-fit,
* 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
static void np_draw_lyric_cover_bg(int W, int H)
{
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
return;
}
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
}
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
* 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:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
np_cover_load(meow_media_current());
np_cover_theme_update();
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
* 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
* (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
np_draw_lyric_cover_bg(W, H);
np_set_lyric_ncm_colors();
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
* 纯黑底之上(仍纯黑,即现状回退)。 */
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_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
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;
}
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
* - 音量键 = 系统音量(settings_volume_adj 内含插孔消抖 + 线性渐变);
* - 播放键 = 媒体播放/暂停切换(路由最近活跃媒体会话,与主界面同判);
* 边沿由调用方按防抖后稳定值计算(vold_edge = 稳定按下 ∧ 上一稳定为抬起)。
* 抽成独立小函数供宿主单测(np_apply_key_actions 仅依赖 settings_volume_adj/
* meow_media_playctl/now_ms/logmsg,可 -ffunction-sections 链接隔离)。 */
static void np_apply_key_actions(int vold_edge, int volu_edge, int play_edge,
const meow_media_session_t *s,
unsigned long long *last_input)
{
if (vold_edge) {
settings_volume_adj(-settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume DOWN <- ramp step=%d", settings_volume_step);
} else if (volu_edge) {
settings_volume_adj(+settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume UP <- ramp step=%d", settings_volume_step);
}
if (play_edge) {
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
if (last_input) *last_input = now_ms();
}
}
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; /* 音量键防抖/基线上收 key_router(进程唯一) */
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;
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
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(「播放页三键无效」根因)。 */
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
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; }
}
}
}
}
}
/* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
* 与页无关)音量键无反应。 */
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
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;
}
(void)kd_tick(&kd_play, play);
play = kd_play.stable;
/* 音量键/播放键边沿 → 动作(与主界面/子页同语义;见 np_apply_key_actions) */
np_apply_key_actions(kev.vol_dn_edge, kev.vol_up_edge,
play && !m_prev_play, meow_media_current(), &np_last_input);
m_prev_play = play;
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
}
/* 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 仍钉哨兵,复用) */
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
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;
}