925 lines
43 KiB
C
925 lines
43 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 滚动(活动行纯白偏上锚定,上下行逐级灰阶)+ 点行跳转;默认=封面模糊置底
|
||
* (低透明非纯黑中性叠加层,需求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 只作「歌单封面兜底」。 */
|
||
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);
|
||
}
|
||
|
||
/* 从「当前实时播放队列」第 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);
|
||
}
|