diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 749ba29..ffeeb38 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -3726,3 +3726,100 @@ if (now - last_meta >= 1000) { send_meta(fd, ...); last_meta = now; } (旧:5-7s);launcher 日志 `ipc: META app=dev.player.ncm` 空闲时应约每 30s 一条(旧:~10/s); 播放中约每 5s 一条、暂停/切歌各多一条;同时观察 ncm CPU 与首屏/列表滑动是否恢复。 - **未部署/未 push**:设备只读访问,未改设备;本节所有验证均在宿主完成。 + +## 43. M12:列表首行主标题不显示(两行列表行高 < 文本块,SDK 视口顶裁剪把首行标题整条跳过)(2026-11-05) + +### 43.1 现象(用户原话) + +> 「网易云有一个问题 歌单列表的第1个歌单名字不显示 副标题(歌曲数目)有显示 。点开歌单 加载歌曲列表 +> 第1个歌曲名字不显示 副标题(歌手名)有显示。」 + +特征:两类列表(首页歌单列表 / 歌单详情曲目列表)都只有**首行主标题完全空白**、副标题正常, +其余行标题/副标题都正常。设备二进制 431ca4dd(含 M5/M6/M10/M11)。 + +### 43.2 根因(代码路径 + 触发机制,已坐实) + +SDK `meow_listview` 的双行渲染(`patch_v2/sdk/meow_ui_widget.c:2061`,src2 分支): + +```c +ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; +if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(rtx, ty, it.title, ...); /* 标题:视口顶裁剪守卫 */ +int sy = ty + thh + MEOW_LISTV_SUB_GAP; +if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(rtx, sy, it.subtitle, ...); /* 副标题 */ +``` + +- `thh = meow_ui_text_height(theme->font_body)`。真机 ncm 进程(pid 6693)环境: + `MEOW_MULTI_FONT=1`、主库 `MEOW_FONT_APP=source_han_serif`;从设备 + `/usr/data/mnt/sd_0/MeowOS/fonts/source_han_serif/regular/font.idx` 的 MFNI 面头直接读出 + 20px 面 `ascent=24 / descent=6` → `meow_font_line_height(20) = 30` + (多字库取最大:google_sans 类库 20px 面 = 26,不改变结果)→ **文本块 = 2*30 + 2 = 62**。 +- ncm 行高 `APP_ROW_H = 56`(与 SDK `item_h` 默认 56 相同)→ `ty = ry + (56-62)/2 = ry - 3`。 +- 首行(scroll=0)`ry == vy0`(行顶 == 控件视口顶)→ `ty = vy0 - 3 < vy0` → **标题被整条跳过** + (不是画了被裁,是守卫直接不画);`sy = vy0 + 29` 在视口内 → 副标题照画。 +- 其余行 `ty = ry - 3 > vy0` → 标题仍画(比本行顶高 3px,文字整体仍在视口内,肉眼难察觉)。 +- 两类列表同用 `meow_listview_create(..., src2, ...)` + 双行 + `APP_ROW_H`,故症状完全一致。 + +排除的其它方向: + +| 怀疑方向 | 结论 | +|---|---| +| ① title 指针悬垂/空串(绑定生命周期) | **排除**:`home_src2`/`pl_src2` 每帧刷新静态缓冲,副标题取自同一缓冲且可见 | +| ② 首行被顶部栏/裁剪区遮住(布局偏移) | **排除**:组头红条在 `r_list` 上方 8px;行卡片 z=5、文本 z=10、叠层 z=60 均不覆盖首行标题位 | +| ③ 首行字体色 = 背景色(选中/主题色) | **排除**:同一次绘制里 title=`onSurface`、subtitle=`onSurfaceVariant`;且用户确认「完全空白」非同色 | +| ④ 首项特殊路径(我喜欢的音乐/当前播放) | **排除**:歌单详情首行是任意曲目,无特殊渲染分支 | + +### 43.3 修复(app 侧,不动 SDK / 不动 meow_ui.a) + +行高改为**按当前主题字体实测行高派生**,保证文本块装得下(卡高 == 文本块): + +| 文件 | 改动 | +|---|---| +| `app/ncm_app_internal.h` | 新增 `NCM_ROW_SUB_GAP`(与 SDK 同口径 2)、`ncm_row_h_for_line_h(line_h)`(= `max(APP_ROW_H, 2*行高 + 2 + NCM_ROW_GAP)`,偶数)、`ncm_row_h()`(取 `meow_ui_text_height(meow_theme_get()->font_body)`)。`APP_ROW_H` 降级为**行高下限**(注释同步)。 | +| `app/ui/home.c` / `playlist.c` / `queue.c` / `cache.c` / `search.c` | 各页 `g_item_h = ncm_row_h()` 在 page create 取一次(字体档位切换走重建页面,避免同一页内行高漂移),`meow_listview_set_item_h` 与全部卡片几何/可见行推算/页脚「x-y / n 首」/补载视口换算统一用 `g_item_h`。 | +| `app/test/test_meow_row_text_fit.c`(新) | 纯头内联几何回归(不链 SDK):真机已装字库 20px 面实测行高矩阵 14/21/26/27/30 → 断言文本块 ≤ 行高、首行 ty ≥ 0、副标题不越行、卡高 ≥ 文本块、行高偶数且 ≥ 下限;并用旧 56 + 行高 30 断言 `ty<0`(bug 形态哨兵,防再写死)。 | +| `app/test/test_first_row_title_render.c`(新) | **渲染级回归**:宿主 5x7 回退行高是 14,照原样渲染复现不了。用 `-Wl,--wrap=meow_ui_text_height` 把行高替换成真机实测 30,驱动**真实 SDK listview 双行渲染**,读回 `MEOW_UI_DUMP` 的 PPM 数像素:旧行高 56 → 首行标题带 0 文字像素、副标题带 186;新行高 70 → 标题带 84、副标题带 100。 | +| `bench/plpage_sim.c` | 滚动换行高改用 `ncm_row_h()`;临时目录/出图改走 `$TMPDIR`(Termux 下 `/tmp` 不可写,原写死 `/tmp` 无法运行)。 | +| `build_ncm_host.sh` | **补 `meow_ui_time`**:M11 新增 `sdk/meow_ui_time.c` 后本脚本未同步,main 上宿主链接报 `undefined symbol: meow_ui_time_ms`(与 M12 无关的既有断裂,不补则本 mission 无法出图验证)。 | + +行高取值(本机默认字体 source_han_serif,行高 30):`2*30 + 2 + 8 = 70` → 卡高 62 == 文本块 62, +`ty = +4`(首行标题正常落卡内)。其它字库:行高 27 → 64;26 → 62;21 → 52 但受下限钳到 56(与旧视觉一致)。 + +### 43.4 验证(宿主,0 真机写、0 真实 NCM 请求) + +| 项 | 结果 | +|---|---| +| `app/test/test_meow_row_text_fit`(新) | **PASS:48 checks / 0 fail**(行高矩阵 5 档;含「旧 56 + 行高 30 → ty=-3」哨兵) | +| `app/test/test_first_row_title_render`(新,真实 SDK 渲染 + PPM 像素计数) | **PASS**:legacy `item=56 ty=-3 → title_ink=0 sub_ink=186`(复现用户症状);fixed `item=70 ty=4 → title_ink=84 sub_ink=100`(首行标题正常) | +| `app/test/test_meow_row_gap`(既有卡片几何) | **PASS**(row_gap=8 card_h=48 top2top=56 symmetric=1) | +| `bench/plpage_sim`(真实 playlist.c 全链路:懒加载分页/触底/回滚补物化) | **RESULT: PASS**(全用例 PASS) | +| `ncm_host` 出图(home/playlist/cache/search/searchpl/plsplay/queuetest) | 全部 rc=0;与**改动前基线逐字节相同**(宿主行高 14 → `ncm_row_h()` 仍为 56,几何零变化 → 证明重构本身无视觉回归) | +| `ncm_host` 页面自检(queuetest/modetest/barhit/searchtap/cachetest) | 全 PASS(covswitch 因测试自身写死 `/tmp/*.bmp`、Termux 下不可写而 `init FAIL`——与 M12 无关的既有环境限制) | +| 宿主告警 | 改动/新增 TU(5 个 ui + ncm_app_internal.h + 2 个 test + 1 个 sim)`-Wall -Wextra` **0 warning**;`build_ncm_host.sh` 全量链接通过 | +| 真机只读取证 | 读 `/proc/6693/environ`(字体档/多字体开关)+ 设备 `font.idx` 面头(ascent/descent);**未改设备** | +| mipsel / 真机 | **未跑**(本机无 zig 交叉链;交叉编译 + 热更由塔台做) | + +### 43.5 诚实出入 / 遗留 + +- **修复形态是「行高变高」**:本机默认字库下行高 56 → 70(每屏可见行数略减)。这是必然代价——旧行高 + 本来就装不下 62px 的两行文本(文字一直溢出卡外 3~7px),只是首行标题被守卫整条吞掉才暴露。 + 若用户切到行高 21 的字库(source_han_sans 等),行高回到 56,视觉与旧版一致。 +- **滚动时顶部半露行仍会隐藏标题**:SDK 的语义是「标题不完全在视口内就不画」(`ty >= vy0`), + 这是滚动裁剪的正常表现,不属于本 bug;scroll=0 的首行标题现已正常。 +- **根因在 SDK 通用渲染**:2 行档默认 `item_h=56` 假定字体行高 ≤27;任何 App 的两行列表在 20px + 字库(行高 30)下都会丢首行标题。本 mission scope 只到 `patch_v2/ncm_app/**`,故 app 侧派生行高 + 规避;**SDK 侧仍应修**(2 行分支把 `ty` 钳到 `vy0` 或按文本块反推行高),已作为 finding 上报塔台。 +- **`build_ncm_host.sh` 的 M11 漏编**:本脚本补了 `meow_ui_time`(否则宿主链接失败)。同类还写死 + `/tmp` 的两处(`bench/plpage_sim.c` 已改 `$TMPDIR`;`app/test/test_ncm_app.c` covswitch 仍写死, + 未改——与本 bug 无关)。 +- **未部署/未 push**:设备只读访问,未改设备;所有验证在宿主完成。 + +### 43.6 真机验证要点(塔台热更后按此验收) + +1. 首页「我的音乐」歌单列表:**第 1 个歌单名字可见**,副标题「N 首」仍在;第 2 行起视觉正常。 +2. 点开任一歌单(含「我喜欢的音乐」):**第 1 首歌名可见**,副标题「歌手 / 专辑」仍在。 +3. 同类页面顺带确认:播放队列(ACT_QUEUE)、已缓存列表(ACT_CACHE)、搜索结果单曲页签,首行标题都在。 +4. 预期可见变化:每行变高(本机字库下 56 → 70px)、每屏可见行数略减;卡片内两行文字完整、不再溢出卡片。 +5. 滚动列表:顶部半露行标题不可见属正常裁剪;回到顶部(scroll=0)后首行标题必须重新出现。 +6. 若系统字体切到 source_han_sans 等小行高字库:行高回落 56,视觉与旧版一致且首行标题仍正常。 diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 41099a1..d211ddf 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -62,14 +62,14 @@ enum { #define APP_W 480 #define APP_H 720 #define APP_TOPBAR_H 56 -#define APP_ROW_H 56 /* 列表行高 */ +#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际行高按字体行高派生,见 ncm_row_h) */ #define APP_PAD 16 #define APP_OP_W 48 /* 行右侧操作区触达宽 */ #define APP_FOOTER_H 26 -/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=APP_ROW_H) - * 不变——卡片在其行高内上下对称内缩 NCM_ROW_GAP/2,故文本(SDK 按 APP_ROW_H 垂直居中)仍落卡中心; - * 相邻卡顶间距恒=APP_ROW_H=卡高+NCM_ROW_GAP。要求偶数,保证上下对称。 */ +/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=ncm_row_h()) + * 不变——卡片在其行高内上下对称内缩 NCM_ROW_GAP/2,故文本(SDK 按行高垂直居中)仍落卡中心; + * 相邻卡顶间距恒=行高=卡高+NCM_ROW_GAP。要求偶数,保证上下对称。 */ #define NCM_ROW_GAP 8 /* 第 idx 行(行高 item,滚动 off,列表顶 list_y)的圆角卡:顶 y 与高 h。 */ static inline int ncm_row_card_y(int item, int idx, int off, int list_y) @@ -78,6 +78,32 @@ static inline int ncm_row_card_y(int item, int idx, int off, int list_y) } static inline int ncm_row_card_h(int item) { return item - NCM_ROW_GAP; } +/* ---- 列表行高(M12:首行标题不显示根因修复)---- + * SDK meow_listview 的双行渲染(sdk/meow_ui_widget.c 的 2 行分支)把「标题+副标题」文本块 + * block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) + * 在行内垂直居中(ty = 行顶 + (item - block) / 2),且标题只在 `ty >= 视口顶` 时才画 + * (越界钳制,防泄漏)。行高 item 小于 block 时 ty = 行顶 - Δ,于是: + * - 第一行(行顶 == 视口顶)标题被钳掉、副标题(ty + thh + 2)仍在视口内 → 真机 + * 「歌单列表/歌曲列表第 1 行只有副标题」,其余行标题画在自己行顶上方 Δ(不易察觉); + * - 文本块也比圆角卡高,两行文字溢出卡外。 + * 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 = 30)block = 62,旧固定行高 + * 56 → Δ = 3。故行高必须由字体实测行高派生:block + NCM_ROW_GAP → 卡高 == 文本块, + * 文本完整落在卡内并仍居中。行高由 NCM_ROW_GAP 为偶数、2*行高 为偶数保证偶数(卡对称内缩)。 */ +#define NCM_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行间距) */ +static inline int ncm_row_h_for_line_h(int line_h) +{ + if (line_h < 1) line_h = 1; /* 字体未就绪的防御(0 高 → 退回下限) */ + int h = 2 * line_h + NCM_ROW_SUB_GAP + NCM_ROW_GAP; + if (h < APP_ROW_H) h = APP_ROW_H; + if (h & 1) h++; + return h; +} +/* 当前主题字体下的两行列表行高(页面 create 时取值并缓存;字体档位切换走重建页面)。 */ +static inline int ncm_row_h(void) +{ + return ncm_row_h_for_line_h(meow_ui_text_height(meow_theme_get()->font_body)); +} + /* ---- 播放页(§22 重做:封面页/歌词页双页,NCM_CACHE_DESIGN §6)---- * 沉浸底恒 NCM_PLAYER_BG(不随系统明暗反转——「不动沉浸底」);日夜间主题配色只映射到 * 半透明叠加层(歌词页 scrim)与 accent(红)。 */ diff --git a/patch_v2/ncm_app/app/test/test_first_row_title_render.c b/patch_v2/ncm_app/app/test/test_first_row_title_render.c new file mode 100644 index 0000000..a6696c9 --- /dev/null +++ b/patch_v2/ncm_app/app/test/test_first_row_title_render.c @@ -0,0 +1,174 @@ +/* app/test/test_first_row_title_render.c — 宿主渲染回归(M12):列表首行标题必须真正画出来。 + * + * 目的:在宿主上用**真实 SDK listview 双行渲染**复现/验证真机根因,而不只是验几何公式。 + * 真机字体(source_han_serif 20px,font.idx: ascent 24 + descent 6)行高 = 30, + * 双行文本块 = 2*30 + 2 = 62。旧固定行高 56 < 62 → SDK 的 2 行分支 + * ty = 行顶 + (item - block)/2 = 行顶 - 3 + * 而标题只在 `ty >= 视口顶` 时才画 → 第一行(行顶 == 视口顶)标题被整条跳过、副标题仍在。 + * + * 宿主 5x7 字体的行高是 14(gfx 回退),照原样渲染永远复现不了。故用链接期 + * `-Wl,--wrap=meow_ui_text_height` 把行高替换成真机实测的 30,驱动同一份 SDK 代码: + * A 旧行高 56 → 首行标题带无文字像素(bug 形态) + * B ncm_row_h_for_line_h(30) = 70 → 首行标题带/副标题带都有文字像素(修复形态) + * 断言直接读回 MEOW_UI_DUMP 写出的 PPM(按像素计数)。 + * + * 编译/运行(先跑 build_ncm_host.sh 产出 out/host/sdk_*.o): + * gcc -O2 -Wall -Wextra -DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ + * -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac \ + * app/test/test_first_row_title_render.c out/host/sdk_*.o \ + * -Wl,--wrap=meow_ui_text_height -lz -lpthread -lm -o test_first_row_title_render + * ./test_first_row_title_render # 产物落 $TMPDIR/first_row_*.ppm + * + * 断言: + * ① 行高替换生效(meow_ui_text_height(20) == 30); + * ② A:首行标题带零文字像素、副标题带有 → 复现「只有副标题」; + * ③ B:首行标题带与副标题带都有像素 → 修复后首行标题正常; + * ④ A/B 副标题带像素数都不为 0(副标题路径未受影响)。 + */ +#include "../ncm_app_internal.h" +#include +#include +#include +#include + +/* --wrap 的替换实现:真机 source_han_serif 20px 实测行高(font.idx ascent+descent)。 */ +int __wrap_meow_ui_text_height(int size) +{ + (void)size; + return 30; +} + +#define LV_X 16 +#define LV_Y 40 +#define LV_W 448 +#define LV_H 300 + +static int src2(int idx, meow_list_item *out, void *ctx) +{ + (void)ctx; + static char title[32], sub[32]; + if (idx < 0 || idx >= 5) return 0; + snprintf(title, sizeof title, "T%d", idx + 1); + snprintf(sub, sizeof sub, "S%d", idx + 1); + memset(out, 0, sizeof *out); + out->icon_kind = MEOW_LIST_ICON_NONE; + out->title = title; + out->subtitle = sub; + out->right = MEOW_LIST_RIGHT_NONE; + out->enabled = 1; + return 1; +} + +/* ---- 极简 PPM(P6) 读取 + 区域文字像素计数 ---- */ +static unsigned char *ppm_load(const char *path, int *w, int *h) +{ + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "open %s failed\n", path); return NULL; } + char magic[3] = {0}; + int maxv = 0; + if (fscanf(f, "%2s %d %d %d", magic, w, h, &maxv) != 4 || + strcmp(magic, "P6") != 0 || *w <= 0 || *h <= 0) { + fclose(f); fprintf(stderr, "bad ppm %s\n", path); return NULL; + } + if (fgetc(f) < 0) { fclose(f); fprintf(stderr, "truncated ppm %s\n", path); return NULL; } /* 单个空白 */ + size_t n = (size_t)*w * (size_t)*h * 3u; + unsigned char *buf = (unsigned char *)malloc(n); + if (!buf || fread(buf, 1, n, f) != n) { free(buf); fclose(f); return NULL; } + fclose(f); + return buf; +} + +/* 区域内「与页底色不同」的像素数。页底色取 (470,700)(远离列表控件)。 */ +static int ink_count(const unsigned char *img, int w, int x0, int y0, int x1, int y1) +{ + const unsigned char *bg = img + ((size_t)700 * (size_t)w + 470u) * 3u; + int n = 0; + for (int y = y0; y < y1; y++) + for (int x = x0; x < x1; x++) { + const unsigned char *p = img + ((size_t)y * (size_t)w + (size_t)x) * 3u; + int d = abs((int)p[0] - (int)bg[0]) + abs((int)p[1] - (int)bg[1]) + + abs((int)p[2] - (int)bg[2]); + if (d > 24) n++; + } + return n; +} + +/* 造一个双行列表(容器=关、无图标/右尾件)并整帧绘制(paint_full 内部 flush → dump PPM)。 */ +static void render_once(int item) +{ + meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, 5, + src2, NULL, MEOW_LIST_SEL_NONE); + meow_listview_set_container(lv, 0); + meow_listview_set_item_renderer(lv, 2, 0, 0); + meow_listview_set_item_h(lv, item); + meow_listview_set_count(lv, 5); + meow_ui_set_page_bg(meow_theme_get()->bg); + meow_ui_paint_full(); /* 画 + meow_ui_flush → MEOW_UI_DUMP 落 PPM */ + meow_widget_free(lv); +} + +static char g_dump[256]; + +int main(void) +{ + const char *td = getenv("TMPDIR"); + if (!td || !td[0]) td = "/tmp"; + snprintf(g_dump, sizeof g_dump, "%s/first_row.ppm", td); + setenv("MEOW_UI_DUMP", g_dump, 1); + + meow_ui_opts o = { APP_W, APP_H }; + if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; } + meow_theme_set_mode(MEOW_THEME_DARK); + + int fail = 0; + if (meow_ui_text_height(20) != 30) { + printf(" FAIL 行高替换未生效(expected 30,got %d)\n", meow_ui_text_height(20)); + fail = 1; + } + + const int lh = 30; + const int block = 2 * lh + NCM_ROW_SUB_GAP; /* 62 */ + const int fixed = ncm_row_h_for_line_h(lh); /* 70 */ + const int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */ + const int ty_fixed = (fixed - block) / 2; /* +4 */ + + int n_title_a = 0, n_sub_a = 0, n_title_b = 0, n_sub_b = 0; + + /* A:旧固定行高 56(bug 形态)。标题带取「文本行顶 ∩ 控件视口」(ty<0 时从 0 起), + * 这样「被裁剪守卫整条跳过」与「只画出上半截」也能区分开。 */ + render_once(APP_ROW_H); + { + int pw = 0, ph = 0; + unsigned char *img = ppm_load(g_dump, &pw, &ph); + if (!img) return 1; + int ty0 = (ty_legacy > 0) ? ty_legacy : 0; + n_title_a = ink_count(img, pw, LV_X + 16, LV_Y + ty0, + LV_X + 16 + 40, LV_Y + ty_legacy + lh); + n_sub_a = ink_count(img, pw, LV_X + 16, LV_Y + ty_legacy + lh + NCM_ROW_SUB_GAP, + LV_X + 16 + 40, LV_Y + ty_legacy + 2 * lh + NCM_ROW_SUB_GAP); + free(img); + } + + /* B:修复后的行高(ncm_row_h_for_line_h) */ + render_once(fixed); + { + int pw = 0, ph = 0; + unsigned char *img = ppm_load(g_dump, &pw, &ph); + if (!img) return 1; + n_title_b = ink_count(img, pw, LV_X + 16, LV_Y + ty_fixed, + LV_X + 16 + 40, LV_Y + ty_fixed + lh); + n_sub_b = ink_count(img, pw, LV_X + 16, LV_Y + ty_fixed + lh + NCM_ROW_SUB_GAP, + LV_X + 16 + 40, LV_Y + ty_fixed + 2 * lh + NCM_ROW_SUB_GAP); + free(img); + } + + printf("legacy item=%d ty=%d: title_ink=%d sub_ink=%d\n", APP_ROW_H, ty_legacy, n_title_a, n_sub_a); + printf("fixed item=%d ty=%d: title_ink=%d sub_ink=%d\n", fixed, ty_fixed, n_title_b, n_sub_b); + + if (!(n_title_a == 0 && n_sub_a > 0)) fail = 1; /* ② 旧行高首行标题被跳过(bug 复现) */ + if (!(n_title_b > 0 && n_sub_b > 0)) fail = 1; /* ③ 修复后首行标题/副标题都在 */ + + printf("%s: first-row title render (legacy skipped=%d, fixed drawn=%d)\n", + fail ? "FAIL" : "PASS", n_title_a == 0, n_title_b > 0); + return fail ? 1 : 0; +} diff --git a/patch_v2/ncm_app/app/test/test_meow_row_gap.c b/patch_v2/ncm_app/app/test/test_meow_row_gap.c index f347af6..9dfc876 100644 --- a/patch_v2/ncm_app/app/test/test_meow_row_gap.c +++ b/patch_v2/ncm_app/app/test/test_meow_row_gap.c @@ -9,10 +9,12 @@ * test_meow_row_gap.c -o test_meow_row_gap && ./test_meow_row_gap * * 断言集(对应「相邻 item 顶之间 = item 高 + gap」的可见形式): - * ① 相邻卡顶间距 = APP_ROW_H(= 卡高 + gap;SDK 行距不变); + * ① 相邻卡顶间距 = item 高(= 卡高 + gap;SDK 行距不变); * ② 卡片间可见外距 = NCM_ROW_GAP; - * ③ 卡在其行高内上下对称内缩 → SDK 文本(按 APP_ROW_H 垂直居中)落卡中心; + * ③ 卡在其行高内上下对称内缩 → SDK 文本(按行高垂直居中)落卡中心; * ④ 滚动偏移只整体平移,不改变上述间距/对称。 + * 行高在这里固定用 APP_ROW_H(几何对任意 item 成立;M12 起真机行高由 ncm_row_h() 按字体 + * 行高派生,文本块必须装得下,见 test_meow_row_text_fit.c)。 */ #include "../ncm_app_internal.h" /* APP_ROW_H / NCM_ROW_GAP / ncm_row_card_y / ncm_row_card_h */ #include @@ -20,7 +22,7 @@ int main(void) { - const int item = APP_ROW_H; /* 行距(SDK listview item_h,不变) */ + const int item = APP_ROW_H; /* 几何取固定行高(对任意 item 成立) */ const int gap = NCM_ROW_GAP; const int list_y = 100; diff --git a/patch_v2/ncm_app/app/test/test_meow_row_text_fit.c b/patch_v2/ncm_app/app/test/test_meow_row_text_fit.c new file mode 100644 index 0000000..1709f81 --- /dev/null +++ b/patch_v2/ncm_app/app/test/test_meow_row_text_fit.c @@ -0,0 +1,93 @@ +/* app/test/test_meow_row_text_fit.c — 宿主回归(M12):两行列表行高必须容纳行文本块。 + * + * 背景(真机根因):SDK meow_listview 的双行渲染(sdk/meow_ui_widget.c 的 2 行分支)把 + * block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行) + * 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。 + * 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在, + * 即用户报的「歌单列表/歌曲列表第 1 行只有副标题,名字空白」。 + * 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62,旧固定 + * 行高 56 → ty = 行顶 - 3。 + * + * 本测试只验 app 侧行高派生(ncm_row_h_for_line_h,纯整数、不链 SDK): + * 行高 = max(APP_ROW_H, block + NCM_ROW_GAP)(偶数)→ 卡高 == 文本块,文本完整落在卡内。 + * + * 编译/运行(同 test_meow_row_gap.c,需仓库根 ROOT 下的 SDK 头): + * gcc -O2 -Wall -Wextra -I. -I../sdk -I../meow_launcherd -Icore \ + * -Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c \ + * -Ivendor/minimp3 -Ivendor/dr_flac \ + * test_meow_row_text_fit.c -o test_meow_row_text_fit && ./test_meow_row_text_fit + * + * 断言矩阵 = 真机已装字库 20px 面实测行高(font.idx 的 ascent+descent): + * source_han_serif 30(当前生效)、misans/default 27、google_sans/meslo 26、 + * source_han_sans 21;14 = 宿主 5x7 回退。逐项断言: + * ① block ≤ item(否则居中偏移为负) + * ② 首行 ty ≥ 行顶(绝对:ty_abs ≥ 视口顶)→ 标题不被裁剪守卫跳过 + * ③ 副标题底边 sy + 行高 ≤ item(副标题不越行) + * ④ 卡高(item - NCM_ROW_GAP)≥ block(两行文本全落在圆角卡内) + * ⑤ item ≥ APP_ROW_H 下限且为偶数(卡上下对称内缩 gap/2) + * ⑥ 旧固定行高 56 在行高 30 下 ty < 0(bug 形态复现,防止再写死 56) + */ +#include "../ncm_app_internal.h" +#include + +static int g_checks, g_fail; + +static void chk(int cond, int line_h, const char *what) +{ + g_checks++; + if (!cond) { + g_fail++; + printf(" FAIL lh=%d: %s\n", line_h, what); + } +} + +int main(void) +{ + static const int lh_all[] = { 14, 21, 26, 27, 30 }; + const int n = (int)(sizeof lh_all / sizeof lh_all[0]); + const int list_y = 120; /* 任意列表顶(只验相对几何) */ + + for (int k = 0; k < n; k++) { + int lh = lh_all[k]; + int item = ncm_row_h_for_line_h(lh); + int block = 2 * lh + NCM_ROW_SUB_GAP; + int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */ + int sy = ty + lh + NCM_ROW_SUB_GAP; /* 副标题行顶 */ + int card_h = ncm_row_card_h(item); + + printf("lh=%2d item=%3d block=%3d ty=%d sy=%d card_h=%3d\n", + lh, item, block, ty, sy, card_h); + + chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)"); + chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶"); + chk(sy + lh <= item, lh, "副标题底边仍在行内"); + chk(card_h >= block, lh, "两行文本块必须完整落在圆角卡内"); + chk(item >= APP_ROW_H, lh, "行高不得小于 APP_ROW_H 下限"); + chk((item & 1) == 0, lh, "行高必须偶数(卡上下对称内缩 gap/2)"); + + /* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */ + int ty_abs = list_y + ty; + chk(ty_abs >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉"); + chk(ty_abs + lh <= list_y + item, lh, "首行标题底边在行内"); + + /* 卡在行内对称 → 卡中心 == 行中心 */ + int cy = ncm_row_card_y(item, 0, 0, list_y); + chk(cy + card_h / 2 == list_y + item / 2, lh, "卡中心 == 行中心"); + } + + /* 行高随字体行高单调不减 */ + chk(ncm_row_h_for_line_h(30) >= ncm_row_h_for_line_h(26), 30, "行高随字体行高单调不减"); + + /* ⑥ 回归哨兵:旧固定行高 56 + 当前默认字体(行高 30)就是 bug 形态 */ + { + const int lh = 30; + int block = 2 * lh + NCM_ROW_SUB_GAP; /* 62 */ + int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */ + chk(ty_legacy < 0, lh, "旧行高 56 + 行高 30 → ty<0(首行标题被跳过,bug 复现)"); + chk(ncm_row_h_for_line_h(lh) > APP_ROW_H, lh, "默认字体下行高必须大于旧值 56"); + } + + printf("%s: %d checks, %d fail (font line-height matrix: %d)\n", + g_fail ? "FAIL" : "PASS", g_checks, g_fail, n); + return g_fail ? 1 : 0; +} diff --git a/patch_v2/ncm_app/app/ui/cache.c b/patch_v2/ncm_app/app/ui/cache.c index 8c9ee87..5612bbf 100644 --- a/patch_v2/ncm_app/app/ui/cache.c +++ b/patch_v2/ncm_app/app/ui/cache.c @@ -25,6 +25,7 @@ static ncm_rect_t r_cont, r_list; static int g_bg_id, g_draw_id, g_empty_id, g_footer_id; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_last_count; +static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */ /* 扫描 worker(磁盘 IO 不进 UI 线程;gen 计数做完成信号) */ static pthread_mutex_t g_cmu = PTHREAD_MUTEX_INITIALIZER; @@ -89,7 +90,7 @@ static void c_row_bg(void *ctx) if (cnt <= 0) return; const meow_theme *th = meow_theme_get(); const ncm_pal_t *pa = ncm_palette(g_dark); - int item = APP_ROW_H; + int item = g_item_h; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; @@ -116,7 +117,7 @@ static void c_draw(void *ctx) const ncm_pal_t *pa = ncm_palette(g_dark); int cnt = appstate_cache_count(); if (cnt <= 0) return; - int item = APP_ROW_H, isz = 24; + int item = g_item_h, isz = 24; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; @@ -351,6 +352,7 @@ static void c_page_create(meow_view *v, void *ctx) (void)v; ((cache_ctrl *)ctx)->view = v; r_cont = ncm_content_rect(); + g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */ r_list = NR(16, r_cont.y + 8, r_cont.w - 32, r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8)); if (r_list.h < 0) r_list.h = 0; @@ -358,7 +360,7 @@ static void c_page_create(meow_view *v, void *ctx) g_tb = ncm_topbar_create("已缓存", 1, "清空"); /* 文本动作(动作0)=清空缓存 */ g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, c_src2, NULL, MEOW_LIST_SEL_NONE); - meow_listview_set_item_h(g_list, APP_ROW_H); + meow_listview_set_item_h(g_list, g_item_h); meow_ui_activity_pop(); meow_widget_set_z(g_list, 10); g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index e5c2f95..e497743 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -33,6 +33,7 @@ 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_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */ static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; /* 歌单加载后台 worker(P1:从 UI 线程挪出,gen 代数做完成信号)。 */ @@ -100,7 +101,7 @@ static void home_row_bg(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 item = g_item_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); @@ -194,7 +195,7 @@ static void home_draw(void *ctx) /* 歌单行:红 mini-vinyl 缩略 + 按压红条(文本由 listview 画在 z=10)。 */ if (g_load_err == 0 && g_st.playlist_count > 0) { - int item = APP_ROW_H; + int item = g_item_h; int off = meow_listview_scroll(g_list); int isz = 24; int first = off / item, last = (off + r_list.h) / item + 1; @@ -453,6 +454,7 @@ static void home_page_create(meow_view *v, void *ctx) ((home_ctrl *)ctx)->view = v; home_retry_reset(); /* M10:离线重试节奏闸(首次进离线立即探测一次) */ r_cont = ncm_content_rect(); + g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */ 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); @@ -481,7 +483,7 @@ static void home_page_create(meow_view *v, void *ctx) 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_listview_set_item_h(g_list, g_item_h); meow_ui_activity_pop(); /* 显式 z:行卡片(5) < listview(10) < tabbar(20) < 红态叠层(60) < topbar(999) */ meow_widget_set_z(g_list, 10); diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index 9564f30..c2dd74c 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -24,6 +24,7 @@ static int g_bg_id, g_draw_id, g_empty_id, g_footer_id; static char g_title[256]; static volatile int g_load_err; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; +static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */ /* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。 * §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页 @@ -125,7 +126,7 @@ static void pl_row_bg(void *ctx) if (meow_ui_activity_active() != ACT_PLAYLIST) return; if (g_load_err != 0 || g_st.track_ids_count == 0) return; const meow_theme *th = meow_theme_get(); - int item = APP_ROW_H; + int item = g_item_h; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > g_st.track_ids_count) last = g_st.track_ids_count; @@ -195,7 +196,7 @@ static void pl_draw(void *ctx) /* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */ if (g_load_err == 0 && g_st.track_ids_count > 0) { - int item = APP_ROW_H, isz = 24; + int item = g_item_h, isz = 24; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > g_st.track_ids_count) last = g_st.track_ids_count; @@ -238,8 +239,8 @@ static void pl_draw_footer(void *ctx) int total = g_st.track_ids_count; if (total <= 0) return; /* 视口位置 / 总数(滑窗模型下「已物化数」无用户意义,视口区间更直观) */ - int first = meow_listview_scroll(g_list) / APP_ROW_H + 1; - int last = first + r_list.h / APP_ROW_H; + int first = meow_listview_scroll(g_list) / g_item_h + 1; + int last = first + r_list.h / g_item_h; if (last > total) last = total; char foot[96]; snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total); @@ -361,8 +362,8 @@ static void pl_maybe_ensure(void) int total = g_st.track_ids_count; if (total <= 0) return; int scroll = meow_listview_scroll(g_list); - int first = scroll / APP_ROW_H; - int last = (scroll + r_list.h) / APP_ROW_H + 1; + int first = scroll / g_item_h; + int last = (scroll + r_list.h) / g_item_h + 1; if (last > total) last = total; int paged = g_paged_hi; if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */ @@ -581,6 +582,7 @@ static void pl_page_create(meow_view *v, void *ctx) (void)v; ((playlist_ctrl *)ctx)->view = v; r_cont = ncm_content_rect(); + g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */ r_card = NR(16, r_cont.y + 8, r_cont.w - 32, 148); r_cov = NR(r_card.x + 16, r_card.y + 20, 80, 80); int by = r_card.y + r_card.h + 12; @@ -594,7 +596,7 @@ static void pl_page_create(meow_view *v, void *ctx) g_tb = ncm_topbar_create("歌单", 1, NULL); g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, pl_src2, NULL, MEOW_LIST_SEL_NONE); - meow_listview_set_item_h(g_list, APP_ROW_H); + meow_listview_set_item_h(g_list, g_item_h); meow_ui_activity_pop(); meow_widget_set_z(g_list, 10); g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, diff --git a/patch_v2/ncm_app/app/ui/queue.c b/patch_v2/ncm_app/app/ui/queue.c index 199c3f8..c78b494 100644 --- a/patch_v2/ncm_app/app/ui/queue.c +++ b/patch_v2/ncm_app/app/ui/queue.c @@ -28,6 +28,7 @@ static ncm_rect_t r_cont, r_list; static int g_bg_id, g_draw_id, g_empty_id, g_footer_id; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_last_count, g_last_cur; +static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */ /* 队列补载(PLAYLIST)后台 worker 参数:目标区间 [lo,hi)(全局下标)。SEARCH 无需补载。 */ static pthread_mutex_t g_qmu = PTHREAD_MUTEX_INITIALIZER; @@ -92,7 +93,7 @@ static void q_row_bg(void *ctx) if (cnt <= 0) return; const meow_theme *th = meow_theme_get(); const ncm_pal_t *pa = ncm_palette(g_dark); - int item = APP_ROW_H; + int item = g_item_h; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; @@ -120,7 +121,7 @@ static void q_draw(void *ctx) const ncm_pal_t *pa = ncm_palette(g_dark); int cnt = appstate_queue_count(); if (cnt <= 0) return; - int item = APP_ROW_H, isz = 24; + int item = g_item_h, isz = 24; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; @@ -156,8 +157,8 @@ static void q_draw_footer(void *ctx) if (meow_ui_activity_active() != ACT_QUEUE) return; int total = appstate_queue_count(); if (total <= 0) return; - int first = meow_listview_scroll(g_list) / APP_ROW_H + 1; - int last = first + r_list.h / APP_ROW_H; + int first = meow_listview_scroll(g_list) / g_item_h + 1; + int last = first + r_list.h / g_item_h; if (last > total) last = total; char foot[96]; snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total); @@ -203,8 +204,8 @@ static void q_maybe_ensure(void) int total = appstate_queue_count(); if (total <= 0) return; int scroll = meow_listview_scroll(g_list); - int first = scroll / APP_ROW_H; - int last = (scroll + r_list.h) / APP_ROW_H + 1; + int first = scroll / g_item_h; + int last = (scroll + r_list.h) / g_item_h + 1; int lo = first - 24, hi = last + 24; if (lo < 0) lo = 0; if (hi > total) hi = total; @@ -325,6 +326,7 @@ static void q_page_create(meow_view *v, void *ctx) (void)v; ((queue_ctrl *)ctx)->view = v; r_cont = ncm_content_rect(); + g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */ r_list = NR(16, r_cont.y + 8, r_cont.w - 32, r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8)); if (r_list.h < 0) r_list.h = 0; @@ -332,7 +334,7 @@ static void q_page_create(meow_view *v, void *ctx) g_tb = ncm_topbar_create("播放列表", 1, NULL); g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, q_src2, NULL, MEOW_LIST_SEL_NONE); - meow_listview_set_item_h(g_list, APP_ROW_H); + meow_listview_set_item_h(g_list, g_item_h); meow_ui_activity_pop(); meow_widget_set_z(g_list, 10); g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, diff --git a/patch_v2/ncm_app/app/ui/search.c b/patch_v2/ncm_app/app/ui/search.c index 1a7005e..6bd8721 100644 --- a/patch_v2/ncm_app/app/ui/search.c +++ b/patch_v2/ncm_app/app/ui/search.c @@ -33,6 +33,7 @@ static ncm_rect_t r_cont, r_list, r_btn, r_tabs[2]; static int g_empty_id, g_footer_id, g_btn_id, g_tabbar_id; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_scroll_song, g_scroll_pl; /* 两页签各自滚动位置(§23)*/ +static int g_item_h = APP_ROW_H; /* 两行列表行高(M12:按字体行高派生,见 ncm_row_h) */ static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_t g_thr; @@ -206,7 +207,7 @@ static void sr_maybe_load_more(void) int n = appstate_search_count(g_tab); int cap = (g_tab == 0) ? NCM_SEARCH_SONG_MAX : NCM_SEARCH_PL_MAX; if (n <= 0 || n >= cap || !appstate_search_has_more(g_tab)) return; - int item = APP_ROW_H; + int item = g_item_h; int scroll = meow_listview_scroll(g_list); if (scroll + r_list.h < n * item - item * 2) return; /* 未近底 */ g_req_tab = g_tab; g_req_offset = n; g_req_replace = 0; @@ -445,6 +446,7 @@ static void sr_page_create(meow_view *v, void *ctx) (void)v; ((search_ctrl *)ctx)->view = v; r_cont = ncm_content_rect(); + g_item_h = ncm_row_h(); /* M12:行高按当前字体两行文本块派生(首行标题不被视口顶钳掉) */ ncm_rect_t field = NR(16, r_cont.y + 12, r_cont.w - 32, 48); r_btn = NR(field.x, field.y + field.h + 12, field.w, 44); int by = r_btn.y + r_btn.h + 8; @@ -462,7 +464,7 @@ static void sr_page_create(meow_view *v, void *ctx) sizeof g_st.search_query, MEOW_IME_PINYIN); g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, sr_src2, NULL, MEOW_LIST_SEL_NONE); - meow_listview_set_item_h(g_list, APP_ROW_H); + meow_listview_set_item_h(g_list, g_item_h); meow_ui_activity_pop(); meow_widget_set_z(g_list, 10); g_btn_id = meow_draw_custom_register(60, r_btn.x, r_btn.y, r_btn.w, r_btn.h, diff --git a/patch_v2/ncm_app/bench/plpage_sim.c b/patch_v2/ncm_app/bench/plpage_sim.c index 0ee9320..bbce939 100644 --- a/patch_v2/ncm_app/bench/plpage_sim.c +++ b/patch_v2/ncm_app/bench/plpage_sim.c @@ -161,14 +161,14 @@ static int cond_first_screen(void) return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1; } -/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */ +/* 滚动到指定首行(行高 ncm_row_h() px;swipe 距离按当前行差换算) */ static void scroll_to_row(int row) { static int cur; int d = row - cur; cur = row; playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN, - (d >= 0 ? d : -d) * APP_ROW_H); + (d >= 0 ? d : -d) * ncm_row_h()); } int main(void) @@ -176,15 +176,19 @@ int main(void) printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n", SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH); - /* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */ - setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0); + /* 宿主 SDK 离屏初始化(与 ncm_host 同口径)。临时目录优先 $TMPDIR(Termux/容器里 /tmp 常不可写) */ + const char *td = getenv("TMPDIR"); + if (!td || !td[0]) td = "/tmp"; + char dmp[256], cdt[256]; + snprintf(dmp, sizeof dmp, "%s/plpage_sim.ppm", td); + setenv("MEOW_UI_DUMP", dmp, 0); meow_ui_opts o = { APP_W, APP_H }; if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; } meow_theme_set_mode(MEOW_THEME_DARK); ncm_on_theme(MEOW_THEME_DARK); /* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */ - char cdt[] = "/tmp/plpage_cache_XXXXXX"; + snprintf(cdt, sizeof cdt, "%s/plpage_cache_XXXXXX", td); if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; } snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt); g_st.playlist_count = 1;