Files
MeowOS-For-R3P2/patch_v2/reader_app/app/settings.c
T
AskaEth 409fd1edf7 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 时跳过执行而非失败
2026-11-09 21:55:33 +08:00

298 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* app/settings.c — 全局设置页(C,书架顶栏「设置」入口 → 独立页 ACT_SETTINGS)。
*
* 配置项(MVP,见 READER_UI_DESIGN §4.3):默认字号/默认行距/默认背景(黑白米黄)/主题(亮暗跟随
* 全局)/书架布局(list-grid)/书架分组(格式-目录)/页头条/页底条。全部存 App 私有 reader.conf(rdconf),不进系统
* settings.conf。UI:meow_topappbar(返回+标题) + meow_listview 行(每行 label + 当前值尾部箭头,
* 点行开 meow_dialog(SELECT) 单选;值即改即存)。
*/
#include "../reader_internal.h"
#include "progress.h"
#include "reader_conf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SET_X 0
#define SET_W 480
#define SET_APPBAR_H 56
static meow_widget *g_appbar;
static meow_widget *g_list;
static int g_active;
/* 行项枚举(顺序 = 列表顺序)。ITEM_BASE = 固定的全局项数;重解析本书章节仅在
* 已打开一本「支持章节解析」(TXT+seg 模式)的书时追加(见 settings_on_enter)。 */
enum {
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 /* 8:不含重解析的固定项数 */
static int g_nitems = ITEM_BASE;
static const char *const g_labels[ITEM_COUNT] = {
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "书架分组",
"页头条", "页底条", "重解析本书章节"
};
static void settings_redraw(void);
static const char *bg_label(const rdconf *c)
{
if (!c) return "黑";
if (!strcmp(c->bg, "white")) return "白";
if (!strcmp(c->bg, "sepia")) return "米黄";
if (!strcmp(c->bg, "auto")) return "自动";
return "黑";
}
static const char *theme_label(const rdconf *c)
{
if (!c) return "跟随系统";
if (!strcmp(c->theme, "light")) return "日";
if (!strcmp(c->theme, "dark")) return "夜";
return "跟随系统";
}
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)
{
const rdconf *c = rdconf_get();
if (!c) { snprintf(out, (size_t)outsz, "-"); return; }
switch (item) {
case ITEM_FS: snprintf(out, (size_t)outsz, "%d", c->fs); break;
case ITEM_LS: snprintf(out, (size_t)outsz, "%d", c->ls); break;
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;
default: snprintf(out, (size_t)outsz, "-"); break;
}
}
/* ---- listview 行源(src2:每次绘制读最新 rdconf 值) ---- */
static int settings_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_nitems || idx >= ITEM_COUNT) return 0;
static char valbuf[48];
item_value(idx, valbuf, sizeof valbuf);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_labels[idx];
out->subtitle = valbuf;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = (idx == ITEM_REPARSE) ? reader_has_chapter_book() : 1;
return 1;
}
/* 由当前值求 SELECT 对话框的选中下标。 */
static int idx_for(int item, const char *const *opts, int n, const rdconf *c)
{
if (item == ITEM_FS) { for (int i = 0; i < n; i++) if (atoi(opts[i]) == c->fs) return i; return 0; }
if (item == ITEM_LS) { for (int i = 0; i < n; i++) if (atoi(opts[i]) == c->ls) return i; return 0; }
if (item == ITEM_BG) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], bg_label(c))) return i;
return 0;
}
if (item == ITEM_THEME) {
for (int i = 0; i < n; i++) if (!strcmp(opts[i], theme_label(c))) return i;
return 0;
}
if (item == ITEM_SHELF) {
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;
}
/* 选完整数数组项(字号/行距);写回 rdconf。 */
static void apply_fs_ls(int item, const char *sel)
{
rdconf *c = rdconf_get();
if (!c) return;
int v = atoi(sel);
if (item == ITEM_FS) { if (v >= 14 && v <= 40) c->fs = v; }
else if (item == ITEM_LS) { if (v >= 100 && v <= 200) c->ls = v; }
rdconf_persist();
}
static void apply_choice(int item, int sel)
{
rdconf *c = rdconf_get();
if (!c) return;
switch (item) {
case ITEM_BG:
if (sel < 0) sel = 0;
if (sel > 3) sel = 3;
if (sel == 0) strcpy(c->bg, "black");
else if (sel == 1) strcpy(c->bg, "white");
else if (sel == 2) strcpy(c->bg, "sepia");
else strcpy(c->bg, "auto");
break;
case ITEM_THEME:
if (sel < 0) sel = 0;
if (sel > 2) sel = 2;
if (sel == 0) strcpy(c->theme, "auto");
else if (sel == 1) strcpy(c->theme, "light");
else strcpy(c->theme, "dark");
break;
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;
}
rdconf_persist();
}
/* 打开一个 SELECT 对话框并应用。 */
static void settings_open_choice(int item)
{
if (item == ITEM_REPARSE) return; /* 重解析走 settings_reparse(非 SELECT) */
const rdconf *c = rdconf_get();
if (!c) return;
const char *title = g_labels[item];
meow_dialog *d = meow_dialog_create(MEOW_DLG_SELECT, title);
if (!d) return;
const char *opts[16]; int n = 0; int cur = 0;
static char numopts[16][8];
if (item == ITEM_FS) {
for (int v = 14; v <= 40; v += 2) { snprintf(numopts[n], sizeof numopts[n], "%d", v); opts[n] = numopts[n]; n++; }
} else if (item == ITEM_LS) {
for (int v = 100; v <= 200; v += 10) { snprintf(numopts[n], sizeof numopts[n], "%d", v); opts[n] = numopts[n]; n++; }
} else if (item == ITEM_BG) {
static const char *b[4] = { "黑", "白", "米黄", "自动" };
for (int i = 0; i < 4; i++) opts[n++] = b[i];
} else if (item == ITEM_THEME) {
static const char *t[3] = { "跟随系统", "日", "夜" };
for (int i = 0; i < 3; i++) opts[n++] = t[i];
} 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];
}
cur = idx_for(item, opts, n, c);
meow_dialog_set_items(d, opts, n, cur);
meow_dialog_show(d);
int res = meow_dialog_result(d);
meow_dialog_free(d);
if (res < 0 || res >= n) return; /* 取消 */
if (item == ITEM_FS || item == ITEM_LS) apply_fs_ls(item, opts[res]);
else apply_choice(item, res);
settings_redraw();
}
static void settings_redraw(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_clear(th->bg);
if (g_appbar) meow_widget_draw(g_appbar);
if (g_list) meow_widget_draw(g_list);
meow_ui_flush();
}
/* R3.4 «重解析本书章节»:确认 → 清缓存+重扫 → 结果对话框(成功/失败/进行中=确认弹窗)。 */
static void settings_reparse(void)
{
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "重解析本书章节?\n将清除章节缓存并重新扫描全书。");
if (!d) return;
meow_dialog_show(d);
int ok = meow_dialog_result(d);
meow_dialog_free(d);
if (ok != 1) { settings_redraw(); return; } /* 取消 */
int rc = reader_reparse_chapters(); /* 阻塞:重扫全书段 */
char msg[120];
if (rc >= 0) snprintf(msg, sizeof msg, "重解析完成:共识别 %d 章(已保存缓存)", rc);
else if (rc == -2) snprintf(msg, sizeof msg, "重解析失败:扫描出错,已回退为无章节(未保存)");
else snprintf(msg, sizeof msg, "该书不支持章节解析");
meow_dialog *r = meow_dialog_create(MEOW_DLG_INFO, msg);
if (r) { meow_dialog_show(r); meow_dialog_result(r); meow_dialog_free(r); }
settings_redraw();
}
/* view 生命周期 */
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void settings_on_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
int sbh = meow_ui_statusbar_height();
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
g_appbar = meow_topappbar_create(0, sbh, 480, SET_APPBAR_H, "阅读设置");
if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
int ly = sbh + SET_APPBAR_H + 8;
/* 仅当打开的书「支持章节解析」(TXT+seg)才显示「重解析本书章节」项 */
g_nitems = ITEM_BASE + (reader_has_chapter_book() ? 1 : 0);
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, g_nitems,
settings_src, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) {
meow_listview_set_container(g_list, 1);
meow_listview_set_item_tier(g_list, 2);
}
settings_redraw();
}
static void settings_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
static void settings_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
}
meow_view *settings_view_create(void)
{
static meow_view_hooks hooks = { settings_on_create, settings_on_enter, settings_on_leave,
settings_on_destroy, NULL, NULL, NULL };
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &hooks, NULL);
}
int settings_is_active(void) { return g_active; }
void settings_reload(void) { if (g_active) settings_redraw(); }
int settings_on_tap(int x, int y)
{
/* 顶栏返回 */
if (g_appbar) {
int bi = meow_topappbar_hit(g_appbar, x, y);
if (bi == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
if (bi >= 0) return 1;
}
if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0 && idx < g_nitems && idx < ITEM_COUNT) {
if (idx == ITEM_REPARSE) { settings_reparse(); return 1; }
settings_open_choice(idx);
return 1;
}
if (idx >= 0) { settings_redraw(); return 1; }
}
return 0;
}