Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_model.h
T
AskaEth 2881ce908a feat(reader): M33 目录模式逐级下钻 — 子目录/书分层显示(416 本不再平铺)
- folder 模式从「按第一级目录分段」改为当前目录视图:子目录在前(带子树书数),
  直接书在后;进入/返回只过滤已扫平的 g_books,不 readdir、不重扫(M32 索引不动)。
- 纯函数 shelf_build_rows_at(books,n,folder,cwd,...) + nav 工具(enter/up/breadcrumb/
  count)全部宿主可测;format 模式行序列/副题逐字不变,path 仍是完整路径(进度键不变)。
- 交互:点目录进入;顶栏返回箭头/左缘返回手势回上一级,根层再返回退出 App;顶栏标题=
  当前目录名(按像素宽截断),副题行=面包屑全路径+当前层书数;list/grid 均支持导航。
- 状态:folder↔format、list↔grid 切换保留 cwd;刷新重扫后 cwd 失效则收缩到有书的祖先。
- 边界:空目录不显示(导航只读 g_books,不额外 readdir);深层目录逐级进入;真实名为
  「未分类」的目录按普通目录处理,不与根层书合并。
- 测试:shelf_group_smoke 63→99 断言;M27 的按第一级分组断言改写为「逐级进入后可见该书」
  (同等强度),新增进入/返回/面包屑/计数/空目录/纯内存/rowcap/UTF-8 截断断言。
- 交付 0.1.7/code8(M32 已占用任务书里的 0.1.6/code7);真机 0.1.4 未被覆盖。
2026-11-10 19:26:44 +08:00

132 lines
8.1 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* app/shelf_model.h — 书架数据模型:扫描 Books/ + 生成行序列(纯 C,不依赖 SDK/设备)。
*
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
* 扫描计时(test/shelf_scan_bench.c)共用。
*
* 目录视图(M33):folder 模式不再是「按第一级目录分段」,而是**逐级进入**——书架只显示
* 当前目录(cwd,相对 Books/,根="")的内容:子目录在前(每个带子树书数)、直接属于该层的
* 书在后。cwd 由 App 维护;本模块的 shelf_build_rows_at() 只在已扫平的 books[].group 上做
* 前缀过滤,**不读盘、不 readdir**(M32 索引省下的 readdir 不能又被导航用回去)。
* M27 的 top(第一级目录名)仍保留在 shelf_book 里供索引/副题/诊断使用,但不再决定行序列。
* 完整相对路径保留在 group[]:path 仍是书文件的完整绝对路径。
*
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),导航只改 cwd,
* 绝不改写 path;format/folder 两个模式间切换不影响进度。
*/
#ifndef SHELF_MODEL_H
#define SHELF_MODEL_H
#include <stdint.h>
/* 书籍上限:真机真实书库 646 文件/约 595 本候选,2048 给 3×+ 余量。
* 超限不再静默停扫:report->truncated=1,由 UI 出提示。 */
#define SHELF_MAX_BOOKS 2048
#define SHELF_GROUP_MAX 96 /* 目录字段容量(含 NUL):95 字节 ≈ 31 个汉字 */
/* 扫描深度上限(root=0;文件所在目录 depth ≤ 该值才会被收录)。
* 真机真实布局 Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 最深 4 层,取 6 留余量。
* 超过上限的书籍文件计入 report->depth_skipped(不静默跳过)。 */
#define SHELF_SCAN_MAX_DEPTH 6
/* 行数上限:每本书最多贡献 1 行书 + 1 行段头(段头数 ≤ 书数),故 2N 必够。 */
#define SHELF_MAX_ROWS (SHELF_MAX_BOOKS * 2)
typedef struct shelf_book {
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
char title[128];
char sub[160]; /* 默认副题:格式 · 大小 [· 读],与 M27 前逐字一致;
* format 模式与 folder 导航视图(书就在当前层)共用 */
char sub_folder[160]; /* 深层子路径 · 格式 · 大小 [· 读](M33 起导航视图不再显示;
* 保留字段供测试/兼容,不要用它改写 path) */
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径:folder 导航的前缀键;根下="" */
char top[SHELF_GROUP_MAX]; /* 第一级目录名(M27 遗留字段,供索引/诊断;M33 起不决定行序列) */
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
long size;
uint64_t ts; /* 最近阅读 unix;无=0 */
} shelf_book;
typedef struct shelf_row {
int section; /* 1=段头行(仅 format 模式) */
int dir; /* 1=目录导航行(仅 folder 模式);0=书行 */
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
int book; /* 书行=books 下标;段头/目录行=-1 */
int count; /* 目录行:该子目录下(含更深层)书籍数 */
char name[SHELF_GROUP_MAX]; /* 目录行:子目录名(不含父路径) */
char info[24]; /* 目录行:展示副题,如 "12 本" */
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
} shelf_row;
/* 进度时间戳查询(生产=rdprog_load;测试/基线=NULL,全部 ts=0)。 */
typedef uint64_t (*shelf_ts_fn)(const char *path);
/* M32:扫描时逐目录回调(书架索引的失效校验数据源)。
* rel = 相对 root 的目录路径(root 自身为 ""),books_here = 该目录里**实际收录**的
* 书籍文件数,mtime = 该目录的 stat 结果(秒 + 纳秒)。
* 回调**不能中止扫描**(返回 void):索引写盘失败只应放弃索引,书架必须照常出书。 */
typedef void (*shelf_dir_fn)(void *ctx, const char *rel, int64_t mtime_sec,
long mtime_nsec, int books_here);
/* 最近阅读排序:ts 降序,同 ts 按书名(不区分大小写)。扫描后与「刚读完置顶」共用。 */
void shelf_sort_recent(shelf_book *books, int nbooks);
typedef struct shelf_scan_report {
int nbooks;
int truncated; /* 1=有书因 SHELF_MAX_BOOKS 上限被丢弃 */
int depth_skipped; /* 因目录深度 > SHELF_SCAN_MAX_DEPTH 未收录的书籍文件数 */
int root_missing; /* 1=root 打不开(无卡/无 Books 目录) */
} shelf_scan_report;
/* 递归扫描 root(目录深度 ≤ SHELF_SCAN_MAX_DEPTH,超限文件计数但不出书),
* 按 ts 降序/书名升序排序后写 books(容量 cap)。
* 返回写入本数。空目录不产生任何记录(段头由 build_rows 按书生成,天然无空段头)。
* dir_fn 非 NULL 时,每进入并成功扫描一个目录回调一次(见 shelf_dir_fn)。 */
int shelf_scan_books_ex(const char *root, shelf_book *books, int cap,
shelf_ts_fn ts_fn, shelf_scan_report *rep,
shelf_dir_fn dir_fn, void *dir_ctx);
/* 同 shelf_scan_books_ex(不采集目录 mtime),供旧调用方/基准复用。 */
int shelf_scan_books(const char *root, shelf_book *books, int cap,
shelf_ts_fn ts_fn, shelf_scan_report *rep);
/* 生成行序列:
* folder_mode=0:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致;
* folder_mode=1:当前目录 cwd(相对 Books/,根="")的导航行——子目录在前(按名排序,
* 每个带 count/info),直接属于该层的书在后(沿用 books[] 全局顺序)。
* 纯内存:只读 books[].group,不做任何 opendir/readdir/stat。
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
int shelf_build_rows_at(const shelf_book *books, int nbooks, int folder_mode,
const char *cwd, shelf_row *rows, int rowcap);
/* 兼容旧调用:等价于 shelf_build_rows_at(..., cwd=""),即 folder 模式显示 Books/ 根层。 */
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
shelf_row *rows, int rowcap);
/* ---- 目录导航(纯字符串/纯内存工具,宿主可测) ---- */
/* cwd 子树(含 cwd 本层书)的书籍数。cwd 无效/无书返回 0。 */
int shelf_nav_count_under(const shelf_book *books, int nbooks, const char *cwd);
/* 进入子目录:cwd + "/" + name → out。1=成功;0=非法/超长(out 不改)。
* 根目录 cwd="" 时结果为 name(无前导 '/')。 */
int shelf_nav_enter(const char *cwd, const char *name, char *out, int outsz);
/* 返回上一级:非根返回 1 并写 out(根父为 "");已在根返回 0。 */
int shelf_nav_up(const char *cwd, char *out, int outsz);
/* 面包屑:根 → "Books/";子目录 → "Books/<cwd>"。UTF-8 安全截断到 outsz;
* 超长时保留末尾(当前目录最重要)并加 "…" 前缀。返回写入字节数。 */
int shelf_breadcrumb(const char *cwd, char *out, int outsz);
/* 当前目录名(cwd 最后一级);根返回 ""。返回写入字节数。 */
int shelf_dir_name(const char *cwd, char *out, int outsz);
/* 工具(单测直接调用)。 */
int shelf_fmt_from_ext(const char *name);
const char *shelf_fmt_name(int fmt);
void shelf_fmt_size(char *out, int outsz, long size);
/* 按 fmt/size/ts/group 重建 b->sub 与 b->sub_folder(扫描后与「刚读完置顶」共用)。 */
void shelf_fmt_sub(shelf_book *b);
/* UTF-8 安全截断:最多写 cap-1 字节;若截断点落在多字节序列中间,整段丢弃该残缺字符。
* 返回写入字节数(不含 NUL)。 */
int shelf_copy_utf8(char *dst, int cap, const char *src);
#endif /* SHELF_MODEL_H */