- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
+ level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
+ song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
+ vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
511 lines
23 KiB
C
511 lines
23 KiB
C
/* ui/player.c — 播放页(ACT_PLAYER):黑胶唱盘 + 唱针 + 进度/控制/滚动歌词(竖/横布局可切)。
|
||
*
|
||
* 黑胶(NCM_UI_DESIGN §5.1,软渲无圆形原语,全 fill_rect 行扫):
|
||
* ① 盘体 = 对每行 dy 取 half=√(R²-dy²) 画 fill_rect(同心圆)。
|
||
* ② 封面粉圆 = 方盖(S=R×√2)内切于圆 + 四角把「圆外-方外」两段盖回盘色 = 封面成圆。
|
||
* ③ 唱针 = 从盘心右上到盘缘一根 3px 唱臂 + 端头;播放压盘/暂停微抬(状态切换才画)。
|
||
* 盘恒黑(palette.disc),accent(进度已播/播放钮/歌词高亮)日夜换红明暗(palette.red/red_lit)。
|
||
*
|
||
* 性能(§6,专项):**分带刷新**——内容拆 盘带(仅圆盘) + 动态带(歌名/进度/控制/歌词/页脚) 两个绘制物,
|
||
* 各带独立且互不重叠 bbox。tick 只标脏「变化的那一条带」,盘带仅 歌曲/封面/播停态 变化才重画
|
||
* (修 player_tick 每 tick 刷整 r_cont 的隐患)。播放页恒沉浸(day 瓷白 / night 近黑,
|
||
* 不随系统明暗整体反转,仅 accent 换红明暗,§5.4)。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <math.h>
|
||
|
||
typedef struct { meow_view *view; int active; } player_ctrl;
|
||
static player_ctrl g_plc;
|
||
|
||
static meow_widget *g_tb;
|
||
static ncm_rect_t r_cont;
|
||
static int g_custom_disc, g_custom_dyn;
|
||
static int g_landscape;
|
||
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
|
||
static int g_last_dl, g_last_err; /* 缓冲进度粒度值 / 错误沿(§18) */
|
||
static int g_last_eff; /* 生效档沿(§20:降级标注出现/消失刷动态带) */
|
||
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
|
||
|
||
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
|
||
static ncm_rect_t g_disc_r, g_dyn_r; /* 盘带 / 动态带 bbox(不重叠) */
|
||
static int g_cx, g_cy, g_R, g_S; /* 盘心 / 半径 / 封面边长 */
|
||
static ncm_rect_t g_bar; /* 进度条 */
|
||
static int g_times_y;
|
||
static int g_ctl_y, g_cprev_x, g_cplay_x, g_cnext_x, g_cr;
|
||
static int g_title_y, g_artist_y, g_info_x, g_info_w;
|
||
static ncm_rect_t g_lyr; /* 歌词区 */
|
||
|
||
#define CTL_RR 30 /* 侧钮命中半径(命中容差) */
|
||
|
||
static int cur_song(void)
|
||
{
|
||
int c = player_manager_playing_song();
|
||
return (c >= 0 && c < g_st.track_ids_count) ? c : -1;
|
||
}
|
||
|
||
/* 当前播放曲元数据(§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);
|
||
}
|
||
|
||
static void tri(int x0, int y0, int h, int w, int dir, uint32_t color)
|
||
{
|
||
for (int i = 0; i < h; i++) {
|
||
int wi = 1 + i * w / h;
|
||
if (dir == 1) meow_ui_fill_rect(x0, y0 + i, wi, 1, color);
|
||
else meow_ui_fill_rect(x0 - wi + 1, y0 + i, wi, 1, color);
|
||
}
|
||
}
|
||
|
||
static void draw_ctl(int cx, int cy, int r, int kind, uint32_t base, uint32_t fg)
|
||
{
|
||
for (int i = 0; i < 2 * r; i++) {
|
||
int dy = i - r;
|
||
int half = (int)sqrt((double)(r * r - dy * dy));
|
||
if (half <= 0) continue;
|
||
meow_ui_fill_rect(cx - half, cy - r + i, 2 * half, 1, base);
|
||
}
|
||
int t = r / 2;
|
||
if (kind == 0) { meow_ui_fill_rect(cx - r - 2, cy - t, 4, 2*t, fg);
|
||
tri(cx + r - 4, cy - t, 2*t, 2*t, -1, fg); }
|
||
else if (kind == 1) { tri(cx - t, cy - t, 2*t, 2*t, 1, fg); }
|
||
else if (kind == 2) { meow_ui_fill_rect(cx - t - 2, cy - t, 4, 2*t, fg);
|
||
meow_ui_fill_rect(cx + t - 2, cy - t, 4, 2*t, fg); }
|
||
else { tri(cx - r + 4, cy - t, 2*t, 2*t, 1, fg);
|
||
meow_ui_fill_rect(cx + r - 2, cy - t, 4, 2*t, fg); }
|
||
}
|
||
|
||
static void arm_line(int x0, int y0, int x1, int y1, uint32_t color, int wpx)
|
||
{
|
||
int dx = x1 - x0, dy = y1 - y0;
|
||
int steps = (abs(dx) > abs(dy) ? abs(dx) : abs(dy)) / 2 + 1;
|
||
if (steps < 1) steps = 1;
|
||
for (int i = 0; i <= steps; i++) {
|
||
int x = x0 + dx * i / steps;
|
||
int y = y0 + dy * i / steps;
|
||
meow_ui_fill_rect(x - wpx / 2, y - wpx / 2, wpx, wpx, color);
|
||
}
|
||
}
|
||
|
||
/* 几何(竖/横布局)。带 bbox:盘带=圆盘矩形,动态带=其余(互不重叠)。 */
|
||
static void player_compute_geom(void)
|
||
{
|
||
r_cont = ncm_content_rect();
|
||
if (g_landscape) {
|
||
g_cx = 180; g_cy = 360; g_R = 120; g_S = (int)(g_R * 1.414);
|
||
g_info_x = 340; g_info_w = r_cont.x + r_cont.w - 16 - g_info_x;
|
||
g_title_y = 150; g_artist_y = 188;
|
||
g_bar = NR(g_info_x, 240, 108, 4); g_times_y = 226;
|
||
g_ctl_y = 310; g_cprev_x = 348; g_cplay_x = 404; g_cnext_x = 460; g_cr = 26;
|
||
g_lyr = NR(300, 372, 164, 280);
|
||
} else {
|
||
g_cx = 240; g_cy = 272; g_R = 104; g_S = (int)(g_R * 1.414);
|
||
g_info_x = 240; g_info_w = r_cont.w;
|
||
g_title_y = 388; g_artist_y = 422;
|
||
g_bar = NR(56, 456, 368, 4); g_times_y = 436;
|
||
g_ctl_y = 508; g_cprev_x = 176; g_cplay_x = 240; g_cnext_x = 304; g_cr = 30;
|
||
g_lyr = NR(16, 546, 448, 150);
|
||
}
|
||
int cb = r_cont.y + r_cont.h - APP_FOOTER_H;
|
||
if (g_lyr.y + g_lyr.h > cb) g_lyr.h = cb - g_lyr.y;
|
||
if (g_lyr.h < 0) g_lyr.h = 0;
|
||
if (g_landscape) {
|
||
g_disc_r = NR(g_cx - g_R, g_cy - g_R, 2*g_R, 2*g_R); /* 左盘区 */
|
||
int dy0 = g_title_y - 12;
|
||
g_dyn_r = NR(g_info_x - 20, dy0, r_cont.x + r_cont.w - (g_info_x - 20), cb - dy0 + 8);
|
||
} else {
|
||
g_disc_r = NR(0, g_cy - g_R, r_cont.w, 2*g_R); /* 全宽圆盘带 */
|
||
g_dyn_r = NR(0, g_cy + g_R, r_cont.w, cb - (g_cy + g_R)); /* 其余(含歌名/进度/歌词) */
|
||
}
|
||
}
|
||
|
||
/* 盘带绘制物:盘体 + 方盖内切圆(四角盖色) + 封面 + 唱针。仅 歌曲/封面/播停态 见变化才重画。 */
|
||
static void plr_draw_disc(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_PLAYER) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
int cx = g_cx, cy = g_cy, R = g_R, S = g_S;
|
||
int st = player_manager_state();
|
||
int paused = (st == NCM_PC_PAUSED);
|
||
|
||
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, pa->disc);
|
||
}
|
||
for (int rr = R - 3; rr >= R - 12; rr -= 4) {
|
||
for (int dy = -rr; dy <= rr; dy++) {
|
||
int half = (int)sqrt((double)(rr*rr - dy*dy));
|
||
if (half <= 0) continue;
|
||
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->disc_edge);
|
||
}
|
||
}
|
||
|
||
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。(cover_get 持锁,绘完 cover_put,§18) */
|
||
meow_img *cov = cover_get();
|
||
int hS = S / 2;
|
||
if (cov) meow_ui_img_cover(cx - hS, cy - hS, S, cov);
|
||
else {
|
||
for (int dy = -hS; dy <= hS; dy++) {
|
||
int half = (int)sqrt((double)(hS*hS - dy*dy));
|
||
if (half <= 0) continue;
|
||
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->red_ctr);
|
||
}
|
||
meow_ui_text_center(cx, cy - 10, "\u266a", th->font_body, pa->red);
|
||
}
|
||
cover_put();
|
||
for (int dy = -R; dy <= R; dy++) {
|
||
int half = (int)sqrt((double)(R*R - dy*dy));
|
||
if (half <= hS) continue;
|
||
meow_ui_fill_rect(cx - half, cy + dy, half - hS, 1, pa->disc);
|
||
meow_ui_fill_rect(cx + hS, cy + dy, half - hS + 1, 1, pa->disc);
|
||
}
|
||
meow_ui_fill_rect(cx - 2, cy - 2, 4, 4, pa->player_bg); /* 盘心孔 */
|
||
|
||
/* 唱针:播放压盘 / 暂停微抬 6px。 */
|
||
int arm_top = paused ? 6 : 0;
|
||
int ax0 = cx + 40, ay0 = cy - 70 + arm_top;
|
||
int ax1 = cx + R - 6, ay1 = cy + 8 + arm_top;
|
||
arm_line(ax0, ay0, ax1, ay1, th->outline, 3);
|
||
meow_ui_fill_rect(ax1 - 3, ay1 - 3, 6, 6, th->onSurface);
|
||
|
||
/* 高光(廉价):左下 45° 1px 弧。 */
|
||
for (int a = 0; a < 20; a++) {
|
||
double t = (a / 19.0) * (3.14159 / 2);
|
||
int hx = cx - (int)((R - 6) * 0.71 * cos(t));
|
||
int hy = cy + (int)((R - 6) * 0.71 * sin(t));
|
||
meow_ui_fill_rect_alpha(hx, hy, 2, 1, 0xFFFFFF, 60);
|
||
}
|
||
}
|
||
|
||
/* 动态带绘制物:歌名/歌手 + 进度条/时间 + 控制钮 + 滚动歌词 + 页脚。tick 主要刷此带。 */
|
||
static void plr_draw_dyn(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_PLAYER) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
ncm_song_t sm;
|
||
int have = (cur_song_meta(&sm) == 0);
|
||
const char *nm = have ? sm.name : "暂无播放";
|
||
const char *ar = have ? sm.art_name : "";
|
||
|
||
if (g_landscape) {
|
||
meow_ui_text(g_info_x, g_title_y, ncm_text_fit(nm, g_info_w, 28), 28, th->onSurface);
|
||
meow_ui_text(g_info_x, g_artist_y, ncm_text_fit(ar, g_info_w, 20), 20, th->onSurfaceVariant);
|
||
} else {
|
||
meow_ui_text_center(g_info_x, g_title_y, nm, 28, th->onSurface);
|
||
meow_ui_text_center(g_info_x, g_artist_y, ar, 20, th->onSurfaceVariant);
|
||
}
|
||
|
||
/* 进度条 + 时间 */
|
||
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 (g_landscape) {
|
||
meow_ui_text(g_bar.x, g_times_y, lp, 20, th->onSurfaceVariant);
|
||
meow_ui_text_right(g_bar.x + g_bar.w, g_times_y, rp, 20, th->onSurfaceVariant);
|
||
} else {
|
||
meow_ui_text(44, g_times_y, lp, 20, th->onSurfaceVariant);
|
||
meow_ui_text_right(r_cont.x + r_cont.w - 44, g_times_y, rp, 20, th->onSurfaceVariant);
|
||
}
|
||
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, th->surfaceVariant);
|
||
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
|
||
|
||
/* §20:实际生效音质档(与用户档不同时小字标注「已降级至 xx」;缓冲期未定不显示)。
|
||
* 位置:时间行中部(左右是时间戳,中央留白),竖/横屏同一条带。 */
|
||
{
|
||
int eff = player_manager_eff_level();
|
||
if (have && eff >= 0 && player_manager_state() != 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));
|
||
int qcx = g_landscape ? (g_bar.x + g_bar.w / 2) : (r_cont.x + r_cont.w / 2);
|
||
uint32_t qc = (eff != g_st.level) ? pa->red : th->onSurfaceVariant;
|
||
meow_ui_text_center(qcx, g_times_y, qb, 18, qc);
|
||
}
|
||
}
|
||
|
||
int st = player_manager_state();
|
||
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
|
||
/* 暂停/停止(出错待重试)都画「播放」三角;播放/缓冲画暂停符 */
|
||
draw_ctl(g_cplay_x, g_ctl_y, g_cr,
|
||
(st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? 1 : 2, pa->red, pa->on_red);
|
||
draw_ctl(g_cnext_x, g_ctl_y, g_cr - 6, 3, th->surfaceContainer, th->onSurface);
|
||
|
||
/* 歌词区(分隔线 + 滚动歌词,当前行红高亮居中) */
|
||
meow_ui_fill_rect(g_lyr.x, g_lyr.y, g_lyr.w, 1, th->outlineVariant);
|
||
int n = lyric_count();
|
||
int line_h = (g_landscape) ? 24 : 26;
|
||
int center_y = g_lyr.y + g_lyr.h / 2;
|
||
int lcx = g_lyr.x + g_lyr.w / 2;
|
||
int perr = player_manager_error();
|
||
if (perr != 0) {
|
||
/* §18:播放错误明示(旧版 err 无 UI 消费者 → 失败无声死等) */
|
||
const char *em = (perr == NCM_ERR_VIP) ? "VIP 专享,开通会员后可播放"
|
||
: (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
|
||
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
|
||
: "加载失败,请检查网络";
|
||
meow_ui_text_center(lcx, center_y - 20, em, 20, pa->red);
|
||
meow_ui_text_center(lcx, center_y + 10, "点播放键重试 / 切下一首", 20, th->onSurfaceVariant);
|
||
}
|
||
else if (st == NCM_PC_BUFFER) {
|
||
/* §18:缓冲反馈(song_url 获取 + 下载全程不再无提示) */
|
||
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(lcx, center_y - 10, bbuf, 20, th->onSurfaceVariant);
|
||
}
|
||
else if (n == 0) {
|
||
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
|
||
meow_ui_text_center(lcx, center_y - 10,
|
||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…",
|
||
20, th->onSurfaceVariant);
|
||
}
|
||
else {
|
||
int64_t ps = player_manager_pos_ms();
|
||
int cur2 = lyric_line_at(ps);
|
||
int lw = g_lyr.w - 8; /* 歌词列宽(裁剪,防窄列溢出) */
|
||
for (int i = cur2 - 6; i <= cur2 + 5; i++) {
|
||
if (i < 0 || i >= n) continue;
|
||
int yy = center_y + (i - cur2) * line_h;
|
||
if (yy < g_lyr.y || yy + line_h > g_lyr.y + g_lyr.h) continue; /* 裁剪到歌词带(自定义绘制物不按 bbox 裁剪) */
|
||
int is_cur = (i == cur2);
|
||
meow_ui_text_center(lcx, yy, ncm_text_fit(lyric_line_text(i), lw, is_cur ? 24 : 20),
|
||
is_cur ? 24 : 20, is_cur ? pa->red : th->onSurfaceVariant);
|
||
const char *tl = lyric_line_tl(i);
|
||
if (is_cur && tl && tl[0])
|
||
meow_ui_text_center(lcx, yy + 24, ncm_text_fit(tl, lw, 20), 20, th->secondary);
|
||
}
|
||
}
|
||
|
||
/* 页脚提示(用「・」U+30FB 而非 U+00B7「·」:common 字库按 GB2312 解码 A1A4 得 U+30FB,
|
||
* 不含 U+00B7 → 后者在设备上是豆腐块。REPORT §11 二期。) */
|
||
char foot[64];
|
||
snprintf(foot, sizeof foot, "左缘右滑返回 ・ ⟳ 切横屏");
|
||
meow_ui_text_center(r_cont.x + r_cont.w / 2, r_cont.y + r_cont.h - APP_FOOTER_H,
|
||
foot, 20, th->onSurfaceVariant);
|
||
}
|
||
|
||
int player_is_active(void) { return g_plc.active; }
|
||
int player_screen(void) { return g_landscape; }
|
||
|
||
int player_set_screen(int landscape)
|
||
{
|
||
int old = g_landscape;
|
||
g_landscape = landscape ? 1 : 0;
|
||
player_compute_geom();
|
||
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||
return old;
|
||
}
|
||
|
||
void player_on_tap(int x, int y)
|
||
{
|
||
if (g_tb) {
|
||
int ah = meow_topappbar_hit(g_tb, x, y);
|
||
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
|
||
if (ah >= 0) { player_set_screen(!g_landscape); return; }
|
||
}
|
||
if (x >= g_cplay_x - g_cr && x <= g_cplay_x + g_cr && y >= g_ctl_y - g_cr && y <= g_ctl_y + g_cr) {
|
||
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()); /* 出错后点播放=重试(§18) */
|
||
else player_manager_pause(1);
|
||
return;
|
||
}
|
||
if (x >= g_cprev_x - CTL_RR && x <= g_cprev_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
|
||
player_manager_next(-1); return;
|
||
}
|
||
if (x >= g_cnext_x - CTL_RR && x <= g_cnext_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
|
||
player_manager_next(1); return;
|
||
}
|
||
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
|
||
int64_t dur = player_manager_dur_ms();
|
||
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void player_play_song(int idx)
|
||
{
|
||
if (idx < 0 || idx >= g_st.track_ids_count) return;
|
||
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
|
||
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
|
||
* §18:g_st.cover_url 只作「歌单封面兜底」——不再把本曲 al_pic 写进去
|
||
* (旧写法污染兜底值:al_pic 缺失的歌会回落成**上一首的封面**,且撞上
|
||
* cover_request 去重 → 封面永远停在上一首,切歌不更新的确定性子根因)。 */
|
||
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);
|
||
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); }
|
||
void player_on_swipe(int dir, int dist) { (void)dir; (void)dist; }
|
||
|
||
/* tick:分带刷新——只标脏变化的那一带(§6)。盘带仅在歌曲/封面/播停态变化时重画。 */
|
||
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();
|
||
int line = (lyric_count() > 0) ? lyric_line_at(pos) : -1;
|
||
|
||
if (sc != g_last_song) {
|
||
g_last_song = sc;
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌名/歌手变 */
|
||
}
|
||
/* 元数据 id 变化(含快照滞后 settle:切歌瞬间快照仍是旧曲,play 线程更新后此处纠正)→
|
||
* 请求新曲专辑封面(黑胶盘换图)+ 刷动态带。 */
|
||
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);
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||
}
|
||
if (cover_changed()) {
|
||
cover_ack();
|
||
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
|
||
}
|
||
if (pos != g_last_pos_ms || st != g_last_state) {
|
||
g_last_pos_ms = pos;
|
||
int state_changed = (st != g_last_state);
|
||
g_last_state = st;
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 进度/控制钮/时间 */
|
||
if (state_changed)
|
||
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h); /* 唱针抬/压 */
|
||
}
|
||
if (line != g_last_line) {
|
||
g_last_line = line;
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌词 */
|
||
}
|
||
/* §18:缓冲进度前进(~2KB 粒度)刷动态带(「缓冲中… N%」);仅缓冲态需要。 */
|
||
int64_t dd = 0, dtot = 0;
|
||
player_manager_dl(&dd, &dtot);
|
||
int dq = (int)(dd / 2048);
|
||
if (dq != g_last_dl) {
|
||
g_last_dl = dq;
|
||
if (st == NCM_PC_BUFFER)
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||
}
|
||
/* §18:错误沿——错误条出现/消失刷动态带 + 新错误弹 snack(旧版 err 无任何 UI 出口)。 */
|
||
int perr = player_manager_error();
|
||
if (perr != g_last_err) {
|
||
g_last_err = perr;
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
|
||
if (perr == NCM_ERR_VIP) ncm_snack("VIP 专享歌曲");
|
||
else if (perr) ncm_snack("播放失败");
|
||
}
|
||
/* §20:生效档变化(降级判定完成/切歌重置)刷动态带。 */
|
||
int eff = player_manager_eff_level();
|
||
if (eff != g_last_eff) {
|
||
g_last_eff = eff;
|
||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.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 */
|
||
g_tb = ncm_topbar_create("正在播放", 1, "\xe2\x86\xbb"); /* 返回 + ⟳ 布局 */
|
||
meow_ui_activity_pop();
|
||
g_custom_disc = meow_draw_custom_register(10, g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h,
|
||
plr_draw_disc, NULL);
|
||
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
|
||
plr_draw_dyn, NULL);
|
||
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;
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
meow_ui_set_page_bg(pa->player_bg); /* 播放页恒沉浸,不随系统明暗整体反转 */
|
||
/* §18 自愈:重进播放页强制重估(g_last_meta_id 只在 create 清过一次——离开期间
|
||
* 切歌/封面失败后回来没有重评估路径;清零后首个 tick 按当前快照重请求封面+刷歌名)。 */
|
||
g_last_meta_id[0] = 0;
|
||
g_last_song = -1;
|
||
g_last_dl = -1;
|
||
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_custom_disc) { meow_draw_unregister(g_custom_disc); g_custom_disc = 0; }
|
||
if (g_custom_dyn) { meow_draw_unregister(g_custom_dyn); g_custom_dyn = 0; }
|
||
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
}
|
||
|
||
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);
|
||
}
|