Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/home.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

390 lines
15 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/home.c — 我的音乐页(ACT_HOME,根页,已登录)。
*
* 网易云风格(NCM_UI_DESIGN §4.2):红系 topbar(标题「我的音乐」+搜索/刷新/设置动作)+
* 个人区卡片(头像/昵称/ID)+ 双页签「我创建的/我收藏的」(meow_tabbar + 各自 meow_listview)+
* 组头红条 + 歌单行(rounded card + 红 mini-vinyl 缩略 + 红色按压竖条)。
*
* 品牌红走 ncm_palette(g_dark)(App 无权改 SDK MD3 蓝 primary),代码零硬编码散落:
* 中性色读 meow_theme_get(),红读 ncm_palette()。SDK listview 的 MD3 蓝选中不可改,
* 故用 MEOW_LIST_SEL_NONE + 自定义红态叠层(§4.2 叠加红条)。
*
* 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11
* 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct { meow_view *view; int active; } home_ctrl;
static home_ctrl g_hc;
static meow_widget *g_tb, *g_list, *g_tabs;
static ncm_rect_t r_cont, r_list, r_tabs, r_group;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static volatile int g_load_err;
static int g_tab; /* 0=我创建的 1=我收藏的 */
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单加载后台 worker(P1:从 UI 线程挪出,gen 代数做完成信号)。 */
static pthread_mutex_t g_home_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_home_thr;
static int g_home_thr_on;
static volatile int g_home_gen;
static int g_home_acked;
static volatile int g_home_loading;
static const char *tab_label(int tab) { return tab ? "我收藏的" : "我创建的"; }
/* ---- tab 数据子集(is_collected 过滤)---- */
static int tab_count(int tab)
{
int c = 0;
for (int i = 0; i < g_st.playlist_count; i++)
if ((g_st.playlists[i].is_collected != 0) == (tab == 1)) c++;
return c;
}
static int tab_global_idx(int tab, int k)
{
int c = 0;
for (int i = 0; i < g_st.playlist_count; i++)
if ((g_st.playlists[i].is_collected != 0) == (tab == 1)) {
if (c == k) return i;
c++;
}
return -1;
}
static void home_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
static int home_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
int gi = tab_global_idx(g_tab, idx);
if (gi < 0) return 0;
ncm_playlist_t *pl = &g_st.playlists[gi];
static char title[128], sub[64];
snprintf(title, sizeof title, "%s", pl->name);
snprintf(sub, sizeof sub, "%d 首", pl->track_count);
out->icon_kind = MEOW_LIST_ICON_NONE; /* 图标槽由红 overlays 画迷你黑胶 */
out->title = title;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
return 1;
}
/* ---- 歌单行 rounded card 背景(z=5,画在 listview 文本之下)---- */
static void home_row_bg(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err != 0 || g_st.playlist_count == 0) return;
const meow_theme *th = meow_theme_get();
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
int n = tab_count(g_tab);
if (last > n) last = n;
for (int i = first; i < last; i++) {
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer);
}
}
/* ---- 主绘制(z=60):个人卡/组头红条/tab 红态/行缩略与按压红条,>listview 红态叠层 ---- */
static void home_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int w = r_cont.w;
/* 个人区·卡片(16, y0, w-32, 72):圆角 16 surfaceContainer。 */
ncm_rect_t pc = NR(16, r_cont.y + 8, w - 32, 72);
ncm_fill_round(pc.x, pc.y, pc.w, pc.h, 16, th->surfaceContainer);
int asz = 40, ax = pc.x + 16, acy = pc.y + (pc.h - asz) / 2;
ncm_fill_round(ax, acy, asz, asz, asz / 2, pa->red_ctr); /* 圆形头像底 */
char glyph[8];
int gn = first_glyph(g_st.nickname, glyph, sizeof glyph);
if (!gn) snprintf(glyph, sizeof glyph, "\u266a"); /* ♪ 无昵称占位 */
meow_ui_text_center(ax + asz / 2, acy + (asz - th->font_body) / 2, glyph, th->font_body, pa->red);
char idtxt[64];
snprintf(idtxt, sizeof idtxt, "ID: %s", g_st.uid[0] ? g_st.uid : "-");
meow_ui_text(pc.x + asz + 28, pc.y + 6, g_st.nickname[0] ? g_st.nickname : "我的歌单",
th->font_body, th->onSurface);
meow_ui_text(pc.x + asz + 28, pc.y + 6 + th->font_body + 4, idtxt, th->font_body, th->onSurfaceVariant);
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
/* 双页签(g_tabs=meow_tabbar widget 建)——红下划线/红选中字叠层。 */
if (g_tabs) {
int iw = r_tabs.w / 2;
int ay = r_tabs.y + (r_tabs.h - meow_ui_text_height(th->font_body)) / 2;
meow_ui_fill_rect(r_tabs.x + g_tab * iw, r_tabs.y + r_tabs.h - 2, iw, 2, pa->red);
meow_ui_text_center(r_tabs.x + g_tab * iw + iw / 2, ay, tab_label(g_tab), th->font_body, pa->red);
}
/* 组头红条(red_ctr 底 + 红字 + 红侧条)。 */
ncm_fill_round(r_group.x, r_group.y, r_group.w, r_group.h, 8, pa->red_ctr);
meow_ui_fill_rect(r_group.x, r_group.y + 4, 4, r_group.h - 8, pa->red);
char ght[64];
snprintf(ght, sizeof ght, "%s · %d 个", tab_label(g_tab), tab_count(g_tab));
meow_ui_text(r_group.x + 16, r_group.y + (r_group.h - th->font_body) / 2,
ght, th->font_body, pa->red);
meow_ui_text_right(r_group.x + r_group.w - 16, r_group.y + (r_group.h - th->font_body) / 2,
"+ 新建", th->font_body, pa->red);
/* 歌单行:红 mini-vinyl 缩略 + 按压红条(文本由 listview 画在 z=10)。 */
if (g_load_err == 0 && g_st.playlist_count > 0) {
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
int isz = 24;
int first = off / item, last = (off + r_list.h) / item + 1;
int n = tab_count(g_tab);
if (last > n) last = n;
for (int i = first; i < last; i++) {
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
int icy = ry + (item - isz) / 2;
int bx = r_list.x + 16;
ncm_fill_round(bx, icy, isz, isz, 6, pa->red);
meow_ui_fill_rect(bx + isz / 2 - 2, icy + isz / 2 - 2, 4, 4, pa->on_red); /* 盘芯 */
if (g_press_row == i) {
meow_ui_fill_rect(r_list.x, ry, 4, item - 2, pa->red);
meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, item - 2, pa->red, 26);
}
}
}
}
/* 空态 / 错误 / LOADING。 */
static void home_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err == 0 && g_st.playlist_count != 0) return;
if (g_home_loading) { ncm_draw_empty(r_list, "LOADING", "正在加载歌单..."); return; }
if (g_st.logged_in == 0) ncm_draw_empty(r_list, "NOT SIGNED IN", "请先登录");
else if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "NETWORK ERROR", "上滑重试");
else ncm_draw_empty(r_list, "NO PLAYLISTS", "还没有歌单");
}
static void home_draw_footer(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err != 0) return;
char foot[192];
snprintf(foot, sizeof foot, "共 %d 个歌单 %s", g_st.playlist_count,
g_st.nickname[0] ? g_st.nickname : "");
ncm_draw_footer(r_cont, foot);
}
/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 */
static void home_apply(void)
{
if (g_list) {
meow_listview_set_count(g_list, tab_count(g_tab));
meow_listview_set_scroll(g_list, 0);
meow_listview_set_sel(g_list, -1);
}
if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab);
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 */
static void *home_worker(void *arg)
{
(void)arg;
int rc;
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists();
if (rc == NCM_OK)
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
pthread_mutex_lock(&g_home_mu);
g_load_err = rc;
g_home_loading = 0;
g_home_gen++;
pthread_mutex_unlock(&g_home_mu);
return NULL;
}
void home_reload(void)
{
if (g_host_mock) { g_load_err = 0; home_apply(); return; }
if (g_home_thr_on) return;
g_load_err = 0;
g_st.playlist_count = 0;
g_home_loading = 1;
g_home_thr_on = 1;
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO;
}
home_apply();
}
void home_tick(uint64_t now)
{
(void)now;
if (!g_home_thr_on) return;
if (g_home_gen == g_home_acked) return;
g_home_acked = g_home_gen;
g_home_thr_on = 0;
g_home_loading = 0;
home_apply();
}
int home_is_active(void) { return g_hc.active; }
int home_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { home_drag_reset(); return 1; }
if (ah == 0) { home_drag_reset(); home_reload(); return 1; } /* 刷新 */
if (ah == 1) { home_drag_reset(); settings_open(); return 1; } /* 设置 */
if (ah >= 2) { home_drag_reset(); search_open(); return 1; } /* 搜索(最右) */
}
if (g_tabs && meow_widget_hit(g_tabs, x, y)) {
int t = meow_tabbar_hit(g_tabs, x, y);
if (t >= 0 && t != g_tab) { g_tab = t; home_drag_reset(); home_apply(); }
return 1;
}
if (g_load_err != 0 || g_st.playlist_count == 0) return 0;
if (meow_widget_hit(g_list, x, y)) {
/* 误触修复(同 playlist.c):小拖动滚动列表后 release 被 SDK 判成 TAP,旧命中用“释放点 +
* 新 scroll”,打到非按下行;改用按下行 g_press_row,移动超阈值(>10)视为滚动不触发。 */
int was_drag = (g_press_delta > 10);
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
home_drag_reset();
if (row < 0 || was_drag) return 1;
playlist_open(tab_global_idx(g_tab, row));
return 1;
}
return 0;
}
void home_touch_down(int x, int y)
{
home_drag_reset();
if (g_load_err != 0 || g_st.playlist_count == 0) return;
if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row >= 0) g_press_row = row;
g_dragging = 1; g_drag_last_y = y;
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
}
void home_touch_move(int x, int y)
{
(void)x;
if (!g_dragging) return;
if (g_press_row >= 0) {
int d = y - g_drag_last_y; if (d < 0) d = -d;
g_press_delta += d;
if (g_press_delta > 10) g_press_row = -1;
}
int dy = y - g_drag_last_y; g_drag_last_y = y;
if (dy == 0) return;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, -dy);
if (meow_listview_scroll(g_list) != before)
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
void home_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_load_err != 0 || g_st.playlist_count == 0) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before)
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
/* 生命周期 */
static void home_page_create(meow_view *v, void *ctx)
{
(void)v;
((home_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72);
r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);
r_group = NR(16, r_tabs.y + r_tabs.h + 4, r_cont.w - 32, 36);
r_list = NR(16, r_group.y + r_group.h + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_group.y + r_group.h + 8));
if (r_list.h < 0) r_list.h = 0;
meow_ui_activity_push(ACT_HOME); /* owner=ACT_HOME */
g_tb = ncm_topbar_create("我的音乐", 0, "\xe2\x86\xbb"); /* 刷新(动作0) */
meow_topappbar_add_action(g_tb, "\xe2\x9a\x99"); /* 设置(动作1) */
meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2, 最右) */
g_tabs = meow_tabbar_create(r_tabs.x, r_tabs.y, r_tabs.w, r_tabs.h);
meow_tabbar_add_tab(g_tabs, tab_label(0));
meow_tabbar_add_tab(g_tabs, tab_label(1));
meow_tabbar_set_selected(g_tabs, 0);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
home_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_ui_activity_pop();
/* 显式 z:行卡片(5) < listview(10) < tabbar(20) < 红态叠层(60) < topbar(999) */
meow_widget_set_z(g_list, 10);
meow_widget_set_z(g_tabs, 20);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, home_row_bg, NULL);
g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
home_draw, NULL);
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
home_draw_empty, NULL);
g_footer_id = meow_draw_custom_register(1, r_cont.x, r_cont.y + r_cont.h - APP_FOOTER_H,
r_cont.w, APP_FOOTER_H, home_draw_footer, NULL);
home_drag_reset();
}
static void home_page_enter(meow_view *v, void *ctx)
{
(void)v;
((home_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
home_reload();
}
static void home_page_leave(meow_view *v, void *ctx)
{
(void)v;
((home_ctrl *)ctx)->active = 0;
}
static void home_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; }
if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_id = 0; }
if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; }
if (g_footer_id){ meow_draw_unregister(g_footer_id);g_footer_id = 0; }
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_tabs) { meow_widget_free(g_tabs); g_tabs = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *home_view_create(void)
{
static const meow_view_hooks h = {
.on_create = home_page_create,
.on_enter = home_page_enter,
.on_leave = home_page_leave,
.on_destroy = home_page_destroy,
};
return meow_view_create(ACT_HOME, MEOW_VIEW_ACTIVITY, &h, &g_hc);
}