Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/home.c
T
AskaEth 44a790dc1d fix(ncm §33): 流媒专项批六项——流式SIGSEGV防御日志/档位保真(无损级必须flac+显示档按实际)/封面4xx不重试/收藏显隐/缓存命名迁移/topbar位图图标
- §2① ncm_api.c level_attempt: 无损级(hires/lossless及以上)命中必须 type=flac,返回 mp3 视为该档不可用续降
  (修「hires 档静默给 mp3 被当命中」)
- §2② player_manager.c eff_level_for_resp: 显示档按响应实际格式/码率——mp3 绝不显示 Hi-Res(≤128k→标准, 否则极高),
  flac 采信链 eff; br 未知钳 ≤EXHIGH
- §1 流式 SIGSEGV(《十面埋伏》sig=11) 审计+UBSAN 探针未复现 → 落地防御日志(dl start/done/interrupt + play done fin/exp/fail/underrun)
- §3 cover.c: download_cover 回填 rc/status; 确定性 4xx(404等)不重试立即跳下一张, 仅瞬时错误退避(rc=%d status=%d 日志)
- §4 playlist.c: pl_show_fav 我创建(is_collected==0)隐藏收藏钮, 我收藏保留, 搜索歌单保守显示(评论槽并入无空洞)
- §5 ncm_cache.c 双点→单点迁移 + player_manager song_local_file 旧键→当前档位键(不重下)
- §6 home.c: topbar 动作 ⚙↻⌕ 迁移 sb_icons 位图(meow_topappbar_set_item_icon, 修 4k 字库豆腐)
- 测试: test_ncm_core 新 test_grade_flac_gate + test_cache 双点迁移断言; degrade_sim/player_flow_sim 桩 flac 化
- 验证: stream/player_flow/window/search/cache_evict/degrade sim 全 PASS + test_ncm_core host+mipsel(qemu) + mem_sim + build_ncm.sh 0 warning
- 新 ncm md5 cb818786 (text 1017110/bss 1362500), 未部署留主会话; 0 真实 NCM 业务请求
2026-09-02 04:20:46 +08:00

402 lines
17 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_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)。
* §26 stale-while-revalidate:网络成功 → appstate_load_playlists 覆写盘缓存 + 更新内存;
* 失败但有缓存在展示 → 保持缓存列表(g_load_err=0),后台静默不打断(断网可离线浏览)。 */
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 = (g_st.playlist_count > 0) ? 0 : 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;
/* §26:进首页先用磁盘缓存秒渲染(若有),后台 worker 静默刷新;无缓存才显示 LOADING。 */
int64_t ts = 0;
int had_cache = (appstate_playlists_cache_load(&ts) == NCM_OK && g_st.playlist_count > 0);
g_load_err = 0;
if (!had_cache) {
g_st.playlist_count = 0; /* 无缓存 → LOADING 空态 */
g_home_loading = 1;
}
g_home_thr_on = 1;
if (ncm_thread_create(&g_home_thr, 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, 最右) */
/* §307 topbar 图标迁移:三位码点(↻ ⚙ ⌕)在 4k 字库缺字形渲染成豆腐(WORK_STATE 310 前置
* 症状①),改为 sb_icons 位图(Material Symbols Rounded,字符标签仅作占位,绘制走位图)。 */
meow_topappbar_set_item_icon(g_tb, 0, MEOW_ICON_REFRESH);
meow_topappbar_set_item_icon(g_tb, 1, MEOW_ICON_SETTINGS);
meow_topappbar_set_item_icon(g_tb, 2, MEOW_ICON_SEARCH);
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);
}