/* 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 #include #include #include #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); }