Files
MeowOS-SDK/tuku/ui/settings/settings.c
T
AskaEth cca3906c80 图库: M5 设置页(阅读方向/PIN/主题状态)与启动 PIN 锁屏;阅读方向持久化(评审 P1)
logic/pin(纯 C):加盐迭代哈希(FNV-1a ×1000 轮 + 每次设置新随机盐,16 位 hex)经
meow_config_* 持久化 pin.enabled/pin.hash/pin.salt;提供 set/verify/change/disable 与
4-8 位纯数字校验;enabled=1 但盐/哈希残缺时 fail-closed;落盘半途失败会还原旧盐,不留
「新盐+旧哈希」的双失效状态。

ui/settings(ACT_SETTINGS):MD3 分组表单(meow_listview 容器圆角卡 + 段头标签)——
阅读方向 RTL/LTR 走 mr_reader_set_direction;启动 PIN 开关与修改(开启/修改均两遍
MEOW_IME_NUM 确认);版本与主题状态行(跟随系统 + 当前明暗,随 app.c 主题 listener
整帧重绘)。控件一律在 on_enter 建(栈顶=ACT_SETTINGS),避免 §6.1 owner 绑到
ACT_LIBRARY 的跨页命中泄漏。

ui/lock(ACT_LOCK):lock_view_should_show() 读 logic/pin;启用时启动先入锁屏,验证前
不绘制任何书库内容;「输入 PIN」按钮弹系统数字键盘(MEOW_IME_NUM),错 PIN 就地提示
重试(无惩罚性锁定),正确则 page_pop 回书库;on_event 消费 SWIPE——左缘返回不能绕过
PIN 或退出 App(meow_view_dispatch_event 先于 app.c 的 on_swipe)。

logic/reader(评审 P1 修复,最小持久化):mr_reader_set_direction 成功切换后 best-effort
写 meow_config("reader.dir","rtl"/"ltr"),写失败只打 WARN 不阻断本次切换;
mr_reader_get_direction 首次调用惰性读一次配置恢复(无配置/非法值 → 默认 RTL),之后
纯内存快路径。宿主测试经 -DMR_READER_TEST_HOOKS 的 mr_reader_test_reset() 模拟重启。

测试/构建:test_pin(85 项,PIN 全分支,含单键半途落盘失败);test_settings_reader_persist
(19 项:默认/落盘/重启恢复/坏值/非法值/只读一次/写失败只告警);可选宿主集成
test_settings_lock_host(真实 SDK view/activity/widget + IME 注入,端到端验证错/对 PIN
解锁与设置页两遍开启;SDK 无源码时 SKIP);run_tests.sh 保持 46/46 并纳入上述测试
(reader.c 的配置/日志依赖由 test/settings_reader_stub.c 存根),build_tuku.sh 加入
logic/pin.c。交叉构建静态 MIPS ELF(zig-mipsel)零警告。

安全口径:PIN 防随手翻看,不是安全边界——TF 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
2026-11-04 21:25:17 +08:00

450 lines
17 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.
/* 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 <stdio.h>
#include <string.h>
#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);
}