/* 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 /* 书籍上限:真机真实书库 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/"。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 */