Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/home.c
T
AskaEth 3397a702aa fix(ncm §30): 真机四连症状——JSON unescape + 封面流式 + 歌单豆腐/分隔符 tofu + 首屏诊断日志
症状/根因/修法(0 真实 NCM 业务请求;宿主+qemu 全绿):
- ③ LRC 挤一行/时间戳未解析:ncm_json_get_str_at 不做 JSON 转义还原,
  lrc.lyric 的 \n 转义原样进串 → lrc_parse 无真实换行整段一行。修:ncm_json.c 加
  json_unescape(RFC8259 全转义 + \uXXXX/代理对),get_str_at 返回前还原。
- ⑤ 封面全 failed→give up:cover.c 静态 g_buf 288KB < 封面 1.34MB → NCM_ERR_FULL。
  设备 RAM 仅 56MB。修:cover.c 改流式写文件(ncm_http_download cb→ncm_cache_write +
  meow_img_load_file 临时文件),NCM_COVER_MAX 8MB 改下载上限(非缓冲,bss 未涨)。
- ① 162 首歌单所有条目「三个豆腐」+「加载中」:占位符 ···(U+00B7)在 default 字体缺
  该字形 → 3 豆腐(font.idx glyph 扫实证 U+00B7 MISSING)。修:占位 ···→...、全 App
  · 分隔符→/;playlist_worker 首屏打 first-screen 诊断日志。
- ② 返回首页歌单列表 1 秒方框:字体缺 U+00B7/特殊字形(◢◤/Kannada)tofu;瞬态疑 glyph
  atlas 冷缓存(SDK 内部未深挖)。修:·→/。
验证:test_ncm_core(+test_json_unescape, host+qemu) / listvirt44 / dispatch / box42 /
scroll17 / anim32 / widgets / overlay_suppress / sdk_input_keys / theme / vol / ime_fix /
pinyin / notif_gesture / notif_open / sdk_fixes / sb_collapse / ui_audio_host / notify_ipc /
notify_integration + playlist_race_sim / window_sim / search_sim / cache_evict_sim(1-9) /
player_flow_sim(P1-12) / stream_sim(16+2resume) 全 PASS;mipsel -Wall -Wextra 0 warning。
新 md5:ncm d91cd906、ncm_host b731654a、test_ncm_core_host 7a38194c。

(症状④ VOL_DOWN×3 杀 App 的 launcher 源修复 app_launcher.c 门控 !a->framework 已随
 并发 commit 54a6e82 存入 HEAD;launcher 二进制 03dfcf76 留主会话/固件打包。)
注:build_ncm_host.sh 补 SDK 292/295 的 anim/box/scroll 源(宿主链接)。
2026-09-02 02:25:37 +08:00

397 lines
16 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 (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);
}