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

+46 -7
View File
@@ -6,11 +6,12 @@
* A3 边界:末页前进→下一册、首页后退→上一册(调用方语义);路径末尾斜杠等价;
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
* A6 书库扫描接线:有续读记录的册,行副标题出现「· 续读 Pn」角标。
* A6 书库扫描接线(M13 两段式):scan 只列单层、副标题占位「…」;
* probe/on_tick 补位后出现页数与「· 续读 Pn」角标。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
* test/settings_reader_stub.c -o test/test_cont_adjacent
* test/settings_reader_stub.c -pthread -o test/test_cont_adjacent
* 或 test/run_webtoon_tests.sh。
*/
#include "logic/library.h"
@@ -158,6 +159,20 @@ static void test_adjacent(const char *root)
CHECK(mr_book_adjacent("/", 1, out, (int)sizeof out) == -1, "root path invalid");
}
/* 模拟 UI 后台探测:逐行单层 probe 并补位(page>0 的书行由 format_subtitle 拼续读角标)。 */
static void probe_all_rows(mr_library *lib)
{
for (int i = 0; i < lib->count; i++) {
mr_library_probe p;
if (mr_library_probe_cached(lib->items[i].path, &p) == 0)
mr_library_apply_probe(lib, i, &p);
else
mr_library_mark_error(lib, i);
}
}
/* A6 两段式(M13):scan 只做单层枚举、行副标题是占位「…」;后台 probe/on_tick 补位后
* 才有页数与「· 续读 Pn」角标。 */
static void test_library_badge(const char *root)
{
mr_test_cfg_clear();
@@ -173,28 +188,52 @@ static void test_library_badge(const char *root)
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
CHECK(n > 0, "library scan n=%d", n);
/* 第一段:扫描期不判定,占位副标题。 */
int seen1 = 0, seen2 = 0;
for (int i = 0; i < lib.count; i++) {
mr_library_entry *e = &lib.items[i];
if (strcmp(e->name, "vol1") == 0) {
seen1 = 1;
CHECK(strstr(e->subtitle, "续读 P5") != NULL, "vol1 subtitle=%s", e->subtitle);
CHECK(strstr(e->subtitle, "page") != NULL, "vol1 keeps pages text: %s", e->subtitle);
CHECK(e->probe_state == MR_LIB_PENDING && e->pages == -1,
"vol1 pending after scan");
CHECK(strcmp(e->subtitle, "…") == 0, "vol1 placeholder subtitle=%s", e->subtitle);
}
if (strcmp(e->name, "vol2") == 0) {
seen2 = 1;
CHECK(strstr(e->subtitle, "续读") == NULL, "vol2 no badge: %s", e->subtitle);
CHECK(strcmp(e->subtitle, "1 page") == 0, "vol2 subtitle=%s", e->subtitle);
CHECK(e->probe_state == MR_LIB_PENDING && strcmp(e->subtitle, "…") == 0,
"vol2 placeholder subtitle=%s", e->subtitle);
}
}
CHECK(seen1 == 1, "vol1 listed");
CHECK(seen2 == 1, "vol2 listed");
/* 第二段:后台探测补位后,页数与续读角标出现。 */
probe_all_rows(&lib);
seen1 = seen2 = 0;
for (int i = 0; i < lib.count; i++) {
mr_library_entry *e = &lib.items[i];
if (strcmp(e->name, "vol1") == 0) {
seen1 = 1;
CHECK(e->has_images == 1 && e->pages == 1, "vol1 probed pages=%d", e->pages);
CHECK(strstr(e->subtitle, "page") != NULL, "vol1 keeps pages text: %s", e->subtitle);
CHECK(strstr(e->subtitle, "续读 P5") != NULL, "vol1 subtitle=%s", e->subtitle);
}
if (strcmp(e->name, "vol2") == 0) {
seen2 = 1;
CHECK(e->has_images == 1 && e->pages == 1, "vol2 probed pages=%d", e->pages);
CHECK(strstr(e->subtitle, "续读") == NULL, "vol2 no badge: %s", e->subtitle);
CHECK(strcmp(e->subtitle, "1 page") == 0, "vol2 subtitle=%s", e->subtitle);
}
}
CHECK(seen1 == 1, "vol1 listed after probe");
CHECK(seen2 == 1, "vol2 listed after probe");
mr_library_free(&lib);
/* 记录清除后角标消失 */
/* 记录清除后角标消失(重扫 + 再探测;探测缓存只存页数,角标按当前配置重算)。 */
mr_cont_save(vol1, 0, MR_MODE_WEBTOON);
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) > 0, "rescan");
probe_all_rows(&lib);
for (int i = 0; i < lib.count; i++)
if (strcmp(lib.items[i].name, "vol1") == 0)
CHECK(strstr(lib.items[i].subtitle, "续读") == NULL, "cleared badge gone");