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

+115
View File
@@ -7,8 +7,14 @@
* 收进结果并自然排序——因此根视图不会列出后代条目,进入子目录时再重新扫描该层。
*
* 内存:条目数组倍增分配;路径/名称定长,随 mr_library_free() 释放。
*
* M7 追加(不改扫描语义):
* - 续读角标:含图册的副标题后缀「· 续读 Pn」(读 logic/reader 的每册 last page);
* - 册间衔接:mr_book_adjacent 给出同父目录下按书库自然序的相邻「含图册」,
* 供阅读器末页前进/首页后退跨册;mr_library_dir_has_images 是共享的「是不是一本书」判据。
*/
#include "library.h"
#include "logic/reader.h" /* mr_cont_badge:书库行续读角标(M7) */
#include <ctype.h>
#include <dirent.h>
@@ -215,6 +221,17 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
else
snprintf(e.subtitle, sizeof e.subtitle, "%d folder%s", e.subdirs,
e.subdirs == 1 ? "" : "s");
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
if (e.has_images) {
char badge[24];
if (mr_cont_badge(e.path, badge, (int)sizeof badge)) {
int used = (int)strlen(e.subtitle);
if (used < (int)sizeof e.subtitle - 2)
snprintf(e.subtitle + used, sizeof e.subtitle - (size_t)used,
" · %s", badge);
}
}
if (lib_add(lib, &e) != 0) {
closedir(d);
mr_library_free(lib);
@@ -245,3 +262,101 @@ int mr_path_parent(const char *path, char *out, int outsz)
out[n] = '\0';
return 0;
}
/* ============================================================ */
/* 册间衔接(M7) */
/* ============================================================ */
int mr_library_dir_has_images(const char *dir)
{
if (!dir || !dir[0]) return 0;
DIR *d = opendir(dir);
if (!d) return 0;
int found = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
if (name[0] == '.' || !mr_path_is_image(name)) continue;
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
struct stat st;
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
found = 1;
break;
}
closedir(d);
return found;
}
static int name_ptr_cmp(const void *pa, const void *pb)
{
const char *a = *(const char *const *)pa;
const char *b = *(const char *const *)pb;
return mr_natural_cmp(a, b);
}
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || (delta != 1 && delta != -1)) return -1;
char norm[MR_PATH_MAX];
size_t dl = strlen(dir);
while (dl > 1 && dir[dl - 1] == '/') dl--;
if (dl >= sizeof norm) return -1;
memcpy(norm, dir, dl);
norm[dl] = '\0';
const char *slash = strrchr(norm, '/');
if (!slash || !slash[1]) return -1;
const char *cur = slash + 1;
char parent[MR_PATH_MAX];
if (mr_path_parent(norm, parent, (int)sizeof parent) != 0) return -1;
DIR *d = opendir(parent);
if (!d) return 0;
char **names = NULL;
int n = 0, cap = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (ignored_dir_name(name)) continue;
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
struct stat st;
if (lstat(full, &st) != 0 || S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
if (!mr_library_dir_has_images(full)) continue; /* 只算「一本书」,中间目录跳过 */
if (n >= cap) {
int nc = cap ? cap * 2 : 16;
char **nn = (char **)realloc(names, (size_t)nc * sizeof *nn);
if (!nn) break;
names = nn;
cap = nc;
}
size_t len = strlen(name);
names[n] = (char *)malloc(len + 1);
if (!names[n]) break;
memcpy(names[n], name, len + 1);
n++;
}
closedir(d);
int rc = 0;
if (n > 1) qsort(names, (size_t)n, sizeof *names, name_ptr_cmp);
int idx = -1;
for (int i = 0; i < n; i++)
if (strcmp(names[i], cur) == 0) { idx = i; break; }
int target = idx + delta;
if (idx >= 0 && target >= 0 && target < n) {
if (join_path(out, outsz, parent, names[target]) == 0) rc = 1;
}
for (int i = 0; i < n; i++) free(names[i]);
free(names);
return rc;
}