225 lines
11 KiB
C
225 lines
11 KiB
C
/* ui/minibar.c — 底部 mini 播放栏(跨 Activity 常驻;非播放页 + 有活动播放会话时显示)。
|
||
*
|
||
* 需求(用户定案 2026-09-03):ncm 首页/歌单/搜索/设置等**非播放页**底部,当存在活动播放会话
|
||
* (player_manager 有当前曲目,state != NCM_PC_STOPPED)时显示一条紧凑 mini 播放栏(与主流
|
||
* 音乐 App 一致):封面缩略(40×40,复用 cover 缩略链)+ 歌名/歌手(截断)+ 播放/暂停钮
|
||
* (点击 toggle)+ 细进度带;**点击栏体 → 跳转 ncm 播放页(ACT_PLAYER)**;播放页内不显示此栏。
|
||
* 替代原先「顶栏进播放页图标」方案(本栏常驻非播放页,进播放页更直接,与主流 App 交互一致)。
|
||
*
|
||
* 实现(零侵入页面 UI,跨页面常驻,最小侵):
|
||
* - 绘制 = SDK 绘制物注册表 CUSTOM 项(z=MINI_Z,bbox=底部 MINI_H 带)。CUSTOM 项不按 owner
|
||
* Activity 过滤(见 meow_ui_draw.c paint():仅 WIDGET 按 owner 过滤、VIEW 按 entered 过滤,
|
||
* CUSTOM 恒画)→ 跨 Activity 常驻;且只在它所在区域被失效/整帧重画时被调(区域收敛,不整屏)。
|
||
* - 命中 = app.c app_on_tap **顶层先调 mini_bar_hit()**(返回 1 即消费,不再落页面 on_tap)——
|
||
* 点播放/暂停钮 toggle、点栏体 push(ACT_PLAYER)。不走 meow_ui_global:宿主 test_host_stub
|
||
* 只桩了 global_render/hit 而未桩 add/remove,为双端 0 warning 可链,改走 app_on_tap 拦截。
|
||
* - 刷新 = mini_bar_tick()(app_on_tick 每帧调):可见性/歌曲/暂停态/进度 1% 桶变化 →
|
||
* 失效整条栏带并重绘;切歌 → 复用 cover_request 拉新封面(非播放页也随切歌更新)。
|
||
* - 绘制安全:封面 cover_get() 持锁返回,绘完 cover_put()(与播放/歌单页同口径,§18 修 UAF)。
|
||
*
|
||
* 性能:栏带仅 ~480×64;只在该带失效时重画(区域收敛),不整屏;进度按 1% 桶驱动
|
||
* (约 1 次/2s,不逐帧刷)。播放/进度/元数据读取全部走 player_manager 与 cover(锁内快照),
|
||
* 不新开线程、不碰页面布局。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <string.h>
|
||
#include <math.h>
|
||
#include "ncm_icons.h"
|
||
|
||
/* ---- 几何 ---- */
|
||
#define MINI_H 64 /* 栏带高(含底部进度带/下缘) */
|
||
#define MINI_PAD 12 /* 左内边距 */
|
||
#define MINI_COV 40 /* 封面缩略边长 */
|
||
#define MINI_GAP 12 /* 封面↔文本↔按钮 间距 */
|
||
#define MINI_BTN 32 /* 播放/暂停图标绘制边长 */
|
||
#define MINI_HITR 22 /* 播放/暂停钮命中半径(略大于图标,好点) */
|
||
#define MINI_Z 400 /* 栏带 z:高于页面内容(≤60)/页脚(1),不与 topbar(999,顶部) 相交 */
|
||
#define MINI_TITLE 18 /* 歌名字号 */
|
||
#define MINI_ART 14 /* 歌手字号 */
|
||
|
||
/* ---- 状态(UI 线程串行,无需锁)---- */
|
||
static int g_mini_id; /* CUSTOM 绘制物 id(0=未注册) */
|
||
static ncm_rect_t g_bar_r; /* 栏带矩形(绘制 bbox + 命中区) */
|
||
static int g_last_vis, g_last_state, g_last_song;
|
||
static long g_last_bucket; /* 进度桶(0..100,1% 粒度) */
|
||
static char g_last_id[24]; /* 上次绘制的歌曲 id(切歌判据) */
|
||
|
||
/* 是否显示 mini 栏:有活动播放会话(state≠STOPPED)且当前非播放页。 */
|
||
static int mini_visible(void)
|
||
{
|
||
if (player_manager_state() == NCM_PC_STOPPED) return 0;
|
||
if (meow_ui_activity_active() == ACT_PLAYER) return 0;
|
||
return 1;
|
||
}
|
||
|
||
/* 当前播放曲元数据:优先播放快照(窗口无关);快照未就绪退窗口拷贝——与 player.c cur_song_meta 同口径。 */
|
||
static int mini_cur_song_meta(ncm_song_t *out)
|
||
{
|
||
if (player_manager_current_song(out) == 0) return 0;
|
||
int sc = player_manager_playing_song();
|
||
if (sc >= 0 && appstate_song_copy(sc, out) == 0) return 0;
|
||
return -1;
|
||
}
|
||
|
||
/* 圆角图四角裁掉(盖回栏底色):局部复制 player.c round_corners_bg(mini 栏独立绘制,
|
||
* 不复用跨文件 static)。 */
|
||
static void mini_round_corners(int x, int y, int w, int h, int r, uint32_t bg)
|
||
{
|
||
if (r > w / 2) r = w / 2;
|
||
if (r > h / 2) r = h / 2;
|
||
for (int dy = 0; dy < r; dy++) {
|
||
int inset = r - (int)sqrt((double)(r * r - (r - dy) * (r - dy)));
|
||
if (inset < 0) inset = 0;
|
||
if (inset > w / 2) inset = w / 2;
|
||
meow_ui_fill_rect(x, y + dy, inset, 1, bg);
|
||
meow_ui_fill_rect(x + w - inset, y + dy, inset, 1, bg);
|
||
meow_ui_fill_rect(x, y + h - 1 - dy, inset, 1, bg);
|
||
meow_ui_fill_rect(x + w - inset, y + h - 1 - dy, inset, 1, bg);
|
||
}
|
||
}
|
||
|
||
/* MD3 图标 blit(播放/暂停):ncm_icons.h 32×32 单通道 alpha 最近邻放大到 size×size,居中 (cx,cy)。 */
|
||
static void mini_draw_icon(int idx, int cx, int cy, int size, uint32_t color)
|
||
{
|
||
const unsigned char *src = ncm_icons[idx].data;
|
||
if (size <= 0) return;
|
||
int x0 = cx - size / 2, y0 = cy - size / 2;
|
||
for (int y = 0; y < size; y++) {
|
||
int sy = (y * NCM_ICON_SIZE) / size;
|
||
if (sy >= NCM_ICON_SIZE) sy = NCM_ICON_SIZE - 1;
|
||
for (int x = 0; x < size; x++) {
|
||
int sx = (x * NCM_ICON_SIZE) / size;
|
||
if (sx >= NCM_ICON_SIZE) sx = NCM_ICON_SIZE - 1;
|
||
int a = src[sy * NCM_ICON_SIZE + sx];
|
||
if (a > 4) meow_ui_fill_rect_alpha(x0 + x, y0 + y, 1, 1, color, a);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 绘制整条 mini 栏。draw_cb 由绘制物注册表在「区域/整帧重画」时调用;仅 mini_visible() 才画。 */
|
||
static void mini_draw(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (!mini_visible()) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
ncm_rect_t r = g_bar_r;
|
||
int W = r.w, H = r.h;
|
||
|
||
/* 底(surfaceContainer,与列表卡片同色)+ 顶部 1px 分隔线 */
|
||
meow_ui_fill_rect(r.x, r.y, W, H, th->surfaceContainer);
|
||
meow_ui_fill_rect(r.x, r.y, W, 1, th->outlineVariant);
|
||
|
||
/* 当前曲元数据(快照优先) */
|
||
ncm_song_t sm;
|
||
int have = (mini_cur_song_meta(&sm) == 0);
|
||
const char *nm = (have && sm.name[0]) ? sm.name : "暂无播放";
|
||
const char *ar = (have && sm.art_name[0]) ? sm.art_name : "";
|
||
|
||
/* 封面缩略(复用 cover 链;cover_get 持锁返回,cover_put 配对) */
|
||
int cx = r.x + MINI_PAD, cy = r.y + (H - MINI_COV) / 2;
|
||
ncm_fill_round(cx, cy, MINI_COV, MINI_COV, 8, pa->red_ctr);
|
||
meow_img *cov = cover_get();
|
||
if (cov) {
|
||
meow_ui_img_cover(cx, cy, MINI_COV, cov);
|
||
mini_round_corners(cx, cy, MINI_COV, MINI_COV, 8, th->surfaceContainer);
|
||
} else {
|
||
meow_ui_text_center(cx + MINI_COV / 2, cy + (MINI_COV - meow_ui_text_height(th->font_body)) / 2,
|
||
"\u266a", th->font_body, pa->red);
|
||
}
|
||
cover_put();
|
||
|
||
/* 歌名/歌手(截断到播放钮左) */
|
||
int tx = cx + MINI_COV + MINI_GAP;
|
||
int maxw = r.x + W - MINI_PAD - tx - (MINI_BTN + MINI_GAP * 2);
|
||
if (maxw < 40) maxw = 40;
|
||
int ty = r.y + (H - (MINI_TITLE + MINI_ART + 4)) / 2;
|
||
meow_ui_text(tx, ty, ncm_text_fit(nm, maxw, MINI_TITLE), MINI_TITLE, th->onSurface);
|
||
meow_ui_text(tx, ty + MINI_TITLE + 2, ncm_text_fit(ar, maxw, MINI_ART), MINI_ART, th->onSurfaceVariant);
|
||
|
||
/* 播放/暂停钮(右侧红 icon;PAUSED/STOPPED → 播放,否则暂停) */
|
||
int st = player_manager_state();
|
||
int bx = r.x + W - MINI_PAD - MINI_BTN / 2 - MINI_GAP / 2;
|
||
int bcy = r.y + H / 2;
|
||
mini_draw_icon((st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? NCM_I_PLAY : NCM_I_PAUSE,
|
||
bx, bcy, MINI_BTN, pa->red);
|
||
|
||
/* 细进度带(底缘 3px:track surfaceVariant + fill 品牌红) */
|
||
int64_t pos = player_manager_pos_ms();
|
||
int64_t dur = player_manager_dur_ms();
|
||
if (dur <= 0 && have) dur = sm.duration_ms;
|
||
int frac = dur > 0 ? (int)(pos * 1000 / dur) : 0;
|
||
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
|
||
meow_ui_fill_rect(r.x, r.y + H - 3, W, 3, th->surfaceVariant);
|
||
meow_ui_fill_rect(r.x, r.y + H - 3, (int)((int64_t)W * frac / 1000), 3, pa->red);
|
||
}
|
||
|
||
/* 命中:app.c app_on_tap 顶层先调。非显示态/栏带外 → 0(不消费)。点播放/暂停钮 → toggle;
|
||
* 点栏体 → push(ACT_PLAYER)(进播放页)。返回 1=消费(拦截,不再落页面 on_tap)。 */
|
||
int mini_bar_hit(int x, int y)
|
||
{
|
||
if (!mini_visible()) return 0;
|
||
if (y < g_bar_r.y || y >= g_bar_r.y + g_bar_r.h ||
|
||
x < g_bar_r.x || x >= g_bar_r.x + g_bar_r.w) return 0;
|
||
int bx = g_bar_r.x + g_bar_r.w - MINI_PAD - MINI_BTN / 2 - MINI_GAP / 2;
|
||
int bcy = g_bar_r.y + g_bar_r.h / 2;
|
||
int dx = x - bx, dy = y - bcy;
|
||
if (dx * dx + dy * dy <= MINI_HITR * MINI_HITR) {
|
||
int st = player_manager_state();
|
||
if (st == NCM_PC_PAUSED) player_manager_pause(0); /* 恢复 */
|
||
else if (st != NCM_PC_STOPPED) player_manager_pause(1); /* 暂停(含 BUFFER 保守停) */
|
||
return 1;
|
||
}
|
||
meow_view_page_push(ACT_PLAYER);
|
||
return 1;
|
||
}
|
||
|
||
/* 每帧(app_on_tick):可见性/歌曲/暂停态/进度 1% 桶 变化 → 失效整条栏带并重绘;
|
||
* 切歌 → 复用 cover_request 拉新封面(非播放页也随切歌更新;cover_request 幂等去重,宿主 mock 内跳过网络)。 */
|
||
void mini_bar_tick(uint64_t now)
|
||
{
|
||
(void)now;
|
||
int vis = mini_visible();
|
||
int st = player_manager_state();
|
||
int sc = player_manager_playing_song();
|
||
ncm_song_t sm;
|
||
int have = (mini_cur_song_meta(&sm) == 0);
|
||
const char *id = have ? sm.id : "";
|
||
int64_t pos = player_manager_pos_ms(), dur = player_manager_dur_ms();
|
||
long bucket = dur > 0 ? (long)(pos * 100 / dur) : 0;
|
||
if (bucket < 0) bucket = 0; if (bucket > 100) bucket = 100;
|
||
int id_changed = (id[0] && strcmp(id, g_last_id) != 0);
|
||
|
||
if (vis == g_last_vis && st == g_last_state && sc == g_last_song &&
|
||
bucket == g_last_bucket && !id_changed) return;
|
||
|
||
g_last_vis = vis; g_last_state = st; g_last_song = sc;
|
||
g_last_bucket = bucket;
|
||
ncm_strlcpy(g_last_id, id, sizeof g_last_id);
|
||
|
||
/* 切歌 → 请求当前曲封面(专辑图优先,无则歌单封面兜底) */
|
||
if (vis && id_changed) {
|
||
if (have && sm.al_pic[0]) cover_request(sm.al_pic, sm.id);
|
||
else if (have && g_st.cover_url[0]) cover_request(g_st.cover_url, sm.id);
|
||
}
|
||
if (g_mini_id) meow_ui_invalidate(g_bar_r.x, g_bar_r.y, g_bar_r.w, g_bar_r.h);
|
||
}
|
||
|
||
/* 登记(main 建页后调一次,host/device 通用):注册 CUSTOM 绘制物(跨 Activity 常驻)。 */
|
||
void mini_bar_init(void)
|
||
{
|
||
if (g_mini_id) return;
|
||
int W = meow_ui_width(), H = meow_ui_height();
|
||
g_bar_r = NR(0, H - MINI_H, W, MINI_H);
|
||
g_last_vis = -1; g_last_state = -1; g_last_song = -1; g_last_bucket = -1;
|
||
g_last_id[0] = 0;
|
||
g_mini_id = meow_draw_custom_register(MINI_Z, g_bar_r.x, g_bar_r.y, g_bar_r.w, g_bar_r.h,
|
||
mini_draw, NULL);
|
||
}
|
||
|
||
/* 注销(app 退出前)。 */
|
||
void mini_bar_shutdown(void)
|
||
{
|
||
if (g_mini_id) { meow_draw_unregister(g_mini_id); g_mini_id = 0; }
|
||
}
|