feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接

- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页);
  排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测)
- 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化
  (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码
- 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」
- 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出
- 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示)
- 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判,
  超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险)
- 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的
  阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
This commit is contained in:
AskaEth committed 2026-11-04 21:49:33 +08:00
1 parent 8cbb240931
commit ff97f3cee7
14 files changed
+2539 -93

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
/* test_webtoon_strip.c — 条漫排版/滚动窗口纯逻辑宿主测试(无 SDK/硬件)。
*
* 覆盖:
* W1 单页:高度=适宽、总高、max_offset=0、窗口只有本页、顶/底同时成立;
* W2 多页:按页高累加 offsets;page_at 二分定位(页边界/末页/越界);
* W3 可见窗口:视口±1 页;max_pages 截断;条底/条顶窗口不越界;
* W4 尺寸兜底:未知 meta → 2:3 占位;横图适宽;极端长图夹到上限;
* W5 参数/生命周期:空册、非法视口、重复 free、重建;
* W6 跳页落位:offset_for_page 与 page_at 互逆(含末页 max 夹取)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. test/test_webtoon_strip.c logic/reader/nav.c \
* -o test/test_webtoon_strip && ./test/test_webtoon_strip
* 或 test/run_webtoon_tests.sh。
*/
#include "logic/reader/nav.h"
#include <stdio.h>
#include <string.h>
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define VW 480
#define VH 720
typedef struct fake_meta {
int n;
int w[16];
int h[16];
int known[16];
} fake_meta;
static int meta_cb(void *ctx, int i, int *w, int *h)
{
fake_meta *m = (fake_meta *)ctx;
if (!m || i < 0 || i >= m->n) return 0;
*w = m->w[i];
*h = m->h[i];
return m->known[i];
}
static void meta_page(fake_meta *m, int i, int w, int h, int known)
{
m->w[i] = w;
m->h[i] = h;
m->known[i] = known;
if (i + 1 > m->n) m->n = i + 1;
}
static void test_single_page(void)
{
fake_meta m;
memset(&m, 0, sizeof m);
meta_page(&m, 0, 800, 1200, 1);
mr_strip s;
memset(&s, 0, sizeof s);
CHECK(mr_strip_build(&s, 1, VW, VH, meta_cb, &m) == 0, "build single");
CHECK(s.count == 1, "count=%d", s.count);
CHECK(mr_strip_page_h(&s, 0) == 720, "single h=%d", mr_strip_page_h(&s, 0));
CHECK(mr_strip_total(&s) == 720, "single total=%d", mr_strip_total(&s));
CHECK(mr_strip_max_offset(&s) == 0, "single max=%d", mr_strip_max_offset(&s));
CHECK(mr_strip_page_at(&s, 0) == 0, "single page at 0");
CHECK(mr_strip_page_at(&s, 719) == 0, "single page at 719");
CHECK(mr_strip_page_at(&s, 720) == 0, "single page at total clamps");
CHECK(mr_strip_at_end(&s, 0) == 1, "single at end");
CHECK(mr_strip_at_start(&s, 0) == 1, "single at start");
int f = -1, l = -1;
CHECK(mr_strip_window(&s, 0, 4, &f, &l) == 1 && f == 0 && l == 0,
"single window %d..%d", f, l);
CHECK(mr_strip_offset_for_page(&s, 0) == 0, "single offset page0");
CHECK(mr_strip_offset_for_page(&s, 9) == 0, "single offset OOB clamps");
mr_strip_free(&s);
}
static void test_multi_pages(void)
{
fake_meta m;
memset(&m, 0, sizeof m);
/* 0: 800x1200 -> 720;1: 未知 -> 720 占位;2: 横图 1500x1000 -> 320;
* 3: 方图 400x400 -> 480;4: 极端长图 1x2000 -> 夹到 200000。 */
meta_page(&m, 0, 800, 1200, 1);
meta_page(&m, 1, 0, 0, 0);
meta_page(&m, 2, 1500, 1000, 1);
meta_page(&m, 3, 400, 400, 1);
meta_page(&m, 4, 1, 2000, 1);
mr_strip s;
memset(&s, 0, sizeof s);
CHECK(mr_strip_build(&s, 5, VW, VH, meta_cb, &m) == 0, "build multi");
CHECK(mr_strip_page_h(&s, 0) == 720, "h0=%d", mr_strip_page_h(&s, 0));
CHECK(mr_strip_page_h(&s, 1) == 720, "h1(unknown)=%d", mr_strip_page_h(&s, 1));
CHECK(mr_strip_page_h(&s, 2) == 320, "h2(landscape)=%d", mr_strip_page_h(&s, 2));
CHECK(mr_strip_page_h(&s, 3) == 480, "h3(square)=%d", mr_strip_page_h(&s, 3));
CHECK(mr_strip_page_h(&s, 4) == MR_STRIP_MAX_PAGE_H, "h4(extreme)=%d",
mr_strip_page_h(&s, 4));
CHECK(mr_strip_top(&s, 0) == 0, "top0=%d", mr_strip_top(&s, 0));
CHECK(mr_strip_top(&s, 1) == 720, "top1=%d", mr_strip_top(&s, 1));
CHECK(mr_strip_top(&s, 2) == 1440, "top2=%d", mr_strip_top(&s, 2));
CHECK(mr_strip_top(&s, 3) == 1760, "top3=%d", mr_strip_top(&s, 3));
CHECK(mr_strip_top(&s, 4) == 2240, "top4=%d", mr_strip_top(&s, 4));
CHECK(mr_strip_top(&s, 5) == 202240, "top[count]=%d", mr_strip_top(&s, 5));
CHECK(mr_strip_total(&s) == 202240, "total=%d", mr_strip_total(&s));
CHECK(mr_strip_max_offset(&s) == 201520, "max=%d", mr_strip_max_offset(&s));
CHECK(mr_strip_page_at(&s, 0) == 0, "page_at 0");
CHECK(mr_strip_page_at(&s, 719) == 0, "page_at 719");
CHECK(mr_strip_page_at(&s, 720) == 1, "page_at 720");
CHECK(mr_strip_page_at(&s, 1439) == 1, "page_at 1439");
CHECK(mr_strip_page_at(&s, 1440) == 2, "page_at 1440");
CHECK(mr_strip_page_at(&s, 1759) == 2, "page_at 1759");
CHECK(mr_strip_page_at(&s, 1760) == 3, "page_at 1760");
CHECK(mr_strip_page_at(&s, 2239) == 3, "page_at 2239");
CHECK(mr_strip_page_at(&s, 2240) == 4, "page_at 2240");
CHECK(mr_strip_page_at(&s, 9999999) == 4, "page_at huge -> last");
CHECK(mr_strip_page_at(&s, -50) == 0, "page_at negative -> first");
CHECK(mr_strip_page_at(&s, 0) == 0 && mr_strip_page_at(NULL, 0) == -1, "page_at NULL");
/* 视口窗口:off=0 只看得到页 0(页 1 从 720 开始)→ ±1 = [0,1] */
int f = -1, l = -1;
CHECK(mr_strip_window(&s, 0, 4, &f, &l) == 2 && f == 0 && l == 1,
"window top %d..%d", f, l);
/* off=1000:可见页 1..2 → 窗口 [0,3] */
CHECK(mr_strip_window(&s, 1000, 4, &f, &l) == 4 && f == 0 && l == 3,
"window mid %d..%d", f, l);
/* off=max:可见页 4 → 窗口 [3,4] */
CHECK(mr_strip_window(&s, 201520, 4, &f, &l) == 2 && f == 3 && l == 4,
"window bottom %d..%d", f, l);
/* 只用 1 个槽位:窗口收缩到焦点页 */
CHECK(mr_strip_window(&s, 1000, 1, &f, &l) == 1 && f >= 1 && f <= 2 && f == l,
"window max_pages=1 %d..%d", f, l);
CHECK(mr_strip_window(&s, -100, 4, &f, &l) == 2 && f == 0 && l == 1,
"window negative off %d..%d", f, l);
CHECK(mr_strip_at_end(&s, 0) == 0, "not at end at 0");
CHECK(mr_strip_at_end(&s, 201519) == 0, "not at end just before max");
CHECK(mr_strip_at_end(&s, 201520) == 1, "at end at max");
CHECK(mr_strip_at_start(&s, 0) == 1, "at start 0");
CHECK(mr_strip_at_start(&s, 1) == 0, "not at start 1");
/* 跳页落位:每页 offset 回读一致(末页夹到 max) */
for (int i = 0; i < 5; i++) {
int off = mr_strip_offset_for_page(&s, i);
int back = mr_strip_page_at(&s, off);
CHECK(back == i, "roundtrip page %d -> off %d -> page %d", i, off, back);
}
CHECK(mr_strip_offset_for_page(&s, 4) == 2240, "last page offset = its top");
/* 短册(内容不足一屏):任何页都夹到 max=0 */
fake_meta m2;
memset(&m2, 0, sizeof m2);
meta_page(&m2, 0, 800, 300, 1); /* 高 180 */
meta_page(&m2, 1, 800, 300, 1);
mr_strip s2;
memset(&s2, 0, sizeof s2);
CHECK(mr_strip_build(&s2, 2, VW, VH, meta_cb, &m2) == 0, "build short");
CHECK(mr_strip_total(&s2) == 360 && mr_strip_max_offset(&s2) == 0, "short totals");
CHECK(mr_strip_offset_for_page(&s2, 1) == 0, "short last page clamps to 0");
CHECK(mr_strip_at_end(&s2, 0) == 1 && mr_strip_at_start(&s2, 0) == 1, "short edges");
mr_strip_free(&s2);
/* 越界参数 */
CHECK(mr_strip_page_h(&s, -1) == 0 && mr_strip_page_h(&s, 5) == 0, "page_h OOB");
CHECK(mr_strip_top(&s, -1) == 0 && mr_strip_top(&s, 6) == 0, "top OOB");
mr_strip_free(&s);
CHECK(s.top == NULL && s.height == NULL && s.count == 0, "free clears");
mr_strip_free(&s);
}
static void test_build_contract(void)
{
fake_meta m;
memset(&m, 0, sizeof m);
meta_page(&m, 0, 800, 1200, 1);
mr_strip s;
memset(&s, 0, sizeof s);
CHECK(mr_strip_build(NULL, 1, VW, VH, meta_cb, &m) == -1, "NULL strip");
CHECK(mr_strip_build(&s, 0, VW, VH, meta_cb, &m) == -1, "count 0 -> -1");
CHECK(s.top == NULL && s.count == 0, "failed build clears");
CHECK(mr_strip_build(&s, 1, 0, VH, meta_cb, &m) == -1, "vw 0 -> -1");
CHECK(mr_strip_build(&s, 1, VW, 0, meta_cb, &m) == -1, "vh 0 -> -1");
/* meta 回调为 NULL → 全部按 2:3 占位 */
CHECK(mr_strip_build(&s, 2, VW, VH, NULL, NULL) == 0, "NULL meta build");
CHECK(mr_strip_page_h(&s, 0) == 720 && mr_strip_page_h(&s, 1) == 720,
"NULL meta fallback heights");
mr_strip_free(&s);
/* meta 返回失败(0)也按占位处理 */
CHECK(mr_strip_build(&s, 1, VW, VH, meta_cb, &m) == 0, "rebuild after free");
CHECK(mr_strip_page_h(&s, 0) == 720, "rebuild height");
mr_strip_free(&s);
/* 空排版查询全部安全 */
mr_strip e;
memset(&e, 0, sizeof e);
CHECK(mr_strip_total(&e) == 0 && mr_strip_max_offset(&e) == 0, "empty totals");
CHECK(mr_strip_page_at(&e, 0) == -1, "empty page_at");
CHECK(mr_strip_offset_for_page(&e, 0) == -1, "empty offset_for_page");
int f = 9, l = 9;
CHECK(mr_strip_window(&e, 0, 4, &f, &l) == 0 && l == -1, "empty window");
CHECK(mr_strip_at_end(&e, 0) == 0 && mr_strip_at_start(&e, 0) == 0, "empty edges");
}
int main(void)
{
test_single_page();
test_multi_pages();
test_build_contract();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}