图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)

用户反馈四件事一起改:
- 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路
  (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 +
  .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片=
  mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。
- 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内
  缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期
  生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。
  2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次
  (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。
- 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏
  「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。
- 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态
  「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。

集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/
普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比
mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。

reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push,
走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。

回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
This commit is contained in:
AskaEth committed 2026-11-05 20:26:40 +08:00
1 parent d07b99056f
commit 64336f95d9
16 files changed
+940 -1927

No files matched your search

+32 -94
View File
@@ -15,13 +15,12 @@
* T6 坏档容错:非 zip/截断/无图片页/声明 96MiB 的 zip 炸弹 → 打开失败或 0 页,不崩。
* T7 统一开册契约:目录册与档案册走同一个 mr_book(count/meta/open/close/destroy),
* 越界 open 返回 NULL,close_auto 幂等。
* T8 书库列档(M13 单层 + 懒判定):.cbz 与文件夹册同列自然序(bookA < box < ... <
* Vol10.cbz);扫描期只成行、副标题占位,探测补位后才有页数/书标记;
* 坏档/无图片页档案仍列出但为非书(访问放开,不再隐藏);
* 不注入探测回调时档案文件不成行(M7 语义),目录仍按单层列出。
* T8 书库列档(M18 文件管理器模型):目录 + .cbz/.zip 同列(目录优先、组内自然序,
* bookA < box < … < Vol10.cbz);扫描期一次成行,副标题是「文件夹/档案册 · 大小」,
* 不再探测页数/封面/续读角标;坏档/无图片页档案也照常列出(点开时才由 reader 判)。
* T9 册间衔接:同父目录自然序混排(bookA → vol2.cbz → Vol10.cbz),
* 首/末册返回 0;不注入探测回调时档案册不参与(M7 语义)。
* T10 续读角标(M13 两段式):档案路径作为册标识,probe 补位后副标题后缀「· 续读 Pn」。
* T10 封面解压 + 无续读角标:档案首图仍可解到缓存;书库行不再带「· 续读 Pn」。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -Werror -O1 -I. test/test_cbz.c logic/library.c logic/reader.c \
@@ -716,28 +715,15 @@ 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 补位。 */
/* 文件管理器模型:目录 + 档案册同列(目录优先、组内自然序),扫描期一次成行;
* 档案册只按扩展名成行,不探测页数/封面(打开时才由 reader 解档)。 */
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);
int n = mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH);
CHECK(n == 8, "library rows=%d (want 2 dirs + 6 archives)", n);
static const char *want[8] = {
"bookA", "box", "broken.cbz", "emptypages.cbz",
@@ -746,68 +732,33 @@ static void test_library(void)
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);
}
for (int i = 0; i < 2 && i < lib.count; i++)
CHECK(lib.items[i].kind == MR_LIB_KIND_DIR &&
strcmp(lib.items[i].subtitle, "文件夹") == 0,
"%s dir row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
for (int i = 2; i < 8 && i < lib.count; i++)
CHECK(lib.items[i].kind == MR_LIB_KIND_ARCHIVE &&
strncmp(lib.items[i].subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
"%s archive row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
const mr_library_entry *e = lib_find(&lib, "bookA");
CHECK(e && e->pages == 2 && e->has_images == 1 && e->subdirs == 0, "bookA fields");
CHECK(e && strcmp(e->subtitle, "2 pages") == 0, "bookA subtitle=%s",
e ? e->subtitle : "");
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 && 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");
CHECK(e && e->kind == MR_LIB_KIND_DIR && e->size == 0, "bookA dir entry");
e = lib_find(&lib, "vol2.cbz");
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)");
CHECK(e && strstr(e->path, "vol2.cbz") != NULL, "vol2.cbz path=%s",
e ? e->path : "");
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE && e->size > 0 &&
strstr(e->path, "vol2.cbz") != NULL,
"vol2.cbz archive entry (size=%lld)", e ? e->size : -1);
e = lib_find(&lib, "Vol10.cbz");
CHECK(e && e->pages == 1 && strcmp(e->subtitle, "1 page") == 0,
"Vol10.cbz pages/subtitle");
/* 坏档/无图片页档案:列出但探测为非书(M13 访问放开,不再隐藏)。 */
static const char *not_book[] = { "notabook.zip", "emptypages.cbz",
/* 坏档/无图片页档案也照常列出(书库不预判,点开时 reader 才判可读性)。 */
static const char *all_arch[] = { "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)");
for (unsigned i = 0; i < sizeof all_arch / sizeof all_arch[0]; i++) {
e = lib_find(&lib, all_arch[i]);
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE, "%s listed as archive", all_arch[i]);
}
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz not listed");
mr_library_free(&lib);
/* 不注入探测回调:M7 纯目录书库(档案文件不成行,目录仍按 M13 单层列出)。 */
memset(&lib, 0, sizeof lib);
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);
}
/* ============================================================ */
@@ -863,7 +814,7 @@ static void test_adjacent(void)
}
/* ============================================================ */
/* T10 续读角标 + 封面 */
/* T10 封面解压 + 无续读角标 */
/* ============================================================ */
static void test_badge_cover(void)
{
@@ -873,31 +824,18 @@ static void test_badge_cover(void)
mr_test_cfg_clear();
mr_cont_save(vol2, 2, MR_MODE_WEBTOON);
/* M13 两段式:扫描期只有占位副标题;后台 probe 补位后才出现页数与续读角标。 */
/* 文件管理器模型:书库行只有「档案册 · 大小」,不再有「· 续读 Pn」角标
* (续读仍由 reader 打开该册时恢复,见 test_mode_switch_host)。 */
mr_library lib;
memset(&lib, 0, sizeof lib);
CHECK(mr_library_scan_ex(&lib, g_lib, MR_LIB_MAX_DEPTH, archive_pages_probe,
NULL) == 8, "rescan for badge (single-layer rows)");
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 8, "rescan 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",
CHECK(e && strstr(e->subtitle, "续读") == NULL,
"no continuation badge in file-manager rows: %s", e ? e->subtitle : "");
CHECK(e && strstr(e->subtitle, "档案册") != NULL, "archive subtitle=%s",
e ? e->subtitle : "");
e = lib_find(&lib, "Vol10.cbz");
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");
CHECK(e && strstr(e->subtitle, "续读") == NULL, "Vol10 no badge either");
mr_library_free(&lib);
/* 封面 = 档内自然序首图(ch01/p2.png),解压到缓存 */