700 lines
29 KiB
C
700 lines
29 KiB
C
/* ui/player.c — 播放页(ACT_PLAYER)重做:封面页/歌词页双页(屏幕正中左/右滑切换)。
|
||
*
|
||
* 设计(NCM_CACHE_DESIGN §6 定案 2026-09-01):
|
||
* - 去掉标题栏(不再用 ncm_topbar);返回仍走左缘手势(SWIPE_LEFT → app_on_swipe → page_pop)
|
||
* /系统返回钮语义。
|
||
* - 双页结构:封面页 / 歌词页。屏幕正中左/右滑切换。手势分流:左缘 ≤24px 归系统返回
|
||
* (SDK SWIPE_LEFT),顶部热区归通知中心,中部横滑归页切换(页面指示点显示当前页)。
|
||
* - 封面页:方形大图+圆角(cover.c LRU,无旋转动画)+ 进度条(可点 seek)+ MD3 矢量控制钮
|
||
* (prev/play-pause/next,ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)
|
||
* + 元信息 + 实际音质档 + 页面指示点;缓冲/错误态沿用(「缓冲中…N%」/中文错误条+重试)。
|
||
* - 歌词页:LRC 滚动(当前行高亮居中,上下行渐隐)+ 点行跳转;默认=封面模糊置底,
|
||
* 省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode: default/power_save)。
|
||
* - 日夜间切换:主题配色只映射到半透明叠加层(歌词页 scrim)与 accent(红),
|
||
* 不动沉浸底(恒 NCM_PLAYER_BG)。
|
||
*
|
||
* 性能(§6 专项,分带刷新纪律沿用):
|
||
* - 封面页拆「封面静态带 g_cover_r」+「动态带 g_dyn_r」(互不重叠 bbox):tick 只标脏
|
||
* 动态带(进度/控制/元信息/指示点),封面带仅 歌曲/封面 变化才重画(不整页重绘)。
|
||
* - 歌词页为「整屏带状物」(bg 置底 + 歌词叠加):仅在 进入/切歌/封面变化/换背景模式/
|
||
* 换行(~5s 一次)时整屏重画;pos tick 不重画歌词页(歌词按行进,非逐帧滚动)。
|
||
* - 模糊底图 = 封面按屏幕铺底(meow_ui_img_scaled 最近邻)+ scrim 叠加;区域重画时绘制
|
||
* clip 收敛(meow_ui_draw.c),只算相交区,避免 overdraw。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <math.h>
|
||
#include "ncm_icons.h"
|
||
|
||
/* 播放页固定视觉色(沉浸底恒 NCM_PLAYER_BG;文本/控件用固定浅色 —— 日夜间仅 accent 红
|
||
* 与歌词 scrim 随 g_dark/主题变化,不动底。参照 §8.1「语义色」但播放页为沉浸深色介质,
|
||
* 文本若跟主题 onSurface(浅色主题为近黑)会隐形,故播放页用固定浅前景。 */
|
||
#define PLR_TEXT 0xEDEDED /* 主文本 */
|
||
#define PLR_TEXT_DIM 0xA6A6AC /* 次文本/时间 */
|
||
#define PLR_CTRL_BG 0x2A2A30 /* prev/next 圆形钮底 */
|
||
#define PLR_CTRL_FG 0xE8E8E8
|
||
#define PLR_TRACK_BG 0x33333A /* 进度条未播轨 */
|
||
#define PLR_EDGE_W 24 /* 左缘返回热区(与 SDK EDGE_W 一致,防抢返回) */
|
||
#define PLR_SWIPE_MS 65 /* 中部横滑判页阈值(与 SDK SWIPE_THRESH 一致) */
|
||
|
||
typedef struct { meow_view *view; int active; } player_ctrl;
|
||
static player_ctrl g_plc;
|
||
|
||
static int g_ppage; /* 0=封面页 1=歌词页 */
|
||
static int g_pstatic, g_pdyn; /* 自定义绘制物 id(按当前页注册) */
|
||
static int g_W, g_H; /* 屏尺寸(沉浸:0,0,W,H) */
|
||
static int g_last_pos_ms, g_last_state, g_last_song, g_last_line, g_last_eff;
|
||
static int g_last_dl, g_last_err;
|
||
static char g_last_meta_id[24];
|
||
|
||
/* 中部横滑跟踪(on_touch_down/move 记录;resolved in on_tap/on_swipe) */
|
||
static int g_touch_on;
|
||
static int g_touch_sx, g_touch_sy;
|
||
static int g_drag_latched, g_drag_dx;
|
||
|
||
/* ---- 封面页几何 ---- */
|
||
static ncm_rect_t g_cover_r; /* 封面页静态带(方形大图) */
|
||
static ncm_rect_t g_dyn_r; /* 封面页动态带(元信息/进度/控制/指示点) */
|
||
static int g_title_y, g_artist_y, g_qual_y, g_times_y;
|
||
static ncm_rect_t g_bar;
|
||
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
|
||
|
||
/* ---- 歌词页几何 ---- */
|
||
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
|
||
static int g_lyr_lh; /* 行高 */
|
||
static int g_lyr_meta_y; /* 顶部元信息 y */
|
||
|
||
#define CTL_RR (g_r_side + 8) /* 侧钮命中容差(圆外一点仍可点) */
|
||
|
||
static int cur_song(void)
|
||
{
|
||
int c = player_manager_playing_song();
|
||
return (c >= 0 && c < appstate_queue_count()) ? c : -1; /* §23:按队列来源界 */
|
||
}
|
||
|
||
/* 当前播放曲元数据(§17):优先播放快照(窗口无关);快照未就绪时退窗口拷贝。 */
|
||
static int cur_song_meta(ncm_song_t *out)
|
||
{
|
||
if (player_manager_current_song(out) == 0) return 0;
|
||
int sc = cur_song();
|
||
if (sc >= 0 && appstate_song_copy(sc, out) == 0) return 0;
|
||
return -1;
|
||
}
|
||
|
||
static void fmt_ms(int64_t ms, char *out, size_t sz)
|
||
{
|
||
if (ms < 0) ms = 0;
|
||
int sec = (int)(ms / 1000);
|
||
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
|
||
}
|
||
|
||
/* 颜色插值(t=0..255 混向 b)。播放页歌词渐隐/叠加用(无 GPU,逐通道整数混合)。 */
|
||
static uint32_t mix_c(uint32_t a, uint32_t b, int t)
|
||
{
|
||
if (t < 0) t = 0; if (t > 255) t = 255;
|
||
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
|
||
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
|
||
int r = ar + ((br - ar) * t + 128) / 255;
|
||
int g = ag + ((bg - ag) * t + 128) / 255;
|
||
int bl = ab + ((bb - ab) * t + 128) / 255;
|
||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
|
||
}
|
||
|
||
/* MD3 图标 blit:把 ncm_icons.h 的 32x32 单通道 alpha 位图最近邻放大到 size×size,居中于
|
||
* (cx,cy),逐像素以 color/alpha 混到 back_buf(fill_rect_alpha blend565)。 */
|
||
static void draw_md3_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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 控制钮:圆形底 + MD3 图标(无逐帧三角行扫)。icon 尺寸按底半径缩放(~4r/3 使 24 内容入圈)。 */
|
||
static void draw_ctrl_btn(int icon, int cx, int cy, int r, uint32_t circle, uint32_t fg)
|
||
{
|
||
for (int dy = -r; dy <= r; dy++) {
|
||
int half = (int)sqrt((double)(r * r - dy * dy));
|
||
if (half <= 0) continue;
|
||
meow_ui_fill_rect(cx - half, cy + dy, 2 * half, 1, circle);
|
||
}
|
||
draw_md3_icon(icon, cx, cy, (r * 4) / 3, fg);
|
||
}
|
||
|
||
/* 圆角封面四角裁掉:把方形图四角(圆角外-方形内)盖回背景色(廉价圆角,无 GPU 裁剪)。 */
|
||
static void round_corners_bg(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);
|
||
}
|
||
}
|
||
|
||
static void draw_page_dots(int y)
|
||
{
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
for (int i = 0; i < 2; i++) {
|
||
int cx = g_W / 2 - 13 + i * 26;
|
||
uint32_t c = (i == g_ppage) ? pa->red : th->outlineVariant;
|
||
for (int dy = -4; dy <= 4; dy++) {
|
||
int half = (int)sqrt((double)(16 - dy * dy));
|
||
if (half <= 0) continue;
|
||
meow_ui_fill_rect(cx - half, y + dy, 2 * half, 1, c);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 几何(沉浸:0,0,W,H)。封面页/歌词页各自布局。 */
|
||
static void player_compute_geom(void)
|
||
{
|
||
g_W = meow_ui_width();
|
||
g_H = meow_ui_height();
|
||
int W = g_W, H = g_H;
|
||
|
||
int cs = 304; /* 封面边长(方形) */
|
||
if (cs > W - 2 * APP_PAD) cs = W - 2 * APP_PAD;
|
||
g_cover_r = NR((W - cs) / 2, 64, cs, cs); /* 64..368 */
|
||
|
||
g_title_y = g_cover_r.y + g_cover_r.h + 32; /* 400 */
|
||
g_artist_y = g_title_y + 32; /* 432 */
|
||
g_qual_y = g_artist_y + 26; /* 458 */
|
||
g_times_y = g_qual_y + 26; /* 484 */
|
||
g_bar = NR(48, g_times_y + 20, W - 96, 4); /* 504 (h=4) */
|
||
g_ctl_y = g_bar.y + 64; /* 568 */
|
||
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
|
||
g_r_side = 24; g_r_play = 34;
|
||
g_dots_y = g_ctl_y + 62; /* 630 */
|
||
|
||
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
|
||
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
|
||
|
||
g_lyr_cx = H / 2; /* 360 */
|
||
g_lyr_lh = 30;
|
||
g_lyr_meta_y = 22;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 绘制物 */
|
||
/* ============================================================ */
|
||
|
||
/* 封面页静态带:方形大图 + 圆角 + 兜底占位。仅 歌曲/封面 变化才重画。 */
|
||
static void plr_draw_cover_static(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 0) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
meow_img *cov = cover_get();
|
||
if (cov) {
|
||
meow_ui_img_cover(g_cover_r.x, g_cover_r.y, g_cover_r.w, cov);
|
||
round_corners_bg(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h, 16, NCM_PLAYER_BG);
|
||
} else {
|
||
ncm_fill_round(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h, 16, pa->red_ctr);
|
||
meow_ui_text_center(g_cover_r.x + g_cover_r.w / 2,
|
||
g_cover_r.y + g_cover_r.h / 2 - 12, "\u266a", th->font_body, pa->red);
|
||
}
|
||
cover_put();
|
||
}
|
||
|
||
/* 封面页动态带:元信息 + 实际音质档 + 进度条/时间 + 控制钮 + 指示点 + 缓冲/错误态。
|
||
* tick 主要刷此带(进度/控制/元数据变化)。 */
|
||
static void plr_draw_cover_dyn(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 0) return;
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
int W = g_W;
|
||
ncm_song_t sm;
|
||
int have = (cur_song_meta(&sm) == 0);
|
||
const char *nm = have ? sm.name : "暂无播放";
|
||
const char *ar = have ? sm.art_name : "";
|
||
|
||
int st = player_manager_state();
|
||
int perr = player_manager_error();
|
||
|
||
/* 元信息(歌名/歌手) */
|
||
meow_ui_text_center(W / 2, g_title_y, ncm_text_fit(nm, W - 32, 28), 28, PLR_TEXT);
|
||
meow_ui_text_center(W / 2, g_artist_y, ncm_text_fit(ar, W - 32, 20), 20, PLR_TEXT_DIM);
|
||
|
||
/* §20 实际生效音质档(与用户档不同→红字「已降级至 xx」);缓冲/错误态不显示 */
|
||
int eff = player_manager_eff_level();
|
||
if (have && eff >= 0 && st != NCM_PC_STOPPED) {
|
||
char qb[72];
|
||
if (eff != g_st.level)
|
||
snprintf(qb, sizeof qb, "已降级至 %s", ncm_level_name(eff));
|
||
else
|
||
snprintf(qb, sizeof qb, "%s", ncm_level_name(eff));
|
||
uint32_t qc = (eff != g_st.level) ? pa->red_lit : PLR_TEXT_DIM;
|
||
meow_ui_text_center(W / 2, g_qual_y, qb, 18, qc);
|
||
}
|
||
|
||
/* 缓冲/错误态(沿用 §18 三态;错误时用文案替代时间行) */
|
||
if (perr != 0) {
|
||
const char *em = (perr == NCM_ERR_VIP) ? "VIP 专享,开通会员后可播放"
|
||
: (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
|
||
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
|
||
: "加载失败,请检查网络";
|
||
meow_ui_text_center(W / 2, g_times_y - 8, em, 20, pa->red_lit);
|
||
meow_ui_text_center(W / 2, g_times_y + 18, "点播放键重试 / 切下一首", 18, PLR_TEXT_DIM);
|
||
} else {
|
||
/* 进度条 + 时间 */
|
||
int pos = (int)player_manager_pos_ms();
|
||
int dur = (int)player_manager_dur_ms();
|
||
if (dur <= 0 && have) dur = (int)sm.duration_ms;
|
||
int frac = dur > 0 ? (int)((int64_t)pos * 1000 / dur) : 0;
|
||
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
|
||
char lp[16], rp[16];
|
||
fmt_ms(pos, lp, sizeof lp);
|
||
fmt_ms(dur, rp, sizeof rp);
|
||
if (st == NCM_PC_BUFFER) {
|
||
int64_t dd = 0, dt = 0;
|
||
player_manager_dl(&dd, &dt);
|
||
char bbuf[48];
|
||
if (dt > 0) {
|
||
int pct = (int)(dd * 100 / dt);
|
||
if (pct > 100) pct = 100;
|
||
snprintf(bbuf, sizeof bbuf, "缓冲中… %d%%", pct);
|
||
} else if (dd > 0) {
|
||
snprintf(bbuf, sizeof bbuf, "缓冲中… %lldKB", (long long)(dd / 1024));
|
||
} else {
|
||
snprintf(bbuf, sizeof bbuf, "缓冲中…");
|
||
}
|
||
meow_ui_text_center(W / 2, g_times_y - 8, bbuf, 20, PLR_TEXT_DIM);
|
||
} else {
|
||
meow_ui_text(48, g_times_y, lp, 20, PLR_TEXT_DIM);
|
||
meow_ui_text_right(W - 48, g_times_y, rp, 20, PLR_TEXT_DIM);
|
||
}
|
||
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, PLR_TRACK_BG);
|
||
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
|
||
}
|
||
|
||
/* 控制钮(MD3 图标) */
|
||
draw_ctrl_btn(NCM_I_PREV, g_prev_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
|
||
draw_ctrl_btn((st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? NCM_I_PLAY : NCM_I_PAUSE,
|
||
g_play_x, g_ctl_y, g_r_play, pa->red, pa->on_red);
|
||
draw_ctrl_btn(NCM_I_NEXT, g_next_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
|
||
|
||
/* 页面指示点 */
|
||
draw_page_dots(g_dots_y);
|
||
}
|
||
|
||
/* 歌词页(整屏带状物):封面模糊置底 + scrim + 顶部元信息 + 滚动歌词(当前行高亮居中 +
|
||
* 上下行渐隐)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
|
||
static void plr_draw_lyrics(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 1) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
int W = g_W, H = g_H;
|
||
|
||
/* 底:封面等比居中放大(contain:等比缩放到屏内最大、居中;四周主题底),无封面 → 沉浸底。
|
||
* 不拉伸铺满(方图硬拉 480×720 变形丑,2026-09-02 用户报)。
|
||
* ⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏 → 下次 paint 自锁死,§18)。 */
|
||
meow_img *cov = cover_get();
|
||
meow_ui_fill_rect(0, 0, W, H, NCM_PLAYER_BG);
|
||
if (cov) {
|
||
int cw = meow_img_w(cov), ch = meow_img_h(cov);
|
||
if (cw > 0 && ch > 0) {
|
||
int dw = W, dh = (int)((long)ch * W / cw);
|
||
if (dh > H) { dh = H; dw = (int)((long)cw * H / ch); }
|
||
meow_ui_img_scaled((W - dw) / 2, (H - dh) / 2, dw, dh, cov);
|
||
}
|
||
}
|
||
cover_put();
|
||
/* 半透明叠加层:default=主题 scrim(日/夜跟随主题);power_save=黑色更重(§22)。 */
|
||
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
|
||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 195);
|
||
else
|
||
meow_ui_fill_rect_alpha(0, 0, W, H, th->scrim, 150);
|
||
|
||
/* 顶部元信息(title · artist,Apple Music 式顶部小字) */
|
||
ncm_song_t sm;
|
||
if (cur_song_meta(&sm) == 0) {
|
||
char mb[300];
|
||
snprintf(mb, sizeof mb, "%s / %s", sm.name, sm.art_name);
|
||
meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT);
|
||
}
|
||
|
||
/* 歌词:当前行居中高亮;上下行渐隐(距中心越远越接近底色)。 */
|
||
int n = lyric_count();
|
||
int64_t ps = player_manager_pos_ms();
|
||
int cur = (n > 0) ? lyric_line_at(ps) : -1;
|
||
int cy = g_lyr_cx;
|
||
int lo = g_lyr_meta_y + 44, hi = g_dots_y - 6; /* 歌词窗口(避顶部元信息/底部指示点) */
|
||
for (int i = cur - 7; i <= cur + 6; i++) {
|
||
if (i < 0 || i >= n) continue;
|
||
int yy = cy + (i - cur) * g_lyr_lh;
|
||
if (yy < lo || yy + g_lyr_lh > hi) continue;
|
||
int is_cur = (i == cur);
|
||
int dist = (i - cur) < 0 ? (cur - i) : (i - cur);
|
||
int fade = dist * 42; /* 0..255,越远越淡 */
|
||
if (fade > 214) fade = 214; /* 远行仍隐约可见,不全隐 */
|
||
uint32_t base = is_cur ? pa->red_lit : PLR_TEXT;
|
||
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? 0x000000 : th->scrim;
|
||
uint32_t col = mix_c(base, bgcol, fade);
|
||
int size = is_cur ? 24 : 20;
|
||
meow_ui_text_center(W / 2, yy, ncm_text_fit(lyric_line_text(i), W - 24, size), size, col);
|
||
if (is_cur) {
|
||
const char *tl = lyric_line_tl(i);
|
||
if (tl && tl[0])
|
||
meow_ui_text_center(W / 2, yy + 28, ncm_text_fit(tl, W - 24, 18), 18,
|
||
mix_c(pa->red_lit, bgcol, 120));
|
||
}
|
||
}
|
||
if (n == 0) {
|
||
meow_ui_text_center(W / 2, cy - 10,
|
||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
|
||
}
|
||
|
||
draw_page_dots(g_dots_y);
|
||
}
|
||
|
||
/* 按当前页注册两个绘制物(封面页=静态带+动态带;歌词页=整屏带状物)。 */
|
||
static void player_apply_bands(void)
|
||
{
|
||
if (g_pstatic) { meow_draw_unregister(g_pstatic); g_pstatic = 0; }
|
||
if (g_pdyn) { meow_draw_unregister(g_pdyn); g_pdyn = 0; }
|
||
if (g_ppage == 0) {
|
||
g_pstatic = meow_draw_custom_register(8, g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h,
|
||
plr_draw_cover_static, NULL);
|
||
g_pdyn = meow_draw_custom_register(9, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
|
||
plr_draw_cover_dyn, NULL);
|
||
} else {
|
||
g_pdyn = meow_draw_custom_register(9, 0, 0, g_W, g_H, plr_draw_lyrics, NULL);
|
||
}
|
||
}
|
||
|
||
static void player_static_invalidate(void)
|
||
{
|
||
if (g_ppage == 0 && g_pstatic)
|
||
meow_ui_invalidate(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h);
|
||
else if (g_ppage == 1 && g_pdyn)
|
||
meow_ui_invalidate(0, 0, g_W, g_H); /* 封面/背景变化 → 整屏重画歌词页 */
|
||
}
|
||
|
||
static void player_dyn_invalidate(void)
|
||
{
|
||
if (g_ppage == 0 && g_pdyn)
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||
}
|
||
|
||
/* 切页:只改页面 + 重新注册带状物 + 整屏重画(切页低频,可全量)。 */
|
||
static void player_page_go(int dir)
|
||
{
|
||
int target = g_ppage + dir;
|
||
if (target < 0) target = 0;
|
||
if (target > 1) target = 1;
|
||
if (target == g_ppage) return;
|
||
g_ppage = target;
|
||
player_apply_bands();
|
||
meow_ui_paint_full();
|
||
}
|
||
|
||
int player_is_active(void) { return g_plc.active; }
|
||
int player_screen(void) { return 0; } /* 重做后恒竖屏 */
|
||
int player_page(void) { return g_ppage; }
|
||
|
||
int player_set_screen(int landscape) { (void)landscape; return 0; }
|
||
|
||
int player_set_page(int page)
|
||
{
|
||
int old = g_ppage;
|
||
if (page < 0) page = 0;
|
||
if (page > 1) page = 1;
|
||
if (page != g_ppage) {
|
||
g_ppage = page;
|
||
player_apply_bands();
|
||
meow_ui_paint_full();
|
||
}
|
||
return old;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 输入 */
|
||
/* ============================================================ */
|
||
|
||
static int in_circle(int cx, int cy, int r, int x, int y); /* 前向声明(供 touch_down 判交互区) */
|
||
|
||
/* 中部横滑跟踪:起点不在左缘(>24)且不在顶部热区才记录(左缘归返回、顶部归通知中心)。
|
||
* 封面页在「进度条/控制钮」上按下不锁 page-drag(否则拖进度条被误判为翻页,§22)。 */
|
||
void player_touch_down(int x, int y)
|
||
{
|
||
g_touch_sx = x; g_touch_sy = y;
|
||
int interactive = 0;
|
||
if (g_ppage == 0) {
|
||
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w)
|
||
interactive = 1;
|
||
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y) ||
|
||
in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y) ||
|
||
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y))
|
||
interactive = 1;
|
||
}
|
||
g_touch_on = (x > PLR_EDGE_W) && (y >= meow_ui_statusbar_hotzone()) && !interactive;
|
||
g_drag_latched = 0; g_drag_dx = 0;
|
||
}
|
||
|
||
void player_touch_move(int x, int y)
|
||
{
|
||
if (!g_touch_on) return;
|
||
int dx = x - g_touch_sx, dy = y - g_touch_sy;
|
||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||
if (adx >= PLR_SWIPE_MS && adx > ady) { g_drag_latched = 1; g_drag_dx = dx; }
|
||
}
|
||
|
||
static void player_reset_touch(void)
|
||
{
|
||
g_touch_on = 0; g_drag_latched = 0; g_drag_dx = 0;
|
||
}
|
||
|
||
/* 歌词页:点行 seek(把 tap y 归到最近一行的下标)。 */
|
||
static int lyrics_line_tap(int y)
|
||
{
|
||
int n = lyric_count();
|
||
if (n <= 0) return -1;
|
||
int64_t ps = player_manager_pos_ms();
|
||
int cur = lyric_line_at(ps);
|
||
int off = y - g_lyr_cx;
|
||
int dioff = (off >= 0) ? (off + g_lyr_lh / 2) / g_lyr_lh : (off - g_lyr_lh / 2) / g_lyr_lh;
|
||
int i = cur + dioff;
|
||
if (i < 0) i = 0;
|
||
if (i >= n) i = n - 1;
|
||
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。 */
|
||
if (y < g_lyr_meta_y + 44 || y > g_dots_y - 6) return -1;
|
||
return i;
|
||
}
|
||
|
||
static int in_circle(int cx, int cy, int r, int x, int y)
|
||
{
|
||
int dx = x - cx, dy = y - cy;
|
||
return (dx * dx + dy * dy) <= (r * r);
|
||
}
|
||
|
||
void player_on_tap(int x, int y)
|
||
{
|
||
/* 中部横滑已被 touch_move 锁存:on_tap 是「尾部抬手(中部左滑落 TAP)」→ 判页切换。 */
|
||
if (g_drag_latched) {
|
||
int dir = (g_drag_dx < 0) ? 1 : -1; /* 左滑→下一页(歌词);右滑→上一页(封面) */
|
||
player_reset_touch();
|
||
player_page_go(dir);
|
||
return;
|
||
}
|
||
player_reset_touch();
|
||
|
||
if (g_ppage == 0) {
|
||
/* 播放/暂停:PAUSED/STOPPED(出错难重试)→ 播放;否则暂停 */
|
||
if (in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y)) {
|
||
int st = player_manager_state();
|
||
if (st == NCM_PC_PAUSED) player_manager_pause(0);
|
||
else if (st == NCM_PC_STOPPED && cur_song() >= 0) player_manager_play(cur_song());
|
||
else player_manager_pause(1);
|
||
return;
|
||
}
|
||
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(-1); return; }
|
||
if (in_circle(g_next_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(1); return; }
|
||
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
|
||
int64_t dur = player_manager_dur_ms();
|
||
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
|
||
return;
|
||
}
|
||
} else {
|
||
int line = lyrics_line_tap(y);
|
||
if (line >= 0) {
|
||
long t = lyric_line_time(line);
|
||
if (t >= 0) player_seek(t);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void player_on_swipe(int dir, int dist)
|
||
{
|
||
(void)dist;
|
||
player_reset_touch();
|
||
if (dir == MEOW_SWIPE_RIGHT) player_page_go(-1); /* 中部右滑 → 封面页(上一页) */
|
||
/* MEOW_SWIPE_UP/DOWN 与左缘返回(SWIPE_LEFT 在 app_on_swipe 层已消费)不处理 */
|
||
}
|
||
|
||
void player_play_song(int idx)
|
||
{
|
||
if (idx < 0 || idx >= g_st.track_ids_count) return;
|
||
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:歌单窗口队列 */
|
||
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
|
||
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
|
||
* §18:g_st.cover_url 只作「歌单封面兜底」。 */
|
||
ncm_song_t s;
|
||
const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0])
|
||
? s.al_pic : g_st.cover_url;
|
||
player_manager_play(idx);
|
||
appstate_set_playing_song(idx, NULL);
|
||
cover_request(cover, s.id); /* 曲目专辑封面:带 id,死链时 song/detail 兜底(REPORT §NEW 任务1) */
|
||
g_ppage = 0; /* 新曲 → 回封面页 */
|
||
meow_view_page_push(ACT_PLAYER);
|
||
}
|
||
|
||
/* 搜索单曲起播(§23):走搜索队列(search_songs[]),不覆写歌单窗口;
|
||
* 上/下一首在搜索结果集内循环,回退原歌单时窗口上下文不丢。 */
|
||
void player_play_search_song(int i)
|
||
{
|
||
if (i < 0 || i >= g_st.search_song_count) return;
|
||
g_st.queue_src = NCM_QUEUE_SEARCH;
|
||
ncm_song_t s;
|
||
if (appstate_search_song(i, &s) != 0) return;
|
||
const char *cover = s.al_pic[0] ? s.al_pic : g_st.cover_url;
|
||
player_manager_play(i);
|
||
appstate_set_playing_song(i, NULL);
|
||
cover_request(cover, s.id);
|
||
g_ppage = 0;
|
||
meow_view_page_push(ACT_PLAYER);
|
||
}
|
||
|
||
void player_pause_toggle(int on)
|
||
{
|
||
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
|
||
else player_manager_pause(1);
|
||
(void)on;
|
||
}
|
||
|
||
void player_next(int dir) { player_manager_next(dir); }
|
||
void player_seek(int64_t ms) { player_manager_seek(ms); }
|
||
|
||
/* tick:封面页只标脏「动态带」;歌词页仅在 换行/切歌/封面 变化才整屏重画(不逐帧滚动)。
|
||
* snack/错误沿用 chrome_tick + 错误沿。 */
|
||
void player_tick(uint64_t now)
|
||
{
|
||
(void)now;
|
||
chrome_tick();
|
||
if (!g_plc.active) return;
|
||
|
||
int sc = cur_song();
|
||
int pos = (int)player_manager_pos_ms();
|
||
int st = player_manager_state();
|
||
|
||
/* 元数据 id 变化(含快照滞后 settle)→ 请求新曲封面 + 刷静态带(封面)与动态带(歌名) */
|
||
ncm_song_t s2;
|
||
if (sc >= 0 && cur_song_meta(&s2) == 0 && strcmp(s2.id, g_last_meta_id) != 0) {
|
||
ncm_strlcpy(g_last_meta_id, s2.id, sizeof g_last_meta_id);
|
||
cover_request(s2.al_pic[0] ? s2.al_pic : g_st.cover_url, s2.id);
|
||
player_static_invalidate();
|
||
player_dyn_invalidate();
|
||
}
|
||
if (sc != g_last_song) {
|
||
g_last_song = sc;
|
||
player_dyn_invalidate();
|
||
}
|
||
if (cover_changed()) {
|
||
cover_ack();
|
||
player_static_invalidate(); /* 封面页重画封面;歌词页整屏重画背景 */
|
||
}
|
||
|
||
int perr = player_manager_error();
|
||
if (perr != g_last_err) {
|
||
g_last_err = perr;
|
||
if (perr == NCM_ERR_VIP) ncm_snack("VIP 专享歌曲");
|
||
else if (perr) ncm_snack("播放失败");
|
||
player_dyn_invalidate();
|
||
}
|
||
|
||
if (g_ppage == 0) {
|
||
if (pos != g_last_pos_ms || st != g_last_state) {
|
||
g_last_pos_ms = pos;
|
||
g_last_state = st;
|
||
player_dyn_invalidate();
|
||
}
|
||
int eff = player_manager_eff_level();
|
||
if (eff != g_last_eff) {
|
||
g_last_eff = eff;
|
||
player_dyn_invalidate();
|
||
}
|
||
int64_t dd = 0, dtot = 0;
|
||
player_manager_dl(&dd, &dtot);
|
||
int dq = (int)(dd / 2048);
|
||
if (dq != g_last_dl && st == NCM_PC_BUFFER) {
|
||
g_last_dl = dq;
|
||
player_dyn_invalidate();
|
||
}
|
||
} else {
|
||
/* 歌词页:仅在「当前行」变化时整屏重画(bg+歌词;行间静止,不逐帧刷)。 */
|
||
int line = (lyric_count() > 0) ? lyric_line_at(pos) : -1;
|
||
if (line != g_last_line) {
|
||
g_last_line = line;
|
||
meow_ui_invalidate(0, 0, g_W, g_H);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 页面生命周期 */
|
||
/* ============================================================ */
|
||
|
||
static void plr_page_create(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((player_ctrl *)ctx)->view = v;
|
||
player_compute_geom();
|
||
meow_ui_activity_push(ACT_PLAYER); /* owner=ACT_PLAYER */
|
||
meow_ui_activity_pop();
|
||
g_ppage = 0;
|
||
player_apply_bands();
|
||
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
|
||
g_last_dl = -1; g_last_err = 0; g_last_eff = -2; /* -2: 与 eff=-1(未定) 区分,首 tick 必刷 */
|
||
g_last_meta_id[0] = 0;
|
||
}
|
||
|
||
static void plr_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((player_ctrl *)ctx)->active = 1;
|
||
meow_ui_set_page_bg(NCM_PLAYER_BG); /* 播放页沉浸底恒 NCM_PLAYER_BG(§22 不动沉浸底) */
|
||
/* §18 自愈:重进播放页强制重估(切歌/封面失败后回来无重评估路径) */
|
||
g_last_meta_id[0] = 0;
|
||
g_last_song = -1;
|
||
g_last_dl = -1;
|
||
player_apply_bands();
|
||
meow_ui_paint_full();
|
||
if (!g_host_mock) player_manager_load_lyric();
|
||
}
|
||
|
||
static void plr_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((player_ctrl *)ctx)->active = 0;
|
||
}
|
||
|
||
static void plr_page_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
if (g_pstatic) { meow_draw_unregister(g_pstatic); g_pstatic = 0; }
|
||
if (g_pdyn) { meow_draw_unregister(g_pdyn); g_pdyn = 0; }
|
||
}
|
||
|
||
meow_view *player_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_create = plr_page_create,
|
||
.on_enter = plr_page_enter,
|
||
.on_leave = plr_page_leave,
|
||
.on_destroy = plr_page_destroy,
|
||
};
|
||
return meow_view_create(ACT_PLAYER, MEOW_VIEW_ACTIVITY, &h, &g_plc);
|
||
}
|