fix(reader): M27 r3 评审整改 — cap 边界截断/默认副题不变/提示两行且 6s 消失/版本号

对应评审 P2-1、P3-7、P3-1、P3-2、P3-10、P3-9:
- P2-1:scan_dir 在目录边界遇 cap 时探测子树(count_books_under)再置 truncated;
  同目录内超限逻辑不变。新增两条断言:cap 卡边界 → truncated=1;恰好装满且后续为空目录 → 不误报
- P3-7:shelf_book 拆 sub(默认副题,与 base 逐字一致)/sub_folder(folder 模式前置子路径);
  list 按模式选,宫格始终用 base;断言 format 副题逐字等于 base
- P3-1:超量+超深合并提示拆两行绘制(610 起,不再被 480px 右缘切掉)
- P3-2:提示 6s 到期自动消失(app.c 书架前台时调 shelf_on_tick)
- P3-10:app.json version 0.1.1 / version_code 2
- P3-9:shelf_scan_bench.c 头部注明 /proc/uptime 10ms 分辨率、只适合真机
This commit is contained in:
AskaEth committed 2026-11-09 22:45:01 +08:00
1 parent 63ead3ec1e
commit e0a82a3d47
8 files changed
+129 -44

No files matched your search

+48 -18
View File
@@ -35,8 +35,10 @@ static meow_widget *g_appbar; /* meow_topappbar */
static int g_layout; /* 0=list 1=grid(读 rdconf->shelf) */
static int g_group_folder;/* 0=format 1=folder(读 rdconf->shelf_group) */
static uint32_t g_bg, g_fg, g_dim;
static char g_notice[160]; /* 提示(opendir 失败/超上限截断等) */
static int g_notice_ms; /* 剩余显示 ms(0=无) */
static char g_notice[160]; /* 提示第 1 行(opendir 失败/超上限截断等) */
static char g_notice2[160]; /* 提示第 2 行(超量+超深并存时才用;空=单行) */
static int g_notice_ms; /* 1=有提示(0=无) */
static uint64_t g_notice_until; /* 提示到期时刻(meow_ui_time_ms;6s 后自动消失) */
/* 段头行(list 模式):[段头, 书, 书, ..., 段头, ...];SHELF_MAX_ROWS=2×书上限(见 shelf_model.h)。 */
static shelf_row g_rows[SHELF_MAX_ROWS];
@@ -51,29 +53,43 @@ static uint64_t shelf_ts_of(const char *path)
return (rdprog_load(path, &p) == 1) ? p.ts : 0;
}
static void notice_set(const char *line1, const char *line2)
{
snprintf(g_notice, sizeof g_notice, "%s", line1 ? line1 : "");
snprintf(g_notice2, sizeof g_notice2, "%s", line2 ? line2 : "");
g_notice_ms = 1;
g_notice_until = meow_ui_time_ms() + 6000; /* 6s 后由 shelf_on_tick 清掉,不常驻压列表 */
}
static void scan_books(void)
{
const char *dir = READER_SD_ROOT "/Books";
g_notice[0] = 0;
g_notice2[0] = 0;
g_notice_ms = 0;
shelf_scan_report rep;
g_nbooks = shelf_scan_books(dir, g_books, SHELF_MAX_BOOKS, shelf_ts_of, &rep);
if (rep.root_missing) {
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
g_notice_ms = 4000;
char line[160];
snprintf(line, sizeof line, "未找到 %s 目录,请放入书籍(TXT)", dir);
notice_set(line, NULL);
} 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 层未显示",
/* 长句拆两行(480px 单行会被切掉) */
char l1[160], l2[160];
snprintf(l1, sizeof l1, "书籍超过 %d 本,已只显示前 %d 本", SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
snprintf(l2, sizeof l2, "另有 %d 个文件目录超过 %d 层未显示",
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
g_notice_ms = 4000;
notice_set(l1, l2);
} else if (rep.truncated) {
char line[160];
snprintf(line, sizeof line, "书籍超过 %d 本,已只显示前 %d 本",
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
notice_set(line, NULL);
} else if (rep.depth_skipped > 0) {
char line[160];
snprintf(line, sizeof line, "有 %d 个文件因目录层数超过 %d 层未显示",
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
notice_set(line, NULL);
}
}
@@ -106,7 +122,7 @@ static void shelf_list_bind(int idx, meow_lv_item *item, void *ctx)
int bi = g_rows[idx].book;
if (bi < 0 || bi >= g_nbooks) return;
item->title = g_books[bi].title;
item->subtitle = g_books[bi].sub;
item->subtitle = g_group_folder ? g_books[bi].sub_folder : g_books[bi].sub;
item->icon_kind = MEOW_LIST_ICON_FILE;
item->right = MEOW_LIST_RIGHT_ARROW;
}
@@ -267,6 +283,16 @@ meow_view *shelf_view_create(void)
int shelf_is_active(void) { return g_active; }
/* 书架 tick(app.c 在书架为前台页时调用):提示条 6s 到期自动消失。 */
void shelf_on_tick(uint64_t now)
{
if (g_notice_ms > 0 && now >= g_notice_until) {
g_notice_ms = 0;
g_notice_until = 0;
shelf_redraw();
}
}
/* 切换 list/grid(书架顶栏「布局切换」动作;改 reader.conf 并重建)。 */
void shelf_set_view(int grid)
{
@@ -336,8 +362,12 @@ void shelf_redraw(void)
meow_ui_text_center(480 / 2, cy + 150, "Books/ 目录", 20, th->primary);
}
if (g_notice_ms > 0) {
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
meow_ui_text(20, 630, g_notice, 20, th->primary);
int two = g_notice2[0] != 0;
int top = two ? 610 : 620;
int h = two ? 58 : 44;
meow_ui_fill_rect(0, top, 480, h, g_bg);
meow_ui_text(20, top + 4, g_notice, 20, th->primary);
if (two) meow_ui_text(20, top + 30, g_notice2, 20, th->primary);
}
meow_ui_flush();
}
+23 -13
View File
@@ -71,13 +71,14 @@ typedef struct scan_ctx {
shelf_ts_fn ts_fn;
} scan_ctx;
/* 已超过深度上限的目录:只数其中还能识别的书籍文件,不出书(不静默跳过)。
* 自身再留 8 层硬上限,防符号链接/异常深目录把计数也拖死。 */
static void count_books_under(scan_ctx *c, const char *dir, int depth)
/* 统计 dir 子树里还能识别的书籍文件数(只数不收集)。自身留 8 层硬上限,
* 防符号链接/异常深目录把计数也拖死。返回书籍文件数。 */
static int count_books_under(const char *dir, int depth)
{
if (depth > SHELF_SCAN_MAX_DEPTH + 8) return;
if (depth > SHELF_SCAN_MAX_DEPTH + 8) return 0;
int n = 0;
DIR *dp = opendir(dir);
if (!dp) return;
if (!dp) return 0;
struct dirent *de;
while ((de = readdir(dp))) {
if (de->d_name[0] == '.') continue;
@@ -85,10 +86,11 @@ static void count_books_under(scan_ctx *c, const char *dir, int depth)
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++;
if (S_ISDIR(st.st_mode)) { n += count_books_under(path, depth + 1); continue; }
if (shelf_fmt_from_ext(de->d_name) >= 0) n++;
}
closedir(dp);
return n;
}
/* 递归收集。rel = 相对 root 的完整目录路径(root 自身为 ""),
@@ -108,7 +110,14 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel, c
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
if (depth + 1 > SHELF_SCAN_MAX_DEPTH) {
count_books_under(c, path, depth + 1); /* 超深:计数→可见提示 */
c->depth_skipped += count_books_under(path, depth + 1); /* 超深:计数→可见提示 */
continue;
}
if (c->n >= c->cap) {
/* 上限卡在目录边界:探一下这个子树是否真有书,有书才算被截断。
* 只探空目录不置位,避免“正好装满且后面全是空目录”误报。 */
if (!c->truncated && count_books_under(path, depth + 1) > 0) c->truncated = 1;
if (c->truncated) break; /* 已确认丢书,后面的目录/文件不必再跑 */
continue;
}
char subrel[SHELF_GROUP_MAX * 16];
@@ -132,17 +141,18 @@ static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel, c
b->ts = c->ts_fn ? c->ts_fn(b->path) : 0;
char sz[24];
shelf_fmt_size(sz, sizeof sz, b->size);
/* 副题:第一级之后的子路径(分类/作品)· 格式 · 大小 [· 读] */
/* 默认副题:与 M27 前逐字一致(format 模式/宫格都用它)。 */
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz,
b->ts ? " · 读" : "");
/* folder 副题:第一级之后的子路径(分类/作品)前置,其余同默认副题。 */
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 ? " · 读" : "");
snprintf(b->sub_folder, sizeof b->sub_folder, "%s · %s", pre, b->sub);
} else {
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(f), sz,
b->ts ? " · 读" : "");
snprintf(b->sub_folder, sizeof b->sub_folder, "%s", b->sub);
}
c->n++;
}
+2 -1
View File
@@ -32,7 +32,8 @@
typedef struct shelf_book {
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
char title[128];
char sub[160]; /* 副题:更深子路径 · 格式 · 大小 [· 读] */
char sub[160]; /* 默认(format/宫格)副题:格式 · 大小 [· 读],与 M27 前逐字一致 */
char sub_folder[160]; /* folder 模式副题:更深子路径 · 格式 · 大小 [· 读] */
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径(供副题);根下="" */
char top[SHELF_GROUP_MAX]; /* 分组键:相对路径的第一级目录名;根下="" */
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */