diff --git a/patch_v2/meow_launcherd/bt_page.c b/patch_v2/meow_launcherd/bt_page.c new file mode 100644 index 0000000..48b2c0f --- /dev/null +++ b/patch_v2/meow_launcherd/bt_page.c @@ -0,0 +1,215 @@ +/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76)。 + * + * 页面(sub_page_t 脚手架 subpage.c 驱动): + * - 顶栏:返回 + 标题「蓝牙」 + 右上开关(network.bluetooth_enabled) + * - 已连接设备:tap → bluetoothctl disconnect(角色经 bt_role_refresh 判定) + * - 已配对设备:tap → bluetoothctl connect(已连者标「已连接」) + * - 附近设备:扫描发现(net_bt_scan,后台有界 bluetoothctl 写 /tmp 缓存,异步不阻塞)—— + * 未配对者 tap → pair;已配对者 tap → connect。实机配对成功率/超时需真机验证。 + * + * 与网络线共用 net.c(net_bt_connected/paired/scan/scan_trigger/hci_powered/bt_role)。 + * 开关写 network.bluetooth_enabled + hciconfig;连接/断开/配对走 bluetoothctl(带超时)。 + */ +#include "meow_launcher_internal.h" + +#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→disconnect) */ +#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→connect) */ +#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→pair/connect) */ + +typedef struct { + meow_net_t conn[S_NET_MAX]; + meow_net_t paired[S_NET_MAX]; + meow_net_t near[S_NET_MAX]; + int conn_n, paired_n, near_n; + int bt_on; + int full; /* 1=本次 rows() 全量重扫(conn/paired popen);0=仅附近(file)+角色(节流) */ + char mode[48]; /* bt_role 中文描述(次行摘要) */ +} bt_ctx_t; + + +/* 设备 MAC(net_bt_* 已填 mac) */ +static int bt_is_conn(const bt_ctx_t *c, const char *mac) +{ + if (!mac[0]) return 0; + for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1; + return 0; +} + +static int bt_is_paired(const bt_ctx_t *c, const char *mac) +{ + if (!mac[0]) return 0; + for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1; + return 0; +} + + +/* 顶栏开关当前值 */ +static int bt_top_sw(void *ctx, int *on) +{ + (void)ctx; + int v = 0; + if (meow_settings_get_bool("network.bluetooth_enabled", &v) != 0) v = 0; + *on = v; + return 1; +} + +/* 顶栏开关切换:写设置 + hciconfig up/down + 触发扫描 + 同步状态栏 */ +static void bt_top_sw_set(void *ctx, int on) +{ + (void)ctx; + meow_settings_set_bool("network.bluetooth_enabled", on); + s_net_toggle_bt(on); + g_sys.bt_on = on; + bt_role_refresh(1); + paint_sb_right(); + meow_settings_save(); + if (on) net_bt_scan_trigger(); +} + + +/* 填行:刷新数据后构建分组列表。 + * conn/paired 走 bluetoothctl(慢);仅全量(full)时重扫,避开子页 live tick 反复 spawn。 */ +static int bt_rows(void *ctx, sub_row_t *rbuf, int max) +{ + bt_ctx_t *c = ctx; + int full = c->full; c->full = 0; /* 消费一次全量标记 */ + c->bt_on = hci_powered(); + if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired */ + c->conn_n = net_bt_connected(c->conn, S_NET_MAX); + c->paired_n = net_bt_paired(c->paired, S_NET_MAX); + if (c->conn_n < 0) c->conn_n = 0; + if (c->paired_n < 0) c->paired_n = 0; + } + c->near_n = net_bt_scan(c->near, S_NET_MAX); /* 附近:读 /tmp 缓存(快,实测无 popen) */ + if (c->near_n < 0) c->near_n = 0; + /* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */ + net_bt_mode_str(c->mode, sizeof c->mode); + + int n = 0; + /* 已连接设备 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (c->conn_n == 0) { + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + } else for (int i = 0; i < c->conn_n && n < max; i++) { + rbuf[n].kind = SUB_ACT; + rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac; + rbuf[n].sub = c->conn[i].mac; + rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i; + n++; + } + /* 已配对设备 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (c->paired_n == 0) { + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + } else for (int i = 0; i < c->paired_n && n < max; i++) { + rbuf[n].kind = SUB_ACT; + rbuf[n].label = c->paired[i].name[0] ? c->paired[i].name : c->paired[i].mac; + rbuf[n].sub = bt_is_conn(c, c->paired[i].mac) ? "已连接" + : (c->paired[i].mac[0] ? c->paired[i].mac : ""); + rbuf[n].on = bt_is_conn(c, c->paired[i].mac); + rbuf[n].act = BT_ACT_PAIRED_BASE + i; + n++; + } + /* 附近设备(扫描发现,未配对) */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (c->near_n == 0) { + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + } else for (int i = 0; i < c->near_n && n < max; i++) { + int is_paired = bt_is_paired(c, c->near[i].mac); + if (is_paired) continue; /* 已配对者归于「已配对设备」,附近只列未配对 */ + rbuf[n].kind = SUB_ACT; + rbuf[n].label = c->near[i].name[0] ? c->near[i].name : c->near[i].mac; + rbuf[n].sub = c->near[i].mac; + rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i; + n++; + } + return n; +} + + +/* 执行 bluetoothctl 命令(带超时)+ 结果弹窗反馈 */ +static void bt_run(const char *title, const char *cmd) +{ + char *o = popen_read_tmo(8000, cmd); + char line[48]; + const char *txt[4]; txt[0] = line; + int ok = o && (strstr(o, "successful") || strstr(o, "Connected") || + strstr(o, "already") || strstr(o, "Paired")); + snprintf(line, sizeof line, "%s", ok ? "操作成功" : "操作失败/超时"); + if (o) free(o); + s_dd_modal(title, txt, 1, -1); +} + + +/* 动作行(SUB_ACT)点击:连接/断开/配对 */ +static int bt_act(void *ctx, int idx, int act) +{ + (void)idx; + bt_ctx_t *c = ctx; + char mac[24] = ""; + /* 定位 MAC(按 act 段) */ + if (act >= BT_ACT_NEAR_BASE) { + int i = act - BT_ACT_NEAR_BASE; + if (i < 0 || i >= c->near_n) return 0; + snprintf(mac, sizeof mac, "%s", c->near[i].mac); + } else if (act >= BT_ACT_PAIRED_BASE) { + int i = act - BT_ACT_PAIRED_BASE; + if (i < 0 || i >= c->paired_n) return 0; + snprintf(mac, sizeof mac, "%s", c->paired[i].mac); + } else if (act >= BT_ACT_CONN_BASE) { + int i = act - BT_ACT_CONN_BASE; + if (i < 0 || i >= c->conn_n) return 0; + snprintf(mac, sizeof mac, "%s", c->conn[i].mac); + } + if (!mac[0]) return 0; + + if (act >= BT_ACT_NEAR_BASE) { + /* 附近未配对 → pair;已配对(理论上过滤了)→ connect */ + char cmd[192]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl pair %s 2>/dev/null", mac); + bt_run("配对", cmd); + } else if (act >= BT_ACT_PAIRED_BASE) { + /* 已配对:未连 → connect;已连 → disconnect */ + if (bt_is_conn(c, mac)) { + char cmd[192]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac); + bt_run("断开", cmd); + } else { + char cmd[192]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl connect %s 2>/dev/null", mac); + bt_run("连接", cmd); + } + } else { + /* 已连接 → disconnect */ + char cmd[192]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac); + bt_run("断开", cmd); + } + bt_role_refresh(1); + net_bt_scan_trigger(); + c->full = 1; /* 动作后:下一次 rows() 全量重扫 */ + logmsg("bt_page: act act=%d mac=%s", act, mac); + return 1; /* 重绘 + 重填行 */ +} + + +/* 运行蓝牙独立子页面(阻塞到返回) */ +int run_bt_page(void) +{ + static bt_ctx_t c; + memset(&c, 0, sizeof c); + sub_page_t p; + memset(&p, 0, sizeof p); + p.title = "蓝牙"; + p.top_sw = bt_top_sw; + p.top_sw_set = bt_top_sw_set; + p.rows = bt_rows; + p.act = bt_act; + p.detail = NULL; /* 蓝牙动作都走 SUB_ACT(act),无 SUB_ROW 详情 */ + p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */ + p.ctx = &c; + c.full = 1; /* 首行全量重扫 */ + net_bt_scan_trigger(); /* 进页立即触发一次附近扫描(异步,不阻塞) */ + bt_role_refresh(1); + return meow_subpage_run(&p); +} diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index 0fa3405..180ef06 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -25,6 +25,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf" "$CC" -I"../sdk" -Os -static \ main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \ notif_center.c app_launcher.c audio.c backlight.c net.c about.c \ + subpage.c wifi_page.c bt_page.c \ font_theme.c keys.c util.c \ meow_touch.c meow_settings.c meow_notif.c \ "../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd diff --git a/patch_v2/meow_launcherd/fb_compositor.c b/patch_v2/meow_launcherd/fb_compositor.c index 593cc61..3c741d9 100644 --- a/patch_v2/meow_launcherd/fb_compositor.c +++ b/patch_v2/meow_launcherd/fb_compositor.c @@ -56,7 +56,7 @@ int fb_open(void) * virtual framebuffer so we can read/write both buffers for the * backup/restore below; mmapping only one frame would let the menu * land on the buffer the panel is NOT showing. */ - if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* single buffer */ + if (fb_yres_virtual < (unsigned int)fb_yres) fb_yres_virtual = (unsigned int)fb_yres; /* single buffer */ fb_mem = mmap(NULL, (size_t)fb_line * fb_yres_virtual, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb_mem == MAP_FAILED) { @@ -105,7 +105,7 @@ int fb_open(void) /* Snapshot the player's currently-displayed frame into a heap backup. */ void fb_backup_player(void) { - if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } /* single buffer */ + if (fb_yres_virtual <= (unsigned int)fb_yres) { fb_orig_yoffset = -1; return; } /* single buffer */ if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0; fb_orig_yoffset = (int)fb_active_yoffset; fb_player_backup_len = (size_t)fb_line * fb_yres; @@ -129,7 +129,7 @@ void fb_backup_player(void) * panel must show buffer0 for the menu and apps to be visible. */ void fb_norm_buf0(void) { - if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; } + if (fb_fd < 0 || fb_yres_virtual <= (unsigned int)fb_yres) { fb_active_yoffset = 0; return; } if (fb_active_yoffset == 0) return; /* already showing buffer0 */ unsigned char v[160]; memset(v, 0, sizeof v); @@ -149,7 +149,7 @@ void fb_norm_buf0(void) * no-op,无频闪),供唤醒/重绘路径调用,确保重画的界面一定落在被显示的缓冲。 */ void fb_pan_buf0_force(void) { - if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; } + if (fb_fd < 0 || fb_yres_virtual <= (unsigned int)fb_yres) { fb_active_yoffset = 0; return; } unsigned char v[160]; memset(v, 0, sizeof v); if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; /* read current vinfo */ diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index 054b54c..60229b2 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -553,11 +553,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, * 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消 * 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。 * M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */ - if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { + if (t - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) { if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); } m_prev_pwr = pwr; continue; - } else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { + } else if (t - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) { pwr_menu_state = 1; logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */ @@ -597,7 +597,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, /* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏 * (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */ logmsg("pm: power release after power-menu -> no toggle (suppress)"); - } else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { + } else if (pwr_hold_at && (t - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); meow_toggle_panel(); if (meow_screen_on) { diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 69089e3..f360248 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -175,6 +175,8 @@ extern "C" { #define NET_LIST_BT_NEARBY 4 /* 蓝牙附近设备(已配对 + 扫描发现;下拉,只读展示) */ #define NET_LIST_BT_MODE 5 /* 蓝牙当前连接模式(只读信息;对应"蓝牙模式"行,s_net_get 显示) */ #define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */ +#define NET_LIST_WIFI_PAGE 7 /* M2.76:WiFi 独立子页面入口(tap → run_wifi_page) */ +#define NET_LIST_BT_PAGE 8 /* M2.76:蓝牙独立子页面入口(tap → run_bt_page) */ #define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */ #define BG_PRESETS_N 3 #define HK_PRESETS_N 2 @@ -414,8 +416,46 @@ typedef struct { } srow_t; -/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作) */ -typedef struct { int id; char name[48]; } meow_net_t; +/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作)。 + * M2.76:追加 mac[24](AP BSSID / 蓝牙设备 MAC),供连接/配对/详情展示;无则空串。 */ +typedef struct { int id; char name[48]; char mac[24]; } meow_net_t; + + +/* ================================================================== */ +/* M2.76:WiFi/蓝牙独立子页面脚手架(安卓风:顶栏+返回+分组列表+滚动) */ +/* 实现:subpage.c(统一循环) + wifi_page.c / bt_page.c(数据/动作)。 */ +/* 与设置页同主题(s_ui_theme,MD3 语义色,明暗自动);滑动方向沿用 */ +/* 设置页语义(含 input.swipe_invert);局部刷新不整屏重绘;模态弹窗 */ +/* 复用 launcher 既有 s_dd_modal(点外关闭不穿透,见 settings_page.c)。*/ +/* ================================================================== */ +#define SUB_ROWS_MAX 44 /* 子页一次最多行数(分组标题+各行,防爆) */ + +/* 子页一行:kind 决定渲染(组标题 / 普通行 / 开关行 / 动作行<右chevron>) */ +typedef enum { SUB_HDR, SUB_ROW, SUB_SW, SUB_ACT } sub_kind_t; +typedef struct { + sub_kind_t kind; + const char *label; /* 主文本 */ + const char *sub; /* 副文本/摘要(右侧 dim 文字),可空 */ + int on; /* SUB_SW:开关态 */ + int act; /* 动作码(回传给 act 回调定位具体行) */ +} sub_row_t; + +/* 子页描述(页面提供回调,脚手架负责绘制/滚动/触摸/按键/自动息屏) */ +typedef struct sub_page { + const char *title; /* 顶栏标题 */ + int (*top_sw)(void *ctx, int *on); /* 顶栏开关:返回 1=显示开关并写 *on;0=无开关 */ + void (*top_sw_set)(void *ctx, int on);/* 顶栏开关切换(写+生效+触发重扫) */ + int (*rows)(void *ctx, sub_row_t *rbuf, int max); /* 填行,返回行数 */ + int (*act)(void *ctx, int idx, int act); /* 点动作行/开关行;返回 1=重绘+重填行 */ + int (*detail)(void *ctx, int idx, int act);/* 点 SUB_ROW 行(如连接/设备详情);返回 1=重绘 */ + int live; /* 1=驻留时周期性(约2s)重扫行(蓝牙/附近扫描刷新用) */ + void *ctx; /* 页面私有上下文 */ +} sub_page_t; + +/* 运行子页(阻塞到返回;返回 0)。由 wifi_page.c / bt_page.c 各自实现。 */ +int meow_subpage_run(sub_page_t *p); +int run_wifi_page(void); +int run_bt_page(void); /* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */ @@ -586,6 +626,17 @@ void meow_sys_light(void); void meow_sys_refresh(void); void meow_toggle_panel(void); void net_wifi_ssid(char *buf, int len); +/* M2.76:网络/蓝牙列表现拆分给 WiFi/蓝牙独立子页面用(原 static,改为跨模块暴露) */ +void net_wifi_scan_trigger(void); +void net_bt_scan_trigger(void); +int net_wifi_info(char (*info)[48], int max); +int net_wifi_saved(meow_net_t *out, int max); +int net_wifi_scan(meow_net_t *out, int max); +int net_bt_paired(meow_net_t *out, int max); +int net_bt_scan(meow_net_t *out, int max); +int net_bt_connected(meow_net_t *out, int max); +int bt_role(char *desc, int len); +void net_bt_mode_str(char *buf, int len); unsigned long long now_ms(void); void paint_sb_clock(void); void paint_sb_right(void); diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index a091731..e1b32b6 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/net.c b/patch_v2/meow_launcherd/net.c index dacca49..bfa28f1 100644 --- a/patch_v2/meow_launcherd/net.c +++ b/patch_v2/meow_launcherd/net.c @@ -59,7 +59,7 @@ int proc_by_comm(const char *target) static char g_bt_mode[48] = "开启未连接"; /* 设置页"蓝牙模式"行展示(bt_role_refresh 刷新) */ -static int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时) */ +int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时),跨模块给 bt_page 用 */ /* ===== M2.39/M2.40 网络/WiFi/蓝牙(设置页「网络」分组)===== @@ -186,7 +186,7 @@ void net_wifi_ssid(char *buf, int len) * 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。 * 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。 * 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */ -static int net_wifi_info(char (*info)[48], int max) +int net_wifi_info(char (*info)[48], int max) { if (!wlan_connected()) return 0; char *o = popen_read_tmo(1500, @@ -217,8 +217,8 @@ static int net_wifi_info(char (*info)[48], int max) /* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "\t\t\t"。 - * 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */ -static int net_wifi_saved(meow_net_t *out, int max) + * 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN)/.mac(BSSID),返回项数(≤max)。 */ +int net_wifi_saved(meow_net_t *out, int max) { int n = 0; char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null"); @@ -234,6 +234,9 @@ static int net_wifi_saved(meow_net_t *out, int max) char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++; while (*sp == ' ' || *sp == '\t') sp++; char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++; + /* BSSID 列(SSID 后):\t\t\t,拷进 mac 供详情/连接展示 */ + char *bp = se; while (*bp == ' ' || *bp == '\t') bp++; + char *be = bp; while (*be && *be != ' ' && *be != '\t' && *be != '\n' && *be != '\r') be++; if (sp != se) { /* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"), * 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */ @@ -243,6 +246,10 @@ static int net_wifi_saved(meow_net_t *out, int max) memcpy(raw, sp, (size_t)l); raw[l] = 0; out[n].id = id; wpa_decode_ssid(raw, out[n].name, sizeof out[n].name); + if (be > bp && (be - bp) < (int)sizeof out[n].mac) { + memcpy(out[n].mac, bp, (size_t)(be - bp)); + out[n].mac[be - bp] = 0; + } n++; } } @@ -255,7 +262,7 @@ static int net_wifi_saved(meow_net_t *out, int max) /* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。 * 结果稍后经 net_wifi_scan() 读。 */ -static void net_wifi_scan_trigger(void) +void net_wifi_scan_trigger(void) { if (!wlan_connected()) return; system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null"); @@ -267,8 +274,8 @@ static void net_wifi_scan_trigger(void) * flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。 * M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条, * 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。 - * 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */ -static int net_wifi_scan(meow_net_t *out, int max) + * 填 out[i].id=-1;name 存 SSID(或隐藏标记)/mac 存 BSSID。返回项数(≤max,且 ≤S_NET_MAX)。 */ +int net_wifi_scan(meow_net_t *out, int max) { int n = 0; if (max > S_NET_MAX) max = S_NET_MAX; @@ -315,6 +322,7 @@ static int net_wifi_scan(meow_net_t *out, int max) seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0; named[idx] = has; out[idx].id = -1; + snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid); /* 保留 BSSID 供详情展示 */ if (has) { /* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */ char raw[96]; @@ -341,8 +349,8 @@ static int net_wifi_scan(meow_net_t *out, int max) /* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device "。 - * hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */ -static int net_bt_paired(meow_net_t *out, int max) + * 填 out[i].mac=MAC / .name=名称;hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */ +int net_bt_paired(meow_net_t *out, int max) { if (!hci_powered()) return 0; int n = 0; @@ -358,11 +366,15 @@ static int net_bt_paired(meow_net_t *out, int max) char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++; char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; /* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */ + int ml = (int)(mac - p); if (ml > 47) ml = 47; + if (ml > 0 && ml < (int)sizeof out[n].mac) { + memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0; + } if (*nm) snprintf(out[n].name, sizeof out[n].name, "%s", nm); else { - int ml = (int)(mac - p); if (ml > 47) ml = 47; - memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 0; + int l = ml; if (l > 47) l = 47; + memcpy(out[n].name, p, (size_t)l); out[n].name[l] = 0; } out[n].id = -1; n++; @@ -374,6 +386,42 @@ static int net_bt_paired(meow_net_t *out, int max) } +/* 蓝牙已连接设备(bluetoothctl `devices Connected`):每行 "Device "。 + * M2.76:供蓝牙独立子页面「已连接设备」分组展示。hci 未上电返回 0。 */ +int net_bt_connected(meow_net_t *out, int max) +{ + if (!hci_powered()) return 0; + char *o = popen_read_tmo(1500, + "PATH=/bin:/sbin:/usr/bin:/usr/sbin printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null"); + if (!o) return 0; + int n = 0; + char *line = o; + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); + if (eol) *eol = 0; + char *q = line; while (*q == ' ' || *q == '\t') q++; + if (!strncmp(q, "Device ", 7)) { + q += 7; + char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++; + char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; + int ml = (int)(mac - q); if (ml > 47) ml = 47; + if (ml > 0 && ml < (int)sizeof out[n].mac) { + memcpy(out[n].mac, q, (size_t)ml); out[n].mac[ml] = 0; + } + char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; + *e = 0; + if (*nm) snprintf(out[n].name, sizeof out[n].name, "%s", nm); + else { int l = ml; if (l > 47) l = 47; memcpy(out[n].name, q, (size_t)l); out[n].name[l] = 0; } + out[n].id = -1; + n++; + } + line = eol ? eol + 1 : NULL; + } + free(o); + return n; +} + + /* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)---- * M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s), * 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存; @@ -382,7 +430,7 @@ static int net_bt_paired(meow_net_t *out, int max) static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */ -static void net_bt_scan_trigger(void) +void net_bt_scan_trigger(void) { if (!hci_powered()) return; long t = (long)now_ms(); @@ -395,7 +443,7 @@ static void net_bt_scan_trigger(void) /* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device ",去重;含已配对 * 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。 * 返回项数(≤max,且 ≤S_NET_MAX)。 */ -static int net_bt_scan(meow_net_t *out, int max) +int net_bt_scan(meow_net_t *out, int max) { if (!hci_powered()) return 0; if (max > S_NET_MAX) max = S_NET_MAX; @@ -424,6 +472,9 @@ static int net_bt_scan(meow_net_t *out, int max) char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; *e = 0; /* 去行尾换行 */ strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0; + if (ml < (int)sizeof out[n].mac) { + memcpy(out[n].mac, dv, (size_t)ml); out[n].mac[ml] = 0; /* 保留 MAC 供配对/连接 */ + } if (*nm) snprintf(out[n].name, sizeof out[n].name, "%s", nm); else { @@ -484,7 +535,7 @@ static int a2dp_side(const char *sec) /* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */ -static int bt_role(char *desc, int len) +int bt_role(char *desc, int len) { if (desc && len > 0) desc[0] = 0; if (!g_sys.bt_seen || !hci_powered()) { @@ -575,6 +626,14 @@ void bt_role_refresh(int force) g_sys.bt_on = (g_sys.bt_role != BT_ROLE_OFF) ? 1 : 0; } +/* M2.76:蓝牙角色中文描述(节流刷新,读缓存 g_bt_mode;供蓝牙子页摘要行, + * 避免子页驻留期间每 tick 反复 popen bt_role)。 */ +void net_bt_mode_str(char *buf, int len) +{ + bt_role_refresh(0); + snprintf(buf, len, "%s", g_bt_mode); +} + /* 网络信息缓存(绘制路径只读,s_net_refresh 刷新;下拉打开时才重新解析列表) */ static char g_net_ssid[64] = "未连接"; @@ -616,6 +675,8 @@ int s_net_get(const srow_t *r, char *buf, int len) case NET_LIST_BT_NEARBY: snprintf(buf, len, "%d 台设备", g_net_bt_n); return 0; case NET_LIST_BT_MODE: snprintf(buf, len, "%s", g_bt_mode); return 0; case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0; + case NET_LIST_WIFI_PAGE: snprintf(buf, len, "%s", g_net_ssid); return 0; /* M2.76:子页入口摘要 */ + case NET_LIST_BT_PAGE: snprintf(buf, len, "%s", g_bt_mode); return 0; /* M2.76 */ } snprintf(buf, len, "-"); return 0; diff --git a/patch_v2/meow_launcherd/settings_page.c b/patch_v2/meow_launcherd/settings_page.c index 37f77f2..eb1e5c2 100644 --- a/patch_v2/meow_launcherd/settings_page.c +++ b/patch_v2/meow_launcherd/settings_page.c @@ -43,79 +43,76 @@ static const int s_time_tiers[] = { /* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */ static const srow_t SMENU[] = { /* 显示 */ - { "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 }, - { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 }, - { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0,0 }, + { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0,0 }, + { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动), * 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */ - { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, - { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, - { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N }, - /* 网络 */ - { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID }, - { "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED }, - { "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN }, - { "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE }, - { "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY }, + { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 }, + { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0,0 }, + { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N,0 }, + /* 网络:M2.76 WiFi/蓝牙升级为独立子页面(安卓风)。入口行为「列表行」+右侧 chevron, + * 摘要显示当前状态(s_net_get);点开进入对应子页(顶栏+返回+分组列表),详见 + * wifi_page.c / bt_page.c。原来散落的开关/列表/信息行收敛进子页面。 */ + { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_PAGE }, + { "蓝牙", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_PAGE }, /* 电源 */ - { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 }, - { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 }, - { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N }, + { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0,0 }, + { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0,0 }, + { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N,0 }, /* 开机 */ - { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 }, + { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, + { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0,0 }, /* 输入 */ - { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N }, - { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 }, - { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 }, - { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N,0 }, + { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0,0 }, + { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0,0 }, + { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* 字体主题 */ - { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, /* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。 * 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); * 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */ - { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/// 列表; * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ - { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 }, - { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 }, - { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, + { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0,0 }, + { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0,0 }, + { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0,0 }, + { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, /* 通知 */ - { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 }, + { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, + { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0,0 }, /* 日志 */ - { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0,0 }, + { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0,0 }, /* 音频 */ - { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 }, - { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 }, - { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 }, - { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N }, + { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, + { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0,0 }, + { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0,0 }, + { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0,0 }, + { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N,0 }, /* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */ - { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 }, { "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT }, }; @@ -1043,11 +1040,11 @@ void run_settings_page(void) if (pwr && pwr_hold_at) { /* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state) * 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */ - if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { + if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) { if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); } /* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */ - } else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { + } else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) { pwr_menu_state = 1; logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); show_power_menu(); /* 动作在函数内 system();取消则续循环 */ @@ -1076,7 +1073,7 @@ void run_settings_page(void) /* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */ if (pwr_suppress_click) { logmsg("settings pm: power release after power-menu -> no toggle (suppress)"); - } else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { + } else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); meow_toggle_panel(); meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ @@ -1200,9 +1197,13 @@ void run_settings_page(void) s_commit(r, 1); } else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd - * (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 */ + * (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 + * M2.76:WiFi/蓝牙升级为独立子页面 → 点开进 run_wifi_page/ + * run_bt_page(阻塞,内部自开 fds;返回后仍须排空本页 tfd)。 */ after_modal = 1; - s_net_open(r); + if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh(); } + else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh(); } + else s_net_open(r); if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} diff --git a/patch_v2/meow_launcherd/subpage.c b/patch_v2/meow_launcherd/subpage.c new file mode 100644 index 0000000..d31891b --- /dev/null +++ b/patch_v2/meow_launcherd/subpage.c @@ -0,0 +1,504 @@ +/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙)统一脚手架(M2.76)。 + * + * 安卓风子页骨架:顶栏(标题 + 返回 + 右上开关)+ 分组列表(组标题/行/开关/动作) + * + 滚动(跟手,方向沿用设置页 input.swipe_invert 语义)+ 左缘左滑返回 + 底部返回。 + * + * 由页面(wifi_page.c / bt_page.c)通过 sub_page_t 提供: + * - title 顶栏标题 + * - top_sw 顶栏开关(0=不显示;1=显示并回写当前值) + * - top_sw_set 开关切换(写设置 + 生效 + 触发重扫) + * - rows 填行(组标题/普通行/开关行/动作行),返回行数 + * - act 点动作/开关行 + * - detail 点普通行(如当前连接详情、网络/设备详情) + * - live 1=驻留时周期性(约2s)重扫行(蓝牙附近扫描等动态数据) + * + * 与设置页同主题(s_ui_theme,MD3 语义色,明暗自动);局部刷新不整屏重绘; + * 模态弹窗交给 launcher 既有 s_dd_modal(点外关闭不穿透),本脚手架在回调返回后 + * 彻底排空本页 tfd + 重置触摸基线(复刻 run_settings_page 后置排空,防穿透)。 + * + * 物理键:电源=系统级语义(单击息屏/亮屏、长按电源菜单、8s 强关)、音量=系统音量、 + * 播放=抓住——不再映射为返回(APPS §3.4.1 全局接管)。 + */ +#include "meow_launcher_internal.h" + + +/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface_variant 整屏底)---- */ +#define SUB_TB_Y SB_BAR_H /* 28:顶栏顶缘(状态栏下) */ +#define SUB_TB_H 68 /* 顶栏高 28..96 */ +#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 / 分隔线 */ +#define SUB_CONT_TOP (SUB_TB_BOT + 6) /* 102:内容区顶缘 */ +#define SUB_ROW_H 58 /* 行高(对齐 SROW_H,便于触摸) */ +#define SUB_HDR_H 40 /* 组标题行高 */ +#define SUB_PAD_X 16 /* 水平内边距 */ +#define SUB_SW_W 48 /* 开关宽(s_draw_switch) */ +#define SUB_SW_H 28 +#define SUB_BTN_X 8 /* 底部返回按钮左缘 */ +#define SUB_BTN_W 120 /* 底部返回按钮宽 */ + + +/* 返回行高:组标题 40,其余 58 */ +static int sub_row_h(const sub_row_t *r) +{ + return (r->kind == SUB_HDR) ? SUB_HDR_H : SUB_ROW_H; +} + +/* 滚动 clamp 到 [0, max](scroll_max 由调用方保证 >=0) */ +static int sub_clamp(int v, int max) +{ + if (v < 0) v = 0; + if (v > max) v = max; + return v; +} + + +/* ---- 计算行 y 前缀和(ypos[i]=第 i 行顶缘相对内容区顶;ypos[n]=总高)---- */ +static void sub_ypos(const sub_row_t *rows, int n, int ypos[SUB_ROWS_MAX + 1]) +{ + ypos[0] = 0; + for (int i = 0; i < n; i++) ypos[i + 1] = ypos[i] + sub_row_h(&rows[i]); +} + + +/* ---- 绘制子页(进 back_buf;调用方再局部标脏 + flush)---- + * rows/nrows 为当前行集;scroll_off 为滚动偏移;top_on 为顶栏开关当前值; + * top_sw_en 为是否显示顶栏开关。 */ +static void subpage_paint(sub_page_t *p, const sub_row_t *rows, int nrows, + int scroll_off, int top_on, int top_sw_en) +{ + const meow_ui_theme_t *th = s_ui_theme(); + int W = fb_xres, H = fb_yres; + int ypos[SUB_ROWS_MAX + 1]; + sub_ypos(rows, nrows, ypos); + int total = ypos[nrows]; + int view_h = H - SUB_CONT_TOP; + int scroll_max = total > view_h ? total - view_h : 0; + + /* 全屏底 surface + 顶栏区 */ + fill_rect(0, 0, W, H, th->surface); + draw_statusbar(); + + /* 顶栏:返回箭头 + 标题 + 右上开关;顶栏底缘 outline 分隔线 */ + int tb_cy = SUB_TB_Y + SUB_TB_H / 2; /* 顶栏垂直中心 */ + s_draw_chevron_left(SUB_PAD_X, tb_cy, th->on_surface); + if (p->title) ui_draw(SUB_PAD_X + 24, tb_cy - meow_font_line_height(22) / 2, + p->title, th->on_surface, 22); + if (top_sw_en) { + int sx = W - SUB_PAD_X - SUB_SW_W; + int sy = tb_cy - SUB_SW_H / 2; + s_draw_switch(sx, sy, top_on, th); + } + fill_rect(0, SUB_TB_BOT, W, 2, th->outline); + + /* 内容区(surface_variant 整屏底) */ + fill_rect(0, SUB_TB_BOT, W, H - SUB_TB_BOT, th->surface_variant); + + /* 分组列表行(裁剪到视口;顶部状态栏/顶栏、底部返回区不画) */ + for (int i = 0; i < nrows; i++) { + int ry = SUB_CONT_TOP + ypos[i] - scroll_off; + int rh = sub_row_h(&rows[i]); + if (ry + rh < SUB_CONT_TOP || ry >= H) continue; /* 视口外整行跳过(配滚动) */ + const sub_row_t *r = &rows[i]; + if (r->kind == SUB_HDR) { + ui_draw(SUB_PAD_X + 4, ry + (SUB_HDR_H - meow_font_line_height(18)) / 2, + r->label ? r->label : "", th->on_surface_variant, 18); + } else { + int ly = ry + (SUB_ROW_H - meow_font_line_height(22)) / 2; + ui_draw(SUB_PAD_X + 4, ly, r->label ? r->label : "", th->on_surface, 22); + if (r->sub && r->sub[0]) { + int vw = ui_width(r->sub, 22); + if (r->kind == SUB_ACT) vw += 8; /* 给右 chevron 留位 */ + ui_draw(W - SUB_PAD_X - 12 - vw, ly, r->sub, th->on_surface_variant, 22); + } + if (r->kind == SUB_SW) { + int sy = ry + (SUB_ROW_H - SUB_SW_H) / 2; + s_draw_switch(W - SUB_PAD_X - SUB_SW_W, sy, r->on, th); + } else if (r->kind == SUB_ACT) { + int cy = ry + SUB_ROW_H / 2; + s_draw_chevron(W - SUB_PAD_X - 6, cy, th->on_surface_variant); + } + /* 行底分隔线(on_surface_outline 弱化) */ + meow_ui_fill_rect(SUB_PAD_X + 4, ry + SUB_ROW_H - 1, W - 2 * (SUB_PAD_X + 4), 1, + th->outline); + } + } + + /* 滚动条(内容超出视口时) */ + if (scroll_max > 0) { + int thumb_h = view_h * view_h / total; + if (thumb_h < 24) thumb_h = 24; + int th_y = SUB_CONT_TOP + (view_h - thumb_h) * scroll_off / (total - view_h); + meow_gfx_fill_round(W - 12, SUB_CONT_TOP, 5, view_h, 3, th->surface_container); + meow_gfx_fill_round(W - 12, th_y, 5, thumb_h, 3, th->on_surface_variant); + } + + /* 底部返回按钮(左)+ 版本号(右),复用设置页底部对齐 */ + { + int foot_y = H - meow_font_line_height(20) - 8; + s_draw_chevron_left(SUB_PAD_X + 4, foot_y + meow_font_line_height(20) / 2, + th->on_surface_variant); + ui_draw(SUB_PAD_X + 16, foot_y, "返回", th->on_surface_variant, 20); + char ver[32]; + snprintf(ver, sizeof ver, "MeowOS v%s", g_meowos_version); + ui_draw(W - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20); + } +} + + +/* ---- 运行子页(阻塞到返回;返回 0)---- */ +int meow_subpage_run(sub_page_t *p) +{ + int W = fb_xres, H = fb_yres; + sub_row_t rows[SUB_ROWS_MAX]; + int ypos[SUB_ROWS_MAX + 1]; + int nrows = 0, scroll_off = 0, total = 0, view_h = 0, scroll_max = 0; + + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); + struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; + memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); + int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; + } + int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } + unsigned long long touch_cooldown_until = now_ms() + 300; + unsigned long long last_tick = now_ms(); + unsigned long long last_act = now_ms(); + unsigned long long last_live = now_ms(); + int after_modal = 0; /* 子页内模态关闭后:覆盖过状态栏,需整帧重绘还原 */ + + /* 触摸状态 */ + int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; + int scroll_drag = 0, scroll_base = 0, scroll_y0 = 0, scroll_invert = 0; + int wake_suppress = 0; + unsigned long long pwr_hold_at = 0; + int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0; + + /* 顶栏开关初始值 */ + int top_on = 0, top_sw_en = 0; + if (p->top_sw) top_sw_en = p->top_sw(p->ctx, &top_on); + + /* 首填行 + 首绘 */ + nrows = p->rows ? p->rows(p->ctx, rows, SUB_ROWS_MAX) : 0; + if (nrows > SUB_ROWS_MAX) nrows = SUB_ROWS_MAX; + sub_ypos(rows, nrows, ypos); + total = ypos[nrows]; + view_h = H - SUB_CONT_TOP; + scroll_max = total > view_h ? total - view_h : 0; + subpage_paint(p, rows, nrows, scroll_off, top_on, top_sw_en); + meow_dirty_full(); meow_ui_flush(); + + for (;;) { + unsigned long long now = now_ms(); + if (sigterm_flag) break; + int need_paint = 0; + + /* ---- 自动息屏态:留在子页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- */ + if (meow_auto_slept && !meow_screen_on) { + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int wake = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1; + } + if (!wake && screen_off_mode == 1 && tfd >= 0) { + struct pollfd spfd = { tfd, POLLIN, 0 }; + if (poll(&spfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1; + } + } + } + if (wake) { + meow_panel_on(); + meow_auto_slept = 0; + wake_suppress = 1; + last_act = now; + memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + touch_cooldown_until = now_ms() + 300; + td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1; + after_modal = 1; + subpage_paint(p, rows, nrows, scroll_off, top_on, top_sw_en); + meow_dirty_full(); meow_ui_flush(); + } + usleep(POLL_MS * 1000); + continue; + } + + /* ---- 按键 ---- */ + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int vold = 0, volu = 0, play = 0, pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + } + (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); + (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); + vold = kd_vold.stable; volu = kd_volu.stable; + play = kd_play.stable; pwr = kd_pwr.stable; + if (wake_suppress) { + if (!pwr && !vold && !volu && !play) wake_suppress = 0; + } else { + /* 音量键 = 系统音量(对齐设置页,不做光标移动) */ + if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } + if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } + if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; } + /* 电源键 = 系统级语义(单击息屏/亮屏、长按电源菜单、8s 强关;不映射返回) */ + if (pwr && !m_prev_pwr) { + pwr_hold_at = now; pwr_menu_state = 0; + pwr_suppress_click = 0; + last_act = now; + if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; + } + if (pwr && pwr_hold_at) { + if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) { + if (sim_no_exec) { logmsg("subpage pm: [sim-no-exec] POWER HOLD -> force poweroff"); } + else { logmsg("subpage pm: POWER HOLD -> force poweroff"); meow_power_action(POWER_SHUTDOWN); } + } else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) { + pwr_menu_state = 1; + logmsg("subpage pm: POWER HOLD -> show power menu"); + show_power_menu(); + last_act = now_ms(); + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 1; + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + after_modal = 1; + need_paint = 1; + goto sub_keys_seeded; + } + } + if (!pwr && m_prev_pwr) { + if (pwr_suppress_click) { + logmsg("subpage pm: power release after menu -> no toggle"); + } else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && + !pwr_menu_state && !pwr_woke) { + logmsg("subpage pm: power single-click -> toggle panel"); + meow_toggle_panel(); + meow_auto_slept = 0; + last_act = now_ms(); + need_paint = 1; + } else if (pwr_woke) { + logmsg("subpage pm: power release after auto wake -> no toggle"); + } + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 0; + } + } + if (vol_ramp_dir != 0) { + int held = (vol_ramp_dir > 0) ? volu : vold; + (void)vol_ramp_tick(held); + g_sys.vol = settings_volume_get(); + paint_sb_right(); + need_paint = 1; + } + m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; + sub_keys_seeded: + ; + + /* ---- 触摸 ---- */ + if (tfd >= 0 && meow_screen_on) { + struct pollfd pfd = { tfd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_ABS) { + if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val; + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; + if (td && !td_ok && tlx >= 0 && tly >= 0) { + tdx = tlx; tdy = tly; td_ok = 1; + scroll_y0 = tdy; + } + if (td_ok && !scroll_drag) { + int ddy = tly - scroll_y0; + if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { + scroll_drag = 1; + meow_settings_get_bool("input.swipe_invert", &scroll_invert); + scroll_base = scroll_off + (scroll_invert ? -ddy : ddy); + } + } + if (scroll_drag && td_ok) { + int dydt = tly - scroll_y0; + scroll_off = sub_clamp(scroll_base + (scroll_invert ? dydt : -dydt), + scroll_max); + need_paint = 1; + } + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val && !td) { + if (now_ms() < touch_cooldown_until) { tlx = tly = -1; } + else { td = 1; td_ok = 0; tlx = -1; tly = -1; scroll_drag = 0; } + } else if (!val && td) { + td = 0; + if (scroll_drag) { + scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; + need_paint = 1; + } else if (td_ok) { + int dx = tlx - tdx, dy = tly - tdy; + /* 左缘左滑返回(复用全局判定) */ + if (meow_edge_back(tdx, dx, dy)) { + if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } + goto subdone; + } + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + if (adx < 60 && ady < 60) { + int tx = tdx, ty = tdy; + /* 底部返回按钮 */ + if (ty >= H - 52 && tx >= SUB_BTN_X && tx <= SUB_BTN_X + SUB_BTN_W) { + if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } + goto subdone; + } + /* 顶栏返回箭头 */ + if (ty >= SUB_TB_Y && ty < SUB_TB_BOT) { + /* 左上角 48x顶栏高 = 返回箭头 */ + if (tx >= 0 && tx < 48) { + if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } + goto subdone; + } + /* 顶栏开关 */ + if (top_sw_en && + tx >= W - SUB_PAD_X - SUB_SW_W - 8 && tx < W && + ty >= SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2 - 8 && + ty < SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2 + SUB_SW_H + 8) { + if (p->top_sw_set) p->top_sw_set(p->ctx, !top_on); + top_on = !top_on; + if (p->rows) nrows = p->rows(p->ctx, rows, SUB_ROWS_MAX); + if (nrows > SUB_ROWS_MAX) nrows = SUB_ROWS_MAX; + sub_ypos(rows, nrows, ypos); + total = ypos[nrows]; + scroll_max = total > view_h ? total - view_h : 0; + if (scroll_off > scroll_max) scroll_off = scroll_max; + last_act = now; + need_paint = 1; + } + continue; + } + /* 普通行命中 */ + int cy = ty - SUB_CONT_TOP + scroll_off; + int idx = -1; + for (int i = 0; i < nrows; i++) { + if (cy >= ypos[i] && cy < ypos[i + 1]) { idx = i; break; } + } + if (idx >= 0) { + sub_kind_t kind = rows[idx].kind; + if (kind == SUB_SW || kind == SUB_ACT) { + if (p->act) p->act(p->ctx, idx, rows[idx].act); + } else if (kind == SUB_ROW) { + if (p->detail) p->detail(p->ctx, idx, rows[idx].act); + } + /* 动作后重扫行:连接/配对/开关已变更数据,列表须反映 */ + if (p->rows) nrows = p->rows(p->ctx, rows, SUB_ROWS_MAX); + if (nrows > SUB_ROWS_MAX) nrows = SUB_ROWS_MAX; + sub_ypos(rows, nrows, ypos); + total = ypos[nrows]; + scroll_max = total > view_h ? total - view_h : 0; + if (scroll_off > scroll_max) scroll_off = scroll_max; + } + if (idx >= 0) { + /* 回调可能开模态(s_dd_modal 用独立 fd 消费);返回后彻底 + * 排空本页 tfd + 重置触摸基线,防穿透(复刻设置页后置排空)。 */ + if (tfd >= 0) { + unsigned char e[16]; + while (read(tfd, e, sizeof e) == 16) {} + usleep(40000); + while (read(tfd, e, sizeof e) == 16) {} + } + td = 0; td_ok = 0; scroll_drag = 0; + scroll_base = 0; scroll_y0 = 0; + tdx = tdy = tlx = tly = -1; + touch_cooldown_until = now_ms() + 300; + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + after_modal = 1; + need_paint = 1; + } + } + } + td_ok = 0; + } + } + } + } + } + + /* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */ + if (p->live && now - last_live >= 2000) { + last_live = now; + int new_n = p->rows ? p->rows(p->ctx, rows, SUB_ROWS_MAX) : 0; + if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX; + if (new_n != nrows) { + nrows = new_n; + sub_ypos(rows, nrows, ypos); + total = ypos[nrows]; + scroll_max = total > view_h ? total - view_h : 0; + if (scroll_off > scroll_max) scroll_off = scroll_max; + last_act = now; + need_paint = 1; + } + } + + /* ---- 重绘 ---- */ + if (need_paint) { + last_act = now; + subpage_paint(p, rows, nrows, scroll_off, top_on, top_sw_en); + if (after_modal) { meow_dirty_full(); after_modal = 0; } + else meow_dirty_add(0, SUB_TB_Y, fb_xres, fb_yres - SUB_TB_Y); /* 含顶栏(开关/返回/标题) */ + meow_ui_flush(); + } + + /* ---- 自动息屏(空闲超时关屏,留在子页,等输入唤醒)---- */ + if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && + (unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) { + meow_panel_off(); + meow_auto_slept = 1; + wake_suppress = 1; + logmsg("subpage: idle %ds -> auto screen-off (stay)", screen_off_timeout); + } + /* ~1s 状态栏时钟刷新 */ + if (now - last_tick >= 1000) { + last_tick = now; + draw_clock(); + fb_norm_buf0(); + meow_ui_flush(); + } + usleep(POLL_MS * 1000); + } +subdone: + if (tfd >= 0) close(tfd); + kp_close(&kp_power); kp_close(&kp_keys); + logmsg("subpage '%s' closed", p->title ? p->title : ""); + return 0; +} diff --git a/patch_v2/meow_launcherd/wifi_page.c b/patch_v2/meow_launcherd/wifi_page.c new file mode 100644 index 0000000..d2e5970 --- /dev/null +++ b/patch_v2/meow_launcherd/wifi_page.c @@ -0,0 +1,203 @@ +/* wifi_page.c — MeowOS 系统设置「WiFi 独立子页面」(安卓风,M2.76)。 + * + * 页面(sub_page_t 脚手架 subpage.c 驱动): + * - 顶栏:返回 + 标题「WiFi」 + 右上开关(network.wifi_enabled) + * - 当前连接:SSID 行,点开 → 详情(SSID/AP MAC/本机 MAC/AP IP/本机 IP/频道) + * - 已保存网络:每项点 → wpa_cli select_network 连接;当前连接的项标「已连接」 + * - 附近网络:扫描结果(按 BSSID 去重——修复"AP 的 MAC 被当成单独设备列出"; + * 中文 SSID 经 wpa_decode_ssid 还原 \xNN 原始字节 → UTF-8 显示)点开 → 详情(SSID/AP MAC) + * + * 均复用 net.c 既有读取(net_wifi_saved / net_wifi_scan / net_wifi_info / net_wifi_ssid / + * wlan_connected),写入只走 meow_settings_*;本机 MAC 等详情读 wpa_cli status。 + */ +#include "meow_launcher_internal.h" + + +/* action code 判位(act 回调据 act 值定位网络) */ +#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */ +#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */ +#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,detail) */ + +typedef struct { + meow_net_t saved[S_NET_MAX]; + meow_net_t scan[S_NET_MAX]; + int saved_n, scan_n; + char ssid[64]; + int connected; /* wlan_connected() */ + int wifi_on; /* network.wifi_enabled */ + long last_scan_ms; /* 扫描触发节流(net_wifi_scan_trigger 间隔) */ +} wifi_ctx_t; + + +/* 顶栏开关当前值 */ +static int wifi_top_sw(void *ctx, int *on) +{ + (void)ctx; + int v = 1; + if (meow_settings_get_bool("network.wifi_enabled", &v) != 0) v = 1; + *on = v; + return 1; +} + + +/* 顶栏开关切换:写设置 + 开/关网卡 + 触发重扫 + 同步状态栏 */ +static void wifi_top_sw_set(void *ctx, int on) +{ + wifi_ctx_t *c = ctx; + meow_settings_set_bool("network.wifi_enabled", on); + s_net_toggle_wifi(on); + g_sys.wifi_on = on; + paint_sb_right(); /* 状态栏 WiFi 图标随开关即时刷新 */ + meow_settings_save(); + if (on) { net_wifi_scan_trigger(); c->last_scan_ms = now_ms(); } +} + + +/* 触发一次 WiFi 扫描(节流:1.5s 内不重复 spawn wpa_cli scan) */ +static void wifi_scan(void *ctx) +{ + wifi_ctx_t *c = ctx; + if (!c->connected) return; + long t = (long)now_ms(); + if (c->last_scan_ms && t - c->last_scan_ms < 1500) return; + c->last_scan_ms = t; + net_wifi_scan_trigger(); +} + + +/* 填行:刷新数据后构建分组列表 */ +static int wifi_rows(void *ctx, sub_row_t *rbuf, int max) +{ + wifi_ctx_t *c = ctx; + /* 刷新数据(popen 读列表;节流触发扫描) */ + c->connected = wlan_connected(); + if (c->connected) { + net_wifi_ssid(c->ssid, sizeof c->ssid); + wifi_scan(c); + } else { + c->ssid[0] = 0; + } + c->saved_n = net_wifi_saved(c->saved, S_NET_MAX); + c->scan_n = net_wifi_scan(c->scan, S_NET_MAX); + if (c->saved_n < 0) c->saved_n = 0; + if (c->scan_n < 0) c->scan_n = 0; + + int n = 0; + /* 当前连接 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "当前连接"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (n < max) { + rbuf[n].kind = SUB_ROW; + rbuf[n].label = c->connected && c->ssid[0] ? "已连接" : "未连接"; + rbuf[n].sub = c->connected && c->ssid[0] ? c->ssid : ""; + rbuf[n].on = 0; rbuf[n].act = WF_ACT_CURR; + n++; + } + /* 已保存网络 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已保存网络"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (c->saved_n == 0) { + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已保存网络"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + } else for (int i = 0; i < c->saved_n && n < max; i++) { + rbuf[n].kind = SUB_ACT; + rbuf[n].label = c->saved[i].name[0] ? c->saved[i].name : "<未命名>"; + /* 当前连接的 SSID 标记「已连接」;否则显示 BSSID(若有) */ + rbuf[n].sub = (c->connected && c->ssid[0] && !strcmp(c->saved[i].name, c->ssid)) ? "已连接" + : (c->saved[i].mac[0] ? c->saved[i].mac : ""); + rbuf[n].on = 0; rbuf[n].act = WF_ACT_SAVED + i; + n++; + } + /* 附近网络 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近网络"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (c->scan_n == 0) { + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "未扫描到附近网络"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + } else for (int i = 0; i < c->scan_n && n < max; i++) { + rbuf[n].kind = SUB_ROW; + rbuf[n].label = c->scan[i].name[0] ? c->scan[i].name : "(隐藏网络)"; + rbuf[n].sub = c->scan[i].mac[0] ? c->scan[i].mac : ""; + rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i; + n++; + } + return n; +} + + +/* 附近网络详情(只读):SSID / AP MAC / 提示 */ +static int wifi_nearby_detail_cb(wifi_ctx_t *c, int scan_idx) +{ + if (scan_idx < 0 || scan_idx >= c->scan_n) return 0; + char line[S_NET_MAX][48]; + const char *txt[S_NET_MAX]; + int n = 0; + snprintf(line[n], sizeof line[0], "SSID: %s", c->scan[scan_idx].name[0] ? c->scan[scan_idx].name : "(隐藏网络)"); txt[n] = line[n]; n++; + if (c->scan[scan_idx].mac[0]) { snprintf(line[n], sizeof line[n], "AP MAC: %s", c->scan[scan_idx].mac); txt[n] = line[n]; n++; } + snprintf(line[n], sizeof line[n], "密码/连接由 HibyOS 处理"); txt[n] = line[n]; n++; + s_dd_modal(c->scan[scan_idx].name[0] ? c->scan[scan_idx].name : "附近网络", txt, n, -1); + return 1; +} + + +/* 普通行(SUB_ROW)点击:当前连接详情 / 附近网络详情 */ +static int wifi_detail(void *ctx, int idx, int act) +{ + (void)idx; + wifi_ctx_t *c = ctx; + if (act == WF_ACT_CURR) { + /* 当前连接详情(wpa_cli status + 网关):SSID/AP MAC/本机 MAC/AP IP/本机 IP/频道 */ + char info[S_NET_MAX][48]; + const char *txt[S_NET_MAX]; + int n = net_wifi_info(info, S_NET_MAX); + if (n == 0) { + snprintf(info[0], sizeof info[0], "%s", c->connected ? "已连接(无详情)" : "未连接"); + txt[0] = info[0]; n = 1; + } else { + for (int i = 0; i < n; i++) txt[i] = info[i]; + } + s_dd_modal(c->ssid[0] ? c->ssid : "当前连接", txt, n, -1); + return 1; + } + if (act >= WF_ACT_SCAN_BASE) return wifi_nearby_detail_cb(c, act - WF_ACT_SCAN_BASE); + return 0; +} + + +/* 动作行(SUB_ACT)点击:已保存网络 → 连接 */ +static int wifi_act(void *ctx, int idx, int act) +{ + (void)idx; + wifi_ctx_t *c = ctx; + if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 detail,不走这里 */ + int sidx = act - WF_ACT_SAVED; + if (sidx < 0 || sidx >= c->saved_n) return 0; + if (c->saved[sidx].id < 0) return 0; + char cmd[192]; + snprintf(cmd, sizeof cmd, + "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 select_network %d 2>/dev/null", + c->saved[sidx].id); + system(cmd); + net_wifi_scan_trigger(); + c->last_scan_ms = now_ms(); + logmsg("wifi_page: select_network %d", c->saved[sidx].id); + return 1; /* 重绘 + 重填行 */ +} + + +/* 运行 WiFi 独立子页面(阻塞到返回) */ +int run_wifi_page(void) +{ + static wifi_ctx_t c; /* static:行次指针稳定 */ + memset(&c, 0, sizeof c); + sub_page_t p; + memset(&p, 0, sizeof p); + p.title = "WiFi"; + p.top_sw = wifi_top_sw; + p.top_sw_set = wifi_top_sw_set; + p.rows = wifi_rows; + p.act = wifi_act; + p.detail = wifi_detail; + p.live = 0; /* WiFi 列表在动作/开关时重扫即可,避免驻留 popen 轮询 */ + p.ctx = &c; + /* 打开页面立即触发一次扫描,附近列表更贴近最新 */ + net_wifi_scan_trigger(); + c.last_scan_ms = now_ms(); + bt_role_refresh(0); /* 顺带刷新网络缓存(不影响本页) */ + return meow_subpage_run(&p); +}