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

700 lines
29 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/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);
}