图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)

rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
This commit is contained in:
AskaEth committed 2026-11-05 12:23:31 +08:00
1 parent c9cff99af3
commit 9f34417c49
12 files changed
+2299 -358

No files matched your search

+32 -31
View File
@@ -3,12 +3,14 @@
* 覆盖:
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写;
* T2 扩展名过滤:png/jpg/jpeg/bmp 大小写不敏感,gif/txt 不支持;
* T3 目录扫描:只列出含图目录与可导航中间目录;隐藏/系统目录、空目录、无图目录排除;
* pages/subdirs/cover/subtitle 正确;父目录路径推导。
* T3 目录扫描(M13 单层语义):只列当前层直接子目录(自然升序),不递归探测子树;
* 系统目录/空目录/无图目录都列出;隐藏项/普通文件/符号链接不列;
* max_depth 兼容参数被忽略;行初始为「待探测」占位;父目录路径推导;
* T4 阅读方向状态(logic/reader.c)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_library.c logic/library.c logic/reader.c \
* -o test/test_library && ./test/test_library
* gcc -Wall -Wextra -O1 -I. -I../sdk test/test_library.c logic/library.c logic/reader.c \
* test/settings_reader_stub.c -pthread -o build/host/test_library
* 或直接 test/run_tests.sh。
*/
#include "logic/library.h"
@@ -148,44 +150,43 @@ static void test_scan(void)
snprintf(p, sizeof p, "%s/MeowAPPs", root); mkdir_p(p);
snprintf(p, sizeof p, "%s/MeowAPPs/m.png", root); touch_file(p);
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p); /* 根目录直接图片不成为行 */
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p); /* 普通文件不成为行 */
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
CHECK(n == 4, "scan count=%d (want 4)", n);
CHECK(n == 7, "scan count=%d (want 7: all direct subdirs)", n);
CHECK(lib.count == n, "count matches");
if (lib.count == 4) {
CHECK(strcmp(lib.items[0].name, "Nested") == 0, "sort[0]=Nested got %s", lib.items[0].name);
CHECK(strcmp(lib.items[1].name, "Series 2") == 0, "sort[1]=Series 2 got %s", lib.items[1].name);
CHECK(strcmp(lib.items[2].name, "Series 02") == 0, "sort[2]=Series 02 got %s", lib.items[2].name);
CHECK(strcmp(lib.items[3].name, "Series 10") == 0, "sort[3]=Series 10 got %s", lib.items[3].name);
CHECK(lib.truncated == 0, "not truncated");
if (lib.count == 7) {
static const char *want[7] = {
"Empty", "MeowAPPs", "Nested", "Series 2", "Series 02", "Series 3", "Series 10"
};
for (int i = 0; i < 7; i++)
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
"sort[%d]=%s got %s", i, want[i], lib.items[i].name);
}
const mr_library_entry *e = find_entry(&lib, "Series 10");
CHECK(e && e->pages == 2 && e->has_images == 1 && e->subdirs == 0, "Series 10 pages=2");
CHECK(e && strcmp(e->subtitle, "2 pages") == 0, "Series 10 subtitle '%s'", e ? e->subtitle : "");
CHECK(e && strstr(e->cover, "page1.png") != NULL, "Series 10 cover natural-first '%s'",
e ? e->cover : "");
e = find_entry(&lib, "Series 2");
CHECK(e && e->pages == 1 && e->has_images == 1, "Series 2 pages=1");
e = find_entry(&lib, "Nested");
CHECK(e && e->pages == 0 && e->has_images == 0 && e->subdirs == 1, "Nested intermediate");
CHECK(e && strcmp(e->subtitle, "1 folder") == 0, "Nested subtitle '%s'", e ? e->subtitle : "");
CHECK(e && strstr(e->cover, "Deeper/a.png") != NULL, "Nested cover from descendant '%s'",
e ? e->cover : "");
CHECK(find_entry(&lib, "Empty") == NULL, "empty dir excluded");
CHECK(find_entry(&lib, "Series 3") == NULL, "gif-only dir excluded");
/* 访问放开:系统目录/空目录/无图目录都列出;隐藏项/文件不列。 */
CHECK(find_entry(&lib, "MeowAPPs") != NULL, "system dir listed");
CHECK(find_entry(&lib, "Empty") != NULL, "empty dir listed");
CHECK(find_entry(&lib, "Series 3") != NULL, "no-image dir listed");
CHECK(find_entry(&lib, "Nested") != NULL, "intermediate dir listed");
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
CHECK(find_entry(&lib, "MeowAPPs") == NULL, "system dir excluded");
CHECK(find_entry(&lib, "root.png") == NULL, "loose image file excluded");
/* 深度 1:Nested 不再可探测到 Deeper 的图,应被排除。 */
/* 首帧不做懒判定:所有行待探测(副标题占位)。 */
for (int i = 0; i < lib.count; i++) {
CHECK(lib.items[i].pages == -1, "%s pages pending", lib.items[i].name);
CHECK(lib.items[i].has_images == -1, "%s book state pending", lib.items[i].name);
CHECK(lib.items[i].probe_state == MR_LIB_PENDING, "%s probe pending", lib.items[i].name);
CHECK(strcmp(lib.items[i].subtitle, "…") == 0, "%s subtitle placeholder", lib.items[i].name);
}
/* max_depth 参数只为兼容旧调用保留:不再递归,结果一致。 */
mr_library lib1;
memset(&lib1, 0, sizeof lib1);
CHECK(mr_library_scan(&lib1, root, 1) == 3, "max_depth=1 excludes Nested");
CHECK(mr_library_scan(&lib1, root, 1) == 7, "max_depth ignored (single layer)");
mr_library_free(&lib1);
mr_library_free(&lib);