feat(reader): M27 书架按目录分组(ui.shelf_group=folder|format,默认 format 不变)

- 新增 shelf_model.c/.h:扫描/行序列从 shelf.c 拆出(纯 C,宿主可测);shelf_book 加 group[96](相对 Books/ 的目录路径,根下空串),path 语义/内容完全不变(进度键仍为 rdprog_hash(path))
- folder 模式:目录段头(根下=未分类),未分类置顶、其余按目录名排序;组内沿用最近阅读优先/其次书名;空目录不产生段头
- format 模式逐行保持旧行为;新增 reader.conf ui.shelf_group,缺键默认 format
- 上限 256→2048(SHELF_MAX_ROWS=2N=4096,段头≤书数),超限置可读提示,不再静默截断
- 顶栏加「目录/格式」切换,设置页加「书架分组」;UTF-8 安全截断长中文目录名
- 单测 test/shelf_group_smoke.c(38 断言)+ 计时 test/shelf_scan_bench.c
- build_reader.sh:链 meow_ui.a 解析冒烟 stbi_*;无 qemu 时跳过执行而非失败
This commit is contained in:
AskaEth committed 2026-11-09 21:55:33 +08:00
1 parent db672cbe8f
commit 409fd1edf7
11 files changed
+800 -129

No files matched your search

+21 -4
View File
@@ -1,7 +1,7 @@
/* app/settings.c — 全局设置页(C,书架顶栏「设置」入口 → 独立页 ACT_SETTINGS)。
*
* 配置项(MVP,见 READER_UI_DESIGN §4.3):默认字号/默认行距/默认背景(黑白米黄)/主题(亮暗跟随
* 全局)/书架布局(list-grid)/页头条/页底条。全部存 App 私有 reader.conf(rdconf),不进系统
* 全局)/书架布局(list-grid)/书架分组(格式-目录)/页头条/页底条。全部存 App 私有 reader.conf(rdconf),不进系统
* settings.conf。UI:meow_topappbar(返回+标题) + meow_listview 行(每行 label + 当前值尾部箭头,
* 点行开 meow_dialog(SELECT) 单选;值即改即存)。
*/
@@ -23,13 +23,14 @@ static int g_active;
/* 行项枚举(顺序 = 列表顺序)。ITEM_BASE = 固定的全局项数;重解析本书章节仅在
* 已打开一本「支持章节解析」(TXT+seg 模式)的书时追加(见 settings_on_enter)。 */
enum {
ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR,
ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_GROUP, ITEM_HDR, ITEM_FTR,
ITEM_REPARSE, ITEM_COUNT
};
#define ITEM_BASE ITEM_REPARSE /* 7:不含重解析的固定项数 */
#define ITEM_BASE ITEM_REPARSE /* 8:不含重解析的固定项数 */
static int g_nitems = ITEM_BASE;
static const char *const g_labels[ITEM_COUNT] = {
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条", "重解析本书章节"
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "书架分组",
"页头条", "页底条", "重解析本书章节"
};
static void settings_redraw(void);
@@ -53,6 +54,11 @@ static const char *shelf_label(const rdconf *c)
if (!c) return "列表";
return (!strcmp(c->shelf, "grid")) ? "宫格" : "列表";
}
static const char *group_label(const rdconf *c)
{
if (!c) return "格式";
return (!strcmp(c->shelf_group, "folder")) ? "目录" : "格式";
}
/* 当前行值(subtitle)。 */
static void item_value(int item, char *out, int outsz)
@@ -65,6 +71,7 @@ static void item_value(int item, char *out, int outsz)
case ITEM_BG: snprintf(out, (size_t)outsz, "%s", bg_label(c)); break;
case ITEM_THEME: snprintf(out, (size_t)outsz, "%s", theme_label(c)); break;
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
case ITEM_GROUP: snprintf(out, (size_t)outsz, "%s", group_label(c)); break;
case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); break;
case ITEM_FTR: snprintf(out, (size_t)outsz, "%s", c->footer ? "显示" : "隐藏"); break;
case ITEM_REPARSE: snprintf(out, (size_t)outsz, "%s", "点按重新扫描章节"); break;
@@ -105,6 +112,10 @@ static int idx_for(int item, const char *const *opts, int n, const rdconf *c)
for (int i = 0; i < n; i++) if (!strcmp(opts[i], shelf_label(c))) return i;
return 0;
}
if (item == ITEM_GROUP) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], group_label(c))) return i;
return 0;
}
if (item == ITEM_HDR) return c->header ? 0 : 1;
if (item == ITEM_FTR) return c->footer ? 0 : 1;
return 0;
@@ -144,6 +155,9 @@ static void apply_choice(int item, int sel)
case ITEM_SHELF:
shelf_set_view(sel ? 1 : 0); /* 写 rdconf + 重建书架 */
return;
case ITEM_GROUP:
shelf_set_group(sel ? 1 : 0); /* 写 ui.shelf_group + 重建书架(不改 path) */
return;
case ITEM_HDR: c->header = sel ? 0 : 1; break; /* 0 显示 1 隐藏 */
case ITEM_FTR: c->footer = sel ? 0 : 1; break;
default: return;
@@ -175,6 +189,9 @@ static void settings_open_choice(int item)
} else if (item == ITEM_SHELF) {
static const char *s[2] = { "列表", "宫格" };
for (int i = 0; i < 2; i++) opts[n++] = s[i];
} else if (item == ITEM_GROUP) {
static const char *g[2] = { "格式", "目录" };
for (int i = 0; i < 2; i++) opts[n++] = g[i];
} else if (item == ITEM_HDR || item == ITEM_FTR) {
static const char *h[2] = { "显示", "隐藏" };
for (int i = 0; i < 2; i++) opts[n++] = h[i];