图库: 单层浏览 + 懒判定与 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

+70 -26
View File
@@ -15,12 +15,13 @@
* T6 坏档容错:非 zip/截断/无图片页/声明 96MiB 的 zip 炸弹 → 打开失败或 0 页,不崩。
* T7 统一开册契约:目录册与档案册走同一个 mr_book(count/meta/open/close/destroy),
* 越界 open 返回 NULL,close_auto 幂等。
* T8 书库列档:.cbz 与文件夹册同列自然序(bookA < box < vol2.cbz < Vol10.cbz),
* 行=文件名+页数;坏档/无图档案不列;只含档案册的目录写 "N books";
* 不注入探测回调时行为与 M7 完全一致(纯目录书库)。
* T8 书库列档(M13 单层 + 懒判定):.cbz 与文件夹册同列自然序(bookA < box < ... <
* Vol10.cbz);扫描期只成行、副标题占位,探测补位后才有页数/书标记;
* 坏档/无图片页档案仍列出但为非书(访问放开,不再隐藏);
* 不注入探测回调时档案文件不成行(M7 语义),目录仍按单层列出。
* T9 册间衔接:同父目录自然序混排(bookA → vol2.cbz → Vol10.cbz),
* 首/末册返回 0;不注入探测回调时档案册不参与(M7 语义)。
* T10 续读角标:档案路径作为册标识,副标题后缀「· 续读 Pn」。
* T10 续读角标(M13 两段式):档案路径作为册标识,probe 补位后副标题后缀「· 续读 Pn」。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -Werror -O1 -I. test/test_cbz.c logic/library.c logic/reader.c \
@@ -715,19 +716,49 @@ static const mr_library_entry *lib_find(const mr_library *lib, const char *name)
return NULL;
}
/* M13:模拟 UI 后台探测补位(目录单层探测 / 档案册注入页数回调)。 */
static void lib_probe_all(mr_library *lib)
{
for (int i = 0; i < lib->count; i++) {
mr_library_probe p;
if (mr_library_probe_path(lib->items[i].path, lib->items[i].is_archive,
archive_pages_probe, NULL, &p) == 0)
mr_library_apply_probe(lib, i, &p);
else
mr_library_mark_error(lib, i);
}
}
static void test_library(void)
{
mr_test_cfg_clear();
/* M13:扫描只做单层枚举——目录(全部列出,含无图的 box)与档案文件(.cbz/.zip)
* 都先成行、副标题占位;书/非书与页数由后台 probe 补位。 */
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe, NULL);
CHECK(n == 4, "library rows=%d (want bookA, box, vol2.cbz, Vol10.cbz)", n);
if (n == 4) {
CHECK(strcmp(lib.items[0].name, "bookA") == 0, "row0=%s", lib.items[0].name);
CHECK(strcmp(lib.items[1].name, "box") == 0, "row1=%s", lib.items[1].name);
CHECK(strcmp(lib.items[2].name, "vol2.cbz") == 0, "row2=%s", lib.items[2].name);
CHECK(strcmp(lib.items[3].name, "Vol10.cbz") == 0, "row3=%s", lib.items[3].name);
CHECK(n == 8, "library rows=%d (want 2 dirs + 6 archives)", n);
static const char *want[8] = {
"bookA", "box", "broken.cbz", "emptypages.cbz",
"notabook.zip", "truncated.cbz", "vol2.cbz", "Vol10.cbz"
};
for (int i = 0; i < 8 && i < lib.count; i++) {
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
"row[%d]=%s (want %s)", i, lib.items[i].name, want[i]);
CHECK(lib.items[i].probe_state == MR_LIB_PENDING && lib.items[i].pages == -1,
"%s pending after scan", lib.items[i].name);
CHECK(strcmp(lib.items[i].subtitle, "…") == 0,
"%s placeholder subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
}
/* 后台补位(等价 UI 的 on_tick):目录走单层目录探测,档案走注入的 zip 页数回调。 */
for (int i = 0; i < lib.count; i++) {
mr_library_probe p;
int ok = (mr_library_probe_path(lib.items[i].path, lib.items[i].is_archive,
archive_pages_probe, NULL, &p) == 0);
CHECK(ok, "%s probed", lib.items[i].name);
if (ok) mr_library_apply_probe(&lib, i, &p);
}
const mr_library_entry *e = lib_find(&lib, "bookA");
@@ -737,15 +768,17 @@ static void test_library(void)
CHECK(e && strstr(e->cover, "001.png") != NULL, "bookA cover=%s",
e ? e->cover : "");
/* box 只含档案册:目录本身不直接含图 → 不是书(M13 不做递归/子表统计)。 */
e = lib_find(&lib, "box");
CHECK(e && e->has_images == 0 && e->subdirs == 0, "box fields");
CHECK(e && strcmp(e->subtitle, "2 books") == 0, "box subtitle=%s",
CHECK(e && e->has_images == 0 && e->subdirs == 0 && e->is_archive == 0,
"box fields");
CHECK(e && strcmp(e->subtitle, "无图片") == 0, "box subtitle=%s",
e ? e->subtitle : "");
CHECK(e && e->cover[0] == '\0', "box cover should be empty");
e = lib_find(&lib, "vol2.cbz");
CHECK(e && e->pages == 3 && e->has_images == 1 && e->subdirs == 0,
"vol2.cbz fields");
CHECK(e && e->pages == 3 && e->has_images == 1 && e->subdirs == 0 &&
e->is_archive == 1, "vol2.cbz fields");
CHECK(e && strcmp(e->subtitle, "3 pages") == 0, "vol2.cbz subtitle=%s",
e ? e->subtitle : "");
CHECK(e && e->cover[0] == '\0', "archive cover should be empty (UI extracts)");
@@ -756,19 +789,24 @@ static void test_library(void)
CHECK(e && e->pages == 1 && strcmp(e->subtitle, "1 page") == 0,
"Vol10.cbz pages/subtitle");
CHECK(lib_find(&lib, "notabook.zip") == NULL, "text-only zip listed");
CHECK(lib_find(&lib, "emptypages.cbz") == NULL, "no-image cbz listed");
CHECK(lib_find(&lib, "broken.cbz") == NULL, "broken cbz listed");
CHECK(lib_find(&lib, "truncated.cbz") == NULL, "truncated cbz listed");
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz listed");
/* 坏档/无图片页档案:列出但探测为非书(M13 访问放开,不再隐藏)。 */
static const char *not_book[] = { "notabook.zip", "emptypages.cbz",
"broken.cbz", "truncated.cbz" };
for (unsigned i = 0; i < sizeof not_book / sizeof not_book[0]; i++) {
e = lib_find(&lib, not_book[i]);
CHECK(e && e->is_archive == 1 && e->has_images == 0 && e->pages == 0 &&
strcmp(e->subtitle, "无图片") == 0,
"%s: not a book (subtitle=%s)", not_book[i], e ? e->subtitle : "(missing)");
}
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz not listed");
mr_library_free(&lib);
/* 不注入探测回调:与 M7 完全一致(档案册不参与,只含档案册的目录也不 nav)。 */
/* 不注入探测回调:M7 纯目录书库(档案文件不成行,目录仍按 M13 单层列出)。 */
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 1,
"legacy scan rows=%d (want 1: bookA)", lib.count);
CHECK(lib.count >= 1 && strcmp(lib.items[0].name, "bookA") == 0,
"legacy scan row0");
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 2,
"legacy scan rows=%d (want 2: bookA, box)", lib.count);
CHECK(lib.count >= 2 && strcmp(lib.items[0].name, "bookA") == 0 &&
strcmp(lib.items[1].name, "box") == 0, "legacy scan rows");
mr_library_free(&lib);
}
@@ -835,11 +873,16 @@ static void test_badge_cover(void)
mr_test_cfg_clear();
mr_cont_save(vol2, 2, MR_MODE_WEBTOON);
/* M13 两段式:扫描期只有占位副标题;后台 probe 补位后才出现页数与续读角标。 */
mr_library lib;
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe,
NULL) == 4, "rescan for badge");
NULL) == 8, "rescan for badge (single-layer rows)");
const mr_library_entry *e = lib_find(&lib, "vol2.cbz");
CHECK(e && strcmp(e->subtitle, "…") == 0, "vol2 placeholder before probe: %s",
e ? e->subtitle : "");
lib_probe_all(&lib);
e = lib_find(&lib, "vol2.cbz");
CHECK(e && strstr(e->subtitle, "续读 P3") != NULL, "badge subtitle=%s",
e ? e->subtitle : "");
CHECK(e && strstr(e->subtitle, "3 pages") != NULL, "badge keeps page text: %s",
@@ -848,10 +891,11 @@ static void test_badge_cover(void)
CHECK(e && strstr(e->subtitle, "续读") == NULL, "Vol10 should have no badge");
mr_library_free(&lib);
/* 清除记录后角标消失 */
/* 清除记录后角标消失(重扫 + 再探测;探测缓存只存页数,角标按当前配置重算)。 */
mr_cont_save(vol2, 0, MR_MODE_WEBTOON);
memset(&lib, 0, sizeof lib);
mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe, NULL);
lib_probe_all(&lib);
e = lib_find(&lib, "vol2.cbz");
CHECK(e && strstr(e->subtitle, "续读") == NULL, "cleared badge gone");
mr_library_free(&lib);