Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/player.c
T
MeowOS bdd9e898f4 feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- 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。未部署真机(留主会话)
2026-09-01 15:55:42 +08:00

511 lines
23 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui/player.c — 播放页(ACT_PLAYER):黑胶唱盘 + 唱针 + 进度/控制/滚动歌词(竖/横布局可切)。
*
* 黑胶(NCM_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);
}