Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/home.c
T
AskaEth 35a636ef42 fix(ncm): 离线重试改单调钟节奏闸,断网不再每个 tick 重试(M10)
根因(真机日志 19000100_000000_7534.log):meow_ui_time_ms() 在设备上返回坏值——
低 32 位是「开机秒数×1000」(无毫秒),高 32 位每次调用随机(REPORT §42)。
旧守卫 `!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)`
因此恒真:worker 一收工,下一个 tick 立刻再重试;真机 74s 内 10 次
(间隔 5-7s 只是失败请求本身的连接超时)。第二条链是 else 分支清零重试时刻,
online/offline 一抖动就再试。

修复:
- 新增 app/logic/retry_ctl.{h,c}:节奏闸只做差值,时钟由调用方给可信单调钟
  (home.c 改传 core 的 ncm_monotonic_ms());回退不下溢(重定基准、不重试);
  busy 单飞;在线/离线抖动不重置基准;首次进离线立即探测一次,之后严格 ≥30s。
- home_tick 不再使用入参 now(真机坏值无效化);新增 home_retry_reset()。
- 静默重试不再走完整 home_reload:新增 home_reload_ex(silent) 不预读盘
  (旧实现每轮在 UI 线程同步读 playlists.bin)、home_apply_ex(keep_scroll)
  保留滚动位置、worker 记 g_home_changed 决定是否需要重绘——旧实现每轮
  home_apply() 复位滚动+整帧重绘,是「歌单列表滑动特别卡」的直接来源。
- worker 线程 pthread_detach(旧代码每轮泄漏线程栈)。
2026-11-05 13:00:24 +08:00

536 lines
25 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 缩略 + 红色按压竖条)。
* 批6:顶栏「回播放页」按钮(sb_icons 位图,动作4 最右)——无活动音频也生效,按下 push(ACT_PLAYER)。
* M5:顶栏「已缓存」按钮(动作3,MEOW_ICON_DOWNLOAD 位图)→ 已缓存列表页 ACT_CACHE。
*
* 品牌红走 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 启动卡顿已修,勿回退)。
*
* M10:离线自动重试不再用「裸 u64 差值 + 状态抖动清零」的 30s 守卫(真机 meow_ui_time_ms()
* 高 32 位是垃圾 → 守卫恒真 → 断网重试风暴 / META 洪泛 / 列表被反复拽回顶部)。改由
* logic/retry_ctl.c 统一管节奏(core 单调钟、单飞、时钟回退安全、抖动不重置),
* 静默重试不预读盘/不复位滚动/无可见变化不重绘。详见 REPORT §42。
*/
#include "../ncm_app_internal.h"
#include "../logic/retry_ctl.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 int g_home_silent; /* 1=本次加载是后台静默刷新(M10 离线重试) */
static int g_home_changed; /* 1=本次加载改了可见数据/离线态(决定是否重绘) */
/* M10:离线自动重试节奏闸。时钟用 core 单调钟(ncm_monotonic_ms),不用 SDK meow_ui_time_ms
* ——真机后者高 32 位是垃圾,任何 `now - last >= N` 都会变成「恒真」(REPORT §42)。 */
#define HOME_OFFLINE_RETRY_MS 30000
static ncm_retry_ctl g_retry;
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 = ncm_row_card_y(item, i, off, r_list.y);
int th2 = ncm_row_card_h(item);
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);
/* §22 会员等级小字:account_vip() 枚举(用户语义 0=无 / 1=畅听会员 / 2=黑胶 VIP /
* 3=超级会员);昵称右侧画小红徽章(red_ctr 底 + 红字),无会员则不画。 */
{
int vip = account_vip();
if (vip != NCM_VIP_NONE) {
const char *vip_lab = (vip == NCM_VIP_CVIP) ? "畅听会员"
: (vip == NCM_VIP_SVIP) ? "黑胶 VIP"
: (vip == NCM_VIP_SUPER) ? "超级会员" : "";
const char *nm = g_st.nickname[0] ? g_st.nickname : "我的歌单";
int nmw = meow_ui_text_width(nm, th->font_body);
int bs = th->font_body - 2; /* 徽章字号略小 */
if (bs < 16) bs = 16;
int bw = meow_ui_text_width(vip_lab, bs) + 12;
int bx = pc.x + asz + 28 + nmw + 8;
int by = pc.y + 6 + (th->font_body - (bs + 4)) / 2;
if (bx + bw <= pc.x + pc.w - 24) { /* 不越卡片右缘(避开“>”) */
ncm_fill_round(bx, by, bw, bs + 4, 6, pa->red_ctr);
meow_ui_text_center(bx + bw / 2, by + 2, vip_lab, bs, pa->red);
}
}
}
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
/* M9 离线模式:个人卡右侧可见提示(红点 + 红字,避开「>」)。断网不再退扫码,
* 用户能从这一处看出「当前是离线,缓存照常可播」。 */
if (appstate_is_offline()) {
const char *ol = "离线模式";
int osz = th->font_body - 2;
if (osz < 16) osz = 16;
int ow = meow_ui_text_width(ol, osz);
int ox = pc.x + pc.w - 28 - ow;
int oy = pc.y + (pc.h - osz) / 2;
if (ox > pc.x + asz + 28) {
meow_ui_fill_rect(ox - 14, oy + osz / 2 - 3, 6, 6, pa->red);
meow_ui_text(ox, oy, ol, osz, pa->red);
}
}
/* 双页签(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 = ncm_row_card_y(item, i, off, r_list.y);
int th2 = ncm_row_card_h(item);
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
int icy = ry + (th2 - 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, th2, pa->red);
meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, th2, pa->red, 26);
}
/* 擦除 z=10 的 SDK 行分隔线(现落进卡片间空隙):以页底色填空隙带;不越出列表视口底。 */
int gy = ry + th2, gh = NCM_ROW_GAP;
if (gy + gh > r_list.y + r_list.h) gh = r_list.y + r_list.h - gy;
if (gh > 0) meow_ui_fill_rect(r_list.x, gy, r_list.w, gh, th->bg);
}
}
}
/* 空态 / 错误 / 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", "请重新登录");
/* M9 离线模式:网络类失败不再是「登录过期」,提示可离线听已缓存。 */
else if (appstate_is_offline()) ncm_draw_empty(r_list, "离线模式", "网络不可用 · 可播放已缓存(顶栏「已缓存」)");
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%s", g_st.playlist_count,
g_st.nickname[0] ? g_st.nickname : "",
appstate_is_offline() ? " · 离线模式" : "");
ncm_draw_footer(r_cont, foot);
}
/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。
* keep_scroll=1:后台静默刷新用——保留用户当前滚动位置(数量变化由 SDK 钳位);
* 0=复位到顶部(进页/切 tab/手动刷新,与原行为一致)。
* M10:离线重试不再每轮把列表拽回顶部(旧代码每轮调本函数复位滚动 + 整帧重绘,
* 重试风暴时表现为「歌单列表滑动特别卡」)。 */
static void home_apply_ex(int keep_scroll)
{
if (g_list) {
int keep = keep_scroll ? meow_listview_scroll(g_list) : 0;
meow_listview_set_count(g_list, tab_count(g_tab));
meow_listview_set_scroll(g_list, keep);
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();
}
static void home_apply(void) { home_apply_ex(0); }
/* 后台 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;
int from_cache = 0;
int off_before = appstate_is_offline();
int cnt_before = g_st.playlist_count;
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else {
/* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。
* AUTH=会话确失效(置回登录页标志,UI 线程消费);瞬断=保持旧 cookie 走缓存/旧会话。 */
int crc = account_cookie_maybe_refresh();
if (crc == NCM_ERR_AUTH) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists_ex(&from_cache);
}
if (rc == NCM_OK)
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
/* M9:网络类失败→离线(保留登录态);成功→回在线。但「接口缓存命中」不算回在线——
* 离线时读到 120s 内的旧缓存同样返回 NCM_OK,不能据此清掉离线提示/重试节奏。
* 判定用本次请求的出参(评审 P2-2:不再用全局命中计数做前后差值)。 */
if (!(rc == NCM_OK && from_cache)) appstate_session_gate(rc);
pthread_mutex_lock(&g_home_mu);
g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */
g_home_loading = 0;
/* M10:本次加载是否产生「看得见的变化」——离线态翻转 / 歌单数量变化 / 仍处空态
* (空态文案随 load_err、离线提示变化)。无变化则静默重试不重绘,不打断滑动。 */
g_home_changed = (appstate_is_offline() != off_before) ||
(g_st.playlist_count != cnt_before) ||
(g_st.playlist_count == 0);
g_home_gen++;
pthread_mutex_unlock(&g_home_mu);
return NULL;
}
/* silent=0:进页/手动刷新(读盘缓存秒开 + 复位滚动);1:后台静默刷新(不动界面)。 */
static void home_reload_ex(int silent)
{
if (g_host_mock) { if (!silent) { g_load_err = 0; home_apply(); } return; }
if (g_home_thr_on) return; /* 单飞:同一时刻只一个 worker */
if (!silent) {
/* §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;
}
}
/* M10 静默刷新:界面已有数据 → 不预读盘(旧实现每轮都在 UI 线程同步读 playlists.bin)、
* 不复位滚动、不重绘;等 worker 完成后由 home_tick 按 g_home_changed 决定是否重绘。 */
g_home_silent = silent;
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;
return;
}
pthread_detach(g_home_thr); /* 只靠 gen 代次收工、不 join:线程栈即刻回收(旧代码每轮泄漏) */
if (!silent) home_apply();
}
void home_reload(void) { home_reload_ex(0); }
/* M10:清离线重试节奏(页创建/登出/宿主测试用)。 */
void home_retry_reset(void) { ncm_retry_ctl_init(&g_retry); }
void home_tick(uint64_t now)
{
/* M10 离线恢复:断网下严格 ≥30s 静默重试一次(home worker 单飞;成功后 appstate_session_gate
* 自动回在线并重绘)。仅在 home 可见时轮询,不轰炸服务端(风控纪律)。
*
* 节奏判定不使用入参 now:真机 meow_ui_time_ms() 的高 32 位是垃圾(REPORT §42),
* `now - last >= 30000` 会恒真 → 旧实现每个 tick 都重试(真机 74s 内 10 次,CPU 48%、
* load 3.65、整机卡死)。这里统一用 core 单调钟 + retry_ctl 闸:
* - 单飞(worker 未收工不重试)、
* - 时钟回退不下溢(回退时重定基准,不立即重试)、
* - 在线/离线抖动不重置基准(旧 else 分支清零 g_offline_retry_ms,一抖就重试)。 */
(void)now;
if (appstate_is_offline() && !g_host_mock &&
ncm_retry_ctl_poll(&g_retry, 1, g_home_thr_on, ncm_monotonic_ms(), HOME_OFFLINE_RETRY_MS)) {
meow_log(MEOW_LOG_INFO, "home: offline retry");
ncm_api_cache_refresh_path("/api/user/playlist"); /* 离线恢复探测必须真打网络 */
home_reload_ex(1);
}
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;
int changed, silent;
pthread_mutex_lock(&g_home_mu);
changed = g_home_changed;
silent = g_home_silent;
pthread_mutex_unlock(&g_home_mu);
if (!silent || changed) home_apply_ex(silent ? 1 : 0); /* 静默刷新保留滚动位置 */
}
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();
ncm_api_cache_refresh_path("/api/user/playlist"); /* 手动刷新绕过接口缓存 */
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 (ah == 3) { home_drag_reset(); cache_open(); return 1; } /* 已缓存列表 */
if (ah == 4) { home_drag_reset(); meow_view_page_push(ACT_PLAYER); 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;
home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */
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) */
/* M5:已缓存列表入口(动作3)——songs/ 池全部已缓存歌曲(点播/删除/清空)。 */
meow_topappbar_add_action(g_tb, "\xe2\x87\xa9"); /* 已缓存(动作3) */
/* 批6:回播放页(动作4, 最右)。无活动音频也生效——按下即 push(ACT_PLAYER),播放页按 appstate
* 现有语义展示空态(暂无播放)/上次会话状态。 */
meow_topappbar_add_action(g_tb, "\xe2\x99\xaa"); /* 回播放页(动作4, 最右) */
/* §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);
meow_topappbar_set_item_icon(g_tb, 3, MEOW_ICON_DOWNLOAD); /* 已缓存列表 */
meow_topappbar_set_item_icon(g_tb, 4, MEOW_ICON_QUEUE_MUSIC); /* 回播放页 */
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);
}