Files

927 lines
43 KiB
C
Raw Permalink 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 滚动(活动行纯白偏上锚定,上下行逐级灰阶)+ 点行跳转;默认=封面模糊置底
* (低透明非纯黑中性叠加层,需求1 加深),省电=封面背景+黑色半透明叠加层
* (设置 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 一致) */
/* 歌词页布局(§22 重做批):左边距 / 子行高 / 可见行数上限 / 折行段缓冲。
* 超宽歌词折行显示(中西文兼容)+ 活动行高亮(纯白)偏上锚定 + 上下行逐级灰阶,
* 顶部元信息与歌词正文同左边距。 */
#define LYR_PAD 24 /* 歌词页左边距(顶部元信息与歌词正文一致) */
#define LYR_SUB_LH 44 /* 一个歌词条目(含折行)的子行高 */
#define LYR_NAME_SZ 28 /* 顶部歌名字号 */
#define LYR_ART_SZ 20 /* 顶部歌手字号 */
#define LYR_CUR_SZ 28 /* 当前歌词行字号 */
#define LYR_OTH_SZ 24 /* 上下行歌词字号 */
#define LYR_TL_SZ 20 /* 当前行翻译字号 */
#define LYR_MAX_ROWS 9 /* 歌词页可见视觉行数上限(含折行,≤9) */
#define LYR_SEG_SZ 256 /* 折行段缓冲 */
/* 需求3 活动行锚定:块中心相对屏中线偏上 LYR_ABOVE_OFF px;上方至少 LYR_ABOVE_MIN 行已过歌词。 */
#define LYR_ABOVE_OFF 50
#define LYR_ABOVE_MIN 2
/* 需求1 叠加层加深(非纯黑,保留封面微透出 → 歌词清晰、封面隐约):
* default=近黑中性,alpha 132→176、色值略暗;power_save=纯黑 alpha 195→200。 */
#define LYR_SCRIM_D 0x10101A /* 默认层暗色 scrim */
#define LYR_SCRIM_L 0x201A16 /* 默认层亮色 scrim(暖黑) */
#define LYR_SCRIM_PS 0x000000 /* 省电层 scrim(纯黑) */
#define LYR_SCRIM_A 176
#define LYR_SCRIM_PS_A 200
/* 需求2 歌词行灰阶:按距活动行行距 0..5 分级(0=活动行纯白,越远越灰;最远可见 0x555555)。
* 锚点对照用户需求:±1 行 0xBBBBBB、中深 0x808080、最远 0x555555,逐级单调变灰。 */
static const uint32_t lyr_gray_tbl[6] = {
0xFFFFFF, 0xBBBBBB, 0x929292, 0x808080, 0x6A6A6A, 0x555555
};
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_mode_x, g_queue_x, g_r_side, g_r_play, g_dots_y;
/* §NEW 模式/队列钮:三键左(mode,prev 更左)/ 三键右(queue,next 更右)。间距 60 保不遮挡。 */
#define GAP_SIDE 60
#define MODE_BTN_R (g_r_side) /* 与侧钮同半径(命中取 CTL_RR) */
#define MODE_LBL_SZ 14 /* 模式/队列钮文字字号(两字,居中) */
/* ---- 歌词页几何 ---- */
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
static int g_lyr_anchor; /* 活动行块中心 y(屏中线偏上,需求3) */
static int g_lyr_meta_y; /* 顶部元信息 y(避开状态栏) */
/* 歌词布局暂存(每次 plr_draw_lyrics 重建;UI 线程串行,静态复用) */
static char s_lyr_seg[LYR_MAX_ROWS][LYR_SEG_SZ]; /* 折行段复用缓冲(一次画一个条目) */
static int s_lyr_ent[LYR_MAX_ROWS]; /* 可见条目 → 歌词行下标 */
static int s_lyr_rw[LYR_MAX_ROWS]; /* 条目原文折行段数(画原文;当前行翻译紧随其后) */
static int s_lyr_bh[LYR_MAX_ROWS]; /* 条目块高(子行数)= 原文折行数 + 当前行翻译占 1 行 */
static int s_lyr_sz[LYR_MAX_ROWS]; /* 条目标题字号 */
static int s_lyr_y[LYR_MAX_ROWS]; /* 条目块顶 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);
}
/* 折行:把 txt 按「词/字符」折成 ≤max_rows 段,每段 ≤maxw px。中文按字符、英文按词断行;
* 宽测量用 meow_ui_text_width(宿主/生产同口径)。段写入 seg[][LYR_SEG_SZ],返回段数(≥1)。 */
static int lyr_wrap_lines(const char *txt, int size, int maxw,
char seg[][LYR_SEG_SZ], int max_rows)
{
if (!txt || !txt[0] || max_rows <= 0) { seg[0][0] = 0; return 1; }
int n = 0;
size_t len = strlen(txt), pos = 0;
while (pos < len && n < max_rows) {
size_t start = pos;
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
int overflow = 0;
while (pos < len) {
unsigned char c = (unsigned char)txt[pos];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
if (pos + (size_t)ch > len) ch = (int)(len - pos);
size_t cand = (pos + (size_t)ch) - start;
size_t cp = cand < LYR_SEG_SZ - 1 ? cand : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
if (meow_ui_text_width(seg[n], size) > maxw && cand > 0) { overflow = 1; break; }
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
pos += (size_t)ch;
}
if (overflow) {
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
unsigned char c = (unsigned char)txt[start];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
linelen = (size_t)ch;
}
size_t cp = linelen < LYR_SEG_SZ - 1 ? linelen : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
pos = start + linelen;
if (last_ws > 0) {
size_t sp = start + (size_t)last_ws + 1;
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
}
n++;
} else {
size_t cp = (len - start) < LYR_SEG_SZ - 1 ? (len - start) : LYR_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
n++;
break;
}
}
if (n == 0) { seg[0][0] = 0; n = 1; }
return n;
}
/* 歌词可见条目布局:活动行块中心锚定在 g_lyr_anchor(屏中线偏上,需求3 中间偏上),
* 活动行上方保底 ≥LYR_ABOVE_MIN 行已过歌词,下方填满,总视觉行数 ≤ LYR_MAX_ROWS(≤9)。
* 条目块高 s_lyr_bh(子行数)= 原文折行数 s_lyr_rw + 当前行翻译占 1 个子行(其余条目无翻译,bh=rw)。
* 结果写 s_lyr_ent/rw/bh/sz/y(s_lyr_y 自上向下排好;活动行(白色原文)中心 = g_lyr_anchor)。 */
static int lyr_layout(int n, int cur, int maxw)
{
/* 清静态暂存(防帧间残留:上帧条目数 > 本周,绘制循环读到陈旧 s_lyr_* 越界复用) */
for (int k = 0; k < LYR_MAX_ROWS; k++) {
s_lyr_rw[k] = 0; s_lyr_bh[k] = 0; s_lyr_ent[k] = -1; s_lyr_y[k] = 0;
}
if (cur < 0 || cur >= n) return 0;
int rc = lyr_wrap_lines(lyric_line_text(cur), LYR_CUR_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (rc > LYR_MAX_ROWS) rc = LYR_MAX_ROWS;
/* 当前行翻译(次级色小字)紧随其原文行、占块高 1 个子行:块高必须含它,否则下一歌词条目
* 被排到与翻译重叠的 y(英文歌原文折行多 → 翻译叠进下一行被压住,Bug ②)。 */
const char *tl = lyric_line_tl(cur);
int has_tl = (tl && tl[0]) ? 1 : 0;
int rc_tot = rc + has_tl; /* 活动行块总子行数(原文折行 + 翻译行) */
int cap = LYR_MAX_ROWS - rc_tot; /* 活动行之外可用的视觉行数 */
if (cap < 0) cap = 0;
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
if (above_want < LYR_ABOVE_MIN) above_want = (cur >= LYR_ABOVE_MIN) ? LYR_ABOVE_MIN : cur;
if (above_want > cap) above_want = cap; /* 总行数 ≤ LYR_MAX_ROWS */
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
int a_cnt = 0, usedA = 0;
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (usedA + rr > above_want) break;
for (int k = a_cnt; k > 0; k--) {
s_lyr_ent[k] = s_lyr_ent[k - 1]; s_lyr_rw[k] = s_lyr_rw[k - 1];
s_lyr_bh[k] = s_lyr_bh[k - 1];
s_lyr_sz[k] = s_lyr_sz[k - 1];
}
s_lyr_ent[0] = i; s_lyr_rw[0] = rr; s_lyr_bh[0] = rr; s_lyr_sz[0] = LYR_OTH_SZ;
a_cnt++; usedA += rr;
}
/* 2) 当前行(下标 a_cnt):原文 rc 段 + 翻译 1 段(有翻译时) */
s_lyr_ent[a_cnt] = cur; s_lyr_rw[a_cnt] = rc; s_lyr_bh[a_cnt] = rc_tot; s_lyr_sz[a_cnt] = LYR_CUR_SZ;
/* 3) 下方(cur+1 向下):余量 = cap - actual 上方行数 */
int below_want = cap - usedA;
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur; /* 下方行数上限 */
int b_cnt = 0, usedB = 0;
for (int i = cur + 1; i < n && usedB < below_want; i++) {
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
if (usedB + rr > below_want) break;
int k = a_cnt + 1 + b_cnt;
s_lyr_ent[k] = i; s_lyr_rw[k] = rr; s_lyr_bh[k] = rr; s_lyr_sz[k] = LYR_OTH_SZ;
b_cnt++; usedB += rr;
}
int cnt = a_cnt + 1 + b_cnt;
if (cnt == 0) return 0;
/* 4) y 位置:活动行(白色原文)中心 = g_lyr_anchor;上方向上、下方向下加(块间无缝隙)。
* 块高用 s_lyr_bh(含翻译行),使下一条目始于翻译行之后(不重叠)。 */
s_lyr_y[a_cnt] = g_lyr_anchor - rc * LYR_SUB_LH / 2;
for (int k = a_cnt + 1; k < cnt; k++)
s_lyr_y[k] = s_lyr_y[k - 1] + s_lyr_bh[k - 1] * LYR_SUB_LH;
for (int k = a_cnt - 1; k >= 0; k--)
s_lyr_y[k] = s_lyr_y[k + 1] - s_lyr_bh[k] * LYR_SUB_LH;
return cnt;
}
/* 颜色插值(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);
}
/* 文字钮:圆形底 + 居中两字文案(§NEW 模式/队列钮)。无逐帧三角行扫,纯圆形填充。 */
static void draw_text_btn(const char *label, 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);
}
if (label && label[0])
meow_ui_text_center(cx, cy - MODE_LBL_SZ / 2, label, MODE_LBL_SZ, 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 */
/* §22 时间行:放大字号并下移到进度条下方,不与进度条相交。
* 布局:进度条(484,h=4) → 时间行(500, font 24) → 控制钮(564)。 */
g_bar = NR(48, g_qual_y + 26, W - 96, 4); /* 484 (h=4) */
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
g_ctl_y = g_times_y + 64; /* 564 */
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
g_mode_x = g_prev_x - GAP_SIDE; /* 播放模式三态钮(prev 更左) */
g_queue_x = g_next_x + GAP_SIDE; /* 当前播放队列钮(next 更右) */
g_r_side = 24; g_r_play = 34;
g_dots_y = g_ctl_y + 62; /* 626 */
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
/* 歌词页几何:中心线 / 活动行块中心(屏中线偏上,需求3)/ 顶部元信息 y。 */
g_lyr_cx = H / 2; /* 360 */
g_lyr_anchor = g_lyr_cx - LYR_ABOVE_OFF; /* 310 活动行偏上锚定 */
g_lyr_meta_y = ncm_statusbar() + 20;
}
/* ============================================================ */
/* 绘制物 */
/* ============================================================ */
/* 封面页静态带:方形大图 + 圆角 + 兜底占位。仅 歌曲/封面 变化才重画。 */
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 {
/* §22 时间字号放大(20→24)且下移到进度条下方,不再与进度条相交 */
meow_ui_text(48, g_times_y, lp, 24, PLR_TEXT_DIM);
meow_ui_text_right(W - 48, g_times_y, rp, 24, 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);
/* §NEW 播放模式三态钮(三键左,prev 更左)+ 当前播放队列钮(三键右,next 更右):
* 文案反映当前模式(随机/列表/单曲);队列钮进入实时播放列表。 */
draw_text_btn(ncm_mode_name(player_manager_play_mode()), g_mode_x, g_ctl_y,
MODE_BTN_R, PLR_CTRL_BG, PLR_CTRL_FG);
draw_text_btn("\u961f\u5217", g_queue_x, g_ctl_y, MODE_BTN_R, 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;
int W = g_W, H = g_H;
/* 底:封面铺满全屏(cover-fit,四周无黑边;无 GPU 高斯模糊,用低透明中性叠层近似「模糊底」),
* 无封面 → 主题中性底(非纯黑)。⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏,
* 下次 paint 自锁死,§18)。§22:default=封面+半透明中性叠层;power_save=封面+黑色更重叠层。 */
meow_img *cov = cover_get();
meow_ui_fill_rect(0, 0, W, H, 0x1A1A20); /* 无封面兜底主题底(中性深灰,非纯黑) */
if (cov) {
int cw = meow_img_w(cov), ch = meow_img_h(cov);
if (cw > 0 && ch > 0) {
long dw = W, dh = (long)ch * W / cw; /* cover-fit:铺满宽度,高按等比超出裁剪 */
if (dh < H) { dh = H; dw = (long)cw * H / ch; }
meow_ui_img_scaled((W - (int)dw) / 2, (H - (int)dh) / 2, (int)dw, (int)dh, cov);
}
}
cover_put();
/* 半透明叠加层:default=主题中性(近黑但不纯黑,封面色彩微透出 → 模糊底观感);
* power_save=黑色更重(§22 保留原「封面背景+黑叠加」语义)。需求1 加深:alpha/色值加暗,
* 但仍保留「封面隐约」而非纯黑整体吞掉。 */
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
meow_ui_fill_rect_alpha(0, 0, W, H, LYR_SCRIM_PS, LYR_SCRIM_PS_A);
else
meow_ui_fill_rect_alpha(0, 0, W, H, g_dark ? LYR_SCRIM_D : LYR_SCRIM_L, LYR_SCRIM_A);
/* 顶部元信息(两行居左):第一行歌名(大字)、第二行歌手名(小字,多歌手保留已拼串)。
* 与歌词正文同左边距 LYR_PAD;下移避开状态栏(g_lyr_meta_y = statusbar + 20)。 */
ncm_song_t sm;
if (cur_song_meta(&sm) == 0) {
meow_ui_text(LYR_PAD, g_lyr_meta_y,
ncm_text_fit(sm.name, W - 2 * LYR_PAD, LYR_NAME_SZ), LYR_NAME_SZ, PLR_TEXT);
meow_ui_text(LYR_PAD, g_lyr_meta_y + LYR_NAME_SZ + 6,
ncm_text_fit(sm.art_name, W - 2 * LYR_PAD, LYR_ART_SZ), LYR_ART_SZ, PLR_TEXT_DIM);
}
/* 歌词:活动行纯白居中(含折行,块中心偏上锚定);上下行按行距逐级灰阶;≤LYR_MAX_ROWS。 */
int n = lyric_count();
int64_t ps = player_manager_pos_ms();
int cur = (n > 0) ? lyric_line_at(ps) : -1;
int maxw = W - 2 * LYR_PAD;
int cnt = lyr_layout(n, cur, maxw);
/* 渐隐目标色 = 歌词页背底层(与 default/power_save 叠层一致),供翻译行融入背景 */
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? LYR_SCRIM_PS
: (g_dark ? LYR_SCRIM_D : LYR_SCRIM_L);
for (int k = 0; k < cnt; k++) {
int i = s_lyr_ent[k], rows = s_lyr_rw[k], sz = s_lyr_sz[k], y = s_lyr_y[k];
int is_cur = (i == cur);
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur)); /* 距活动行行距 */
uint32_t col = lyr_gray_tbl[dist > 5 ? 5 : dist]; /* 0=纯白,越远越灰 */
int rr = lyr_wrap_lines(lyric_line_text(i), sz, maxw, s_lyr_seg, rows);
if (rr > rows) rr = rows;
for (int r = 0; r < rr; r++)
meow_ui_text(LYR_PAD, y + r * LYR_SUB_LH, s_lyr_seg[r], sz, col);
if (is_cur) {
const char *tl = lyric_line_tl(i);
if (tl && tl[0])
meow_ui_text(LYR_PAD, y + rows * LYR_SUB_LH + 2,
ncm_text_fit(tl, W - 2 * LYR_PAD, LYR_TL_SZ), LYR_TL_SZ,
mix_c(0xFFFFFF, bgcol, 120));
}
}
if (n == 0) {
meow_ui_text(LYR_PAD, g_lyr_cx - 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) ||
in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y) ||
in_circle(g_queue_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 命中最临近行;
* 活动行已偏上锚定,非屏中线,故按 s_lyr_y/bh 逐块扫描而非 cx 偏移)。 */
static int lyrics_line_tap(int y)
{
int n = lyric_count();
if (n <= 0) return -1;
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。
* 元信息块 = 歌名(28)+6+歌手(20)+边距 → g_lyr_meta_y+66。 */
if (y < g_lyr_meta_y + 66 || y > g_dots_y - 6) return -1;
int64_t ps = player_manager_pos_ms();
int cur = lyric_line_at(ps);
int maxw = g_W - 2 * LYR_PAD;
int cnt = lyr_layout(n, cur, maxw);
if (cnt <= 0) return -1;
for (int k = 0; k < cnt; k++) {
int yb = s_lyr_y[k];
int yh = s_lyr_bh[k] * LYR_SUB_LH; /* 块高含当前行翻译,点翻译行归本行 */
if (y >= yb && y < yb + yh) return s_lyr_ent[k];
}
/* 落在可见块之外 → 就近取整(顶部元信息/底部指示点已在上面排除) */
return (y < s_lyr_y[0]) ? s_lyr_ent[0] : s_lyr_ent[cnt - 1];
}
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; }
/* §NEW 播放模式三态钮:点击循环切换(SHUFFLE→LISTLOOP→SINGLE→SHUFFLE)+ 持久化 */
if (in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y)) {
int m = player_manager_play_mode();
int nxt = (m + 1) % NCM_MODE_COUNT;
player_manager_set_play_mode(nxt);
appstate_apply_play_mode(nxt); /* 记住模式(退出后保留) */
player_dyn_invalidate();
return;
}
/* §NEW 当前播放队列钮:进实时播放列表页 */
if (in_circle(g_queue_x, g_ctl_y, CTL_RR, x, y)) { queue_open(); 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 只作「歌单封面兜底」。占位行直点(appstate_song_copy 失败)时
* s 未初始化,**不能**把 s.id 传给 cover_request(会带出一段野曲 id 参与兜底判冷却)——
* 仅物化成功才带曲目 id,否则 NULL(歌单封面,不带兜底)。 */
ncm_song_t s;
int have_s = (appstate_song_copy(idx, &s) == 0);
const char *cover = (have_s && s.al_pic[0]) ? s.al_pic : g_st.cover_url;
player_manager_play(idx);
appstate_set_playing_song(idx, NULL);
cover_request(cover, have_s ? s.id : NULL);
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);
}
/* 从「当前实时播放队列」第 idx 首播放(§NEW 队列页点播):保持当前 queue_src 不变
* (不把队列切回歌单/不覆写歌单窗口)——上/下一首仍在该队列内循环。封面用窗口/搜索数组
* 内的元数据(不触发网络),占位行由播放线程物化后快照接管。 */
void player_play_queue_song(int idx)
{
int cnt = appstate_queue_count();
if (idx < 0 || idx >= cnt) return;
ncm_song_t s;
int have = 0;
if (appstate_get_queue() == NCM_QUEUE_SEARCH) {
if (appstate_search_song(idx, &s) == 0) have = 1;
} else if (appstate_song_copy(idx, &s) == 0) {
have = 1;
}
const char *cover = have ? (s.al_pic[0] ? s.al_pic : g_st.cover_url) : g_st.cover_url;
player_manager_play(idx);
appstate_set_playing_song(idx, NULL);
if (have) 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);
}