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:
1 parent
409fd1edf7
commit
63ead3ec1e
5 files changed
+211
-82
No files matched your search
@@ -9,7 +9,8 @@
|
||||
* - 封面:MVP 无封面提取(engine F1);无则圆角占位(surfaceContainerHigh + 格式徽标)。
|
||||
*
|
||||
* M27 分组(`ui.shelf_group`,默认 format 保持旧行为):
|
||||
* format = 按格式分段(TXT/FB2/EPUB/MOBI/CBZ),folder = 按 Books/ 下的目录分段(根下=未分类)。
|
||||
* format = 按格式分段(TXT/FB2/EPUB/MOBI/CBZ);
|
||||
* folder = 按 Books/ 下的「第一级目录」分段(根下=未分类),更深子路径(分类/作品)进副题。
|
||||
* 扫描/行序列在 app/shelf_model.c(纯 C,宿主可测);本文件只做控件装配与绘制。
|
||||
*/
|
||||
#include "../reader_internal.h"
|
||||
@@ -60,10 +61,19 @@ static void scan_books(void)
|
||||
if (rep.root_missing) {
|
||||
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
|
||||
g_notice_ms = 4000;
|
||||
} else if (rep.truncated && rep.depth_skipped > 0) {
|
||||
snprintf(g_notice, sizeof g_notice,
|
||||
"书籍超过 %d 本(只显示前 %d 本);另有 %d 个文件目录超过 %d 层未显示",
|
||||
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS, rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
g_notice_ms = 4000;
|
||||
} else if (rep.truncated) {
|
||||
snprintf(g_notice, sizeof g_notice, "书籍超过 %d 本,已只显示前 %d 本",
|
||||
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
|
||||
g_notice_ms = 4000;
|
||||
} else if (rep.depth_skipped > 0) {
|
||||
snprintf(g_notice, sizeof g_notice, "有 %d 个文件因目录层数超过 %d 层未显示",
|
||||
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
|
||||
g_notice_ms = 4000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define SHELF_SCAN_MAX_DEPTH 2 /* 子目录深度上限(Books/a/b 有书,Books/a/b/c 不扫) */
|
||||
|
||||
int shelf_fmt_from_ext(const char *name)
|
||||
{
|
||||
const char *d = strrchr(name, '.');
|
||||
@@ -69,11 +67,33 @@ typedef struct scan_ctx {
|
||||
int cap;
|
||||
int n;
|
||||
int truncated;
|
||||
int depth_skipped;
|
||||
shelf_ts_fn ts_fn;
|
||||
} scan_ctx;
|
||||
|
||||
/* 递归收集:rel = 相对 root 的目录路径(root 自身为 "")。 */
|
||||
static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel)
|
||||
/* 已超过深度上限的目录:只数其中还能识别的书籍文件,不出书(不静默跳过)。
|
||||
* 自身再留 8 层硬上限,防符号链接/异常深目录把计数也拖死。 */
|
||||
static void count_books_under(scan_ctx *c, const char *dir, int depth)
|
||||
{
|
||||
if (depth > SHELF_SCAN_MAX_DEPTH + 8) return;
|
||||
DIR *dp = opendir(dir);
|
||||
if (!dp) return;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(dp))) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
char path[400];
|
||||
if (snprintf(path, sizeof path, "%s/%s", dir, de->d_name) >= (int)sizeof path) continue;
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) continue;
|
||||
if (S_ISDIR(st.st_mode)) { count_books_under(c, path, depth + 1); continue; }
|
||||
if (shelf_fmt_from_ext(de->d_name) >= 0) c->depth_skipped++;
|
||||
}
|
||||
closedir(dp);
|
||||
}
|
||||
|
||||
/* 递归收集。rel = 相对 root 的完整目录路径(root 自身为 ""),
|
||||
* top = 第一级目录名(root 自身/根下文件为 "")。 */
|
||||
static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel, const char *top)
|
||||
{
|
||||
if (depth > SHELF_SCAN_MAX_DEPTH) return; /* 防深嵌套/循环 */
|
||||
if (c->n >= c->cap) return; /* 已满:不再进新目录 */
|
||||
@@ -87,10 +107,14 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel)
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) continue;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char subrel[SHELF_GROUP_MAX * 4];
|
||||
if (snprintf(subrel, sizeof subrel, "%s%s%s", rel, rel[0] ? "/" : "", de->d_name)
|
||||
>= (int)sizeof subrel) continue;
|
||||
scan_dir(c, path, depth + 1, subrel);
|
||||
if (depth + 1 > SHELF_SCAN_MAX_DEPTH) {
|
||||
count_books_under(c, path, depth + 1); /* 超深:计数→可见提示 */
|
||||
continue;
|
||||
}
|
||||
char subrel[SHELF_GROUP_MAX * 16];
|
||||
snprintf(subrel, sizeof subrel, "%s%s%s", rel, rel[0] ? "/" : "", de->d_name);
|
||||
/* 第一级目录名 = top;更深层沿用。de->d_name 在递归返回前有效。 */
|
||||
scan_dir(c, path, depth + 1, subrel, depth == 0 ? de->d_name : top);
|
||||
continue;
|
||||
}
|
||||
int f = shelf_fmt_from_ext(de->d_name);
|
||||
@@ -102,13 +126,24 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel)
|
||||
char *dot = strrchr(b->title, '.');
|
||||
if (dot) *dot = 0; /* 标题去扩展名 */
|
||||
shelf_copy_utf8(b->group, (int)sizeof b->group, rel);
|
||||
shelf_copy_utf8(b->top, (int)sizeof b->top, top);
|
||||
b->fmt = f;
|
||||
b->size = (long)st.st_size;
|
||||
b->ts = c->ts_fn ? c->ts_fn(b->path) : 0;
|
||||
char sz[24];
|
||||
shelf_fmt_size(sz, sizeof sz, b->size);
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
/* 副题:第一级之后的子路径(分类/作品)· 格式 · 大小 [· 读] */
|
||||
const char *rest = strchr(rel, '/');
|
||||
if (rest) rest++;
|
||||
if (rest && rest[0]) {
|
||||
char pre[100];
|
||||
shelf_copy_utf8(pre, (int)sizeof pre, rest);
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s · %s%s", pre, shelf_fmt_name(f), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
} else {
|
||||
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz,
|
||||
b->ts ? " · 读" : "");
|
||||
}
|
||||
c->n++;
|
||||
}
|
||||
closedir(dp);
|
||||
@@ -117,7 +152,7 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel)
|
||||
int shelf_scan_books(const char *root, shelf_book *books, int cap,
|
||||
shelf_ts_fn ts_fn, shelf_scan_report *rep)
|
||||
{
|
||||
if (rep) { rep->nbooks = 0; rep->truncated = 0; rep->root_missing = 0; }
|
||||
if (rep) { rep->nbooks = 0; rep->truncated = 0; rep->depth_skipped = 0; rep->root_missing = 0; }
|
||||
if (!root || !books || cap <= 0) {
|
||||
if (rep) rep->root_missing = 1;
|
||||
return 0;
|
||||
@@ -130,22 +165,40 @@ int shelf_scan_books(const char *root, shelf_book *books, int cap,
|
||||
closedir(dp);
|
||||
|
||||
scan_ctx c;
|
||||
c.books = books; c.cap = cap; c.n = 0; c.truncated = 0; c.ts_fn = ts_fn;
|
||||
scan_dir(&c, root, 0, "");
|
||||
c.books = books; c.cap = cap; c.n = 0; c.truncated = 0; c.depth_skipped = 0; c.ts_fn = ts_fn;
|
||||
scan_dir(&c, root, 0, "", "");
|
||||
qsort(books, (size_t)c.n, sizeof(shelf_book), cmp_recent_then_name);
|
||||
if (rep) { rep->nbooks = c.n; rep->truncated = c.truncated; }
|
||||
if (rep) {
|
||||
rep->nbooks = c.n;
|
||||
rep->truncated = c.truncated;
|
||||
rep->depth_skipped = c.depth_skipped;
|
||||
}
|
||||
return c.n;
|
||||
}
|
||||
|
||||
/* ---- 行序列 ---- */
|
||||
|
||||
/* 段头排序:根组(group=="")恒置顶,其余按目录名不区分大小写排序。 */
|
||||
/* 分组键 = top(Books 第一级目录名)。长度截到第一个 '/',排序时忽略大小写。 */
|
||||
static int top_len(const char *s)
|
||||
{
|
||||
const char *p = strchr(s, '/');
|
||||
return p ? (int)(p - s) : (int)strlen(s);
|
||||
}
|
||||
static int top_cmp(const char *a, const char *b)
|
||||
{
|
||||
int la = top_len(a), lb = top_len(b);
|
||||
if (!la != !lb) return la ? 1 : -1; /* 根组 "" 恒置顶 */
|
||||
int m = la < lb ? la : lb;
|
||||
int c = strncasecmp(a, b, (size_t)m);
|
||||
if (c) return c;
|
||||
return la - lb;
|
||||
}
|
||||
static int top_eq(const char *a, const char *b) { return top_cmp(a, b) == 0; }
|
||||
|
||||
/* 段头排序:根组(top=="")恒置顶,其余按第一级目录名不区分大小写排序。 */
|
||||
static int cmp_group_key(const void *pa, const void *pb)
|
||||
{
|
||||
const char *a = *(const char *const *)pa;
|
||||
const char *b = *(const char *const *)pb;
|
||||
if (!a[0] != !b[0]) return a[0] ? 1 : -1; /* "" 在前 */
|
||||
return strcasecmp(a, b);
|
||||
return top_cmp(*(const char *const *)pa, *(const char *const *)pb);
|
||||
}
|
||||
|
||||
static int build_rows_format(const shelf_book *books, int nbooks,
|
||||
@@ -178,14 +231,14 @@ static int build_rows_format(const shelf_book *books, int nbooks,
|
||||
static int build_rows_folder(const shelf_book *books, int nbooks,
|
||||
shelf_row *rows, int rowcap)
|
||||
{
|
||||
/* 去重目录键(""=根组)。键是指向 books[].group 的字符串,books 生存期覆盖 rows。 */
|
||||
/* 去重分组键(books[].top,""=根组)。键指向 books 内字符串,生存期覆盖 rows。 */
|
||||
static const char *keys[SHELF_MAX_BOOKS];
|
||||
int nkeys = 0;
|
||||
for (int i = 0; i < nbooks; i++) {
|
||||
const char *k = books[i].group;
|
||||
const char *k = books[i].top;
|
||||
int seen = 0;
|
||||
for (int j = 0; j < nkeys; j++) {
|
||||
if (strcmp(keys[j], k) == 0) { seen = 1; break; }
|
||||
if (top_eq(keys[j], k)) { seen = 1; break; }
|
||||
}
|
||||
if (!seen) keys[nkeys++] = k;
|
||||
}
|
||||
@@ -200,7 +253,7 @@ static int build_rows_folder(const shelf_book *books, int nbooks,
|
||||
rows[nrows].label = keys[ki][0] ? keys[ki] : SHELF_GROUP_ROOT;
|
||||
nrows++;
|
||||
for (int i = 0; i < nbooks; i++) {
|
||||
if (strcmp(books[i].group, keys[ki]) != 0) continue;
|
||||
if (!top_eq(books[i].top, keys[ki])) continue;
|
||||
if (nrows >= rowcap) break;
|
||||
rows[nrows].section = 0;
|
||||
rows[nrows].fmt = books[i].fmt;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user