- 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。未部署真机(留主会话)
140 lines
5.3 KiB
C
140 lines
5.3 KiB
C
/* ui/common/chrome.c — dev.player.ncm 共享 chrome(TopAppBar 建置 / 几何 / 空态&页脚 / 底部提示条)。
|
||
*
|
||
* 与 files_app/ui/common/chrome.c 同理:把「页面 chrome 固定不随内容重建」分出去。绘制全部读
|
||
* meow_theme_get() 语义色(§8.1),不硬编码配色。topbar 由 meow_topappbar 建,自动注册进绘制物
|
||
* 注册表(§6.2),owner=创建时 active activity。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <string.h>
|
||
|
||
int ncm_statusbar(void)
|
||
{
|
||
int sb = meow_ui_statusbar_height();
|
||
return sb < 0 ? 0 : sb;
|
||
}
|
||
|
||
ncm_rect_t ncm_topbar_rect(void)
|
||
{
|
||
int sb = ncm_statusbar();
|
||
return NR(0, sb, meow_ui_width(), APP_TOPBAR_H);
|
||
}
|
||
|
||
ncm_rect_t ncm_content_rect(void)
|
||
{
|
||
int sb = ncm_statusbar();
|
||
int y = sb + APP_TOPBAR_H;
|
||
int h = meow_ui_height() - y;
|
||
if (h < 0) h = 0;
|
||
return NR(0, y, meow_ui_width(), h);
|
||
}
|
||
|
||
meow_widget *ncm_topbar_create(const char *title, int back, const char *action_label)
|
||
{
|
||
ncm_rect_t r = ncm_topbar_rect();
|
||
meow_widget *tb = meow_topappbar_create(r.x, r.y, r.w, r.h, title);
|
||
meow_topappbar_set_back(tb, back);
|
||
if (action_label) meow_topappbar_add_action(tb, action_label);
|
||
meow_widget_set_z(tb, 999); /* chrome 置顶(§6.2:> 一切自动 z) */
|
||
return tb;
|
||
}
|
||
|
||
/* 空态/错误:居中两行(kind 大字 + what 小字)。kind 用品牌红(§3.1 品牌元素走 App 调色板)。 */
|
||
void ncm_draw_empty(ncm_rect_t r, const char *kind, const char *what)
|
||
{
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
int cy = r.y + r.h / 2;
|
||
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, pa->red);
|
||
if (what && what[0])
|
||
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->onSurfaceVariant);
|
||
}
|
||
|
||
/* 底部信息(右对齐 dim)。 */
|
||
void ncm_draw_footer(ncm_rect_t r, const char *text)
|
||
{
|
||
const meow_theme *th = meow_theme_get();
|
||
if (!text || !text[0]) return;
|
||
meow_ui_text_right(r.x + r.w - APP_PAD, r.y + r.h - 26, text, 20, th->dim);
|
||
}
|
||
|
||
/* 底部提示条(非阻塞):画在内容区底部之上,2s 后由 tick 清除。
|
||
* 只画一次并标脏;g_snack_until 为过期时间(0=无)。 */
|
||
static char s_snack[192];
|
||
static uint64_t s_snack_until; /* monotonic ms 过期时刻 */
|
||
static ncm_rect_t s_snack_r;
|
||
static int s_snack_id; /* <0=未注册 */
|
||
|
||
static void snack_draw(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_time_ms() >= s_snack_until && s_snack_until != 0) {
|
||
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
|
||
s_snack_until = 0;
|
||
return;
|
||
}
|
||
const meow_theme *th = meow_theme_get();
|
||
/* 深色底 + 白字 */
|
||
meow_ui_fill_rect(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h, th->inverseSurface);
|
||
meow_ui_text_center(s_snack_r.x + s_snack_r.w / 2, s_snack_r.y + (s_snack_r.h - 26) / 2 + 4,
|
||
s_snack, 20, th->inverseOnSurface);
|
||
}
|
||
|
||
void ncm_snack(const char *text)
|
||
{
|
||
if (!text) return;
|
||
strncpy(s_snack, text, sizeof s_snack - 1);
|
||
s_snack[sizeof s_snack - 1] = 0;
|
||
s_snack_until = meow_ui_time_ms() + 2000;
|
||
int w = meow_ui_width() - 2 * APP_PAD;
|
||
s_snack_r = NR(APP_PAD, meow_ui_height() - 56, w, 40);
|
||
if (s_snack_id < 0)
|
||
s_snack_id = meow_draw_custom_register(999, s_snack_r.x, s_snack_r.y, s_snack_r.w,
|
||
s_snack_r.h, snack_draw, NULL);
|
||
else
|
||
meow_draw_set_z(s_snack_id, 999);
|
||
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
|
||
}
|
||
|
||
/* 周期检查:snack 到期清除。可由各页 tick 调。 */
|
||
void chrome_tick(void)
|
||
{
|
||
if (s_snack_until && meow_ui_time_ms() >= s_snack_until) {
|
||
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
|
||
s_snack_until = 0;
|
||
}
|
||
}
|
||
|
||
/* ---- §20 音质档位显示名/规格(八档 UI 权威口径,NCM_CACHE_DESIGN §3;settings/player 共用)----
|
||
* 档位中文名以我方设计文档为准(BIZ_NOTES §2.1:CNMPlayer 标签只做 level 串对齐参考)。 */
|
||
const char *ncm_level_name(int lv)
|
||
{
|
||
switch (lv) {
|
||
case NCM_LEVEL_SURROUND: return "沉浸环绕声";
|
||
case NCM_LEVEL_SKY: return "臻音全景声";
|
||
case NCM_LEVEL_JYMASTER: return "超清母带";
|
||
case NCM_LEVEL_JYEFFECT: return "高清臻音";
|
||
case NCM_LEVEL_DOLBY: return "杜比全景声"; /* 映射保留,不进 UI 清单 */
|
||
case NCM_LEVEL_HIRES: return "Hi-Res";
|
||
case NCM_LEVEL_LOSSLESS: return "无损 SQ";
|
||
case NCM_LEVEL_EXHIGH: return "极高 HQ";
|
||
case NCM_LEVEL_HIGHER: return "较高"; /* 仅降级链中间档,不进 UI */
|
||
default: return "标准";
|
||
}
|
||
}
|
||
|
||
const char *ncm_level_spec(int lv)
|
||
{
|
||
switch (lv) {
|
||
case NCM_LEVEL_SURROUND: return "5.1 声道";
|
||
case NCM_LEVEL_SKY: return "Audio Vivid";
|
||
case NCM_LEVEL_JYMASTER: return "192kHz/24bit";
|
||
case NCM_LEVEL_JYEFFECT: return "96kHz/24bit";
|
||
case NCM_LEVEL_DOLBY: return "Dolby Atmos";
|
||
case NCM_LEVEL_HIRES: return "192kHz/24bit";
|
||
case NCM_LEVEL_LOSSLESS: return "48kHz/16bit";
|
||
case NCM_LEVEL_EXHIGH: return "≤320kbps";
|
||
case NCM_LEVEL_HIGHER: return "192kbps";
|
||
default: return "128kbps";
|
||
}
|
||
}
|