Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/minibar.c
T

225 lines
11 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.
/* 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; }
}