/* ui/settings/settings.c — 设置页(ACT_SETTINGS):MD3 分组表单。 * * 内容(用户原话「允许用户在APP设置中设置APP启动 PIN」+「同步系统主题状态」): * 阅读:阅读方向 RTL/LTR —— 直接读写 M3 的 mr_reader_get/set_direction(不另立状态); * 安全:启动 PIN 开关(开启需两遍 MEOW_IME_NUM 输入确认)、修改 PIN; * 关于:版本号、主题状态(跟随系统 + 当前明暗,随系统实时同步)。 * * 观感/主题:SDK 的 meow_listview 容器模式(MD3 surfaceContainer 圆角卡 + 行间空隙) * + 段头标签;开关/箭头由 SDK renderer 用主题语义色绘制;所有颜色取 meow_theme_get(), * 不缓存颜色——app.c 的主题 listener 整帧 invalidate,本页 on_draw/src 回调下一帧即用新色。 * * 控件归属(§6.1,M3 桩注释①):本页非根页,控件**必须**在 on_enter 里建(此时 * activity 栈顶=ACT_SETTINGS),否则 owner 会绑到 ACT_LIBRARY 造成跨页命中。 * * 返回:左缘返回由 app.c 的 on_swipe 统一 page_pop(本页 on_event 不消费 SWIPE)。 * PIN 输入:SDK 系统数字键盘(MEOW_IME_NUM),不自绘键盘;文本经 on_change 实时收集, * 「非空 + 关闭键盘 = 校验、空 = 取消」(SDK 阻塞 IME 的返回值不能当确认判据,见 * settings_read_pin 注释);键盘打开期间步骤提示画在键盘面板上方(置顶 widget)。 */ #include "tuku_internal.h" #include "logic/pin.h" #include #include #define ST_TOP_H 56 #define ST_SEC_H 34 #define ST_ROW_H 64 #define ST_GAP 12 /* meow_view on_event 的事件码:公共伞头未导出(定义在 sdk/meow_ui_internal.h: * MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定)。此处按契约镜像,避免 App include SDK 内部头。 * (已在交付报告里作为 SDK 缺口提出:建议公共头导出事件枚举。) */ #define MR_VEV_TAP 2 /* PIN 行下标(g_lv_pin) */ #define ST_PIN_ROW_SWITCH 0 #define ST_PIN_ROW_CHANGE 1 static mr_page_ctrl g_ctrl; static meow_widget *g_tb; static meow_widget *g_lv_dir; /* 阅读:1 行 */ static meow_widget *g_lv_pin; /* 安全:2 行 */ static meow_widget *g_lv_about; /* 关于:2 行 */ static meow_widget *g_overlay; /* 置顶提示条(仅拨号键盘期间显示步骤文案) */ static int g_yl_dir, g_yl_pin, g_yl_about; /* 段头标签 y */ static int g_note_y; static int g_ov_y, g_ov_h; /* 提示条几何(App 自记;widget 句柄不透明) */ static char g_note[96]; static int g_note_err; static int g_pin_on; /* 缓存的 PIN 启用态(避免每帧读 3 个小配置文件的 TF IO) */ static char g_theme_line[64]; static char g_prompt[64]; /* 非空=正在输入 PIN(键盘打开期间顶部提示条) */ static char g_ime_pin[MR_PIN_MAX_LEN + 12]; /* ============================================================ */ /* 文案/状态 */ /* ============================================================ */ static int theme_is_dark(const meow_theme *th) { int r = (int)((th->surface >> 16) & 0xFF); int g = (int)((th->surface >> 8) & 0xFF); int b = (int)(th->surface & 0xFF); return (r * 299 + g * 587 + b * 114) / 1000 < 128; } static void theme_line_update(void) { const meow_theme *th = meow_theme_get(); meow_theme_mode m = meow_theme_get_mode(); const char *mode = (m == MEOW_THEME_AUTO) ? "跟随系统" : (m == MEOW_THEME_DARK) ? "固定深色" : "固定浅色"; snprintf(g_theme_line, sizeof g_theme_line, "%s · 当前%s", mode, theme_is_dark(th) ? "深色" : "浅色"); } static void settings_note(const char *msg, int is_err) { snprintf(g_note, sizeof g_note, "%s", msg ? msg : ""); g_note_err = is_err; } static void settings_repaint(void) { meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); } /* ============================================================ */ /* PIN 输入(系统数字键盘) */ /* ============================================================ */ static void ime_pin_change(const char *text, void *ctx) { (void)ctx; snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : ""); } /* 阻塞弹出 MEOW_IME_NUM 收集 4-8 位数字。返回 1=收到非空文本;0=空(视为取消)。 * 说明(SDK 现状,如实记录):阻塞 IME 只以「关闭键盘」结束;数字键盘的「确定」只置内部 * submit 标记而不关闭,随后「取消/点背景」关闭时该标记又被清 0(sdk/meow_ui_ime.c 的 * K_ESC/背景路径),故 meow_ui_text_input 的返回值不可作为「确认」判据。本页改用 * on_change 实时收集文本:非空 + 关闭键盘 = 校验;空 = 取消(提示条照此告知用户)。 * 键盘面板覆盖屏底(y≥420),步骤提示画在其上方,否则打字时看不见当前步骤。 */ static int settings_read_pin(const char *prompt, char *out, int outsz) { meow_ime_ctx c; memset(&c, 0, sizeof c); c.mode = MEOW_IME_NUM; c.maxlen = (int)sizeof g_ime_pin; c.on_change = ime_pin_change; c.ctx = NULL; g_ime_pin[0] = '\0'; settings_note("", 0); snprintf(g_prompt, sizeof g_prompt, "%s", prompt); settings_repaint(); /* 先画提示条(键盘区背景快照从当前帧取) */ (void)meow_ui_text_input(&c); g_prompt[0] = '\0'; snprintf(out, (size_t)outsz, "%s", g_ime_pin); return out[0] != '\0'; } /* 两遍输入(开启/修改共用):1=一致且合法(写入 out);0=取消/不一致/非法(已置 note)。 */ static int settings_pin_two_pass(const char *prompt, char *out, int outsz) { char a[32] = "", b[32] = ""; if (!settings_read_pin(prompt, a, (int)sizeof a)) { settings_note("已取消", 0); return 0; } if (!mr_pin_valid(a)) { settings_note("PIN 需为 4-8 位数字,未保存", 1); return 0; } if (!settings_read_pin("再次输入以确认", b, (int)sizeof b)) { settings_note("已取消", 0); return 0; } if (strcmp(a, b) != 0) { settings_note("两次输入不一致,未保存", 1); return 0; } snprintf(out, (size_t)outsz, "%s", a); return 1; } static void settings_toggle_pin(void) { if (!g_pin_on) { char pin[32] = ""; if (!settings_pin_two_pass("设置启动 PIN(4-8 位数字)", pin, (int)sizeof pin)) return; int r = mr_pin_set(pin); if (r == MR_PIN_OK) settings_note("启动 PIN 已开启 · 下次启动生效", 0); else settings_note("保存失败:数据目录不可写", 1); g_pin_on = mr_pin_enabled(); return; } int r = mr_pin_disable(); if (r == MR_PIN_OK) settings_note("启动 PIN 已关闭", 0); else settings_note("关闭失败:配置不可写", 1); g_pin_on = mr_pin_enabled(); } static void settings_change_pin(void) { if (!g_pin_on) { settings_note("请先开启启动 PIN", 0); return; } char pin[32] = ""; if (!settings_pin_two_pass("设置新 PIN(4-8 位数字)", pin, (int)sizeof pin)) return; int r = mr_pin_set(pin); /* 重设即换新盐;旧 PIN 失效 */ if (r == MR_PIN_OK) settings_note("PIN 已更新 · 下次启动生效", 0); else settings_note("保存失败:数据目录不可写", 1); g_pin_on = mr_pin_enabled(); } /* ============================================================ */ /* 行数据源(src2:每帧重取 → 开关/副标题随状态即时刷新) */ /* ============================================================ */ static int src_dir(int idx, meow_list_item *it, void *ctx) { (void)ctx; if (idx != 0) return 0; memset(it, 0, sizeof *it); int rtl = mr_reader_get_direction() == MR_DIR_RTL; it->title = "阅读方向"; it->subtitle = rtl ? "右到左 · 日式漫画(RTL)" : "左到右 · 常规漫画(LTR)"; it->right = MEOW_LIST_RIGHT_SWITCH; it->on = rtl; it->enabled = 1; return 1; } static int src_pin(int idx, meow_list_item *it, void *ctx) { (void)ctx; memset(it, 0, sizeof *it); int on = g_pin_on; if (idx == ST_PIN_ROW_SWITCH) { it->title = "启动 PIN"; it->subtitle = on ? "已启用 · 启动时先输入 PIN" : "未设置 · 启动直接进入书库"; it->right = MEOW_LIST_RIGHT_SWITCH; it->on = on; it->enabled = 1; return 1; } if (idx == ST_PIN_ROW_CHANGE) { it->title = "修改 PIN"; it->subtitle = on ? "重新设置 4-8 位数字" : "开启后可修改"; it->right = MEOW_LIST_RIGHT_ARROW; it->enabled = on; /* 未启用时变淡且不响应命中 */ return 1; } return 0; } static int src_about(int idx, meow_list_item *it, void *ctx) { (void)ctx; memset(it, 0, sizeof *it); if (idx == 0) { it->title = "版本"; it->subtitle = "0.1 · os.meow.picbrowser"; it->enabled = 1; return 1; } if (idx == 1) { theme_line_update(); it->title = "主题"; it->subtitle = g_theme_line; it->enabled = 1; return 1; } return 0; } /* ============================================================ */ /* 布局 */ /* ============================================================ */ /* 置顶提示条(z=MEOW_Z_TOP):键盘面板从屏底覆盖,步骤文案必须画在其上方; * 仅 g_prompt 非空时绘制,否则透明不挡顶栏。 * 注:meow_widget 对 App 不透明(公共头只给句柄),坐标由本文件自己记录。 */ static void settings_overlay_render(meow_widget *w, const meow_theme *th, void *ctx) { (void)w; (void)ctx; if (!g_prompt[0]) return; int wd = meow_ui_width(); meow_ui_fill_rect(0, g_ov_y, wd, g_ov_h, th->surfaceContainerHighest); meow_ui_text_center(wd / 2, g_ov_y + 28, g_prompt, th->font_title, th->onSurface); meow_ui_text_center(wd / 2, g_ov_y + 76, "输入后关闭键盘即校验 · 空输入=取消", th->font_body, th->onSurfaceVariant); meow_ui_fill_rect(0, g_ov_y + g_ov_h - 1, wd, 1, th->outlineVariant); } static void settings_build(void) { /* 控件先以占位矩形创建,统一由 settings_place() 摆位(避免两处布局数学不一致)。 */ g_tb = meow_topappbar_create(0, 0, 1, 1, "设置"); if (g_tb) meow_topappbar_set_back(g_tb, 1); g_lv_dir = meow_listview_create(0, 0, 1, 1, 1, src_dir, NULL, MEOW_LIST_SEL_NONE); g_lv_pin = meow_listview_create(0, 0, 1, 1, 2, src_pin, NULL, MEOW_LIST_SEL_NONE); g_lv_about = meow_listview_create(0, 0, 1, 1, 2, src_about, NULL, MEOW_LIST_SEL_NONE); /* 列表:MD3 容器卡 + 双行(标题/副标题)+ 右尾件(开关/箭头)。 * 容器模式由 SDK 默认 renderer 绘制(surfaceContainer 圆角卡 + 行间空隙)。 */ meow_widget *lvs[3] = { g_lv_dir, g_lv_pin, g_lv_about }; for (int i = 0; i < 3; i++) { if (!lvs[i]) continue; meow_listview_set_item_h(lvs[i], ST_ROW_H); meow_listview_set_item_renderer(lvs[i], 2, 0, 1); meow_listview_set_container(lvs[i], 1); } g_overlay = meow_widget_custom_create(0, 0, 1, 1, settings_overlay_render, NULL, NULL); if (g_overlay) meow_widget_set_z(g_overlay, MEOW_Z_TOP); } /* 布局参数(窗口可能变化;每次进入重新摆位)。 */ static void settings_place(meow_view *v) { int w = meow_ui_width(); int sb = meow_ui_statusbar_height(); v->x = 0; v->y = 0; v->w = w; v->h = meow_ui_height(); int lx = 8, lw = w - 16; int y = sb + ST_TOP_H + 6; if (g_tb) meow_widget_set_rect(g_tb, 0, sb, w, ST_TOP_H); g_yl_dir = y; y += ST_SEC_H; if (g_lv_dir) meow_widget_set_rect(g_lv_dir, lx, y, lw, ST_ROW_H); y += ST_ROW_H + ST_GAP; g_yl_pin = y; y += ST_SEC_H; if (g_lv_pin) meow_widget_set_rect(g_lv_pin, lx, y, lw, ST_ROW_H * 2); y += ST_ROW_H * 2 + ST_GAP; g_yl_about = y; y += ST_SEC_H; if (g_lv_about) meow_widget_set_rect(g_lv_about, lx, y, lw, ST_ROW_H * 2); y += ST_ROW_H * 2 + 14; g_note_y = y; g_ov_y = sb; g_ov_h = 128; if (g_overlay) meow_widget_set_rect(g_overlay, 0, g_ov_y, w, g_ov_h); } static void settings_section_label(int y, const char *text, const meow_theme *th) { meow_ui_text(24, y + 6, text, th->font_body, th->primary); } /* ============================================================ */ /* 绘制 */ /* ============================================================ */ static void settings_draw(meow_view *v, const meow_theme *th, void *ctx) { (void)v; (void)ctx; int w = meow_ui_width(); settings_section_label(g_yl_dir, "阅读", th); settings_section_label(g_yl_pin, "安全", th); settings_section_label(g_yl_about, "关于", th); if (g_note[0]) { meow_ui_text(24, g_note_y, g_note, th->font_body, g_note_err ? th->error : th->primary); } int hy = g_note_y + (g_note[0] ? 30 : 0); meow_ui_text(24, hy, "改动即时保存,下次启动生效", th->font_body, th->onSurfaceVariant); meow_ui_text(24, hy + 28, "PIN 仅防随手查看,非安全边界", th->font_body, th->onSurfaceVariant); meow_ui_fill_rect(0, meow_ui_height() - 1, w, 1, th->outlineVariant); } /* ============================================================ */ /* 事件 */ /* ============================================================ */ static int settings_on_event(meow_view *v, int ev, int a, int b, void *ctx) { (void)v; (void)ctx; if (ev == MR_VEV_TAP) { int x = a, y = b; if (g_tb) { int hit = meow_topappbar_hit(g_tb, x, y); if (hit == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; } if (hit >= 0) return 1; } if (g_lv_dir) { int idx = meow_listview_hit(g_lv_dir, x, y); if (idx == 0) { mr_reader_set_direction(mr_reader_get_direction() == MR_DIR_RTL ? MR_DIR_LTR : MR_DIR_RTL); settings_note(mr_reader_get_direction() == MR_DIR_RTL ? "阅读方向:右到左(RTL)" : "阅读方向:左到右(LTR)", 0); settings_repaint(); return 1; } } if (g_lv_pin) { int idx = meow_listview_hit(g_lv_pin, x, y); if (idx == ST_PIN_ROW_SWITCH) { settings_toggle_pin(); settings_repaint(); return 1; } if (idx == ST_PIN_ROW_CHANGE) { settings_change_pin(); settings_repaint(); return 1; } } if (g_lv_about) { int idx = meow_listview_hit(g_lv_about, x, y); if (idx == 1) { settings_note("主题跟随系统设置,App 内不可改", 0); settings_repaint(); return 1; } if (idx == 0) return 1; /* 版本行:仅展示 */ } return 0; } /* SWIPE 不消费:app.c 对非 library 页的左缘返回统一 page_pop。 */ return 0; } /* ============================================================ */ /* view 生命周期 */ /* ============================================================ */ static void settings_page_enter(meow_view *v, void *ctx) { ((mr_page_ctrl *)ctx)->active = 1; meow_ui_set_fullscreen(0); /* 保留系统状态栏(与书库一致) */ meow_ui_set_page_bg(meow_theme_get()->bg); if (!g_tb) { /* 栈顶此时已是 ACT_SETTINGS(page_push 先 push 再 on_enter)→ 控件 owner 正确。 */ settings_build(); meow_widget *all[5] = { g_tb, g_lv_dir, g_lv_pin, g_lv_about, g_overlay }; for (int i = 0; i < 5; i++) if (all[i]) meow_view_add_widget(v, all[i]); } settings_place(v); g_pin_on = mr_pin_enabled(); /* 进入页时读一次;页内改动由操作函数同步 */ mr_nav_report(); meow_ui_paint_full(); } static void settings_page_leave(meow_view *v, void *ctx) { (void)v; ((mr_page_ctrl *)ctx)->active = 0; } static void settings_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; /* 控件随 meow_view_free 统一释放(v->widgets);这里只清指针,避免悬垂。 */ g_tb = NULL; g_lv_dir = NULL; g_lv_pin = NULL; g_lv_about = NULL; g_overlay = NULL; } meow_view *settings_view_create(void) { static const meow_view_hooks h = { .on_enter = settings_page_enter, .on_leave = settings_page_leave, .on_destroy = settings_page_destroy, .on_draw = settings_draw, .on_event = settings_on_event, }; return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl); }