/* 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 #include #include #include 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); }