637 lines
34 KiB
C
637 lines
34 KiB
C
/* 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 60
|
||
#define NP_TITLE_Y 412
|
||
#define NP_ARTIST_Y 450
|
||
#define NP_ALBUM_Y 480
|
||
#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_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
|
||
#define NP_LYRIC_Y 640
|
||
|
||
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 路径变化才重载)。 */
|
||
static void np_cover_load(const meow_media_session_t *s)
|
||
{
|
||
if (!s) return;
|
||
if (strcmp(g_np_cover_path, s->artwork) == 0) return;
|
||
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[200];
|
||
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,逐行整数插值;封面主题背景用)。 */
|
||
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;
|
||
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 span = h - 1;
|
||
for (int yy = 0; yy < h; yy++) {
|
||
int t = span > 0 ? yy * 255 / span : 0;
|
||
int r = tr + ((br - tr) * t + 128) / 255;
|
||
int g = tg + ((bg - tg) * t + 128) / 255;
|
||
int b = tb + ((bb - tb) * t + 128) / 255;
|
||
fill_rect(x, y + yy, w, 1, ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b);
|
||
}
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
/* 主题刷新:播放钮底色/图标随当前主题(明暗/风格切换后跟手);批5 封面取色时随 accent。 */
|
||
static void np_refresh_theme(void)
|
||
{
|
||
if (!g_np_init) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_widget_opts op; memset(&op, 0, sizeof op);
|
||
op.bg = g_np_cc_valid ? g_np_cc.accent : th->primary;
|
||
op.fg = g_np_cc_valid ? g_np_cc.accent_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 按会话状态只画其一——两钮同位,改画时
|
||
* 上一帧残留被新画圆底盖掉)。 */
|
||
static void np_draw_ctl(const meow_media_session_t *s)
|
||
{
|
||
np_refresh_theme();
|
||
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;
|
||
}
|
||
|
||
/* 歌词页整页绘制:铺封面取色渐变(或 theme surface)底 → 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。 */
|
||
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();
|
||
np_lyric_refresh(meow_media_current());
|
||
if (g_np_lyric)
|
||
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||
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);
|
||
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);
|
||
/* 进度(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);
|
||
/* 歌词占位(M7.0b 文案走 meow_str) */
|
||
const char *ly = meow_str_zh("np.lyric_placeholder", "歌词(占位)");
|
||
ui_draw((W - ui_width(ly, 16)) / 2, NP_LYRIC_Y, ly, th->on_surface_variant, 16);
|
||
if (redraw_sb) draw_statusbar();
|
||
}
|
||
|
||
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。 */
|
||
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;
|
||
uint32_t pos = meow_media_display_pos(s);
|
||
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏)。 */
|
||
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
|
||
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] = "";
|
||
|
||
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);
|
||
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_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; }
|
||
|
||
/* 媒体变化检测(元数据/源/状态):立即整页重画 */
|
||
const meow_media_session_t *s = meow_media_current();
|
||
int meta_changed = !s || strcmp(last_app, s->app_id) ||
|
||
strcmp(last_title, s->title) || 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);
|
||
last_state = s->state;
|
||
} else { last_app[0] = 0; last_title[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) {
|
||
np_lyric_refresh(s);
|
||
if (g_np_lyric) {
|
||
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||
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;
|
||
}
|