图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)

P1-1(会静默开错图):取消「书库从列表推导图片下标」。
- 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的
  全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确,
  再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。
- 删除 mr_library_image_index(不再有跨枚举的隐式契约)。
- 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。
- 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/
  library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411);
  评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**,
  2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。

连带处理(评审 r1):
1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth);
   顺手删掉 library_page_create 里同款死写。
2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted,
   book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册;
   根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、
   rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。
3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子
   mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。
4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。

回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例)
与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
This commit is contained in:
AskaEth committed 2026-11-05 20:26:40 +08:00
1 parent 64336f95d9
commit 4befbfe14c
17 files changed
+523 -112

No files matched your search

+3 -1
View File
@@ -11,8 +11,10 @@
- 列表 = 当前目录一层的**子目录 + 受支持图片(png/jpg/jpeg/bmp)+ 档案册(.cbz/.zip)**; - 列表 = 当前目录一层的**子目录 + 受支持图片(png/jpg/jpeg/bmp)+ 档案册(.cbz/.zip)**;
目录优先,组内自然序(大小写不敏感,同键按原始字节兜底,`A.jpg`/`a.jpg` 不抖动)。 目录优先,组内自然序(大小写不敏感,同键按原始字节兜底,`A.jpg`/`a.jpg` 不抖动)。
- 点目录 = 进入(空目录也进);点图片 = 从该图开始读(下标与阅读器目录册同一套过滤器/比较器); - 点目录 = 进入(空目录也进);点图片 = 从该图开始读(按**文件名**交给阅读器,
由阅读器的全量目录册定位起始页——列表被 `MR_LIB_SCAN_MAX` 截断时也不会点错图);
点档案册 = 打开档案册(第 1 页/续读)。 点档案册 = 打开档案册(第 1 页/续读)。
- 单目录超过 2000 项时列表只显示前 2000 项并给出「已截断」提示行;图片仍可进入阅读器翻到。
- 缩略图功能已关闭:行内只有类型图标 + 文件名 + 类型·大小;首帧只做单层 - 缩略图功能已关闭:行内只有类型图标 + 文件名 + 类型·大小;首帧只做单层
`opendir/readdir + lstat`,滚动/点击路径不解码、不访问文件、不重排字符串。 `opendir/readdir + lstat`,滚动/点击路径不解码、不访问文件、不重排字符串。
- 浏览根锁定 TF 卡根(`/usr/data/mnt/sd_0`):根目录不显示返回箭头,左缘返回 = 退出 App; - 浏览根锁定 TF 卡根(`/usr/data/mnt/sd_0`):根目录不显示返回箭头,左缘返回 = 退出 App;
+32 -9
View File
@@ -257,13 +257,16 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out) int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
{ {
static char trunc_sub[48]; static char trunc_sub[96];
static int trunc_ready; static int trunc_ready;
if (!lib || !out) return 0; if (!lib || !out) return 0;
if (idx == lib->count && lib->truncated) { if (idx == lib->count && lib->truncated) {
if (!trunc_ready) { if (!trunc_ready) {
snprintf(trunc_sub, sizeof trunc_sub, "仅显示前 %d 项", MR_LIB_SCAN_MAX); /* 截断只是列表显示上限:图片仍能从任一可见图片进入阅读器后翻到,
* 文案不要让人误以为「没有更多图片」。 */
snprintf(trunc_sub, sizeof trunc_sub,
"仅显示前 %d 项(图片可在阅读器中翻到)", MR_LIB_SCAN_MAX);
trunc_ready = 1; trunc_ready = 1;
} }
memset(out, 0, sizeof *out); memset(out, 0, sizeof *out);
@@ -284,14 +287,15 @@ int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
return 1; return 1;
} }
int mr_library_image_index(const mr_library *lib, int idx) int mr_path_within_root(const char *path, const char *root)
{ {
if (!lib || idx < 0 || idx >= lib->count) return -1; if (!path || !path[0] || !root || !root[0]) return 0;
if (lib->items[idx].kind != MR_LIB_KIND_IMAGE) return -1; size_t rl = strlen(root);
int n = 0; while (rl > 1 && root[rl - 1] == '/') rl--;
for (int i = 0; i < idx; i++) if (rl == 1 && root[0] == '/') return path[0] == '/';
if (lib->items[i].kind == MR_LIB_KIND_IMAGE) n++; if (strncmp(path, root, rl) != 0) return 0;
return n; char c = path[rl];
return c == '\0' || c == '/';
} }
int mr_path_parent(const char *path, char *out, int outsz) int mr_path_parent(const char *path, char *out, int outsz)
@@ -433,6 +437,25 @@ int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
return rc; return rc;
} }
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
mr_lib_archive_fn archive_pages, void *ud,
char *out, int outsz)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || !root || !root[0]) return -1;
/* 只允许在浏览根内翻册:当前册或相邻册越根一律当「没有相邻册」。 */
if (!mr_path_within_root(dir, root)) return 0;
int r = mr_book_adjacent_ex(dir, delta, out, outsz, archive_pages, ud);
if (r != 1) return r;
if (!mr_path_within_root(out, root)) {
out[0] = '\0';
return 0;
}
return 1;
}
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz) int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
{ {
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL); return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
+11 -6
View File
@@ -86,12 +86,9 @@ int mr_path_parent(const char *path, char *out, int outsz);
* 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */ * 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out); int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out);
/* 图片行在该目录图片自然序中的下标(0 基);idx 不是图片行/越界返回 -1。 /* path 是否落在浏览根内(等于 root 或以 "root/" 开头)。1=在内;0=越界/参数非法。
* * 根锁定用:书库导航与阅读器「相邻册」都不得越过 SD 卡根。 */
* ⚠️ 这是书库→阅读器的契约:阅读器的目录册(logic/reader/page_source.c)只枚举 int mr_path_within_root(const char *path, const char *root);
* 「非隐藏 + mr_path_is_image + 普通文件」并按 mr_natural_cmp 排序。书库扫描用的是
* 同一套过滤器与比较器,因此本函数数出的下标就是 mr_reader_open_dir_at() 的起始页。 */
int mr_library_image_index(const mr_library *lib, int idx);
/* 目录是否**直接**含受支持图片。1=有;0=没有/不可读。 /* 目录是否**直接**含受支持图片。1=有;0=没有/不可读。
* 仅用于阅读器的「相邻册」判定,不再是书库行语义。 */ * 仅用于阅读器的「相邻册」判定,不再是书库行语义。 */
@@ -111,6 +108,14 @@ typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz, int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
mr_lib_archive_fn archive_pages, void *ud); mr_lib_archive_fn archive_pages, void *ud);
/* 册间衔接(根锁定版):在 mr_book_adjacent_ex 之上拒绝浏览根之外的相邻册。
* dir 必须在 root 内;找到的相邻册也必须在 root 内,否则按「没有相邻册」返回 0。
* 1=找到并写入 out;0=没有相邻册或越根被拒(out 为空串);-1=参数非法。
* 阅读器用它保证「读到末页翻册」不会跳到 SD 卡根之外(例如挂载点以上的其它目录)。 */
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
mr_lib_archive_fn archive_pages, void *ud,
char *out, int outsz);
#ifdef MR_LIBRARY_TEST_HOOKS #ifdef MR_LIBRARY_TEST_HOOKS
/* 宿主测试用 IO 计数(scan/dir_has_images 的 opendir/readdir/lstat 次数;线程安全不保证)。 */ /* 宿主测试用 IO 计数(scan/dir_has_images 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
typedef struct mr_library_io_stats { typedef struct mr_library_io_stats {
+1 -1
View File
@@ -323,7 +323,7 @@ int mr_cache_store(const char *path, const void *data, size_t len)
int i = idx_find_locked(path); int i = idx_find_locked(path);
if (i >= 0) idx_remove_locked(i); if (i >= 0) idx_remove_locked(i);
make_room_locked((long long)len); make_room_locked((long long)len);
pthread_mutex_unlock(&g_lock); /* 写盘不持锁:大页不阻塞缩略图线程 */ pthread_mutex_unlock(&g_lock); /* 写盘不持锁:大页不阻塞并发读者 */
FILE *f = fopen(tmp, "wb"); FILE *f = fopen(tmp, "wb");
int ok = f && fwrite(data, 1, len, f) == len; int ok = f && fwrite(data, 1, len, f) == len;
+6 -7
View File
@@ -5,21 +5,20 @@
* 解码回调按路径解码——两种阅读模式(翻页/条漫)与目录册走完全相同的代码路径。 * 解码回调按路径解码——两种阅读模式(翻页/条漫)与目录册走完全相同的代码路径。
* *
* 目录与上限(纪律): * 目录与上限(纪律):
* - 目录 = <数据目录>/cache(数据目录形如 .../MeowData/os.meow.picbrowser/data, * - 目录 = <数据目录>/cache(数据目录形如 .../MeowData/os.meow.picbrowser/data);
* 与缩略图缓存同目录;文件名统一前缀 mrpage_,与缩略图 *.bmp 互不影响); * 文件名统一前缀 mrpage_,与其它可能落在这目录的临时文件互不影响;
* - 上限 = 64 页 或 32MiB,先到者触发 LRU 淘汰(按最近使用计数); * - 上限 = 64 页 或 32MiB,先到者触发 LRU 淘汰(按最近使用计数);
* - **单条目溢出**:某页解压后本身就超过 32MiB 时允许一次性溢出——只保留该条、 * - **单条目溢出**:某页解压后本身就超过 32MiB 时允许一次性溢出——只保留该条、
* 其它条目全部淘汰,占用上限受页源单页上限 64MiB(MR_ZIP_PAGE_MAX_BYTES)约束。 * 其它条目全部淘汰,占用上限受页源单页上限 64MiB(MR_ZIP_PAGE_MAX_BYTES)约束。
* 这样超大页仍可阅读,不会因为预算拒绝落缓存而变成永久空白页; * 这样超大页仍可阅读,不会因为预算拒绝落缓存而变成永久空白页;
* - mr_cache_init() 首次成功时清掉上一进程遗留的 mrpage_*(孤儿页) * - mr_cache_init() 首次成功时清掉上一进程遗留的 mrpage_*(孤儿页);
* (注意:缩略图 LRU 也必须在同一目录里跳过 mrpage_*,否则会互删);
* - 写入走 "<name>.part" + rename,进程崩溃不会留下半个缓存文件。 * - 写入走 "<name>.part" + rename,进程崩溃不会留下半个缓存文件。
* *
* 键 = 档案路径 + 档案 mtime/size + 条目名 + 页下标:档案被替换(mtime/size 变)或 * 键 = 档案路径 + 档案 mtime/size + 条目名 + 页下标:档案被替换(mtime/size 变)或
* 同名条目重复出现时不会命中旧内容。文件名保留图片扩展名(解码入口按扩展名区分 * 同名条目重复出现时不会命中旧内容。文件名保留图片扩展名(解码入口按扩展名区分
* JPEG 的「解码期缩解」路径)。 * JPEG 的「解码期缩解」路径)。
* *
* 线程:内部互斥量;可在解码线程/缩略图线程/主线程并发调用。 * 线程:内部互斥量;可在解码线程/主线程并发调用。
* 纯 C:宿主测试可直接编译(见 tuku/test/test_cbz.c)。 * 纯 C:宿主测试可直接编译(见 tuku/test/test_cbz.c)。
*/ */
#ifndef MR_PAGE_CACHE_H #ifndef MR_PAGE_CACHE_H
@@ -42,8 +41,8 @@ extern "C" {
* 成功 0;参数非法/缓冲不足 -1。 */ * 成功 0;参数非法/缓冲不足 -1。 */
int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz); int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz);
/* 文件名是否属于解压页缓存(mrpage_ 前缀)。缩略图 LRU 与启动清孤儿共用这一判据, /* 文件名是否属于解压页缓存(mrpage_ 前缀)。启动清孤儿用这一判据,
* 保证两套缓存同目录时互不误删(P2-1)。1=解压页缓存。 */ * 保证同目录的其它文件不被误删。1=解压页缓存。 */
int mr_cache_is_page_name(const char *name); int mr_cache_is_page_name(const char *name);
/* 建立/切换缓存目录并清孤儿。0=可用;-1=不可用(之后 store/touch 均失败)。 /* 建立/切换缓存目录并清孤儿。0=可用;-1=不可用(之后 store/touch 均失败)。
+2 -2
View File
@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。 # run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
# - test_library:单层目录扫描(目录+图片+档案册)/自然排序/图片下标/父目录/阅读方向 # - test_library:单层目录扫描(目录+图片+档案册)/自然排序/父目录/根前缀判据/阅读方向
# - test_browse_nav:访问放开(系统/空/无图目录可进入)、图片与档案册成行、截断、不可读 # - test_browse_nav:访问放开(系统/空/无图目录可进入)、图片与档案册成行、截断、不可读
# - test_browse_lazy:行合成零 IO/副标题预生成/图片下标 == 阅读器目录册下标(集成陷阱) # - test_browse_lazy:行合成零 IO/副标题预生成/书库行与阅读器目录册同序(按名定位契约)
# - test_browse_perf:500 目录×8 层嵌套 + 2000 项大目录,断言单层扫描与行合成成本 # - test_browse_perf:500 目录×8 层嵌套 + 2000 项大目录,断言单层扫描与行合成成本
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/ # - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
# 坏值/非法值/只读一次/写失败只告警) # 坏值/非法值/只读一次/写失败只告警)
+94 -27
View File
@@ -1,12 +1,15 @@
/* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 图片下标一致性(纯 C,无 SDK)。 /* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 书库行与阅读器目录册同序(纯 C,无 SDK)。
* *
* 文件名沿用历史(旧的「懒判定」已随 M18 文件管理器模型删除);本测试现在覆盖: * 文件名沿用历史(旧的「懒判定」已随 M18 文件管理器模型删除);本测试现在覆盖:
* L1 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行; * L1 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
* L2 **书库→阅读器下标记号契约**:mr_library_image_index 数出的下标 == 阅读器目录册 * L2 **书库行与阅读器目录册同源**:书库图片行的名称序列 == 阅读器目录册
* (logic/reader/page_source.c: mr_dir_book_init)里同一张图的下标。 * (logic/reader/page_source.c: mr_dir_book_init)的页名序列(同过滤器/同比较器)。
* 点图现在由阅读器按文件名自己定位下标,书库不再推导下标;本用例守住「两边枚举一致」。
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、 * 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件; * 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化)。 * L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化);
* L4 截断安全(评审 P1-1):2100 张图(默认 cap=2000)时,2000 个可见图片行按文件名
* 都能定位到同一文件(0 mismatch),被截掉的图仍在阅读器页表里可翻到。
* *
* 编译(在 tuku/ 下): * 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \ * gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
@@ -137,8 +140,8 @@ static void test_rows_zero_io(const char *root)
mr_library_free(&lib); mr_library_free(&lib);
} }
/* ---- L2:书库图片下标 == 阅读器目录册下标 ---- */ /* ---- L2:书库图片行名称序列 == 阅读器目录册页名序列 ---- */
static void test_image_index_matches_reader(const char *dir) static void test_library_rows_match_reader(const char *dir)
{ {
mr_library lib; mr_library lib;
memset(&lib, 0, sizeof lib); memset(&lib, 0, sizeof lib);
@@ -149,25 +152,15 @@ static void test_image_index_matches_reader(const char *dir)
int pages = mr_dir_book_init(&db, dir, 720, fake_load, fake_unload, NULL); int pages = mr_dir_book_init(&db, dir, 720, fake_load, fake_unload, NULL);
CHECK(pages > 0, "reader dir book pages=%d", pages); CHECK(pages > 0, "reader dir book pages=%d", pages);
/* 书库里的图片行按出现顺序,其下标必须依次是 0..n-1(无空洞/无重排)。 */ int images = 0;
int expect = 0; for (int i = 0; i < lib.count; i++)
for (int i = 0; i < lib.count; i++) { if (lib.items[i].kind == MR_LIB_KIND_IMAGE) images++;
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue; CHECK(images == pages, "library images=%d vs reader pages=%d", images, pages);
int idx = mr_library_image_index(&lib, i);
CHECK(idx == expect, "image row %s index=%d (want %d)",
lib.items[i].name, idx, expect);
expect++;
}
CHECK(expect == pages, "library images=%d vs reader pages=%d", expect, pages);
if (expect != pages) {
mr_library_free(&lib);
mr_dir_book_free(&db);
return;
}
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(下标即起始页)。 */ /* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(同过滤器/同自然序)。
* 点图时阅读器按文件名重新定位下标,所以「名字集合 + 顺序」一致就是契约。 */
int k = 0; int k = 0;
for (int i = 0; i < lib.count; i++) { for (int i = 0; i < lib.count && k < pages; i++) {
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue; if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
char page_path[MR_PATH_MAX]; char page_path[MR_PATH_MAX];
mr_dir_book_path(&db, k, page_path, (int)sizeof page_path); mr_dir_book_path(&db, k, page_path, (int)sizeof page_path);
@@ -176,17 +169,90 @@ static void test_image_index_matches_reader(const char *dir)
k++; k++;
} }
/* 档案册/目录不占图片下标。 */ /* 目录/档案册不是页:它们的名字不能出现在阅读器页名里。 */
for (int i = 0; i < lib.count; i++) { for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) continue; if (lib.items[i].kind == MR_LIB_KIND_IMAGE) continue;
CHECK(mr_library_image_index(&lib, i) == -1, int found = 0;
"non-image %s has no image index", lib.items[i].name); for (int j = 0; j < pages; j++)
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) found = 1;
CHECK(found == 0, "non-image %s is not a reader page", lib.items[i].name);
} }
mr_dir_book_free(&db); mr_dir_book_free(&db);
mr_library_free(&lib); mr_library_free(&lib);
} }
/* ---- L4:2100 张图(默认 cap)——列表截断但按名定位 2000/2000 不错位 ---- */
static void test_cap_name_lookup(const char *root)
{
char big[MR_PATH_MAX], p[MR_PATH_MAX];
joinp(big, sizeof big, root, "big");
CHECK(mkdir_p(big) == 0, "mkdir big");
for (int i = 1; i <= 2100; i++) {
snprintf(p, sizeof p, "%s/p%04d.jpg", big, i);
if (touch_file(p) != 0) { CHECK(0, "big image %s", p); return; }
}
mr_library lib;
memset(&lib, 0, sizeof lib);
int n = mr_library_scan(&lib, big, MR_LIB_MAX_DEPTH);
CHECK(n == MR_LIB_SCAN_MAX, "big dir lists cap rows (%d)", n);
CHECK(lib.truncated == 1 && lib.count == MR_LIB_SCAN_MAX, "big dir truncated at cap");
mr_dir_book db;
memset(&db, 0, sizeof db);
CHECK(mr_dir_book_init(&db, big, 720, fake_load, fake_unload, NULL) == 2100,
"reader enumerates all 2100 pages");
/* 书库不再推导下标;点击传文件名。这里验证 2000 个可见图片行按名定位
* 都能命中**同一个文件**(0 mismatch),被截掉的 100 张也仍在页表里。 */
int checked = 0, mismatch = 0;
for (int i = 0; i < lib.count; i++) {
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
checked++;
int idx = -1;
for (int j = 0; j < db.count; j++)
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) { idx = j; break; }
char page[MR_PATH_MAX];
mr_dir_book_path(&db, idx, page, (int)sizeof page);
if (idx < 0 || strcmp(page, lib.items[i].path) != 0) mismatch++;
}
CHECK(checked == MR_LIB_SCAN_MAX, "checked %d library image rows", checked);
CHECK(mismatch == 0, "name lookup mismatch=%d (want 0)", mismatch);
/* 被截掉的图(列表外的那 100 张)在阅读器页表里仍存在(可翻到)。
* 截断发生在 readdir 顺序(非自然序),不假定是哪 100 张,按集合差验证。 */
int missing = 0, missing_idx = -1;
char missing_name[MR_NAME_MAX] = "";
for (int j = 0; j < db.count; j++) {
int in_list = 0;
for (int i = 0; i < lib.count; i++)
if (strcmp(lib.items[i].name, db.names[j]) == 0) { in_list = 1; break; }
if (!in_list) {
if (missing == 0) {
missing_idx = j;
snprintf(missing_name, sizeof missing_name, "%s", db.names[j]);
}
missing++;
}
}
CHECK(missing == 2100 - MR_LIB_SCAN_MAX, "truncated-out images=%d (want 100)", missing);
char want_path[MR_PATH_MAX], page[MR_PATH_MAX];
snprintf(want_path, sizeof want_path, "%s/%s", big, missing_name);
mr_dir_book_path(&db, missing_idx, page, (int)sizeof page);
CHECK(missing_idx >= 0 && strcmp(page, want_path) == 0,
"truncated-out %s is still reader page %d", missing_name, missing_idx);
mr_library_row row;
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1 &&
strstr(row.subtitle, "2000") && strstr(row.subtitle, "阅读器"),
"truncation row keeps images discoverable: %s",
row.subtitle ? row.subtitle : "");
mr_dir_book_free(&db);
mr_library_free(&lib);
}
int main(void) int main(void)
{ {
char tmpl[] = "./mr_rows_XXXXXX"; char tmpl[] = "./mr_rows_XXXXXX";
@@ -219,7 +285,8 @@ int main(void)
test_rows_zero_io(root); test_rows_zero_io(root);
joinp(p, sizeof p, root, "mixed"); joinp(p, sizeof p, root, "mixed");
test_image_index_matches_reader(p); test_library_rows_match_reader(p);
test_cap_name_lookup(root);
rm_rf(root); rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
-2
View File
@@ -165,8 +165,6 @@ int main(void)
CHECK(strcmp(biglib.items[PERF_BIG - 1].name, "img1999.png") == 0, CHECK(strcmp(biglib.items[PERF_BIG - 1].name, "img1999.png") == 0,
"last=%s", biglib.items[PERF_BIG - 1].name); "last=%s", biglib.items[PERF_BIG - 1].name);
CHECK(biglib.items[PERF_BIG - 1].kind == MR_LIB_KIND_IMAGE, "last is image"); CHECK(biglib.items[PERF_BIG - 1].kind == MR_LIB_KIND_IMAGE, "last is image");
CHECK(mr_library_image_index(&biglib, PERF_BIG - 1) == PERF_BIG - 1,
"image index at end");
} }
printf(" big dir scan: %lldus, %d rows, opendir=%ld lstat=%ld\n", printf(" big dir scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
big_us, bn, st.opendirs, st.lstats); big_us, bn, st.opendirs, st.lstats);
+17 -11
View File
@@ -5,7 +5,8 @@
* 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分; * 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分;
* U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问); * U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问);
* U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读 * U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读
* (mr_reader_open_dir_at(当前目录, 图片下标));点档案册=打开档案册; * (mr_reader_open_dir_at_name(当前目录, 文件名);阅读器自己定位下标);
* 点档案册=打开档案册;
* U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出); * U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出);
* 子目录返回=回根;不存在越过浏览根的导航; * 子目录返回=回根;不存在越过浏览根的导航;
* U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」, * U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」,
@@ -47,18 +48,28 @@ int library_test_go_up(void);
int library_test_drag_active(void); int library_test_drag_active(void);
int library_test_has_list(void); int library_test_has_list(void);
/* 阅读器入口打桩:只记录被打开的册路径与起始页(本测试不建 reader 页)。 */ /* 阅读器入口打桩:记录被打开的册路径 / 起始页 / 按名定位的文件名(本测试不建 reader 页)。 */
#define MR_READER_START_RESUME (-1) #define MR_READER_START_RESUME (-1)
void mr_reader_open_dir_at(const char *dir, int start_index); void mr_reader_open_dir_at(const char *dir, int start_index);
void mr_reader_open_dir_at_name(const char *dir, const char *name);
static char g_opened[MR_PATH_MAX]; static char g_opened[MR_PATH_MAX];
static char g_opened_name[MR_NAME_MAX];
static int g_opened_start; static int g_opened_start;
static int g_opened_n; static int g_opened_n;
void mr_reader_open_dir_at(const char *dir, int start_index) void mr_reader_open_dir_at(const char *dir, int start_index)
{ {
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : ""); snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
g_opened_name[0] = '\0';
g_opened_start = start_index; g_opened_start = start_index;
g_opened_n++; g_opened_n++;
} }
void mr_reader_open_dir_at_name(const char *dir, const char *name)
{
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
snprintf(g_opened_name, sizeof g_opened_name, "%s", name ? name : "");
g_opened_start = MR_READER_START_RESUME;
g_opened_n++;
}
static int g_fail; static int g_fail;
#define CHECK(cond, ...) do { \ #define CHECK(cond, ...) do { \
@@ -279,22 +290,17 @@ int main(void)
CHECK(library_test_load(pics_dir) == 1, "entered Pictures"); CHECK(library_test_load(pics_dir) == 1, "entered Pictures");
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir()); CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir());
/* 图片下标 = 该目录里排在该图之前的图片行数(阅读器目录册同序)。 */ /* 点图片:把**文件名**交给阅读器(阅读器用自己的全量目录册定位起始页;
* 书库列表可能被 MR_LIB_SCAN_MAX 截断,所以不再传下标)。 */
const char *target = "img10.png"; const char *target = "img10.png";
int ti = row_index_for(target); int ti = row_index_for(target);
CHECK(ti >= 0, "target image row present"); CHECK(ti >= 0, "target image row present");
int expect_idx = 0;
for (int i = 0; i < ti; i++) {
meow_list_item tmp;
if (!library_test_src(i, &tmp)) continue;
if (tmp.subtitle && strncmp(tmp.subtitle, "图片 · ", strlen("图片 · ")) == 0)
expect_idx++;
}
g_opened_n = 0; g_opened_n = 0;
tap_row(target); tap_row(target);
CHECK(g_opened_n == 1 && strcmp(g_opened, pics_dir) == 0, CHECK(g_opened_n == 1 && strcmp(g_opened, pics_dir) == 0,
"tap image opens reader for current dir (%s)", g_opened); "tap image opens reader for current dir (%s)", g_opened);
CHECK(g_opened_start == expect_idx, "start index=%d (want %d)", g_opened_start, expect_idx); CHECK(strcmp(g_opened_name, target) == 0, "reader gets file name (%s)", g_opened_name);
CHECK(g_opened_start == MR_READER_START_RESUME, "name-based open does not pass an index");
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image does not navigate"); CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image does not navigate");
g_opened_n = 0; g_opened_n = 0;
+79 -3
View File
@@ -7,7 +7,9 @@
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册; * A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录; * A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
* A6 书库行模型(M18):目录+图片+档案册同列、目录优先;副标题扫描期生成, * A6 书库行模型(M18):目录+图片+档案册同列、目录优先;副标题扫描期生成,
* 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复)。 * 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复);
* A7 册间衔接根锁定(M18 评审 P2-1):相邻册越过浏览根时按「没有相邻册」处理,
* 根内正常翻册不受影响(mr_book_adjacent_rooted)。
* *
* 编译(在 tuku/ 下): * 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \ * gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
@@ -160,7 +162,7 @@ static void test_adjacent(const char *root)
} }
/* A6 文件管理器行模型:扫描即出最终行(无占位/无后台补位/无续读角标); /* A6 文件管理器行模型:扫描即出最终行(无占位/无后台补位/无续读角标);
* 目录优先;当前层的图片行带图片下标(点图 → reader 起始页)。 */ * 目录优先;图片/档案册按类型成行。 */
static void test_library_rows(const char *root) static void test_library_rows(const char *root)
{ {
mr_test_cfg_clear(); mr_test_cfg_clear();
@@ -191,7 +193,6 @@ static void test_library_rows(const char *root)
if (strcmp(e->name, "loose.png") == 0) { if (strcmp(e->name, "loose.png") == 0) {
seen_img = 1; seen_img = 1;
CHECK(e->kind == MR_LIB_KIND_IMAGE, "loose.png is an image row"); CHECK(e->kind == MR_LIB_KIND_IMAGE, "loose.png is an image row");
CHECK(mr_library_image_index(&lib, i) == 0, "loose.png is image #0");
} }
} }
CHECK(seen1 == 1, "vol1 listed"); CHECK(seen1 == 1, "vol1 listed");
@@ -199,6 +200,80 @@ static void test_library_rows(const char *root)
mr_library_free(&lib); mr_library_free(&lib);
} }
/* A7 册间衔接的根锁定:相邻册必须在浏览根内,否则按「没有相邻册」处理。
* 夹具:base/{aaa_outside, root}/{...};root 自己也是「册」(直接含图),
* 明文 _ex 会给出 root 的兄弟 aaa_outside,rooted 必须拒绝(不越 SD 卡根)。 */
static void test_rooted_adjacent(void)
{
char tmpl[] = "./mr_rooted_XXXXXX";
char *base = mkdtemp(tmpl);
CHECK(base != NULL, "mkdtemp rooted fixture");
if (!base) return;
char root[MR_PATH_MAX], outside[MR_PATH_MAX], in1[MR_PATH_MAX], in2[MR_PATH_MAX];
joinp(root, sizeof root, base, "root");
joinp(outside, sizeof outside, base, "aaa_outside");
joinp(in1, sizeof in1, root, "aaa_inside1");
joinp(in2, sizeof in2, root, "aaa_inside2");
CHECK(mkdir_p(outside) == 0 && mkdir_p(in1) == 0 && mkdir_p(in2) == 0,
"mkdir rooted fixture");
char p[MR_PATH_MAX];
joinp(p, sizeof p, root, "000.png"); CHECK(touch_file(p) == 0, "root image");
joinp(p, sizeof p, outside, "001.png"); CHECK(touch_file(p) == 0, "outside image");
joinp(p, sizeof p, in1, "001.png"); CHECK(touch_file(p) == 0, "inside1 image");
joinp(p, sizeof p, in2, "001.png"); CHECK(touch_file(p) == 0, "inside2 image");
char out[MR_PATH_MAX];
/* 明文衔接:root 的兄弟 aaa_outside 在浏览根之外,_ex 会返回它。 */
memset(out, 0, sizeof out);
CHECK(mr_book_adjacent_ex(root, -1, out, (int)sizeof out, NULL, NULL) == 1 &&
strcmp(out, outside) == 0,
"plain adjacency reaches outside root (%s)", out);
/* 根锁定版:同样的调用必须拒绝越根。 */
memset(out, 0x7f, sizeof out);
out[0] = '\0';
CHECK(mr_book_adjacent_rooted(root, -1, root, NULL, NULL,
out, (int)sizeof out) == 0 && out[0] == '\0',
"rooted adjacency refuses book outside root (%s)", out);
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL,
out, (int)sizeof out) == 0 && out[0] == '\0',
"rooted adjacency: no next inside root");
/* 根内正常翻册仍然可用(不能被误锁死)。 */
memset(out, 0, sizeof out);
CHECK(mr_book_adjacent_rooted(in1, 1, root, NULL, NULL,
out, (int)sizeof out) == 1 &&
strcmp(out, in2) == 0, "rooted next inside root (%s)", out);
CHECK(mr_book_adjacent_rooted(in2, -1, root, NULL, NULL,
out, (int)sizeof out) == 1 &&
strcmp(out, in1) == 0, "rooted prev inside root (%s)", out);
CHECK(mr_book_adjacent_rooted(in1, -1, root, NULL, NULL,
out, (int)sizeof out) == 0,
"rooted: no book before inside1 (outside is out of root)");
CHECK(mr_book_adjacent_rooted(in2, 1, root, NULL, NULL,
out, (int)sizeof out) == 0,
"rooted: no book after inside2");
/* 当前册本身在根外 → 视为没有相邻册。 */
CHECK(mr_book_adjacent_rooted(outside, 1, root, NULL, NULL,
out, (int)sizeof out) == 0, "current dir outside root");
/* 参数边界。 */
CHECK(mr_book_adjacent_rooted(root, 1, NULL, NULL, NULL,
out, (int)sizeof out) == -1, "rooted NULL root");
CHECK(mr_book_adjacent_rooted(NULL, 1, root, NULL, NULL,
out, (int)sizeof out) == -1, "rooted NULL dir");
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL, NULL, 32) == -1,
"rooted NULL out");
CHECK(mr_book_adjacent_rooted(root, 1, "", NULL, NULL,
out, (int)sizeof out) == -1, "rooted empty root");
rm_rf(base);
}
int main(void) int main(void)
{ {
char tmpl[] = "./mr_cont_adj_XXXXXX"; char tmpl[] = "./mr_cont_adj_XXXXXX";
@@ -240,6 +315,7 @@ int main(void)
test_has_images(root); test_has_images(root);
test_adjacent(root); test_adjacent(root);
test_library_rows(root); test_library_rows(root);
test_rooted_adjacent();
rm_rf(root); rm_rf(root);
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail); printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
+22 -19
View File
@@ -5,7 +5,7 @@
* T2 扩展名过滤:图片 png/jpg/jpeg/bmp 与档案册 cbz/zip 大小写不敏感;gif/txt 不支持; * T2 扩展名过滤:图片 png/jpg/jpeg/bmp 与档案册 cbz/zip 大小写不敏感;gif/txt 不支持;
* T3 单层扫描:目录 + 受支持图片 + 档案册混排(目录优先、组内自然序); * T3 单层扫描:目录 + 受支持图片 + 档案册混排(目录优先、组内自然序);
* 其它普通文件/隐藏项/符号链接不列;副标题扫描期生成一次(无占位/无懒判定); * 其它普通文件/隐藏项/符号链接不列;副标题扫描期生成一次(无占位/无懒判定);
* 图片行下标(mr_library_image_index)与行类型;不可读路径 -> -1;max_depth 忽略; * 行类型;不可读路径 -> -1;max_depth 忽略;浏览根前缀判据 mr_path_within_root;
* T4 阅读方向状态(logic/reader.c)。 * T4 阅读方向状态(logic/reader.c)。
* *
* 编译(在 tuku/ 下): * 编译(在 tuku/ 下):
@@ -80,13 +80,6 @@ static const mr_library_entry *find_entry(const mr_library *lib, const char *nam
return NULL; return NULL;
} }
static int find_index(const mr_library *lib, const char *name)
{
for (int i = 0; i < lib->count; i++)
if (strcmp(lib->items[i].name, name) == 0) return i;
return -1;
}
static void test_natural(void) static void test_natural(void)
{ {
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10"); CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
@@ -214,17 +207,27 @@ static void test_scan(void)
CHECK(find_entry(&lib, "a.png") == NULL, "no recursion into subdirs"); CHECK(find_entry(&lib, "a.png") == NULL, "no recursion into subdirs");
CHECK(find_entry(&lib, "Deeper") == NULL, "no recursion into subdirs (deeper)"); CHECK(find_entry(&lib, "Deeper") == NULL, "no recursion into subdirs (deeper)");
/* 图片下标 = 本层图片自然序(书库点图 → 阅读器起始页的契约)。 */ /* 浏览根前缀判据(书库导航/阅读器册间衔接的根锁定;纯路径工具)。 */
int i_cover = find_index(&lib, "cover.JPG"); CHECK(mr_path_within_root("/usr/data/mnt/sd_0/a/b", "/usr/data/mnt/sd_0") == 1,
int i_root = find_index(&lib, "root.png"); "nested path inside root");
CHECK(mr_library_image_index(&lib, i_cover) == 0, "cover.JPG is image #0"); CHECK(mr_path_within_root("/usr/data/mnt/sd_0", "/usr/data/mnt/sd_0") == 1,
CHECK(mr_library_image_index(&lib, i_root) == 1, "root.png is image #1"); "root itself is inside");
CHECK(mr_library_image_index(&lib, find_index(&lib, "book.cbz")) == -1, CHECK(mr_path_within_root("/usr/data/mnt/sd_0/", "/usr/data/mnt/sd_0") == 1,
"archive has no image index"); "root with trailing slash is inside");
CHECK(mr_library_image_index(&lib, find_index(&lib, "Empty")) == -1, CHECK(mr_path_within_root("/usr/data/mnt/sd_0evil", "/usr/data/mnt/sd_0") == 0,
"dir has no image index"); "prefix-lookalike rejected");
CHECK(mr_library_image_index(&lib, -1) == -1 && mr_library_image_index(&lib, 99) == -1, CHECK(mr_path_within_root("/usr/data/mnt/sd_00", "/usr/data/mnt/sd_0") == 0,
"out-of-range image index"); "prefix-lookalike with digits rejected");
CHECK(mr_path_within_root("/usr/data/mnt", "/usr/data/mnt/sd_0") == 0,
"parent of root rejected");
CHECK(mr_path_within_root("/usr/data/mnt/sd_0x/a", "/usr/data/mnt/sd_0") == 0,
"sibling-prefixed path rejected");
CHECK(mr_path_within_root("/a/b", "/a/") == 1, "root trailing slash tolerated");
CHECK(mr_path_within_root("/a", "/") == 1, "filesystem root accepts abs path");
CHECK(mr_path_within_root("/", "/") == 1, "filesystem root itself");
CHECK(mr_path_within_root("rel/path", "/") == 0, "relative path not inside /");
CHECK(mr_path_within_root(NULL, "/") == 0 && mr_path_within_root("/a", NULL) == 0 &&
mr_path_within_root("/a", "") == 0, "NULL/empty safe");
/* 行合成:纯内存,返回扫描期生成的字符串。 */ /* 行合成:纯内存,返回扫描期生成的字符串。 */
mr_library_row row; mr_library_row row;
+31 -1
View File
@@ -8,7 +8,8 @@
* H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳); * H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳);
* H4 离开/重进:恢复上次页 + 模式(续读); * H4 离开/重进:恢复上次页 + 模式(续读);
* H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时 * H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时
* 停在原地(不退出、不崩)。 * 停在原地(不退出、不崩);
* H6 册间衔接根锁定:浏览根内没有相邻册时,不跨到根外的兄弟册(M18 评审观察项)。
* *
* 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。 * 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。
*/ */
@@ -164,6 +165,10 @@ int main(void)
meow_view *reader = reader_view_create(); meow_view *reader = reader_view_create();
meow_view_page_push(ACT_LIBRARY); meow_view_page_push(ACT_LIBRARY);
/* 阅读器册间衔接的浏览根:测试夹具根(生产默认 MR_ROOT_DIR)。 */
mr_reader_test_set_root(root);
CHECK(strcmp(mr_reader_test_root(), root) == 0, "reader browse root installed");
/* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */ /* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */
int w = meow_ui_width(); int w = meow_ui_width();
int mode_x = w - 10 - 48 * 3 + 24; int mode_x = w - 10 - 48 * 3 + 24;
@@ -258,6 +263,31 @@ int main(void)
CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put"); CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put");
CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)"); CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)");
/* ---- H6 册间衔接根锁定:浏览根=book2 时,兄弟册 book1 在根外,不得翻过去 ---- */
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library for root-lock case");
mr_reader_test_set_root(book2);
CHECK(strcmp(mr_reader_test_root(), book2) == 0, "reader root narrowed to book2");
mr_reader_open_dir_at(book2, 0); /* 显式首页,避开续读页差异 */
CHECK(meow_ui_activity_active() == ACT_READER, "reopened book2");
CHECK(strcmp(mr_reader_dir(), book2) == 0, "at book2");
if (mr_reader_ui_mode() != MR_MODE_WEBTOON) mr_reader_test_toggle_mode();
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon for root-lock case");
CHECK(mr_reader_ui_page() == 0, "root-lock case at first page (got %d)",
mr_reader_ui_page());
/* 首页下拉 → book1 是 book2 的上一册,但在浏览根外:必须原地不动。 */
reader_on_touch_down(240, 300);
for (int i = 1; i <= 6; i++) {
reader_on_touch_move(240, 300 + i * 40);
usleep(5 * 1000);
}
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "root-lock down-swipe consumed");
CHECK(strcmp(mr_reader_dir(), book2) == 0,
"adjacent book outside browse root is refused (dir=%s)", mr_reader_dir());
CHECK(meow_ui_activity_active() == ACT_READER, "root-lock: still in reader");
mr_reader_test_set_root(root); /* 复原夹具根 */
/* ---- 收尾 ---- */ /* ---- 收尾 ---- */
meow_view_page_pop(); meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library"); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library");
+113
View File
@@ -14,6 +14,7 @@
*/ */
#include "tuku_internal.h" #include "tuku_internal.h"
#include "ui/reader/reader.h" #include "ui/reader/reader.h"
#include "logic/reader/page_source.h"
#include <dirent.h> #include <dirent.h>
#include <stdio.h> #include <stdio.h>
@@ -28,12 +29,14 @@ void library_test_set_root(const char *root);
const char *library_test_dir(void); const char *library_test_dir(void);
int library_test_rows(void); int library_test_rows(void);
int library_test_src(int idx, meow_list_item *item); int library_test_src(int idx, meow_list_item *item);
int library_test_open_row(int idx);
/* ui/reader/reader.c 的测试接缝(-DMR_READER_HOST_TEST) */ /* ui/reader/reader.c 的测试接缝(-DMR_READER_HOST_TEST) */
int mr_reader_ui_mode(void); int mr_reader_ui_mode(void);
int mr_reader_ui_page(void); int mr_reader_ui_page(void);
int mr_reader_ui_count(void); int mr_reader_ui_count(void);
int mr_reader_ui_open_err(void); int mr_reader_ui_open_err(void);
void mr_reader_open_dir_at_name(const char *dir, const char *name);
static int g_fail; static int g_fail;
#define CHECK(cond, ...) do { \ #define CHECK(cond, ...) do { \
@@ -152,6 +155,22 @@ static int expected_image_index(const char *name)
return n; return n;
} }
/* 阅读器目录册用的假解码回调(本测试不经过它们取页)。 */
static meow_img *cap_fake_load(const char *path, void *ud, int maxdim)
{
(void)path; (void)ud; (void)maxdim;
return NULL;
}
static void cap_fake_unload(meow_img *img) { (void)img; }
/* 在阅读器的全量目录册里按文件名取页下标(与阅读器打开时同一份枚举)。 */
static int reader_page_of_name(const mr_dir_book *db, const char *name)
{
for (int i = 0; i < db->count; i++)
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
return -1;
}
int main(void) int main(void)
{ {
const char *base = getenv("TMPDIR"); const char *base = getenv("TMPDIR");
@@ -184,6 +203,16 @@ int main(void)
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; } if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
} }
/* Big:2100 张图(超过默认 MR_LIB_SCAN_MAX=2000,复现评审 P1-1 截断错位场景)。
* 1x1 BMP(stb 可解)以控制夹具体积/建盘时间。 */
char big[MR_PATH_MAX];
snprintf(big, sizeof big, "%s/Big", root);
if (mkdir_p(big) != 0) { printf("SKIP: mkdir Big failed\n"); return 0; }
for (int i = 1; i <= 2100; i++) {
snprintf(p, sizeof p, "%s/p%04d.bmp", big, i);
if (write_bmp24(p, 1, 1) != 0) { printf("SKIP: big image write failed\n"); return 0; }
}
meow_ui_opts o = { 480, 720 }; meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; } if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
@@ -238,6 +267,90 @@ int main(void)
mr_reader_ui_page(), want2); mr_reader_ui_page(), want2);
meow_view_page_pop(); meow_view_page_pop();
/* ---- P1-1 回归:单目录 2100 张图(超过默认 cap=2000)不得「点 A 开 B」 ---- */
CHECK(library_test_load(root) == 1, "load root for cap regression");
tap_row("Big");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap Big dir stays in library");
CHECK(strcmp(library_test_dir(), big) == 0, "entered Big (%s)", library_test_dir());
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1,
"Big rows=%d (cap + truncation row)", library_test_rows());
/* 截断提示行仍在,且文案不让人误以为「没有更多图」。 */
meow_list_item trunc_it;
CHECK(library_test_src(library_test_rows() - 1, &trunc_it) == 1 &&
trunc_it.enabled == 0 && trunc_it.title && strcmp(trunc_it.title, "已截断") == 0,
"truncation row present");
CHECK(trunc_it.subtitle && strstr(trunc_it.subtitle, "2000") &&
strstr(trunc_it.subtitle, "阅读器"),
"truncation text keeps images discoverable: %s",
trunc_it.subtitle ? trunc_it.subtitle : "");
/* 阅读器目录册是**全量** 2100 页(书库 cap 只是列表显示上限)。 */
mr_dir_book db;
memset(&db, 0, sizeof db);
CHECK(mr_dir_book_init(&db, big, 720, cap_fake_load, cap_fake_unload, NULL) == 2100,
"reader sees all 2100 pages");
/* 首 3 + 尾 3 + 20 个确定性伪随机行,逐一点开断言「点哪张开哪张」。 */
int picks[26], np = 0;
picks[np++] = 0;
picks[np++] = 1;
picks[np++] = 2;
picks[np++] = MR_LIB_SCAN_MAX - 3;
picks[np++] = MR_LIB_SCAN_MAX - 2;
picks[np++] = MR_LIB_SCAN_MAX - 1;
unsigned seed = 12345u;
for (int i = 0; i < 20; i++) {
seed = seed * 1103515245u + 12345u;
picks[np++] = (int)((seed >> 16) % MR_LIB_SCAN_MAX);
}
int mismatched = 0;
for (int i = 0; i < np; i++) {
int row = picks[i];
meow_list_item it;
memset(&it, 0, sizeof it);
CHECK(library_test_src(row, &it) == 1 && it.title, "pick %d: row source", row);
if (!it.title) continue;
int want_page = reader_page_of_name(&db, it.title);
CHECK(want_page >= 0, "pick %d: %s is a reader page", row, it.title);
CHECK(library_test_open_row(row) == 1, "open row %d", row);
CHECK(meow_ui_activity_active() == ACT_READER, "pick %d opens reader", row);
CHECK(strcmp(mr_reader_dir(), big) == 0, "pick %d reader dir", row);
CHECK(mr_reader_ui_count() == 2100, "pick %d reader pages=%d", row,
mr_reader_ui_count());
if (mr_reader_ui_page() != want_page) mismatched++;
CHECK(mr_reader_ui_page() == want_page,
"pick %d: clicked %s -> page %d (want %d)", row, it.title,
mr_reader_ui_page(), want_page);
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "pick %d back to library", row);
}
CHECK(mismatched == 0, "no click/page mismatch across %d picks", np);
/* 被书库截掉的图(第 2001 张起)在阅读器里仍可打开/翻到。 */
CHECK(reader_page_of_name(&db, "p2099.bmp") == 2098, "p2099 lives at page 2098");
mr_reader_open_dir_at_name(big, "p2099.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "truncated-out image opens reader");
CHECK(mr_reader_ui_count() == 2100, "truncated-out image: all pages present");
CHECK(mr_reader_ui_page() == 2098, "truncated-out image opens at its page (%d)",
mr_reader_ui_page());
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library after truncated image");
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1, "library list unchanged after reads");
mr_dir_book_free(&db);
/* 名字找不到(文件已改名/删除):退回续读/首页,不崩、不串册。 */
mr_reader_open_dir_at_name(pics, "does-not-exist.bmp");
CHECK(meow_ui_activity_active() == ACT_READER, "unknown name still opens reader");
CHECK(strcmp(mr_reader_dir(), pics) == 0, "unknown name keeps target dir");
CHECK(mr_reader_ui_count() == 6 && mr_reader_ui_open_err() == 0,
"unknown name falls back safely (pages=%d err=%d)",
mr_reader_ui_count(), mr_reader_ui_open_err());
CHECK(mr_reader_ui_page() >= 0 && mr_reader_ui_page() < 6,
"unknown name fallback page in range (%d)", mr_reader_ui_page());
meow_view_page_pop();
/* 档案册路径(无 zip 夹具时只验证入口不被当成目录):点 txt 不存在行;跳过。 */ /* 档案册路径(无 zip 夹具时只验证入口不被当成目录):点 txt 不存在行;跳过。 */
meow_view_free(reader); meow_view_free(reader);
+13 -5
View File
@@ -146,6 +146,14 @@ static void double_tap(int x, int y)
pump_ms(120); pump_ms(120);
} }
/* 直接切模式(绕过顶栏 tap 的命中时序):本文件只关心绘制/解码,不验证 chrome 命中;
* 顶栏动作本身的回归在 test_mode_switch_host 覆盖。M18 评审:负载下该用例曾偶发。 */
static void mode_toggle(void)
{
mr_reader_test_toggle_mode();
pump_ms(100);
}
static void draw_once(void) { meow_ui_paint_full(); } static void draw_once(void) { meow_ui_paint_full(); }
int main(void) int main(void)
@@ -239,7 +247,7 @@ int main(void)
"paged drag: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws()); "paged drag: zero resample (scaled %d -> %d)", st, mr_reader_ui_scaled_draws());
/* ---- P4 条漫模式:切换后滚动,长宽比 1.6 页也必须零重采样 ---- */ /* ---- P4 条漫模式:切换后滚动,长宽比 1.6 页也必须零重采样 ---- */
tap(mode_x, bar_y); mode_toggle();
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
pump_ms(800); pump_ms(800);
draw_once(); draw_once();
@@ -260,14 +268,14 @@ int main(void)
mr_reader_ui_scaled_draws()); mr_reader_ui_scaled_draws());
/* ---- P5 「先适高、再切条漫」:strip 恒按屏宽排版,解码必须跟随 ---- */ /* ---- P5 「先适高、再切条漫」:strip 恒按屏宽排版,解码必须跟随 ---- */
tap(mode_x, bar_y); /* 回翻页 */ mode_toggle(); /* 回翻页 */
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "back to paged"); CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "back to paged");
pump_ms(500); pump_ms(500);
tap(fit_x, bar_y); /* 适宽 → 适高 */ tap(fit_x, bar_y); /* 适宽 → 适高 */
CHECK(mr_reader_ui_fit() == MR_FIT_HEIGHT, "fit-height selected (got %d)", CHECK(mr_reader_ui_fit() == MR_FIT_HEIGHT, "fit-height selected (got %d)",
mr_reader_ui_fit()); mr_reader_ui_fit());
pump_ms(700); pump_ms(700);
tap(mode_x, bar_y); /* 适高档 → 条漫 */ mode_toggle(); /* 适高档 → 条漫 */
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon after fit-height"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon after fit-height");
pump_ms(900); pump_ms(900);
draw_once(); draw_once();
@@ -290,7 +298,7 @@ int main(void)
mr_reader_ui_scaled_draws()); mr_reader_ui_scaled_draws());
/* ---- P6 双击放大再缩回(评审 P2-1:缩回后必须按新几何重解)---- */ /* ---- P6 双击放大再缩回(评审 P2-1:缩回后必须按新几何重解)---- */
tap(mode_x, bar_y); /* 回翻页 */ mode_toggle(); /* 回翻页 */
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "paged for zoom test"); CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "paged for zoom test");
pump_ms(600); pump_ms(600);
draw_once(); draw_once();
@@ -338,7 +346,7 @@ int main(void)
CHECK(mr_reader_ui_blit_draws() > bt && mr_reader_ui_scaled_draws() == st, CHECK(mr_reader_ui_blit_draws() > bt && mr_reader_ui_scaled_draws() == st,
"800x1280 paged drag: zero resample (blit +%d, scaled +%d)", "800x1280 paged drag: zero resample (blit +%d, scaled +%d)",
mr_reader_ui_blit_draws() - bt, mr_reader_ui_scaled_draws() - st); mr_reader_ui_blit_draws() - bt, mr_reader_ui_scaled_draws() - st);
tap(mode_x, bar_y); /* 条漫 */ mode_toggle(); /* 条漫 */
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "800x1280 webtoon"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "800x1280 webtoon");
pump_ms(800); pump_ms(800);
draw_once(); draw_once();
+16 -8
View File
@@ -4,7 +4,8 @@
* *
* 浏览语义(M18,用户要求「完全像文件管理器」): * 浏览语义(M18,用户要求「完全像文件管理器」):
* - 列表 = 当前目录一层的「子目录 + 受支持图片 + .cbz/.zip 档案册」,目录优先、组内自然序; * - 列表 = 当前目录一层的「子目录 + 受支持图片 + .cbz/.zip 档案册」,目录优先、组内自然序;
* 点目录=进入(空目录也进),点图片=从该图开始读(mr_reader_open_dir_at(目录, 图片下标)), * 点目录=进入(空目录也进),点图片=从该图开始读(mr_reader_open_dir_at_name(目录, 文件名),
* 由阅读器在全量目录册里定位起始页;列表被截断也不会点错图),
* 点档案册=打开档案册(第 1 页/续读); * 点档案册=打开档案册(第 1 页/续读);
* - 首帧只做**单层**枚举(一次 opendir/readdir+lstat):没有「目录含图=一本书」的探测、 * - 首帧只做**单层**枚举(一次 opendir/readdir+lstat):没有「目录含图=一本书」的探测、
* 页数、封面、角标,也没有懒算线程/缩略图解码——行副标题在扫描期生成一次; * 页数、封面、角标,也没有懒算线程/缩略图解码——行副标题在扫描期生成一次;
@@ -268,8 +269,7 @@ int library_go_up(void)
char parent[MR_PATH_MAX]; char parent[MR_PATH_MAX];
if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0; if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0;
/* 防御:父目录若越出浏览根(理论上不会发生),钳回根。 */ /* 防御:父目录若越出浏览根(理论上不会发生),钳回根。 */
size_t rl = strlen(g_root_dir); if (!mr_path_within_root(parent, g_root_dir))
if (strlen(parent) < rl || strncmp(parent, g_root_dir, rl) != 0)
snprintf(parent, sizeof parent, "%s", g_root_dir); snprintf(parent, sizeof parent, "%s", g_root_dir);
if (!load_dir(parent)) { if (!load_dir(parent)) {
/* 父目录不可读 → 提示并停留(不越根、不丢当前列表)。 */ /* 父目录不可读 → 提示并停留(不越根、不丢当前列表)。 */
@@ -289,9 +289,9 @@ static void library_open_entry(int idx)
return; return;
} }
if (e->kind == MR_LIB_KIND_IMAGE) { if (e->kind == MR_LIB_KIND_IMAGE) {
/* 下标由 logic 用与阅读器目录册相同的过滤器/比较器数出,保证「点 A 开 A」。 */ /* 按文件名定位起始页:由阅读器用自己的全量目录册算下标。
int start = mr_library_image_index(&g_lib, idx); * 书库列表可能被 MR_LIB_SCAN_MAX 截断,绝不能从列表推导下标(会点 A 开 B)。 */
mr_reader_open_dir_at(g_cur_dir, start < 0 ? 0 : start); mr_reader_open_dir_at_name(g_cur_dir, e->name);
return; return;
} }
if (e->kind == MR_LIB_KIND_ARCHIVE) { if (e->kind == MR_LIB_KIND_ARCHIVE) {
@@ -401,8 +401,8 @@ void library_on_tick(void)
/* ============================================================ */ /* ============================================================ */
static void library_page_create(meow_view *v, void *ctx) static void library_page_create(meow_view *v, void *ctx)
{ {
mr_page_ctrl *c = (mr_page_ctrl *)ctx; (void)v;
c->view = v; (void)ctx;
int w = meow_ui_width(); int w = meow_ui_width();
int sb = meow_ui_statusbar_height(); int sb = meow_ui_statusbar_height();
@@ -525,6 +525,14 @@ int library_test_src(int idx, meow_list_item *item)
return lib_src(idx, item, NULL); return lib_src(idx, item, NULL);
} }
/* 直接打开第 idx 行(等价点击行体;测试里避免滚动/坐标干扰)。 */
int library_test_open_row(int idx)
{
if (idx < 0 || idx >= g_lib.count) return 0;
library_open_entry(idx);
return 1;
}
int library_test_hint_active(void) int library_test_hint_active(void)
{ {
return g_hint[0] != '\0'; return g_hint[0] != '\0';
+70 -10
View File
@@ -21,7 +21,7 @@
* *
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page * 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
* 经 meow_config 持久化(键 reader.mode/page.<册标识>,见 logic/reader.c),下次打开同一册 * 经 meow_config 持久化(键 reader.mode/page.<册标识>,见 logic/reader.c),下次打开同一册
* 自动恢复。书库行角标由 logic/library.c 扫描时读取同一份状态(副标题后缀「续读 Pn」)。 * 自动恢复。书库列表是文件管理器模型(M18),不再显示续读角标。
* *
* 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁; * 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁;
* 2.5s 超时则降级落位并提示)。 * 2.5s 超时则降级落位并提示)。
@@ -86,6 +86,11 @@
static mr_page_ctrl g_ctrl; static mr_page_ctrl g_ctrl;
static char g_dir[MR_PATH_MAX]; static char g_dir[MR_PATH_MAX];
static int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */ static int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */
/* 下次 on_enter 的「按文件名定位起始页」(书库点图;只对目录册生效,open_book 消费后清空)。
* 用文件名而不是下标:书库列表可能被 MR_LIB_SCAN_MAX 截断,下标不可信。 */
static char g_start_name[MR_NAME_MAX];
/* 浏览根(生产=MR_ROOT_DIR,宿主测试可改):册间衔接不得越过它。 */
static char g_root[MR_PATH_MAX] = MR_ROOT_DIR;
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */ static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
static mr_book g_book; static mr_book g_book;
@@ -1433,10 +1438,28 @@ static void open_book_reset_state(void)
g_meta = NULL; g_meta = NULL;
} }
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */ /* 在目录册里按文件名定位页下标:先精确,再大小写不敏感(书库展示名与磁盘名一致,
* 兜底只为容错)。-1=没找到。 */
static int dir_book_find_index(const mr_dir_book *db, const char *name)
{
if (!db || !name || !name[0] || db->count <= 0) return -1;
for (int i = 0; i < db->count; i++)
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
for (int i = 0; i < db->count; i++)
if (db->names[i] && strcasecmp(db->names[i], name) == 0) return i;
return -1;
}
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。
* 若 g_start_name 非空(书库点图),优先用**刚枚举出来的目录册**按 basename 定位起始页:
* 枚举源与翻页完全一致,且不受书库 MR_LIB_SCAN_MAX 截断影响。 */
static int open_book(const char *dir, int start_page, int mode) static int open_book(const char *dir, int start_page, int mode)
{ {
if (!dir || !dir[0]) return 0; if (!dir || !dir[0]) return 0;
char want_name[MR_NAME_MAX];
snprintf(want_name, sizeof want_name, "%s", g_start_name);
g_start_name[0] = '\0'; /* 只在本次打开生效 */
open_book_reset_state(); open_book_reset_state();
/* reader_page_enter 传入的 dir 就是 g_dir:自拷贝是 snprintf 的未定义行为, /* reader_page_enter 传入的 dir 就是 g_dir:自拷贝是 snprintf 的未定义行为,
* 其它调用(册间衔接)才需要复制。 */ * 其它调用(册间衔接)才需要复制。 */
@@ -1472,6 +1495,12 @@ static int open_book(const char *dir, int start_page, int mode)
meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count); meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count);
int page = start_page < 0 ? g_count - 1 : start_page; int page = start_page < 0 ? g_count - 1 : start_page;
/* 书库点图:按文件名在刚打开的目录册里定位(与翻页同一份枚举/比较器)。
* 找不到(文件已改名/删除)才退回 start_page/续读。 */
if (want_name[0] && g_handle.kind == MR_BOOK_KIND_DIR) {
int idx = dir_book_find_index(&g_handle.u.dir, want_name);
if (idx >= 0) page = idx;
}
page = mr_nav_clamp_page(page, g_count); page = mr_nav_clamp_page(page, g_count);
if (page < 0) page = 0; if (page < 0) page = 0;
@@ -1525,8 +1554,9 @@ static int book_switch(int delta)
if (delta == 0) return 0; if (delta == 0) return 0;
if (g_count <= 0) return 0; if (g_count <= 0) return 0;
char next[MR_PATH_MAX]; char next[MR_PATH_MAX];
int r = mr_book_adjacent_ex(g_dir, delta > 0 ? 1 : -1, next, (int)sizeof next, /* 根锁定:相邻册必须仍在浏览根内,绝不跳到 SD 卡挂载点以上。 */
cbz_pages_probe, NULL); int r = mr_book_adjacent_rooted(g_dir, delta > 0 ? 1 : -1, g_root,
cbz_pages_probe, NULL, next, (int)sizeof next);
if (r != 1) { if (r != 1) {
toast(delta > 0 ? "已是最后一册" : "已是第一册"); toast(delta > 0 ? "已是最后一册" : "已是第一册");
return 0; return 0;
@@ -2040,7 +2070,6 @@ static void reader_state_reset(void)
static void reader_page_enter(meow_view *v, void *ctx) static void reader_page_enter(meow_view *v, void *ctx)
{ {
mr_page_ctrl *c = (mr_page_ctrl *)ctx; mr_page_ctrl *c = (mr_page_ctrl *)ctx;
c->view = v;
c->active = 1; c->active = 1;
g_quitting = 0; g_quitting = 0;
@@ -2120,6 +2149,7 @@ static void reader_page_leave(meow_view *v, void *ctx)
free(g_meta); free(g_meta);
g_meta = NULL; g_meta = NULL;
g_start_at = MR_READER_START_RESUME; g_start_at = MR_READER_START_RESUME;
g_start_name[0] = '\0'; /* 未消费的文件名定位不跨页残留 */
mr_book_close_auto(&g_book, &g_handle); mr_book_close_auto(&g_book, &g_handle);
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; } if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
@@ -2160,7 +2190,9 @@ const char *mr_reader_dir(void)
return g_dir; return g_dir;
} }
void mr_reader_open_dir_at(const char *path, int start_index) /* 打开入口共用部分:已在本页时先 pop(完整 leave/enter 生命周期),再设目标并 push。
* start_name 非空 = 按文件名定位(只对目录册),start_index 忽略。 */
static void reader_open_common(const char *path, int start_index, const char *start_name)
{ {
if (!path) return; if (!path) return;
/* 已在本页(书库再次点另一张图/另一本册时可能仍是 ACT_READER): /* 已在本页(书库再次点另一张图/另一本册时可能仍是 ACT_READER):
@@ -2172,13 +2204,31 @@ void mr_reader_open_dir_at(const char *path, int start_index)
if (reopen) meow_view_page_pop(); if (reopen) meow_view_page_pop();
snprintf(g_dir, sizeof g_dir, "%s", path); snprintf(g_dir, sizeof g_dir, "%s", path);
/* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */ g_start_name[0] = '\0';
g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME if (start_name && start_name[0]) {
: (start_index < 0 ? 0 : start_index); /* 按文件名定位:由 open_book 在刚枚举出的目录册里找下标(不受列表截断影响)。 */
meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at); snprintf(g_start_name, sizeof g_start_name, "%s", start_name);
g_start_at = MR_READER_START_RESUME;
meow_log(MEOW_LOG_INFO, "reader open: %s (name=%s)", g_dir, g_start_name);
} else {
/* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */
g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME
: (start_index < 0 ? 0 : start_index);
meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at);
}
meow_view_page_push(ACT_READER); meow_view_page_push(ACT_READER);
} }
void mr_reader_open_dir_at(const char *path, int start_index)
{
reader_open_common(path, start_index, NULL);
}
void mr_reader_open_dir_at_name(const char *dir, const char *name)
{
reader_open_common(dir, MR_READER_START_RESUME, name);
}
void mr_reader_open_dir(const char *path) void mr_reader_open_dir(const char *path)
{ {
mr_reader_open_dir_at(path, MR_READER_START_RESUME); mr_reader_open_dir_at(path, MR_READER_START_RESUME);
@@ -2205,4 +2255,14 @@ int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; }
int mr_reader_ui_decode_count(void) { return g_decode_count; } int mr_reader_ui_decode_count(void) { return g_decode_count; }
int mr_reader_ui_fit(void) { return g_fit; } int mr_reader_ui_fit(void) { return g_fit; }
int mr_reader_ui_chrome(void) { return g_chrome; } int mr_reader_ui_chrome(void) { return g_chrome; }
/* 直接切模式(避开顶栏 tap 命中时序):与顶栏动作走同一个 mode_set。 */
void mr_reader_test_toggle_mode(void)
{
mode_set(g_mode == MR_MODE_PAGED ? MR_MODE_WEBTOON : MR_MODE_PAGED, -1);
}
void mr_reader_test_set_root(const char *root)
{
if (root && root[0]) snprintf(g_root, sizeof g_root, "%s", root);
}
const char *mr_reader_test_root(void) { return g_root; }
#endif #endif
+13
View File
@@ -19,6 +19,14 @@ void reader_on_touch_move(int x, int y);
* 显式下标优先于续读记录:用户在列表里点了某张图,就不该被上次的页码带走。 */ * 显式下标优先于续读记录:用户在列表里点了某张图,就不该被上次的页码带走。 */
#define MR_READER_START_RESUME (-1) #define MR_READER_START_RESUME (-1)
void mr_reader_open_dir_at(const char *dir, int start_index); void mr_reader_open_dir_at(const char *dir, int start_index);
/* 书库 → 阅读器(按文件名定位起始页;M18 评审 P1-1 的首选入口):
* 打开 dir 后,由阅读器自己的目录册枚举(logic/reader/page_source.c: mr_dir_book_init,
* 与翻页用的是同一份全量枚举)按 basename 找起始页(先大小写敏感,再大小写不敏感兜底)。
* 找不到则退回续读记录/首页。这样书库不需要(也不能)从可能被 MR_LIB_SCAN_MAX 截断的
* 列表里推导下标——目录条目超过 2000 时也不会「点 A 开 B」。name 为 NULL/空则等价续读。 */
void mr_reader_open_dir_at_name(const char *dir, const char *name);
/* 保留旧入口:等价于 mr_reader_open_dir_at(dir, MR_READER_START_RESUME)。 */ /* 保留旧入口:等价于 mr_reader_open_dir_at(dir, MR_READER_START_RESUME)。 */
void mr_reader_open_dir(const char *dir); void mr_reader_open_dir(const char *dir);
@@ -36,6 +44,11 @@ int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重
int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */ int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */
int mr_reader_ui_fit(void); /* 当前 fit 档(MR_FIT_*) */ int mr_reader_ui_fit(void); /* 当前 fit 档(MR_FIT_*) */
int mr_reader_ui_chrome(void); /* 1=顶栏可见 */ int mr_reader_ui_chrome(void); /* 1=顶栏可见 */
/* 直接切模式(绕过顶栏 tap 的命中时序;测试用,产品路径仍走顶栏动作)。 */
void mr_reader_test_toggle_mode(void);
/* 浏览根(生产=MR_ROOT_DIR):册间衔接不得越过它。测试可指向合成夹具根。 */
void mr_reader_test_set_root(const char *root);
const char *mr_reader_test_root(void);
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus