fix(reader): M27 r2 — 扫描深度提到 6 + 按 Books 第一级目录分组

真机真实布局暴露两处问题(评审 P1):
- Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 在 depth 3-4,旧 depth≤2 让 413 本 3.8GB 不可见
  → SHELF_SCAN_MAX_DEPTH=2→6;report 新增 depth_skipped,超深书籍文件计数并在 g_notice 可见提示
  (有 N 个文件因目录层数超过 6 层未显示),不再静默
- 按完整相对路径分组会把 413 本书炸成 410 个每条 1 书的段头
  → 分组键改为 Books/ 第一级目录(shelf_book.top);完整相对路径仍在 group[],
    第一级之后的子路径(分类/作品)放进副题 sub(如 01、玄幻/01、斗破 · TXT · 2.1MB)
  → 真机书库只剩 3 组:400+本高质量完本合集 / kh / 轻小说
- 单测 50 断言:depth3/4 收录、depth7 计入 depth_skipped、四种真实路径的组名、
  a/b 与 a/c 同组 a、副题含子路径、format 回归、根组未分类、path/进度不变
This commit is contained in:
AskaEth committed 2026-11-09 22:29:46 +08:00
1 parent 409fd1edf7
commit 63ead3ec1e
5 files changed
+211 -82

No files matched your search

+21 -12
View File
@@ -3,11 +3,13 @@
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
* 扫描计时(test/shelf_scan_bench.c)共用。
*
* 目录归属(M27):folder 模式按「相对 Books/ 的目录路径」分组,如 "轻小说"、"科幻/长篇";
* Books/ 根下的书 group="",显示段头 SHELF_GROUP_ROOT。分组键是相对路径(不是目录名),
* 故 Books/a/b 与 Books/b 不会混。
* 目录归属(M27 r2):folder 模式按「Books/ 下的第一级目录」分组(如 "轻小说"、
* "400+本高质量完本合集");更深的相对子路径("01、玄幻/01、斗破")进该书的副题,
* 不再每组一本书把书架炸成上百个段头。Books/ 根下的书 top="",显示段头 SHELF_GROUP_ROOT。
* 分组键是「第一级目录名」,完整相对路径保留在 group[] 供副题/调试用。
* a/b 与 a/c 同属 a 组;a/b 与 b 仍是两个组(不会因为只取末级目录名而混)。
*
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),分组只写 group,
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),分组只写 top/group,
* 绝不改写 path;两个分组模式间切换不影响进度。
*/
#ifndef SHELF_MODEL_H
@@ -15,10 +17,14 @@
#include <stdint.h>
/* 书籍上限:真实场景 180 本(Books/轻小说),2048 给 11× 余量。
/* 书籍上限:真机真实书库 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)
#define SHELF_GROUP_ROOT "未分类" /* Books/ 根下书籍的段头 */
@@ -26,8 +32,9 @@
typedef struct shelf_book {
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
char title[128];
char sub[160];
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的目录路径;根下="" */
char sub[160]; /* 副题:更深子路径 · 格式 · 大小 [· 读] */
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径(供副题);根下="" */
char top[SHELF_GROUP_MAX]; /* 分组键:相对路径的第一级目录名;根下="" */
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
long size;
uint64_t ts; /* 最近阅读 unix;无=0 */
@@ -37,7 +44,7 @@ typedef struct shelf_row {
int section; /* 1=段头行 */
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
int book; /* 书行=books 下标;段头=-1 */
const char *label; /* 段头标题;NULL=格式名(指向 books[].group 或静态串) */
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
} shelf_row;
/* 进度时间戳查询(生产=rdprog_load;测试/基线=NULL,全部 ts=0)。 */
@@ -46,18 +53,20 @@ typedef uint64_t (*shelf_ts_fn)(const char *path);
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(子目录深度 ≤2,与旧行为一致),按 ts 降序/书名升序排序后写 books(容量 cap)。
* 返回写入本数。目录为空不产生任何记录(段头由 build_rows 按书生成,天然无空段头)。 */
/* 递归扫描 root(目录深度 ≤ SHELF_SCAN_MAX_DEPTH,超限文件计数但不出书),
* 按 ts 降序/书名升序排序后写 books(容量 cap)。
* 返回写入本数。空目录不产生任何记录(段头由 build_rows 按书生成,天然无空段头)。 */
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:先按目录分组,根目录组「未分类」置顶,其余组按目录名排序;
* 组内沿用 books[] 的全局顺序(最近阅读优先、其次书名)。
* folder_mode=1:按 books[].top(Books/ 第一级目录)分组,根组「未分类」置顶,
* 其余组按组名排序;组内沿用 books[] 全局顺序(最近阅读优先、其次书名)。
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
shelf_row *rows, int rowcap);