From e0a82a3d47125d35acfaee58cadd09efbd284910 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 9 Nov 2026 22:45:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(reader):=20M27=20r3=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E6=95=B4=E6=94=B9=20=E2=80=94=20cap=20=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E6=88=AA=E6=96=AD/=E9=BB=98=E8=AE=A4=E5=89=AF=E9=A2=98?= =?UTF-8?q?=E4=B8=8D=E5=8F=98/=E6=8F=90=E7=A4=BA=E4=B8=A4=E8=A1=8C?= =?UTF-8?q?=E4=B8=94=206s=20=E6=B6=88=E5=A4=B1/=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对应评审 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 分辨率、只适合真机 --- patch_v2/reader_app/app.c | 1 + patch_v2/reader_app/app.json | 2 +- patch_v2/reader_app/app/shelf.c | 66 ++++++++++++++------ patch_v2/reader_app/app/shelf_model.c | 36 +++++++---- patch_v2/reader_app/app/shelf_model.h | 3 +- patch_v2/reader_app/reader_internal.h | 1 + patch_v2/reader_app/test/shelf_group_smoke.c | 55 +++++++++++++--- patch_v2/reader_app/test/shelf_scan_bench.c | 9 ++- 8 files changed, 129 insertions(+), 44 deletions(-) diff --git a/patch_v2/reader_app/app.c b/patch_v2/reader_app/app.c index 903a925..17fdde3 100644 --- a/patch_v2/reader_app/app.c +++ b/patch_v2/reader_app/app.c @@ -41,6 +41,7 @@ static void app_on_tick(void) { uint64_t now = meow_ui_time_ms(); if (reader_is_active()) { reader_on_tick(now); return; } + if (shelf_is_active()) { shelf_on_tick(now); return; } } static void app_on_touch_down(int x, int y) diff --git a/patch_v2/reader_app/app.json b/patch_v2/reader_app/app.json index cfd7b34..f5d9d73 100644 --- a/patch_v2/reader_app/app.json +++ b/patch_v2/reader_app/app.json @@ -1 +1 @@ -{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1","version_code":1,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true} +{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.1","version_code":2,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true} diff --git a/patch_v2/reader_app/app/shelf.c b/patch_v2/reader_app/app/shelf.c index 1883ac7..ffe3ea2 100644 --- a/patch_v2/reader_app/app/shelf.c +++ b/patch_v2/reader_app/app/shelf.c @@ -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(); } diff --git a/patch_v2/reader_app/app/shelf_model.c b/patch_v2/reader_app/app/shelf_model.c index fa39cef..93b3ab7 100644 --- a/patch_v2/reader_app/app/shelf_model.c +++ b/patch_v2/reader_app/app/shelf_model.c @@ -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++; } diff --git a/patch_v2/reader_app/app/shelf_model.h b/patch_v2/reader_app/app/shelf_model.h index 403f059..0d8bf35 100644 --- a/patch_v2/reader_app/app/shelf_model.h +++ b/patch_v2/reader_app/app/shelf_model.h @@ -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 */ diff --git a/patch_v2/reader_app/reader_internal.h b/patch_v2/reader_app/reader_internal.h index 69fef53..0e98479 100644 --- a/patch_v2/reader_app/reader_internal.h +++ b/patch_v2/reader_app/reader_internal.h @@ -21,6 +21,7 @@ void shelf_on_swipe(int dir, int dx, int dy); void shelf_redraw(void); void shelf_set_view(int grid); /* 1=grid 0=list;读 reader.conf ui.shelf_layout(A) */ void shelf_set_group(int folder); /* 1=folder 0=format;写 ui.shelf_group(M27) */ +void shelf_on_tick(uint64_t now); /* 提示条 6s 到期消失(书架前台时由 app.c 调) */ /* ---- 阅读页(app/reader.c)---- */ meow_view *reader_view_create(void); diff --git a/patch_v2/reader_app/test/shelf_group_smoke.c b/patch_v2/reader_app/test/shelf_group_smoke.c index bd99674..caf1444 100644 --- a/patch_v2/reader_app/test/shelf_group_smoke.c +++ b/patch_v2/reader_app/test/shelf_group_smoke.c @@ -8,7 +8,8 @@ * G4 空目录不产生段头;depth 3/4 收录,depth > SHELF_SCAN_MAX_DEPTH 计数且不出书 * G5 Books/ 根下的书归「未分类」;字面名为「未分类」的目录与根组是两个不同键 * G6 UTF-8 安全截断(中文目录名不切出半个字符) - * G7 超上限不静默:truncated=1;行数上限(段头 ≤ 书数 ⇒ 2N 必够) + * G7 超上限不静默:truncated=1(含「cap 卡在目录边界」与「恰好装满不误报」两种); + * 行数上限(段头 ≤ 书数 ⇒ 2N 必够) * G8 进度不受分组影响:path 不变、rdprog_load 前后同值、ts 两种模式一致 */ #include "shelf_model.h" @@ -165,11 +166,17 @@ static void test_scan_and_modes(const char *root, const char *tmproot) CHECK(strcmp(b1->path, want) == 0, "path 仍是完整绝对路径"); } - /* 副题:第一级之后的子路径(分类/作品)必须可见 */ - CHECK(bdp && strstr(bdp->sub, "01、玄幻/01、斗破") != NULL, "副题含 01、玄幻/01、斗破"); - CHECK(b5 && strstr(b5->sub, "sub") != NULL, "副题含 sub"); - CHECK(b3 && strncmp(b3->sub, "TXT", 3) == 0, "一级目录书的副题无多余子路径"); - CHECK(b4 && strncmp(b4->sub, "TXT", 3) == 0, "根下书副题无多余子路径"); + /* 副题:format 模式必须与 base 逐字一致;子路径只在 folder 副题里出现 */ + CHECK(bdp && strcmp(bdp->sub, "TXT · 5B") == 0 && strstr(bdp->sub, "/") == NULL, + "format 副题与 base 逐字一致(不含子路径)"); + CHECK(bdp && strstr(bdp->sub_folder, "01、玄幻/01、斗破") != NULL, + "folder 副题含 01、玄幻/01、斗破"); + CHECK(b5 && strcmp(b5->sub, "MOBI · 4B") == 0, "MOBI 副题与 base 逐字一致"); + CHECK(b5 && strstr(b5->sub_folder, "sub") != NULL, "folder 副题含 sub"); + CHECK(b3 && strcmp(b3->sub, "TXT · 5B") == 0 && strcmp(b3->sub_folder, b3->sub) == 0, + "一级目录书两种副题相同(无多余子路径)"); + CHECK(b4 && strcmp(b4->sub, "TXT · 4B") == 0 && strcmp(b4->sub_folder, b4->sub) == 0, + "根下书两种副题相同"); /* G2 format 模式(改动前行为):格式段头 + 最近阅读优先 + 全局书名排序 */ shelf_row rows[128]; @@ -208,6 +215,32 @@ static void test_scan_and_modes(const char *root, const char *tmproot) int ns = shelf_scan_books(root, small, 2, ts_of, &rep2); CHECK(ns == 2 && rep2.truncated == 1, "超上限时 truncated=1(不静默)"); + /* P2-1:cap 正好卡在目录边界(A/d1 填满,A/d2 被丢)也必须置 truncated */ + char rootc[400]; + snprintf(rootc, sizeof rootc, "%s/capbound", tmproot); + snprintf(p, sizeof p, "%s/A/d1", rootc); mkdirs(p); + snprintf(p, sizeof p, "%s/A/d2", rootc); mkdirs(p); + snprintf(p, sizeof p, "%s/A/d1/0.txt", rootc); write_file(p, "0"); + snprintf(p, sizeof p, "%s/A/d1/1.txt", rootc); write_file(p, "1"); + snprintf(p, sizeof p, "%s/A/d2/0.txt", rootc); write_file(p, "0"); + snprintf(p, sizeof p, "%s/A/d2/1.txt", rootc); write_file(p, "1"); + shelf_book cb[2]; + shelf_scan_report rep6; + int ncb = shelf_scan_books(rootc, cb, 2, NULL, &rep6); + CHECK(ncb == 2 && rep6.truncated == 1, "cap 卡在目录边界时 truncated=1(不静默)"); + + /* 反向:恰好装满且后面没有书(只剩空目录)不能误报 truncated */ + char rootd[400]; + snprintf(rootd, sizeof rootd, "%s/capok", tmproot); + snprintf(p, sizeof p, "%s/A", rootd); mkdirs(p); + snprintf(p, sizeof p, "%s/A/0.txt", rootd); write_file(p, "0"); + snprintf(p, sizeof p, "%s/A/1.txt", rootd); write_file(p, "1"); + snprintf(p, sizeof p, "%s/Empty", rootd); mkdirs(p); + shelf_book cb2[2]; + shelf_scan_report rep7; + int ncb2 = shelf_scan_books(rootd, cb2, 2, NULL, &rep7); + CHECK(ncb2 == 2 && rep7.truncated == 0, "恰好装满且后续为空目录 → 不误报 truncated"); + /* 不存在的根:root_missing=1 */ shelf_scan_report rep3; char nope[400]; snprintf(nope, sizeof nope, "%s/no_such_dir", tmproot); @@ -228,10 +261,12 @@ static void test_scan_and_modes(const char *root, const char *tmproot) dump_rows(rows, nd2, mb2, got, sizeof got); CHECK(strcmp(got, "S:a|B:1(a/b)|B:2(a/c)|") == 0, "a/b 与 a/c 同组 a(一个段头)"); CHECK(nd2 == 3, "a/b + a/c = 1 段头 + 2 书"); - CHECK(find_book(mb2, nm2, "1") && strstr(find_book(mb2, nm2, "1")->sub, "b") != NULL, - "a/b 的副题含 b"); - CHECK(find_book(mb2, nm2, "2") && strstr(find_book(mb2, nm2, "2")->sub, "c") != NULL, - "a/c 的副题含 c"); + CHECK(find_book(mb2, nm2, "1") && strstr(find_book(mb2, nm2, "1")->sub_folder, "b · TXT") != NULL, + "a/b 的 folder 副题含 b"); + CHECK(find_book(mb2, nm2, "2") && strstr(find_book(mb2, nm2, "2")->sub_folder, "c · TXT") != NULL, + "a/c 的 folder 副题含 c"); + CHECK(find_book(mb2, nm2, "1") && strcmp(find_book(mb2, nm2, "1")->sub, "TXT · 1B") == 0, + "a/b 的 format 副题仍与 base 一致"); /* G5.2 字面名「未分类」的目录与根组是两个独立键(不按显示名合并) */ char root2[400], mini[420]; diff --git a/patch_v2/reader_app/test/shelf_scan_bench.c b/patch_v2/reader_app/test/shelf_scan_bench.c index 9bf0e64..d6f5c19 100644 --- a/patch_v2/reader_app/test/shelf_scan_bench.c +++ b/patch_v2/reader_app/test/shelf_scan_bench.c @@ -4,7 +4,14 @@ * progress = 每本书调 rdprog_load(生产路径,含 rdprog_dir 的 mkdir 循环 + fopen); * noop = 不读进度(基线,只测 readdir/stat/快照)。 * 进度目录由 MEOW_DATA_DIR 决定(同 App:剥 /data → .../progress);未设则 /tmp/m27bench_data。 - * 输出每轮 scan / build_rows(format+folder) 毫秒,末行 RESULT 汇总 min/avg。 + * 输出每轮 scan / build_rows(format+folder) 微秒,末行 RESULT 汇总 min/avg/depth_skipped。 + * + * ⚠ 计时精度:本工具用 /proc/uptime(整数、10ms 分辨率),因为 zig mipsel musl 的 + * time64/浮点 ABI 在真机上不可用(见 patch_v2/sdk/CLOCK_ABI.md)。因此: + * - 真机:适合(扫描 595 本几十 ms 量级); + * - 宿主:只能看到 0us / 10ms 的粗值,不可用于亚毫秒结论;宿主请改用 clock_gettime + * 另写临时程序,或直接读函数的返回值。(M27 r1 报文里的 0.4ms/1.3ms 是切换前用 + * clock_gettime+double 的宿主临时版本测的,不是本工具输出的。) */ #include "shelf_model.h" #include "progress.h"