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

+118 -35
View File
@@ -1,19 +1,25 @@
/* logic/library.h — 漫画书库扫描/排序(纯 C,不依赖 meow_ui.h)。
/* logic/library.h — 书库目录浏览(纯 C,不依赖 meow_ui.h)。
*
* 分层契约同 examples/hello:logic 层只含文件系统与纯数据,宿主 gcc 可直接裸测。
* (M7 起另提供每册续读角标与册间顺序查询;续读状态本身由 logic/reader.c 持久化,
* 本层只读它来拼副标题,宿主测试用配置存根顶替。)
* 书库语义:
* - 从 TF 卡根(MR_ROOT_DIR)起列「直接子目录」;M11 起另列直接子文件里的
* .cbz/.zip(档案册即一本书,与文件夹册同列自然序混排);
* - 只保留自身含 PNG/JPEG/BMP 的目录,或子树中最终含书的中间目录(可继续导航);
* - 行的 pages=该目录直接图片数(档案册=档内图片页数);subdirs=其下含图子目录数;
* - cover=缩略图源文件:优先该目录自然序第一张图,否则取第一个含图子目录的 cover
* (档案册留空,UI 解压档内首图);
* - 条目按名称自然排序(page2 < page10),含前导零与数字/字母混合名。
*
* 深度上限 MR_LIB_MAX_DEPTH 用于防止坏卡/超大树的无限递归;隐藏目录与系统目录
* (. 开头、MeowAPPs/MeowData/MeowOS/lost+found)不参与书库。符号链接跳过(防环)。
* M13 浏览语义(用户要求:性能 + 访问放开;参考 os.meow.files 的 fsx_scan_dir):
* - mr_library_scan / mr_library_scan_ex 只枚举**当前目录一层**:一次 opendir/readdir +
* 逐项 lstat,列出全部子目录(自然升序),不做任何递归「子树含图」探测;
* - 访问放开:系统目录(MeowAPPs/MeowData/MeowOS/lost+found)不再隐藏,
* 空目录/无图目录一律列出并可进入;仅跳过 "."/".." 与其它 "." 开头的隐藏项、
* 符号链接(防环);
* - 「直接含图目录=一本书」的判定改为**后台懒算**:mr_library_probe_dir 只读一层,
* mr_library_probe_path 按行类型(目录/档案册)走同一套 mtime 失效缓存(线程安全),
* 结果经 mr_library_apply_probe 补回行(页数/首图/副标题)。首帧/滚动不做子目录 IO;
* - 单目录条目上限 MR_LIB_SCAN_MAX(默认 2000):到上限后若还有条目则截断并置
* lib->truncated=1(UI 用 mr_library_row_at 末尾的提示行标注)。
*
* M11 档案册(.cbz/.zip):注入 archive_pages 回调时,当前层的档案文件也成行(与目录同列
* 自然序混排);档案册的页数同样走懒探测(mr_library_probe_path + 注入回调),logic 层
* 不依赖 zip 容器。未注入时(archive_pages==NULL)只列目录,等价 M7 书库。
*
* M7 续读角标 / 册间衔接:mr_library_format_subtitle 在探测完成后拼「· 续读 Pn」
* (mr_cont_badge);mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变。
*/
#ifndef MR_LIBRARY_H
#define MR_LIBRARY_H
@@ -24,24 +30,55 @@ extern "C" {
#define MR_PATH_MAX 512
#define MR_NAME_MAX 256
#define MR_LIB_MAX_DEPTH 8
#define MR_LIB_MAX_DEPTH 8 /* 兼容旧调用:扫描已不递归,该参数被忽略 */
/* 单目录列出的条目上限(超大目录截断 + UI 提示;可用 -D 覆写便于宿主测试)。 */
#ifndef MR_LIB_SCAN_MAX
#define MR_LIB_SCAN_MAX 2000
#endif
/* 行懒判定状态:0=待探测(副标题占位「…」)/1=已探测/2=探测失败(不可读)。 */
#define MR_LIB_PENDING 0
#define MR_LIB_READY 1
#define MR_LIB_FAILED 2
typedef struct mr_library_entry {
char name[MR_NAME_MAX]; /* 目录名/档案文件名(行主标题) */
char path[MR_PATH_MAX]; /* 完整路径(导航/进 reader 用) */
char cover[MR_PATH_MAX]; /* 缩略图源图(空=none;档案册由 UI 解压首图) */
char subtitle[48]; /* 预先格式化好的 "N pages" / "N books" / "N folders" */
int pages; /* 本目录直接图片数(档案册=档内图片页数) */
int subdirs; /* 本目录下含图子目录数 */
int has_images; /* 1=可直接阅读(直接含图 或 档案册) */
char path[MR_PATH_MAX]; /* 完整路径(导航/进 reader/探测用) */
char cover[MR_PATH_MAX]; /* 后台探测填:自然序第一张直接子图(档案册留空,UI 解压首图) */
char subtitle[48]; /* 占位「…」;探测后 "N pages"/"空目录"/"无图片"/"无法读取" */
int pages; /* -1=未判定;>=0=直接图片数(档案册=档内图片页数) */
int subdirs; /* 兼容字段(M11 及旧读者):新扫描不统计,恒 0 */
int has_images; /* 1=书;0=非书;-1=未判定 */
int probe_state; /* MR_LIB_PENDING / MR_LIB_READY / MR_LIB_FAILED */
int empty; /* 1=空目录(目录且 probe_state==READY 后有效) */
int is_archive; /* 1=.cbz/.zip 档案册行 */
} mr_library_entry;
typedef struct mr_library {
mr_library_entry *items;
int count;
int count; /* 当前层列出的条目数 */
int cap;
int truncated; /* 1=达到 MR_LIB_SCAN_MAX 被截断 */
} mr_library;
/* 单行探测结果(目录=本层直接图片数;档案册=档内图片页数;均不递归)。 */
typedef struct mr_library_probe {
int pages; /* 受支持图片数(png/jpg/jpeg/bmp;档案册=档内页数) */
int empty; /* 1=目录没有可见子项(档案册恒 0) */
char cover[MR_PATH_MAX]; /* 目录的自然序第一张图(档案册留空) */
} mr_library_probe;
/* 行数据(纯内存合成;UI 的每帧 src 回调直接取用,零 IO)。 */
typedef struct mr_library_row {
const char *title; /* 主标题 */
const char *subtitle; /* 副标题(可空串) */
int is_book; /* 1=书(显示页数/缩略图,点入阅读,无进入箭头) */
int is_archive; /* 1=档案册行 */
int disabled; /* 1=提示行(不可点,如截断标注) */
int index; /* 对应 mr_library.items 下标;提示行=-1 */
} mr_library_row;
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
int mr_path_is_image(const char *name);
@@ -53,22 +90,19 @@ int mr_path_is_archive(const char *name);
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序。 */
int mr_natural_cmp(const char *a, const char *b);
/* 扫描 dir 的直接子目录,写入 lib(先释放 lib 旧内容)。
* 返回条目数(>=0);dir 不可读返回 -1(lib 仍为空且可安全 mr_library_free)。
* max_depth<=0 或超过 MR_LIB_MAX_DEPTH 时按上限处理。 */
/* 扫描 dir 的**直接子目录**(单层;不读子目录内容),写入 lib(先释放 lib 旧内容)。
* 返回条目数(>=0);dir 不可读/不存在/不是目录返回 -1(lib 仍为空且可安全 free)。
* max_depth 仅为兼容旧调用保留,已忽略(不再递归)。 */
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
/* ============================================================ */
/* 档案册(M11):.cbz/.zip 当一本书,与文件夹册同列 */
/* ------------------------------------------------------------
* logic 层不依赖 zip 容器(保持宿主裸测/链接面干净):档案册探测由 UI 注入。
* archive_pages(path, ud) 对普通文件返回图片页数(>0=是一本书),0=不是/坏档。
* 传 NULL 时行为与 mr_library_scan 完全一致(纯目录书库),M7 语义不变。
*
* 档案册行的口径:name=文件名(含扩展名)、path=档案文件路径、pages=档内页数、
* has_images=1(点入直接阅读)、cover 留空(缩略图由 UI 解压首图生成)、
* 副标题 "N pages"、续读角标同文件夹册(按档案路径记)。
* 只含档案册的子目录算「可导航」(nav),副标题写 "N books"。
* archive_pages(path, ud) 返回档内图片页数(>0=是一本书),0=不是/坏档。
* 传 NULL 时行为与 M7 完全一致(纯目录书库,档案文件不成行)。
* 档案册行:name=文件名、path=档案路径、is_archive=1、cover 留空(UI 解压首图)、
* 页数/书标记由后台 probe 经同一注入回调懒算。
* ============================================================ */
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
@@ -79,14 +113,52 @@ int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
void mr_library_free(mr_library *lib);
/* 取父目录路径(去掉末尾 '/' 后截断最后一段)。根 "/" 返回 "/"。
* 成功 0;参数非法/输出缓冲不足 -1。 */
* 成功 0;参数非法/输出缓冲不足 -1。可一路向上到文件系统 '/'。 */
int mr_path_parent(const char *path, char *out, int outsz);
/* ---- 后台懒判定(单层探测;可由工作线程调用) ---- */
/* 单层探测目录 dir:直接图片数 + 自然序首图 + 是否空目录。不递归、不跟随符号链接。
* 0=成功;-1=参数非法/不可读(out 清零)。 */
int mr_library_probe_dir(const char *dir, mr_library_probe *out);
/* 带缓存探测目录:按目录 mtime(秒+纳秒)+ size 失效;内部互斥锁,可多线程并发调用。
* 0=成功(可能来自缓存);-1=不可读(失败不缓存)。 */
int mr_library_probe_cached(const char *dir, mr_library_probe *out);
/* 按行探测(目录或档案册),带同一套缓存:
* is_archive=0 → 目录(等价 mr_library_probe_cached);
* is_archive=1 → 调 archive_pages(path,ud) 取档内页数(cover 留空、empty=0);
* archive_pages 为 NULL 时返回 -1。
* 0=成功;-1=参数非法/不可读。探测期间 mtime/size 变化则不写入缓存(避免假新鲜)。 */
int mr_library_probe_path(const char *path, int is_archive,
mr_lib_archive_fn archive_pages, void *ud,
mr_library_probe *out);
/* 清空探测缓存(目录整体切换/测试用)。 */
void mr_library_probe_cache_clear(void);
/* 把探测结果补回第 idx 行(pages/has_images/cover/subtitle/empty)。
* 1=已应用;0=参数非法/越界。 */
int mr_library_apply_probe(mr_library *lib, int idx, const mr_library_probe *p);
/* 标记第 idx 行探测失败(不可读):副标题「无法读取」,视为非书。1=已应用;0=非法。 */
int mr_library_mark_error(mr_library *lib, int idx);
/* 按行当前状态重算副标题(占位/页数/空目录/无图片/无法读取),
* 并在书行(has_images==1)后缀「· 续读 Pn」(M7 角标)。 */
void mr_library_format_subtitle(mr_library_entry *e);
/* 行合成(纯内存、零 IO、O(1);UI 线程每帧调用):idx 越界返回 0。
* idx==count 且 lib->truncated 时输出末尾提示行(disabled=1)。
* 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out);
/* 目录是否**直接**含受支持图片(= 书库语义里的「一本书」,可直接进 reader)。
* 1=有;0=没有/不可读。 */
int mr_library_dir_has_images(const char *dir);
/* 册间衔接(M7):同父目录下按书库自然序排列的「可直接阅读」兄弟册。
/* 册间衔接(M7):同父目录下按书库自然序排列的「直接含图」兄弟册。
* delta=+1 下一册 / -1 上一册。1=找到并写入 out 完整路径;
* 0=没有相邻册(当前册是首/末册,或当前册不在兄弟表中);-1=参数非法。
* 语义与书库一致:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
@@ -94,12 +166,23 @@ int mr_library_dir_has_images(const char *dir);
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz);
/* 册间衔接 + 档案册(M11):兄弟表 = 「直接含图目录」∪「含图片页的 .cbz/.zip」,
* 两者同列按名称自然序混排(mission 基线:跨册衔接按同父目录自然序)。
* 因此末页前进可以从文件夹册进到同目录的档案册,反之亦然。
* 两者同列按名称自然序混排(跨册衔接按同父目录自然序)。
* archive_pages 传 NULL 时与 mr_book_adjacent 完全等价。 */
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
mr_lib_archive_fn archive_pages, void *ud);
#ifdef MR_LIBRARY_TEST_HOOKS
/* 宿主测试用 IO 计数(scan/probe_dir 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
typedef struct mr_library_io_stats {
long opendirs;
long readdirs;
long lstats;
} mr_library_io_stats;
void mr_library_test_io_reset(void);
void mr_library_test_io_get(mr_library_io_stats *out);
#endif
#ifdef __cplusplus
}
#endif