feat(ncm): 底部mini播放栏(替代顶栏图标) — 异Activity常驻,非播放页+有活动播放时显示:封面缩略+歌名/歌手+播放暂停+进度带+点击进播放页,宿主barhit/plsplay/home/player PASS

This commit is contained in:
AskaEth committed 2026-09-03 18:07:57 +08:00
1 parent 26a140e00f
commit 671f0d1835
6 files changed
+279 -3

No files matched your search

+5
View File
@@ -86,6 +86,8 @@ static void app_on_key(int keycode, int is_down)
static void app_on_tap(int x, int y)
{
/* mini 播放栏:非播放页底部常驻,顶层先拦(点钮 toggle / 点栏体进播放页),不落页面 on_tap。 */
if (mini_bar_hit(x, y)) return;
if (login_is_active()) { login_on_tap(x, y); return; }
if (player_is_active()) { player_on_tap(x, y); return; }
if (settings_is_active()) { settings_on_tap(x, y); return; }
@@ -158,6 +160,7 @@ static void app_on_tick(void)
if (act != g_reported_act) { g_reported_act = act; ipc_act_report(act); }
}
uint64_t now = meow_ui_time_ms();
mini_bar_tick(now); /* mini 播放栏:可见性/歌曲/暂停态/进度变化 → 失效栏带并重绘 */
/* §NEW cookie 续期判会话失效 → 清登录态回登录页(worker 在后台续期发现 301/-460 时置标志;
* 在此 UI 线程执行 logout+切页——meow_view_page_replace 非线程安全,只能 UI 线程做) */
if (account_cookie_need_relogin()) {
@@ -223,6 +226,7 @@ int main(void)
g_plr_view = player_view_create();
g_set_view = settings_view_create();
g_search_view = search_view_create();
mini_bar_init(); /* mini 播放栏:登记跨 Activity 绘制物(进 root 页前,首帧即参与重画) */
/* 进入根页:已登录→home,否则→login */
meow_view_page_push(g_st.logged_in ? ACT_HOME : ACT_LOGIN);
@@ -237,6 +241,7 @@ int main(void)
g_app_quit = 1;
player_manager_stop_thread();
ipc_media_stop();
mini_bar_shutdown();
meow_view_free(g_set_view);
meow_view_free(g_plr_view);
meow_view_free(g_pl_view);
+6
View File
@@ -349,6 +349,12 @@ void settings_touch_move(int x, int y);
void settings_open(void); /* 进设置页 */
void settings_reload(void);
/* mini 播放栏(ui/minibar.c,跨 Activity 常驻;非播放页 + 有活动播放会话时显示)*/
void mini_bar_init(void); /* 登记 CUSTOM 绘制物(main 建页后调) */
void mini_bar_shutdown(void); /* 注销(app 退出前) */
void mini_bar_tick(uint64_t now);/* 每帧:可见性/歌曲/暂停态/进度变化 → 失效栏带并重绘 */
int mini_bar_hit(int x, int y); /* app_on_tap 顶层先行拦截:1=消费(toggle/进播放页) */
/* search */
meow_view *search_view_create(void);
int search_is_active(void);
+42 -1
View File
@@ -95,6 +95,9 @@ static int page_id(const char *name)
if (strcmp(name, "grid") == 0) return ACT_PLAYER; /* 保留:旧横屏(重做后恒竖屏) */
if (strcmp(name, "playerbuf") == 0) return ACT_PLAYER; /* §18 缓冲态 */
if (strcmp(name, "playererr") == 0) return ACT_PLAYER; /* §18 错误态 */
if (strcmp(name, "plsplay") == 0) return ACT_PLAYLIST; /* mini 栏目检(非播放页+播放态) */
if (strcmp(name, "homeplay") == 0) return ACT_HOME; /* mini 栏目检(首页+播放态) */
if (strcmp(name, "barhit") == 0) return ACT_PLAYLIST; /* mini 栏命中自检 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
if (strcmp(name, "search") == 0) return ACT_SEARCH;
if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */
@@ -123,6 +126,7 @@ int main(int argc, char **argv)
g_dark = dark;
meow_theme_add_listener(ncm_on_theme);
ncm_on_theme(dark ? MEOW_THEME_DARK : MEOW_THEME_LIGHT); /* 同步初始 g_dark */
mini_bar_init(); /* mini 播放栏:登记跨 Activity 绘制物(真实页面 + 真实 SDK 绘制物注册表) */
/* mock 状态 */
if (g_st.cache_dir[0] == 0) {
@@ -229,6 +233,18 @@ int main(int argc, char **argv)
appstate_search_history_add("Jay \u5553\u8868");
appstate_search_history_add("lofi beat");
appstate_search_history_add("Jay \u5553\u8868"); /* 去重 → 仍 3 条 */
} else if (strcmp(name, "plsplay") == 0 || strcmp(name, "homeplay") == 0 ||
strcmp(name, "barhit") == 0) {
/* mini 播放栏目检/命中自检:非播放页(歌单/首页)+ 播放态 → 底部显示 mini 栏 */
mock_playlists(); mock_songs();
pthread_mutex_lock(&g_play.mu);
g_play.state = NCM_PC_PLAYING;
g_play.cur_song = 0;
g_play.pos_ms = 94000;
g_play.dur_ms = 180000;
g_play.play_song = g_st.songs[0]; /* 播放快照(§17) */
g_play.play_song_valid = 1;
pthread_mutex_unlock(&g_play.mu);
} else {
mock_playlists();
}
@@ -236,7 +252,8 @@ int main(int argc, char **argv)
meow_view *v = NULL;
if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create();
else if (strcmp(name, "home") == 0) v = home_view_create();
else if (strcmp(name, "playlist") == 0) v = playlist_view_create();
else if (strcmp(name, "playlist") == 0 || strcmp(name, "plsplay") == 0 ||
strcmp(name, "barhit") == 0) v = playlist_view_create();
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr */
else if (strcmp(name, "settings") == 0) v = settings_view_create();
else if (strcmp(name, "search") == 0 || strcmp(name, "searchhist") == 0 ||
@@ -277,6 +294,8 @@ int main(int argc, char **argv)
p0, p1, p2, latched_change, ok ? "PASS" : "FAIL");
if (!ok) return 1;
}
/* 任意页先喂一次 mini 栏 tick:可见性/歌曲/进度变化 → 失效栏带,随后整帧重画包含 mini 栏 */
mini_bar_tick(meow_ui_time_ms());
/* player 页:其 on_tick 通常驱动进度;这里直接再画一次确保状态就绪 */
meow_ui_paint_full();
if (page_id(name) == ACT_PLAYER) {
@@ -316,6 +335,28 @@ int main(int argc, char **argv)
(ok1 && ok2) ? "PASS" : "FAIL");
if (!(ok1 && ok2)) return 1;
}
if (strcmp(name, "barhit") == 0) {
/* mini 播放栏命中自检(几何与 minibar.c 同口径:W=480 H=720 → 栏带 (0,656,480,64),
* 播放钮中心 (446,688),命中半径 22;栏体外 (100,600) 不消费):
* ① 播放态下点播放钮 → 暂停;再点 → 恢复(toggle);
* ② 点栏体 → 消费并 push(ACT_PLAYER)(进播放页);进播放页后 active=ACT_PLAYER → 栏隐藏(不消费);
* ③ 点播放钮回到非播放页后,栏体外点按不消费(0,落页面 on_tap)。 */
int w = APP_W, h = APP_H, bh = 64, by = h - bh;
int bx = w - 12 - 32 / 2 - 12 / 2, bcy = by + 32; /* 与 minibar.c MINI_* 一致 */
pthread_mutex_lock(&g_play.mu); int s0 = g_play.state; pthread_mutex_unlock(&g_play.mu);
int h1 = mini_bar_hit(bx, bcy); /* 播放钮:toggle */
pthread_mutex_lock(&g_play.mu); int s1 = g_play.state; pthread_mutex_unlock(&g_play.mu);
int h2 = mini_bar_hit(bx, bcy); /* 再点:toggle 回 */
pthread_mutex_lock(&g_play.mu); int s2 = g_play.state; pthread_mutex_unlock(&g_play.mu);
int ok_tg = (h1 == 1 && s1 == NCM_PC_PAUSED) && (h2 == 1 && s2 == NCM_PC_PLAYING);
int h3 = mini_bar_hit(100, bcy); /* 栏体 → 进播放页 */
int act_after = meow_ui_activity_active(); /* 应为 ACT_PLAYER */
int h4 = mini_bar_hit(100, bcy); /* 已在播放页 → 栏隐藏 → 不消费 */
printf("barhit: toggle %d->%d->%d=%s consume-body=%d act=%d(PLAYER) hidden=%d -> %s\n",
s0, s1, s2, ok_tg ? "PASS" : "FAIL", h3, act_after, h4,
(ok_tg && h3 == 1 && act_after == ACT_PLAYER && h4 == 0) ? "PASS" : "FAIL");
if (!ok_tg) return 1;
}
meow_ui_shutdown();
printf("dumped %s -> %s\n", name, dump);
+224
View File
@@ -0,0 +1,224 @@
/* 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; }
}