feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。
- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
(P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步
范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
1 parent
e87ccdf061
commit
7c7d891570
59 files changed
+5305
-590
No files matched your search
@@ -8,9 +8,10 @@
|
|||||||
* 核心设计(用户定案,勿推翻):
|
* 核心设计(用户定案,勿推翻):
|
||||||
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
|
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
|
||||||
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
|
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
|
||||||
* - 单进程双线程(+路由线程):IO 线程(accept/读帧/解析/ring 写)、播放线程
|
* - 单进程四线程:IO 线程(accept/读帧/解析/ring 写)、播放线程
|
||||||
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
|
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
|
||||||
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)。
|
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)、
|
||||||
|
* 音量执行线程(MSG_AUDIO_VOLUME 异步落 amixer + 持久化,末值去抖,见事项三)。
|
||||||
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
|
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
|
||||||
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
|
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
|
||||||
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
|
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
|
||||||
@@ -130,6 +131,7 @@ static void session_clean_locked(void);
|
|||||||
static void _audio_mark_reconfig_needed(void);
|
static void _audio_mark_reconfig_needed(void);
|
||||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static pthread_cond_t g_cond;
|
static pthread_cond_t g_cond;
|
||||||
|
static pthread_cond_t g_vol_cond; /* 音量异步执行线程等待(CLOCK_MONOTONIC,main 中初始化) */
|
||||||
|
|
||||||
struct client {
|
struct client {
|
||||||
int used;
|
int used;
|
||||||
@@ -137,10 +139,23 @@ struct client {
|
|||||||
char app_id[MEOW_APP_ID_MAX];
|
char app_id[MEOW_APP_ID_MAX];
|
||||||
uint32_t sid;
|
uint32_t sid;
|
||||||
int state; /* CLS_* */
|
int state; /* CLS_* */
|
||||||
|
uint32_t gen; /* 分配代际(音量异步 ACK 防 fd/槽复用竞态) */
|
||||||
|
int vol_ack; /* 音量请求已执行,待 IO 线程回 ACK(vol 线程写/IO 线程清) */
|
||||||
|
uint32_t vol_ack_req; /* 对应 req_id */
|
||||||
|
uint32_t vol_ack_status; /* ACK status(应用后 g_vol_pct) */
|
||||||
uint8_t rbuf[READ_BUF_MAX];
|
uint8_t rbuf[READ_BUF_MAX];
|
||||||
size_t rlen;
|
size_t rlen;
|
||||||
};
|
};
|
||||||
static struct client g_cli[CLI_MAX];
|
static struct client g_cli[CLI_MAX];
|
||||||
|
static uint32_t g_cli_gen = 0; /* 客户端分配代际计数(cli_alloc 递增) */
|
||||||
|
|
||||||
|
/* ---- 音量异步化(事项三):IO 线程只入队末值,专用 vol 线程落 amixer+持久化后
|
||||||
|
* 经 IO 线程回 ACK。驱动 wedge 时 3×popen≈4.5s 不再卡持锁的 dispatch/IO 循环。 ---- */
|
||||||
|
static int g_vol_pending = 0; /* 有待执行音量请求(g_mu 保护) */
|
||||||
|
static int g_vol_pending_pct = 0; /* 末值(0-100;连续请求只保留最新=去抖) */
|
||||||
|
static struct client *g_vol_pending_cli = NULL; /* 末次请求方(ACK 用;可能已回收,靠 gen 校验) */
|
||||||
|
static uint32_t g_vol_pending_req = 0; /* 末次请求 req_id */
|
||||||
|
static uint32_t g_vol_pending_gen = 0; /* 末次请求方 gen(防槽复用误 ACK) */
|
||||||
|
|
||||||
/* 当前授权流(喂养 DAC 的唯一流) */
|
/* 当前授权流(喂养 DAC 的唯一流) */
|
||||||
struct stream_info {
|
struct stream_info {
|
||||||
@@ -308,15 +323,16 @@ static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payloa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
|
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
|
||||||
static void cond_wait_tmo(int ms)
|
static void cond_wait_tmo_cv(pthread_cond_t *cv, int ms)
|
||||||
{
|
{
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
ts.tv_sec += ms / 1000;
|
ts.tv_sec += ms / 1000;
|
||||||
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
|
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
|
||||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||||
pthread_cond_timedwait(&g_cond, &g_mu, &ts);
|
pthread_cond_timedwait(cv, &g_mu, &ts);
|
||||||
}
|
}
|
||||||
|
static void cond_wait_tmo(int ms) { cond_wait_tmo_cv(&g_cond, ms); }
|
||||||
|
|
||||||
/* ---- frame 大小/位深 ---- */
|
/* ---- frame 大小/位深 ---- */
|
||||||
static int fmt_bps(int fmt)
|
static int fmt_bps(int fmt)
|
||||||
@@ -1135,12 +1151,14 @@ static void bt_vol_full(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化
|
/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化
|
||||||
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 */
|
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。
|
||||||
static void vol_apply(int pct)
|
* target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int
|
||||||
|
* 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */
|
||||||
|
static void vol_apply(int pct, int target)
|
||||||
{
|
{
|
||||||
if (pct < 0) pct = 0;
|
if (pct < 0) pct = 0;
|
||||||
if (pct > 100) pct = 100;
|
if (pct > 100) pct = 100;
|
||||||
if (g_target == AUDIO_OUT_BT) {
|
if (target == AUDIO_OUT_BT) {
|
||||||
logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
|
logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1551,6 +1569,7 @@ static struct client *cli_alloc(int fd)
|
|||||||
freec->fd = fd;
|
freec->fd = fd;
|
||||||
freec->state = CLS_OPEN;
|
freec->state = CLS_OPEN;
|
||||||
freec->sid = 0;
|
freec->sid = 0;
|
||||||
|
freec->gen = ++g_cli_gen; /* 代际递增:音量异步 ACK 校验用,防槽复用误发 */
|
||||||
return freec;
|
return freec;
|
||||||
}
|
}
|
||||||
static void cli_free(struct client *c)
|
static void cli_free(struct client *c)
|
||||||
@@ -1771,13 +1790,20 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
int32_t pct = -1;
|
int32_t pct = -1;
|
||||||
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
||||||
if (pct >= 0) {
|
if (pct >= 0) {
|
||||||
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */
|
/* 异步化(事项三):只入队末值,**不在持 g_mu 下 popen amixer**——驱动 wedge 时
|
||||||
vol_apply((int)pct);
|
* 一次 set = 3×popen ≈ 4.5s,会把 OPEN/QUERY/喂流一起卡死。连续请求合并为末值
|
||||||
|
* (去抖),由 vol 线程应用+持久化后经 IO 线程回 ACK。 */
|
||||||
|
g_vol_pending = 1;
|
||||||
|
g_vol_pending_pct = (pct > 100) ? 100 : (int)pct;
|
||||||
|
g_vol_pending_cli = c;
|
||||||
|
g_vol_pending_req = req_id;
|
||||||
|
g_vol_pending_gen = c->gen;
|
||||||
|
pthread_cond_signal(&g_vol_cond);
|
||||||
} else {
|
} else {
|
||||||
g_vol_pct = settings_get_volume();
|
g_vol_pct = settings_get_volume(); /* 读当前:文件读非 popen,可同步 */
|
||||||
|
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
|
||||||
|
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
|
||||||
}
|
}
|
||||||
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
|
|
||||||
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSG_AUDIO_OUTPUT: {
|
case MSG_AUDIO_OUTPUT: {
|
||||||
@@ -1911,6 +1937,14 @@ static void *io_thread(void *arg)
|
|||||||
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
|
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
|
||||||
if (snaps[i].revents & POLLIN) cx_read_frames(c);
|
if (snaps[i].revents & POLLIN) cx_read_frames(c);
|
||||||
}
|
}
|
||||||
|
/* 音量 ACK 回写(事项三):vol 线程执行完只置标记,由 IO 线程统一发送——客户端 fd
|
||||||
|
* 生命周期归 IO 线程独占,跨线程直接 send_frame 有 fd 复用竞态。 */
|
||||||
|
for (int i = 0; i < CLI_MAX; i++) {
|
||||||
|
if (!g_cli[i].used || !g_cli[i].vol_ack) continue;
|
||||||
|
m_ack_t va; va.code = MEOW_OK; va.status = g_cli[i].vol_ack_status;
|
||||||
|
send_frame(g_cli[i].fd, MSG_ACK, g_cli[i].vol_ack_req, &va, sizeof va);
|
||||||
|
g_cli[i].vol_ack = 0;
|
||||||
|
}
|
||||||
pthread_mutex_unlock(&g_mu);
|
pthread_mutex_unlock(&g_mu);
|
||||||
/* 路由线程与播放线程独立;此处不再做 */
|
/* 路由线程与播放线程独立;此处不再做 */
|
||||||
}
|
}
|
||||||
@@ -1921,6 +1955,40 @@ static void *io_thread(void *arg)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ----
|
||||||
|
* 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界)
|
||||||
|
* + settings_set_volume(文件 IO),完成后只置 ACK 标记,由 IO 线程统一发送(防 fd 复用竞态)。
|
||||||
|
* BT 目标只记日志不动 DAC/不持久化(AVRCP 隔离,g_vol_pct 保持不变)。 */
|
||||||
|
static void *vol_thread(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
for (;;) {
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
while (!g_vol_pending && !g_stop)
|
||||||
|
cond_wait_tmo_cv(&g_vol_cond, COND_WAIT_MS);
|
||||||
|
if (g_stop) { pthread_mutex_unlock(&g_mu); break; }
|
||||||
|
int pct = g_vol_pending_pct;
|
||||||
|
int target = g_target; /* 持锁快照:vol_apply 不读全局 */
|
||||||
|
struct client *c = g_vol_pending_cli;
|
||||||
|
uint32_t req = g_vol_pending_req;
|
||||||
|
uint32_t cgen = g_vol_pending_gen;
|
||||||
|
g_vol_pending = 0;
|
||||||
|
g_vol_pending_cli = NULL;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
|
||||||
|
vol_apply(pct, target);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
if (c && c->used && c->gen == cgen) { /* 槽复用校验:只 ACK 原请求方 */
|
||||||
|
c->vol_ack = 1;
|
||||||
|
c->vol_ack_req = req;
|
||||||
|
c->vol_ack_status = (uint32_t)g_vol_pct;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 路由线程主体 ---- */
|
/* ---- 路由线程主体 ---- */
|
||||||
static void *route_thread(void *arg)
|
static void *route_thread(void *arg)
|
||||||
{
|
{
|
||||||
@@ -2149,19 +2217,21 @@ int main(int argc, char **argv)
|
|||||||
logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
|
logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
|
||||||
g_vol_pct, target_name(), g_bt_codec, volctl_name());
|
g_vol_pct, target_name(), g_bt_codec, volctl_name());
|
||||||
|
|
||||||
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
|
/* g_cond/g_vol_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
|
||||||
{
|
{
|
||||||
pthread_condattr_t ca;
|
pthread_condattr_t ca;
|
||||||
pthread_condattr_init(&ca);
|
pthread_condattr_init(&ca);
|
||||||
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
|
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
|
||||||
pthread_cond_init(&g_cond, &ca);
|
pthread_cond_init(&g_cond, &ca);
|
||||||
|
pthread_cond_init(&g_vol_cond, &ca);
|
||||||
pthread_condattr_destroy(&ca);
|
pthread_condattr_destroy(&ca);
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_t ti, tp, tr;
|
pthread_t ti, tp, tr, tv;
|
||||||
pthread_create(&ti, NULL, io_thread, NULL);
|
pthread_create(&ti, NULL, io_thread, NULL);
|
||||||
pthread_create(&tp, NULL, playback_thread, NULL);
|
pthread_create(&tp, NULL, playback_thread, NULL);
|
||||||
pthread_create(&tr, NULL, route_thread, NULL);
|
pthread_create(&tr, NULL, route_thread, NULL);
|
||||||
|
pthread_create(&tv, NULL, vol_thread, NULL);
|
||||||
|
|
||||||
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
|
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
|
||||||
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
|
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
|
||||||
@@ -2191,6 +2261,7 @@ int main(int argc, char **argv)
|
|||||||
pthread_join(ti, NULL);
|
pthread_join(ti, NULL);
|
||||||
pthread_join(tp, NULL);
|
pthread_join(tp, NULL);
|
||||||
pthread_join(tr, NULL);
|
pthread_join(tr, NULL);
|
||||||
|
pthread_join(tv, NULL);
|
||||||
unlink(g_sock);
|
unlink(g_sock);
|
||||||
if (g_logfd >= 0) close(g_logfd);
|
if (g_logfd >= 0) close(g_logfd);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -187,6 +187,24 @@ static int start_audiod_trace(const char *sink, const char *settings_path, const
|
|||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 带假 amixer 的启动(事项三异步化验证:假 amixer 每次 cset 睡 1s,模拟慢 popen)。
|
||||||
|
* 用于断言「MSG_AUDIO_VOLUME 在 IO 线程只入队、不放锁 popen」——慢 amixer 期间 QUERY 仍即时。 */
|
||||||
|
static int start_audiod_fake_amixer(const char *sink, const char *settings_path, const char *amixer_script)
|
||||||
|
{
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setenv("MEOW_AUDIO_SINK", sink, 1);
|
||||||
|
setenv("MEOW_AUDIO_OUT", TWAV, 1);
|
||||||
|
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
|
||||||
|
setenv("MEOW_AUDIO_FORCE", "1", 1);
|
||||||
|
if (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
|
||||||
|
if (amixer_script) setenv("MEOW_AUDIO_AMIXER", amixer_script, 1);
|
||||||
|
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
||||||
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
||||||
{
|
{
|
||||||
@@ -1080,6 +1098,62 @@ int main(void)
|
|||||||
}
|
}
|
||||||
stop_audiod();
|
stop_audiod();
|
||||||
|
|
||||||
|
/* ---- T17:MSG_AUDIO_VOLUME 异步化(事项三)。假 amixer 每次 cset 睡 1s(一次 set=3×cset≈3s)。
|
||||||
|
* 断言:慢 amixer 期间 QUERY 即时返回(dispatch 不放锁 popen);应用后回 ACK + 双组写 + 持久化。 */
|
||||||
|
printf("[T17 volume async (dispatch not blocked by slow amixer)]\n");
|
||||||
|
unlink(TSOCK); unlink(TWAV);
|
||||||
|
const char *tconf17 = "/tmp/meow_test_settings17.conf";
|
||||||
|
FILE *tf17 = fopen(tconf17, "w");
|
||||||
|
if (tf17) { fprintf(tf17, "# meow-settings 1\naudio.volume = 60\n"); fclose(tf17); }
|
||||||
|
const char *fake17 = "/tmp/meow_fake_amixer17.sh";
|
||||||
|
const char *trace17 = "/tmp/meow_fake_amixer17_trace.txt";
|
||||||
|
unlink(trace17);
|
||||||
|
{
|
||||||
|
FILE *fs = fopen(fake17, "w");
|
||||||
|
if (fs) {
|
||||||
|
fprintf(fs, "#!/bin/sh\necho \"cset $4 $5\" >> %s\nsleep 1\nexit 0\n", trace17);
|
||||||
|
fclose(fs);
|
||||||
|
chmod(fake17, 0755);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_audiod_pid = start_audiod_fake_amixer("file", tconf17, fake17);
|
||||||
|
usleep(400000);
|
||||||
|
{
|
||||||
|
struct cli v17;
|
||||||
|
check(cli_connect(&v17) == 0, "connect V17");
|
||||||
|
int32_t pct = 40;
|
||||||
|
check(sf(v17.fd, MSG_AUDIO_VOLUME, 17, &pct, sizeof pct) == 0, "send async volume 40");
|
||||||
|
/* 慢 amixer 执行期间 QUERY 必须即时(<600ms),证明 IO 线程没被 popen 卡住 */
|
||||||
|
struct timespec t0, t1;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||||
|
char q17[1024];
|
||||||
|
int qok = do_query(&v17, q17, sizeof q17);
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||||
|
long qms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
|
||||||
|
check(qok == 0 && qms < 600, "QUERY immediate during slow amixer (async dispatch)");
|
||||||
|
if (qok == 0 && qms >= 600) printf(" (query took %ldms)\n", qms);
|
||||||
|
/* 应用后回 ACK(末值 40) */
|
||||||
|
uint8_t ab[64]; uint16_t at;
|
||||||
|
int rlen = rf_tmo(v17.fd, 4000, &at, ab, sizeof ab);
|
||||||
|
check(rlen >= 0 && at == MSG_ACK, "async volume ACK returned after apply");
|
||||||
|
usleep(100000);
|
||||||
|
{
|
||||||
|
FILE *chk = fopen(tconf17, "r"); int has40 = 0; char l[128];
|
||||||
|
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||||||
|
check(has40, "async volume persisted = 40");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char trb[2048]; int trl = 0;
|
||||||
|
int fd = open(trace17, O_RDONLY);
|
||||||
|
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||||||
|
check(trl > 0 && strstr(trb, "numid=1 ") && strstr(trb, "numid=2 ") && strstr(trb, "numid=11 "),
|
||||||
|
"async dual-group write (numid=1/2/11) via fake amixer");
|
||||||
|
}
|
||||||
|
close(v17.fd);
|
||||||
|
}
|
||||||
|
stop_audiod();
|
||||||
|
unlink(fake17); unlink(trace17);
|
||||||
|
|
||||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
|||||||
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
|
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 \
|
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
|
||||||
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \
|
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \
|
||||||
font_theme.c keys.c util.c meow_dep.c \
|
font_theme.c keys.c util.c meow_dep.c key_router.c \
|
||||||
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
||||||
meow_bgmgr.c \
|
meow_bgmgr.c \
|
||||||
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/* key_router.c — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
|
||||||
|
*
|
||||||
|
* 本模块持有一个进程内唯一的音量键防抖/基线:
|
||||||
|
* kp_keys / kp_power(事件设备 poll 态,key_router_init 单开)
|
||||||
|
* kd_vold / kd_volu(40ms 防抖态,KEY_DEBOUNCE_MS)
|
||||||
|
* m_prev_vold / m_prev_volu(音量边沿基线)
|
||||||
|
* 各 launcher-UI 页每圈调 key_router_pump() —— 它只产「音量边沿 + 原始 held + ramp 状态」,
|
||||||
|
* 产出的边沿由「谁在跑循环」天然归属当前页;页据此决定 settings_volume_adj(±step) /
|
||||||
|
* 光标导航 / ramp 推进。这样 run_menu 的 meow_mode 分支(HibyOS overlay 音量=导航)不被破坏,
|
||||||
|
* 其余页(设置/通知中心/Now Playing/子页)用默认「音量=调音量」。
|
||||||
|
*
|
||||||
|
* 边界(§3 P1 / §4):
|
||||||
|
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸;launcher-UI
|
||||||
|
* 下 sink 恒空,此闸是「不改变行为」的保险)。
|
||||||
|
* - 播放键/电源键 P1 不路由(各页现状不动;播放键严禁 cmd=6)。
|
||||||
|
* - 锁屏(EVIOCGRAB)/App 会话层不调用本模块 —— 边界由「调用栈里当前循环是不是 launcher-UI 页」
|
||||||
|
* 自动强加。
|
||||||
|
*/
|
||||||
|
#include "meow_launcher_internal.h"
|
||||||
|
|
||||||
|
static int g_inited = 0;
|
||||||
|
|
||||||
|
/* 进程唯一事件设备 poll 态(key_router_init 单开;各页另自开 kp_* 仅供播放/电源键用)。 */
|
||||||
|
static struct keypoll kp_power, kp_keys;
|
||||||
|
|
||||||
|
/* 进程唯一音量键防抖态 + 边沿基线。 */
|
||||||
|
static struct kd_state kd_vold, kd_volu;
|
||||||
|
static int m_prev_vold = 0, m_prev_volu = 0;
|
||||||
|
|
||||||
|
/* 当前活动页哨兵(key_router_page_enter/exit 登记;与 sdk meow_ui_activity 交叉校验用)。 */
|
||||||
|
static int g_page_sentinel = 0;
|
||||||
|
|
||||||
|
|
||||||
|
void key_router_init(void)
|
||||||
|
{
|
||||||
|
if (g_inited) return;
|
||||||
|
g_inited = 1;
|
||||||
|
memset(&kp_power, 0, sizeof kp_power);
|
||||||
|
memset(&kp_keys, 0, sizeof kp_keys);
|
||||||
|
memset(&kd_vold, 0, sizeof kd_vold);
|
||||||
|
memset(&kd_volu, 0, sizeof kd_volu);
|
||||||
|
m_prev_vold = 0; m_prev_volu = 0;
|
||||||
|
kp_open(&kp_power, DEV_POWER);
|
||||||
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
|
/* 基线对齐当前 raw:进入时已按住的音量键不产假边沿。 */
|
||||||
|
key_router_rebase();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void key_router_rebase(void)
|
||||||
|
{
|
||||||
|
memset(&kd_vold, 0, sizeof kd_vold);
|
||||||
|
memset(&kd_volu, 0, sizeof kd_volu);
|
||||||
|
if (!g_inited) return;
|
||||||
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
|
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||||
|
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
key_evt_t key_router_pump(void)
|
||||||
|
{
|
||||||
|
key_evt_t ev;
|
||||||
|
memset(&ev, 0, sizeof ev);
|
||||||
|
if (!g_inited) key_router_init();
|
||||||
|
|
||||||
|
/* 设备晚就绪兜底:init 时 open 失败则此处每圈重试(open 失败快速返回,无阻塞)。 */
|
||||||
|
if (!kp_keys.ok) kp_open(&kp_keys, DEV_KEYS);
|
||||||
|
if (!kp_power.ok) kp_open(&kp_power, DEV_POWER);
|
||||||
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
|
|
||||||
|
int vold = 0, volu = 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;
|
||||||
|
}
|
||||||
|
/* 原始(未防抖)值:长按 ramp held 用(松开立即停止;同 run_menu/now_playing 现状)。 */
|
||||||
|
ev.vol_held_dn = vold;
|
||||||
|
ev.vol_held_up = volu;
|
||||||
|
(void)kd_tick(&kd_vold, vold);
|
||||||
|
(void)kd_tick(&kd_volu, volu);
|
||||||
|
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||||
|
ev.vol_st_dn = vold; ev.vol_st_up = volu;
|
||||||
|
|
||||||
|
if (meow_volpage_armed()) {
|
||||||
|
/* 防御闸:App 前台 volume_page 武装时(launcher-UI 下恒空)不放行音量边沿。
|
||||||
|
* 音量键在那态下由 app_launcher 的 meow_volpage_key 消费(翻页),不落系统调音。 */
|
||||||
|
ev.vol_dn_edge = 0;
|
||||||
|
ev.vol_up_edge = 0;
|
||||||
|
} else {
|
||||||
|
ev.vol_dn_edge = (vold && !m_prev_vold);
|
||||||
|
ev.vol_up_edge = (volu && !m_prev_volu);
|
||||||
|
}
|
||||||
|
ev.ramping = (vol_ramp_dir != 0);
|
||||||
|
|
||||||
|
m_prev_vold = vold;
|
||||||
|
m_prev_volu = volu;
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input)
|
||||||
|
{
|
||||||
|
if (ev->vol_dn_edge) {
|
||||||
|
settings_volume_adj(-settings_volume_step);
|
||||||
|
if (last_input) *last_input = now_ms();
|
||||||
|
logmsg("key_router: volume DOWN <- step=%d", settings_volume_step);
|
||||||
|
}
|
||||||
|
if (ev->vol_up_edge) {
|
||||||
|
settings_volume_adj(+settings_volume_step);
|
||||||
|
if (last_input) *last_input = now_ms();
|
||||||
|
logmsg("key_router: volume UP <- step=%d", settings_volume_step);
|
||||||
|
}
|
||||||
|
/* 在 settings_volume_adj 之后重查 vol_ramp_dir:同圈「起 ramp」即立刻迈第一步(同现状
|
||||||
|
* 各页「settings_volume_adj 后 if(vol_ramp_dir) vol_ramp_tick」),且长按续爬随每圈推进。 */
|
||||||
|
if (vol_ramp_dir != 0) {
|
||||||
|
int held = (vol_ramp_dir > 0) ? ev->vol_held_up : ev->vol_held_dn;
|
||||||
|
(void)vol_ramp_tick(held);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void key_router_page_enter(int sentinel)
|
||||||
|
{
|
||||||
|
g_page_sentinel = sentinel;
|
||||||
|
/* 与 sdk 活动栈交叉校验:哨兵非 0 且栈顶不一致 → 记录(不中止,防宿主/真机栈深度差异)。 */
|
||||||
|
if (sentinel) {
|
||||||
|
int act = meow_ui_activity_active();
|
||||||
|
if (act && act != sentinel)
|
||||||
|
logmsg("key_router: page_enter sentinel=%#x but active=%#x", sentinel, act);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void key_router_page_exit(int sentinel)
|
||||||
|
{
|
||||||
|
if (g_page_sentinel == sentinel) g_page_sentinel = 0;
|
||||||
|
/* 页退出即重播基线(§6 嵌套返回风险项:返回瞬间不产假边沿)。 */
|
||||||
|
key_router_rebase();
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* key_router.h — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
|
||||||
|
*
|
||||||
|
* 设计(devdocs/plan/KEY_ROUTER_UNIFY.md §2.5 方案 C / §3 P1):
|
||||||
|
* - 进程内**唯一**的 kd_*(防抖态)+ m_prev_*(边沿基线)+ kp_*(事件设备 poll 态),
|
||||||
|
* 把「读边沿 + 防抖 + 长按 held + ramp 推进」这层从 5 个会话(run_menu /
|
||||||
|
* run_settings_page / run_notif_center / run_now_playing / meow_subpage_run)
|
||||||
|
* 收敛到一处。
|
||||||
|
* - 每个 launcher-UI 页在其 for(;;) 每圈调一次 key_router_pump(),得到「防抖后的
|
||||||
|
* 音量边沿 + 原始 held + ramp 状态」;**真正的动作**(音量边沿 →
|
||||||
|
* settings_volume_adj(±step),还是光标导航)由调用页决定 —— 这样 run_menu 的
|
||||||
|
* meow_mode 分支(HibyOS overlay:音量=导航;MeowOS:音量=调音量)得以保留。
|
||||||
|
* - 非导航页可经 key_router_default_volume()(内含 settings_volume_adj + vol_ramp_tick +
|
||||||
|
* last_input 复位)直接用;run_menu 走自己的 meow_mode 分支。
|
||||||
|
*
|
||||||
|
* 范围与边界(§3 P1,勿越界):
|
||||||
|
* - 本模块只收敛**音量键**(KEY_VOLUMEDOWN/KEY_VOLUMEUP)。播放键/电源键 P1 不路由,
|
||||||
|
* 维持各页现状(播放键严禁改成 cmd=6;电源键单击/菜单/8s 状态机不动)。
|
||||||
|
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸)。
|
||||||
|
* - 锁屏层(EVIOCGRAB)/App 会话层从不调用 key_router_pump,天然退出本路由。
|
||||||
|
*/
|
||||||
|
#ifndef MEOW_KEY_ROUTER_H
|
||||||
|
#define MEOW_KEY_ROUTER_H
|
||||||
|
|
||||||
|
/* 一框音量边沿/held/ramp 输出。P1 只填音量;播放/电源字段后续期(P2/P3)再扩展。
|
||||||
|
* 字段语义:
|
||||||
|
* vol_dn_edge / vol_up_edge —— 防抖后**稳定按下边沿**(= stable ∧ ¬上一 stable;
|
||||||
|
* 1=本圈新按下)。这是「音量边沿」的唯一权威判定。
|
||||||
|
* vol_held_dn / vol_held_up —— **原始(未防抖)**按住态,ramp 长按 held 用
|
||||||
|
* (松开立即停止,同 run_menu/now_playing 现状)。
|
||||||
|
* vol_st_dn / vol_st_up —— **防抖稳定**按住态(wake_suppress「全键抬起」判定用)。
|
||||||
|
* ramping —— vol_ramp_dir != 0(线性渐变是否在跑;repaint 用)。
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
int vol_dn_edge;
|
||||||
|
int vol_up_edge;
|
||||||
|
int vol_held_dn;
|
||||||
|
int vol_held_up;
|
||||||
|
int vol_st_dn;
|
||||||
|
int vol_st_up;
|
||||||
|
int ramping;
|
||||||
|
} key_evt_t;
|
||||||
|
|
||||||
|
/* 进程级单开 kp_*(事件设备)+ 初始化防抖态/基线(幂等,可安全重复调用)。 */
|
||||||
|
void key_router_init(void);
|
||||||
|
|
||||||
|
/* 每页 for(;;) 每圈调用一次:poll 两设备 → 防抖音量键 → 产边沿/held/ramp。
|
||||||
|
* 不做 settings_volume_adj、不驱动 vol_ramp_tick、不复位 last_input —— 皆由调用页决定
|
||||||
|
* (见 key_router_default_volume 与各页自己的分支)。 */
|
||||||
|
key_evt_t key_router_pump(void);
|
||||||
|
|
||||||
|
/* 模态/菜单/嵌套会话返回/唤醒后重播基线:kd_* 清零 + m_prev_* 对齐当前 raw
|
||||||
|
* (对齐现状各页「memset(kd_*) + kp_poll + m_prev_*=当前 raw」的语义,防假边沿)。
|
||||||
|
* 各页在原本做该重播处调用。 */
|
||||||
|
void key_router_rebase(void);
|
||||||
|
|
||||||
|
/* 页进入/退出登记(记录当前哨兵;与 sdk meow_ui_activity_active 交叉校验)。 */
|
||||||
|
void key_router_page_enter(int sentinel);
|
||||||
|
void key_router_page_exit(int sentinel);
|
||||||
|
|
||||||
|
/* 默认「音量键=调音量」页回调(非导航页直接用):音量边沿 → settings_volume_adj(±step);
|
||||||
|
* 若有 ramp 在爬 → vol_ramp_tick(held);边沿 → 复位 *last_input。
|
||||||
|
* run_menu 不走本函数(自行 meow_mode 分支)。 */
|
||||||
|
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input);
|
||||||
|
|
||||||
|
#endif /* MEOW_KEY_ROUTER_H */
|
||||||
@@ -1214,10 +1214,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER);
|
kp_open(&kp_power, DEV_POWER);
|
||||||
kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */
|
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动。
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
* 音量键防抖/基线已上收 key_router(进程唯一);本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||||
|
key_router_rebase(); /* 进入主菜单:音量基线对齐当前 raw(防残留边沿) */
|
||||||
|
|
||||||
logmsg("menu open: napps=%d state=%d cursor=%d",
|
logmsg("menu open: napps=%d state=%d cursor=%d",
|
||||||
napps, state, cursor);
|
napps, state, cursor);
|
||||||
@@ -1226,10 +1227,8 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* when the menu opens (e.g. the hotkey release tail) do not fire
|
* when the menu opens (e.g. the hotkey release tail) do not fire
|
||||||
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
||||||
* to cur before we compare */
|
* to cur before we compare */
|
||||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
int m_prev_play = 0, m_prev_pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||||
}
|
}
|
||||||
@@ -1433,26 +1432,20 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* does not affect it) — the launcher must NOT touch brightness/panel
|
* does not affect it) — the launcher must NOT touch brightness/panel
|
||||||
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
||||||
* row (below) or sim-exit. */
|
* row (below) or sim-exit. */
|
||||||
|
/* 音量键走 key_router(进程唯一防抖/基线,产边沿/held/ramp);播放/电源键仍本页自理。 */
|
||||||
|
key_evt_t kev = key_router_pump();
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
int play = 0, pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||||
}
|
}
|
||||||
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
|
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
|
||||||
/* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */
|
|
||||||
int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */
|
|
||||||
(void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */
|
|
||||||
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
|
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
|
||||||
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
* 稳定态 play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
||||||
(void)kd_tick(&kd_vold, vold);
|
|
||||||
(void)kd_tick(&kd_volu, volu);
|
|
||||||
(void)kd_tick(&kd_play, play);
|
(void)kd_tick(&kd_play, play);
|
||||||
(void)kd_tick(&kd_pwr, pwr);
|
(void)kd_tick(&kd_pwr, pwr);
|
||||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
|
||||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
||||||
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
|
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
|
||||||
@@ -1497,15 +1490,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
|
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
|
||||||
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
||||||
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1530,15 +1521,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr = run_lock_screen();
|
int lr = run_lock_screen();
|
||||||
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
|
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
if (lr == 1)
|
if (lr == 1)
|
||||||
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
||||||
@@ -1567,11 +1556,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
|
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
|
||||||
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
||||||
if (meow_mode) {
|
if (meow_mode) {
|
||||||
if (vold && !m_prev_vold) {
|
if (kev.vol_dn_edge) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
||||||
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
||||||
} else if (volu && !m_prev_volu) {
|
} else if (kev.vol_up_edge) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
settings_volume_adj(+settings_volume_step);
|
settings_volume_adj(+settings_volume_step);
|
||||||
logmsg("volume UP <- ramp step=%d", settings_volume_step);
|
logmsg("volume UP <- ramp step=%d", settings_volume_step);
|
||||||
@@ -1579,22 +1568,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
} else {
|
} else {
|
||||||
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
||||||
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
|
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
|
||||||
if (vold && !m_prev_vold && napps > 0 && state == 0) {
|
if (kev.vol_dn_edge && napps > 0 && state == 0) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
cursor++;
|
cursor++;
|
||||||
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
||||||
logmsg("menu cursor -> %d", cursor);
|
logmsg("menu cursor -> %d", cursor);
|
||||||
}
|
}
|
||||||
if (volu && !m_prev_volu && napps > 0 && state == 0) {
|
if (kev.vol_up_edge && napps > 0 && state == 0) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
cursor--;
|
cursor--;
|
||||||
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
||||||
logmsg("menu cursor -> %d", cursor);
|
logmsg("menu cursor -> %d", cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */
|
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值)。
|
||||||
|
* 长按 raw held 取自 key_router(kev.vol_held_*,同现状 rvolu/rvold),松开立即停止。 */
|
||||||
if (meow_mode && vol_ramp_dir != 0) {
|
if (meow_mode && vol_ramp_dir != 0) {
|
||||||
int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */
|
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
|
||||||
int going = vol_ramp_tick(held);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
|
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
|
||||||
@@ -1618,7 +1608,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
logmsg("play key: media-reserved (no media source)");
|
logmsg("play key: media-reserved (no media source)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
|
m_prev_play = play;
|
||||||
|
|
||||||
/* touch (event reads) */
|
/* touch (event reads) */
|
||||||
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
|
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
|
||||||
@@ -1683,15 +1673,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||||
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
|
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
|
||||||
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
|
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(800);
|
||||||
@@ -1783,15 +1771,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||||
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
|
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
|
||||||
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(800);
|
||||||
@@ -1835,15 +1821,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||||||
run_now_playing(apps, napps);
|
run_now_playing(apps, napps);
|
||||||
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
|
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(800);
|
||||||
@@ -1858,15 +1842,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
logmsg("menu: touch topbar bg-manager button");
|
logmsg("menu: touch topbar bg-manager button");
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
|
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
|
||||||
run_bg_page(apps, napps);
|
run_bg_page(apps, napps);
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(800);
|
||||||
@@ -1898,15 +1880,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
logmsg("menu: touch settings row -> settings page");
|
logmsg("menu: touch settings row -> settings page");
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
|
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
|
||||||
run_settings_page();
|
run_settings_page();
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms(); /* 回主界面即视为输入 */
|
last_input_ms = now_ms(); /* 回主界面即视为输入 */
|
||||||
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
|
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
|
||||||
@@ -1975,15 +1955,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
/* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */
|
/* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */
|
||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr = run_lock_screen();
|
int lr = run_lock_screen();
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
if (lr == 1)
|
if (lr == 1)
|
||||||
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
||||||
@@ -2219,6 +2197,7 @@ int main(int argc, char **argv)
|
|||||||
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
|
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
|
||||||
* menu open so edits apply without a daemon restart) */
|
* menu open so edits apply without a daemon restart) */
|
||||||
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
|
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
|
||||||
|
key_router_init(); /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)单开 kp_*/
|
||||||
|
|
||||||
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
|
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
|
||||||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
|
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ extern "C" {
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
||||||
#include <pthread.h> /* M0:IPC 线程 */
|
#include <pthread.h> /* M0:IPC 线程 */
|
||||||
|
#include "key_router.h" /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1) */
|
||||||
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
||||||
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
||||||
#include <sys/un.h> /* M0:sockaddr_un */
|
#include <sys/un.h> /* M0:sockaddr_un */
|
||||||
@@ -847,6 +848,9 @@ int nm_wifi_saved(meow_net_t *out, int max);
|
|||||||
int nm_wifi_scan(meow_net_t *out, int max);
|
int nm_wifi_scan(meow_net_t *out, int max);
|
||||||
void nm_wifi_scan_trigger(void);
|
void nm_wifi_scan_trigger(void);
|
||||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
|
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
|
||||||
|
/* WiFi 设置页 bug2(2026-09-15):hidden=1 走 netmgrd connecth(scan_ssid=1 + 更长 COMPLETED 等待),
|
||||||
|
* 回退直连同步对齐;err 回填 netmgrd/直连的诚实结果文本(成功含 ip / 失败含原因)。 */
|
||||||
|
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen);
|
||||||
int nm_wifi_disconnect(char *err, int elen);
|
int nm_wifi_disconnect(char *err, int elen);
|
||||||
int nm_wifi_toggle(int on);
|
int nm_wifi_toggle(int on);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,14 @@
|
|||||||
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
||||||
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
|
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
|
||||||
|
|
||||||
|
/* IPC 读超时(ms)。netmgrd 单线程 poll,connect/connecth 会阻塞 ~10s/~15s 有界等待
|
||||||
|
* wpa_state=COMPLETED(bug1),scan 内含 1.5s 触发等待(P3)——客户端 per-command 超时需
|
||||||
|
* 覆盖这些同步等待,其余读级子命令维持 1.5s 不被卡死。 */
|
||||||
|
#define NM_TMO_DFL 1500
|
||||||
|
#define NM_TMO_SCAN 5000
|
||||||
|
#define NM_TMO_CONNECT 12000
|
||||||
|
#define NM_TMO_CONNECTH 17000
|
||||||
|
|
||||||
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
|
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
|
||||||
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
||||||
static time_t g_nm_ok_ts = 0;
|
static time_t g_nm_ok_ts = 0;
|
||||||
@@ -50,7 +58,7 @@ static int nm_r_all(int fd, uint8_t *b, size_t n)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
|
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
|
||||||
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype)
|
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
|
||||||
{
|
{
|
||||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||||
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
||||||
@@ -60,7 +68,7 @@ static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t
|
|||||||
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
|
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
|
||||||
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
||||||
struct pollfd pf = { fd, POLLIN, 0 };
|
struct pollfd pf = { fd, POLLIN, 0 };
|
||||||
int pr = poll(&pf, 1, 1500);
|
int pr = poll(&pf, 1, tmo_ms);
|
||||||
if (pr <= 0) return -1;
|
if (pr <= 0) return -1;
|
||||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||||
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
||||||
@@ -81,15 +89,16 @@ static int nm_hello(int fd)
|
|||||||
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
||||||
h.version = 1;
|
h.version = 1;
|
||||||
uint8_t rbuf[256]; uint16_t rtype = 0;
|
uint8_t rbuf[256]; uint16_t rtype = 0;
|
||||||
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype) < 0 || rtype != MSG_ACK)
|
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
|
||||||
return -1;
|
return -1;
|
||||||
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
||||||
return (ak.code == MEOW_OK) ? 0 : -1;
|
return (ak.code == MEOW_OK) ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。 */
|
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
|
||||||
|
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。 */
|
||||||
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
||||||
char *text, int tlen)
|
char *text, int tlen, int tmo_ms)
|
||||||
{
|
{
|
||||||
m_net_cmd_t q;
|
m_net_cmd_t q;
|
||||||
memset(&q, 0, sizeof q);
|
memset(&q, 0, sizeof q);
|
||||||
@@ -100,7 +109,7 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
|
|||||||
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
|
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
|
||||||
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
|
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
|
||||||
memset(rbuf, 0, sizeof rbuf);
|
memset(rbuf, 0, sizeof rbuf);
|
||||||
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype) < 0 || rtype != res_type) {
|
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
|
||||||
close(fd); nm_cache(0); return NETMGR_UNREACH;
|
close(fd); nm_cache(0); return NETMGR_UNREACH;
|
||||||
}
|
}
|
||||||
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
|
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
|
||||||
@@ -112,11 +121,14 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
|
|||||||
return (int)res.code;
|
return (int)res.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int netmgr_wifi_tmo(const char *cmd, const char *arg, char *text, int tlen, int tmo_ms)
|
||||||
|
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen, tmo_ms); }
|
||||||
|
|
||||||
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
|
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
|
||||||
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen); }
|
{ return netmgr_wifi_tmo(cmd, arg, text, tlen, NM_TMO_DFL); }
|
||||||
|
|
||||||
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
|
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
|
||||||
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen); }
|
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
|
||||||
|
|
||||||
int netmgr_ok(void)
|
int netmgr_ok(void)
|
||||||
{
|
{
|
||||||
@@ -361,7 +373,7 @@ int nm_wifi_saved(meow_net_t *out, int max)
|
|||||||
int nm_wifi_scan(meow_net_t *out, int max)
|
int nm_wifi_scan(meow_net_t *out, int max)
|
||||||
{
|
{
|
||||||
char text[MEOW_IPC_MAX - 32];
|
char text[MEOW_IPC_MAX - 32];
|
||||||
int rc = netmgr_wifi("scan", NULL, text, sizeof text); /* netmgrd 内含触发+有界等待+读结果 */
|
int rc = netmgr_wifi_tmo("scan", NULL, text, sizeof text, NM_TMO_SCAN); /* netmgrd 内含触发+有界等待+读结果 */
|
||||||
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
|
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
|
||||||
if (rc != MEOW_OK) return 0;
|
if (rc != MEOW_OK) return 0;
|
||||||
return nm_parse_scan(text, out, max);
|
return nm_parse_scan(text, out, max);
|
||||||
@@ -374,49 +386,133 @@ void nm_wifi_scan_trigger(void)
|
|||||||
if (!netmgr_ok()) net_wifi_scan_trigger();
|
if (!netmgr_ok()) net_wifi_scan_trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 直连回退:wpa_cli add/set/enable/select 新网络(复刻 netmgrd wifi_connect 逻辑)—— */
|
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
|
||||||
static int nm_direct_connect(const char *ssid, const char *psk, char *err, int elen)
|
* 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
|
||||||
|
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
|
||||||
|
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
|
||||||
|
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
||||||
|
|
||||||
|
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
|
||||||
{
|
{
|
||||||
const char *wpa = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
char cmd[384];
|
||||||
char cmd[256];
|
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
|
||||||
snprintf(cmd, sizeof cmd, "%s add_network 2>/dev/null", wpa);
|
char *o = popen_read_tmo(tmo, cmd);
|
||||||
char *o = popen_read_tmo(1200, cmd);
|
if (!o) { out[0] = 0; return -1; }
|
||||||
int id = o ? atoi(o) : -1;
|
int l = (int)strlen(o);
|
||||||
if (o) free(o);
|
if (l >= cap) l = cap - 1;
|
||||||
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
memcpy(out, o, (size_t)l); out[l] = 0;
|
||||||
{
|
free(o);
|
||||||
char *r;
|
return l;
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d ssid '\"%s\"' 2>/dev/null", wpa, id, ssid);
|
}
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
if (psk && psk[0]) {
|
static int nm_direct_save_config(void)
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d psk '\"%s\"' 2>/dev/null", wpa, id, psk);
|
{
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
char out[128];
|
||||||
} else {
|
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d key_mgmt NONE 2>/dev/null", wpa, id);
|
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
|
||||||
|
{
|
||||||
|
char out[1024]; char args[256];
|
||||||
|
int id = -1;
|
||||||
|
if (nm_direct_wpa(out, sizeof out, "list_networks", 1500) > 0) {
|
||||||
|
char *ln = strtok(out, "\n");
|
||||||
|
while (ln) {
|
||||||
|
char *tab0 = strchr(ln, '\t');
|
||||||
|
if (tab0) {
|
||||||
|
int this_id = atoi(ln);
|
||||||
|
char *ss = tab0 + 1;
|
||||||
|
char *tab1 = strchr(ss, '\t');
|
||||||
|
if (tab1) *tab1 = 0;
|
||||||
|
char clean[128], unesc[160], norm[128];
|
||||||
|
size_t cl = strlen(ss);
|
||||||
|
if (cl >= 2 && ss[0] == '"' && ss[cl - 1] == '"') {
|
||||||
|
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
|
||||||
|
} else {
|
||||||
|
snprintf(clean, sizeof clean, "%s", ss);
|
||||||
|
}
|
||||||
|
nm_decode_ssid(clean, unesc, sizeof unesc);
|
||||||
|
char *p = unesc; while (*p == ' ' || *p == '\t') p++;
|
||||||
|
snprintf(norm, sizeof norm, "%s", p);
|
||||||
|
size_t nl = strlen(norm);
|
||||||
|
while (nl > 0 && (norm[nl - 1] == ' ' || norm[nl - 1] == '\t')) norm[--nl] = 0;
|
||||||
|
if (strcmp(norm, ssid) == 0) { id = this_id; break; }
|
||||||
|
}
|
||||||
|
ln = strtok(NULL, "\n");
|
||||||
}
|
}
|
||||||
snprintf(cmd, sizeof cmd, "%s enable_network %d 2>/dev/null", wpa, id);
|
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
snprintf(cmd, sizeof cmd, "%s select_network %d 2>/dev/null", wpa, id);
|
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
}
|
}
|
||||||
if (err) snprintf(err, elen, "connect: ssid=%s network_id=%d", ssid, id);
|
int created = 0;
|
||||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s id=%d", ssid, id);
|
if (id < 0) {
|
||||||
|
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
|
||||||
|
if (err) snprintf(err, elen, "add_network 无 id");
|
||||||
|
return MEOW_ERR_INVAL;
|
||||||
|
}
|
||||||
|
id = atoi(out);
|
||||||
|
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
||||||
|
created = 1;
|
||||||
|
snprintf(args, sizeof args, "set_network %d ssid '\"%s\"'", id, ssid);
|
||||||
|
nm_direct_wpa(out, sizeof out, args, 1500);
|
||||||
|
if (psk && psk[0]) { snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||||
|
else { snprintf(args, sizeof args, "set_network %d key_mgmt NONE", id); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||||
|
} else if (psk && psk[0]) {
|
||||||
|
snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk);
|
||||||
|
nm_direct_wpa(out, sizeof out, args, 1500);
|
||||||
|
}
|
||||||
|
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||||
|
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||||
|
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||||
|
|
||||||
|
int wait_ms = hidden ? 15000 : 10000;
|
||||||
|
int waited = 0, ok = 0; char ip[40] = "";
|
||||||
|
for (;;) {
|
||||||
|
char st[512];
|
||||||
|
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
|
||||||
|
char *ws = strstr(st, "wpa_state=");
|
||||||
|
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
|
||||||
|
ok = 1;
|
||||||
|
char *ib = strstr(st, "ip_address=");
|
||||||
|
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
|
||||||
|
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
|
||||||
|
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waited >= wait_ms) break;
|
||||||
|
usleep(200000);
|
||||||
|
waited += 200;
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
|
||||||
|
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
|
||||||
|
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
|
||||||
|
return MEOW_ERR_NODEV;
|
||||||
|
}
|
||||||
|
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
|
||||||
|
int persisted = nm_direct_save_config();
|
||||||
|
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
|
||||||
|
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
|
||||||
return MEOW_OK;
|
return MEOW_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
|
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
|
||||||
{
|
{
|
||||||
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
|
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
|
||||||
char arg[132];
|
char arg[132];
|
||||||
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* connect 带空 psk → netmgrd 复用既有网络 */
|
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
|
||||||
char text[192];
|
char text[192];
|
||||||
int rc = netmgr_wifi("connect", arg, text, sizeof text);
|
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
|
||||||
if (rc == NETMGR_UNREACH) return nm_direct_connect(ssid, psk, err, elen);
|
hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
|
||||||
|
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
|
||||||
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
|
||||||
|
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
|
||||||
|
|
||||||
int nm_wifi_disconnect(char *err, int elen)
|
int nm_wifi_disconnect(char *err, int elen)
|
||||||
{
|
{
|
||||||
char text[128];
|
char text[128];
|
||||||
|
|||||||
@@ -1168,16 +1168,15 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
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;
|
int m_prev_play = 0, m_prev_pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||||
}
|
}
|
||||||
|
key_router_rebase(); /* 进入通知中心:音量基线对齐当前 raw(防残留边沿) */
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -1304,12 +1303,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
if (wak) {
|
if (wak) {
|
||||||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
key_router_rebase();
|
||||||
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||||||
@@ -1331,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
||||||
|
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
int play = 0, pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
if (wake_suppress) {
|
||||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||||
} else {
|
} else {
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||||
if (play && !m_prev_play) {
|
if (play && !m_prev_play) {
|
||||||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||||||
last_act = now;
|
last_act = now;
|
||||||
@@ -1366,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
show_power_menu();
|
show_power_menu();
|
||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
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);
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
key_router_rebase();
|
||||||
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1; need_paint = 1;
|
after_modal = 1; need_paint = 1;
|
||||||
goto ncp_keys_seeded;
|
goto ncp_keys_seeded;
|
||||||
@@ -1390,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vol_ramp_dir != 0) {
|
if (vol_ramp_dir != 0) {
|
||||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||||
int going = vol_ramp_tick(held);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
m_prev_play = play; m_prev_pwr = pwr;
|
||||||
ncp_keys_seeded:
|
ncp_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1562,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
||||||
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
||||||
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
||||||
|
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
||||||
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
||||||
|
|||||||
@@ -198,6 +198,54 @@ static void np_center_text(int y, const char *s, int size, uint32_t color, int m
|
|||||||
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
|
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
|
||||||
|
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
|
||||||
|
{
|
||||||
|
if (!s) s = "";
|
||||||
|
if (ui_width(s, size) <= maxw) {
|
||||||
|
ui_draw(x, y, s, color, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[160];
|
||||||
|
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
|
||||||
|
if (keep < 0) keep = 0;
|
||||||
|
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
|
||||||
|
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
|
||||||
|
strcat(buf, "...");
|
||||||
|
ui_draw(x, y, buf, color, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
|
||||||
|
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
|
||||||
|
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
|
||||||
|
static void np_draw_lyric_title(const meow_media_session_t *s)
|
||||||
|
{
|
||||||
|
if (!s) return;
|
||||||
|
if (!s->title[0] && !s->artist[0]) return;
|
||||||
|
int pad = 24;
|
||||||
|
int y = SB_BAR_H + 20;
|
||||||
|
int maxw = fb_xres - 2 * pad;
|
||||||
|
if (s->title[0])
|
||||||
|
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
|
||||||
|
if (s->artist[0])
|
||||||
|
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
|
||||||
|
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
|
||||||
|
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
|
||||||
|
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
|
||||||
|
static void np_set_lyric_ncm_colors(void)
|
||||||
|
{
|
||||||
|
if (!g_np_lyric) return;
|
||||||
|
meow_lyric_view_colors c;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
c.scrim = 0x000000;
|
||||||
|
c.cur = 0xFFFFFF;
|
||||||
|
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
|
||||||
|
meow_lyric_view_set_colors(g_np_lyric, &c);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================================================================
|
/* ==================================================================
|
||||||
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
||||||
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
||||||
@@ -346,28 +394,49 @@ static void np_draw_home_bar(void)
|
|||||||
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底,
|
/* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
|
||||||
* 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */
|
* 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
|
||||||
|
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
|
||||||
|
static void np_draw_lyric_cover_bg(int W, int H)
|
||||||
|
{
|
||||||
|
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
|
||||||
|
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
|
||||||
|
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
|
||||||
|
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
|
||||||
|
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
|
||||||
|
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
|
||||||
|
* lyric_view(自绘 scrim 关闭,仅歌词/空态)→ 顶部元信息(歌名/歌手)。 */
|
||||||
static void np_draw_lyric_page(int redraw_sb)
|
static void np_draw_lyric_page(int redraw_sb)
|
||||||
{
|
{
|
||||||
const meow_ui_theme_t *th = s_ui_theme();
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
int W = fb_xres, H = fb_yres;
|
int W = fb_xres, H = fb_yres;
|
||||||
if (!g_np_init) np_widgets_create();
|
if (!g_np_init) np_widgets_create();
|
||||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||||
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。
|
/* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
|
||||||
* 批6:有封面时以封面作整页背景(衬在 lyric_view 半透明 scrim 之下),取色只做附加;
|
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
|
||||||
* 无封面回退渐变/主题面。
|
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
|
||||||
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
||||||
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
||||||
np_cover_load(meow_media_current());
|
np_cover_load(meow_media_current());
|
||||||
np_cover_theme_update();
|
np_cover_theme_update();
|
||||||
np_apply_lyric_colors();
|
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
|
||||||
/* 用户 2026-09-04:歌词页背景统一黑色半透明叠加层(弃封面取色/封面图半透明底)——
|
* 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
|
||||||
* 部分高亮度封面对歌词显示不友好,黑底保证对比度。歌词行颜色/布局/滚动不变。 */
|
* (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
|
||||||
|
np_draw_lyric_cover_bg(W, H);
|
||||||
|
np_set_lyric_ncm_colors();
|
||||||
|
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
|
||||||
|
* 纯黑底之上(仍纯黑,即现状回退)。 */
|
||||||
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
|
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
|
||||||
np_lyric_refresh(meow_media_current());
|
np_lyric_refresh(meow_media_current());
|
||||||
if (g_np_lyric)
|
if (g_np_lyric)
|
||||||
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||||||
|
np_draw_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
|
||||||
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
|
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
|
||||||
if (redraw_sb) draw_statusbar();
|
if (redraw_sb) draw_statusbar();
|
||||||
}
|
}
|
||||||
@@ -486,6 +555,31 @@ static int64_t np_progress_seek(int tx, int ty)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
|
||||||
|
* - 音量键 = 系统音量(settings_volume_adj 内含插孔消抖 + 线性渐变);
|
||||||
|
* - 播放键 = 媒体播放/暂停切换(路由最近活跃媒体会话,与主界面同判);
|
||||||
|
* 边沿由调用方按防抖后稳定值计算(vold_edge = 稳定按下 ∧ 上一稳定为抬起)。
|
||||||
|
* 抽成独立小函数供宿主单测(np_apply_key_actions 仅依赖 settings_volume_adj/
|
||||||
|
* meow_media_playctl/now_ms/logmsg,可 -ffunction-sections 链接隔离)。 */
|
||||||
|
static void np_apply_key_actions(int vold_edge, int volu_edge, int play_edge,
|
||||||
|
const meow_media_session_t *s,
|
||||||
|
unsigned long long *last_input)
|
||||||
|
{
|
||||||
|
if (vold_edge) {
|
||||||
|
settings_volume_adj(-settings_volume_step);
|
||||||
|
if (last_input) *last_input = now_ms();
|
||||||
|
logmsg("np: volume DOWN <- ramp step=%d", settings_volume_step);
|
||||||
|
} else if (volu_edge) {
|
||||||
|
settings_volume_adj(+settings_volume_step);
|
||||||
|
if (last_input) *last_input = now_ms();
|
||||||
|
logmsg("np: volume UP <- ramp step=%d", settings_volume_step);
|
||||||
|
}
|
||||||
|
if (play_edge) {
|
||||||
|
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
|
||||||
|
if (last_input) *last_input = now_ms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void run_now_playing(struct app *apps, int napps)
|
void run_now_playing(struct app *apps, int napps)
|
||||||
{
|
{
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
@@ -495,15 +589,17 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
||||||
kp_open(&kp_power, DEV_POWER);
|
kp_open(&kp_power, DEV_POWER);
|
||||||
struct kd_state kd_play;
|
struct kd_state kd_play; /* 音量键防抖/基线上收 key_router(进程唯一) */
|
||||||
memset(&kd_play, 0, sizeof kd_play);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
/* 读播放键按住态(用于边沿) */
|
/* 读播放键按住态(用于边沿) */
|
||||||
int m_prev_play = 0;
|
int m_prev_play = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
|
}
|
||||||
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
|
||||||
|
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
|
||||||
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
|
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
|
||||||
|
|
||||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||||
@@ -521,6 +617,7 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
||||||
* owner==active 闸门——漏 push 则 active()!=0x51,
|
* owner==active 闸门——漏 push 则 active()!=0x51,
|
||||||
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
||||||
|
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
||||||
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
||||||
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
||||||
@@ -631,17 +728,30 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 播放键边沿 → 播放/暂停切换 */
|
/* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
|
||||||
|
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
|
||||||
|
* 与页无关)音量键无反应。 */
|
||||||
|
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
int play = 0;
|
int play = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (play && !m_prev_play) {
|
|
||||||
const meow_media_session_t *s = meow_media_current();
|
|
||||||
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
|
|
||||||
np_last_input = now_ms();
|
|
||||||
}
|
}
|
||||||
|
(void)kd_tick(&kd_play, play);
|
||||||
|
play = kd_play.stable;
|
||||||
|
/* 音量键/播放键边沿 → 动作(与主界面/子页同语义;见 np_apply_key_actions) */
|
||||||
|
np_apply_key_actions(kev.vol_dn_edge, kev.vol_up_edge,
|
||||||
|
play && !m_prev_play, meow_media_current(), &np_last_input);
|
||||||
m_prev_play = play;
|
m_prev_play = play;
|
||||||
|
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
|
||||||
|
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
|
||||||
|
if (vol_ramp_dir != 0) {
|
||||||
|
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
|
||||||
|
int going = vol_ramp_tick(held);
|
||||||
|
g_sys.vol = settings_volume_get();
|
||||||
|
paint_sb_right();
|
||||||
|
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
|
||||||
|
}
|
||||||
|
|
||||||
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
||||||
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
||||||
@@ -714,6 +824,7 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
usleep(10000);
|
usleep(10000);
|
||||||
}
|
}
|
||||||
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_keys);
|
kp_close(&kp_keys);
|
||||||
kp_close(&kp_power);
|
kp_close(&kp_power);
|
||||||
|
|||||||
@@ -1315,16 +1315,15 @@ void run_settings_page(void)
|
|||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
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;
|
int m_prev_play = 0, m_prev_pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||||
}
|
}
|
||||||
|
key_router_rebase(); /* 进入设置页:音量基线对齐当前 raw(防残留边沿) */
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -1358,6 +1357,7 @@ void run_settings_page(void)
|
|||||||
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
||||||
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
||||||
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
||||||
|
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
set_paint_page(sel);
|
set_paint_page(sel);
|
||||||
meow_dirty_full(); meow_ui_flush();
|
meow_dirty_full(); meow_ui_flush();
|
||||||
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
||||||
@@ -1396,13 +1396,12 @@ void run_settings_page(void)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
|
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
* 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||||
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
||||||
@@ -1423,28 +1422,25 @@ void run_settings_page(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 按键 ---- */
|
/* ---- 按键 ---- */
|
||||||
|
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
int play = 0, pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
if (wake_suppress) {
|
||||||
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
|
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
|
||||||
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
|
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
|
||||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||||
} else {
|
} else {
|
||||||
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
||||||
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
||||||
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||||
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
||||||
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
||||||
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
||||||
@@ -1480,14 +1476,11 @@ void run_settings_page(void)
|
|||||||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
||||||
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
||||||
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
memset(&kd_play, 0, sizeof kd_play);
|
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -1514,14 +1507,14 @@ void run_settings_page(void)
|
|||||||
}
|
}
|
||||||
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
||||||
if (vol_ramp_dir != 0) {
|
if (vol_ramp_dir != 0) {
|
||||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||||
int going = vol_ramp_tick(held);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
m_prev_play = play; m_prev_pwr = pwr;
|
||||||
settings_keys_seeded:
|
settings_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1658,14 +1651,10 @@ void run_settings_page(void)
|
|||||||
scroll_y0 = 0;
|
scroll_y0 = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
key_router_rebase(); /* 音量基线重播 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
||||||
@@ -1703,14 +1692,10 @@ void run_settings_page(void)
|
|||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
/* 重置设置页按键防抖基线,防残边 */
|
/* 重置设置页按键防抖基线,防残边 */
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
key_router_rebase(); /* 音量基线重播 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
||||||
@@ -1768,6 +1753,7 @@ void run_settings_page(void)
|
|||||||
done:
|
done:
|
||||||
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
||||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_power); kp_close(&kp_keys);
|
kp_close(&kp_power); kp_close(&kp_keys);
|
||||||
logmsg("settings page closed");
|
logmsg("settings page closed");
|
||||||
|
|||||||
@@ -247,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now;
|
last_act = now;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
key_router_rebase();
|
||||||
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
||||||
@@ -302,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||||
pwr_suppress_click = 1;
|
pwr_suppress_click = 1;
|
||||||
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -456,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
scroll_base = 0; scroll_y0 = 0;
|
scroll_base = 0; scroll_y0 = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
|
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -746,16 +741,15 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
{
|
{
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
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;
|
int m_prev_play = 0, m_prev_pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||||
}
|
}
|
||||||
|
key_router_rebase(); /* 进入子页:音量基线对齐当前 raw(防残留边沿) */
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -783,6 +777,7 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
sp_configure_page(p, top_sw_en, top_on);
|
sp_configure_page(p, top_sw_en, top_on);
|
||||||
|
|
||||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
||||||
|
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
sp_paint_page();
|
sp_paint_page();
|
||||||
meow_dirty_full(); meow_ui_flush();
|
meow_dirty_full(); meow_ui_flush();
|
||||||
|
|
||||||
@@ -817,12 +812,11 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now;
|
last_act = now;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
key_router_rebase();
|
||||||
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
|
||||||
@@ -835,23 +829,20 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
|
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
|
||||||
|
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
int play = 0, pwr = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
if (wake_suppress) {
|
||||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||||
} else {
|
} else {
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||||
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
|
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
|
||||||
if (pwr && !m_prev_pwr) {
|
if (pwr && !m_prev_pwr) {
|
||||||
pwr_hold_at = now; pwr_menu_state = 0;
|
pwr_hold_at = now; pwr_menu_state = 0;
|
||||||
@@ -870,14 +861,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||||
pwr_suppress_click = 1;
|
pwr_suppress_click = 1;
|
||||||
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -902,13 +891,13 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vol_ramp_dir != 0) {
|
if (vol_ramp_dir != 0) {
|
||||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||||
(void)vol_ramp_tick(held);
|
(void)vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
m_prev_play = play; m_prev_pwr = pwr;
|
||||||
sub_keys_seeded:
|
sub_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1024,14 +1013,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
drag_last_y = 0;
|
drag_last_y = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
|
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -1090,6 +1077,7 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
subdone:
|
subdone:
|
||||||
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
||||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_power); kp_close(&kp_keys);
|
kp_close(&kp_power); kp_close(&kp_keys);
|
||||||
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/* test_key_router_vol.c — 宿主单测:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)。
|
||||||
|
*
|
||||||
|
* 测「key_router 自身的音量编排」——它只产「音量边沿 + 原始 held + ramp 状态」,
|
||||||
|
* 由调用页决定 settings_volume_adj(±step) / 光标导航 / ramp 推进。本测用内存桩:
|
||||||
|
* - kp_poll/kp_open/kp_is_down → 注入 raw 键位矩阵到 key_router 的静态 kp_keys.cur[];
|
||||||
|
* - kd_tick → 用 keys.c 的真实现(40ms 防抖,V4 依赖);keys.c 内 kp_open/kp_poll/
|
||||||
|
* kp_is_down/meow_volpage_armed 用宏重命名避开真实 ioctl 与桩冲突;
|
||||||
|
* - settings_volume_adj/settings_volume_step/vol_ramp_dir/vol_ramp_tick → 内存桩;
|
||||||
|
* - now_ms → 假时钟(手动推进,40ms 防抖可瞬时测);
|
||||||
|
* - meow_ui_activity_* → 内存桩(V5 嵌套:真语义=纯静态数组,桩复刻)。
|
||||||
|
*
|
||||||
|
* 断言:
|
||||||
|
* V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位;
|
||||||
|
* V2 同会话重复(同 raw 仍按住)→ 不再触发 settings_volume_adj;
|
||||||
|
* V3 长按 held → vol_ramp_tick 被驱动(vol_ramp_dir!=0,held=正确方向 raw);松开 → dir=0;
|
||||||
|
* V4 40ms 防抖:raw 在 40ms 内抖动 → 不产边沿;稳定按下 ≥40ms → 才产边沿;
|
||||||
|
* V5 嵌套返回:page_enter(0x53)→page_exit(0x53)→page_enter(0x55),基线重播后 held 键不产假边沿;
|
||||||
|
* V6 meow_volpage_armed() → 音量边沿被抑制(不下发,不调 settings_volume_adj)。
|
||||||
|
*
|
||||||
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
|
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
|
||||||
|
* -Wl,--gc-sections test_key_router_vol.c -o test_key_router_vol
|
||||||
|
* ./test_key_router_vol
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ---- 假时钟 ---- */
|
||||||
|
static unsigned long long g_now = 100000;
|
||||||
|
unsigned long long now_ms(void) { return g_now; }
|
||||||
|
static void advance_ms(unsigned long long d) { g_now += d; }
|
||||||
|
|
||||||
|
/* ---- 音量机外部符号桩(key_router 依赖)---- */
|
||||||
|
int settings_volume_step = 5;
|
||||||
|
static int g_adj_count = 0, g_adj_delta = 0;
|
||||||
|
int vol_ramp_dir = 0; /* 桩全局:模拟 audio.c 的 ramp 状态 */
|
||||||
|
static int g_tick_count = 0, g_tick_held = -1;
|
||||||
|
|
||||||
|
void settings_volume_adj(int delta)
|
||||||
|
{
|
||||||
|
g_adj_count++; g_adj_delta = delta;
|
||||||
|
if (delta > 0) vol_ramp_dir = 1; else if (delta < 0) vol_ramp_dir = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vol_ramp_tick(int key_held)
|
||||||
|
{
|
||||||
|
g_tick_count++; g_tick_held = key_held;
|
||||||
|
if (!key_held) vol_ramp_dir = 0;
|
||||||
|
return vol_ramp_dir != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_volpage = 0; /* 桩:meow_volpage_armed */
|
||||||
|
|
||||||
|
/* ---- keys.c 内 kp_open/kp_poll/kp_is_down/meow_volpage_armed 用宏重命名,避开真实 ioctl ----
|
||||||
|
* (宏在 #include keys.c 前生效,仅重命名 keys.c 体内的调用/定义) ---- */
|
||||||
|
#define kp_open kp_open_rt_
|
||||||
|
#define kp_poll kp_poll_rt_
|
||||||
|
#define kp_is_down kp_is_down_rt_
|
||||||
|
#define meow_volpage_armed meow_volpage_armed_rt_
|
||||||
|
#include "keys.c" /* 真 kd_tick(40ms 防抖素材) */
|
||||||
|
#undef kp_open
|
||||||
|
#undef kp_poll
|
||||||
|
#undef kp_is_down
|
||||||
|
#undef meow_volpage_armed
|
||||||
|
|
||||||
|
/* host 桩声明(key_router.c 需要;恢复到正常名,与 keys.c 被更名后的定义不冲突) */
|
||||||
|
struct keypoll;
|
||||||
|
int kp_open(struct keypoll *kp, const char *dev);
|
||||||
|
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx), void *ctx);
|
||||||
|
int kp_is_down(const struct keypoll *k, int code);
|
||||||
|
int meow_volpage_armed(void);
|
||||||
|
|
||||||
|
#include "key_router.c" /* 同 TU #include:拿到静态 kp_keys/kp_power/kd_vold/kd_volu + 全部函数 */
|
||||||
|
#include "key_router.h"
|
||||||
|
|
||||||
|
/* ---- meow_ui_activity_* 桩(复刻 sdk/meow_ui_activity.c:纯静态数组,真语义)---- */
|
||||||
|
static int g_act[32]; static int g_act_n = 0;
|
||||||
|
void meow_ui_activity_push(int id) { if (g_act_n < 32) g_act[g_act_n++] = id; }
|
||||||
|
void meow_ui_activity_pop(void) { if (g_act_n > 0) g_act_n--; }
|
||||||
|
int meow_ui_activity_active(void) { return g_act_n > 0 ? g_act[g_act_n - 1] : 0; }
|
||||||
|
|
||||||
|
/* ---- 注入 raw 到 key_router 静态 kp_keys ---- */
|
||||||
|
static void set_vol(int vold, int volu)
|
||||||
|
{
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++) kp_keys.cur[w] = 0;
|
||||||
|
if (vold) kp_keys.cur[KEY_VOLUMEDOWN / 32] |= (1UL << (KEY_VOLUMEDOWN % 32));
|
||||||
|
if (volu) kp_keys.cur[KEY_VOLUMEUP / 32] |= (1UL << (KEY_VOLUMEUP % 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- host 桩:kp_*(prev=cur 模拟真实 poll 的键位更新)+ meow_volpage_armed ---- */
|
||||||
|
int kp_open(struct keypoll *kp, const char *dev)
|
||||||
|
{ (void)dev; memset(kp, 0, sizeof *kp); kp->ok = 1; return 0; }
|
||||||
|
int kp_poll(struct keypoll *kp, int (*cb)(int, int, void *), void *ctx)
|
||||||
|
{
|
||||||
|
(void)cb; (void)ctx;
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++) kp->prev[w] = kp->cur[w];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int kp_is_down(const struct keypoll *k, int code)
|
||||||
|
{ return (k->cur[code / 32] >> (code % 32)) & 1; }
|
||||||
|
int meow_volpage_armed(void) { return g_volpage; }
|
||||||
|
|
||||||
|
/* 其它全局桩(keys.c 内被更名函数引用;本测 GC 后不保留,仅满足声明一致性) */
|
||||||
|
uint32_t menu_bg = 0;
|
||||||
|
int screen_off_timeout = 0, screen_off_mode = 0, power_longpress_menu_ms = 0;
|
||||||
|
int power_forceoff_ms = 0, refresh_rate_cfg = 60, refresh_always_max = 0;
|
||||||
|
|
||||||
|
void logmsg(const char *fmt, ...) { (void)fmt; }
|
||||||
|
|
||||||
|
static int g_fails = 0;
|
||||||
|
#define CHECK(cond, name) do { \
|
||||||
|
if (cond) printf(" PASS %s\n", name); \
|
||||||
|
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
|
||||||
|
|
||||||
|
static void reset_router(void)
|
||||||
|
{
|
||||||
|
set_vol(0, 0);
|
||||||
|
key_router_rebase();
|
||||||
|
g_adj_count = 0; g_adj_delta = 0; g_tick_count = 0; g_tick_held = -1;
|
||||||
|
vol_ramp_dir = 0; g_volpage = 0;
|
||||||
|
g_act_n = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按下并稳定:register(pump) → hold ≥40ms → pump(stable=1, 边沿)。 */
|
||||||
|
static key_evt_t press_settle(int vold, int volu)
|
||||||
|
{
|
||||||
|
set_vol(vold, volu);
|
||||||
|
key_router_pump(); /* 登记 raw 跳变(防抖窗口起点) */
|
||||||
|
advance_ms(50); /* ≥KEY_DEBOUNCE_MS(40) */
|
||||||
|
return key_router_pump(); /* stable→1,边沿 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 抬起并稳定:register(pump) → → pump(stable=0)。 */
|
||||||
|
static key_evt_t release_settle(void)
|
||||||
|
{
|
||||||
|
set_vol(0, 0);
|
||||||
|
key_router_pump(); /* 登记 release */
|
||||||
|
advance_ms(50);
|
||||||
|
return key_router_pump(); /* stable→0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
unsigned long long last_input = 0;
|
||||||
|
|
||||||
|
/* ---- V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump(); /* 全抬起基线 */
|
||||||
|
last_input = 0;
|
||||||
|
key_evt_t ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V1 vol-down edge fires");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
|
||||||
|
"V1 vol-down -> settings_volume_adj(-step) exactly once");
|
||||||
|
CHECK(last_input == g_now, "V1 last_input reset on edge");
|
||||||
|
|
||||||
|
/* ---- V2 同会话重复(同 raw 仍按住)→ 不再触发 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V2a press edge fires");
|
||||||
|
ev = key_router_pump(); /* 同 raw 仍按住,无新边沿 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V2 held: no re-trigger edge");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 0, "V2 held: no double settings_volume_adj");
|
||||||
|
|
||||||
|
/* ---- V3 长按 held → vol_ramp_tick 被驱动(held=正确方向 raw);松开 → dir=0 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump(); last_input = 0;
|
||||||
|
ev = press_settle(1, 0); /* 按下(vol down)并转稳定 */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_tick_count == 1 && g_tick_held == ev.vol_held_dn && vol_ramp_dir != 0,
|
||||||
|
"V3 hold vol-down -> ramp driven with held=raw down");
|
||||||
|
advance_ms(50);
|
||||||
|
ev = key_router_pump(); /* 继续长按(raw=1 仍按住) */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_tick_count == 2 && g_tick_held == 1, "V3 long-press continues ramp");
|
||||||
|
ev = release_settle(); /* 松开:raw held 立即为 0 */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(vol_ramp_dir == 0, "V3 release -> ramp dir 0 (stops)");
|
||||||
|
|
||||||
|
/* ---- V4 40ms 防抖:raw 在 40ms 内抖动不产边沿;稳定 ≥40ms 才产 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
set_vol(1, 0); advance_ms(20); key_router_pump(); /* 按下未满 40ms */
|
||||||
|
set_vol(0, 0); advance_ms(20); key_router_pump(); /* 40ms 内又抬起 */
|
||||||
|
ev = key_router_pump();
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V4 <40ms jitter -> no edge");
|
||||||
|
ev = press_settle(1, 0); /* 稳定按下 ≥40ms */
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V4 stable >=40ms press -> edge");
|
||||||
|
ev = release_settle(); /* 抬起稳定 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0 && ev.vol_up_edge == 0, "V4 released -> no residual edge");
|
||||||
|
ev = press_settle(0, 1); /* 稳定按音量加 */
|
||||||
|
CHECK(ev.vol_up_edge == 1, "V4 vol-up stable press -> up edge");
|
||||||
|
|
||||||
|
/* ---- V5 嵌套返回:page_enter/exit 重播基线,held 键不产假边沿 ---- */
|
||||||
|
reset_router();
|
||||||
|
meow_ui_activity_push(0x55); key_router_page_enter(0x55); /* 主菜单 */
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V5a press edge in menu");
|
||||||
|
/* 嵌套进设置页:压栈 + 登记 */
|
||||||
|
meow_ui_activity_push(0x53); key_router_page_enter(0x53);
|
||||||
|
CHECK(meow_ui_activity_active() == 0x53, "V5b settings active sentinel");
|
||||||
|
ev = release_settle(); /* 设置页内把键抬起 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V5c settings-up: no fresh edge (consumer)");
|
||||||
|
/* 设置页返回(模拟 run_settings_page 结束):弹栈 + 重播基线 → m_prev=当前 raw(0) */
|
||||||
|
key_router_page_exit(0x53); meow_ui_activity_pop();
|
||||||
|
key_router_page_enter(0x55);
|
||||||
|
CHECK(meow_ui_activity_active() == 0x55, "V5d menu active again");
|
||||||
|
/* 主菜单基线已重播;再次按下应为新边沿,而非残留/双发 */
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V5e fresh press after nest return -> single edge");
|
||||||
|
|
||||||
|
/* ---- V6 meow_volpage_armed() → 音量边沿被抑制(不下发)---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
g_volpage = 1;
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V6 volpage armed -> edge suppressed");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 0, "V6 volpage armed -> settings_volume_adj NOT called");
|
||||||
|
g_volpage = 0;
|
||||||
|
|
||||||
|
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/* test_np_keys.c — 宿主单测:Now Playing 播放页/歌词页物理键分发(Task C)。
|
||||||
|
*
|
||||||
|
* 背景(真机现象):桌面播放页与歌词页上,音量键 + 播放键「完全无反应」。
|
||||||
|
* 根因(now_playing.c):run_now_playing() 原先只接播放键(164)与电源键,未接音量键
|
||||||
|
* (114/115),导致播放页/歌词页(同一循环,与页无关)调不了音量。
|
||||||
|
*
|
||||||
|
* 本测复现 run_now_playing() 的按键动作应用(np_apply_key_actions):音量键走
|
||||||
|
* settings_volume_adj(±step)、播放键走 meow_media_playctl(按会话当前态 1=播放 2=暂停)。
|
||||||
|
* 断言:
|
||||||
|
* K1 音量减边沿 → settings_volume_adj(-step);
|
||||||
|
* K2 音量加边沿 → settings_volume_adj(+step);
|
||||||
|
* K3 播放键边沿 + 会话正在播放 → meow_media_playctl(2,0)(暂停);
|
||||||
|
* K4 播放键边沿 + 会话已暂停 → meow_media_playctl(1,0)(播放);
|
||||||
|
* K5 播放键边沿 + 无会话 → 不下发(空态不误报);
|
||||||
|
* K6 无任何边沿 → 两个回调都不触发;
|
||||||
|
* K7 音量/播放边沿都会更新 last_input(自动息屏计时复位)。
|
||||||
|
*
|
||||||
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
|
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
|
||||||
|
* -Wl,--gc-sections -DMEOW_NP_KEYS_TEST \
|
||||||
|
* test_np_keys.c -o test_np_keys
|
||||||
|
* ./test_np_keys
|
||||||
|
* 说明:now_playing.c 用 #include 同 TU 平移(惯例同 test_vol_ramp_jack);-ffunction-sections
|
||||||
|
* + --gc-sections 只保留 np_apply_key_actions 及其依赖,其余(渲染/控件/绘制)被回收,
|
||||||
|
* 故仅需桩 settings_volume_adj/settings_volume_step/meow_media_playctl/now_ms/logmsg。
|
||||||
|
*/
|
||||||
|
#include "now_playing.c" /* 同 TU #include:拿到 static np_apply_key_actions */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int g_fails = 0;
|
||||||
|
#define CHECK(cond, name) do { \
|
||||||
|
if (cond) printf(" PASS %s\n", name); \
|
||||||
|
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
|
||||||
|
|
||||||
|
/* ---- 桩:np_apply_key_actions 依赖的 launcher 外部符号 ---- */
|
||||||
|
int settings_volume_step = 5;
|
||||||
|
static int g_adj_count = 0, g_adj_delta = 0;
|
||||||
|
void settings_volume_adj(int delta) { g_adj_count++; g_adj_delta = delta; }
|
||||||
|
|
||||||
|
static int g_playctl_count = 0;
|
||||||
|
static uint32_t g_playctl_cmd = 0;
|
||||||
|
static int64_t g_playctl_seek = 0;
|
||||||
|
int meow_media_playctl(uint32_t cmd, int64_t seek_ms)
|
||||||
|
{ g_playctl_count++; g_playctl_cmd = cmd; g_playctl_seek = seek_ms; return 0; }
|
||||||
|
|
||||||
|
static unsigned long long g_now = 100000;
|
||||||
|
unsigned long long now_ms(void) { return g_now; }
|
||||||
|
|
||||||
|
void logmsg(const char *fmt, ...) { (void)fmt; }
|
||||||
|
|
||||||
|
/* 会话(meow_media_session_t 来自 meow_media.h,含 state 字段) */
|
||||||
|
static meow_media_session_t g_s;
|
||||||
|
|
||||||
|
static void reset_callbacks(void)
|
||||||
|
{
|
||||||
|
g_adj_count = 0; g_adj_delta = 0;
|
||||||
|
g_playctl_count = 0; g_playctl_cmd = 0; g_playctl_seek = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memset(&g_s, 0, sizeof g_s);
|
||||||
|
unsigned long long last_input = 0;
|
||||||
|
|
||||||
|
/* K1 音量减边沿 → settings_volume_adj(-step) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100000;
|
||||||
|
np_apply_key_actions(1, 0, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
|
||||||
|
"K1 vol-down edge -> settings_volume_adj(-step)");
|
||||||
|
CHECK(last_input == g_now, "K7a vol-down updates last_input");
|
||||||
|
|
||||||
|
/* K2 音量加边沿 → settings_volume_adj(+step) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100050;
|
||||||
|
np_apply_key_actions(0, 1, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == +settings_volume_step,
|
||||||
|
"K2 vol-up edge -> settings_volume_adj(+step)");
|
||||||
|
CHECK(last_input == g_now, "K7b vol-up updates last_input");
|
||||||
|
|
||||||
|
/* K3 播放键边沿 + 正在播放 → MSG_PLAYCTL(2,0) 暂停 */
|
||||||
|
g_s.state = MEOW_MEDIA_PLAYING;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_playctl_count == 1 && g_playctl_cmd == 2 && g_playctl_seek == 0,
|
||||||
|
"K3 play edge (playing) -> playctl pause(2)");
|
||||||
|
|
||||||
|
/* K4 播放键边沿 + 已暂停 → MSG_PLAYCTL(1,0) 播放 */
|
||||||
|
g_s.state = MEOW_MEDIA_PAUSED;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_playctl_count == 1 && g_playctl_cmd == 1 && g_playctl_seek == 0,
|
||||||
|
"K4 play edge (paused) -> playctl play(1)");
|
||||||
|
|
||||||
|
/* K5 播放键边沿 + 无会话 → 不下发(并仍复位 last_input) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100100;
|
||||||
|
np_apply_key_actions(0, 0, 1, NULL, &last_input);
|
||||||
|
CHECK(g_playctl_count == 0, "K5 play edge (no session) -> no playctl");
|
||||||
|
CHECK(last_input == g_now, "K7c play edge updates last_input (no session)");
|
||||||
|
|
||||||
|
/* K6 无任何边沿 → 两个回调都不触发 */
|
||||||
|
reset_callbacks(); last_input = 0;
|
||||||
|
np_apply_key_actions(0, 0, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 0 && g_playctl_count == 0, "K6 no edge -> no callbacks");
|
||||||
|
|
||||||
|
/* K7 音量递减时播放键不同时触发(音量边沿优先,播放单独判) */
|
||||||
|
g_s.state = MEOW_MEDIA_PLAYING;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(1, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_playctl_count == 1,
|
||||||
|
"K8 vol-down + play_edge both fire (independent)");
|
||||||
|
|
||||||
|
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -186,23 +186,29 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
|||||||
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
|
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
|
||||||
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
|
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
|
||||||
|
|
||||||
|
/* MD3 容器化(P0):行=surface 上 surfaceContainer 圆角卡;段头=surface 上的 primary 段标签(无卡
|
||||||
|
* 无分隔线);与 settings_page.c set_lv_draw 同构造。 */
|
||||||
|
int cx = x + SET_CARD_X;
|
||||||
|
int cw = w - 2 * SET_CARD_X;
|
||||||
|
int cy = y + 8 / 2;
|
||||||
|
int ch = SET_ITEM_H - 8;
|
||||||
if (r->ctrl == SC_HEADER) {
|
if (r->ctrl == SC_HEADER) {
|
||||||
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
|
|
||||||
int ty = y + (SET_ITEM_H - thh) / 2;
|
int ty = y + (SET_ITEM_H - thh) / 2;
|
||||||
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
|
if (ty >= 0) meow_ui_text(cx + 16, ty, r->label, mt->font_body, mt->primary);
|
||||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
|
meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->surfaceContainer);
|
||||||
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
|
if (sel) meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->primaryContainer);
|
||||||
|
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->primary, 26);
|
||||||
int ly = y + (SET_ITEM_H - thh) / 2;
|
int ly = y + (SET_ITEM_H - thh) / 2;
|
||||||
|
int rgt = cx + cw - 16;
|
||||||
switch (r->ctrl) {
|
switch (r->ctrl) {
|
||||||
case SC_SWITCH: {
|
case SC_SWITCH: {
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
meow_widget *sw = set_sw_take();
|
meow_widget *sw = set_sw_take();
|
||||||
if (sw) {
|
if (sw) {
|
||||||
int on = 0; meow_settings_get_switch(r->key, &on);
|
int on = 0; meow_settings_get_switch(r->key, &on);
|
||||||
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
|
meow_widget_set_rect(sw, rgt - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
|
||||||
meow_switch_set(sw, on);
|
meow_switch_set(sw, on);
|
||||||
meow_widget_draw(sw);
|
meow_widget_draw(sw);
|
||||||
}
|
}
|
||||||
@@ -211,26 +217,25 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
|||||||
case SC_SLIDER: {
|
case SC_SLIDER: {
|
||||||
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
|
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
|
||||||
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
|
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
||||||
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
|
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SC_LIST: {
|
case SC_LIST: {
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
meow_ui_text(x + w - 16 - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
|
meow_ui_text(rgt - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: { /* SC_ENUM / SC_PRESET */
|
default: { /* SC_ENUM / SC_PRESET */
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
char vb[40]; s_display_value(r, vb, sizeof vb);
|
char vb[40]; s_display_value(r, vb, sizeof vb);
|
||||||
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body);
|
int vx = rgt - meow_ui_text_width(vb, mt->font_body);
|
||||||
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->onSurface, 90);
|
||||||
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void mk_widgets(void)
|
static void mk_widgets(void)
|
||||||
@@ -247,7 +252,7 @@ static void draw_all(void)
|
|||||||
{
|
{
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
|
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
|
||||||
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surfaceVariant);
|
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surface); /* P0:内容底 surface(卡在其上抬升) */
|
||||||
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
|
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,16 +310,17 @@ int main(void)
|
|||||||
meow_ui_activity_push(SET_SENTINEL);
|
meow_ui_activity_push(SET_SENTINEL);
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
draw_all();
|
draw_all();
|
||||||
/* 段头行整体填 surfaceContainerHigh(覆盖列表底 surfaceVariant)→ 对列表底背景即非背景;
|
/* MD3 容器化(P0):段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
|
||||||
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「段头底 vs 列表底」判渲染而非文字像素。 */
|
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。
|
||||||
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「卡/段头 vs 列表底」判渲染而非文字像素。 */
|
||||||
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||||
CHECK(hdrcnt > 0, "section header band renders (hdr=%d)", hdrcnt);
|
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||||
CHECK(rowcnt > 40, "row band renders (title/value/switch) cnt=%d", rowcnt);
|
CHECK(hdrcnt == 0, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||||
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景 */
|
CHECK(rowcnt > 40, "row card renders (title/value/switch) cnt=%d", rowcnt);
|
||||||
|
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景(卡内 switch) */
|
||||||
{
|
{
|
||||||
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
|
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
|
||||||
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surfaceVariant);
|
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surface);
|
||||||
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
|
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ static void mk_widgets(void)
|
|||||||
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
|
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
|
||||||
meow_listview_set_item_renderer(lv, 2, 0, 1);
|
meow_listview_set_item_renderer(lv, 2, 0, 1);
|
||||||
meow_listview_set_item_h(lv, SUB_ITEM_H);
|
meow_listview_set_item_h(lv, SUB_ITEM_H);
|
||||||
|
meow_listview_set_container(lv, 1); /* MD3 容器模式(M7.1a P0):圆角卡+空隙+去分隔线 */
|
||||||
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
|
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
|
||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
}
|
}
|
||||||
@@ -153,12 +154,14 @@ int main(void)
|
|||||||
meow_ui_activity_push(SUB_SENTINEL);
|
meow_ui_activity_push(SUB_SENTINEL);
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
draw_all();
|
draw_all();
|
||||||
|
/* MD3 容器模式:段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
|
||||||
|
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。 */
|
||||||
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
||||||
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
||||||
char nm[80];
|
char nm[80];
|
||||||
snprintf(nm, sizeof nm, "section header renders (band=%d)", hdrcnt);
|
snprintf(nm, sizeof nm, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||||
CHECK(hdrcnt > 0, "%s", nm);
|
CHECK(hdrcnt == 0, "%s", nm);
|
||||||
snprintf(nm, sizeof nm, "row renders (title hdr/switch/arrow) cnt=%d", rowcnt);
|
snprintf(nm, sizeof nm, "row renders (card) cnt=%d", rowcnt);
|
||||||
CHECK(rowcnt > 40, "%s", nm);
|
CHECK(rowcnt > 40, "%s", nm);
|
||||||
|
|
||||||
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
|
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
|
||||||
@@ -190,6 +193,7 @@ int main(void)
|
|||||||
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
|
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
|
||||||
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
|
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
|
||||||
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
|
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
|
||||||
|
meow_listview_set_container(scroll_lv, 1); /* 与子页一致:MD3 容器模式 */
|
||||||
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
|
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
|
||||||
CHECK(s != NULL, "meow_scroll_wrap(listview)");
|
CHECK(s != NULL, "meow_scroll_wrap(listview)");
|
||||||
int maxs = meow_listview_max_scroll(scroll_lv);
|
int maxs = meow_listview_max_scroll(scroll_lv);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
|
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
|
||||||
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
|
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
|
||||||
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
|
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
|
||||||
|
#define WF_ACT_MANUAL 2000 /* 「手动添加隐藏网络」动作行(SUB_ACT,connecth) */
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
meow_net_t saved[S_NET_MAX];
|
meow_net_t saved[S_NET_MAX];
|
||||||
@@ -122,13 +123,22 @@ static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
|
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
|
/* bug2:附近网络末尾的「手动添加隐藏网络」入口(SUB_ACT → wifi_act)。 */
|
||||||
|
if (n < max) {
|
||||||
|
rbuf[n].kind = SUB_ACT;
|
||||||
|
rbuf[n].label = "手动添加隐藏网络";
|
||||||
|
rbuf[n].sub = "";
|
||||||
|
rbuf[n].on = 0;
|
||||||
|
rbuf[n].act = WF_ACT_MANUAL;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* SDK IME 输密码(PW 模式=ASCII 软键盘输入,现有输入法复用;回车提交/ESC或点背景取消)。
|
/* SDK IME 通用文本输入(MEOW_IME_ASCII:QWERTY,顶条「中」可切拼音输中文 SSID)。
|
||||||
* 返回 1=已输入;0=取消。 */
|
* 返回 1=已输入;0=取消。 */
|
||||||
static int wifi_ask_psk(char *psk, int maxlen)
|
static int wifi_ask_text(char *out, int maxlen)
|
||||||
{
|
{
|
||||||
meow_ime_ctx ic;
|
meow_ime_ctx ic;
|
||||||
memset(&ic, 0, sizeof ic);
|
memset(&ic, 0, sizeof ic);
|
||||||
@@ -136,36 +146,72 @@ static int wifi_ask_psk(char *psk, int maxlen)
|
|||||||
ic.maxlen = maxlen;
|
ic.maxlen = maxlen;
|
||||||
ic.buf[0] = 0;
|
ic.buf[0] = 0;
|
||||||
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
|
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
|
||||||
if (res && ic.buf[0]) { snprintf(psk, maxlen, "%s", ic.buf); return 1; }
|
if (res && ic.buf[0]) { snprintf(out, maxlen, "%s", ic.buf); return 1; }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bug1:connect 阻塞前画非模态「正在连接…」横幅。子页单线程,阻塞期间画面停在此横幅。 */
|
||||||
|
static void wifi_connecting_banner(const char *ssid)
|
||||||
|
{
|
||||||
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
|
int W = meow_ui_width();
|
||||||
|
int y = 108, h = 44;
|
||||||
|
char line[160];
|
||||||
|
snprintf(line, sizeof line, "正在连接 %s ...", ssid && ssid[0] ? ssid : "网络");
|
||||||
|
meow_ui_fill_rect(0, y, W, h, th->surface_container);
|
||||||
|
meow_ui_text_center(W / 2, y + h / 2 - meow_ui_text_height(20) / 2, line, 20, th->on_surface);
|
||||||
|
meow_dirty_full();
|
||||||
|
meow_ui_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bug2:手动添加隐藏网络(附近网络占位行点击 / 「手动添加隐藏网络」动作行共用)。 */
|
||||||
|
static void wifi_manual_add(wifi_ctx_t *c)
|
||||||
|
{
|
||||||
|
char ssid[96] = "";
|
||||||
|
if (!wifi_ask_text(ssid, sizeof ssid)) return;
|
||||||
|
const char *items[2];
|
||||||
|
items[0] = "加密网络(WPA/WPA2/WPA3)";
|
||||||
|
items[1] = "开放网络";
|
||||||
|
int pick = s_dd_modal("网络加密方式", items, 2, -1);
|
||||||
|
if (pick < 0) return;
|
||||||
|
int open = (pick == 1);
|
||||||
|
char psk[96] = "";
|
||||||
|
if (!open && !wifi_ask_text(psk, sizeof psk)) return;
|
||||||
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(ssid);
|
||||||
|
int rc = nm_wifi_connect_ex(ssid, open ? NULL : psk, 1, err, sizeof err);
|
||||||
|
char line[192]; const char *txt[1]; txt[0] = line;
|
||||||
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
|
logmsg("wifi_page: manual connect ssid=%s hidden=1 open=%d rc=%d err=%s", ssid, open, rc, err);
|
||||||
|
s_dd_modal(ssid, txt, 1, -1);
|
||||||
|
nm_wifi_scan_trigger();
|
||||||
|
c->last_scan_ms = now_ms();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
|
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
|
||||||
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
|
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
|
||||||
{
|
{
|
||||||
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
|
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
|
||||||
meow_net_t *s = &c->scan[scan_idx];
|
meow_net_t *s = &c->scan[scan_idx];
|
||||||
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
|
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
|
||||||
const char *txt[1]; txt[0] = "隐藏网络无法直接连接";
|
wifi_manual_add(c); /* bug2:占位隐藏网络 → 手动录入(SSID 待输) */
|
||||||
s_dd_modal("附近网络", txt, 1, -1);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
|
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
|
||||||
char psk[96] = "";
|
char psk[96] = "";
|
||||||
if (!open) {
|
if (!open) {
|
||||||
if (!wifi_ask_psk(psk, sizeof psk)) return 1; /* 用户取消输密码 */
|
if (!wifi_ask_text(psk, sizeof psk)) return 1; /* 用户取消输密码 */
|
||||||
}
|
}
|
||||||
char err[160]; err[0] = 0;
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(s->name); /* bug1:阻塞前横幅,避免「点完无反馈」 */
|
||||||
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
|
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
|
||||||
char line[192];
|
char line[192];
|
||||||
const char *txt[1]; txt[0] = line;
|
const char *txt[1]; txt[0] = line;
|
||||||
if (rc == MEOW_OK) {
|
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||||
snprintf(line, sizeof line, "连接成功:%s%s", s->name,
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
err[0] ? "" : "(已启用)");
|
|
||||||
} else {
|
|
||||||
snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
|
||||||
}
|
|
||||||
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
|
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
|
||||||
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
|
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
|
||||||
s_dd_modal(s->name, txt, 1, -1);
|
s_dd_modal(s->name, txt, 1, -1);
|
||||||
@@ -208,20 +254,23 @@ static int wifi_detail(void *ctx, int idx, int act)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络) */
|
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络)。
|
||||||
|
* 另承接「手动添加隐藏网络」入口(bug2)。 */
|
||||||
static int wifi_act(void *ctx, int idx, int act)
|
static int wifi_act(void *ctx, int idx, int act)
|
||||||
{
|
{
|
||||||
(void)idx;
|
(void)idx;
|
||||||
wifi_ctx_t *c = ctx;
|
wifi_ctx_t *c = ctx;
|
||||||
|
if (act == WF_ACT_MANUAL) { wifi_manual_add(c); return 1; }
|
||||||
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
|
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
|
||||||
int sidx = act - WF_ACT_SAVED;
|
int sidx = act - WF_ACT_SAVED;
|
||||||
if (sidx < 0 || sidx >= c->saved_n) return 0;
|
if (sidx < 0 || sidx >= c->saved_n) return 0;
|
||||||
if (c->saved[sidx].id < 0) return 0;
|
if (c->saved[sidx].id < 0) return 0;
|
||||||
char err[160]; err[0] = 0;
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(c->saved[sidx].name); /* bug1:阻塞前横幅 */
|
||||||
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
|
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
|
||||||
char line[192]; const char *txt[1]; txt[0] = line;
|
char line[192]; const char *txt[1]; txt[0] = line;
|
||||||
if (rc == MEOW_OK) snprintf(line, sizeof line, "已连接:%s", c->saved[sidx].name);
|
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||||
else snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
|
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
|
||||||
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
|
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
|
||||||
nm_wifi_scan_trigger();
|
nm_wifi_scan_trigger();
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
|
|||||||
|
|
||||||
#define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */
|
#define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */
|
||||||
#define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */
|
#define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */
|
||||||
|
/* WiFi 设置页 bug1(2026-09-15):connect 有界等待 wpa_state=COMPLETED 的上限(ms)。
|
||||||
|
* 普通 ~10s;隐藏网络(connecth,主动探测+关联更慢)~15s。env 可调(宿主桩测试缩短)。 */
|
||||||
|
#define WIFI_CONNECT_WAIT_MS 10000 /* MEOW_WIFI_CONNECT_WAIT_MS */
|
||||||
|
#define WIFI_CONNECTH_WAIT_MS 15000 /* MEOW_WIFI_CONNECTH_WAIT_MS */
|
||||||
#define IFACE_MAX 16
|
#define IFACE_MAX 16
|
||||||
#define RFKILL_MAX 8
|
#define RFKILL_MAX 8
|
||||||
#define DNS_MAX 8
|
#define DNS_MAX 8
|
||||||
@@ -112,6 +116,8 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
|
|||||||
#define DNS_SELF_IP "127.0.0.1"
|
#define DNS_SELF_IP "127.0.0.1"
|
||||||
|
|
||||||
static int g_poll_sec = NET_POLL_SEC_DEF;
|
static int g_poll_sec = NET_POLL_SEC_DEF;
|
||||||
|
static int g_connect_wait_ms = WIFI_CONNECT_WAIT_MS; /* MEOW_WIFI_CONNECT_WAIT_MS */
|
||||||
|
static int g_connecth_wait_ms = WIFI_CONNECTH_WAIT_MS; /* MEOW_WIFI_CONNECTH_WAIT_MS */
|
||||||
static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
|
static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
|
||||||
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
|
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
|
||||||
|
|
||||||
@@ -2650,13 +2656,82 @@ static const char *str_capsafe(const char *s, char *tmp, size_t cap)
|
|||||||
return tmp;
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。 */
|
||||||
|
static int wpa_cmd(char *out, int cap, const char *args)
|
||||||
|
{
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", g_wpa_cli, args);
|
||||||
|
return run_cmd(out, cap, RUNCMD_TMO_MS, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi 设置页 bug4-②(2026-09-15):list_networks/scan_results 对非 ASCII 字节输出 \xNN 转义
|
||||||
|
* (中文 SSID)——还原为原始字节串再与输入 SSID 做同表示比较;不还原则中文 SSID 永不命中
|
||||||
|
* 既有网络 → 新建同名 open 记录顶影原记录。net.c wpa_decode_ssid / netmgr_ipc.c nm_decode_ssid
|
||||||
|
* 已有两份同功能实现(后续可收拢,本次先在 netmgrd 补齐)。 */
|
||||||
|
static int wpa_hexval(char c)
|
||||||
|
{
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
static void wpa_unescape_ssid(const char *src, char *dst, size_t dcap)
|
||||||
|
{
|
||||||
|
size_t o = 0;
|
||||||
|
while (*src && o + 1 < dcap) {
|
||||||
|
if (src[0] == '\\' && src[1] == 'x' && wpa_hexval(src[2]) >= 0 && wpa_hexval(src[3]) >= 0) {
|
||||||
|
dst[o++] = (char)((wpa_hexval(src[2]) << 4) | wpa_hexval(src[3]));
|
||||||
|
src += 4;
|
||||||
|
} else {
|
||||||
|
dst[o++] = *src++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[o] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi 设置页 bug4-③(2026-09-15):save_config 结果校验。wpa_supplicant 仅在 conf 含
|
||||||
|
* update_config=1 时允许写盘;固件模板 /etc/wpa_supplicant.conf 无此项 → 首次 FAIL 时先
|
||||||
|
* `set update_config 1`(运行期开关,wpa_supplicant 标准用法;成功落盘后文件自带
|
||||||
|
* update_config=1)再重试一次。返回 1=已持久化;0=仍失败(调用方须在报文/日志明示
|
||||||
|
* 「记录未持久化」)。仅 MeowOS 模式调用(写盘红线不变,NETMGR_DESIGN §14)。 */
|
||||||
|
static int wifi_save_config(void)
|
||||||
|
{
|
||||||
|
char out[160];
|
||||||
|
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) return 1;
|
||||||
|
wpa_cmd(out, sizeof out, "set update_config 1");
|
||||||
|
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) {
|
||||||
|
net_log("info", "write", "save_config 初次 FAIL → set update_config 1 重试成功(此后 conf 自带该位)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
net_log("warn", "write", "save_config 失败(含 set update_config 1 重试):记录未持久化");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi 设置页 bug1(2026-09-15):连接状态机——有界等待 wpa_state=COMPLETED(每 200ms 查
|
||||||
|
* status,上限 timeout_ms,防卡死)。返回 1=已关联(out 带回 ssid/ip 供诚实报文);0=超时。 */
|
||||||
|
static int wifi_wait_completed(int timeout_ms, wifi_t *out)
|
||||||
|
{
|
||||||
|
int waited = 0;
|
||||||
|
for (;;) {
|
||||||
|
wifi_t w;
|
||||||
|
wifi_status(&w);
|
||||||
|
if (w.connected) { if (out) *out = w; return 1; }
|
||||||
|
if (waited >= timeout_ms) return 0;
|
||||||
|
struct timespec ts = { 0, 200 * 1000000L };
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
waited += 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void wifi_scan(ctl_result_t *r)
|
static void wifi_scan(ctl_result_t *r)
|
||||||
{
|
{
|
||||||
char cmd[160];
|
char cmd[160];
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli);
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli);
|
||||||
char buf[128];
|
char buf[128];
|
||||||
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
|
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
|
||||||
struct timespec ts = { 0, 300 * 1000000L }; /* 触发后稍候再读结果(有界)。 */
|
/* P3(WiFi 设置页附带,2026-09-15):触发后 300ms→1500ms 再读结果——真机扫描常 1-2s,
|
||||||
|
* 300ms 读到的多是上一轮陈旧结果。IPC 客户端 per-cmd 超时相应放宽(netmgr_ipc scan=3.5s)。 */
|
||||||
|
struct timespec ts = { 1, 500 * 1000000L };
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
char bufout[MEOW_IPC_MAX - 16];
|
char bufout[MEOW_IPC_MAX - 16];
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli);
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli);
|
||||||
@@ -2675,7 +2750,13 @@ static void wifi_networks(ctl_result_t *r)
|
|||||||
ctl_sets(r, MEOW_OK, buf);
|
ctl_sets(r, MEOW_OK, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void wifi_connect(const char *arg, ctl_result_t *r)
|
/* WiFi 连接(bug1/bug2/bug4 修复,2026-09-15)。hidden=1 走隐藏网络流程(connecth):
|
||||||
|
* 对所用 id(新建/命中皆然)追加 scan_ssid=1,并把 COMPLETED 等待上限放宽(主动探测+关联更慢)。
|
||||||
|
* 流程:匹配既有(\xNN 还原同表示比较)→ 新建或(命中且带新凭据时)更新 psk →
|
||||||
|
* enable+select → 有界等待 wpa_state=COMPLETED(不再无条件报成功)→
|
||||||
|
* 成功:enable_network all(消除 select 的 disabled 副作用)+ save_config(校验+重试);
|
||||||
|
* 失败:本次新建的网络 remove 回收(防坏记录堆积),报文明示失败原因。 */
|
||||||
|
static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden)
|
||||||
{
|
{
|
||||||
char ssid[96] = "", psk[96] = "";
|
char ssid[96] = "", psk[96] = "";
|
||||||
if (arg && arg[0]) {
|
if (arg && arg[0]) {
|
||||||
@@ -2694,11 +2775,10 @@ static void wifi_connect(const char *arg, ctl_result_t *r)
|
|||||||
str_capsafe(psk, tmp_psk, sizeof tmp_psk);
|
str_capsafe(psk, tmp_psk, sizeof tmp_psk);
|
||||||
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
|
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
|
||||||
|
|
||||||
/* 找既有网络:list_networks → 匹配 ssid(剥引号比较);命中则复用其 id。 */
|
/* 找既有网络:list_networks → 匹配 ssid(剥引号 + \xNN 还原后同表示比较);命中则复用其 id。 */
|
||||||
char cmd[200];
|
char cmd[256];
|
||||||
char list[1024];
|
char list[1024];
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli);
|
int n = wpa_cmd(list, sizeof list, "list_networks");
|
||||||
int n = run_cmd(list, sizeof list, RUNCMD_TMO_MS, cmd);
|
|
||||||
int id = -1;
|
int id = -1;
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
char *ln = strtok(list, "\n");
|
char *ln = strtok(list, "\n");
|
||||||
@@ -2709,61 +2789,81 @@ static void wifi_connect(const char *arg, ctl_result_t *r)
|
|||||||
char *ss = tab0 + 1;
|
char *ss = tab0 + 1;
|
||||||
char *tab1 = strchr(ss, '\t');
|
char *tab1 = strchr(ss, '\t');
|
||||||
if (tab1) *tab1 = 0;
|
if (tab1) *tab1 = 0;
|
||||||
char clean[128], norm_clean[128];
|
char clean[128], unesc[160], norm_clean[128];
|
||||||
size_t cl = strlen(ss);
|
size_t cl = strlen(ss);
|
||||||
if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') {
|
if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') {
|
||||||
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
|
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
|
||||||
} else {
|
} else {
|
||||||
snprintf(clean, sizeof clean, "%s", ss);
|
snprintf(clean, sizeof clean, "%s", ss);
|
||||||
}
|
}
|
||||||
str_capsafe(clean, norm_clean, sizeof norm_clean);
|
wpa_unescape_ssid(clean, unesc, sizeof unesc); /* bug4-②:中文 SSID 转义还原 */
|
||||||
|
str_capsafe(unesc, norm_clean, sizeof norm_clean);
|
||||||
if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id;
|
if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id;
|
||||||
}
|
}
|
||||||
ln = strtok(NULL, "\n");
|
ln = strtok(NULL, "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
char out[128];
|
char out[128];
|
||||||
|
int created = 0;
|
||||||
if (id < 0) {
|
if (id < 0) {
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s add_network 2>/dev/null", g_wpa_cli);
|
if (wpa_cmd(out, sizeof out, "add_network") <= 0) {
|
||||||
if (run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd) <= 0) {
|
|
||||||
ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)");
|
ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
id = atoi(out);
|
id = atoi(out);
|
||||||
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
|
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
|
||||||
snprintf(cmd, sizeof cmd,
|
created = 1;
|
||||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d ssid '\"%s\"' 2>/dev/null",
|
snprintf(cmd, sizeof cmd, "set_network %d ssid '\"%s\"'", id, tmp_ssid);
|
||||||
g_wpa_cli, id, tmp_ssid);
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd);
|
if (tmp_psk[0]) snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
|
||||||
if (tmp_psk[0]) {
|
else snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id);
|
||||||
snprintf(cmd, sizeof cmd,
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d psk '\"%s\"' 2>/dev/null",
|
} else if (tmp_psk[0]) {
|
||||||
g_wpa_cli, id, tmp_psk);
|
/* bug4-①/bug1-④:命中既有网络且调用方带了新 psk → 先更新凭据再连
|
||||||
} else {
|
* (旧记录密码错误可经扫描路径重输修复;psk 为空则不动既有凭据)。 */
|
||||||
snprintf(cmd, sizeof cmd,
|
snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
|
||||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d key_mgmt NONE 2>/dev/null",
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
g_wpa_cli, id);
|
}
|
||||||
}
|
if (hidden) {
|
||||||
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd);
|
/* bug2:隐藏 AP 必须 scan_ssid=1,supplicant 才会主动发 Probe Request(命中既有也要补)。 */
|
||||||
|
snprintf(cmd, sizeof cmd, "set_network %d scan_ssid 1", id);
|
||||||
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
}
|
}
|
||||||
{ /* enable + select 该网络(复用/新建皆用)。 */
|
{ /* enable + select 该网络(复用/新建皆用)。 */
|
||||||
char en[256], sel[256];
|
snprintf(cmd, sizeof cmd, "enable_network %d", id);
|
||||||
snprintf(en, sizeof en, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s enable_network %d 2>/dev/null",
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
g_wpa_cli, id);
|
snprintf(cmd, sizeof cmd, "select_network %d", id);
|
||||||
run_cmd(out, sizeof out, RUNCMD_TMO_MS, en);
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
snprintf(sel, sizeof sel, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s select_network %d 2>/dev/null",
|
|
||||||
g_wpa_cli, id);
|
|
||||||
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sel);
|
|
||||||
/* 期3:MeowOS 模式 connect 后持久化 wpa_supplicant.conf(HibyOS 只读由服务端门控,这里仅 MeowOS 生效)。 */
|
|
||||||
if (g_mode_meow) {
|
|
||||||
char sc[160];
|
|
||||||
snprintf(sc, sizeof sc, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
|
|
||||||
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
char msg[140];
|
/* bug1:有界等待 wpa_state=COMPLETED——密码错误/AP 不在范围/握手失败不再误报成功。 */
|
||||||
snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select%s)", tmp_ssid, id,
|
wifi_t w;
|
||||||
g_mode_meow ? "+save_config" : "");
|
int wait_ms = hidden ? g_connecth_wait_ms : g_connect_wait_ms;
|
||||||
|
if (!wifi_wait_completed(wait_ms, &w)) {
|
||||||
|
if (created) { /* 本次新建且失败 → 回收防坏记录堆积(bug4 联动) */
|
||||||
|
snprintf(cmd, sizeof cmd, "remove_network %d", id);
|
||||||
|
wpa_cmd(out, sizeof out, cmd);
|
||||||
|
if (g_mode_meow) wifi_save_config();
|
||||||
|
}
|
||||||
|
net_log("info", "write", "wifi connect ssid=%s id=%d 超时未关联(%dms)", tmp_ssid, id, wait_ms);
|
||||||
|
char msg[128];
|
||||||
|
snprintf(msg, sizeof msg, "连接失败:密码错误或信号不佳(超时未关联)");
|
||||||
|
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* bug4-④:验证成功后 enable_network all 复位其它网络 disabled 态(select_network 副作用),
|
||||||
|
* 再 save_config 落盘(bug4-③:结果校验,FAIL 时 set update_config 1 重试)。
|
||||||
|
* 写盘仍受 MeowOS 模式门控(红线不变,NETMGR_DESIGN §14)。 */
|
||||||
|
int persisted = 0;
|
||||||
|
if (g_mode_meow) {
|
||||||
|
wpa_cmd(out, sizeof out, "enable_network all");
|
||||||
|
persisted = wifi_save_config();
|
||||||
|
}
|
||||||
|
net_log("info", "write", "wifi connect ssid=%s id=%d 已连接 ip=%s persisted=%d",
|
||||||
|
tmp_ssid, id, w.ip[0] ? w.ip : "-", persisted);
|
||||||
|
char msg[160];
|
||||||
|
snprintf(msg, sizeof msg, "已连接:%s ip=%s%s",
|
||||||
|
w.ssid[0] ? w.ssid : tmp_ssid, w.ip[0] ? w.ip : "-",
|
||||||
|
(g_mode_meow && !persisted) ? "(记录未持久化)" : "");
|
||||||
ctl_sets(r, MEOW_OK, msg);
|
ctl_sets(r, MEOW_OK, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2840,7 +2940,9 @@ static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
|||||||
} else if (!strcmp(cmd, "networks")) {
|
} else if (!strcmp(cmd, "networks")) {
|
||||||
wifi_networks(r);
|
wifi_networks(r);
|
||||||
} else if (!strcmp(cmd, "connect")) {
|
} else if (!strcmp(cmd, "connect")) {
|
||||||
wifi_connect(arg, r);
|
wifi_connect_ex(arg, r, 0);
|
||||||
|
} else if (!strcmp(cmd, "connecth")) {
|
||||||
|
wifi_connect_ex(arg, r, 1); /* bug2:隐藏网络(scan_ssid=1 + 更长 COMPLETED 等待) */
|
||||||
} else if (!strcmp(cmd, "disconnect")) {
|
} else if (!strcmp(cmd, "disconnect")) {
|
||||||
char cmd2[160];
|
char cmd2[160];
|
||||||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli);
|
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli);
|
||||||
@@ -2857,13 +2959,13 @@ static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
|||||||
g_wpa_cli, nid);
|
g_wpa_cli, nid);
|
||||||
char buf[128];
|
char buf[128];
|
||||||
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
||||||
/* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。 */
|
/* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。
|
||||||
if (g_mode_meow) {
|
* bug4-③:save_config 结果校验(FAIL 时 set update_config 1 重试),报文不再谎称已落盘。 */
|
||||||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
|
int persisted = 0;
|
||||||
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
if (g_mode_meow) persisted = wifi_save_config();
|
||||||
}
|
char msg[96];
|
||||||
char msg[64];
|
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid,
|
||||||
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? "+save_config" : "");
|
g_mode_meow ? (persisted ? "+save_config" : "(记录未持久化)") : "");
|
||||||
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
|
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
|
||||||
} else if (!strcmp(cmd, "rfkill")) {
|
} else if (!strcmp(cmd, "rfkill")) {
|
||||||
rfkill_ctl(arg, r);
|
rfkill_ctl(arg, r);
|
||||||
@@ -3704,7 +3806,7 @@ static int cli_policy(int op, const char *entity, const char *arg)
|
|||||||
|
|
||||||
static const char *usage(void)
|
static const char *usage(void)
|
||||||
{
|
{
|
||||||
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off"
|
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|connecth <ssid>[,psk]|disconnect|on|off"
|
||||||
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
|
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
|
||||||
" | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
" | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
||||||
" | apps | conns [entity] | top | dns status|stats|flush"
|
" | apps | conns [entity] | top | dns status|stats|flush"
|
||||||
@@ -3757,9 +3859,9 @@ static int cli_run(int n, char **v)
|
|||||||
if (!strcmp(cmd, "wifi")) {
|
if (!strcmp(cmd, "wifi")) {
|
||||||
if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; }
|
if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; }
|
||||||
if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
|
if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
|
||||||
if (!strcmp(arg, "connect")) {
|
if (!strcmp(arg, "connect") || !strcmp(arg, "connecth")) {
|
||||||
if (!arg2) { fprintf(stderr, "connect 需 <ssid>[,psk]\n"); return 1; }
|
if (!arg2) { fprintf(stderr, "%s 需 <ssid>[,psk]\n", arg); return 1; }
|
||||||
return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", arg2);
|
return cli_cmd_result(MSG_NET_WIFI_CTL, arg, arg2);
|
||||||
}
|
}
|
||||||
if (!strcmp(arg, "forget")) {
|
if (!strcmp(arg, "forget")) {
|
||||||
if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; }
|
if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; }
|
||||||
@@ -3821,6 +3923,8 @@ int main(int argc, char **argv)
|
|||||||
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
|
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
|
||||||
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
|
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
|
||||||
if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e);
|
if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e);
|
||||||
|
if ((e = getenv("MEOW_WIFI_CONNECT_WAIT_MS")) && atoi(e) >= 200) g_connect_wait_ms = atoi(e);
|
||||||
|
if ((e = getenv("MEOW_WIFI_CONNECTH_WAIT_MS")) && atoi(e) >= 200) g_connecth_wait_ms = atoi(e);
|
||||||
if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e);
|
if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e);
|
||||||
if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e);
|
if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e);
|
||||||
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e);
|
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e);
|
||||||
|
|||||||
@@ -423,6 +423,25 @@ kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null
|
|||||||
# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认)
|
# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认)
|
||||||
grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; }
|
grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; }
|
||||||
|
|
||||||
|
# ---- T19: WiFi connect 状态机/持久化/隐藏网络/中文 SSID(宿主桩直驱 wifi_connect_ex)----
|
||||||
|
# daemon IPC 写级需系统身份(SO_PEERCRED /proc/exe),宿主 CLI=CALL_OTHER 必拒;故用 #include
|
||||||
|
# meow_netmgrd.c 的独立宿主二进制直驱 static wifi_connect_ex,桩 wpa_cli 由二进制自建。
|
||||||
|
if [ -z "$Q" ]; then
|
||||||
|
WCON="$T/test_wifi_connect"
|
||||||
|
if cc -Os -Wall -Wextra -Wno-format-truncation -I../meow_launcherd \
|
||||||
|
test_wifi_connect.c -o "$WCON" >/dev/null 2>&1; then
|
||||||
|
if "$WCON" >"$T/wcon.out" 2>/dev/null && grep -q "FAIL=0" "$T/wcon.out"; then
|
||||||
|
PASS=$((PASS+1)); echo "PASS T19 wifi-connect host (12 asserts, FAIL=0)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host"; cat "$T/wcon.out"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host build"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "SKIP T19 wifi-connect(qemu 下 fork/exec 宿主 /bin/sh 不便,宿主已覆盖)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
echo "PASS=$PASS FAIL=$FAIL"
|
||||||
[ $FAIL -eq 0 ]
|
[ $FAIL -eq 0 ]
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/* test_wifi_connect.c — WiFi 设置页 bug1/bug2/bug4 宿主桩测试(2026-09-15)。
|
||||||
|
*
|
||||||
|
* 通过 #include meow_netmgrd.c 直接驱动 static 的 wifi_connect_ex(),绕过 daemon 的
|
||||||
|
* SO_PEERCRED 系统身份鉴权(宿主 CLI=CALL_OTHER 写级必拒,无法经 IPC 测连接路径)。
|
||||||
|
* 桩 wpa_cli 由本程序写入临时目录;控制文件(status_mode/list/save_fail_once)决定
|
||||||
|
* status 序列 / list_networks 原文 / save_config 是否首败,调用日志写入 <dir>/log 供断言。
|
||||||
|
*
|
||||||
|
* 编译:cc -Wall -Wextra -I../meow_launcherd test_wifi_connect.c -o test_wifi_connect
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#define main meow_netmgrd_main
|
||||||
|
#include "meow_netmgrd.c"
|
||||||
|
#undef main
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
static char g_dir[128];
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
|
||||||
|
static void ck(const char *name, int cond, const char *detail)
|
||||||
|
{
|
||||||
|
if (cond) { g_pass++; printf("PASS %s\n", name); }
|
||||||
|
else { g_fail++; printf("FAIL %s: %s\n", name, detail ? detail : ""); }
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void putf(const char *name, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char path[192], body[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(body, sizeof body, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
snprintf(path, sizeof path, "%s/%s", g_dir, name);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (f) { fputs(body, f); fclose(f); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int log_has(const char *needle)
|
||||||
|
{
|
||||||
|
char path[192], buf[8192];
|
||||||
|
snprintf(path, sizeof path, "%s/log", g_dir);
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return 0;
|
||||||
|
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
||||||
|
fclose(f);
|
||||||
|
buf[n] = 0;
|
||||||
|
return strstr(buf, needle) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset_log(void)
|
||||||
|
{
|
||||||
|
char path[192];
|
||||||
|
snprintf(path, sizeof path, "%s/log", g_dir);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (f) fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_stub(void)
|
||||||
|
{
|
||||||
|
char path[192];
|
||||||
|
snprintf(path, sizeof path, "%s/wpa_cli", g_dir);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (!f) { fprintf(stderr, "stub open fail\n"); exit(2); }
|
||||||
|
fputs(
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"D=\"${MN_WIFI_DIR:-/tmp}\"\n"
|
||||||
|
"echo \"$@\" >> \"$D/log\"\n"
|
||||||
|
"case \"$1\" in\n"
|
||||||
|
" status)\n"
|
||||||
|
" m=$(cat \"$D/status_mode\" 2>/dev/null || echo completed)\n"
|
||||||
|
" if [ \"$m\" = \"associating\" ]; then\n"
|
||||||
|
" echo \"wpa_state=ASSOCIATING\"; echo \"ssid=MyWiFi\"\n"
|
||||||
|
" else\n"
|
||||||
|
" echo \"wpa_state=COMPLETED\"; echo \"ssid=MyWiFi\"\n"
|
||||||
|
" echo \"ip_address=10.78.193.50\"; echo \"signal=-45\"\n"
|
||||||
|
" fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" list_networks)\n"
|
||||||
|
" if [ -f \"$D/list\" ]; then cat \"$D/list\"\n"
|
||||||
|
" else printf 'network id / ssid / bssid / flags\\n0\\tMyWiFi\\tany\\t[CURRENT]\\n'\n"
|
||||||
|
" fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" add_network) echo 1 ;;\n"
|
||||||
|
" set_network) echo OK ;;\n"
|
||||||
|
" enable_network) echo OK ;;\n"
|
||||||
|
" select_network) echo OK ;;\n"
|
||||||
|
" disconnect) echo OK ;;\n"
|
||||||
|
" save_config)\n"
|
||||||
|
" if [ -f \"$D/save_fail_once\" ]; then rm -f \"$D/save_fail_once\"; echo FAIL; else echo OK; fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" set) echo OK ;;\n"
|
||||||
|
" remove_network) echo OK ;;\n"
|
||||||
|
" *) echo OK ;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"exit 0\n",
|
||||||
|
f);
|
||||||
|
fclose(f);
|
||||||
|
chmod(path, 0755);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
snprintf(g_dir, sizeof g_dir, "/tmp/mnwifi.XXXXXX");
|
||||||
|
if (!mkdtemp(g_dir)) { fprintf(stderr, "mkdtemp fail\n"); return 2; }
|
||||||
|
write_stub();
|
||||||
|
setenv("MN_WIFI_DIR", g_dir, 1);
|
||||||
|
|
||||||
|
g_mode_meow = 1;
|
||||||
|
g_connect_wait_ms = 400;
|
||||||
|
g_connecth_wait_ms = 400;
|
||||||
|
{
|
||||||
|
char p[192];
|
||||||
|
snprintf(p, sizeof p, "%s/wpa_cli", g_dir);
|
||||||
|
g_wpa_cli = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl_result_t r;
|
||||||
|
|
||||||
|
/* ① 成功:命中既有 id=0 + 带新 psk → 更新凭据,等待 COMPLETED,成功报 ip。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
reset_log();
|
||||||
|
wifi_connect_ex("MyWiFi,secret", &r, 0);
|
||||||
|
ck("connect-success code", r.code == MEOW_OK, r.text);
|
||||||
|
ck("connect-success text ip", strstr(r.text, "已连接") && strstr(r.text, "ip=10.78.193.50"), r.text);
|
||||||
|
ck("connect-success psk update", log_has("set_network 0 psk"), "");
|
||||||
|
ck("connect-success enable all", log_has("enable_network all"), "");
|
||||||
|
|
||||||
|
/* ② 错误密码/不在范围:status 恒 ASSOCIATING → 超时失败,新建 id 被回收。 */
|
||||||
|
putf("status_mode", "associating");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("OtherWiFi", &r, 0);
|
||||||
|
ck("connect-timeout code", r.code == MEOW_ERR_NODEV, r.text);
|
||||||
|
ck("connect-timeout text", strstr(r.text, "连接失败") != NULL, r.text);
|
||||||
|
ck("connect-timeout remove new", log_has("remove_network 1"), "");
|
||||||
|
|
||||||
|
/* ③ 中文 SSID \xNN 还原匹配:list_networks 原文转义,输入 UTF-8 名 → 命中 id=0 不新建。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\t\\xe5\\xa4\\xa9\\xe5\\xa4\\xa9\tany\t[CURRENT]\n");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("天天", &r, 0);
|
||||||
|
ck("connect-escape match", r.code == MEOW_OK, r.text);
|
||||||
|
ck("connect-escape no add", !log_has("add_network"), "");
|
||||||
|
|
||||||
|
/* ④ connecth:隐藏网络命中既有 id → 追加 scan_ssid 1。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("MyWiFi", &r, 1);
|
||||||
|
ck("connecth scan_ssid", r.code == MEOW_OK && log_has("set_network 0 scan_ssid 1"), r.text);
|
||||||
|
|
||||||
|
/* ⑤ save_config 首败 → set update_config 1 重试 → 持久化成功。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
|
||||||
|
putf("save_fail_once", "1");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("MyWiFi", &r, 0);
|
||||||
|
ck("save-config retry", r.code == MEOW_OK && !strstr(r.text, "记录未持久化"), r.text);
|
||||||
|
ck("save-config set update_config", log_has("set update_config 1"), "");
|
||||||
|
|
||||||
|
printf("==============================\nPASS=%d FAIL=%d\n", g_pass, g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -8,16 +8,17 @@ set -e
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
ROOT=../..
|
ROOT=../..
|
||||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||||
|
INC="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(音量收编 v2:MSG_AUDIO_VOLUME/MSG_ACK)
|
||||||
|
|
||||||
if [ "$1" = "host" ]; then
|
if [ "$1" = "host" ]; then
|
||||||
gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
gcc -Wall -Wextra -Os -I"$INC" -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
||||||
test_wd_fw.c -o test_wd_fw -lm
|
-Wno-unused-variable test_wd_fw.c -o test_wd_fw -lm
|
||||||
./test_wd_fw
|
./test_wd_fw
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== meow_watchdog (mipsel static) =="
|
echo "== meow_watchdog (mipsel static) =="
|
||||||
"$CC" -Os -Wall -Wextra -static meow_watchdog.c -o meow_watchdog
|
"$CC" -Os -Wall -Wextra -static -I"$INC" meow_watchdog.c -o meow_watchdog
|
||||||
ls -la meow_watchdog
|
ls -la meow_watchdog
|
||||||
md5sum meow_watchdog
|
md5sum meow_watchdog
|
||||||
file meow_watchdog
|
file meow_watchdog
|
||||||
@@ -19,7 +19,10 @@
|
|||||||
* (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active
|
* (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active
|
||||||
* 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。
|
* 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。
|
||||||
* 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。
|
* 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。
|
||||||
* 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。
|
* 音量±(115/114) -> 音量收编 v2(FUTURE_AUDIO 事项三):首选发 audiod 权威
|
||||||
|
* MSG_AUDIO_VOLUME(audiod 写 DAC numid=1/2 + DSD_VOLUME + 持久化
|
||||||
|
* settings.conf,同源同键单写者);audiod 未起(HibyOS/开机早期)
|
||||||
|
* 才回退本地 amixer 直写兜底 + 本地持久化。
|
||||||
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
|
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
|
||||||
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
|
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
|
||||||
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
|
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
|
||||||
@@ -50,8 +53,12 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <sys/time.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
|
||||||
|
|
||||||
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
||||||
#define FBIOGET_VSCREENINFO 0x4600
|
#define FBIOGET_VSCREENINFO 0x4600
|
||||||
@@ -214,6 +221,14 @@ static void write_brightness(int val)
|
|||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* settings.conf 路径(缺省 /usr/data/meow/settings.conf;env MEOW_SETTINGS_FILE 覆盖,
|
||||||
|
* 供宿主测试隔离——与 audiod MEOW_AUDIO_SETTINGS 同款测试钩子,真机无 env 走默认)。 */
|
||||||
|
static const char *wd_settings_path(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_SETTINGS_FILE");
|
||||||
|
return (e && e[0]) ? e : MEOW_SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
|
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
|
||||||
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
|
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
|
||||||
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),`wd_saved_bri=(br>0)?br:60` 便落到
|
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),`wd_saved_bri=(br>0)?br:60` 便落到
|
||||||
@@ -225,7 +240,7 @@ static void write_brightness(int val)
|
|||||||
* → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */
|
* → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */
|
||||||
static int wd_settings_brightness(void)
|
static int wd_settings_brightness(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
FILE *f = fopen(wd_settings_path(), "r");
|
||||||
if (!f) return -1;
|
if (!f) return -1;
|
||||||
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
||||||
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
||||||
@@ -372,7 +387,7 @@ static int wd_vol_step = 1; /* 跳步 */
|
|||||||
static void wd_vol_load(void)
|
static void wd_vol_load(void)
|
||||||
{
|
{
|
||||||
wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1;
|
wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1;
|
||||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
FILE *f = fopen(wd_settings_path(), "r");
|
||||||
if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; }
|
if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; }
|
||||||
char buf[128];
|
char buf[128];
|
||||||
while (fgets(buf, sizeof buf, f)) {
|
while (fgets(buf, sizeof buf, f)) {
|
||||||
@@ -391,13 +406,17 @@ static void wd_vol_load(void)
|
|||||||
wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step);
|
wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。 */
|
/* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。
|
||||||
|
* ⚠️ 音量收编 v2 兜底函数:仅 audiod 未起(HibyOS/开机早期)时由 wd_vol_adj 回退调用;
|
||||||
|
* MeowOS 常态一律走 wd_audiod_set_volume(audiod 唯一权威,防多写者漂移)。
|
||||||
|
* 锥度为定案平方锥度(与 audiod dac_vol_set 同款,勿改语义:CS43198=衰减量,0 最响/255 静音)。 */
|
||||||
static void wd_vol_write(int pct)
|
static void wd_vol_write(int pct)
|
||||||
{
|
{
|
||||||
int t = 100 - pct, dac;
|
int t = 100 - pct, dac;
|
||||||
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
||||||
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
||||||
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
if (dac > 255) dac = 255;
|
||||||
|
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
||||||
char cmd[160];
|
char cmd[160];
|
||||||
snprintf(cmd, sizeof cmd,
|
snprintf(cmd, sizeof cmd,
|
||||||
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
||||||
@@ -406,14 +425,97 @@ static void wd_vol_write(int pct)
|
|||||||
wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct);
|
wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 音量收编 v2(FUTURE_AUDIO 事项三):首选 audiod 权威 MSG_AUDIO_VOLUME ----
|
||||||
|
* watchdog 为 per-App 小进程,audiod sock 0666 无鉴权可直接连。一次性短连接(按键 rare,
|
||||||
|
* 无常驻连接/陈旧态负担):非阻塞 connect + poll 300ms,SO_SNDTIMEO/RCVTIMEO 1s,发 i32 pct
|
||||||
|
* 等 MSG_ACK。audiod 侧已异步化(末值去抖,ACK=已应用+已持久化),正常 <<1s 返回。
|
||||||
|
* 返回 0=audiod 已受理(DAC 写+持久化均由 audiod 完成,本进程不再写 settings 防双写);
|
||||||
|
* -1=audiod 缺席/失败(调用方回退 wd_vol_write 本地直写 + wd_vol_save 本地持久化)。 */
|
||||||
|
#define WD_AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock" /* 同 audiod AUD_SOCK_DEFAULT */
|
||||||
|
#define WD_AUD_CONN_TMO_MS 300
|
||||||
|
#define WD_AUD_IO_TMO_MS 1000
|
||||||
|
|
||||||
|
static const char *wd_aud_sock(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_AUDIO_SOCK"); /* 与 SDK sock_path() 同款 env(宿主测覆盖) */
|
||||||
|
return (e && e[0]) ? e : WD_AUD_SOCK_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wd_wr_all(int fd, const void *p, size_t n)
|
||||||
|
{
|
||||||
|
const uint8_t *b = p; size_t o = 0;
|
||||||
|
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static int wd_rd_all(int fd, void *p, size_t n)
|
||||||
|
{
|
||||||
|
uint8_t *b = p; size_t o = 0;
|
||||||
|
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return -1; o += (size_t)r; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wd_audiod_set_volume(int pct)
|
||||||
|
{
|
||||||
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
int fl = fcntl(fd, F_GETFL, 0);
|
||||||
|
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||||
|
struct sockaddr_un a;
|
||||||
|
memset(&a, 0, sizeof a);
|
||||||
|
a.sun_family = AF_UNIX;
|
||||||
|
snprintf(a.sun_path, sizeof a.sun_path, "%s", wd_aud_sock());
|
||||||
|
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||||||
|
if (r < 0 && errno == EINPROGRESS) {
|
||||||
|
struct pollfd p;
|
||||||
|
p.fd = fd; p.events = POLLOUT; p.revents = 0;
|
||||||
|
if (poll(&p, 1, WD_AUD_CONN_TMO_MS) > 0) {
|
||||||
|
int err = 0;
|
||||||
|
socklen_t el = sizeof err;
|
||||||
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
|
||||||
|
} else {
|
||||||
|
r = -1; /* 超时/错误 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r < 0) { close(fd); return -1; }
|
||||||
|
if (fl >= 0) fcntl(fd, F_SETFL, fl); /* 回阻塞模式;读写超时由 SO_*TIMEO 兜底 */
|
||||||
|
struct timeval tv;
|
||||||
|
tv.tv_sec = WD_AUD_IO_TMO_MS / 1000; tv.tv_usec = (WD_AUD_IO_TMO_MS % 1000) * 1000;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||||
|
/* 发 MSG_AUDIO_VOLUME(i32 pct);req_id 任意(一次性连接单请求) */
|
||||||
|
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||||
|
uint32_t magic = MEOW_IPC_MAGIC, flen = sizeof(int32_t), freq = 1;
|
||||||
|
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
|
||||||
|
memcpy(frame + 0, &magic, 4);
|
||||||
|
memcpy(frame + 4, &ver, 2);
|
||||||
|
memcpy(frame + 6, &type, 2);
|
||||||
|
memcpy(frame + 8, &freq, 4);
|
||||||
|
memcpy(frame + 12, &flen, 4);
|
||||||
|
int32_t p32 = pct;
|
||||||
|
memcpy(frame + MEOW_IPC_HDR_LEN, &p32, sizeof p32);
|
||||||
|
if (wd_wr_all(fd, frame, sizeof frame) != 0) { close(fd); return -1; }
|
||||||
|
/* 等 MSG_ACK(有界:RCVTIMEO;audiod 异步执行点应用后回 ACK) */
|
||||||
|
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||||
|
if (wd_rd_all(fd, hdr, sizeof hdr) != 0) { close(fd); return -1; }
|
||||||
|
uint32_t rmagic, rlen;
|
||||||
|
uint16_t rver, rtype;
|
||||||
|
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2); memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
|
||||||
|
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION || rtype != MSG_ACK || rlen > 64) { close(fd); return -1; }
|
||||||
|
uint8_t pl[64];
|
||||||
|
if (rlen && wd_rd_all(fd, pl, rlen) != 0) { close(fd); return -1; }
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
|
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
|
||||||
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
|
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
|
||||||
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
|
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
|
||||||
* 与 SDK meow_ui_vol_set 同款语义。 */
|
* 与 SDK meow_ui_vol_set 同款语义。 */
|
||||||
static void wd_vol_save(void)
|
static void wd_vol_save(void)
|
||||||
{
|
{
|
||||||
system("mkdir -p /usr/data/meow 2>/dev/null");
|
const char *path = wd_settings_path();
|
||||||
const char *path = MEOW_SETTINGS_FILE;
|
if (strcmp(path, MEOW_SETTINGS_FILE) == 0) /* 仅默认路径补目录(env 覆盖=宿主测,目录自备) */
|
||||||
|
system("mkdir -p /usr/data/meow 2>/dev/null");
|
||||||
char tmp[300];
|
char tmp[300];
|
||||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||||
FILE *in = fopen(path, "r");
|
FILE *in = fopen(path, "r");
|
||||||
@@ -490,7 +592,9 @@ static int wd_jack_debounce(void)
|
|||||||
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
|
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。 */
|
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。
|
||||||
|
* 音量收编 v2:首选 audiod 权威(IPC;DAC 写+持久化 audiod 完成);audiod 缺席才本地
|
||||||
|
* amixer 直写 + 本地持久化兜底(wd_vol 本地缓存仅作步进基数,两路都维护)。 */
|
||||||
static void wd_vol_adj(int dir)
|
static void wd_vol_adj(int dir)
|
||||||
{
|
{
|
||||||
int target = wd_vol + dir * wd_vol_step;
|
int target = wd_vol + dir * wd_vol_step;
|
||||||
@@ -498,9 +602,13 @@ static void wd_vol_adj(int dir)
|
|||||||
if (target > wd_vol_max) target = wd_vol_max;
|
if (target > wd_vol_max) target = wd_vol_max;
|
||||||
if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; }
|
if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; }
|
||||||
wd_vol = target;
|
wd_vol = target;
|
||||||
wd_vol_write(wd_vol);
|
if (wd_audiod_set_volume(wd_vol) == 0) {
|
||||||
|
wd_log("volume %s -> %d (via audiod)", dir > 0 ? "UP" : "DOWN", wd_vol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wd_vol_write(wd_vol); /* 兜底:仅 audiod 未起(HibyOS/开机早期) */
|
||||||
wd_vol_save();
|
wd_vol_save();
|
||||||
wd_log("volume %s -> %d", dir > 0 ? "UP" : "DOWN", wd_vol);
|
wd_log("volume %s -> %d (local amixer fallback, audiod absent)", dir > 0 ? "UP" : "DOWN", wd_vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_touch(int immersive, int app_pid,
|
static void handle_touch(int immersive, int app_pid,
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ void net_policy_parse(const char *txt, sbuf_t *s);
|
|||||||
int api_net_policy_get(int sock);
|
int api_net_policy_get(int sock);
|
||||||
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
||||||
int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */
|
int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */
|
||||||
|
int net_bt_aggregate(const char *st, const char *paired, const char *connected, const char *a2dp, const char *devices, sbuf_t *s); /* F1-③ 0xE6 五读文本聚合(selftest 可测) */
|
||||||
|
int api_net_bt(int sock); /* F1-③ GET /api/net/bt 蓝牙只读详情 */
|
||||||
int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */
|
int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */
|
||||||
void panel_strip_ansi(char *s);
|
void panel_strip_ansi(char *s);
|
||||||
int txt_tokens(char *buf, char **toks, int maxtoks);
|
int txt_tokens(char *buf, char **toks, int maxtoks);
|
||||||
@@ -415,6 +417,8 @@ int api_notification_dismiss_all(int sock);
|
|||||||
int api_files_list(int sock, const req_t *r);
|
int api_files_list(int sock, const req_t *r);
|
||||||
int api_files_read(int sock, const req_t *r);
|
int api_files_read(int sock, const req_t *r);
|
||||||
int api_files_download(int sock, const req_t *r);
|
int api_files_download(int sock, const req_t *r);
|
||||||
|
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out); /* F1-④ 搜索核心(selftest 可测) */
|
||||||
|
int api_files_search(int sock, const req_t *r); /* F1-④ GET /api/files/search(只读) */
|
||||||
int api_files_upload(int sock, const req_t *r);
|
int api_files_upload(int sock, const req_t *r);
|
||||||
int rm_recursive(const char *path);
|
int rm_recursive(const char *path);
|
||||||
int api_files_mkdir(int sock, const req_t *r);
|
int api_files_mkdir(int sock, const req_t *r);
|
||||||
|
|||||||
@@ -571,6 +571,142 @@ int api_files_delete(int sock, const req_t *r)
|
|||||||
return send_json(sock, 200, "{\"removed\":true}");
|
return send_json(sock, 200, "{\"removed\":true}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- F1-④ 文件搜索(FUTURE_MEOWPANEL §2.3,只读):白名单内 readdir 递归(限深 4 / 限量 200 /
|
||||||
|
* 限遍历 8000 条),不 run_cmd("find")(防注入、可控)。name=名子串(ASCII 大小写不敏感);
|
||||||
|
* type=file|dir 可选过滤。不跟符号链接(防循环/逃逸出白名单根)。 ---- */
|
||||||
|
#define SEARCH_MAX_DEPTH 4
|
||||||
|
#define SEARCH_MAX_ITEMS 200
|
||||||
|
#define SEARCH_MAX_VISIT 8000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *needle; /* 已小写化的匹配子串 */
|
||||||
|
int want_type; /* 0=不限 1=仅文件 2=仅目录 */
|
||||||
|
int max;
|
||||||
|
int count;
|
||||||
|
long visited;
|
||||||
|
sbuf_t *s;
|
||||||
|
} search_ctx_t;
|
||||||
|
|
||||||
|
static void name_lower(const char *in, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
for (; in[i] && i + 1 < cap; i++) {
|
||||||
|
unsigned char c = (unsigned char)in[i];
|
||||||
|
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : (char)c;
|
||||||
|
}
|
||||||
|
out[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回 1=到达上限应全程停止(truncated)。 */
|
||||||
|
static int search_walk(search_ctx_t *c, const char *dir, int depth)
|
||||||
|
{
|
||||||
|
if (c->count >= c->max || c->visited >= SEARCH_MAX_VISIT) return 1;
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
if (!d) return 0;
|
||||||
|
int stop = 0;
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d))) {
|
||||||
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||||
|
if (++c->visited > SEARCH_MAX_VISIT) { stop = 1; break; }
|
||||||
|
char full[1400];
|
||||||
|
snprintf(full, sizeof full, "%s/%s", dir, e->d_name);
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(full, &st) < 0) continue;
|
||||||
|
if (S_ISLNK(st.st_mode)) continue; /* 不跟符号链接 */
|
||||||
|
int isdir = S_ISDIR(st.st_mode);
|
||||||
|
int type_ok = !c->want_type || (c->want_type == 1 && !isdir) || (c->want_type == 2 && isdir);
|
||||||
|
if (type_ok) {
|
||||||
|
char low[300];
|
||||||
|
name_lower(e->d_name, low, sizeof low);
|
||||||
|
if (strstr(low, c->needle)) {
|
||||||
|
if (c->count >= c->max) { stop = 1; break; }
|
||||||
|
char rel[1200];
|
||||||
|
make_rel(full, rel, sizeof rel);
|
||||||
|
if (c->count) sA(c->s, ",");
|
||||||
|
sA(c->s, "{\"name\":");
|
||||||
|
sA_str(c->s, e->d_name);
|
||||||
|
sA(c->s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld,\"rel\":",
|
||||||
|
isdir ? "dir" : "file", isdir ? 0LL : (long long)st.st_size, (long long)st.st_mtime);
|
||||||
|
sA_str(c->s, rel);
|
||||||
|
sA(c->s, "}");
|
||||||
|
c->count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isdir && depth < SEARCH_MAX_DEPTH && search_walk(c, full, depth + 1)) { stop = 1; break; }
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0=OK(JSON 入 out);-1 白名单外;-2 路径不存在/非目录;-3 name 空。 */
|
||||||
|
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out)
|
||||||
|
{
|
||||||
|
if (!path_in_whitelist(abs_start)) return -1;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(abs_start, &st) < 0 || !S_ISDIR(st.st_mode)) return -2;
|
||||||
|
char needle[300];
|
||||||
|
name_lower(name ? name : "", needle, sizeof needle);
|
||||||
|
if (!needle[0]) return -3;
|
||||||
|
int want_type = 0;
|
||||||
|
if (typef && !strcmp(typef, "file")) want_type = 1;
|
||||||
|
else if (typef && !strcmp(typef, "dir")) want_type = 2;
|
||||||
|
else if (typef && typef[0]) return -4; /* 非法 type 值(api 层映射 422) */
|
||||||
|
if (max <= 0 || max > SEARCH_MAX_ITEMS) max = SEARCH_MAX_ITEMS;
|
||||||
|
search_ctx_t c;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
c.needle = needle; c.want_type = want_type; c.max = max; c.s = out;
|
||||||
|
char rel[1200];
|
||||||
|
make_rel(abs_start, rel, sizeof rel);
|
||||||
|
sA(out, "{\"path\":");
|
||||||
|
sA_str(out, abs_start);
|
||||||
|
sA(out, ",\"rel\":");
|
||||||
|
sA_str(out, rel);
|
||||||
|
sA(out, ",\"name\":");
|
||||||
|
sA_str(out, name);
|
||||||
|
sA(out, ",\"items\":[");
|
||||||
|
int stopped = search_walk(&c, abs_start, 1);
|
||||||
|
sA(out, "],\"count\":%d,\"truncated\":%s}", c.count,
|
||||||
|
(stopped || c.visited >= SEARCH_MAX_VISIT) ? "true" : "false");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int api_files_search(int sock, const req_t *r)
|
||||||
|
{
|
||||||
|
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||||
|
char path[1024] = "/";
|
||||||
|
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
|
||||||
|
char name[512] = "";
|
||||||
|
if (!qparam(r->query, "name", name, sizeof name) || !name[0])
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "缺 name(按名子串搜索)");
|
||||||
|
char typef[16] = "";
|
||||||
|
qparam(r->query, "type", typef, sizeof typef);
|
||||||
|
char maxs[16] = "";
|
||||||
|
int max = SEARCH_MAX_ITEMS;
|
||||||
|
if (qparam(r->query, "max", maxs, sizeof maxs)) { int m = atoi(maxs); if (m > 0) max = m; }
|
||||||
|
char abs[1024];
|
||||||
|
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs))
|
||||||
|
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
int rc = files_search_json(abs, name, typef, max, &s);
|
||||||
|
if (rc == -1) { sbuf_free(&s); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||||
|
if (rc == -2) { sbuf_free(&s); return send_err(sock, 404, "NOT_FOUND", "路径不存在或非目录"); }
|
||||||
|
if (rc == -4) { sbuf_free(&s); return send_err(sock, 422, "UNSUPPORTED", "type 须为 file|dir"); }
|
||||||
|
if (rc != 0) { sbuf_free(&s); return send_err(sock, 400, "BAD_REQUEST", "name 为空"); }
|
||||||
|
/* 放行前自校验(与 overview 同口径:括号平衡/内嵌 NUL/坏字节,浏览器 JSON.parse 不容忍) */
|
||||||
|
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||||||
|
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
|
||||||
|
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
|
||||||
|
if (chk) jv_free(chk);
|
||||||
|
if (bad) {
|
||||||
|
panel_log("files/search: SELF-CHECK fail (n=%zu)", s.n);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return send_err(sock, 500, "INTERNAL", "搜索结果输出未通过 JSON 自校验");
|
||||||
|
}
|
||||||
|
int out = send_json(sock, 200, s.b);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
int multipart_extract_file(const req_t *r, const char *outpath)
|
int multipart_extract_file(const req_t *r, const char *outpath)
|
||||||
{
|
{
|
||||||
const char *b = strstr(r->content_type, "boundary=");
|
const char *b = strstr(r->content_type, "boundary=");
|
||||||
|
|||||||
@@ -175,6 +175,160 @@ int api_net_dns_flush(int sock)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- F1-③ 蓝牙只读详情(FUTURE_MEOWPANEL §2.6):netmgrd MSG_NET_BT_CTL(0xE6) 读级五命令
|
||||||
|
* (status/paired/connected/a2dp/devices)聚合为一份 JSON。读级任意调用者(bt_cmd_is_write=读),
|
||||||
|
* 无阻塞(net_ipc 2s 超时兜底);子命令失败/坏字节 → 该子块空数组降级,绝不拖垮整卡、绝不发非法 JSON。 ---- */
|
||||||
|
|
||||||
|
/* 单发 0xE6 读命令;成功返回文本长度(>0),IPC/服务端失败 -1,坏 UTF-8 -2(该子块降级)。 */
|
||||||
|
static int bt_ctl_read(const char *cmd, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
m_net_cmd_t q; memset(&q, 0, sizeof q);
|
||||||
|
snprintf(q.cmd, sizeof q.cmd, "%s", cmd);
|
||||||
|
uint16_t t; uint32_t code;
|
||||||
|
int n = net_ipc(MSG_NET_BT_CTL, &q, sizeof q, &t, &code, NULL, out, cap);
|
||||||
|
if (n <= 0 || t != MSG_NET_BT_RESULT || code != MEOW_OK) return -1;
|
||||||
|
if (!utf8_valid_n(out, (size_t)n)) return -2;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bt_mac_ok(const char *m)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
if (i % 3 == 2) { if (m[i] != ':') return 0; }
|
||||||
|
else if (!is_hex(m[i])) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Device <MAC> <Name>" 行集 → JSON 数组项(不合法行跳过;名经 sA_str_n 消毒)。返回条数。 */
|
||||||
|
static int bt_devlines_json(sbuf_t *s, const char *txt, int max)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
const char *ln = txt;
|
||||||
|
while (ln && *ln && n < max) {
|
||||||
|
const char *nl = strchr(ln, '\n');
|
||||||
|
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||||
|
const char *p = ln;
|
||||||
|
while ((size_t)(p - ln) < ll && (*p == ' ' || *p == '\t')) p++;
|
||||||
|
if ((size_t)(ln + ll - p) >= 7 + 17 && !strncmp(p, "Device ", 7) && bt_mac_ok(p + 7)) {
|
||||||
|
const char *mac = p + 7;
|
||||||
|
const char *nm = mac + 17;
|
||||||
|
while ((size_t)(nm - ln) < ll && (*nm == ' ' || *nm == '\t')) nm++;
|
||||||
|
size_t nlen = ll - (size_t)(nm - ln);
|
||||||
|
while (nlen && (nm[nlen - 1] == ' ' || nm[nlen - 1] == '\t' || nm[nlen - 1] == '\r')) nlen--;
|
||||||
|
if (n) sA(s, ",");
|
||||||
|
sA(s, "{\"mac\":");
|
||||||
|
sA_str_n(s, mac, 17);
|
||||||
|
sA(s, ",\"name\":");
|
||||||
|
sA_str_n(s, nm, nlen);
|
||||||
|
sA(s, "}");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
ln = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内 " key=value"(空白/行尾收尾)取值。 */
|
||||||
|
static void bt_line_kv(const char *line, const char *key, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
out[0] = 0;
|
||||||
|
const char *v = strstr(line, key);
|
||||||
|
if (!v) return;
|
||||||
|
v += strlen(key);
|
||||||
|
size_t i = 0;
|
||||||
|
while (v[i] && v[i] != ' ' && v[i] != '\t' && v[i] != '\n' && v[i] != '\r' && i + 1 < cap) { out[i] = v[i]; i++; }
|
||||||
|
out[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=<0|1> role=<..>" 行集 → JSON 数组项。 */
|
||||||
|
static int bt_a2dp_json(sbuf_t *s, const char *txt, int max)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
const char *ln = txt;
|
||||||
|
while (ln && *ln && n < max) {
|
||||||
|
const char *nl = strchr(ln, '\n');
|
||||||
|
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||||
|
char line[320];
|
||||||
|
if (ll >= sizeof line) ll = sizeof line - 1;
|
||||||
|
memcpy(line, ln, ll); line[ll] = 0;
|
||||||
|
if (!strncmp(line, "A2DP ", 5) && strncmp(line, "A2DP none", 9) && bt_mac_ok(line + 5)) {
|
||||||
|
char codec[24], rate[16], chn[8], bits[8], acq[8], role[8];
|
||||||
|
bt_line_kv(line, " codec=", codec, sizeof codec);
|
||||||
|
bt_line_kv(line, " rate=", rate, sizeof rate);
|
||||||
|
bt_line_kv(line, " channels=", chn, sizeof chn);
|
||||||
|
bt_line_kv(line, " bits=", bits, sizeof bits);
|
||||||
|
bt_line_kv(line, " acquired=", acq, sizeof acq);
|
||||||
|
bt_line_kv(line, " role=", role, sizeof role);
|
||||||
|
if (n) sA(s, ",");
|
||||||
|
sA(s, "{\"mac\":");
|
||||||
|
sA_str_n(s, line + 5, 17);
|
||||||
|
sA(s, ",\"codec\":"); sA_str(s, codec);
|
||||||
|
sA(s, ",\"rate\":"); sA_str(s, rate);
|
||||||
|
sA(s, ",\"channels\":"); sA_str(s, chn);
|
||||||
|
sA(s, ",\"bits\":"); sA_str(s, bits);
|
||||||
|
sA(s, ",\"acquired\":%s", !strcmp(acq, "1") ? "true" : "false");
|
||||||
|
sA(s, ",\"role\":"); sA_str(s, role);
|
||||||
|
sA(s, "}");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
ln = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 五读文本聚合(任一 NULL=该子块空数组;st=NULL → {"available":false})。 */
|
||||||
|
int net_bt_aggregate(const char *st, const char *paired, const char *connected,
|
||||||
|
const char *a2dp, const char *devices, sbuf_t *s)
|
||||||
|
{
|
||||||
|
if (!st) { sA(s, "{\"available\":false}"); return -1; }
|
||||||
|
int avail = 0, powered = 0; long long paired_n = -1, connected_n = -1;
|
||||||
|
char addr[24] = "";
|
||||||
|
(void)sscanf(st, "bt available=%d powered=%d address=%23s paired=%lld connected=%lld",
|
||||||
|
&avail, &powered, addr, &paired_n, &connected_n);
|
||||||
|
if (!strcmp(addr, "-")) addr[0] = 0;
|
||||||
|
sA(s, "{\"available\":%s", avail ? "true" : "false");
|
||||||
|
sA(s, ",\"powered\":%s", powered ? "true" : "false");
|
||||||
|
sA(s, ",\"address\":"); sA_str(s, addr);
|
||||||
|
if (paired_n >= 0) sA(s, ",\"paired_n\":%lld", paired_n); else sA(s, ",\"paired_n\":null");
|
||||||
|
if (connected_n >= 0) sA(s, ",\"connected_n\":%lld", connected_n); else sA(s, ",\"connected_n\":null");
|
||||||
|
sA(s, ",\"paired\":["); if (paired) bt_devlines_json(s, paired, 32); sA(s, "]");
|
||||||
|
sA(s, ",\"connected\":["); if (connected) bt_devlines_json(s, connected, 16); sA(s, "]");
|
||||||
|
sA(s, ",\"a2dp\":["); if (a2dp) bt_a2dp_json(s, a2dp, 8); sA(s, "]");
|
||||||
|
sA(s, ",\"nearby\":["); if (devices) bt_devlines_json(s, devices, 16); sA(s, "]");
|
||||||
|
sA(s, "}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int api_net_bt(int sock)
|
||||||
|
{
|
||||||
|
char st[MEOW_IPC_MAX], pa[MEOW_IPC_MAX], co[MEOW_IPC_MAX], a2[MEOW_IPC_MAX], dv[MEOW_IPC_MAX];
|
||||||
|
int ns = bt_ctl_read("status", st, sizeof st);
|
||||||
|
if (ns <= 0) /* netmgrd 不可达/status 坏字节 → 整体降级(net 域惯例:200 available:false) */
|
||||||
|
return send_json(sock, 200, "{\"available\":false}");
|
||||||
|
int np = bt_ctl_read("paired", pa, sizeof pa);
|
||||||
|
int nc = bt_ctl_read("connected", co, sizeof co);
|
||||||
|
int na = bt_ctl_read("a2dp", a2, sizeof a2);
|
||||||
|
int nd = bt_ctl_read("devices", dv, sizeof dv);
|
||||||
|
sbuf_t s = {0};
|
||||||
|
net_bt_aggregate(st, np > 0 ? pa : NULL, nc > 0 ? co : NULL, na > 0 ? a2 : NULL, nd > 0 ? dv : NULL, &s);
|
||||||
|
/* 放行前自校验(与 overview 同口径:浏览器 JSON.parse 不容忍尾随垃圾/括号错位/内嵌 NUL/坏字节) */
|
||||||
|
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||||||
|
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
|
||||||
|
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
|
||||||
|
if (chk) jv_free(chk);
|
||||||
|
if (bad) {
|
||||||
|
FILE *df = fopen("/tmp/net_bt_bad.json", "w");
|
||||||
|
if (df && s.b) { fwrite(s.b, 1, s.n, df); fclose(df); }
|
||||||
|
panel_log("net/bt: SELF-CHECK fail, raw dumped /tmp/net_bt_bad.json");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return send_err(sock, 500, "INTERNAL", "蓝牙聚合输出未通过 JSON 自校验(原始数据已留 /tmp/net_bt_bad.json)");
|
||||||
|
}
|
||||||
|
int rc = send_json(sock, 200, s.b);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
int api_net_log(int sock)
|
int api_net_log(int sock)
|
||||||
{
|
{
|
||||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ int handle(int sock, req_t *r)
|
|||||||
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
||||||
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
||||||
@@ -381,6 +382,7 @@ int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
||||||
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */
|
||||||
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
||||||
|
|
||||||
/* ---- 应用 ---- */
|
/* ---- 应用 ---- */
|
||||||
@@ -634,7 +636,116 @@ int panel_selftest(void)
|
|||||||
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
|
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n");
|
/* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言;
|
||||||
|
* 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */
|
||||||
|
{
|
||||||
|
sbuf_t b = {0};
|
||||||
|
const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1";
|
||||||
|
const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n";
|
||||||
|
const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
|
||||||
|
const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n";
|
||||||
|
const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n";
|
||||||
|
if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; }
|
||||||
|
if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *br = jv_parse(b.b, (int)b.n);
|
||||||
|
if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *pw = jv_obj_get(br, "powered");
|
||||||
|
if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *pr = jv_obj_get(br, "paired");
|
||||||
|
if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *cn = jv_obj_get(br, "connected");
|
||||||
|
if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *a2p = jv_obj_get(br, "a2dp");
|
||||||
|
if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *cd = jv_obj_get(a2p->child, "codec");
|
||||||
|
if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *aq = jv_obj_get(a2p->child, "acquired");
|
||||||
|
if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_free(br);
|
||||||
|
sbuf_free(&b);
|
||||||
|
/* status NULL → available:false;子文本 NULL → 空数组 */
|
||||||
|
sbuf_t b2 = {0};
|
||||||
|
net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2);
|
||||||
|
if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; }
|
||||||
|
sbuf_free(&b2);
|
||||||
|
sbuf_t b3 = {0};
|
||||||
|
net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3);
|
||||||
|
jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL;
|
||||||
|
jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL;
|
||||||
|
if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; }
|
||||||
|
jv_free(b3r);
|
||||||
|
sbuf_free(&b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言;
|
||||||
|
* 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */
|
||||||
|
{
|
||||||
|
char tmpl[] = "/tmp/mp_search_XXXXXX";
|
||||||
|
char *root = mkdtemp(tmpl);
|
||||||
|
if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; }
|
||||||
|
char saved_root[1024];
|
||||||
|
snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf);
|
||||||
|
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root);
|
||||||
|
char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200];
|
||||||
|
snprintf(d1, sizeof d1, "%s/MeowOS", root);
|
||||||
|
snprintf(d2, sizeof d2, "%s/MeowOS/sub", root);
|
||||||
|
snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root);
|
||||||
|
snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root);
|
||||||
|
snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root);
|
||||||
|
int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0);
|
||||||
|
const char *mk[3] = { f1, f2, f3 };
|
||||||
|
for (int i = 0; i < 3 && setup_ok; i++) {
|
||||||
|
FILE *f = fopen(mk[i], "w");
|
||||||
|
if (!f) { setup_ok = 0; break; }
|
||||||
|
fputs("x", f); fclose(f);
|
||||||
|
}
|
||||||
|
if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; }
|
||||||
|
int fails = 0;
|
||||||
|
sbuf_t s = {0};
|
||||||
|
if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; }
|
||||||
|
jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL;
|
||||||
|
if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; }
|
||||||
|
jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL;
|
||||||
|
if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; }
|
||||||
|
jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL;
|
||||||
|
jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL;
|
||||||
|
if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; }
|
||||||
|
if (sr) jv_free(sr);
|
||||||
|
sbuf_free(&s);
|
||||||
|
/* type=dir 过滤:建 Songs 目录应仅中目录 */
|
||||||
|
char d3[1200];
|
||||||
|
snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root);
|
||||||
|
if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; }
|
||||||
|
sbuf_t s2 = {0};
|
||||||
|
if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) {
|
||||||
|
jv_t *sr2 = jv_parse(s2.b, (int)s2.n);
|
||||||
|
jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL;
|
||||||
|
jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL;
|
||||||
|
jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL;
|
||||||
|
if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) {
|
||||||
|
printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1;
|
||||||
|
}
|
||||||
|
if (sr2) jv_free(sr2);
|
||||||
|
} else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; }
|
||||||
|
sbuf_free(&s2);
|
||||||
|
/* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */
|
||||||
|
sbuf_t s3 = {0};
|
||||||
|
if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) {
|
||||||
|
if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; }
|
||||||
|
} else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; }
|
||||||
|
sbuf_free(&s3);
|
||||||
|
sbuf_t s4 = {0};
|
||||||
|
if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; }
|
||||||
|
if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; }
|
||||||
|
if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; }
|
||||||
|
sbuf_free(&s4);
|
||||||
|
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root);
|
||||||
|
rm_recursive(root);
|
||||||
|
rmdir(root);
|
||||||
|
if (fails) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -537,7 +537,10 @@ int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢
|
|||||||
void audio_out_close(void);
|
void audio_out_close(void);
|
||||||
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
|
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
|
||||||
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
|
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
|
||||||
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */
|
每音频会话(新 audiod 流 sid)首同步一次 + 值变才重发——切歌/新建
|
||||||
|
会话时 DAC 音量不残留默认响度(修「切歌音量骤增」);同会话内不
|
||||||
|
重复建连(不做 fd 风暴)。audio_out_open SINK_AUDIOD 成功后已自动调;
|
||||||
|
独立导出供宿主验证与复调。 */
|
||||||
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
||||||
int audio_out_buffered_ms(void);
|
int audio_out_buffered_ms(void);
|
||||||
|
|
||||||
|
|||||||
@@ -233,20 +233,27 @@ static void audiod_sync_volume(int pct)
|
|||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
|
/* 会话身份 = audiod 流 sid(g_sid):切歌/新建音频会话 = 每次 audio_out_open 都向 audiod 开一条
|
||||||
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
|
* 新流、audiod 分配新 sid(单调不重用)。此前用进程级 g_vol_synced「首次同步一次」——切歌(新会话)
|
||||||
static int g_vol_synced = 0;
|
* 时音量不再被重发到 audiod,DAC 落在会话建立/重配后的默认/上次残留值(衰减量 0=最响),造成
|
||||||
static int g_vol_last = -1;
|
* 「切歌音量骤增,按一下音量键才回落到设定值」(用户真机 2026-09-05)。audiod 只在 reconfigure
|
||||||
|
* 且参数变化时才 re-apply 音量(_audio_mark_reconfig_needed 同参数跳过),本会话重发是权威兜底。
|
||||||
|
* 修法:**每会话首同步一次**——新 sid(新会话)必重发一次当前音量;同会话内仅值变才重发。
|
||||||
|
* 每会话恰 1 条短连接,不回到「每次 open 都建连」的 fd 风暴(§2026-09-02 根因,vol_sync_sim 60 次
|
||||||
|
* 同值→1 连接)。音量值取自 settings.conf(audiod 权威同源同键),反转/真静音(255) 由 audiod
|
||||||
|
* dac_vol_set 负责,本层只传 pct(0-100)。 */
|
||||||
|
static int g_vol_synced_sid = -1; /* 上次音量同步针对的 audiod 流 sid;-1=从未(必发) */
|
||||||
|
static int g_vol_last = -1;
|
||||||
|
|
||||||
int audio_out_sync_volume(void)
|
int audio_out_sync_volume(void)
|
||||||
{
|
{
|
||||||
int pct = settings_read_volume();
|
int pct = settings_read_volume();
|
||||||
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
|
if (g_vol_synced_sid == g_sid && pct == g_vol_last) return 0; /* 本会话已同步且值未变:零新建连接 */
|
||||||
g_vol_synced = 1;
|
g_vol_synced_sid = g_sid;
|
||||||
g_vol_last = pct;
|
g_vol_last = pct;
|
||||||
audiod_sync_volume(pct);
|
audiod_sync_volume(pct);
|
||||||
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
|
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s, sid=%d)",
|
||||||
pct, settings_conf_path());
|
pct, settings_conf_path(), g_sid);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +386,7 @@ int audio_out_pause(int paused)
|
|||||||
}
|
}
|
||||||
g_sid = sid;
|
g_sid = sid;
|
||||||
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
|
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
|
||||||
|
audio_out_sync_volume(); /* 新会话(新 sid):音量权威兜底重发一次(DAC 复位后不残留默认响度) */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1032,10 +1032,18 @@ static int g_lyr_thr_on;
|
|||||||
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
||||||
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
|
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
|
||||||
|
|
||||||
|
/* 歌词拉取失败后的重试退避(ms,指数翻倍封顶 OS):掉线瞬间失败的请求**不**写死「无歌词」,
|
||||||
|
* 交回退避重试;网络恢复后下一轮到点即重拉成功(真机症状:掉线一会儿再重连后恒「暂无歌词」)。 */
|
||||||
|
#define LYRIC_RETRY_BASE_MS 1000
|
||||||
|
#define LYRIC_RETRY_MAX_MS (30 * 1000)
|
||||||
|
|
||||||
static void *lyric_worker(void *arg)
|
static void *lyric_worker(void *arg)
|
||||||
{
|
{
|
||||||
(void)arg;
|
(void)arg;
|
||||||
char last_id[24] = "";
|
char last_id[24] = "";
|
||||||
|
int last_ok = 1; /* last_id 对应结果是否成功(成功不重拉;失败记退避待重试) */
|
||||||
|
int retry_delay = 0; /* 失败后逐次翻倍的退避基值(ms) */
|
||||||
|
int64_t retry_at = 0; /* 单调时刻:下次重试;0=无待重试 */
|
||||||
while (!g_app_quit) {
|
while (!g_app_quit) {
|
||||||
usleep(300 * 1000);
|
usleep(300 * 1000);
|
||||||
char id[24];
|
char id[24];
|
||||||
@@ -1051,14 +1059,28 @@ static void *lyric_worker(void *arg)
|
|||||||
g_lyr_state = 0;
|
g_lyr_state = 0;
|
||||||
pthread_mutex_unlock(&g_lyrmu);
|
pthread_mutex_unlock(&g_lyrmu);
|
||||||
last_id[0] = 0;
|
last_id[0] = 0;
|
||||||
|
last_ok = 1;
|
||||||
|
retry_delay = 0;
|
||||||
|
retry_at = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!id[0]) continue;
|
if (!id[0]) continue;
|
||||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
|
||||||
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
int is_new = (strcmp(id, last_id) != 0);
|
||||||
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
if (is_new) {
|
||||||
* 不置 last_id → 降载恢复后下轮再拉。 */
|
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
||||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
||||||
|
* 不置 last_id → 降载恢复后下轮再拉。 */
|
||||||
|
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||||
|
last_ok = 0;
|
||||||
|
retry_delay = 0;
|
||||||
|
retry_at = 0;
|
||||||
|
} else {
|
||||||
|
if (last_ok) continue; /* 同一曲已成功拉完(含 NCM_OK 空词=真无词),不重拉 */
|
||||||
|
if (meow_cachegov_prefetch_paused()) continue;
|
||||||
|
if (retry_at > ncm_monotonic_ms()) continue; /* 失败退避未到 */
|
||||||
|
/* 到点重试:保留 retry_delay 以便失败时继续翻倍 */
|
||||||
|
}
|
||||||
lyric_clear();
|
lyric_clear();
|
||||||
pthread_mutex_lock(&g_lyrmu);
|
pthread_mutex_lock(&g_lyrmu);
|
||||||
g_lyr_state = 1; /* 加载中 */
|
g_lyr_state = 1; /* 加载中 */
|
||||||
@@ -1068,22 +1090,32 @@ static void *lyric_worker(void *arg)
|
|||||||
char *tl = (char *)malloc(32 * 1024);
|
char *tl = (char *)malloc(32 * 1024);
|
||||||
if (!lrc || !tl) {
|
if (!lrc || !tl) {
|
||||||
free(lrc); free(tl);
|
free(lrc); free(tl);
|
||||||
|
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
|
||||||
|
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
|
||||||
|
retry_at = ncm_monotonic_ms() + retry_delay;
|
||||||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
|
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
|
||||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||||
if (rc == NCM_OK) {
|
if (rc == NCM_OK) {
|
||||||
|
last_ok = 1; /* 真正结果:空词(NCM_OK 无词也算「已拉完」) */
|
||||||
|
retry_delay = 0;
|
||||||
|
retry_at = 0;
|
||||||
lyric_parse(lrc, tl);
|
lyric_parse(lrc, tl);
|
||||||
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
|
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
|
||||||
} else {
|
} else {
|
||||||
|
/* 拉取失败(网络/HTTP/会话):不写死「无词」,交回退避重试;UI 呈「加载中」而非「暂无歌词」 */
|
||||||
|
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
|
||||||
|
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
|
||||||
|
retry_at = ncm_monotonic_ms() + retry_delay;
|
||||||
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
|
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
|
||||||
}
|
}
|
||||||
free(lrc); free(tl);
|
free(lrc); free(tl);
|
||||||
|
|
||||||
pthread_mutex_lock(&g_lyrmu);
|
pthread_mutex_lock(&g_lyrmu);
|
||||||
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
||||||
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
|
g_lyr_state = last_ok ? 2 : 0; /* 2=已拉完(可 0 行=无词);0=失败待重试(UI 呈加载中) */
|
||||||
pthread_mutex_unlock(&g_lyrmu);
|
pthread_mutex_unlock(&g_lyrmu);
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ static volatile int g_load_err;
|
|||||||
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
|
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
|
||||||
|
|
||||||
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
|
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
|
||||||
* §17 滑动窗口:worker 两种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化首段;
|
* §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页
|
||||||
* ②滑窗补载:物化视口区间 [g_ens_lo,g_ens_hi)(ensure_range,盘缓存优先)。 */
|
* (≤NCM_SONG_BATCH 首,一次一页——不再首屏连拉多批补满窗口,大歌单阻塞久且触发风控);
|
||||||
|
* ②顺序下一页(g_ens_page=1):仅当用户滑到已分页底部(余量 ≤NCM_PL_PAGE_EDGE 首)时拉一页,
|
||||||
|
* 成功推进 g_paged_hi,拉完该歌单全部曲目即止;③回滚补物化:已分页区间内被窗口挤出的
|
||||||
|
* 失效槽按视口 ±24 补(盘缓存命中为主,不占网络分页额度)。 */
|
||||||
|
#define NCM_PL_PAGE_EDGE 20 /* 懒加载触发余量(首):视口底距已分页底部 ≤20 首才拉下一页 */
|
||||||
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static pthread_t g_plthr;
|
static pthread_t g_plthr;
|
||||||
static int g_plthr_on;
|
static int g_plthr_on;
|
||||||
@@ -37,9 +41,11 @@ static volatile int g_plloading;
|
|||||||
static volatile int g_plloaded_idx;
|
static volatile int g_plloaded_idx;
|
||||||
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
|
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
|
||||||
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
|
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
|
||||||
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
|
static volatile int g_ens_lo, g_ens_hi; /* 补载/下一页目标区间(全局下标) */
|
||||||
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
|
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
|
||||||
|
static volatile int g_ens_page; /* 1=顺序下一页任务(成功推进 g_paged_hi) */
|
||||||
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
|
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
|
||||||
|
static volatile int g_paged_hi; /* 已分页高水位:从顶部连续物化到的全局下标(下一页起点) */
|
||||||
|
|
||||||
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
|
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
|
||||||
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
|
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
|
||||||
@@ -239,12 +245,15 @@ static void pl_draw_footer(void *ctx)
|
|||||||
ncm_draw_footer(r_cont, foot);
|
ncm_draw_footer(r_cont, foot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */
|
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。
|
||||||
|
* 拉到数据后必须让列表项重新绑定/重绘:src2 每帧 live 取行(占位行 ↔ 已物化歌名/歌手),
|
||||||
|
* 失效列表带 + 整帧重画把刚物化的歌名/歌手填进各项(修「拉完曲目行仍 ... 加载中」)。 */
|
||||||
static void pl_apply(int reset_scroll)
|
static void pl_apply(int reset_scroll)
|
||||||
{
|
{
|
||||||
if (g_list) {
|
if (g_list) {
|
||||||
meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
|
meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
|
||||||
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
|
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
}
|
}
|
||||||
const ncm_playlist_t *pcur = pl_cur();
|
const ncm_playlist_t *pcur = pl_cur();
|
||||||
if (g_tb && pcur) {
|
if (g_tb && pcur) {
|
||||||
@@ -261,30 +270,24 @@ static void *playlist_worker(void *arg)
|
|||||||
(void)arg;
|
(void)arg;
|
||||||
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
|
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
|
||||||
ncm_playlist_t tpl;
|
ncm_playlist_t tpl;
|
||||||
int tsearch, tidx, tgen, first, lo, hi;
|
int tsearch, tidx, tgen, first, lo, hi, page;
|
||||||
pthread_mutex_lock(&g_plmu);
|
pthread_mutex_lock(&g_plmu);
|
||||||
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
|
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
|
||||||
tidx = g_tgt.idx; tgen = g_tgt.gen;
|
tidx = g_tgt.idx; tgen = g_tgt.gen;
|
||||||
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi;
|
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi; page = g_ens_page;
|
||||||
pthread_mutex_unlock(&g_plmu);
|
pthread_mutex_unlock(&g_plmu);
|
||||||
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
|
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
|
||||||
int rc;
|
int rc;
|
||||||
if (g_host_mock) rc = 0;
|
if (g_host_mock) rc = 0;
|
||||||
else if (first) {
|
else if (first) {
|
||||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
|
/* 首屏:快照式加载(detail 全量 trackIds + 物化第 0 页)。ctx 校验代次/目标,过期自动丢弃。
|
||||||
|
* §懒加载:首屏只拉一页(≤NCM_SONG_BATCH 首,一次仅一批)——不再 ensure_range 补满整个
|
||||||
|
* 滑动窗口(旧版首屏连拉 ≤3 批:5k+ 大歌单「我喜欢的音乐」长时间卡住不出列表,连续
|
||||||
|
* 请求还触发风控);后续页由 pl_maybe_ensure 在用户滑到已分页底部时逐页懒加载。 */
|
||||||
if (tidx < 0) rc = NCM_ERR_ARG;
|
if (tidx < 0) rc = NCM_ERR_ARG;
|
||||||
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
||||||
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗
|
if (rc == NCM_OK)
|
||||||
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈
|
g_paged_hi = (g_st.track_ids_count < NCM_SONG_BATCH) ? g_st.track_ids_count : NCM_SONG_BATCH;
|
||||||
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
|
|
||||||
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
|
|
||||||
if (rc == NCM_OK) {
|
|
||||||
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
|
|
||||||
if (full > NCM_SONG_BATCH) {
|
|
||||||
int r2 = appstate_song_ensure_range(0, full);
|
|
||||||
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
|
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
|
||||||
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
|
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
|
||||||
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
|
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
|
||||||
@@ -292,8 +295,10 @@ static void *playlist_worker(void *arg)
|
|||||||
"playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d",
|
"playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d",
|
||||||
tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc);
|
tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc);
|
||||||
} else {
|
} else {
|
||||||
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */
|
/* 补载:物化目标区间(顺序下一页 / 回滚补物化;盘缓存优先,批网格补载)。
|
||||||
|
* 顺序下一页成功 → 已分页高水位推进到 hi;部分失败不推进(占位行保留,滑到底部重试本页)。 */
|
||||||
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
|
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
|
||||||
|
if (rc == NCM_OK && page && hi > g_paged_hi) g_paged_hi = hi;
|
||||||
}
|
}
|
||||||
pthread_mutex_lock(&g_plmu);
|
pthread_mutex_lock(&g_plmu);
|
||||||
if (first) {
|
if (first) {
|
||||||
@@ -324,7 +329,25 @@ static void *playlist_worker(void *arg)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */
|
/* 发补载 worker(单飞;调用方先判 g_plthr_on)。page=1 为顺序下一页任务。 */
|
||||||
|
static void pl_ensure_post(int lo, int hi, int page)
|
||||||
|
{
|
||||||
|
g_ens_lo = lo; g_ens_hi = hi;
|
||||||
|
g_ens_first = 0;
|
||||||
|
g_ens_page = page;
|
||||||
|
g_plloading = 1;
|
||||||
|
g_plthr_on = 1;
|
||||||
|
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
|
||||||
|
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 懒加载分页触发(Bug 修①:旧版视口 ±24 触发 + 首屏连拉多批 → 大歌单连续拉多页、阻塞 + 风控):
|
||||||
|
* ① 顺序下一页:仅在视口底滑到「已分页底部」(余量 ≤NCM_PL_PAGE_EDGE 首)时拉下一页,
|
||||||
|
* 一次只拉一页(≤NCM_SONG_BATCH);g_paged_hi 达 track_ids_count = 全部拉完,停止。
|
||||||
|
* 远离已分页底部(快速猛滑到深处占位区)不拉——防连续拉多页触发风控。
|
||||||
|
* ② 回滚补物化:视口 ±24 落在「已分页区间内」的失效槽(窗口挤出)按旧逻辑补——
|
||||||
|
* 盘缓存(songmeta)命中为主,不消耗网络分页额度;区间外(未分页)一律不预拉。 */
|
||||||
static void pl_maybe_ensure(void)
|
static void pl_maybe_ensure(void)
|
||||||
{
|
{
|
||||||
if (g_plthr_on || g_host_mock) return;
|
if (g_plthr_on || g_host_mock) return;
|
||||||
@@ -334,23 +357,32 @@ static void pl_maybe_ensure(void)
|
|||||||
int scroll = meow_listview_scroll(g_list);
|
int scroll = meow_listview_scroll(g_list);
|
||||||
int first = scroll / APP_ROW_H;
|
int first = scroll / APP_ROW_H;
|
||||||
int last = (scroll + r_list.h) / APP_ROW_H + 1;
|
int last = (scroll + r_list.h) / APP_ROW_H + 1;
|
||||||
|
if (last > total) last = total;
|
||||||
|
int paged = g_paged_hi;
|
||||||
|
if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */
|
||||||
|
|
||||||
|
/* ① 顺序下一页(懒加载) */
|
||||||
|
if (paged < total) {
|
||||||
|
int margin = paged - last; /* 视口底距已分页底部余量(首;负=已越过) */
|
||||||
|
if (margin <= NCM_PL_PAGE_EDGE && margin >= -NCM_PL_PAGE_EDGE) {
|
||||||
|
int hi = paged + NCM_SONG_BATCH;
|
||||||
|
if (hi > total) hi = total;
|
||||||
|
pl_ensure_post(paged, hi, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ② 回滚补物化(仅已分页区间内) */
|
||||||
int lo = first - 24, hi = last + 24;
|
int lo = first - 24, hi = last + 24;
|
||||||
if (lo < 0) lo = 0;
|
if (lo < 0) lo = 0;
|
||||||
if (hi > total) hi = total;
|
if (hi > paged) hi = paged;
|
||||||
if (lo >= hi) return;
|
if (lo >= hi) return;
|
||||||
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
|
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
|
||||||
g_ens_lo = lo; g_ens_hi = hi;
|
pl_ensure_post(lo, hi, 0);
|
||||||
g_ens_first = 0;
|
|
||||||
g_plloading = 1;
|
|
||||||
g_plthr_on = 1;
|
|
||||||
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
|
|
||||||
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void playlist_refresh(void)
|
void playlist_refresh(void)
|
||||||
{
|
{
|
||||||
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; pl_apply(1); return; }
|
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; g_paged_hi = g_st.track_ids_count; pl_apply(1); return; }
|
||||||
if (g_plthr_on) return;
|
if (g_plthr_on) return;
|
||||||
g_load_err = 0;
|
g_load_err = 0;
|
||||||
g_ens_err = 0;
|
g_ens_err = 0;
|
||||||
@@ -373,7 +405,9 @@ void playlist_refresh(void)
|
|||||||
if (!appstate_playlist_window_is(g_tgt.pl.id))
|
if (!appstate_playlist_window_is(g_tgt.pl.id))
|
||||||
appstate_playlist_reset();
|
appstate_playlist_reset();
|
||||||
|
|
||||||
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
|
g_ens_first = 1; /* 首屏:重建索引 + 物化第 0 页 */
|
||||||
|
g_ens_page = 0;
|
||||||
|
g_paged_hi = 0; /* 分页高水位归零:首屏成功后由 worker 重标 */
|
||||||
g_pl_stale = 0;
|
g_pl_stale = 0;
|
||||||
g_plloading = 1;
|
g_plloading = 1;
|
||||||
g_plthr_on = 1;
|
g_plthr_on = 1;
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/* bench/lyric_sim.c — 歌词「掉线失败 → 重连 → 重试成功」宿主离线回归(0 真实请求)。
|
||||||
|
*
|
||||||
|
* 驱动真实 app/play/player_manager.c 的 lyric_worker(后台歌词线程),网络换成可注入的
|
||||||
|
* ncm_api_lyric 桩:第 1 次调用返回 NCM_ERR_NET(模拟掉线瞬间歌词请求失败),第 2 次返回
|
||||||
|
* NCM_OK + 一段 LRC(模拟网络恢复后重试成功)。
|
||||||
|
*
|
||||||
|
* 复现的真机症状(REPORT/任务):播放中歌词正常;网络掉线一会儿再重连后某曲恒「暂无歌词」。
|
||||||
|
* 根因:旧 lyric_worker 在**发起网络请求前**就把 last_id 记为已拉,且失败也置 lyric_state=2
|
||||||
|
* (=已拉完),故网络失败被写死成「无歌词」,重连后永不重试。本机回归:
|
||||||
|
* ① 第 1 次失败后 lyric_state() 必须**不是** 2(可重试),且无词(lyric_count()==0);
|
||||||
|
* ② 推进单调时钟越过退避点(模拟重连)后必须**自动重试**并成功(第 2 次调用)+ 词展示
|
||||||
|
* (lyric_count()>0、lyric_state()==2)。
|
||||||
|
* 旧代码在 ② 上 FAIL(g_lyr_state 恒 2、第 2 次调用永不发生)。
|
||||||
|
*
|
||||||
|
* 编译(宿主,ncm_app 目录下;gc-sections 只保留歌词路径,避免链整个播放链的符号):
|
||||||
|
* CC=gcc F="-ffunction-sections -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd -Ivendor/jsmn"
|
||||||
|
* $CC $F -c bench/lyric_sim.c -o /tmp/ly.o
|
||||||
|
* $CC $F -c app/play/player_manager.c -o /tmp/ly_pm.o
|
||||||
|
* $CC $F -c app/logic/lyric.c -o /tmp/ly_lrc.o
|
||||||
|
* $CC -Wl,--gc-sections -o lyric_sim /tmp/ly.o /tmp/ly_pm.o /tmp/ly_lrc.o -lpthread -lm
|
||||||
|
* 运行:./lyric_sim (PASS/FAIL 退出码)
|
||||||
|
*/
|
||||||
|
#include "../app/ncm_app_internal.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h> /* usleep */
|
||||||
|
|
||||||
|
/* ---- 全局状态(app.c 定义;本 sim 提供桩) ---- */
|
||||||
|
ncm_app_state_t g_st;
|
||||||
|
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||||
|
volatile int g_app_quit = 0;
|
||||||
|
volatile int g_net_ready = 0;
|
||||||
|
int g_host_mock = 1;
|
||||||
|
int g_dark = 0;
|
||||||
|
|
||||||
|
/* ---- 可控单调时钟(lyric_worker 用 ncm_monotonic_ms 做失败退避) ---- */
|
||||||
|
static int64_t g_now_ms = 1000;
|
||||||
|
int64_t ncm_monotonic_ms(void) { return g_now_ms; }
|
||||||
|
|
||||||
|
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||||
|
{
|
||||||
|
size_t n = 0;
|
||||||
|
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||||||
|
while (src[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||||
|
|
||||||
|
/* appstate 桩:单曲队列(idx 0 恒有效,song id=1000) */
|
||||||
|
int appstate_queue_count(void) { return 1; }
|
||||||
|
|
||||||
|
/* page-cache 治理桩:恒非暂停(不挡歌词预取) */
|
||||||
|
int meow_cachegov_prefetch_paused(void) { return 0; }
|
||||||
|
|
||||||
|
/* 媒体会话歌词上报桩:失败上报空、成功上报 LRC;计数供断言 */
|
||||||
|
static int g_report_empty;
|
||||||
|
static int g_report_lyric;
|
||||||
|
static char g_report_last_lrc[256];
|
||||||
|
void ipc_media_lyric_report(const char *lrc, const char *tl)
|
||||||
|
{
|
||||||
|
(void)tl;
|
||||||
|
if (lrc && lrc[0]) { g_report_lyric++; ncm_strlcpy(g_report_last_lrc, lrc, sizeof g_report_last_lrc); }
|
||||||
|
else { g_report_empty++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词 API 桩:第 1 次=网络失败,其后=成功返回 LRC。 */
|
||||||
|
static int g_fetch_calls;
|
||||||
|
int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
|
||||||
|
char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz)
|
||||||
|
{
|
||||||
|
(void)device_id; (void)cookie; (void)id; (void)tlrc; (void)tlrc_sz;
|
||||||
|
g_fetch_calls++;
|
||||||
|
if (g_fetch_calls == 1) {
|
||||||
|
if (lrc && lrc_sz) lrc[0] = 0; /* 掉线失败:不写词 */
|
||||||
|
return NCM_ERR_NET; /* 网络错误(掉线) */
|
||||||
|
}
|
||||||
|
static const char body[] =
|
||||||
|
"[00:00.00]Line one\n[00:05.00]Line two\n[00:10.00]Line three\n";
|
||||||
|
if (lrc && lrc_sz) snprintf(lrc, lrc_sz, "%s", body);
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wait_ms(int ms)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ms; i += 20) {
|
||||||
|
usleep(20000);
|
||||||
|
if (g_app_quit) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wait_until(const volatile int *val, int target, int timeout_ms)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < timeout_ms; i += 10) {
|
||||||
|
if (*val >= target) return 1;
|
||||||
|
usleep(10000);
|
||||||
|
}
|
||||||
|
return (*val >= target);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memset(&g_st, 0, sizeof g_st);
|
||||||
|
pthread_mutex_init(&g_play.mu, NULL);
|
||||||
|
g_play.state = NCM_PC_PLAYING;
|
||||||
|
g_play.cur_song = 0;
|
||||||
|
g_play.play_song_valid = 1;
|
||||||
|
ncm_strlcpy(g_play.play_song.id, "1000", sizeof g_play.play_song.id);
|
||||||
|
|
||||||
|
if (player_manager_load_lyric() != NCM_OK) { printf("FAIL start worker\n"); return 1; }
|
||||||
|
|
||||||
|
int ok = 1;
|
||||||
|
|
||||||
|
/* ① 第 1 次拉取失败(网络错误):须可重试(state!=2)、无词 */
|
||||||
|
if (!wait_until(&g_fetch_calls, 1, 3000)) { printf("FAIL first fetch never happened\n"); return 1; }
|
||||||
|
wait_ms(400); /* 让 worker 完成失败收尾(state=0, retry_at 已算) */
|
||||||
|
int st1 = lyric_state();
|
||||||
|
int n1 = lyric_count();
|
||||||
|
int ok1 = (g_fetch_calls == 1 && st1 != 2 && n1 == 0);
|
||||||
|
printf("step1 failure: calls=%d state=%d (expect !=2) count=%d rep_empty=%d\n",
|
||||||
|
g_fetch_calls, st1, n1, g_report_empty);
|
||||||
|
printf(" -> %s\n", ok1 ? "PASS" : "FAIL");
|
||||||
|
ok &= ok1;
|
||||||
|
|
||||||
|
/* 模拟网络重连:推进单调时钟越过失败退避点 */
|
||||||
|
g_now_ms = 10000;
|
||||||
|
|
||||||
|
/* ② 重试自动发生并成功:第 2 次调用、state=2、词已展示 */
|
||||||
|
if (!wait_until(&g_fetch_calls, 2, 4000)) {
|
||||||
|
printf("FAIL retry never fired (calls=%d) — old code keeps state=2, no retry\n", g_fetch_calls);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int st2 = lyric_state();
|
||||||
|
int n2 = lyric_count();
|
||||||
|
int ok2 = (g_fetch_calls == 2 && st2 == 2 && n2 > 0 && g_report_lyric >= 1);
|
||||||
|
printf("step2 retry: calls=%d state=%d count=%d rep_lyric=%d lrc=%s\n",
|
||||||
|
g_fetch_calls, st2, n2, g_report_lyric, g_report_last_lrc);
|
||||||
|
printf(" -> %s\n", ok2 ? "PASS" : "FAIL");
|
||||||
|
ok &= ok2;
|
||||||
|
|
||||||
|
g_app_quit = 1;
|
||||||
|
printf("lyric_sim overall -> %s\n", ok ? "PASS" : "FAIL");
|
||||||
|
fflush(stdout);
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
/* bench/plpage_sim.c — 歌单页「懒加载分页 + 加载后列表刷新」离线回归(0 真实 NCM 请求)。
|
||||||
|
*
|
||||||
|
* 驱动真实 app/ui/playlist.c(页面打开 / 滚动触发 / worker / tick 消费全链路)+ 真实
|
||||||
|
* appstate.c(滑动窗口)+ 真实 songmeta.c(/tmp 盘缓存)+ 宿主 SDK(meow_listview/draw 全真,
|
||||||
|
* 与 ncm_host 同一批 out/host/sdk_*.o)。网络端点(ncm_api_playlist_detail /
|
||||||
|
* ncm_api_song_detail)为本地合成 stub(计调用次数、可注入单批失败)。
|
||||||
|
*
|
||||||
|
* Bug1(大歌单懒加载分页)验证点:
|
||||||
|
* ① 打开 400 首歌单:首屏 = 1 detail + 恰 1 批 song_detail(物化 [0,100),第 100 首未物化)
|
||||||
|
* ——不再首屏连拉多批补满窗口(旧行为 ≤3 批连发,5k+ 歌单阻塞 + 风控);
|
||||||
|
* ② 顶部小幅滚动(视口底距已分页底部 >20 首):不发任何新请求;
|
||||||
|
* ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败时 g_paged_hi 不推进(占位保留),
|
||||||
|
* 再滑到底部重试同一页成功(一次一页,不连拉);
|
||||||
|
* ④ 逐页到底(400 = 4 页);全部拉完后滑到尾部:不再发请求(停止);
|
||||||
|
* ⑤ 回滚到顶部:失效槽(窗口挤出)由盘缓存补物化,song_detail 调用数 0 增长(不占网络额度);
|
||||||
|
* Bug2(加载完列表刷新)验证点:
|
||||||
|
* ⑥ 首屏/每页 worker 完成经 playlist_tick 消费后,整帧重画计数(--wrap=meow_ui_paint_full)
|
||||||
|
* 递增,且新物化行经 appstate_song_copy 取到真实歌名/歌手(= pl_src2 将绑定真实数据,
|
||||||
|
* 不再是「... 加载中」占位)。
|
||||||
|
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
|
||||||
|
*
|
||||||
|
* 编译/运行见 build_ncm_sims.sh「plpage_sim」段(宿主,ncm_app 目录下)。
|
||||||
|
*/
|
||||||
|
#include "../app/ncm_app_internal.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
ncm_app_state_t g_st;
|
||||||
|
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||||
|
volatile int g_app_quit = 0;
|
||||||
|
volatile int g_net_ready = 0;
|
||||||
|
int g_host_mock = 0; /* 真实分页链路(不走 mock 跳网分支) */
|
||||||
|
|
||||||
|
/* ---- core 符号最小实现(同 playlist_race_sim 口径) ---- */
|
||||||
|
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||||
|
{
|
||||||
|
size_t n = 0;
|
||||||
|
if (sz) {
|
||||||
|
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
|
||||||
|
dst[n] = 0;
|
||||||
|
}
|
||||||
|
while (src[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
int64_t ncm_now_ms(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- cover / 播放 stub(本 sim 不测封面与播放;playlist.c 引用,链接需要) ---- */
|
||||||
|
void cover_request(const char *url, const char *song_id) { (void)url; (void)song_id; }
|
||||||
|
meow_img *cover_get(void) { return NULL; }
|
||||||
|
void cover_put(void) {}
|
||||||
|
int cover_changed(void) { return 0; }
|
||||||
|
void cover_ack(void) {}
|
||||||
|
void cover_cache_reset(void) {}
|
||||||
|
void player_play_song(int idx) { (void)idx; }
|
||||||
|
|
||||||
|
/* ---- core 符号(appstate.c 引用;运行时不到达,仅链接需要) ---- */
|
||||||
|
int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t uid_sz,
|
||||||
|
char *nick, size_t nick_sz)
|
||||||
|
{ (void)device_id;(void)cookie;(void)uid;(void)uid_sz;(void)nick;(void)nick_sz; return NCM_ERR_AUTH; }
|
||||||
|
int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; }
|
||||||
|
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
|
||||||
|
int limit, int offset, ncm_playlist_t *out, int max, int *out_n)
|
||||||
|
{ (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; }
|
||||||
|
|
||||||
|
/* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */
|
||||||
|
#define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */
|
||||||
|
static int g_detail_calls;
|
||||||
|
static int g_song_calls; /* song_detail 成功批次数 */
|
||||||
|
static int g_song_attempts; /* song_detail 发起次数(含失败批) */
|
||||||
|
static int g_song_fail_n; /* 接下来 N 批 song_detail 网络失败模拟 */
|
||||||
|
|
||||||
|
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
|
||||||
|
char *name, size_t name_sz, char *cover, size_t cover_sz,
|
||||||
|
int64_t *ids, int id_cap, int *out_id_n)
|
||||||
|
{
|
||||||
|
(void)device_id; (void)cookie; (void)playlist_id; (void)cover_sz;
|
||||||
|
g_detail_calls++;
|
||||||
|
if (name) snprintf(name, name_sz, "Sim Playlist");
|
||||||
|
if (cover) cover[0] = 0;
|
||||||
|
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
|
||||||
|
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i;
|
||||||
|
*out_id_n = n;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
|
||||||
|
ncm_song_t *out, int max, int *out_n)
|
||||||
|
{
|
||||||
|
(void)device_id; (void)cookie;
|
||||||
|
g_song_attempts++;
|
||||||
|
if (g_song_fail_n > 0) { /* 单批网络失败模拟(失败不推进分页高水位 → 同页重试) */
|
||||||
|
g_song_fail_n--;
|
||||||
|
if (out_n) *out_n = 0;
|
||||||
|
return NCM_ERR_NET;
|
||||||
|
}
|
||||||
|
g_song_calls++;
|
||||||
|
int m = (n < max) ? n : max;
|
||||||
|
for (int i = 0; i < m; i++) {
|
||||||
|
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
|
||||||
|
snprintf(out[i].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
|
||||||
|
out[i].duration_ms = 200000;
|
||||||
|
out[i].fee = 0;
|
||||||
|
}
|
||||||
|
if (out_n) *out_n = m;
|
||||||
|
return NCM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Bug2 证据:整帧重画计数(链接时 --wrap=meow_ui_paint_full 重定向到此) ---- */
|
||||||
|
extern void __real_meow_ui_paint_full(void);
|
||||||
|
static int g_paint_full_calls;
|
||||||
|
void __wrap_meow_ui_paint_full(void)
|
||||||
|
{
|
||||||
|
g_paint_full_calls++;
|
||||||
|
__real_meow_ui_paint_full();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 驱动辅助:模拟 app_on_tick 周期性消费 worker 完成 ---- */
|
||||||
|
static int pump_until(int (*cond)(void), int timeout_ms)
|
||||||
|
{
|
||||||
|
for (int t = 0; t < timeout_ms; t += 5) {
|
||||||
|
playlist_tick(0);
|
||||||
|
if (cond()) {
|
||||||
|
/* 再喂若干 tick:确保完成态被消费(g_plthr_on 清零),后续滚动可再触发 */
|
||||||
|
for (int k = 0; k < 30; k++) { playlist_tick(0); usleep(2000); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
usleep(5000);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond, fmt, ...) do { \
|
||||||
|
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||||
|
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* 条件(C 无闭包,用全局目标值) */
|
||||||
|
static int g_want_calls;
|
||||||
|
static int cond_calls(void) { return g_song_calls >= g_want_calls; }
|
||||||
|
static int g_want_attempts;
|
||||||
|
static int cond_attempts(void) { return g_song_attempts >= g_want_attempts; }
|
||||||
|
static int cond_first_screen(void)
|
||||||
|
{
|
||||||
|
return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */
|
||||||
|
static void scroll_to_row(int row)
|
||||||
|
{
|
||||||
|
static int cur;
|
||||||
|
int d = row - cur;
|
||||||
|
cur = row;
|
||||||
|
playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN,
|
||||||
|
(d >= 0 ? d : -d) * APP_ROW_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n",
|
||||||
|
SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH);
|
||||||
|
|
||||||
|
/* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */
|
||||||
|
setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0);
|
||||||
|
meow_ui_opts o = { APP_W, APP_H };
|
||||||
|
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
|
||||||
|
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||||
|
ncm_on_theme(MEOW_THEME_DARK);
|
||||||
|
|
||||||
|
/* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */
|
||||||
|
char cdt[] = "/tmp/plpage_cache_XXXXXX";
|
||||||
|
if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
|
||||||
|
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt);
|
||||||
|
g_st.playlist_count = 1;
|
||||||
|
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
|
||||||
|
snprintf(g_st.playlists[0].name, sizeof g_st.playlists[0].name, "SimPlaylist");
|
||||||
|
g_st.playlists[0].track_count = SIM_TOTAL;
|
||||||
|
|
||||||
|
ncm_song_t sq;
|
||||||
|
int paint0;
|
||||||
|
|
||||||
|
/* ---- ① Bug1:打开歌单 → 首屏恰 1 detail + 1 批 song_detail(物化 [0,100)) ---- */
|
||||||
|
printf("== case 1: 首屏只拉一页 ==\n");
|
||||||
|
paint0 = g_paint_full_calls;
|
||||||
|
playlist_view_create();
|
||||||
|
playlist_open(0);
|
||||||
|
CHECK(pump_until(cond_first_screen, 8000), "first-screen loaded (track_ids=%d calls=%d)",
|
||||||
|
g_st.track_ids_count, g_song_calls);
|
||||||
|
CHECK(g_detail_calls == 1, "detail calls=%d (expect 1)", g_detail_calls);
|
||||||
|
CHECK(g_song_calls == 1, "song_detail calls=%d (expect 1, 不再首屏连拉多批)", g_song_calls);
|
||||||
|
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d)",
|
||||||
|
g_st.track_ids_count, SIM_TOTAL);
|
||||||
|
CHECK(appstate_song_copy(99, &sq) == 0 && strcmp(sq.name, "Song1000099") == 0,
|
||||||
|
"row 99 materialized name='%s'", sq.name);
|
||||||
|
CHECK(appstate_song_copy(100, &sq) != 0, "row 100 NOT materialized yet (placeholder)");
|
||||||
|
/* Bug2:首屏完成 → 重画计数递增 + 行绑定真实歌名/歌手(非占位) */
|
||||||
|
CHECK(g_paint_full_calls > paint0, "paint_full after first-screen %d -> %d (list repainted)",
|
||||||
|
paint0, g_paint_full_calls);
|
||||||
|
CHECK(appstate_song_copy(5, &sq) == 0 && strcmp(sq.art_name, "Artist1000005") == 0,
|
||||||
|
"row 5 binds real artist='%s' (src2 填真实数据,非「... 加载中」)", sq.art_name);
|
||||||
|
|
||||||
|
/* ---- ② 顶部小幅滚动:不触发任何新请求(距已分页底部 >20 首) ---- */
|
||||||
|
printf("== case 2: 顶部滚动不拉页 ==\n");
|
||||||
|
scroll_to_row(10);
|
||||||
|
int a0 = g_song_attempts;
|
||||||
|
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
|
||||||
|
CHECK(g_song_attempts == a0, "song_detail attempts=%d (unchanged, 顶部滚动不拉页)", g_song_attempts);
|
||||||
|
|
||||||
|
/* ---- ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败不推进,再滑重试同页成功 ---- */
|
||||||
|
printf("== case 3: 触底拉下一页(一次一页;失败重试同页) ==\n");
|
||||||
|
g_song_fail_n = 1; /* 注入:下一次 song_detail 网络失败 */
|
||||||
|
int att_before = g_song_attempts;
|
||||||
|
scroll_to_row(90); /* 视口底 ≈97 行,余量 ≤20 → 触发第 2 页(失败) */
|
||||||
|
g_want_attempts = att_before + 1;
|
||||||
|
CHECK(pump_until(cond_attempts, 8000), "page-2 first attempt fired (attempts=%d)", g_song_attempts);
|
||||||
|
CHECK(g_song_calls == 1, "page-2 failed -> calls=%d (g_paged_hi 不推进,占位保留)", g_song_calls);
|
||||||
|
CHECK(appstate_song_copy(199, &sq) != 0, "row 199 still placeholder after failed page");
|
||||||
|
scroll_to_row(91); /* 再滑(滚动事件)→ 重试同一页 */
|
||||||
|
g_want_calls = 2;
|
||||||
|
CHECK(pump_until(cond_calls, 8000), "page 2 retried & loaded (calls=%d attempts=%d)",
|
||||||
|
g_song_calls, g_song_attempts);
|
||||||
|
CHECK(g_song_attempts == att_before + 2,
|
||||||
|
"attempts +%d (1 失败 + 1 重试同页 = 一次一页,未连拉)", g_song_attempts - att_before);
|
||||||
|
CHECK(appstate_song_copy(199, &sq) == 0 && strcmp(sq.name, "Song1000199") == 0,
|
||||||
|
"row 199 materialized name='%s'", sq.name);
|
||||||
|
CHECK(g_paint_full_calls > paint0, "paint_full after page-2 consumed (=%d)", g_paint_full_calls);
|
||||||
|
|
||||||
|
/* ---- ④ 逐页到底;全部拉完后滑到尾部:不再发请求 ---- */
|
||||||
|
printf("== case 4: 逐页到底 + 拉完即止 ==\n");
|
||||||
|
att_before = g_song_attempts;
|
||||||
|
scroll_to_row(190); /* 余量 ≤20 → 触发第 3 页 */
|
||||||
|
g_want_calls = 3;
|
||||||
|
CHECK(pump_until(cond_calls, 8000), "page 3 loaded (calls=%d)", g_song_calls);
|
||||||
|
scroll_to_row(290); /* 余量 ≤20 → 触发第 4 页(末页) */
|
||||||
|
g_want_calls = 4;
|
||||||
|
CHECK(pump_until(cond_calls, 8000), "page 4 (last) loaded (calls=%d)", g_song_calls);
|
||||||
|
CHECK(g_song_attempts == att_before + 2, "attempts +%d (第 3、4 页恰各 1 批)",
|
||||||
|
g_song_attempts - att_before);
|
||||||
|
CHECK(appstate_song_copy(399, &sq) == 0 && strcmp(sq.name, "Song1000399") == 0,
|
||||||
|
"tail row 399 materialized name='%s'", sq.name);
|
||||||
|
att_before = g_song_attempts;
|
||||||
|
scroll_to_row(390); /* 已在尾部:全部拉完 → 不再发请求 */
|
||||||
|
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
|
||||||
|
CHECK(g_song_attempts == att_before, "all loaded -> scroll at tail: attempts=%d (unchanged)",
|
||||||
|
g_song_attempts);
|
||||||
|
|
||||||
|
/* ---- ⑤ 回滚到顶部:失效槽(窗口挤出)盘缓存补物化,song_detail 0 增长 ---- */
|
||||||
|
printf("== case 5: 回滚补物化走盘缓存(0 网络) ==\n");
|
||||||
|
att_before = g_song_attempts;
|
||||||
|
scroll_to_row(0); /* 回顶部:窗口已滑走,行 0-35 失效 → 盘缓存补 */
|
||||||
|
for (int k = 0; k < 400 && !appstate_range_loaded(0, 30); k++) { playlist_tick(0); usleep(5000); }
|
||||||
|
CHECK(appstate_range_loaded(0, 30), "rows 0-29 re-materialized after scroll-back");
|
||||||
|
CHECK(g_song_attempts == att_before, "scroll-back refill: song_detail attempts=%d "
|
||||||
|
"(unchanged, 盘缓存命中不占网络)", g_song_attempts);
|
||||||
|
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song1000000") == 0,
|
||||||
|
"row 0 re-materialized name='%s'", sq.name);
|
||||||
|
|
||||||
|
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||||
|
/* 清理临时盘缓存(≤400 个小文件) */
|
||||||
|
{
|
||||||
|
char cmd[600];
|
||||||
|
snprintf(cmd, sizeof cmd, "rm -rf %s", cdt);
|
||||||
|
if (system(cmd) != 0) fprintf(stderr, "cleanup failed: %s\n", cdt);
|
||||||
|
}
|
||||||
|
meow_ui_shutdown();
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -40,8 +40,11 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
|||||||
}
|
}
|
||||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||||
|
|
||||||
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */
|
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接。
|
||||||
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; }
|
* 会话建立→音量同步用例(§切歌音量骤增回归)需要 audio_out_open 真正「开新流/分配新 sid」,
|
||||||
|
* 故 meow_audio_open 桩改为返回递增 sid(模拟 audiod 单调分配),供 §切歌 会话切换复现。 */
|
||||||
|
static int g_sim_sid = 0;
|
||||||
|
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return ++g_sim_sid; }
|
||||||
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
|
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
|
||||||
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
|
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
|
||||||
int meow_audio_close(int sid) { (void)sid; return 0; }
|
int meow_audio_close(int sid) { (void)sid; return 0; }
|
||||||
@@ -161,6 +164,46 @@ int main(void)
|
|||||||
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
|
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* §切歌音量骤增回归:会话建立→音量同步。切歌 = 新 audio_out_open = 新 audiod 流(新 sid),
|
||||||
|
* 必须**每会话重发一次**当前音量——此前 g_vol_synced 进程级「首同步一次」,新会话被跳过,
|
||||||
|
* DAC 停在会话建立后的默认响度(切歌骤增,按音量键才回落)。用真实 audio_out_open/close
|
||||||
|
* (meow_audio_open 桩返回递增 sid)断言:新会话必 +1 连接(重发音量),同会话内重复 sync 不新增。 */
|
||||||
|
{
|
||||||
|
int base = g_nconn; /* 上面值变重发测试后基线 = 2 条连接 */
|
||||||
|
/* 会话 1(首开):应重发 1 条 VOLUME,pct==当前 settings(50) */
|
||||||
|
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session1 audio_out_open\n"); fail = 1; }
|
||||||
|
usleep(50000);
|
||||||
|
if (g_nconn != base + 1) {
|
||||||
|
printf("FAIL session1 establish: conns=%d (expect %d) — 新会话未重发音量\n", (int)g_nconn, base + 1);
|
||||||
|
fail = 1;
|
||||||
|
} else if (g_got_pct != 50) {
|
||||||
|
printf("FAIL session1 establish: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
|
||||||
|
} else {
|
||||||
|
printf("PASS session1 establish -> 1 volume sync (pct=50)\n");
|
||||||
|
}
|
||||||
|
/* 同会话内反复 sync(同值):不得新建连接(防 fd 风暴) */
|
||||||
|
for (int i = 0; i < 20; i++) audio_out_sync_volume();
|
||||||
|
usleep(50000);
|
||||||
|
if (g_nconn != base + 1) {
|
||||||
|
printf("FAIL same-session repeat sync: conns=%d (expect %d)\n", (int)g_nconn, base + 1);
|
||||||
|
fail = 1;
|
||||||
|
} else {
|
||||||
|
printf("PASS same-session repeat sync -> no new connection (storm-free)\n");
|
||||||
|
}
|
||||||
|
audio_out_close();
|
||||||
|
/* 会话 2(切歌):再开 → 必须再重发 1 条(新 sid),pct==50 */
|
||||||
|
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session2 audio_out_open\n"); fail = 1; }
|
||||||
|
usleep(50000);
|
||||||
|
if (g_nconn != base + 2) {
|
||||||
|
printf("FAIL session2 (song switch): conns=%d (expect %d) — 切歌未重发音量\n", (int)g_nconn, base + 2);
|
||||||
|
fail = 1;
|
||||||
|
} else if (g_got_pct != 50) {
|
||||||
|
printf("FAIL session2 switch: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
|
||||||
|
} else {
|
||||||
|
printf("PASS session2 (song switch) -> new volume sync (pct=50)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g_stop = 1;
|
g_stop = 1;
|
||||||
pthread_join(th, NULL);
|
pthread_join(th, NULL);
|
||||||
unlink(g_sock);
|
unlink(g_sock);
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o \
|
|||||||
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块)
|
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块)
|
||||||
run_sim window_sim
|
run_sim window_sim
|
||||||
|
|
||||||
|
echo "==> build plpage_sim(歌单页懒加载分页 + 加载后列表刷新;真实 playlist.c 全链路 + 宿主 SDK,0 网络。"
|
||||||
|
echo " 依赖 out/host/sdk_*.o:若缺请先跑 build_ncm_host.sh)"
|
||||||
|
PPINC="-DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac"
|
||||||
|
for f in bench/plpage_sim.c app/ui/playlist.c app/logic/appstate.c app/logic/songmeta.c app/ui/chrome.c app/ui/ncm_colors.c; do
|
||||||
|
b=$(basename "$f" .c)
|
||||||
|
gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections $PPINC -c "$f" -o /tmp/pp_$b.o
|
||||||
|
done
|
||||||
|
gcc -O2 -Wl,--gc-sections -Wl,--wrap=meow_ui_paint_full -o plpage_sim \
|
||||||
|
/tmp/pp_plpage_sim.o /tmp/pp_playlist.o /tmp/pp_appstate.o /tmp/pp_songmeta.o /tmp/pp_chrome.o /tmp/pp_ncm_colors.o \
|
||||||
|
out/host/sdk_*.o -lz -lpthread -lm
|
||||||
|
run_sim plpage_sim
|
||||||
|
|
||||||
echo "==> build search_sim"
|
echo "==> build search_sim"
|
||||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
|
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
|
||||||
-c bench/search_sim.c -o /tmp/sh.o
|
-c bench/search_sim.c -o /tmp/sh.o
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
|
|
||||||
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
|
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
|
||||||
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
|
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
|
||||||
* 仅 playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 2MB。 */
|
* playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留 2MB;
|
||||||
|
* user_playlist(120 首歌单列表)用 NCM_RESP_MAX(1MB),见 ncm_api_user_playlist。 */
|
||||||
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
|
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
|
||||||
#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */
|
#define RESP_MED (256 * 1024) /* lyric/song_detail/cloudsearch:实际 ≤100KB */
|
||||||
|
|
||||||
const char *ncm_api_level_str(int level) {
|
const char *ncm_api_level_str(int level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
@@ -279,8 +280,11 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
|
|||||||
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
|
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
|
||||||
|
|
||||||
api_json_t c; memset(&c, 0, sizeof c);
|
api_json_t c; memset(&c, 0, sizeof c);
|
||||||
|
/* 我的歌单列表:真机 120 首歌单响应可超 RESP_MED(256KB) → ncm_http 回 NCM_ERR_FULL(-11)
|
||||||
|
* (日志 home: playlists rc=-11)。该响应不含 tracks(每首 tracks:null),1MB 足够,
|
||||||
|
* 用 NCM_RESP_MAX(与 vip/info 同口径的堆瞬态,解析后即释,非 bss)。 */
|
||||||
int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
|
int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
|
||||||
RESP_MED, &c);
|
NCM_RESP_MAX, &c);
|
||||||
if (rc != NCM_OK) return rc;
|
if (rc != NCM_OK) return rc;
|
||||||
|
|
||||||
int arr = ncm_json_path(&c.j, "playlist");
|
int arr = ncm_json_path(&c.j, "playlist");
|
||||||
|
|||||||
@@ -302,10 +302,12 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2)。
|
/* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2 + 1)。
|
||||||
* 批量 song_detail 的 c 数组转义后 full2 ~1.6KB → hex ~3.2KB,旧 2048 溢出
|
* NCM_SONG_BATCH=100 时:c 数组转义后 full2 ~2.5KB(10 位 id) / ~3.4KB(19 位 id)
|
||||||
* → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(实测 batch=64 时 rc=-8)。 */
|
* → hex ~5.1KB / ~6.9KB。旧 params[4096] 对 100 首批量恒溢出 → aes_ecb_hex
|
||||||
char params[4096];
|
* 返回 -1 → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(-8)(真机 first-screen rc=-8)。
|
||||||
|
* 用 NCM_REQ_BODY_MAX(8192) 兜住 100 首最坏情况(body="params="+hex ≤ 8192)。 */
|
||||||
|
char params[NCM_REQ_BODY_MAX];
|
||||||
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
|
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
|
||||||
|
|
||||||
char cookie_hdr[3072];
|
char cookie_hdr[3072];
|
||||||
|
|||||||
@@ -234,6 +234,35 @@ static void test_eapi_musicu_hdr(void) {
|
|||||||
CHECK(strstr(h2, "MUSIC_U=") == NULL, "guest eapi has no MUSIC_U");
|
CHECK(strstr(h2, "MUSIC_U=") == NULL, "guest eapi has no MUSIC_U");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_eapi_song_detail_batch(void) {
|
||||||
|
/* §NEW:NCM_SONG_BATCH=100 的 song_detail eapi 请求构建(不打网络)。
|
||||||
|
* 旧 params[4096] 对 100 首的 AES-ECB hex(~5.1KB) 溢出 → NCM_ERR_CRYPTO(-8),
|
||||||
|
* 真机 first-screen 因此 rc=-8。这里只构建请求验证 params/body 缓冲够。 */
|
||||||
|
printf("== eapi 100-song detail batch ==\n");
|
||||||
|
const char *device = "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00";
|
||||||
|
char clist[4096] = "[", tmp[64];
|
||||||
|
for (int i = 0; i < 100 && strlen(clist) < sizeof clist - 80; i++) {
|
||||||
|
snprintf(tmp, sizeof tmp, "%s{\\\"id\\\":%lld}", i ? "," : "",
|
||||||
|
(long long)(2048962657LL + (long long)i));
|
||||||
|
ncm_strlcat(clist, tmp, sizeof clist);
|
||||||
|
}
|
||||||
|
ncm_strlcat(clist, "]", sizeof clist);
|
||||||
|
char json[4500];
|
||||||
|
snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist);
|
||||||
|
|
||||||
|
char params[8192];
|
||||||
|
CHECKRC(ncm_crypto_eapi("/api/v3/song/detail", json, params, sizeof params),
|
||||||
|
"eapi crypto 100-song");
|
||||||
|
CHECK(strlen(params) > 4096, "params exceeds old 4096 cap (got %zu)", strlen(params));
|
||||||
|
|
||||||
|
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
|
||||||
|
CHECKRC(ncm_req_build(device, "", "/api/v3/song/detail", json, NCM_CRYPT_EAPI,
|
||||||
|
body, sizeof body, hdrs, sizeof hdrs, url, sizeof url),
|
||||||
|
"eapi req_build 100-song");
|
||||||
|
CHECK(strstr(body, "params=") == body, "body starts with params=");
|
||||||
|
CHECK(strlen(body) < NCM_REQ_BODY_MAX, "body fits NCM_REQ_BODY_MAX (got %zu)", strlen(body));
|
||||||
|
}
|
||||||
|
|
||||||
static void test_network(void) {
|
static void test_network(void) {
|
||||||
printf("== network (live) ==\n");
|
printf("== network (live) ==\n");
|
||||||
int net_ok = 0;
|
int net_ok = 0;
|
||||||
@@ -515,6 +544,7 @@ int main(int argc, char **argv) {
|
|||||||
test_qr();
|
test_qr();
|
||||||
test_cookie_merge();
|
test_cookie_merge();
|
||||||
test_eapi_musicu_hdr();
|
test_eapi_musicu_hdr();
|
||||||
|
test_eapi_song_detail_batch();
|
||||||
test_network();
|
test_network();
|
||||||
test_track_all_parse();
|
test_track_all_parse();
|
||||||
test_vip_parse();
|
test_vip_parse();
|
||||||
|
|||||||
+606
-129
@@ -9,6 +9,7 @@
|
|||||||
#include "rdsrc.h"
|
#include "rdsrc.h"
|
||||||
#include "rdtxt.h"
|
#include "rdtxt.h"
|
||||||
#include "seg.h"
|
#include "seg.h"
|
||||||
|
#include "rdchapter.h"
|
||||||
#include "rdlayout.h"
|
#include "rdlayout.h"
|
||||||
#include "rdimg.h"
|
#include "rdimg.h"
|
||||||
#include "../engine/rdft.h"
|
#include "../engine/rdft.h"
|
||||||
@@ -22,6 +23,17 @@ typedef struct seg {
|
|||||||
int page_count; /* 本段页数 */
|
int page_count; /* 本段页数 */
|
||||||
} seg_t;
|
} seg_t;
|
||||||
|
|
||||||
|
/* Phase 2C:双段 cps 窗口槽——只保留段解码码点(utf8 转完即 free,不再驻留整段 lines/pages)。 */
|
||||||
|
typedef struct cps_slot {
|
||||||
|
int seg; /* 段下标;-1 = 空槽 */
|
||||||
|
uint32_t *cps;
|
||||||
|
size_t n;
|
||||||
|
} cps_slot;
|
||||||
|
|
||||||
|
/* 滞回释放/预取阈值(按全书页数百分比;真机可调)。 */
|
||||||
|
#define RD_WIN_NEAR_PCT 2 /* 距下一段界 ≤2%:预取下一段 */
|
||||||
|
#define RD_WIN_FAR_PCT 4 /* >4% 滑出:远槽可被下一段预取替换(由距离最近者策略体现) */
|
||||||
|
|
||||||
struct rd_book {
|
struct rd_book {
|
||||||
char path[400];
|
char path[400];
|
||||||
char font[400];
|
char font[400];
|
||||||
@@ -40,17 +52,28 @@ struct rd_book {
|
|||||||
rd_book_page *pages; int npages; /* flat 页表(文本) */
|
rd_book_page *pages; int npages; /* flat 页表(文本) */
|
||||||
int cur;
|
int cur;
|
||||||
|
|
||||||
/* 当前渲染段复用缓冲 */
|
/* Phase 2C:双段 cps 窗口(渲染微排版取码点用;替换旧单段 seg_lines/seg_pages 缓存) */
|
||||||
uint8_t *seg_utf8; size_t seg_utf8_cap;
|
cps_slot win[2];
|
||||||
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_cap;
|
|
||||||
rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
|
/* Phase 2A:拼接页表滚动状态(上一段的「末页页首→段尾」码点 tail;建下一段时拼接) */
|
||||||
int seg_cache_idx;
|
uint32_t *tail_cps; size_t tail_n;
|
||||||
|
int tail_seg; /* tail 所属段;-1 = 无 */
|
||||||
|
int32_t tail_seg_char; /* tail 在所属段的起始字符偏移 */
|
||||||
|
|
||||||
|
/* Phase 2C:渲染微排版复用缓冲(拼接页临时码点 + 一页行缓冲,避免每帧 malloc) */
|
||||||
|
uint32_t *stitch_cps; size_t stitch_cap;
|
||||||
|
rd_line *render_lines; int render_lines_cap;
|
||||||
|
|
||||||
/* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */
|
/* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */
|
||||||
char seg_dir[400];
|
char seg_dir[400];
|
||||||
int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */
|
int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */
|
||||||
int seg_built; /* 已布局段数;fallback = nsegs */
|
int seg_built; /* 已布局段数;fallback = nsegs */
|
||||||
int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */
|
int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */
|
||||||
|
|
||||||
|
/* R3.3 章节(TXT):解析结果 + 就绪/无章节标志(延迟解析,不拖慢首屏) */
|
||||||
|
rdchapter chaps;
|
||||||
|
int chaps_ready; /* 1=chaps 已填充(含确无章节) */
|
||||||
|
int chaps_none; /* 1=确无章节(扫描过 n==0)或该书不可用 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
|
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
|
||||||
@@ -61,6 +84,7 @@ struct rd_book {
|
|||||||
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
|
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
|
||||||
|
|
||||||
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
|
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
|
||||||
|
static int seg_ensure_built(rd_book *b, int upto_seg); /* 段路径补齐(下文定义) */
|
||||||
void rd_book_set_index_dir(const char *dir)
|
void rd_book_set_index_dir(const char *dir)
|
||||||
{
|
{
|
||||||
if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; }
|
if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; }
|
||||||
@@ -80,7 +104,7 @@ void rd_book_set_seg_base(const char *base)
|
|||||||
#define IDX_MAGIC1 'I'
|
#define IDX_MAGIC1 'I'
|
||||||
#define IDX_MAGIC2 'D'
|
#define IDX_MAGIC2 'D'
|
||||||
#define IDX_MAGIC3 'X'
|
#define IDX_MAGIC3 'X'
|
||||||
#define IDX_VER 1u
|
#define IDX_VER 2u
|
||||||
|
|
||||||
static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; }
|
static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; }
|
||||||
static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; }
|
static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; }
|
||||||
@@ -253,6 +277,267 @@ static void index_cache_save(rd_book *b)
|
|||||||
if (ok) { rename(tmp, path); } else { remove(tmp); }
|
if (ok) { rename(tmp, path); } else { remove(tmp); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- R3.3 章节缓存(MeowData/os.meow.reader/index/<hash>.chap,键=书字节+编码) ----
|
||||||
|
* 章节 char_off/seg_idx/seg_char 仅依赖书字节与编码(不含字号/行距/字体/版面),故失效键
|
||||||
|
* (file_size, file_mtime, enc) 即够,与页索引(含 font/fs/ls/cw/ch)分开、独立秒开。
|
||||||
|
* 目录为 index 同父;g_index_dir 为空(宿主/无卡)→ 不持久,仅内存。 */
|
||||||
|
#define CHAP_MAGIC0 'R'
|
||||||
|
#define CHAP_MAGIC1 'C'
|
||||||
|
#define CHAP_MAGIC2 'H'
|
||||||
|
#define CHAP_MAGIC3 'P'
|
||||||
|
#define CHAP_VER 1u
|
||||||
|
|
||||||
|
static int chap_cache_path(const rd_book *b, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!g_index_dir[0]) return -1;
|
||||||
|
if (snprintf(out, (size_t)outsz, "%s/%08x.chap", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 章节缓存键:书字节 + 编码(含 BOM/编码覆盖),跟页索引键不同(无字体/字号/版面)。 */
|
||||||
|
static int chap_cache_load(rd_book *b)
|
||||||
|
{
|
||||||
|
char path[440];
|
||||||
|
if (chap_cache_path(b, path, sizeof path) != 0) return 0;
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
uint8_t magic[4];
|
||||||
|
uint16_t ver = 0;
|
||||||
|
uint64_t fsize = 0; int64_t fmtime = 0; int32_t fenc = 0;
|
||||||
|
int32_t nsegs = 0, nch = 0; int64_t total = 0;
|
||||||
|
int ok = 0;
|
||||||
|
if (fio_rd(f, magic, sizeof magic) && magic[0] == CHAP_MAGIC0 && magic[1] == CHAP_MAGIC1 &&
|
||||||
|
magic[2] == CHAP_MAGIC2 && magic[3] == CHAP_MAGIC3 &&
|
||||||
|
fio_rd(f, &ver, sizeof ver) && ver == CHAP_VER &&
|
||||||
|
fio_rd(f, &fsize, sizeof fsize) && fio_rd(f, &fmtime, sizeof fmtime) &&
|
||||||
|
fio_rd(f, &fenc, sizeof fenc) &&
|
||||||
|
fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &total, sizeof total) &&
|
||||||
|
fio_rd(f, &nch, sizeof nch) &&
|
||||||
|
nsegs >= 0 && nch >= 0 && nsegs <= 1000000 && nch <= RDCHAP_MAX_ENTRIES) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(b->path, &st) == 0 && (uint64_t)st.st_size == fsize &&
|
||||||
|
(int64_t)st.st_mtime == fmtime && (int32_t)b->enc == fenc) {
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
c.seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
|
||||||
|
c.entries = (rdchapter_entry *)malloc((size_t)(nch ? nch : 1) * sizeof(rdchapter_entry));
|
||||||
|
if (c.seg_char_start && c.entries &&
|
||||||
|
fio_rd(f, c.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t))) {
|
||||||
|
int eok = 1;
|
||||||
|
for (int i = 0; i < nch && eok; i++) {
|
||||||
|
rdchapter_entry *e = &c.entries[i];
|
||||||
|
if (!fio_rd(f, &e->char_off, sizeof e->char_off) ||
|
||||||
|
!fio_rd(f, &e->seg_idx, sizeof e->seg_idx) ||
|
||||||
|
!fio_rd(f, &e->seg_char, sizeof e->seg_char) ||
|
||||||
|
!fio_rd(f, &e->number, sizeof e->number) ||
|
||||||
|
!fio_rd(f, e->title, sizeof e->title)) eok = 0;
|
||||||
|
}
|
||||||
|
if (eok) {
|
||||||
|
c.n = nch; c.nsegs = nsegs;
|
||||||
|
c.has_pre = (nch > 0 && c.entries[0].char_off == 0 && c.entries[0].number == -1) ? 1 : 0;
|
||||||
|
snprintf(c.pre_title, sizeof c.pre_title, "%s", (nch > 0) ? c.entries[0].title : "卷首");
|
||||||
|
b->chaps = c;
|
||||||
|
b->chaps_ready = 1;
|
||||||
|
b->chaps_none = (nch == 0);
|
||||||
|
ok = 1;
|
||||||
|
} else { rdchapter_free(&c); }
|
||||||
|
} else { rdchapter_free(&c); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void chap_cache_save(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b->chaps_ready || b->chaps.nsegs <= 0) return;
|
||||||
|
char path[440];
|
||||||
|
if (chap_cache_path(b, path, sizeof path) != 0) return;
|
||||||
|
char tmp[480];
|
||||||
|
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
|
||||||
|
FILE *f = fopen(tmp, "wb");
|
||||||
|
if (!f) return;
|
||||||
|
struct stat st;
|
||||||
|
uint64_t fsize = 0; int64_t fmtime = 0;
|
||||||
|
if (stat(b->path, &st) == 0) { fsize = (uint64_t)st.st_size; fmtime = (int64_t)st.st_mtime; }
|
||||||
|
const uint8_t magic[4] = { CHAP_MAGIC0, CHAP_MAGIC1, CHAP_MAGIC2, CHAP_MAGIC3 };
|
||||||
|
uint16_t ver = CHAP_VER;
|
||||||
|
int32_t nsegs = b->chaps.nsegs, fenc = (int32_t)b->enc, nch = b->chaps.n;
|
||||||
|
int64_t total = b->chaps.seg_char_start ? b->chaps.seg_char_start[nsegs] : 0;
|
||||||
|
int ok = 1;
|
||||||
|
ok = ok && fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
|
||||||
|
fio_wr(f, &fsize, sizeof fsize) && fio_wr(f, &fmtime, sizeof fmtime) &&
|
||||||
|
fio_wr(f, &fenc, sizeof fenc) &&
|
||||||
|
fio_wr(f, &nsegs, sizeof nsegs) && fio_wr(f, &total, sizeof total) &&
|
||||||
|
fio_wr(f, &nch, sizeof nch);
|
||||||
|
if (ok && nsegs > 0) ok = fio_wr(f, b->chaps.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t));
|
||||||
|
if (ok && nch > 0) {
|
||||||
|
for (int i = 0; i < nch; i++) {
|
||||||
|
const rdchapter_entry *e = &b->chaps.entries[i];
|
||||||
|
ok = ok && fio_wr(f, &e->char_off, sizeof e->char_off) &&
|
||||||
|
fio_wr(f, &e->seg_idx, sizeof e->seg_idx) &&
|
||||||
|
fio_wr(f, &e->seg_char, sizeof e->seg_char) &&
|
||||||
|
fio_wr(f, &e->number, sizeof e->number) &&
|
||||||
|
fio_wr(f, e->title, sizeof e->title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (ok) { rename(tmp, path); } else { remove(tmp); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保章节解析(幂等):命中缓存 → 就绪;未命中 → 扫描(读全部段文件)。
|
||||||
|
* 可靠性(R3.4):仅「扫描全程成功」才落盘(含无章节 n==0);扫描中途出错(段读/解码/内存)
|
||||||
|
* 不落盘、本次降级无章节,下次重试。返回:0=成功(就绪);-1=不可用(非 TXT/无段目录);
|
||||||
|
* -2=扫描出错(不落盘)。 */
|
||||||
|
static int chapter_ensure(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b) return -1;
|
||||||
|
if (b->chaps_ready) return 0;
|
||||||
|
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) {
|
||||||
|
b->chaps_none = 1; b->chaps_ready = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (chap_cache_load(b)) return 0;
|
||||||
|
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &b->chaps);
|
||||||
|
if (rc != 0) { b->chaps_none = 1; b->chaps_ready = 1; return -2; } /* 扫描失败:不落盘,降级无章节 */
|
||||||
|
b->chaps_none = (b->chaps.n == 0);
|
||||||
|
b->chaps_ready = 1;
|
||||||
|
chap_cache_save(b); /* 可靠(成功扫描,无论有无章节)→ 落盘 */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rd_book_chapter_ready(rd_book *b) { return (b && b->chaps_ready) ? 1 : 0; }
|
||||||
|
|
||||||
|
/* R3.4 手动重解析本书章节:先扫描到临时结果,成功才「清旧缓存/旧内存 + 换入新结果 + 落盘」;
|
||||||
|
* 失败则保留旧缓存/旧内存(绝不产生错位章节),不落盘、返回 -2。返回:n=章节数 / 0=无章节(成功)/
|
||||||
|
* -1=不可用(非 TXT+seg)/ -2=扫描失败。幂等。 */
|
||||||
|
int rd_book_reparse_chapters(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b) return -1;
|
||||||
|
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
|
||||||
|
rdchapter tmp; rdchapter_init(&tmp);
|
||||||
|
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &tmp);
|
||||||
|
if (rc != 0) { rdchapter_free(&tmp); return -2; } /* 失败:保留旧缓存/旧内存,未落盘 */
|
||||||
|
rdchapter_free(&b->chaps); /* 清除内存旧解析结果 */
|
||||||
|
b->chaps = tmp;
|
||||||
|
b->chaps_none = (b->chaps.n == 0);
|
||||||
|
b->chaps_ready = 1;
|
||||||
|
char path[440];
|
||||||
|
if (chap_cache_path(b, path, sizeof path) == 0) remove(path); /* 清缓存文件 */
|
||||||
|
chap_cache_save(b); /* 可靠结果 → 落盘 */
|
||||||
|
return b->chaps.n; /* >=0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int rd_book_chapter_n(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b) return -1;
|
||||||
|
if (b->is_img || b->fmt != RD_FMT_TXT) return -1;
|
||||||
|
if (chapter_ensure(b) != 0) return -1;
|
||||||
|
return b->chaps.n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
|
||||||
|
int64_t *char_off, int *seg, int32_t *seg_char, int *number)
|
||||||
|
{
|
||||||
|
if (!b || !title || i < 0) return -1;
|
||||||
|
if (chapter_ensure(b) != 0) return -1;
|
||||||
|
if (i >= b->chaps.n || !b->chaps.entries) return -1;
|
||||||
|
const rdchapter_entry *e = &b->chaps.entries[i];
|
||||||
|
if (tsz > 0) { snprintf(title, (size_t)tsz, "%s", e->title); }
|
||||||
|
if (char_off) *char_off = e->char_off;
|
||||||
|
if (seg) *seg = e->seg_idx;
|
||||||
|
if (seg_char) *seg_char = e->seg_char;
|
||||||
|
if (number) *number = e->number;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t rd_book_page_char_off(rd_book *b, int page)
|
||||||
|
{
|
||||||
|
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
|
||||||
|
if (chapter_ensure(b) != 0) return -1;
|
||||||
|
if (!b->chaps.seg_char_start || b->chaps.nsegs <= 0) return -1;
|
||||||
|
int seg = b->pages[page].seg;
|
||||||
|
if (seg < 0 || seg >= b->chaps.nsegs) return -1;
|
||||||
|
return b->chaps.seg_char_start[seg] + b->pages[page].seg_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rd_book_page_for_char(rd_book *b, int64_t char_off)
|
||||||
|
{
|
||||||
|
if (!b || b->npages <= 0) return -1;
|
||||||
|
if (chapter_ensure(b) != 0) return -1;
|
||||||
|
int nseg = b->chaps.nsegs;
|
||||||
|
if (!b->chaps.seg_char_start || nseg <= 0) return -1;
|
||||||
|
int64_t total = b->chaps.seg_char_start[nseg];
|
||||||
|
if (total <= 0) return 0;
|
||||||
|
if (char_off < 0) char_off = 0;
|
||||||
|
if (char_off >= total) char_off = total - 1;
|
||||||
|
/* 二分所在段:最大的 i 使 seg_char_start[i] <= char_off */
|
||||||
|
int lo = 0, hi = nseg - 1, s = 0;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
if (b->chaps.seg_char_start[mid] <= char_off) { s = mid; lo = mid + 1; }
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
if (seg_ensure_built(b, s) != 0) return -1;
|
||||||
|
if (s >= b->nsegs) s = b->nsegs - 1;
|
||||||
|
int local = (int)(char_off - b->chaps.seg_char_start[s]);
|
||||||
|
int p0 = b->segs[s].page_start, pc = b->segs[s].page_count;
|
||||||
|
if (pc <= 0) return (p0 < b->npages) ? p0 : (b->npages - 1);
|
||||||
|
/* 段内二分:最后一个 seg_char <= local 的页 = 含该字符的页 */
|
||||||
|
int lo2 = p0, hi2 = p0 + pc - 1, best = p0;
|
||||||
|
while (lo2 <= hi2) {
|
||||||
|
int mid = (lo2 + hi2) / 2;
|
||||||
|
if (b->pages[mid].seg_char <= local) { best = mid; lo2 = mid + 1; }
|
||||||
|
else hi2 = mid - 1;
|
||||||
|
}
|
||||||
|
return (best < b->npages) ? best : (b->npages - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rd_book_chapter_for_page(rd_book *b, int page, int *idx)
|
||||||
|
{
|
||||||
|
if (!b || !idx) return -1;
|
||||||
|
if (chapter_ensure(b) != 0) return -1;
|
||||||
|
if (b->chaps.n <= 0) { *idx = -1; return 0; }
|
||||||
|
if (page < 0 || page >= b->npages) return -1;
|
||||||
|
int64_t po = rd_book_page_char_off(b, page);
|
||||||
|
if (po < 0) { *idx = -1; return 0; }
|
||||||
|
int lo = 0, hi = b->chaps.n - 1, best = -1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
if (b->chaps.entries[mid].char_off <= po) { best = mid; lo = mid + 1; }
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
*idx = best;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 段访问(文件/段模式跳转) ---- */
|
||||||
|
int rd_book_seg_n(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b || b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
|
||||||
|
return (b->seg_n > 0) ? b->seg_n : b->nsegs;
|
||||||
|
}
|
||||||
|
int rd_book_seg_page(rd_book *b, int seg)
|
||||||
|
{
|
||||||
|
if (!b || seg < 0) return -1;
|
||||||
|
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
|
||||||
|
if (seg >= b->seg_n) return -1;
|
||||||
|
if (seg_ensure_built(b, seg) != 0) return -1;
|
||||||
|
if (seg >= b->nsegs) return -1;
|
||||||
|
return b->segs[seg].page_start;
|
||||||
|
}
|
||||||
|
return (seg < b->nsegs) ? b->segs[seg].page_start : -1;
|
||||||
|
}
|
||||||
|
int rd_book_seg_page_count(rd_book *b, int seg)
|
||||||
|
{
|
||||||
|
if (!b || seg < 0 || seg >= b->nsegs) return -1;
|
||||||
|
return b->segs[seg].page_count;
|
||||||
|
}
|
||||||
|
int rd_book_seg_of_page(rd_book *b, int page)
|
||||||
|
{
|
||||||
|
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
|
||||||
|
return b->pages[page].seg;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 文本:建索引(逐段 layout) ---- */
|
/* ---- 文本:建索引(逐段 layout) ---- */
|
||||||
static int build_index(rd_book *b)
|
static int build_index(rd_book *b)
|
||||||
{
|
{
|
||||||
@@ -288,8 +573,10 @@ static int build_index(rd_book *b)
|
|||||||
b->segs[seg_idx].page_count = out.npages;
|
b->segs[seg_idx].page_count = out.npages;
|
||||||
for (int p = 0; p < out.npages; p++) {
|
for (int p = 0; p < out.npages; p++) {
|
||||||
if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); }
|
if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); }
|
||||||
|
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
|
||||||
b->pages[b->npages].seg = seg_idx;
|
b->pages[b->npages].seg = seg_idx;
|
||||||
b->pages[b->npages].seg_char = out.pages[p].first_char;
|
b->pages[b->npages].seg_char = out.pages[p].first_char;
|
||||||
|
b->pages[b->npages].flags = (uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u);
|
||||||
b->npages++;
|
b->npages++;
|
||||||
}
|
}
|
||||||
free(lines); free(pages);
|
free(lines); free(pages);
|
||||||
@@ -300,41 +587,169 @@ static int build_index(rd_book *b)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- R3.2 分段流式(TXT):把一段 UTF-8 布局并追加到 flat 页表 ---- */
|
/* ---- 段解码:把段 idx 的原始字节 → utf8 → 码点(malloc 返回,调用方 free)。
|
||||||
static int append_seg_pages(rd_book *b, int seg_idx, const uint8_t *utf8, size_t ulen)
|
* seg 路径读 512KB 段文件;非 seg 路径按 rd_src_seg 重显。窗口/建表/tail 重建共用。 ---- */
|
||||||
|
static int seg_load_cps(rd_book *b, int seg_idx, uint32_t **cps_out, size_t *n_out)
|
||||||
{
|
{
|
||||||
uint32_t *cps = NULL; size_t n = 0;
|
if (!cps_out || !n_out) return -1;
|
||||||
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) return -5;
|
*cps_out = NULL; *n_out = 0;
|
||||||
rd_line *lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
|
uint8_t *utf8 = NULL; size_t cap = 0, ulen = 0;
|
||||||
rd_page *pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
|
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
|
||||||
if (!lines || !pages) { free(lines); free(pages); free(cps); return -6; }
|
size_t rawlen = 0;
|
||||||
rd_layout_in in;
|
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
|
||||||
rd_layout_out out;
|
cap = rawlen * 3 / 2 + 16;
|
||||||
in.chars = cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
|
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
|
||||||
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
|
utf8 = (uint8_t *)malloc(cap);
|
||||||
in.meas = book_meas; in.mctx = b;
|
if (!utf8) return -6;
|
||||||
out.lines = lines; out.pages = pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
|
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
|
||||||
int lrc = rd_layout(&in, &out);
|
if (!raw) { free(utf8); return -6; }
|
||||||
free(cps);
|
size_t rn = 0;
|
||||||
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); return -4; }
|
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
|
||||||
b->segs[seg_idx].page_start = b->npages;
|
if (rc != 0) { free(raw); free(utf8); return -4; }
|
||||||
b->segs[seg_idx].page_count = out.npages;
|
int r2 = rd_to_utf8(b->enc, raw, rn, utf8, cap, &ulen);
|
||||||
for (int p = 0; p < out.npages; p++) {
|
free(raw);
|
||||||
if (b->npages >= b->pages_cap) {
|
if (r2 == RD_ENC_ERR_BUFSZ) { free(utf8); return -2; }
|
||||||
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
|
if (r2 != RD_ENC_OK) { free(utf8); return -3; }
|
||||||
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
|
} else {
|
||||||
if (!np) { free(lines); free(pages); return -6; }
|
cap = RDTXT_SEG_UTF8_MAX + 8;
|
||||||
b->pages = np; b->pages_cap = nc;
|
utf8 = (uint8_t *)malloc(cap);
|
||||||
}
|
if (!utf8) return -6;
|
||||||
b->pages[b->npages].seg = seg_idx;
|
if (rd_src_seg(b->src, seg_idx, utf8, cap, &ulen) != 0) { free(utf8); return -4; }
|
||||||
b->pages[b->npages].seg_char = out.pages[p].first_char;
|
|
||||||
b->npages++;
|
|
||||||
}
|
}
|
||||||
free(lines); free(pages);
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
|
int rc = rd_utf8_to_cp(utf8, ulen, &cps, &n);
|
||||||
|
free(utf8);
|
||||||
|
if (rc != RD_ENC_OK) { free(cps); return -5; }
|
||||||
|
*cps_out = cps; *n_out = n;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → utf8 → 布局)补齐进 flat 页表。
|
/* ---- Phase 2A:拼接页表滚动状态(tail) ---- */
|
||||||
|
static void tail_clear(rd_book *b)
|
||||||
|
{
|
||||||
|
free(b->tail_cps);
|
||||||
|
b->tail_cps = NULL;
|
||||||
|
b->tail_n = 0;
|
||||||
|
b->tail_seg = -1;
|
||||||
|
b->tail_seg_char = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缓存续建时 tail 不在内存(未持久化)→ 由段 seg 的末页首字符重算(读该段 cps 截取末页 tail)。 */
|
||||||
|
static int tail_rebuild(rd_book *b, int seg)
|
||||||
|
{
|
||||||
|
if (b->tail_seg == seg && b->tail_cps && b->tail_n > 0) return 0;
|
||||||
|
tail_clear(b);
|
||||||
|
if (seg < 0 || seg >= b->seg_built) return -1;
|
||||||
|
int pc = b->segs[seg].page_count;
|
||||||
|
if (pc <= 0) return -1;
|
||||||
|
int lastp = b->segs[seg].page_start + pc - 1;
|
||||||
|
if (lastp < 0 || lastp >= b->npages) return -1;
|
||||||
|
int32_t sc = b->pages[lastp].seg_char;
|
||||||
|
if (sc < 0) return -1;
|
||||||
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
|
if (seg_load_cps(b, seg, &cps, &n) != 0) return -1;
|
||||||
|
if ((size_t)sc > n) { free(cps); return -1; }
|
||||||
|
b->tail_n = n - (size_t)sc;
|
||||||
|
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
|
||||||
|
if (!b->tail_cps) { free(cps); return -6; }
|
||||||
|
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
|
||||||
|
b->tail_seg = seg;
|
||||||
|
b->tail_seg_char = sc;
|
||||||
|
free(cps);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pages_push(rd_book *b, int seg, int32_t seg_char, uint8_t flags)
|
||||||
|
{
|
||||||
|
if (b->npages >= b->pages_cap) {
|
||||||
|
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
|
||||||
|
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
|
||||||
|
if (!np) return -6;
|
||||||
|
b->pages = np; b->pages_cap = nc;
|
||||||
|
}
|
||||||
|
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
|
||||||
|
b->pages[b->npages].seg = seg;
|
||||||
|
b->pages[b->npages].seg_char = seg_char;
|
||||||
|
b->pages[b->npages].flags = flags;
|
||||||
|
b->npages++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Phase 2A:滚动拼接。把 seg_idx 段码点布局并追加到 flat 页表。
|
||||||
|
* 建 N+1 时:删 N 的临时末页 → tail_cps + cps_new 拼接重排 → 首张拼接页回填 N(锚不变),
|
||||||
|
* 其余页按 first_char − tail_n 归入 N+1。末段/首段无 tail 时按整段直排。 ---- */
|
||||||
|
static int append_seg_pages(rd_book *b, int seg_idx, const uint32_t *cps, size_t n)
|
||||||
|
{
|
||||||
|
int have_tail = (seg_idx > 0 && b->tail_seg == seg_idx - 1 && b->tail_cps && b->tail_n > 0);
|
||||||
|
if (have_tail) {
|
||||||
|
if (b->segs[seg_idx - 1].page_count <= 0) return -4;
|
||||||
|
b->npages--; /* 删上一段临时末页(拼接页原位回填) */
|
||||||
|
b->segs[seg_idx - 1].page_count--;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t stitched_n = have_tail ? b->tail_n + n : n;
|
||||||
|
uint32_t *scps = NULL;
|
||||||
|
const uint32_t *chars;
|
||||||
|
if (have_tail) {
|
||||||
|
scps = (uint32_t *)malloc((stitched_n ? stitched_n : 1) * sizeof(uint32_t));
|
||||||
|
if (!scps) return -6;
|
||||||
|
memcpy(scps, b->tail_cps, b->tail_n * sizeof(uint32_t));
|
||||||
|
memcpy(scps + b->tail_n, cps, n * sizeof(uint32_t));
|
||||||
|
chars = scps;
|
||||||
|
} else {
|
||||||
|
chars = cps;
|
||||||
|
}
|
||||||
|
|
||||||
|
rd_line *lines = (rd_line *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_line));
|
||||||
|
rd_page *pages = (rd_page *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_page));
|
||||||
|
if (!lines || !pages) { free(lines); free(pages); free(scps); return -6; }
|
||||||
|
rd_layout_in in;
|
||||||
|
rd_layout_out out;
|
||||||
|
in.chars = chars; in.n = (int32_t)stitched_n; in.fs = b->fs; in.line_scale = b->ls;
|
||||||
|
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
|
||||||
|
in.meas = book_meas; in.mctx = b;
|
||||||
|
out.lines = lines; out.pages = pages; out.max_lines = (int)(stitched_n ? stitched_n : 1); out.max_pages = (int)(stitched_n ? stitched_n : 1);
|
||||||
|
int lrc = rd_layout(&in, &out);
|
||||||
|
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); free(scps); return -4; }
|
||||||
|
|
||||||
|
int first_own = 0;
|
||||||
|
if (have_tail) {
|
||||||
|
if (out.npages < 1) { free(lines); free(pages); free(scps); return -4; }
|
||||||
|
int rc = pages_push(b, seg_idx - 1, b->tail_seg_char,
|
||||||
|
(uint8_t)(out.lines[out.pages[0].first_line].para_start ? 1u : 0u));
|
||||||
|
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
|
||||||
|
b->segs[seg_idx - 1].page_count++;
|
||||||
|
first_own = 1;
|
||||||
|
}
|
||||||
|
b->segs[seg_idx].page_start = b->npages;
|
||||||
|
b->segs[seg_idx].page_count = 0;
|
||||||
|
for (int p = first_own; p < out.npages; p++) {
|
||||||
|
int32_t local = out.pages[p].first_char - (have_tail ? (int32_t)b->tail_n : 0);
|
||||||
|
if (local < 0) local = 0;
|
||||||
|
int rc = pages_push(b, seg_idx, local,
|
||||||
|
(uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u));
|
||||||
|
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
|
||||||
|
b->segs[seg_idx].page_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保存本段末页 tail(供建下一段拼接;末段也保存但不会用到)。 */
|
||||||
|
tail_clear(b);
|
||||||
|
if (b->segs[seg_idx].page_count > 0) {
|
||||||
|
int lastp = b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1;
|
||||||
|
int32_t sc = b->pages[lastp].seg_char;
|
||||||
|
if (sc < 0 || (size_t)sc > n) { free(lines); free(pages); free(scps); return -4; }
|
||||||
|
b->tail_n = n - (size_t)sc;
|
||||||
|
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
|
||||||
|
if (!b->tail_cps) { b->tail_n = 0; free(lines); free(pages); free(scps); return -6; }
|
||||||
|
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
|
||||||
|
b->tail_seg = seg_idx;
|
||||||
|
b->tail_seg_char = sc;
|
||||||
|
}
|
||||||
|
free(lines); free(pages); free(scps);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → cps → 拼接布局)补齐进 flat 页表。
|
||||||
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
|
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
|
||||||
static int seg_ensure_built(rd_book *b, int upto_seg)
|
static int seg_ensure_built(rd_book *b, int upto_seg)
|
||||||
{
|
{
|
||||||
@@ -345,8 +760,6 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
|
|||||||
int built_any = 0;
|
int built_any = 0;
|
||||||
while (b->seg_built <= upto_seg) {
|
while (b->seg_built <= upto_seg) {
|
||||||
int idx = b->seg_built;
|
int idx = b->seg_built;
|
||||||
size_t rawlen = 0;
|
|
||||||
if (rdseg_rawlen(b->seg_dir, idx, &rawlen) != 0) break;
|
|
||||||
if (b->segs_cap <= idx) {
|
if (b->segs_cap <= idx) {
|
||||||
int nc = b->segs_cap ? b->segs_cap * 2 : 64;
|
int nc = b->segs_cap ? b->segs_cap * 2 : 64;
|
||||||
if (nc <= idx) nc = idx + 1;
|
if (nc <= idx) nc = idx + 1;
|
||||||
@@ -354,24 +767,13 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
|
|||||||
if (!ns) return -6;
|
if (!ns) return -6;
|
||||||
b->segs = ns; b->segs_cap = nc;
|
b->segs = ns; b->segs_cap = nc;
|
||||||
}
|
}
|
||||||
size_t cap = rawlen * 3 / 2 + 16;
|
if (idx > 0 && tail_rebuild(b, idx - 1) != 0) return -4;
|
||||||
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
if (!b->seg_utf8 || b->seg_utf8_cap < cap) {
|
int rc = seg_load_cps(b, idx, &cps, &n);
|
||||||
uint8_t *t = (uint8_t *)malloc(cap);
|
if (rc != 0) return rc;
|
||||||
if (!t) return -6;
|
rc = append_seg_pages(b, idx, cps, n);
|
||||||
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap;
|
free(cps);
|
||||||
}
|
if (rc != 0) return rc;
|
||||||
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
|
|
||||||
if (!raw) return -6;
|
|
||||||
size_t rn = 0;
|
|
||||||
if (rdseg_load(b->seg_dir, idx, raw, rawlen, &rn) != 0) { free(raw); return -4; }
|
|
||||||
size_t ulen = 0;
|
|
||||||
int rc = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
|
|
||||||
free(raw);
|
|
||||||
if (rc == RD_ENC_ERR_BUFSZ) return -2;
|
|
||||||
if (rc != RD_ENC_OK) return -3;
|
|
||||||
int prc = append_seg_pages(b, idx, b->seg_utf8, ulen);
|
|
||||||
if (prc != 0) return prc;
|
|
||||||
b->seg_built++;
|
b->seg_built++;
|
||||||
built_any = 1;
|
built_any = 1;
|
||||||
}
|
}
|
||||||
@@ -380,59 +782,53 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 正文重显 + 布局到复用缓冲 ---- */
|
/* ---- Phase 2C:双段 cps 窗口(渲染微排版取码点;替换旧单段整段 lines/pages 缓存) ---- */
|
||||||
static int load_seg(rd_book *b, int seg_idx)
|
static void win_clear(rd_book *b)
|
||||||
{
|
{
|
||||||
if (seg_idx == b->seg_cache_idx) return 0;
|
for (int i = 0; i < 2; i++) {
|
||||||
size_t ulen = 0;
|
free(b->win[i].cps);
|
||||||
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
|
b->win[i].cps = NULL;
|
||||||
/* R3.2 段路径:只读当前页所在 512KB 段文件(有界,不全量读整书) */
|
b->win[i].n = 0;
|
||||||
size_t rawlen = 0;
|
b->win[i].seg = -1;
|
||||||
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
|
|
||||||
size_t cap = rawlen * 3 / 2 + 16;
|
|
||||||
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
|
|
||||||
if (!b->seg_utf8 || b->seg_utf8_cap < cap) {
|
|
||||||
uint8_t *t = (uint8_t *)malloc(cap);
|
|
||||||
if (!t) return -6;
|
|
||||||
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap;
|
|
||||||
}
|
|
||||||
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
|
|
||||||
if (!raw) return -6;
|
|
||||||
size_t rn = 0;
|
|
||||||
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
|
|
||||||
if (rc != 0) { free(raw); return -4; }
|
|
||||||
int r2 = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
|
|
||||||
free(raw);
|
|
||||||
if (r2 == RD_ENC_ERR_BUFSZ) return -2;
|
|
||||||
if (r2 != RD_ENC_OK) return -3;
|
|
||||||
} else {
|
|
||||||
size_t cap = RDTXT_SEG_UTF8_MAX + 8;
|
|
||||||
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
|
|
||||||
if (!b->seg_utf8) return -6;
|
|
||||||
if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
|
|
||||||
}
|
}
|
||||||
size_t n = 0;
|
}
|
||||||
free(b->seg_cps); b->seg_cps = NULL;
|
|
||||||
if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
|
static cps_slot *win_find(rd_book *b, int seg)
|
||||||
b->seg_cps_n = n;
|
{
|
||||||
if (b->seg_lines) free(b->seg_lines);
|
if (b->win[0].seg == seg) return &b->win[0];
|
||||||
if (b->seg_pages) free(b->seg_pages);
|
if (b->win[1].seg == seg) return &b->win[1];
|
||||||
b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
|
return NULL;
|
||||||
b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
|
}
|
||||||
if (!b->seg_lines || !b->seg_pages) return -6;
|
|
||||||
rd_layout_in in;
|
static int win_load_seg(rd_book *b, int seg_idx, cps_slot *slot)
|
||||||
rd_layout_out out;
|
{
|
||||||
in.chars = b->seg_cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
|
int rc = seg_load_cps(b, seg_idx, &cps, &n);
|
||||||
in.meas = book_meas; in.mctx = b;
|
if (rc != 0) return rc;
|
||||||
out.lines = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
|
free(slot->cps);
|
||||||
int lrc = rd_layout(&in, &out);
|
slot->cps = cps; slot->n = n; slot->seg = seg_idx;
|
||||||
if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
|
|
||||||
b->seg_nlines = out.nlines; b->seg_npages = out.npages;
|
|
||||||
b->seg_cache_idx = seg_idx;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 确保段 seg_idx 码点在窗口内。双槽都占时,淘汰「离 seg_idx 更远」的槽(距相等淘汰更小段下标,
|
||||||
|
* 倾向保留下一段);滞回释放由 reader_on_tick 的 2%/4% 预取门控体现。 */
|
||||||
|
static int win_ensure(rd_book *b, int seg_idx)
|
||||||
|
{
|
||||||
|
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
|
||||||
|
if (win_find(b, seg_idx)) return 0;
|
||||||
|
int victim = -1;
|
||||||
|
if (b->win[0].seg < 0) victim = 0;
|
||||||
|
else if (b->win[1].seg < 0) victim = 1;
|
||||||
|
else {
|
||||||
|
int d0 = b->win[0].seg - seg_idx; if (d0 < 0) d0 = -d0;
|
||||||
|
int d1 = b->win[1].seg - seg_idx; if (d1 < 0) d1 = -d1;
|
||||||
|
if (d0 > d1) victim = 0;
|
||||||
|
else if (d1 > d0) victim = 1;
|
||||||
|
else victim = (b->win[0].seg < b->win[1].seg) ? 0 : 1;
|
||||||
|
}
|
||||||
|
return win_load_seg(b, seg_idx, &b->win[victim]);
|
||||||
|
}
|
||||||
|
|
||||||
int rd_book_open(rd_book **b, const char *path, const char *font_path,
|
int rd_book_open(rd_book **b, const char *path, const char *font_path,
|
||||||
int fs, int ls, int pw, int ph, int mx, int my, int top)
|
int fs, int ls, int pw, int ph, int mx, int my, int top)
|
||||||
{
|
{
|
||||||
@@ -465,7 +861,8 @@ int rd_book_open(rd_book **b, const char *path, const char *font_path,
|
|||||||
if (rdft_init(&x->ft) != 0) goto close;
|
if (rdft_init(&x->ft) != 0) goto close;
|
||||||
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; }
|
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; }
|
||||||
if (x->ft) rdft_set_px(x->ft, fs);
|
if (x->ft) rdft_set_px(x->ft, fs);
|
||||||
x->seg_cache_idx = -1;
|
x->win[0].seg = x->win[1].seg = -1;
|
||||||
|
x->tail_seg = -1;
|
||||||
int seg_mode = 0;
|
int seg_mode = 0;
|
||||||
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
|
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
|
||||||
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
|
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
|
||||||
@@ -513,10 +910,11 @@ void rd_book_close(rd_book *b)
|
|||||||
if (!b) return;
|
if (!b) return;
|
||||||
free(b->segs);
|
free(b->segs);
|
||||||
free(b->pages);
|
free(b->pages);
|
||||||
free(b->seg_utf8);
|
win_clear(b);
|
||||||
free(b->seg_cps);
|
tail_clear(b);
|
||||||
free(b->seg_lines);
|
free(b->stitch_cps);
|
||||||
free(b->seg_pages);
|
free(b->render_lines);
|
||||||
|
rdchapter_free(&b->chaps);
|
||||||
if (b->ft) rdft_free(b->ft);
|
if (b->ft) rdft_free(b->ft);
|
||||||
if (b->src) rd_src_free(b->src);
|
if (b->src) rd_src_free(b->src);
|
||||||
free(b);
|
free(b);
|
||||||
@@ -563,6 +961,8 @@ int rd_book_set_enc(rd_book *b, rd_enc_t enc)
|
|||||||
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
|
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
|
||||||
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
|
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
|
||||||
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
|
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
|
||||||
|
win_clear(b); /* 编码变了 → 码点空间不同,窗口/tail 全失效 */
|
||||||
|
tail_clear(b);
|
||||||
if (b->seg_dir[0]) {
|
if (b->seg_dir[0]) {
|
||||||
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
|
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
|
||||||
if (oldenc != b->enc) rdseg_clear(b->seg_dir);
|
if (oldenc != b->enc) rdseg_clear(b->seg_dir);
|
||||||
@@ -615,27 +1015,104 @@ int rd_book_set_page(rd_book *b, int page)
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Phase 2C:空闲预取下一段(reader_on_tick 调)。当前页距下一段界 ≤2% 时把 s+1 段码点
|
||||||
|
* 提前解码进窗口(命中/未到阈值则幂等返回),翻到段界即零突发。 */
|
||||||
|
int rd_book_prefetch(rd_book *b)
|
||||||
|
{
|
||||||
|
if (!b || b->is_img || b->npages <= 0) return -1;
|
||||||
|
if (!(b->seg_dir[0] && b->fmt == RD_FMT_TXT)) return 0; /* 仅 TXT 段路径有段界预取意义 */
|
||||||
|
int cur = b->cur; if (cur < 0) cur = 0; if (cur >= b->npages) cur = b->npages - 1;
|
||||||
|
int s = b->pages[cur].seg;
|
||||||
|
if (s < 0 || s + 1 >= b->nsegs) return 0;
|
||||||
|
int bnd = b->segs[s + 1].page_start;
|
||||||
|
int d = cur - bnd; if (d < 0) d = -d;
|
||||||
|
if ((long long)d * 100 / b->npages > RD_WIN_NEAR_PCT) return 0;
|
||||||
|
return win_ensure(b, s + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Phase 2C:页级微排版。把 page_no 从页首字符起只排一页(复用 rd_layout_page)。
|
||||||
|
* 拼接页(TXT 段界末页)在窗口两段 cps 相接的临时缓冲上排,天然连续跨段。
|
||||||
|
* 输出:*chars 与 *chars_n = 绘制用码点视图(窗口 cps 或 b->stitch_cps,下次调用前勿 free),
|
||||||
|
* b->render_lines 填行;*nlines 行数。返回 0=成功,1=空页,负=错误。 */
|
||||||
|
static int page_micro_layout(rd_book *b, int page_no, const uint32_t **chars, size_t *chars_n, int *nlines)
|
||||||
|
{
|
||||||
|
*chars = NULL; *chars_n = 0; *nlines = 0;
|
||||||
|
rd_book_page *pg = &b->pages[page_no];
|
||||||
|
int seg_idx = (int)pg->seg;
|
||||||
|
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
|
||||||
|
|
||||||
|
int stitched = (b->seg_dir[0] && b->fmt == RD_FMT_TXT) &&
|
||||||
|
(page_no == b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1) &&
|
||||||
|
(seg_idx + 1 < b->nsegs);
|
||||||
|
if (win_ensure(b, seg_idx) != 0) return -2;
|
||||||
|
if (stitched && win_ensure(b, seg_idx + 1) != 0) return -2;
|
||||||
|
|
||||||
|
int lh = (b->fs * b->ls) / 100; if (lh < 1) lh = 1;
|
||||||
|
int max_lines = b->content_h / lh + 2;
|
||||||
|
if (b->render_lines_cap < max_lines) {
|
||||||
|
rd_line *t = (rd_line *)realloc(b->render_lines, (size_t)max_lines * sizeof(rd_line));
|
||||||
|
if (!t) return -6;
|
||||||
|
b->render_lines = t; b->render_lines_cap = max_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
cps_slot *s0 = win_find(b, seg_idx);
|
||||||
|
const uint32_t *view; size_t view_n; int32_t start;
|
||||||
|
if (!stitched) {
|
||||||
|
view = s0->cps; view_n = s0->n;
|
||||||
|
start = pg->seg_char;
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if ((size_t)start > view_n) start = (int32_t)view_n;
|
||||||
|
} else {
|
||||||
|
cps_slot *s1 = win_find(b, seg_idx + 1);
|
||||||
|
int32_t tail = pg->seg_char; if (tail < 0) tail = 0;
|
||||||
|
if ((size_t)tail > s0->n) return 1;
|
||||||
|
size_t tail_n = s0->n - (size_t)tail;
|
||||||
|
/* 拼接页只吃「一页能装下」的下一段前缀;按一页行数×行宽×2(零宽字符余量)估上界。 */
|
||||||
|
size_t budget = (size_t)(max_lines + 2) * (size_t)(b->content_w + 2) * 2u + 64u;
|
||||||
|
size_t take = s1->n < budget ? s1->n : budget;
|
||||||
|
size_t total = tail_n + take;
|
||||||
|
if (b->stitch_cap < total) {
|
||||||
|
uint32_t *t = (uint32_t *)realloc(b->stitch_cps, (total ? total : 1) * sizeof(uint32_t));
|
||||||
|
if (!t) return -6;
|
||||||
|
b->stitch_cps = t; b->stitch_cap = total;
|
||||||
|
}
|
||||||
|
memcpy(b->stitch_cps, s0->cps + tail, tail_n * sizeof(uint32_t));
|
||||||
|
memcpy(b->stitch_cps + tail_n, s1->cps, take * sizeof(uint32_t));
|
||||||
|
view = b->stitch_cps; view_n = total;
|
||||||
|
start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mid_para = !(pg->flags & 1u);
|
||||||
|
rd_layout_in in;
|
||||||
|
in.chars = view; in.n = (int32_t)view_n; in.fs = b->fs; in.line_scale = b->ls;
|
||||||
|
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
|
||||||
|
in.meas = book_meas; in.mctx = b;
|
||||||
|
int32_t next_start = 0;
|
||||||
|
int rc = rd_layout_page(&in, start, mid_para, b->render_lines, max_lines, nlines, &next_start);
|
||||||
|
if (rc == RD_LAYOUT_ERR_OVER) return -4;
|
||||||
|
if (rc != RD_LAYOUT_OK) return -3;
|
||||||
|
*chars = view; *chars_n = view_n;
|
||||||
|
return (*nlines > 0) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
|
/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
|
||||||
static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
|
static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
|
||||||
{
|
{
|
||||||
int npix = b->pw * b->ph;
|
int npix = b->pw * b->ph;
|
||||||
for (int i = 0; i < npix; i++) out[i] = bg565;
|
for (int i = 0; i < npix; i++) out[i] = bg565;
|
||||||
rd_book_page *pg = &b->pages[page_no];
|
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
|
||||||
int seg_idx = (int)pg->seg;
|
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
|
||||||
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
|
if (rc != 0) return rc;
|
||||||
if (load_seg(b, seg_idx) != 0) return -2;
|
|
||||||
int local_page = page_no - b->segs[seg_idx].page_start;
|
|
||||||
if (local_page < 0 || local_page >= b->seg_npages) return 1;
|
|
||||||
rd_page *pp = &b->seg_pages[local_page];
|
|
||||||
int asc = rdft_ascender(b->ft);
|
int asc = rdft_ascender(b->ft);
|
||||||
int screen_x0 = b->mx;
|
int screen_x0 = b->mx;
|
||||||
int screen_y0 = b->top;
|
int screen_y0 = b->top;
|
||||||
for (int li = 0; li < pp->nlines; li++) {
|
for (int li = 0; li < nlines; li++) {
|
||||||
rd_line *ln = &b->seg_lines[pp->first_line + li];
|
rd_line *ln = &b->render_lines[li];
|
||||||
int pen = screen_x0 + ln->x;
|
int pen = screen_x0 + ln->x;
|
||||||
int baseline = screen_y0 + ln->y + asc;
|
int baseline = screen_y0 + ln->y + asc;
|
||||||
for (int32_t ci = ln->s; ci < ln->e; ci++) {
|
for (int32_t ci = ln->s; ci < ln->e; ci++) {
|
||||||
uint32_t cp = b->seg_cps[ci];
|
if (ci < 0 || (size_t)ci >= chars_n) continue;
|
||||||
|
uint32_t cp = chars[ci];
|
||||||
int adv = 0, bw = 0, bh = 0, left = 0, btop = 0;
|
int adv = 0, bw = 0, bh = 0, left = 0, btop = 0;
|
||||||
const uint8_t *bmp = NULL;
|
const uint8_t *bmp = NULL;
|
||||||
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop);
|
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop);
|
||||||
@@ -704,6 +1181,8 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
|
|||||||
b->content_w = b->pw - 2 * b->mx;
|
b->content_w = b->pw - 2 * b->mx;
|
||||||
b->content_h = b->ph - b->top - b->my;
|
b->content_h = b->ph - b->top - b->my;
|
||||||
if (b->ft) rdft_set_px(b->ft, fs);
|
if (b->ft) rdft_set_px(b->ft, fs);
|
||||||
|
win_clear(b); /* 页表重建 → 窗口/tail 状态整体复位(码点可复用,但按重建统一清) */
|
||||||
|
tail_clear(b);
|
||||||
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
|
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
|
||||||
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
|
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
|
||||||
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
|
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
|
||||||
@@ -740,22 +1219,20 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
|
|||||||
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
|
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
|
||||||
{
|
{
|
||||||
if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
|
if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
|
||||||
rd_book_page *pg = &b->pages[page_no];
|
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
|
||||||
int seg_idx = (int)pg->seg;
|
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
|
||||||
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
|
if (rc != 0) return rc;
|
||||||
if (load_seg(b, seg_idx) != 0) return -2;
|
|
||||||
int local_page = page_no - b->segs[seg_idx].page_start;
|
|
||||||
if (local_page < 0 || local_page >= b->seg_npages) return 1;
|
|
||||||
rd_page *pp = &b->seg_pages[local_page];
|
|
||||||
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
|
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
|
||||||
if (!line_utf8) return -6;
|
if (!line_utf8) return -6;
|
||||||
for (int li = 0; li < pp->nlines; li++) {
|
for (int li = 0; li < nlines; li++) {
|
||||||
rd_line *ln = &b->seg_lines[pp->first_line + li];
|
rd_line *ln = &b->render_lines[li];
|
||||||
int x = b->mx + ln->x, y = b->top + ln->y;
|
int x = b->mx + ln->x, y = b->top + ln->y;
|
||||||
size_t o = 0;
|
size_t o = 0;
|
||||||
for (int32_t ci = ln->s; ci < ln->e; ci++) {
|
for (int32_t ci = ln->s; ci < ln->e; ci++) {
|
||||||
|
if (ci < 0 || (size_t)ci >= chars_n) continue;
|
||||||
uint8_t tmp[4];
|
uint8_t tmp[4];
|
||||||
int n = rd_cp_to_utf8(b->seg_cps[ci], tmp);
|
int n = rd_cp_to_utf8(chars[ci], tmp);
|
||||||
|
if (o + (size_t)n + 1 > (size_t)(b->content_w * 4 + 32)) break;
|
||||||
memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n;
|
memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n;
|
||||||
}
|
}
|
||||||
line_utf8[o] = 0;
|
line_utf8[o] = 0;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ typedef struct rd_book rd_book;
|
|||||||
typedef struct rd_book_page {
|
typedef struct rd_book_page {
|
||||||
int32_t seg; /* 段下标 */
|
int32_t seg; /* 段下标 */
|
||||||
int32_t seg_char; /* 段内字符偏移(页首) */
|
int32_t seg_char; /* 段内字符偏移(页首) */
|
||||||
|
uint8_t flags; /* bit0 = 首页行是否段首行(缩进/段距判定;Phase 2A/2C) */
|
||||||
} rd_book_page;
|
} rd_book_page;
|
||||||
|
|
||||||
/* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 /
|
/* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 /
|
||||||
@@ -36,6 +37,7 @@ int rd_book_pages(rd_book *b);
|
|||||||
int rd_book_current(rd_book *b); /* 当前页 */
|
int rd_book_current(rd_book *b); /* 当前页 */
|
||||||
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
|
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
|
||||||
int rd_book_set_page(rd_book *b, int page); /* 跳到绝对页;clamp;返回实际页(段刮擦用) */
|
int rd_book_set_page(rd_book *b, int page); /* 跳到绝对页;clamp;返回实际页(段刮擦用) */
|
||||||
|
int rd_book_prefetch(rd_book *b); /* 空闲预取下一段(reader_on_tick 调);0=已备/无需 */
|
||||||
int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */
|
int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */
|
||||||
|
|
||||||
/* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */
|
/* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */
|
||||||
@@ -73,6 +75,25 @@ void rd_book_set_index_dir(const char *dir);
|
|||||||
* 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */
|
* 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */
|
||||||
void rd_book_set_seg_base(const char *base);
|
void rd_book_set_seg_base(const char *base);
|
||||||
|
|
||||||
|
/* R3.3(TXT 章节):章节规范解析(core/rdchapter.c)+ 章节→页映射。
|
||||||
|
* 只对 TXT(seg_mode 分段流式路径)生效;其它格式返回「无章节」,不破坏既有路径。
|
||||||
|
* rd_book_chapter_n 触发解析(首次读全部段文件解码扫行,或命中章节缓存秒回),
|
||||||
|
* 解析/缓存后就绪;顶栏章节名与「当前页属于哪章」据此工作。 */
|
||||||
|
int rd_book_chapter_n(rd_book *b); /* 章节数;-1=非 TXT/CBZ(不可用) */
|
||||||
|
int rd_book_chapter_ready(rd_book *b); /* 1=章节已解析(含无章节=0 但已确认) */
|
||||||
|
int rd_book_reparse_chapters(rd_book *b); /* 手动重解析:清除缓存+重扫+落盘;见 book.c */
|
||||||
|
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
|
||||||
|
int64_t *char_off, int *seg, int32_t *seg_char, int *number);
|
||||||
|
int rd_book_chapter_for_page(rd_book *b, int page, int *idx); /* 当前页所属章节下标;-1=无 */
|
||||||
|
int rd_book_page_for_char(rd_book *b, int64_t char_off); /* 全书字符偏移 → 页号;-1=不可用 */
|
||||||
|
int64_t rd_book_page_char_off(rd_book *b, int page); /* 页首全书字符偏移;-1=不可用 */
|
||||||
|
|
||||||
|
/* 段(文件/段模式跳转用)。非段模式返回 -1。 */
|
||||||
|
int rd_book_seg_n(rd_book *b); /* 段数 */
|
||||||
|
int rd_book_seg_page(rd_book *b, int seg); /* 段首页(ensure built);-1 */
|
||||||
|
int rd_book_seg_page_count(rd_book *b, int seg); /* 段页数;-1 */
|
||||||
|
int rd_book_seg_of_page(rd_book *b, int page); /* 页所属段;-1 */
|
||||||
|
|
||||||
/* 字体/几何 */
|
/* 字体/几何 */
|
||||||
int rd_book_font(rd_book *b); /* 当前字号 */
|
int rd_book_font(rd_book *b); /* 当前字号 */
|
||||||
int rd_book_ls(rd_book *b); /* 行距% */
|
int rd_book_ls(rd_book *b); /* 行距% */
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "book.h"
|
#include "book.h"
|
||||||
#include "progress.h"
|
#include "progress.h"
|
||||||
#include "reader_conf.h"
|
#include "reader_conf.h"
|
||||||
|
#include "rdchapter.h" /* RDCHAP_TITLE_MAX(章节标题) */
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
#define RD_MENU_H 64
|
#define RD_MENU_H 64
|
||||||
#define RD_MENU_BTN 4 /* 返回 | 目录 | 界面 | 设置(MVP 单条) */
|
#define RD_MENU_BTN 5 /* 返回 | 目录 | 跳转 | 界面 | 设置(R3.3 加「跳转」) */
|
||||||
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
|
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
|
||||||
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */
|
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */
|
||||||
#define RD_UI_H 268 /* 「界面」排版面板高 */
|
#define RD_UI_H 268 /* 「界面」排版面板高 */
|
||||||
@@ -36,6 +37,13 @@ static int g_menu_open;
|
|||||||
static int g_ui_open; /* 「界面」排版面板(B) */
|
static int g_ui_open; /* 「界面」排版面板(B) */
|
||||||
static int g_dir_open; /* 目录覆盖层 */
|
static int g_dir_open; /* 目录覆盖层 */
|
||||||
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
|
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
|
||||||
|
static int g_jump_open; /* 跳转覆盖层(R3.3) */
|
||||||
|
static int g_jump_mode; /* 1=章节模式; 2=文件/段模式; 0=未定 */
|
||||||
|
static int g_jump_sel; /* 当前选中章节/段下标 */
|
||||||
|
static int g_jump_scroll; /* 列表滚动(首行项下标) */
|
||||||
|
static char g_jump_buf[8]; /* 章内/段内页码输入缓冲(ASCII 数字) */
|
||||||
|
static int g_chap_cache_idx = -1; /* 章节范围缓存(章起/止页;随字号/行距重排失效) */
|
||||||
|
static int g_chap_cache_start, g_chap_cache_end;
|
||||||
static char g_font_path[400];
|
static char g_font_path[400];
|
||||||
static int g_notice_ms;
|
static int g_notice_ms;
|
||||||
|
|
||||||
@@ -48,12 +56,13 @@ static int g_anim_dir; /* 1=下一页(旧左移/新右入);-
|
|||||||
#define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */
|
#define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */
|
||||||
static int g_scrub; /* 1=刮擦中 */
|
static int g_scrub; /* 1=刮擦中 */
|
||||||
static int g_scrub_page; /* 刮擦目标页(实时预览用) */
|
static int g_scrub_page; /* 刮擦目标页(实时预览用) */
|
||||||
|
static int g_scrub_moved; /* 本次刮擦是否发生了拖动(无拖动=点按,不当翻页跳页) */
|
||||||
|
|
||||||
/* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */
|
/* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */
|
||||||
static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */
|
static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */
|
||||||
|
|
||||||
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
|
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
|
||||||
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
|
static char g_chapter[RDCHAP_TITLE_MAX]; /* 顶栏章节名(TXT 无章节=文件名) */
|
||||||
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
|
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
|
||||||
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
|
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
|
||||||
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
|
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
|
||||||
@@ -168,7 +177,21 @@ static const char *resolve_font(void)
|
|||||||
|
|
||||||
static void chapter_name(void)
|
static void chapter_name(void)
|
||||||
{
|
{
|
||||||
/* TXT 无章节 → 文件名;R1 固定。 */
|
/* TXT 有章节(已解析)→ 当前页所属章名;否则文件名(R1 固定)。
|
||||||
|
* 仅在章节已就绪时用章名(rd_book_chapter_ready 不触发扫描),首屏/未解析时用文件名,
|
||||||
|
* 避免开书即整书扫描占 UI 线程。 */
|
||||||
|
if (!g_book) { g_chapter[0] = 0; return; }
|
||||||
|
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
|
||||||
|
int page = rd_book_current(g_book);
|
||||||
|
int idx = -1;
|
||||||
|
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
|
||||||
|
char t[RDCHAP_TITLE_MAX];
|
||||||
|
if (rd_book_chapter(g_book, idx, t, sizeof t, NULL, NULL, NULL, NULL) == 0) {
|
||||||
|
snprintf(g_chapter, sizeof g_chapter, "%s", t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
|
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +199,7 @@ static void reader_reflow(void)
|
|||||||
{
|
{
|
||||||
if (!g_book) return;
|
if (!g_book) return;
|
||||||
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
|
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
|
||||||
|
g_chap_cache_idx = -1; /* 页索引随字号/行距变 → 章范围缓存失效 */
|
||||||
int seg = 0; int32_t ch = 0;
|
int seg = 0; int32_t ch = 0;
|
||||||
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
|
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
|
||||||
g_prog_dirty = 1;
|
g_prog_dirty = 1;
|
||||||
@@ -266,6 +290,354 @@ static void reader_draw_scrub(const meow_theme *th)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- R3.3 跳转弹层(章节模式 / 文件·段模式) ----------------
|
||||||
|
* 复用 g_dir 系列「半透明底 + 条目列表 + 当前高亮」;顶部提供:当前章节/段信息、页码输入
|
||||||
|
* (自绘数字键盘 0-9 + ⌫ + 跳)、快捷(首 / ±10 / ±100 / 末)。只对 TXT(seg 模式)启用:
|
||||||
|
* rd_book_chapter_n>0 → 章节模式;否则 rd_book_seg_n>0 → 文件/段模式。 */
|
||||||
|
#define J_SHORTCUT_N 6
|
||||||
|
#define J_SHORTCUT_X0 16
|
||||||
|
#define J_SHORTCUT_GAP 6
|
||||||
|
#define J_SHORTCUT_H 32
|
||||||
|
#define J_KEY_W 64
|
||||||
|
#define J_KEY_H 42
|
||||||
|
#define J_KEY_GAP 8
|
||||||
|
#define J_ROW 30
|
||||||
|
enum {
|
||||||
|
J_NONE = 0,
|
||||||
|
J_GO_FIRST = 1, J_GO_M100 = 2, J_GO_M10 = 3,
|
||||||
|
J_GO_P10 = 4, J_GO_P100 = 5, J_GO_LAST = 6,
|
||||||
|
J_DIG_0 = 10, J_DIG_1 = 11, J_DIG_2 = 12, J_DIG_3 = 13, J_DIG_4 = 14,
|
||||||
|
J_DIG_5 = 15, J_DIG_6 = 16, J_DIG_7 = 17, J_DIG_8 = 18, J_DIG_9 = 19,
|
||||||
|
J_BACK = 30, J_JUMP = 31,
|
||||||
|
J_ITEM = 100, /* 100 + i = 列表第 i 项 */
|
||||||
|
};
|
||||||
|
|
||||||
|
static int jump_hdr_y(void) { return g_top; }
|
||||||
|
static int jump_info_y(void) { return g_top + 30; }
|
||||||
|
static int jump_shortcut_y(void){ return g_top + 56; }
|
||||||
|
static int jump_input_y(void) { return g_top + 92; }
|
||||||
|
static int jump_pad_y(void) { return g_top + 128; }
|
||||||
|
static int jump_list_y(void) { return g_top + 128 + 4 * (J_KEY_H + J_KEY_GAP) + 10; }
|
||||||
|
|
||||||
|
static int chapter_range(int idx, int *start, int *end)
|
||||||
|
{
|
||||||
|
if (!g_book || idx < 0) return -1;
|
||||||
|
if (g_chap_cache_idx == idx) { if (start) *start = g_chap_cache_start; if (end) *end = g_chap_cache_end; return 0; }
|
||||||
|
int64_t co = 0;
|
||||||
|
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) != 0) return -1;
|
||||||
|
int s = rd_book_page_for_char(g_book, co);
|
||||||
|
if (s < 0) return -1;
|
||||||
|
int n = rd_book_chapter_n(g_book);
|
||||||
|
int e;
|
||||||
|
if (idx + 1 < n) {
|
||||||
|
int64_t co2 = 0;
|
||||||
|
if (rd_book_chapter(g_book, idx + 1, NULL, 0, &co2, NULL, NULL, NULL) != 0) return -1;
|
||||||
|
e = rd_book_page_for_char(g_book, co2) - 1;
|
||||||
|
} else {
|
||||||
|
e = rd_book_pages(g_book) - 1;
|
||||||
|
}
|
||||||
|
if (e < s) e = s;
|
||||||
|
g_chap_cache_idx = idx; g_chap_cache_start = s; g_chap_cache_end = e;
|
||||||
|
if (start) *start = s;
|
||||||
|
if (end) *end = e;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跳转选中项 idx 的页区间(章节模式=章范围;文件模式=段范围)。 */
|
||||||
|
static int jump_region(int idx, int *start, int *end, int *inp, int *ofp)
|
||||||
|
{
|
||||||
|
if (!g_book) return -1;
|
||||||
|
int s, e;
|
||||||
|
if (g_jump_mode == 1) {
|
||||||
|
if (chapter_range(idx, &s, &e) != 0) return -1;
|
||||||
|
} else if (g_jump_mode == 2) {
|
||||||
|
int nseg = rd_book_seg_n(g_book);
|
||||||
|
if (idx < 0 || idx >= nseg) return -1;
|
||||||
|
s = rd_book_seg_page(g_book, idx);
|
||||||
|
if (s < 0) return -1;
|
||||||
|
e = s + rd_book_seg_page_count(g_book, idx) - 1;
|
||||||
|
if (e < s) e = s;
|
||||||
|
} else return -1;
|
||||||
|
int cur = rd_book_current(g_book);
|
||||||
|
if (cur < s) cur = s;
|
||||||
|
if (cur > e) cur = e;
|
||||||
|
if (start) *start = s;
|
||||||
|
if (end) *end = e;
|
||||||
|
if (inp) *inp = cur - s + 1;
|
||||||
|
if (ofp) *ofp = e - s + 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页底条右侧文本:章内 y/Z · 全书 x/M(章节就绪且范围可算时),否则 全书 x/M。 */
|
||||||
|
static void footer_progress(char *out, int outsz)
|
||||||
|
{
|
||||||
|
int np = rd_book_pages(g_book);
|
||||||
|
int page = rd_book_current(g_book);
|
||||||
|
if (np <= 0) { snprintf(out, outsz, "0/0"); return; }
|
||||||
|
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
|
||||||
|
int idx = -1;
|
||||||
|
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
|
||||||
|
int s = -1, e = -1;
|
||||||
|
if (chapter_range(idx, &s, &e) == 0 && e >= s) {
|
||||||
|
int inp = page - s + 1, ofp = e - s + 1;
|
||||||
|
if (inp > 0 && ofp > 0) { snprintf(out, outsz, "%d/%d · %d/%d", inp, ofp, page + 1, np); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(out, outsz, "%d/%d", page + 1, np);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jump_chip(int x, int y, int w, int h, const char *label, int active, const meow_theme *th)
|
||||||
|
{
|
||||||
|
if (active) meow_ui_fill_rect(x, y, w, h, th->primaryContainer);
|
||||||
|
else meow_ui_fill_rect(x, y, w, h, th->surfaceContainerHighest);
|
||||||
|
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
|
||||||
|
meow_ui_text_center(x + w / 2, y + (h - 20) / 2, label, 20, tc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reader_draw_jump(const meow_theme *th)
|
||||||
|
{
|
||||||
|
if (!g_book) return;
|
||||||
|
int page = rd_book_current(g_book);
|
||||||
|
int np = rd_book_pages(g_book);
|
||||||
|
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
|
||||||
|
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
|
||||||
|
|
||||||
|
/* 头栏 */
|
||||||
|
int y0 = jump_hdr_y();
|
||||||
|
meow_ui_fill_rect(0, y0, g_pw, 30, th->surfaceContainer);
|
||||||
|
char hdr[48];
|
||||||
|
snprintf(hdr, sizeof hdr, "跳转 · %s", (g_jump_mode == 1) ? "章节" : "文件/段");
|
||||||
|
meow_ui_text(16, y0 + 5, hdr, 20, th->onSurface);
|
||||||
|
meow_ui_draw_rect(0, y0 + 29, g_pw, 1, th->outlineVariant);
|
||||||
|
|
||||||
|
if (n <= 0) {
|
||||||
|
meow_ui_text(20, jump_info_y() + 12, "本书无可用跳转目标", 20, th->dim);
|
||||||
|
meow_ui_text(20, g_ph - 30, "点空白关闭", 16, th->dim);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息行 */
|
||||||
|
char info[256]; info[0] = 0;
|
||||||
|
int s, e, inp, ofp;
|
||||||
|
if (g_jump_mode == 1) {
|
||||||
|
char lab[RDCHAP_TITLE_MAX];
|
||||||
|
int num = -1;
|
||||||
|
if (rd_book_chapter(g_book, g_jump_sel, lab, sizeof lab, NULL, NULL, NULL, &num) == 0 &&
|
||||||
|
jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
|
||||||
|
char prefix[24]; prefix[0] = 0;
|
||||||
|
if (num >= 0) snprintf(prefix, sizeof prefix, "第%d章 ", num);
|
||||||
|
snprintf(info, sizeof info, "%s%s · 章内 %d/%d · 全书 %d/%d", prefix, lab, inp, ofp, page + 1, np);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0)
|
||||||
|
snprintf(info, sizeof info, "段 %d/%d · 段内 %d/%d · 全书 %d/%d", g_jump_sel + 1, n, inp, ofp, page + 1, np);
|
||||||
|
}
|
||||||
|
meow_ui_text(16, jump_info_y() + 3, info[0] ? info : "—", 16, th->fg);
|
||||||
|
|
||||||
|
/* 快捷 chips */
|
||||||
|
static const char *sc_lbl[J_SHORTCUT_N] = { "首", "-100", "-10", "+10", "+100", "末" };
|
||||||
|
int x = J_SHORTCUT_X0;
|
||||||
|
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
|
||||||
|
for (int i = 0; i < J_SHORTCUT_N; i++) {
|
||||||
|
jump_chip(x, jump_shortcut_y(), cw, J_SHORTCUT_H, sc_lbl[i], 0, th);
|
||||||
|
x += cw + J_SHORTCUT_GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入行 */
|
||||||
|
meow_ui_text(16, jump_input_y() + 5, (g_jump_mode == 1) ? "章内页" : "段内页", 20, th->fg);
|
||||||
|
char ib[48];
|
||||||
|
snprintf(ib, sizeof ib, "%s", g_jump_buf[0] ? g_jump_buf : "_");
|
||||||
|
meow_ui_text(120, jump_input_y() + 3, ib, 24, th->primary);
|
||||||
|
meow_ui_text_right(g_pw - 16, jump_input_y() + 7, "输页码点「跳」 · ⌫退格", 16, th->dim);
|
||||||
|
|
||||||
|
/* 数字键盘 1..9 / ⌫ 0 跳 */
|
||||||
|
static const char *pad[4][3] = {
|
||||||
|
{ "1", "2", "3" }, { "4", "5", "6" }, { "7", "8", "9" }, { "⌫", "0", "跳" },
|
||||||
|
};
|
||||||
|
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
|
||||||
|
int pad_x = (g_pw - pad_w) / 2;
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 3; c++) {
|
||||||
|
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
|
||||||
|
int ky = jump_pad_y() + r * (J_KEY_H + J_KEY_GAP);
|
||||||
|
jump_chip(kx, ky, J_KEY_W, J_KEY_H, pad[r][c], (r == 3 && c == 2), th);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 条目列表(当前选中高亮) */
|
||||||
|
int top = jump_list_y();
|
||||||
|
int maxshow = (g_ph - top - 8) / J_ROW;
|
||||||
|
if (maxshow < 1) maxshow = 1;
|
||||||
|
/* 让当前选中项进入可视区 */
|
||||||
|
if (g_jump_sel >= 0) {
|
||||||
|
if (g_jump_sel < g_jump_scroll) g_jump_scroll = g_jump_sel;
|
||||||
|
else if (g_jump_sel >= g_jump_scroll + maxshow) g_jump_scroll = g_jump_sel - maxshow + 1;
|
||||||
|
}
|
||||||
|
if (g_jump_scroll + maxshow > n) g_jump_scroll = (n - maxshow < 0 ? 0 : n - maxshow);
|
||||||
|
if (g_jump_scroll < 0) g_jump_scroll = 0;
|
||||||
|
for (int i = 0; i < maxshow; i++) {
|
||||||
|
int item = g_jump_scroll + i;
|
||||||
|
if (item >= n) break;
|
||||||
|
char t[96];
|
||||||
|
if (g_jump_mode == 1) {
|
||||||
|
if (rd_book_chapter(g_book, item, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
|
||||||
|
} else {
|
||||||
|
snprintf(t, sizeof t, "段 %d", item + 1);
|
||||||
|
}
|
||||||
|
int yy = top + i * J_ROW;
|
||||||
|
if (item == g_jump_sel) meow_ui_fill_rect(0, yy, g_pw, J_ROW - 4, th->surfaceContainer);
|
||||||
|
meow_ui_text(24, yy + 4, t, 20, th->fg);
|
||||||
|
meow_ui_draw_rect(12, yy + J_ROW - 6, g_pw - 24, 1, th->outline);
|
||||||
|
}
|
||||||
|
meow_ui_text(20, g_ph - 30, "点条目跳转 · 点空白关闭", 16, th->dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int reader_jump_hit(int x, int y)
|
||||||
|
{
|
||||||
|
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
|
||||||
|
if (n <= 0) return J_NONE;
|
||||||
|
/* 快捷 chips */
|
||||||
|
int sy = jump_shortcut_y();
|
||||||
|
if (y >= sy && y < sy + J_SHORTCUT_H) {
|
||||||
|
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
|
||||||
|
if (x >= J_SHORTCUT_X0) {
|
||||||
|
int k = (x - J_SHORTCUT_X0) / (cw + J_SHORTCUT_GAP);
|
||||||
|
if (k >= 0 && k < J_SHORTCUT_N) return J_GO_FIRST + k;
|
||||||
|
}
|
||||||
|
return J_NONE;
|
||||||
|
}
|
||||||
|
/* 键盘 */
|
||||||
|
int py = jump_pad_y();
|
||||||
|
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
|
||||||
|
int pad_x = (g_pw - pad_w) / 2;
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
int ky = py + r * (J_KEY_H + J_KEY_GAP);
|
||||||
|
if (y >= ky && y < ky + J_KEY_H) {
|
||||||
|
for (int c = 0; c < 3; c++) {
|
||||||
|
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
|
||||||
|
if (x >= kx && x < kx + J_KEY_W) {
|
||||||
|
if (r == 3) {
|
||||||
|
if (c == 2) return J_JUMP;
|
||||||
|
if (c == 0) return J_BACK;
|
||||||
|
return J_DIG_0; /* 第三行 c==1 → 数字 0 */
|
||||||
|
}
|
||||||
|
return J_DIG_0 + (r * 3 + c) + 1; /* 0..2 行:1 2 3 / 4 5 6 / 7 8 9 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return J_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 条目列表 */
|
||||||
|
int top = jump_list_y();
|
||||||
|
if (y >= top) {
|
||||||
|
int idx = (y - top) / J_ROW + g_jump_scroll;
|
||||||
|
if (idx < n) return J_ITEM + idx;
|
||||||
|
return J_NONE;
|
||||||
|
}
|
||||||
|
return J_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reader_jump_to(int page)
|
||||||
|
{
|
||||||
|
if (!g_book) return;
|
||||||
|
int np = rd_book_pages(g_book);
|
||||||
|
if (np <= 0) return;
|
||||||
|
int cur = rd_book_current(g_book);
|
||||||
|
(void)cur;
|
||||||
|
rd_book_set_page(g_book, page);
|
||||||
|
g_prog_dirty = 1;
|
||||||
|
reader_refill_page();
|
||||||
|
reader_redraw();
|
||||||
|
reader_reflow_persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reader_jump_action(int a)
|
||||||
|
{
|
||||||
|
if (a == J_NONE) { g_jump_open = 0; reader_redraw(); return; }
|
||||||
|
int s, e, inp, ofp;
|
||||||
|
if (a >= J_GO_FIRST && a <= J_GO_LAST) {
|
||||||
|
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) != 0) return;
|
||||||
|
int target = rd_book_current(g_book);
|
||||||
|
switch (a) {
|
||||||
|
case J_GO_FIRST: target = s; break;
|
||||||
|
case J_GO_M100: target -= 100; break;
|
||||||
|
case J_GO_M10: target -= 10; break;
|
||||||
|
case J_GO_P10: target += 10; break;
|
||||||
|
case J_GO_P100: target += 100; break;
|
||||||
|
case J_GO_LAST: target = e; break;
|
||||||
|
}
|
||||||
|
if (target < s) target = s;
|
||||||
|
if (target > e) target = e;
|
||||||
|
reader_jump_to(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a >= J_DIG_0 && a <= J_DIG_9) {
|
||||||
|
size_t len = strlen(g_jump_buf);
|
||||||
|
if (len + 1 < sizeof g_jump_buf) { g_jump_buf[len] = (char)('0' + (a - J_DIG_0)); g_jump_buf[len + 1] = 0; }
|
||||||
|
reader_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a == J_BACK) {
|
||||||
|
size_t len = strlen(g_jump_buf);
|
||||||
|
if (len > 0) g_jump_buf[len - 1] = 0;
|
||||||
|
reader_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a == J_JUMP) {
|
||||||
|
if (g_jump_buf[0]) {
|
||||||
|
int tpage = atoi(g_jump_buf);
|
||||||
|
if (tpage < 1) tpage = 1;
|
||||||
|
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
|
||||||
|
int t = s + (tpage - 1);
|
||||||
|
if (t < s) t = s;
|
||||||
|
if (t > e) t = e;
|
||||||
|
reader_jump_to(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a >= J_ITEM) {
|
||||||
|
int item = a - J_ITEM;
|
||||||
|
if (g_jump_mode == 1) {
|
||||||
|
if (jump_region(item, &s, &e, &inp, &ofp) == 0) {
|
||||||
|
g_jump_sel = item;
|
||||||
|
reader_jump_to(s);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int sp = rd_book_seg_page(g_book, item);
|
||||||
|
if (sp >= 0) { g_jump_sel = item; reader_jump_to(sp); }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_jump_open = 0; reader_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打开跳转弹层:解析章节(首次会扫描+缓存),按是否有章节选章节/文件·段模式。 */
|
||||||
|
static void reader_jump_open(void)
|
||||||
|
{
|
||||||
|
if (!g_book) return;
|
||||||
|
g_jump_buf[0] = 0;
|
||||||
|
g_jump_scroll = 0;
|
||||||
|
int nc = rd_book_chapter_n(g_book);
|
||||||
|
if (nc > 0) {
|
||||||
|
g_jump_mode = 1;
|
||||||
|
int idx = -1;
|
||||||
|
rd_book_chapter_for_page(g_book, rd_book_current(g_book), &idx);
|
||||||
|
g_jump_sel = (idx >= 0) ? idx : 0;
|
||||||
|
} else if (rd_book_seg_n(g_book) > 0) {
|
||||||
|
g_jump_mode = 2;
|
||||||
|
int seg = rd_book_seg_of_page(g_book, rd_book_current(g_book));
|
||||||
|
g_jump_sel = (seg >= 0) ? seg : 0;
|
||||||
|
} else {
|
||||||
|
g_jump_mode = 2;
|
||||||
|
g_jump_sel = 0;
|
||||||
|
}
|
||||||
|
g_jump_open = 1;
|
||||||
|
g_menu_open = 0;
|
||||||
|
reader_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
|
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
|
||||||
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
|
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
|
||||||
* 几何 draw/hit 共用(常量)。 */
|
* 几何 draw/hit 共用(常量)。 */
|
||||||
@@ -416,6 +788,7 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
|
|||||||
{
|
{
|
||||||
(void)w; (void)ctx;
|
(void)w; (void)ctx;
|
||||||
meow_ui_clear(g_page_bg);
|
meow_ui_clear(g_page_bg);
|
||||||
|
chapter_name(); /* 每帧同步顶栏/页底章节名(就绪后取章名,否则文件名) */
|
||||||
if (g_book) {
|
if (g_book) {
|
||||||
int page = rd_book_current(g_book);
|
int page = rd_book_current(g_book);
|
||||||
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
|
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
|
||||||
@@ -427,8 +800,6 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
|
|||||||
}
|
}
|
||||||
/* 页头条(书名 + 章节名;可选,rd.header) */
|
/* 页头条(书名 + 章节名;可选,rd.header) */
|
||||||
int sbh = meow_ui_statusbar_height();
|
int sbh = meow_ui_statusbar_height();
|
||||||
int pages = g_book ? rd_book_pages(g_book) : 0;
|
|
||||||
int page = g_book ? rd_book_current(g_book) : 0;
|
|
||||||
const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
|
const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
|
||||||
if (g_show_header) {
|
if (g_show_header) {
|
||||||
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
|
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
|
||||||
@@ -438,24 +809,23 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
|
|||||||
}
|
}
|
||||||
/* 页底条(章节 + 页码;可选,rd.footer) */
|
/* 页底条(章节 + 页码;可选,rd.footer) */
|
||||||
if (g_show_footer) {
|
if (g_show_footer) {
|
||||||
char ft[48];
|
char ft[64];
|
||||||
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", page + 1, pages);
|
footer_progress(ft, sizeof ft);
|
||||||
else snprintf(ft, sizeof ft, "0/0");
|
|
||||||
int fy = g_ph - RD_FOOT_H;
|
int fy = g_ph - RD_FOOT_H;
|
||||||
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
|
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
|
||||||
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
|
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
|
||||||
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
|
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* R3 刮擦指示条(非目录/面板覆盖时显示;菜单为底部条,与右缘条并存即可) */
|
/* R3 刮擦指示条(非目录/面板/跳转覆盖时显示;菜单为底部条,与右缘条并存即可) */
|
||||||
if (g_book && !g_dir_open && !g_ui_open) reader_draw_scrub(th);
|
if (g_book && !g_dir_open && !g_ui_open && !g_jump_open) reader_draw_scrub(th);
|
||||||
|
|
||||||
if (g_menu_open) {
|
if (g_menu_open) {
|
||||||
int y = g_ph - RD_MENU_H;
|
int y = g_ph - RD_MENU_H;
|
||||||
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
|
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
|
||||||
/* 4 按钮(MVP 单条):返回 | 目录 | 界面 | 设置 */
|
/* 5 按钮:返回 | 目录 | 跳转 | 界面 | 设置 */
|
||||||
int bw = g_pw / RD_MENU_BTN;
|
int bw = g_pw / RD_MENU_BTN;
|
||||||
const char *labels[RD_MENU_BTN] = { "返回", "目录", "界面", "设置" };
|
const char *labels[RD_MENU_BTN] = { "返回", "目录", "跳转", "界面", "设置" };
|
||||||
for (int i = 0; i < RD_MENU_BTN; i++) {
|
for (int i = 0; i < RD_MENU_BTN; i++) {
|
||||||
int bx = i * bw;
|
int bx = i * bw;
|
||||||
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
|
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
|
||||||
@@ -463,9 +833,13 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (g_ui_open && g_book) reader_draw_ui_panel(th);
|
if (g_ui_open && g_book) reader_draw_ui_panel(th);
|
||||||
|
if (g_jump_open && g_book) reader_draw_jump(th);
|
||||||
if (g_dir_open && g_book) {
|
if (g_dir_open && g_book) {
|
||||||
/* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */
|
/* 目录覆盖层:TXT 有章节 → 章节列表(复用 rd_book 章节数据,与跳转共享);否则 rdsrc 目录;无则「无章节」。 */
|
||||||
int n = rd_book_toc_n(g_book);
|
int nchap = 0;
|
||||||
|
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
|
||||||
|
int ntoc = (nchap == 0) ? rd_book_toc_n(g_book) : 0;
|
||||||
|
int n = (nchap > 0) ? nchap : ntoc;
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
int top = g_top + 8;
|
int top = g_top + 8;
|
||||||
int row = 34;
|
int row = 34;
|
||||||
@@ -474,19 +848,29 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
|
|||||||
if (maxshow > n) maxshow = n;
|
if (maxshow > n) maxshow = n;
|
||||||
g_dir_top = top; g_dir_row_h = row;
|
g_dir_top = top; g_dir_row_h = row;
|
||||||
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
|
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
|
||||||
char t[96]; int sg; int32_t cch;
|
char t[160];
|
||||||
int page = rd_book_current(g_book);
|
int page = rd_book_current(g_book);
|
||||||
|
int curchap = -1;
|
||||||
|
if (nchap > 0) rd_book_chapter_for_page(g_book, page, &curchap);
|
||||||
for (int i = 0; i < maxshow; i++) {
|
for (int i = 0; i < maxshow; i++) {
|
||||||
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
|
int hl = 0;
|
||||||
|
if (nchap > 0) {
|
||||||
|
if (rd_book_chapter(g_book, i, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
|
||||||
|
hl = (i == curchap);
|
||||||
|
} else {
|
||||||
|
int sg = -1; int32_t cch = 0;
|
||||||
|
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
|
||||||
|
hl = (page == sg);
|
||||||
|
}
|
||||||
int y0 = top + i * row;
|
int y0 = top + i * row;
|
||||||
if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
|
if (hl) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
|
||||||
meow_ui_text(24, y0, t, 20, th->fg);
|
meow_ui_text(24, y0, t, 20, th->fg);
|
||||||
meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
|
meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
|
||||||
}
|
}
|
||||||
meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
|
meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
|
||||||
} else {
|
} else {
|
||||||
g_dir_open = 0;
|
g_dir_open = 0;
|
||||||
meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim);
|
meow_ui_text(20, g_top + 40, (rd_book_chapter_n(g_book) == 0) ? "本书无章节" : "本书无目录", 20, th->dim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,12 +1013,13 @@ static void reader_scrub_preview(int page)
|
|||||||
static void reader_scrub_begin(int x, int y)
|
static void reader_scrub_begin(int x, int y)
|
||||||
{
|
{
|
||||||
(void)x;
|
(void)x;
|
||||||
if (!g_book || g_menu_open || g_dir_open) return;
|
if (!g_book || g_menu_open || g_dir_open || g_jump_open) return;
|
||||||
if (g_pw - x > RD_SCRUB_W) return;
|
if (g_pw - x > RD_SCRUB_W) return;
|
||||||
if (y < rect_y0() || y >= rect_y1()) return;
|
if (y < rect_y0() || y >= rect_y1()) return;
|
||||||
int np = rd_book_pages(g_book);
|
int np = rd_book_pages(g_book);
|
||||||
if (np <= 1) return;
|
if (np <= 1) return;
|
||||||
g_scrub = 1;
|
g_scrub = 1;
|
||||||
|
g_scrub_moved = 0; /* 每次触控重置;拖动(touch_move)才置 1,区分点按/刮擦 */
|
||||||
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
|
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
|
||||||
reader_scrub_preview(scrub_page_for_y(y));
|
reader_scrub_preview(scrub_page_for_y(y));
|
||||||
}
|
}
|
||||||
@@ -710,9 +1095,10 @@ int reader_open(const char *path)
|
|||||||
reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */
|
reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 复位动画/刮擦状态(跨书) */
|
/* 复位动画/刮擦/覆盖层状态(跨书) */
|
||||||
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
|
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
|
||||||
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0;
|
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0;
|
||||||
|
g_chap_cache_idx = -1;
|
||||||
|
|
||||||
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
|
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
|
||||||
if (g_book && !rd_book_is_image(g_book)) {
|
if (g_book && !rd_book_is_image(g_book)) {
|
||||||
@@ -739,6 +1125,23 @@ int reader_open(const char *path)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- R3.4 章节重解析(app/settings.c 调用) ---------- */
|
||||||
|
int reader_has_chapter_book(void)
|
||||||
|
{
|
||||||
|
if (!g_book || rd_book_is_image(g_book)) return 0;
|
||||||
|
/* TXT + 分段流式(seg 模式)即支持章节解析;用 rd_book_seg_n(不触发扫描)判定。 */
|
||||||
|
return (rd_book_seg_n(g_book) > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int reader_reparse_chapters(void)
|
||||||
|
{
|
||||||
|
if (!g_book) return -1;
|
||||||
|
int rc = rd_book_reparse_chapters(g_book);
|
||||||
|
if (rc >= 0) g_chap_cache_idx = -1; /* 章节列表变化 → 章范围缓存失效 */
|
||||||
|
if (g_reader_active) reader_redraw();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 视图生命周期 ---------- */
|
/* ---------- 视图生命周期 ---------- */
|
||||||
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
|
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
|
||||||
static void reader_on_enter(meow_view *v, void *ctx)
|
static void reader_on_enter(meow_view *v, void *ctx)
|
||||||
@@ -766,7 +1169,8 @@ static void reader_on_destroy(meow_view *v, void *ctx)
|
|||||||
if (g_book) rd_book_close(g_book);
|
if (g_book) rd_book_close(g_book);
|
||||||
g_book = NULL;
|
g_book = NULL;
|
||||||
reader_free_page_bufs();
|
reader_free_page_bufs();
|
||||||
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_reader_active = 0;
|
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0; g_scrub = 0; g_reader_active = 0;
|
||||||
|
g_chap_cache_idx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
meow_view *reader_view_create(void)
|
meow_view *reader_view_create(void)
|
||||||
@@ -785,28 +1189,52 @@ void reader_on_tap(int x, int y)
|
|||||||
{
|
{
|
||||||
(void)x;
|
(void)x;
|
||||||
if (!g_book) { return; }
|
if (!g_book) { return; }
|
||||||
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾 */
|
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾(仅真实拖动过才跳;点按右缘=翻下一页) */
|
||||||
g_scrub = 0;
|
g_scrub = 0;
|
||||||
reader_scrub_jump(g_scrub_page);
|
if (g_scrub_moved) { reader_scrub_jump(g_scrub_page); return; }
|
||||||
|
/* 未拖动:当作点按,落到下方三区翻页逻辑(右 1/3 下一页),避免右缘误触跳远页 */
|
||||||
|
}
|
||||||
|
if (g_jump_open) {
|
||||||
|
/* 跳转弹层:快捷/键盘/条目 → 执行;点空白关闭 */
|
||||||
|
int a = reader_jump_hit(x, y);
|
||||||
|
reader_jump_action(a);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (g_dir_open) {
|
if (g_dir_open) {
|
||||||
/* 目录覆盖层:点条目跳转,点空白关闭 */
|
/* 目录覆盖层:TXT 章节(复用 rd_book 章节数据)或 rdsrc 目录;点条目跳转,点空白关闭 */
|
||||||
int n = rd_book_toc_n(g_book);
|
int nchap = 0;
|
||||||
|
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
|
||||||
|
int n = (nchap > 0) ? nchap : rd_book_toc_n(g_book);
|
||||||
if (n > 0 && x >= 0 && y >= g_dir_top) {
|
if (n > 0 && x >= 0 && y >= g_dir_top) {
|
||||||
int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
|
int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
|
||||||
int idx = (y - g_dir_top) / row;
|
int idx = (y - g_dir_top) / row;
|
||||||
if (idx < n && idx >= 0) {
|
if (idx < n && idx >= 0) {
|
||||||
char t[96]; int sg; int32_t cch;
|
if (nchap > 0) {
|
||||||
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
|
int64_t co = 0;
|
||||||
if (g_book && sg >= 0) {
|
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) == 0) {
|
||||||
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
|
int sp = rd_book_page_for_char(g_book, co);
|
||||||
g_dir_open = 0; g_menu_open = 0;
|
if (sp >= 0) {
|
||||||
snprintf(g_chapter, sizeof g_chapter, "%s", t);
|
rd_book_set_page(g_book, sp);
|
||||||
reader_refill_page();
|
g_dir_open = 0; g_menu_open = 0;
|
||||||
reader_redraw();
|
g_chap_cache_idx = -1;
|
||||||
reader_reflow_persist();
|
reader_refill_page();
|
||||||
return;
|
reader_redraw();
|
||||||
|
reader_reflow_persist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char t[96]; int sg; int32_t cch;
|
||||||
|
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
|
||||||
|
if (g_book && sg >= 0) {
|
||||||
|
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
|
||||||
|
g_dir_open = 0; g_menu_open = 0;
|
||||||
|
snprintf(g_chapter, sizeof g_chapter, "%s", t);
|
||||||
|
reader_refill_page();
|
||||||
|
reader_redraw();
|
||||||
|
reader_reflow_persist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,14 +1293,18 @@ void reader_on_tap(int x, int y)
|
|||||||
reader_reflow_persist();
|
reader_reflow_persist();
|
||||||
meow_view_page_pop();
|
meow_view_page_pop();
|
||||||
return;
|
return;
|
||||||
case 1: /* 目录 */
|
case 1: /* 目录(TXT 章节 → 章节列表;无章节显示「本书无章节」) */
|
||||||
if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
|
g_dir_open = 1; g_menu_open = 0; reader_redraw();
|
||||||
|
return;
|
||||||
|
case 2: /* 跳转(章节/文件·段模式) */
|
||||||
|
if (rd_book_chapter_n(g_book) > 0 || rd_book_seg_n(g_book) > 0) { reader_jump_open(); }
|
||||||
|
else if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
|
||||||
else { g_menu_open = 0; reader_redraw(); }
|
else { g_menu_open = 0; reader_redraw(); }
|
||||||
return;
|
return;
|
||||||
case 2: /* 界面(阅读排版设置) */
|
case 3: /* 界面(阅读排版设置) */
|
||||||
g_ui_open = 1; g_menu_open = 0; reader_redraw();
|
g_ui_open = 1; g_menu_open = 0; reader_redraw();
|
||||||
return;
|
return;
|
||||||
case 3: /* 设置 → App 设置页 */
|
case 4: /* 设置 → App 设置页 */
|
||||||
reader_reflow_persist();
|
reader_reflow_persist();
|
||||||
meow_view_page_push(ACT_SETTINGS);
|
meow_view_page_push(ACT_SETTINGS);
|
||||||
return;
|
return;
|
||||||
@@ -901,6 +1333,7 @@ void reader_on_swipe(int dir, int dx, int dy)
|
|||||||
* 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */
|
* 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */
|
||||||
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
|
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
|
||||||
if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; }
|
if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; }
|
||||||
|
if (g_jump_open) { g_jump_open = 0; reader_redraw(); return; }
|
||||||
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
|
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
|
||||||
if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) {
|
if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) {
|
||||||
reader_reflow_persist();
|
reader_reflow_persist();
|
||||||
@@ -913,7 +1346,7 @@ void reader_on_touch_down(int x, int y) { reader_scrub_begin(x, y); }
|
|||||||
void reader_on_touch_move(int x, int y)
|
void reader_on_touch_move(int x, int y)
|
||||||
{
|
{
|
||||||
(void)x;
|
(void)x;
|
||||||
if (g_scrub && g_book) reader_scrub_preview(scrub_page_for_y(y));
|
if (g_scrub && g_book) { g_scrub_moved = 1; reader_scrub_preview(scrub_page_for_y(y)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
|
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
|
||||||
@@ -932,9 +1365,12 @@ void reader_on_tick(uint64_t now)
|
|||||||
if (g_volpage_pending) {
|
if (g_volpage_pending) {
|
||||||
int dir = g_volpage_pending > 0 ? 1 : -1;
|
int dir = g_volpage_pending > 0 ? 1 : -1;
|
||||||
g_volpage_pending = 0;
|
g_volpage_pending = 0;
|
||||||
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open)
|
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
|
||||||
reader_turn_animated(dir);
|
reader_turn_animated(dir);
|
||||||
}
|
}
|
||||||
|
/* Phase 2C:空闲预取下一段(距段界 ≤2% 时提前解码进窗口,翻段界零突发) */
|
||||||
|
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
|
||||||
|
rd_book_prefetch(g_book);
|
||||||
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
|
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
|
||||||
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
|
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
|
||||||
g_prog_last_save_ms = now;
|
g_prog_last_save_ms = now;
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ static meow_widget *g_appbar;
|
|||||||
static meow_widget *g_list;
|
static meow_widget *g_list;
|
||||||
static int g_active;
|
static int g_active;
|
||||||
|
|
||||||
/* 行项枚举(顺序 = 列表顺序) */
|
/* 行项枚举(顺序 = 列表顺序)。ITEM_BASE = 固定的全局项数;重解析本书章节仅在
|
||||||
enum { ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR, ITEM_COUNT };
|
* 已打开一本「支持章节解析」(TXT+seg 模式)的书时追加(见 settings_on_enter)。 */
|
||||||
|
enum {
|
||||||
|
ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR,
|
||||||
|
ITEM_REPARSE, ITEM_COUNT
|
||||||
|
};
|
||||||
|
#define ITEM_BASE ITEM_REPARSE /* 7:不含重解析的固定项数 */
|
||||||
|
static int g_nitems = ITEM_BASE;
|
||||||
static const char *const g_labels[ITEM_COUNT] = {
|
static const char *const g_labels[ITEM_COUNT] = {
|
||||||
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条"
|
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条", "重解析本书章节"
|
||||||
};
|
};
|
||||||
static void settings_redraw(void);
|
static void settings_redraw(void);
|
||||||
|
|
||||||
@@ -61,6 +67,7 @@ static void item_value(int item, char *out, int outsz)
|
|||||||
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
|
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
|
||||||
case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); 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_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;
|
default: snprintf(out, (size_t)outsz, "-"); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,15 +76,15 @@ static void item_value(int item, char *out, int outsz)
|
|||||||
static int settings_src(int idx, meow_list_item *out, void *ctx)
|
static int settings_src(int idx, meow_list_item *out, void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (idx < 0 || idx >= ITEM_COUNT) return 0;
|
if (idx < 0 || idx >= g_nitems || idx >= ITEM_COUNT) return 0;
|
||||||
static char valbuf[16];
|
static char valbuf[48];
|
||||||
item_value(idx, valbuf, sizeof valbuf);
|
item_value(idx, valbuf, sizeof valbuf);
|
||||||
out->icon_kind = MEOW_LIST_ICON_NONE;
|
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||||
out->title = g_labels[idx];
|
out->title = g_labels[idx];
|
||||||
out->subtitle = valbuf;
|
out->subtitle = valbuf;
|
||||||
out->right = MEOW_LIST_RIGHT_ARROW;
|
out->right = MEOW_LIST_RIGHT_ARROW;
|
||||||
out->on = 0;
|
out->on = 0;
|
||||||
out->enabled = 1;
|
out->enabled = (idx == ITEM_REPARSE) ? reader_has_chapter_book() : 1;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +154,7 @@ static void apply_choice(int item, int sel)
|
|||||||
/* 打开一个 SELECT 对话框并应用。 */
|
/* 打开一个 SELECT 对话框并应用。 */
|
||||||
static void settings_open_choice(int item)
|
static void settings_open_choice(int item)
|
||||||
{
|
{
|
||||||
|
if (item == ITEM_REPARSE) return; /* 重解析走 settings_reparse(非 SELECT) */
|
||||||
const rdconf *c = rdconf_get();
|
const rdconf *c = rdconf_get();
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
const char *title = g_labels[item];
|
const char *title = g_labels[item];
|
||||||
@@ -191,6 +199,25 @@ static void settings_redraw(void)
|
|||||||
meow_ui_flush();
|
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 生命周期 */
|
/* view 生命周期 */
|
||||||
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
|
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
|
||||||
static void settings_on_enter(meow_view *v, void *ctx)
|
static void settings_on_enter(meow_view *v, void *ctx)
|
||||||
@@ -203,7 +230,9 @@ static void settings_on_enter(meow_view *v, void *ctx)
|
|||||||
if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
|
if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
|
||||||
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
|
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
|
||||||
int ly = sbh + SET_APPBAR_H + 8;
|
int ly = sbh + SET_APPBAR_H + 8;
|
||||||
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, ITEM_COUNT,
|
/* 仅当打开的书「支持章节解析」(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);
|
settings_src, NULL, MEOW_LIST_SEL_SINGLE);
|
||||||
if (g_list) {
|
if (g_list) {
|
||||||
meow_listview_set_container(g_list, 1);
|
meow_listview_set_container(g_list, 1);
|
||||||
@@ -240,7 +269,8 @@ int settings_on_tap(int x, int y)
|
|||||||
}
|
}
|
||||||
if (g_list) {
|
if (g_list) {
|
||||||
int idx = meow_listview_tap_at(g_list, x, y);
|
int idx = meow_listview_tap_at(g_list, x, y);
|
||||||
if (idx >= 0 && idx < ITEM_COUNT) {
|
if (idx >= 0 && idx < g_nitems && idx < ITEM_COUNT) {
|
||||||
|
if (idx == ITEM_REPARSE) { settings_reparse(); return 1; }
|
||||||
settings_open_choice(idx);
|
settings_open_choice(idx);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ echo "==> host seg test (R3.2)"
|
|||||||
$HOST_CC -O2 -Wall -Wextra -Icore test/seg_smoke.c core/seg.c core/rdenc.c -o test/seg_smoke_host -lm
|
$HOST_CC -O2 -Wall -Wextra -Icore test/seg_smoke.c core/seg.c core/rdenc.c -o test/seg_smoke_host -lm
|
||||||
./test/seg_smoke_host
|
./test/seg_smoke_host
|
||||||
|
|
||||||
|
echo "==> host chapter test (R3.3)"
|
||||||
|
$HOST_CC -O2 -Wall -Wextra -Icore test/chapter_smoke.c core/rdchapter.c core/seg.c core/rdenc.c -o test/chapter_smoke_host -lm
|
||||||
|
./test/chapter_smoke_host
|
||||||
|
|
||||||
echo "==> host loaders test"
|
echo "==> host loaders test"
|
||||||
HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
|
HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
|
||||||
for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
|
for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
|
||||||
@@ -119,21 +123,21 @@ timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-
|
|||||||
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
|
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
|
||||||
echo "==> mipsel book smoke (TXT)"
|
echo "==> mipsel book smoke (TXT)"
|
||||||
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
|
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
|
||||||
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
|
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
|
||||||
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
|
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
|
||||||
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
|
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
|
||||||
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
|
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
|
||||||
|
|
||||||
echo "==> mipsel formats smoke"
|
echo "==> mipsel formats smoke"
|
||||||
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
|
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
|
||||||
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
|
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
|
||||||
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
|
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
|
||||||
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel
|
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel
|
||||||
timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
|
timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
|
||||||
|
|
||||||
# ---------- 7) reader 二进制(mipsel 静态) ----------
|
# ---------- 7) reader 二进制(mipsel 静态) ----------
|
||||||
echo "==> reader binary"
|
echo "==> reader binary"
|
||||||
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
|
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
|
||||||
ENGINE_L="engine/rdft.c"
|
ENGINE_L="engine/rdft.c"
|
||||||
APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
|
||||||
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
|
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
|
||||||
|
|||||||
@@ -0,0 +1,528 @@
|
|||||||
|
/* core/rdchapter.c — TXT 章节规范解析器实现(见 core/rdchapter.h)。 */
|
||||||
|
#include "rdchapter.h"
|
||||||
|
#include "seg.h"
|
||||||
|
#include "rdtxt.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ---------------- 基础字符工具 ---------------- */
|
||||||
|
static int ascii_digit_val(uint32_t cp)
|
||||||
|
{
|
||||||
|
return (cp >= '0' && cp <= '9') ? (int)(cp - '0') : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CJK / 全角宽度 = 2,其余 = 1(近似;不含零宽组合符)。 */
|
||||||
|
static int cp_width(uint32_t cp)
|
||||||
|
{
|
||||||
|
if (cp == 0x3000 || cp == 0x3001 || cp == 0x3002) return 2;
|
||||||
|
if ((cp >= 0x2E80 && cp <= 0x303E) || (cp >= 0x3040 && cp <= 0x30FF) ||
|
||||||
|
(cp >= 0x3100 && cp <= 0x318F) || (cp >= 0x3400 && cp <= 0x4DBF) ||
|
||||||
|
(cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) ||
|
||||||
|
(cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) ||
|
||||||
|
(cp >= 0xFE30 && cp <= 0xFE4F) || (cp >= 0xFF00 && cp <= 0xFF60) ||
|
||||||
|
(cp >= 0xFFE0 && cp <= 0xFFE6)) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
static int disp_width(const uint32_t *cps, int s, int e)
|
||||||
|
{
|
||||||
|
int w = 0;
|
||||||
|
for (int i = s; i < e; i++) w += cp_width(cps[i]);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 章节标记字符集:章 回 节 卷 集 部 篇。 */
|
||||||
|
static int is_marker(uint32_t cp)
|
||||||
|
{
|
||||||
|
return cp == 0x7AE0 || cp == 0x56DE || cp == 0x8282 ||
|
||||||
|
cp == 0x5377 || cp == 0x96C6 || cp == 0x90E8 || cp == 0x7BC7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卷 部 集(卷/集类章节前缀)。 */
|
||||||
|
static int is_volmark(uint32_t cp)
|
||||||
|
{
|
||||||
|
return cp == 0x5377 /* 卷 */ || cp == 0x90E8 /* 部 */ || cp == 0x96C6 /* 集 */;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中文数字字符(含位权)。 */
|
||||||
|
static int is_cn_num_char(uint32_t cp)
|
||||||
|
{
|
||||||
|
return cp == 0x96F6 /* 零 */ || cp == 0x4E00 /* 一 */ || cp == 0x4E8C /* 二 */ ||
|
||||||
|
cp == 0x4E09 /* 三 */ || cp == 0x56DB /* 四 */ || cp == 0x4E94 /* 五 */ ||
|
||||||
|
cp == 0x516D /* 六 */ || cp == 0x4E03 /* 七 */ || cp == 0x516B /* 八 */ ||
|
||||||
|
cp == 0x4E5D /* 九 */ || cp == 0x5341 /* 十 */ || cp == 0x767E /* 百 */ ||
|
||||||
|
cp == 0x5343 /* 千 */ || cp == 0x4E07 /* 万 */ || cp == 0x4E24 /* 两 */;
|
||||||
|
}
|
||||||
|
static int cn_digit_val(uint32_t cp)
|
||||||
|
{
|
||||||
|
switch (cp) {
|
||||||
|
case 0x96F6: return 0; /* 零 */
|
||||||
|
case 0x4E00: return 1; /* 一 */
|
||||||
|
case 0x4E8C: return 2; /* 二 */
|
||||||
|
case 0x4E09: return 3; /* 三 */
|
||||||
|
case 0x56DB: return 4; /* 四 */
|
||||||
|
case 0x4E94: return 5; /* 五 */
|
||||||
|
case 0x516D: return 6; /* 六 */
|
||||||
|
case 0x4E03: return 7; /* 七 */
|
||||||
|
case 0x516B: return 8; /* 八 */
|
||||||
|
case 0x4E5D: return 9; /* 九 */
|
||||||
|
case 0x4E24: return 2; /* 两 */
|
||||||
|
default: return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static int cn_weight_val(uint32_t cp)
|
||||||
|
{
|
||||||
|
switch (cp) {
|
||||||
|
case 0x5341: return 10; /* 十 */
|
||||||
|
case 0x767E: return 100; /* 百 */
|
||||||
|
case 0x5343: return 1000; /* 千 */
|
||||||
|
case 0x4E07: return 10000; /* 万 */
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中文数字子串 [s,e) → 阿拉伯;非法/空返回 -1。实现:digit 累积 + 位权叠加。 */
|
||||||
|
static int cn_num_from_cps(const uint32_t *cps, int s, int e)
|
||||||
|
{
|
||||||
|
long total = 0, cur = 0, digit = 0;
|
||||||
|
int any = 0;
|
||||||
|
for (int i = s; i < e; i++) {
|
||||||
|
uint32_t cp = cps[i];
|
||||||
|
int dv = cn_digit_val(cp);
|
||||||
|
if (dv >= 0) { digit = dv; any = 1; continue; }
|
||||||
|
int w = cn_weight_val(cp);
|
||||||
|
if (w > 0) {
|
||||||
|
cur += (digit ? digit : 1) * (long)w;
|
||||||
|
digit = 0; any = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return -1; /* 非中文数字字符 */
|
||||||
|
}
|
||||||
|
total += cur + digit;
|
||||||
|
return any ? (int)total : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内正文标点计数:句号/逗号/顿号/分号/问号/感叹号/省略号 + ASCII . , ; ! ?
|
||||||
|
* 返回计数(区分「极少」:≤1 视为标题,>1 视为正文)。 */
|
||||||
|
static int sent_punct_count(const uint32_t *cps, int s, int e)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int i = s; i < e; i++) {
|
||||||
|
uint32_t cp = cps[i];
|
||||||
|
if (cp == 0x3002 /* 。 */ || cp == 0xFF0C /* , */ || cp == 0x3001 /* 、 */ ||
|
||||||
|
cp == 0xFF1B /* ; */ || cp == 0xFF1F /* ? */ || cp == 0xFF01 /* ! */ ||
|
||||||
|
cp == 0x2026 /* … */ || cp == '.' || cp == ',' || cp == ';' ||
|
||||||
|
cp == '!' || cp == '?') n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cps[s..] 是否以 utf8 字面量为前缀(逐码点比较)。 */
|
||||||
|
static int cps_prefix(const uint32_t *cps, int s, int e, const uint8_t *utf8)
|
||||||
|
{
|
||||||
|
int i = s;
|
||||||
|
const uint8_t *p = utf8;
|
||||||
|
while (*p && i < e) {
|
||||||
|
uint32_t cp;
|
||||||
|
uint8_t b0 = *p;
|
||||||
|
int n;
|
||||||
|
if (b0 < 0x80) { cp = b0; n = 1; }
|
||||||
|
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | (p[1] & 0x3F); n = 2; }
|
||||||
|
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(p[1] & 0x3F) << 6) | (p[2] & 0x3F); n = 3; }
|
||||||
|
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(p[1] & 0x3F) << 12) | ((uint32_t)(p[2] & 0x3F) << 6) | (p[3] & 0x3F); n = 4; }
|
||||||
|
if (cps[i] != cp) return 0;
|
||||||
|
i++; p += n;
|
||||||
|
}
|
||||||
|
return (*p == 0) && (i > s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ASCII 大小写无关前缀比较(用于 "chapter")。 */
|
||||||
|
static int cps_prefix_ci(const uint32_t *cps, int s, int e, const char *ascii)
|
||||||
|
{
|
||||||
|
int i = s;
|
||||||
|
for (const char *p = ascii; *p; p++) {
|
||||||
|
if (i >= e) return 0;
|
||||||
|
uint32_t cp = cps[i++];
|
||||||
|
uint32_t lo = (cp >= 'A' && cp <= 'Z') ? cp + 32 : cp;
|
||||||
|
char ch = *p;
|
||||||
|
if (lo != (uint32_t)ch) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 罗马数字 → 阿拉伯(ivxl);解析失败 -1。 */
|
||||||
|
static int roman_to_int(const uint32_t *cps, int s, int e, int *end)
|
||||||
|
{
|
||||||
|
struct { uint32_t cp; int v; } tbl[] = {
|
||||||
|
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 }, { 'l', 50 }, { 'c', 100 },
|
||||||
|
};
|
||||||
|
int vals[32], k = 0;
|
||||||
|
int i = s;
|
||||||
|
while (i < e && k < 32) {
|
||||||
|
uint32_t cp = cps[i];
|
||||||
|
if (cp >= 'A' && cp <= 'Z') cp += 32; /* 小写化 */
|
||||||
|
int v = 0;
|
||||||
|
for (int t = 0; t < 5; t++) if (tbl[t].cp == cp) { v = tbl[t].v; break; }
|
||||||
|
if (v == 0) break;
|
||||||
|
vals[k++] = v; i++;
|
||||||
|
}
|
||||||
|
if (k == 0) return -1;
|
||||||
|
int total = 0, prev = 0;
|
||||||
|
for (int j = k - 1; j >= 0; j--) {
|
||||||
|
int v = vals[j];
|
||||||
|
if (v < prev) total -= v; else total += v;
|
||||||
|
prev = v;
|
||||||
|
}
|
||||||
|
*end = i;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 英文数字词(Chapter one / twenty-one / two hundred fifty ...)→ 阿拉伯 ---- */
|
||||||
|
static int cps_ascii_word_eq_ci(const uint32_t *cps, int s, int e, const char *w)
|
||||||
|
{
|
||||||
|
int len = (int)strlen(w);
|
||||||
|
if (e - s != len) return 0;
|
||||||
|
for (int k = 0; k < len; k++) {
|
||||||
|
uint32_t c = cps[s + k];
|
||||||
|
if (c >= 'A' && c <= 'Z') c += 32;
|
||||||
|
if (c != (uint32_t)(unsigned char)w[k]) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 英文数字词一 → 值;不识别返回 -1。覆盖 one..nineteen、twenty..ninety、hundred、thousand。 */
|
||||||
|
static int en_word_val(const uint32_t *cps, int s, int e)
|
||||||
|
{
|
||||||
|
static const struct { const char *w; int v; } tbl[] = {
|
||||||
|
{ "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 }, { "five", 5 },
|
||||||
|
{ "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 }, { "ten", 10 },
|
||||||
|
{ "eleven", 11 }, { "twelve", 12 }, { "thirteen", 13 }, { "fourteen", 14 },
|
||||||
|
{ "fifteen", 15 }, { "sixteen", 16 }, { "seventeen", 17 }, { "eighteen", 18 },
|
||||||
|
{ "nineteen", 19 }, { "twenty", 20 }, { "thirty", 30 }, { "forty", 40 },
|
||||||
|
{ "fifty", 50 }, { "sixty", 60 }, { "seventy", 70 }, { "eighty", 80 },
|
||||||
|
{ "ninety", 90 }, { "hundred", 100 }, { "thousand", 1000 },
|
||||||
|
};
|
||||||
|
for (int t = 0; t < (int)(sizeof tbl / sizeof tbl[0]); t++)
|
||||||
|
if (cps_ascii_word_eq_ci(cps, s, e, tbl[t].w)) return tbl[t].v;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析一段英文数字词(one / twenty-one / two hundred fifty ...)。*end=末字符位置。
|
||||||
|
* 失败(无词/未知词/越界/数值越界)返回 -1。 */
|
||||||
|
static int word_num_from_cps(const uint32_t *cps, int s, int e, int *end)
|
||||||
|
{
|
||||||
|
long total = 0, cur = 0;
|
||||||
|
int i = s, words = 0, last_end = s;
|
||||||
|
while (i < e) {
|
||||||
|
while (i < e && (cps[i] == ' ' || cps[i] == '-' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
|
||||||
|
int ws = i;
|
||||||
|
while (i < e && ((cps[i] >= 'a' && cps[i] <= 'z') || (cps[i] >= 'A' && cps[i] <= 'Z'))) i++;
|
||||||
|
if (i == ws) break;
|
||||||
|
int v = en_word_val(cps, ws, i);
|
||||||
|
if (v < 0) return -1;
|
||||||
|
if (v == 100 || v == 1000) { long base = (cur == 0) ? 1 : cur; total += base * (long)v; cur = 0; }
|
||||||
|
else cur += (long)v;
|
||||||
|
words++;
|
||||||
|
last_end = i;
|
||||||
|
}
|
||||||
|
if (words == 0) return -1;
|
||||||
|
long n = total + cur;
|
||||||
|
if (n <= 0 || n > 100000) return -1;
|
||||||
|
*end = last_end;
|
||||||
|
return (int)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 把 cps[s..e) 转成 UTF-8 到 out(有界)。 */
|
||||||
|
static void cps_to_utf8(const uint32_t *cps, int s, int e, char *out, int outsz)
|
||||||
|
{
|
||||||
|
int o = 0;
|
||||||
|
for (int i = s; i < e; i++) {
|
||||||
|
uint8_t b[4];
|
||||||
|
int n = rd_cp_to_utf8(cps[i], b);
|
||||||
|
if (o + n + 1 > outsz) break;
|
||||||
|
for (int k = 0; k < n; k++) out[o++] = (char)b[k];
|
||||||
|
}
|
||||||
|
out[o] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊前缀章节(楔子/序章/序言/引子/前言/序幕/尾声/后记/番外/外传/终章/大结局/正文)。 */
|
||||||
|
static const uint8_t *const g_special[] = {
|
||||||
|
(const uint8_t *)"\xE6\xA5\x94\xE5\xAD\x90", /* 楔子 */
|
||||||
|
(const uint8_t *)"\xE5\xBA\x8F\xE7\xAB\xA0", /* 序章 */
|
||||||
|
(const uint8_t *)"\xE5\xBA\x8F\xE8\xA8\x80", /* 序言 */
|
||||||
|
(const uint8_t *)"\xE5\xBC\x95\xE5\xAD\x90", /* 引子 */
|
||||||
|
(const uint8_t *)"\xE5\x89\x8D\xE8\xA8\x80", /* 前言 */
|
||||||
|
(const uint8_t *)"\xE5\xBA\x8F\xE5\xB9\x95", /* 序幕 */
|
||||||
|
(const uint8_t *)"\xE5\xB0\xBE\xE5\xA3\xB0", /* 尾声 */
|
||||||
|
(const uint8_t *)"\xE5\x90\x8E\xE8\xAE\xB0", /* 后记 */
|
||||||
|
(const uint8_t *)"\xE7\x95\xAA\xE5\xA4\x96", /* 番外 */
|
||||||
|
(const uint8_t *)"\xE5\xA4\x96\xE4\xBC\xA0", /* 外传 */
|
||||||
|
(const uint8_t *)"\xE7\xBB\x88\xE7\xAB\xA0", /* 终章 */
|
||||||
|
(const uint8_t *)"\xE5\xA4\xA7\xE7\xBB\x93\xE5\xB1\x80", /* 大结局 */
|
||||||
|
(const uint8_t *)"\xE6\xAD\xA3\xE6\x96\x87", /* 正文 */
|
||||||
|
};
|
||||||
|
#define G_SPECIAL_N ((int)(sizeof g_special / sizeof g_special[0]))
|
||||||
|
|
||||||
|
/* 判断一行(cps[ls..le),ls=首个非空白字符)是否为章节标题行。
|
||||||
|
* 命中则填 title(完整行)与 *num;返回 1,否则 0。 */
|
||||||
|
static int chap_match_line(const uint32_t *cps, int ls, int le, char *title, int outsz, int *num)
|
||||||
|
{
|
||||||
|
if (ls >= le) return 0;
|
||||||
|
if (disp_width(cps, ls, le) > RDCHAP_DISP_WMAX) return 0;
|
||||||
|
|
||||||
|
/* A/B:`第` + 阿拉伯/中文数字 + 章回节卷集部篇 */
|
||||||
|
if (cps[ls] == 0x7B2C /* 第 */) {
|
||||||
|
int i = ls + 1;
|
||||||
|
int numv = -1;
|
||||||
|
int dstart = i;
|
||||||
|
while (i < le && ascii_digit_val(cps[i]) >= 0) i++;
|
||||||
|
if (i > dstart) {
|
||||||
|
if (i - dstart > 4) return 0;
|
||||||
|
numv = 0;
|
||||||
|
for (int k = dstart; k < i; k++) numv = numv * 10 + ascii_digit_val(cps[k]);
|
||||||
|
} else {
|
||||||
|
int cstart = i;
|
||||||
|
while (i < le && is_cn_num_char(cps[i])) i++;
|
||||||
|
if (i == cstart) return 0;
|
||||||
|
if (i - cstart > 8) return 0;
|
||||||
|
numv = cn_num_from_cps(cps, cstart, i);
|
||||||
|
if (numv < 0) return 0;
|
||||||
|
}
|
||||||
|
if (i >= le || !is_marker(cps[i])) return 0;
|
||||||
|
i++; /* 消费标记 */
|
||||||
|
if (sent_punct_count(cps, i, le) > 1) return 0;
|
||||||
|
cps_to_utf8(cps, ls, le, title, outsz);
|
||||||
|
*num = numv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* C:阿拉伯数字 + 可选分隔符(、.. 空格 全角空格) + 章回节卷集部篇 */
|
||||||
|
if (ascii_digit_val(cps[ls]) >= 0) {
|
||||||
|
int i = ls, numv = 0, cnt = 0;
|
||||||
|
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
|
||||||
|
if (cnt < 1 || cnt > 4) return 0;
|
||||||
|
if (i < le && (cps[i] == 0x3001 || cps[i] == '.' || cps[i] == ' ' || cps[i] == 0x3000)) i++;
|
||||||
|
if (i >= le || !is_marker(cps[i])) return 0;
|
||||||
|
i++;
|
||||||
|
if (sent_punct_count(cps, i, le) > 1) return 0;
|
||||||
|
cps_to_utf8(cps, ls, le, title, outsz);
|
||||||
|
*num = numv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* F:卷/部/集 + 空白 + 中文数字(卷一 / 卷 一 / 集三) */
|
||||||
|
if (is_volmark(cps[ls])) {
|
||||||
|
int i = ls + 1;
|
||||||
|
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000)) i++;
|
||||||
|
int cstart = i;
|
||||||
|
while (i < le && is_cn_num_char(cps[i])) i++;
|
||||||
|
if (i == cstart) return 0;
|
||||||
|
int numv = cn_num_from_cps(cps, cstart, i);
|
||||||
|
if (numv < 0) return 0;
|
||||||
|
if (sent_punct_count(cps, i, le) > 1) return 0;
|
||||||
|
cps_to_utf8(cps, ls, le, title, outsz);
|
||||||
|
*num = numv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* E:Chapter N(N=阿拉伯 / 罗马数字 / 英文数字词 one..hundred) */
|
||||||
|
if (cps_prefix_ci(cps, ls, le, "chapter")) {
|
||||||
|
int i = ls + 7;
|
||||||
|
if (i >= le) return 0;
|
||||||
|
if (!(cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) return 0; /* 必须空白:ChapterX 不认 */
|
||||||
|
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
|
||||||
|
int numv;
|
||||||
|
if (ascii_digit_val(cps[i]) >= 0) {
|
||||||
|
numv = 0; int cnt = 0;
|
||||||
|
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
|
||||||
|
if (cnt < 1 || cnt > 5) return 0;
|
||||||
|
} else {
|
||||||
|
int endi = i;
|
||||||
|
numv = roman_to_int(cps, i, le, &endi);
|
||||||
|
if (numv < 0) { endi = i; numv = word_num_from_cps(cps, i, le, &endi); }
|
||||||
|
if (numv < 0) return 0;
|
||||||
|
i = endi;
|
||||||
|
}
|
||||||
|
if (sent_punct_count(cps, i, le) > 1) return 0;
|
||||||
|
cps_to_utf8(cps, ls, le, title, outsz);
|
||||||
|
*num = numv;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* D:特殊前缀(楔子/序章/…)——要求整行无正文标点(避免「正文内容。」/「番外正文。」误判) */
|
||||||
|
for (int t = 0; t < G_SPECIAL_N; t++) {
|
||||||
|
if (cps_prefix(cps, ls, le, g_special[t])) {
|
||||||
|
int i = ls;
|
||||||
|
for (const uint8_t *p = g_special[t]; *p; ) { /* 消费前缀码点 */
|
||||||
|
uint8_t b0 = *p;
|
||||||
|
int n = (b0 < 0x80) ? 1 : ((b0 & 0xE0) == 0xC0) ? 2 : ((b0 & 0xF0) == 0xE0) ? 3 : 4;
|
||||||
|
i++; p += n;
|
||||||
|
}
|
||||||
|
if (sent_punct_count(cps, i, le) > 0) return 0;
|
||||||
|
cps_to_utf8(cps, ls, le, title, outsz);
|
||||||
|
*num = -1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c)
|
||||||
|
{
|
||||||
|
if (!c) return -1;
|
||||||
|
rdchapter_init(c);
|
||||||
|
if (!seg_dir || !seg_dir[0]) return 0; /* 无段目录 → 无章节 */
|
||||||
|
|
||||||
|
int nsegs = rdseg_n(seg_dir);
|
||||||
|
if (nsegs < 0) return -1;
|
||||||
|
if (nsegs == 0) { c->nsegs = 0; return 0; }
|
||||||
|
|
||||||
|
c->seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
|
||||||
|
if (!c->seg_char_start) return -6;
|
||||||
|
c->nsegs = nsegs;
|
||||||
|
|
||||||
|
int ent_cap = 0;
|
||||||
|
int64_t global = 0;
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
uint8_t *raw = NULL; size_t rawcap = 0;
|
||||||
|
uint8_t *utf8 = NULL; size_t utf8cap = 0;
|
||||||
|
uint32_t *cps = NULL;
|
||||||
|
|
||||||
|
for (int seg = 0; seg < nsegs && !err; seg++) {
|
||||||
|
size_t rawlen = 0;
|
||||||
|
if (rdseg_rawlen(seg_dir, seg, &rawlen) != 0) { err = 1; break; }
|
||||||
|
if (rawlen > rawcap) {
|
||||||
|
uint8_t *t = (uint8_t *)realloc(raw, rawlen ? rawlen : 1);
|
||||||
|
if (!t) { err = -6; break; }
|
||||||
|
raw = t; rawcap = rawlen;
|
||||||
|
}
|
||||||
|
size_t rn = 0;
|
||||||
|
if (rdseg_load(seg_dir, seg, raw, rawlen, &rn) != 0) { err = 2; break; }
|
||||||
|
size_t ulen = 0;
|
||||||
|
size_t need = rawlen * 3 / 2 + 16;
|
||||||
|
if (need < RDTXT_SEG_UTF8_MAX + 8) need = RDTXT_SEG_UTF8_MAX + 8;
|
||||||
|
if (need > utf8cap) {
|
||||||
|
uint8_t *t = (uint8_t *)realloc(utf8, need);
|
||||||
|
if (!t) { err = -6; break; }
|
||||||
|
utf8 = t; utf8cap = need;
|
||||||
|
}
|
||||||
|
if (rd_to_utf8(enc, raw, rn, utf8, utf8cap, &ulen) != RD_ENC_OK) { err = 3; break; }
|
||||||
|
size_t n = 0;
|
||||||
|
/* 解码为码点失败(多为内存不足)→ 视作扫描失败:不落盘坏结果(见 rdchapter_scan 头部注释)。 */
|
||||||
|
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) { cps = NULL; err = -6; break; }
|
||||||
|
c->seg_char_start[seg] = global; /* 本段全局字符起点 */
|
||||||
|
|
||||||
|
if (cps && n > 0) {
|
||||||
|
int ls = 0;
|
||||||
|
while (ls < (int)n) {
|
||||||
|
int le = ls;
|
||||||
|
while (le < (int)n && cps[le] != '\n') le++;
|
||||||
|
int indent = 0, first = ls;
|
||||||
|
while (first < le && (cps[first] == ' ' || cps[first] == '\t' || cps[first] == 0x3000)) {
|
||||||
|
indent++; first++;
|
||||||
|
}
|
||||||
|
int te = le;
|
||||||
|
while (te > first && (cps[te - 1] == ' ' || cps[te - 1] == '\t' ||
|
||||||
|
cps[te - 1] == '\r' || cps[te - 1] == 0x3000)) te--;
|
||||||
|
if (indent <= RDCHAP_INDENT_MAX && first < te) {
|
||||||
|
char tbuf[RDCHAP_TITLE_MAX];
|
||||||
|
int num = -1;
|
||||||
|
if (chap_match_line(cps, first, te, tbuf, sizeof tbuf, &num) && c->n < RDCHAP_MAX_ENTRIES) {
|
||||||
|
if (ent_cap <= c->n) {
|
||||||
|
int nc = ent_cap ? ent_cap * 2 : 64;
|
||||||
|
if (nc > RDCHAP_MAX_ENTRIES) nc = RDCHAP_MAX_ENTRIES;
|
||||||
|
rdchapter_entry *ne = (rdchapter_entry *)realloc(c->entries, (size_t)nc * sizeof(rdchapter_entry));
|
||||||
|
if (!ne) { err = -6; break; }
|
||||||
|
c->entries = ne; ent_cap = nc;
|
||||||
|
}
|
||||||
|
rdchapter_entry *en = &c->entries[c->n];
|
||||||
|
memset(en, 0, sizeof *en);
|
||||||
|
snprintf(en->title, sizeof en->title, "%s", tbuf);
|
||||||
|
en->char_off = global + first;
|
||||||
|
en->seg_idx = seg;
|
||||||
|
en->seg_char = (int32_t)first;
|
||||||
|
en->number = num;
|
||||||
|
c->n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (le >= (int)n) break;
|
||||||
|
ls = le + 1;
|
||||||
|
}
|
||||||
|
free(cps); cps = NULL;
|
||||||
|
}
|
||||||
|
global += (int64_t)n;
|
||||||
|
}
|
||||||
|
free(cps);
|
||||||
|
free(raw);
|
||||||
|
free(utf8);
|
||||||
|
|
||||||
|
if (err) { rdchapter_free(c); return err; }
|
||||||
|
|
||||||
|
/* 段数恒定:seg_char_start[nsegs] = 全书字符数(尾哨兵)。 */
|
||||||
|
if (global > (int64_t)0x7fffffffLL) { rdchapter_free(c); return -7; } /* 安全上限 */
|
||||||
|
c->seg_char_start[nsegs] = global;
|
||||||
|
|
||||||
|
/* 卷首/前置:第一章节前有内容 → 作为 entries[0](title=文件名或"卷首",号=-1)。 */
|
||||||
|
if (c->n > 0 && c->entries[0].char_off > 0) {
|
||||||
|
if (c->n >= RDCHAP_MAX_ENTRIES) { /* 已满则跳过卷首(理论) */ }
|
||||||
|
else {
|
||||||
|
memmove(&c->entries[1], &c->entries[0], (size_t)c->n * sizeof(rdchapter_entry));
|
||||||
|
rdchapter_entry *pre = &c->entries[0];
|
||||||
|
memset(pre, 0, sizeof *pre);
|
||||||
|
const char *bn = (book_basename && book_basename[0]) ? book_basename : "卷首";
|
||||||
|
strncpy(pre->title, bn, sizeof pre->title - 1);
|
||||||
|
pre->title[sizeof pre->title - 1] = 0;
|
||||||
|
pre->char_off = 0;
|
||||||
|
pre->seg_idx = 0;
|
||||||
|
pre->seg_char = 0;
|
||||||
|
pre->number = -1;
|
||||||
|
c->n++;
|
||||||
|
c->has_pre = 1;
|
||||||
|
snprintf(c->pre_title, sizeof c->pre_title, "%s", bn);
|
||||||
|
}
|
||||||
|
} else if (book_basename && book_basename[0]) {
|
||||||
|
snprintf(c->pre_title, sizeof c->pre_title, "%s", book_basename);
|
||||||
|
} else {
|
||||||
|
snprintf(c->pre_title, sizeof c->pre_title, "%s", "卷首");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rdchapter_init(rdchapter *c)
|
||||||
|
{
|
||||||
|
if (!c) return;
|
||||||
|
memset(c, 0, sizeof *c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rdchapter_free(rdchapter *c)
|
||||||
|
{
|
||||||
|
if (!c) return;
|
||||||
|
free(c->entries); c->entries = NULL;
|
||||||
|
free(c->seg_char_start); c->seg_char_start = NULL;
|
||||||
|
c->n = c->nsegs = c->has_pre = 0;
|
||||||
|
c->pre_title[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdchapter_cn_num(const char *s, int len)
|
||||||
|
{
|
||||||
|
if (!s || len <= 0) return -1;
|
||||||
|
uint32_t local[16];
|
||||||
|
int n = 0;
|
||||||
|
int i = 0;
|
||||||
|
while (i < len && n < 16) {
|
||||||
|
uint8_t b0 = (uint8_t)s[i];
|
||||||
|
uint32_t cp; int nb;
|
||||||
|
if (b0 < 0x80) { cp = b0; nb = 1; }
|
||||||
|
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | ((uint8_t)s[i + 1] & 0x3F); nb = 2; }
|
||||||
|
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 6) | ((uint8_t)s[i + 2] & 0x3F); nb = 3; }
|
||||||
|
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 12) | ((uint32_t)((uint8_t)s[i + 2] & 0x3F) << 6) | ((uint8_t)s[i + 3] & 0x3F); nb = 4; }
|
||||||
|
if (!is_cn_num_char(cp)) break;
|
||||||
|
local[n++] = cp;
|
||||||
|
i += nb;
|
||||||
|
}
|
||||||
|
if (n == 0) return -1;
|
||||||
|
return cn_num_from_cps(local, 0, n);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* core/rdchapter.h — TXT 章节规范解析器:扫描已切好的 512KB 码位对齐段文件(core/seg.c),
|
||||||
|
* 识别章节标题行,输出章节列表(每项:标题、全书字符偏移、所在段/段内字符偏移、阿拉伯章号)。
|
||||||
|
*
|
||||||
|
* 设计约束:
|
||||||
|
* - 逐段流式(`rdseg_load` 读一个段 → `rd_to_utf8` 解码 → `rd_utf8_to_cp` 数码点 → 行扫),
|
||||||
|
* 不整书驻留;单段内存与 book.c `seg_ensure_built` 同量级(~512KB 原文 + ~3 倍 UTF-8)。
|
||||||
|
* - 章节标题行约束:行首缩进 ≤4 半角/全角空格;标题显示宽度 ≤ RDCHAP_DISP_WMAX(CJK=2/ASCII=1);
|
||||||
|
* 行内正文标点 ≤1(句号/逗号/省略号等,容「极少」);行首锚定 + 短 + 无成段标点 → 防误判
|
||||||
|
* (「他说第三章内容」不居行首即不命中;目录页交错等已知限制,见报告)。
|
||||||
|
* - 中文数字 → 阿拉伯(`rdchapter_cn_num`,用于排序/显示);按 char_off 升序输出。
|
||||||
|
* - 开头书名/简介区(第一章节之前的内容)归为「卷首/前置」,作为条目[0](title=文件名或"卷首")。
|
||||||
|
*
|
||||||
|
* 纯 libc + rdenc + seg,无 SDK/FreeType;宿主可测。
|
||||||
|
*/
|
||||||
|
#ifndef RDCHAPTER_H
|
||||||
|
#define RDCHAPTER_H
|
||||||
|
|
||||||
|
#include "rdenc.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define RDCHAP_TITLE_MAX 160 /* 单个标题字符(UTF-8 字节)上限 */
|
||||||
|
#define RDCHAP_DISP_WMAX 40 /* 标题最大显示宽度(CJK=2 / ASCII=1) */
|
||||||
|
#define RDCHAP_INDENT_MAX 4 /* 行首缩进上限(空格/全角空格/制表,按字符计) */
|
||||||
|
#define RDCHAP_MAX_ENTRIES 200000 /* 章节数上限(防失控内存) */
|
||||||
|
|
||||||
|
typedef struct rdchapter_entry {
|
||||||
|
char title[RDCHAP_TITLE_MAX]; /* 完整标题行(如「第一章 风雨」「楔子」),UTF-8 */
|
||||||
|
int64_t char_off; /* 全书字符偏移(段 0 起,跨段累计;标题首字符) */
|
||||||
|
int32_t seg_idx; /* 所在 512KB 段下标 */
|
||||||
|
int32_t seg_char; /* 段内字符偏移(标题首字符) */
|
||||||
|
int32_t number; /* 阿拉伯章号;-1=无号(如楔子/卷首/序章) */
|
||||||
|
} rdchapter_entry;
|
||||||
|
|
||||||
|
typedef struct rdchapter {
|
||||||
|
rdchapter_entry *entries;
|
||||||
|
int n; /* 章节数;0=本书无章节(扫描过) */
|
||||||
|
int64_t *seg_char_start;/* seg_char_start[seg]=该段在全书中的字符起点;[nsegs]=全书字符数 */
|
||||||
|
int nsegs; /* 段数(= seg_char_start 数组长 -1) */
|
||||||
|
char pre_title[RDCHAP_TITLE_MAX]; /* 卷首/前置标题(文件名或"卷首") */
|
||||||
|
int has_pre; /* 1=存在卷首/前置(第一章节前有内容) */
|
||||||
|
} rdchapter;
|
||||||
|
|
||||||
|
void rdchapter_init(rdchapter *c);
|
||||||
|
void rdchapter_free(rdchapter *c);
|
||||||
|
|
||||||
|
/* 扫描 seg_dir 下全部 512KB 段文件,识别章节标题。book_basename 用于卷首标题(可为 NULL/"卷首")。
|
||||||
|
* 0=成功(c->n=章节数,可为 0);负=错误(seg_dir 无效/无段/解码失败等)。 */
|
||||||
|
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c);
|
||||||
|
|
||||||
|
/* 中文数字 → 阿拉伯(「二十三」→23,「一百零一」→101,「两」→2)。s/len 为 UTF-8;非法返回 -1。 */
|
||||||
|
int rdchapter_cn_num(const char *s, int len);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* RDCHAPTER_H */
|
||||||
@@ -312,7 +312,12 @@ int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
|
|||||||
size_t cnt = 0, i = 0;
|
size_t cnt = 0, i = 0;
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
uint8_t b = utf8[i];
|
uint8_t b = utf8[i];
|
||||||
if (b < 0x80) { arr[cnt++] = b; i++; continue; }
|
if (b < 0x80) {
|
||||||
|
/* \r\n → \n、孤 \r → \n:TXT 行尾 \r 若进排版,在无 U+000D 映射的字体里落成
|
||||||
|
* .notdef(豆腐/占宽);归一化后章节/页表/进度锚同处一个无 \r 码点空间。 */
|
||||||
|
if (b == '\r') { arr[cnt++] = '\n'; i++; if (i < len && utf8[i] == '\n') i++; continue; }
|
||||||
|
arr[cnt++] = b; i++; continue;
|
||||||
|
}
|
||||||
unsigned q, lo, hi;
|
unsigned q, lo, hi;
|
||||||
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
|
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
|
||||||
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
|
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
|
||||||
|
|||||||
@@ -226,6 +226,12 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
|
|||||||
npages++;
|
npages++;
|
||||||
page_start = np_start;
|
page_start = np_start;
|
||||||
cur_y = 0;
|
cur_y = 0;
|
||||||
|
/* 孤行上推修正:被推行(l-1)成为新页首行 → 按页首行重算其 y 并计入页内高度,
|
||||||
|
* 否则它带着上一页的 y 残值(渲染错位)且页填充少算一行高(页底溢出)。 */
|
||||||
|
if (np_start == l - 1) {
|
||||||
|
out->lines[l - 1].y = 0;
|
||||||
|
cur_y = lh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
|
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
|
||||||
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
|
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
|
||||||
@@ -245,6 +251,63 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
|
|||||||
return RD_LAYOUT_OK;
|
return RD_LAYOUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
|
||||||
|
rd_line *lines, int max_lines, int *nlines, int32_t *next_start)
|
||||||
|
{
|
||||||
|
if (!in || !in->chars || in->n < 0 || !lines || !nlines || !next_start || max_lines <= 0)
|
||||||
|
return RD_LAYOUT_ERR_INVAL;
|
||||||
|
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if (start > in->n) start = in->n;
|
||||||
|
|
||||||
|
int lh = (in->fs * in->line_scale) / 100;
|
||||||
|
if (lh < 1) lh = 1;
|
||||||
|
int pspacing = lh / 2;
|
||||||
|
if (pspacing < 0) pspacing = 0;
|
||||||
|
|
||||||
|
int nl = 0;
|
||||||
|
int y = 0; /* 页内累计(含段距;规则同 rd_layout 阶段 B 单页填充) */
|
||||||
|
int32_t ps = start;
|
||||||
|
int first_para = 1; /* 首段:首行状态由 mid_para 给定(续行不缩进/非段首) */
|
||||||
|
|
||||||
|
while (ps < in->n) {
|
||||||
|
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
|
||||||
|
if (ps >= in->n) break;
|
||||||
|
int32_t pe = ps;
|
||||||
|
while (pe < in->n && in->chars[pe] != '\n') pe++;
|
||||||
|
|
||||||
|
int32_t pos = ps;
|
||||||
|
int first_line = first_para ? !mid_para : 1;
|
||||||
|
first_para = 0;
|
||||||
|
while (pos < pe) {
|
||||||
|
rd_line ln;
|
||||||
|
para_break_line(in, pe, &pos, first_line, &ln);
|
||||||
|
first_line = 0;
|
||||||
|
/* 页填充判定(同 rd_layout 阶段 B:页内首行不加段距;溢出即页满) */
|
||||||
|
int extra = (ln.para_start && nl > 0) ? pspacing : 0;
|
||||||
|
if (y + lh + extra > in->page_h && nl > 0) {
|
||||||
|
/* 孤行守卫:页末行是段首行且段未结束 → 该行随段入下一页(同 rd_layout) */
|
||||||
|
int cut = nl;
|
||||||
|
if (lines[nl - 1].para_start && !lines[nl - 1].para_end) cut = nl - 1;
|
||||||
|
*nlines = cut;
|
||||||
|
*next_start = (cut < nl) ? lines[cut].s : ln.s;
|
||||||
|
return RD_LAYOUT_OK;
|
||||||
|
}
|
||||||
|
if (nl >= max_lines) { *nlines = nl; *next_start = ln.s; return RD_LAYOUT_ERR_OVER; }
|
||||||
|
ln.y = y + extra;
|
||||||
|
y += lh + extra;
|
||||||
|
lines[nl++] = ln;
|
||||||
|
if (ln.para_end) break;
|
||||||
|
}
|
||||||
|
ps = pos;
|
||||||
|
while (ps < in->n && in->chars[ps] == '\n') ps++;
|
||||||
|
if (pos >= pe && ps >= in->n) break;
|
||||||
|
}
|
||||||
|
*nlines = nl;
|
||||||
|
*next_start = in->n;
|
||||||
|
return RD_LAYOUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
|
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
|
||||||
{
|
{
|
||||||
if (!out || out->npages <= 0) return 0;
|
if (!out || out->npages <= 0) return 0;
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ typedef struct rd_layout_out {
|
|||||||
|
|
||||||
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
|
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
|
||||||
|
|
||||||
|
/* 页级微排版(Phase 2C):从 chars[start](须为行首:start==0 或 chars[start-1]=='\n')起只排一页,
|
||||||
|
* 填满 page_h 即停。mid_para=1 表示 start 行是段中续行(不缩进、非段首行);=0 表示段首行
|
||||||
|
* (缩进 + para_start,与 rd_layout 首行规则一致)。输出 lines[0..*nlines)(内容区坐标,
|
||||||
|
* 与 rd_layout 同断行/分页/孤行守卫规则);*next_start = 下一页首字符偏移(== in->n 表示文末)。
|
||||||
|
* 一致性:同一 (chars, start, mid_para) 下产出 == rd_layout 全排版中该页的行区间(逐行 diff 可证)。 */
|
||||||
|
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
|
||||||
|
rd_line *lines, int max_lines, int *nlines, int32_t *next_start);
|
||||||
|
|
||||||
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
|
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
|
||||||
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
|
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
|
||||||
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
|
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# pack_reader_mpk.sh — 把 os.meow.reader 打成 .mpk(tar.gz),并把 LGPL 合规材料并入包内。
|
# pack_reader_mpk.sh — 把 os.meow.reader 打成 .mpk(tar.gz),并把 LGPL 合规材料并入包内。
|
||||||
# .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt}
|
# .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
|
||||||
# (不含 data/cache;符合 APPS.md §3 与 mips-repo/mpk/README.md 的 .mpk 规范)
|
# (不含 data/cache;符合 APPS.md §3 与 mips-repo/mpk/README.md 的 .mpk 规范)
|
||||||
#
|
#
|
||||||
# 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文,
|
# 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文 + FreeType(FTL) 全文,
|
||||||
# 满足 LGPL-3.0 §4(a)(显著通知)与 §4(b)(随 Combined Work 附 GPL + LGPL 两份文本)。
|
# 满足 LGPL-3.0 §4(a)(显著通知)、§4(b)(随 Combined Work 附 GPL + LGPL 两份文本)与
|
||||||
|
# FTL 署名条款(随二进制分发附许可文本与版权声明)。
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# ./build_reader.sh # 先编出 reader 二进制(本脚本不重编)
|
# ./build_reader.sh # 先编出 reader 二进制(本脚本不重编)
|
||||||
@@ -31,6 +32,7 @@ cp icon.png "$STAGE/" 2>/dev/null || true
|
|||||||
cp reader "$STAGE/"
|
cp reader "$STAGE/"
|
||||||
mkdir -p "$STAGE/resources/licenses"
|
mkdir -p "$STAGE/resources/licenses"
|
||||||
cp resources/licenses/NOTICE resources/licenses/GPL-3.0.txt resources/licenses/LGPL-3.0.txt \
|
cp resources/licenses/NOTICE resources/licenses/GPL-3.0.txt resources/licenses/LGPL-3.0.txt \
|
||||||
|
resources/licenses/FTL.txt \
|
||||||
"$STAGE/resources/licenses/"
|
"$STAGE/resources/licenses/"
|
||||||
|
|
||||||
# busybox tar + gzip(与 launcher/nyai 同管道语义);-cz 直接出 .
|
# busybox tar + gzip(与 launcher/nyai 同管道语义);-cz 直接出 .
|
||||||
@@ -40,6 +42,6 @@ echo "== packed $MPK =="
|
|||||||
tar tzvf "$MPK"
|
tar tzvf "$MPK"
|
||||||
echo "md5: $(md5sum "$MPK" | cut -d' ' -f1)"
|
echo "md5: $(md5sum "$MPK" | cut -d' ' -f1)"
|
||||||
echo
|
echo
|
||||||
echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt"
|
echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt + FTL.txt"
|
||||||
echo "未签名。正式分发前 run:"
|
echo "未签名。正式分发前 run:"
|
||||||
echo " ../meow_sign/meow_sign sign $MPK \"\$HOME/Project/R3ProII/.keys/signing.key\""
|
echo " ../meow_sign/meow_sign sign $MPK \"\$HOME/Project/R3ProII/.keys/signing.key\""
|
||||||
@@ -34,6 +34,11 @@ void reader_on_touch_move(int x, int y);
|
|||||||
* 返回 0=成功(书已打开、页位图已备、可 push);非 0=失败(未能打开,调用方不 push)。 */
|
* 返回 0=成功(书已打开、页位图已备、可 push);非 0=失败(未能打开,调用方不 push)。 */
|
||||||
int reader_open(const char *path);
|
int reader_open(const char *path);
|
||||||
|
|
||||||
|
/* R3.4 章节重解析(app/settings.c 调用):是否打开了支持章节解析的书(TXT+seg 模式)。
|
||||||
|
* reader_reparse_chapters 返回 n=章节数 / 0=无章节(成功)/ -1=不可用 / -2=扫描失败(未落盘)。 */
|
||||||
|
int reader_has_chapter_book(void);
|
||||||
|
int reader_reparse_chapters(void);
|
||||||
|
|
||||||
/* ---- 全局设置页(app/settings.c,C)---- */
|
/* ---- 全局设置页(app/settings.c,C)---- */
|
||||||
meow_view *settings_view_create(void);
|
meow_view *settings_view_create(void);
|
||||||
int settings_is_active(void);
|
int settings_is_active(void);
|
||||||
|
|||||||
@@ -20,10 +20,82 @@ static void write_txt(const char *path)
|
|||||||
"它包含汉字、标点,以及一些 English words 混合排版。"
|
"它包含汉字、标点,以及一些 English words 混合排版。"
|
||||||
"断行、避头尾与分页行为都要正确。\n", i + 1);
|
"断行、避头尾与分页行为都要正确。\n", i + 1);
|
||||||
}
|
}
|
||||||
fprintf(f, "\nChapter two: the quick brown fox jumps over the lazy dog.\n");
|
fprintf(f, "\n第二章 尾声\n这是一段章尾正文,用于验证章节跳转与章内翻页。\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Phase 2A/2C:跨段拼接页连续性(硬用例)。
|
||||||
|
* 构造恰好 512KB 的段 0(5242×100B + 88B,末行为短段落),段 1 开头放唯一标记 SEG1MARKER;
|
||||||
|
* 建段 1 触发滚动拼接后,段界页(段 0 末页)应包含段 1 开头的文本(拼接页含两段文本),
|
||||||
|
* 且跨段往返翻页后锚 (seg,ch) 稳定。 ---- */
|
||||||
|
typedef struct { char *buf; size_t len, cap; } linecol;
|
||||||
|
static void linecol_add(int x, int y, const char *utf8, int len, void *ctx)
|
||||||
|
{
|
||||||
|
(void)x; (void)y;
|
||||||
|
linecol *lc = (linecol *)ctx;
|
||||||
|
if (lc->len + (size_t)len + 2 > lc->cap) {
|
||||||
|
size_t nc = lc->cap ? lc->cap * 2 : 4096;
|
||||||
|
while (nc < lc->len + (size_t)len + 2) nc *= 2;
|
||||||
|
char *nb = (char *)realloc(lc->buf, nc);
|
||||||
|
if (!nb) return;
|
||||||
|
lc->buf = nb; lc->cap = nc;
|
||||||
|
}
|
||||||
|
memcpy(lc->buf + lc->len, utf8, (size_t)len);
|
||||||
|
lc->len += (size_t)len;
|
||||||
|
lc->buf[lc->len++] = '\n';
|
||||||
|
lc->buf[lc->len] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void phase2a_stitch(const char *font)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/rdbook_stitch.txt";
|
||||||
|
FILE *f = fopen(txt, "wb");
|
||||||
|
char line[100];
|
||||||
|
memset(line, 'a', 99); line[99] = '\n';
|
||||||
|
for (int i = 0; i < 5242; i++) fwrite(line, 1, 100, f); /* 524200 B */
|
||||||
|
for (int i = 0; i < 88; i++) fputc('a', f); /* 共 524288 B = 512KB 码位边界 */
|
||||||
|
fprintf(f, "SEG1MARKER 段一开头唯一标记,证明拼接页跨段包含两段文本。\n");
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
fprintf(f, "段一正文第%d行:滚动拼接后段界无短页,页表与进度锚稳定。\n", i + 1);
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
rd_book_set_seg_base("/tmp/meow_seg_stitch");
|
||||||
|
rd_book_set_index_dir("/tmp/meow_seg_stitch_index");
|
||||||
|
rd_book *b = NULL;
|
||||||
|
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
|
||||||
|
CHECK(rc == 0, "stitch open");
|
||||||
|
if (rc != 0) { remove(txt); return; }
|
||||||
|
int nseg = rd_book_seg_n(b);
|
||||||
|
CHECK(nseg >= 2, "stitch multi-seg");
|
||||||
|
if (nseg >= 2) {
|
||||||
|
int p1 = rd_book_seg_page(b, 1); /* 建段 1 → 触发段 0 末页拼接 */
|
||||||
|
CHECK(p1 >= 0, "stitch seg1 built");
|
||||||
|
int ps = rd_book_seg_page(b, 0);
|
||||||
|
int pc = rd_book_seg_page_count(b, 0);
|
||||||
|
CHECK(ps == 0 && pc > 0, "stitch seg0 pages");
|
||||||
|
int bnd = ps + pc - 1;
|
||||||
|
linecol lc = { NULL, 0, 0 };
|
||||||
|
int rr = rd_book_render_text(b, bnd, linecol_add, &lc);
|
||||||
|
CHECK(rr == 0, "stitch boundary render_text ok");
|
||||||
|
CHECK(lc.buf && strstr(lc.buf, "SEG1MARKER") != NULL, "stitch boundary contains seg1 text");
|
||||||
|
free(lc.buf);
|
||||||
|
/* 跨段往返:段界页翻前/后,锚 (seg,ch) 不变 */
|
||||||
|
int as = -1; int32_t ach = -1;
|
||||||
|
rd_book_set_page(b, bnd);
|
||||||
|
rd_book_anchor(b, &as, &ach);
|
||||||
|
CHECK(as == 0 && ach >= 0, "stitch boundary anchor seg0");
|
||||||
|
int cur = rd_book_current(b);
|
||||||
|
int fwd = rd_book_turn(b, 1);
|
||||||
|
int back = rd_book_turn(b, -1);
|
||||||
|
CHECK(fwd == cur + 1 && back == cur, "stitch boundary turn roundtrip");
|
||||||
|
int as2 = -1; int32_t ach2 = -1;
|
||||||
|
rd_book_anchor(b, &as2, &ach2);
|
||||||
|
CHECK(as2 == as && ach2 == ach, "stitch boundary anchor stable");
|
||||||
|
}
|
||||||
|
rd_book_close(b);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
||||||
@@ -82,8 +154,40 @@ int main(int argc, char **argv)
|
|||||||
int np3 = rd_book_set_size(b, 20, 150);
|
int np3 = rd_book_set_size(b, 20, 150);
|
||||||
CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok");
|
CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok");
|
||||||
|
|
||||||
|
/* R3.3 章节:解析 + 章节→页映射 + 章内翻页单调(页号递增不跳变) */
|
||||||
|
int nch = rd_book_chapter_n(b);
|
||||||
|
printf(" chapters=%d\n", nch);
|
||||||
|
CHECK(nch == 2, "book has 2 chapters");
|
||||||
|
if (nch == 2) {
|
||||||
|
char t0[160], t1[160]; int n0 = -1, n1 = -1; int64_t c0 = -1, c1 = -1;
|
||||||
|
CHECK(rd_book_chapter(b, 0, t0, sizeof t0, &c0, NULL, NULL, &n0) == 0, "chapter0 get");
|
||||||
|
CHECK(n0 == 1, "chapter0 number 1");
|
||||||
|
CHECK(c0 == 0, "chapter0 char_off 0");
|
||||||
|
int p0 = rd_book_page_for_char(b, c0);
|
||||||
|
CHECK(p0 == 0 && p0 < rd_book_pages(b), "chapter0 start page 0");
|
||||||
|
CHECK(rd_book_chapter(b, 1, t1, sizeof t1, &c1, NULL, NULL, &n1) == 0, "chapter1 get");
|
||||||
|
CHECK(n1 == 2, "chapter1 number 2");
|
||||||
|
int p1 = rd_book_page_for_char(b, c1);
|
||||||
|
CHECK(p1 >= p0 && p1 < rd_book_pages(b), "chapter1 start page in range");
|
||||||
|
printf(" chapter1('%s') at page %d (of %d)\n", t1, p1, rd_book_pages(b));
|
||||||
|
/* 跳到章二首页,验证章内翻页 +1 单调(不越过章界仍递增) */
|
||||||
|
int landed = rd_book_set_page(b, p1);
|
||||||
|
CHECK(landed == p1, "jump lands on chapter1 start");
|
||||||
|
int cur1 = rd_book_current(b);
|
||||||
|
int nxt1 = rd_book_turn(b, 1);
|
||||||
|
CHECK((nxt1 == cur1 + 1) || (nxt1 == cur1), "chapter-internal turn +1 (clamp-safe)");
|
||||||
|
/* R3.4 手动重解析(清 .chap 缓存 + 重扫)应幂等返回同章数,且章节仍可用 */
|
||||||
|
int nrep = rd_book_reparse_chapters(b);
|
||||||
|
CHECK(nrep == nch, "reparse returns same chapter count");
|
||||||
|
CHECK(rd_book_chapter_n(b) == nch, "reparse keeps chapters");
|
||||||
|
}
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
rd_book_close(b);
|
rd_book_close(b);
|
||||||
|
|
||||||
|
/* Phase 2A/2C:跨段拼接页连续性 + 段界锚稳定(另开一本 >512KB 的多段书) */
|
||||||
|
phase2a_stitch(font);
|
||||||
|
|
||||||
printf("book_smoke: %s\n", fails ? "FAIL" : "PASS");
|
printf("book_smoke: %s\n", fails ? "FAIL" : "PASS");
|
||||||
return fails ? 1 : 0;
|
return fails ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# test/build_turn_repro.sh — cross-compile turn_repro for mipsel (mirrors book_smoke_mipsel link line).
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
ROOT="$HOME/Project/R3ProII"
|
||||||
|
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||||
|
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||||
|
VENDOR="$PWD/vendor"
|
||||||
|
OBJ="$VENDOR/build"
|
||||||
|
FT_BUILD="$VENDOR/freetype-build"
|
||||||
|
FT_SRC="$VENDOR/freetype-src"
|
||||||
|
FT_INCS="-I$FT_BUILD -I$FT_BUILD/include -I$FT_SRC/include"
|
||||||
|
COREINC="-Icore -Ivendor/miniz -Ivendor/stb -Ivendor/libmobi/src -Ivendor/zlib"
|
||||||
|
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC \
|
||||||
|
test/turn_repro.c \
|
||||||
|
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
|
||||||
|
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
|
||||||
|
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" \
|
||||||
|
-o test/turn_repro_mipsel
|
||||||
|
echo "build OK -> test/turn_repro_mipsel"
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/* test/chapter_smoke.c — os.meow.reader 章节规范解析器宿主单测(纯 C,无 SDK/Freetype)。
|
||||||
|
*
|
||||||
|
* 覆盖(按需求 5 样本 + 跨段 + 中文数字转换):
|
||||||
|
* C1 标准中文数字章(第一章/第二章/第十一章 → 1/2/11,char_off 单调)
|
||||||
|
* C2 阿拉伯章(第1章/第2章/第3章 → 1/2/3)
|
||||||
|
* C3 楔子/序章/尾声/番外(无号,number==-1,char_off 单调)
|
||||||
|
* C4 无章节纯文本(n==0,seg_char_start 仍完整)
|
||||||
|
* C5 开头长书名+简介再进第一章(卷首识别 as entries[0]、章号、防误判正文行 )
|
||||||
|
* C6 跨段:>512KB 书,章落在后段(seg_idx>=1,char_off 跨段单调)
|
||||||
|
* C7 中文数字转换:十一/二十三/一百零一/一千零一/两
|
||||||
|
*
|
||||||
|
* 跑法:build_reader.sh 内 `test/chapter_smoke_host`;本文件只依赖 rdchapter + seg + rdenc。
|
||||||
|
*/
|
||||||
|
#include "rdchapter.h"
|
||||||
|
#include "seg.h"
|
||||||
|
#include "rdenc.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
#define CHECK(cond, msg) do { if (cond) { g_pass++; } else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } } while (0)
|
||||||
|
|
||||||
|
static char g_dir[600];
|
||||||
|
|
||||||
|
static void write_file(const char *path, const char *data)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
fwrite(data, 1, strlen(data), f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 建段目录 + 切段,返回段数(<0=错误)。 */
|
||||||
|
static int seg_for(const char *path, const char *base)
|
||||||
|
{
|
||||||
|
rdseg_set_base(base);
|
||||||
|
char dir[600];
|
||||||
|
if (rdseg_dir_for(path, dir, sizeof dir) != 0) return -1;
|
||||||
|
strcpy(g_dir, dir);
|
||||||
|
return rdseg_build(path, RD_ENC_UTF8, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int scan_book(rdchapter *c, const char *basename)
|
||||||
|
{
|
||||||
|
return rdchapter_scan(g_dir, RD_ENC_UTF8, basename, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c1(void)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/ch_s1.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"第一章 风起\n"
|
||||||
|
"这是第一章的正文内容。\n"
|
||||||
|
"第二章 云涌\n"
|
||||||
|
"这是第二章的正文内容。\n"
|
||||||
|
"第十一章 破晓\n"
|
||||||
|
"这是第十一章的正文内容。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base1") >= 1, "c1 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s1") == 0, "c1 scan");
|
||||||
|
CHECK(c.n == 3, "c1 n==3");
|
||||||
|
if (c.n == 3) {
|
||||||
|
CHECK(c.entries[0].number == 1, "c1 #1");
|
||||||
|
CHECK(c.entries[1].number == 2, "c1 #2");
|
||||||
|
CHECK(c.entries[2].number == 11, "c1 #11");
|
||||||
|
CHECK(c.entries[0].char_off == 0, "c1 first at 0 (no preface)");
|
||||||
|
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||||
|
c.entries[1].char_off < c.entries[2].char_off, "c1 char_off monotonic");
|
||||||
|
CHECK(c.entries[0].seg_idx == 0 && c.entries[0].seg_char == 0, "c1 seg0 char0");
|
||||||
|
CHECK(strcmp(c.entries[0].title, "第一章 风起") == 0, "c1 title");
|
||||||
|
CHECK(c.has_pre == 0, "c1 no preface");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c2(void)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/ch_s2.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"第1章 起源\n"
|
||||||
|
"正文A。\n"
|
||||||
|
"第2章 发展\n"
|
||||||
|
"正文B。\n"
|
||||||
|
"第3章 终局\n"
|
||||||
|
"正文C。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base2") >= 1, "c2 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s2") == 0, "c2 scan");
|
||||||
|
CHECK(c.n == 3, "c2 n==3");
|
||||||
|
if (c.n == 3) {
|
||||||
|
CHECK(c.entries[0].number == 1, "c2 #1");
|
||||||
|
CHECK(c.entries[1].number == 2, "c2 #2");
|
||||||
|
CHECK(c.entries[2].number == 3, "c2 #3");
|
||||||
|
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||||
|
c.entries[1].char_off < c.entries[2].char_off, "c2 char_off monotonic");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c3(void)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/ch_s3.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"楔子\n"
|
||||||
|
"一段开篇。\n"
|
||||||
|
"第一章 正传\n"
|
||||||
|
"正文内容。\n"
|
||||||
|
"尾声\n"
|
||||||
|
"后记文字。\n"
|
||||||
|
"番外 归途\n"
|
||||||
|
"番外正文。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base3") >= 1, "c3 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s3") == 0, "c3 scan");
|
||||||
|
CHECK(c.n == 4, "c3 n==4");
|
||||||
|
if (c.n == 4) {
|
||||||
|
CHECK(c.entries[0].number == -1, "c3 楔子 number -1");
|
||||||
|
CHECK(strcmp(c.entries[0].title, "楔子") == 0, "c3 楔子 title");
|
||||||
|
CHECK(c.entries[1].number == 1, "c3 第一章 number 1");
|
||||||
|
CHECK(c.entries[2].number == -1, "c3 尾声 number -1");
|
||||||
|
CHECK(c.entries[3].number == -1, "c3 番外 number -1");
|
||||||
|
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||||
|
c.entries[1].char_off < c.entries[2].char_off &&
|
||||||
|
c.entries[2].char_off < c.entries[3].char_off, "c3 char_off monotonic");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c4(void)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/ch_s4.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"这是一个没有章节标题的长篇小说文本。\n"
|
||||||
|
"它只有连续不断的段落,没有任何章回节的标识。\n"
|
||||||
|
"所有的文字都是纯粹的故事内容。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base4") >= 1, "c4 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s4") == 0, "c4 scan");
|
||||||
|
CHECK(c.n == 0, "c4 no chapters");
|
||||||
|
CHECK(c.nsegs == 1, "c4 nsegs==1");
|
||||||
|
CHECK(c.seg_char_start && c.seg_char_start[0] == 0 && c.seg_char_start[1] > 0, "c4 seg_char_start");
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c5(void)
|
||||||
|
{
|
||||||
|
const char *txt = "/tmp/ch_s5.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"《大漠孤烟》这是一本很长的书名,作者著。\n"
|
||||||
|
"简介:这是一个故事的开头,讲述少年成长。\n"
|
||||||
|
"第一章风雨欲来,全书开场,命运由此转折。\n"
|
||||||
|
"第一章 离家\n"
|
||||||
|
"正文内容。\n"
|
||||||
|
"第二章 远行\n"
|
||||||
|
"正文内容。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base5") >= 1, "c5 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "sample5") == 0, "c5 scan");
|
||||||
|
CHECK(c.n == 3, "c5 n==3 (preface + 2 chapters)");
|
||||||
|
if (c.n == 3) {
|
||||||
|
CHECK(c.has_pre == 1, "c5 has preface");
|
||||||
|
CHECK(c.entries[0].number == -1, "c5 preface number -1");
|
||||||
|
CHECK(strcmp(c.entries[0].title, "sample5") == 0, "c5 preface title = basename");
|
||||||
|
CHECK(c.entries[0].char_off == 0, "c5 preface char_off 0");
|
||||||
|
CHECK(c.entries[1].number == 1 && c.entries[2].number == 2, "c5 chapter numbers 1/2");
|
||||||
|
CHECK(c.entries[1].char_off > 0, "c5 ch1 after preface");
|
||||||
|
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||||
|
c.entries[1].char_off < c.entries[2].char_off, "c5 monotonic");
|
||||||
|
/* 防误判:正文行「第一章风雨欲来,全书开场。」带两个正文标点 → 未被识别为章 */
|
||||||
|
CHECK(c.entries[1].seg_char > 0, "c5 decoy line not counted (ch1 seg_char>0)");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c6(void)
|
||||||
|
{
|
||||||
|
/* 跨段:写 ~600KB filler,在 ~600KB 处放一个章 → 应落在后段(seg_idx>=1) */
|
||||||
|
const char *txt = "/tmp/ch_s6.txt";
|
||||||
|
FILE *f = fopen(txt, "wb");
|
||||||
|
const char *para = "这是一段用于填充页面的中文正文,包含标点,中文English混合,反复出现,占据空间。\n";
|
||||||
|
size_t plen = strlen(para);
|
||||||
|
size_t total = 0;
|
||||||
|
while (total < 600 * 1024) {
|
||||||
|
fwrite(para, 1, plen, f); total += plen;
|
||||||
|
}
|
||||||
|
fwrite("第二百章 跨段检测\n", 1, strlen("第二百章 跨段检测\n"), f);
|
||||||
|
const char *tail = "这是跨段之后的一小段正文。\n";
|
||||||
|
fwrite(tail, 1, strlen(tail), f);
|
||||||
|
fclose(f);
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base6") >= 2, "c6 seg build >=2");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s6") == 0, "c6 scan");
|
||||||
|
/* 前置 filler(600KB 无章节)→ entries[0]=卷首;真正章节在后段 */
|
||||||
|
CHECK(c.n == 2, "c6 n==2 (preface + 1 chapter)");
|
||||||
|
if (c.n == 2) {
|
||||||
|
CHECK(c.entries[0].number == -1, "c6 preface number -1");
|
||||||
|
CHECK(c.entries[1].number == 200, "c6 number 200");
|
||||||
|
CHECK(c.entries[1].seg_idx >= 1, "c6 chapter in later segment");
|
||||||
|
CHECK(c.entries[1].seg_idx < c.nsegs, "c6 seg_idx in range");
|
||||||
|
CHECK(c.entries[1].seg_char >= 0, "c6 seg_char >=0");
|
||||||
|
CHECK(c.seg_char_start[0] == 0 &&
|
||||||
|
c.seg_char_start[0] < c.seg_char_start[1] &&
|
||||||
|
c.seg_char_start[c.nsegs - 1] < c.seg_char_start[c.nsegs], "c6 seg_char_start monotonic");
|
||||||
|
/* 跨段 char_off:章首偏移应在段 1 起点之后 */
|
||||||
|
CHECK(c.entries[1].char_off >= c.seg_char_start[c.entries[1].seg_idx], "c6 char_off within seg");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c7(void)
|
||||||
|
{
|
||||||
|
CHECK(rdchapter_cn_num("十一", 6) == 11, "c7 十一=11");
|
||||||
|
CHECK(rdchapter_cn_num("二十三", 9) == 23, "c7 二十三=23");
|
||||||
|
CHECK(rdchapter_cn_num("一百零一", 12) == 101, "c7 一百零一=101");
|
||||||
|
CHECK(rdchapter_cn_num("一千零一", 12) == 1001, "c7 一千零一=1001");
|
||||||
|
CHECK(rdchapter_cn_num("两", 3) == 2, "c7 两=2");
|
||||||
|
CHECK(rdchapter_cn_num("一百", 6) == 100, "c7 一百=100");
|
||||||
|
CHECK(rdchapter_cn_num("foobar", 6) == -1, "c7 invalid -1");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_c8(void)
|
||||||
|
{
|
||||||
|
/* R3.4 英文章节:文字数字(one/twenty-one/two hundred fifty)+ 罗马(IV)+ 拒绝无空格(ChapterX) */
|
||||||
|
const char *txt = "/tmp/ch_s8.txt";
|
||||||
|
write_file(txt,
|
||||||
|
"Chapter one\n"
|
||||||
|
"正文。\n"
|
||||||
|
"Chapter IV\n"
|
||||||
|
"正文。\n"
|
||||||
|
"Chapter twenty-one\n"
|
||||||
|
"正文。\n"
|
||||||
|
"Chapter two hundred fifty\n"
|
||||||
|
"正文。\n"
|
||||||
|
"ChapterX\n"
|
||||||
|
"正文。\n"
|
||||||
|
"Chapter twelve\n"
|
||||||
|
"正文。\n");
|
||||||
|
CHECK(seg_for(txt, "/tmp/ch_base8") >= 1, "c8 seg build");
|
||||||
|
rdchapter c; rdchapter_init(&c);
|
||||||
|
CHECK(scan_book(&c, "s8") == 0, "c8 scan");
|
||||||
|
/* ChapterX 不认(无空格);其余 5 个认 */
|
||||||
|
CHECK(c.n == 5, "c8 n==5");
|
||||||
|
if (c.n == 5) {
|
||||||
|
static const int exp[5] = { 1, 4, 21, 250, 12 };
|
||||||
|
for (int i = 0; i < 5; i++) CHECK(c.entries[i].number == exp[i], "c8 word number value");
|
||||||
|
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||||
|
c.entries[1].char_off < c.entries[2].char_off &&
|
||||||
|
c.entries[2].char_off < c.entries[3].char_off &&
|
||||||
|
c.entries[3].char_off < c.entries[4].char_off, "c8 char_off monotonic");
|
||||||
|
CHECK(strcmp(c.entries[0].title, "Chapter one") == 0, "c8 title Chapter one");
|
||||||
|
CHECK(strcmp(c.entries[3].title, "Chapter two hundred fifty") == 0, "c8 title two hundred fifty");
|
||||||
|
}
|
||||||
|
rdchapter_free(&c);
|
||||||
|
remove(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_c1();
|
||||||
|
test_c2();
|
||||||
|
test_c3();
|
||||||
|
test_c4();
|
||||||
|
test_c5();
|
||||||
|
test_c6();
|
||||||
|
test_c7();
|
||||||
|
test_c8();
|
||||||
|
printf("chapter_smoke: %d pass, %d fail\n", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -341,10 +341,121 @@ static void t13(void)
|
|||||||
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030");
|
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* t14 — TXT `\r` 归一化(READER_ADVANCED §3-1):`\r\n`→`\n`、孤 `\r`→`\n`,
|
||||||
|
* 码点流不含 0x0D(行尾 \r 在无 U+000D 映射的字体里落成 .notdef 豆腐;MiSans 虽映射到
|
||||||
|
* 零宽空字形不可见,归一化后对所有字体稳健,且章节/页表/进度锚同处无 \r 码点空间)。 */
|
||||||
|
static void t14(void)
|
||||||
|
{
|
||||||
|
const uint8_t u8[] = "ab\r\ncd\ref\n\r\ngh\r";
|
||||||
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
|
CHECK(rd_utf8_to_cp(u8, sizeof u8 - 1, &cps, &n) == RD_ENC_OK, "t14 utf8→cp ok");
|
||||||
|
const uint32_t expect[] = { 'a','b','\n','c','d','\n','e','f','\n','\n','g','h','\n' };
|
||||||
|
CHECK(n == sizeof expect / sizeof expect[0], "t14 count");
|
||||||
|
int no_cr = 1;
|
||||||
|
for (size_t i = 0; i < n; i++) if (cps[i] == 0x0D) no_cr = 0;
|
||||||
|
CHECK(no_cr, "t14 no 0x0D in cps");
|
||||||
|
CHECK(n == 13 && memcmp(cps, expect, n * sizeof(uint32_t)) == 0, "t14 normalized sequence");
|
||||||
|
free(cps);
|
||||||
|
/* GB18030 路径同样归一化(rd_to_utf8 → rd_utf8_to_cp) */
|
||||||
|
const uint8_t gbk[] = { 0xC4,0xE3,0xBA,0xC3, 0x0D,0x0A, 0xCA,0xC0,0xBD,0xE7 }; /* 你好\r\n世界 */
|
||||||
|
uint8_t out[64]; size_t olen = 0;
|
||||||
|
CHECK(rd_to_utf8(RD_ENC_GB18030, gbk, sizeof gbk, out, sizeof out, &olen) == RD_ENC_OK, "t14 gbk decode");
|
||||||
|
uint32_t *c2 = NULL; size_t n2 = 0;
|
||||||
|
CHECK(rd_utf8_to_cp(out, olen, &c2, &n2) == RD_ENC_OK, "t14 gbk→cp");
|
||||||
|
CHECK(n2 == 5 && c2[0] == 0x4F60 && c2[2] == '\n' && c2[3] == 0x4E16, "t14 gbk crlf→lf");
|
||||||
|
free(c2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* t15 — 页级微排版 vs 全排版逐行一致(Phase 2C 硬用例):
|
||||||
|
* 全排版 rd_layout → 对每页 first_char 起 rd_layout_page,断言行区间(s/e/x/y/w/para 标志)
|
||||||
|
* 与该页完全一致,且 next_start == 下一页 first_char(或文末)。含伪随机长文覆盖段界/空段/
|
||||||
|
* Kinsoku/长词/页满边界;显式构造 mid_para=1(段中续行起始)。 */
|
||||||
|
static uint32_t g_lcg = 0x12345678u;
|
||||||
|
static uint32_t lcg(void) { g_lcg = g_lcg * 1664525u + 1013904223u; return g_lcg >> 8; }
|
||||||
|
|
||||||
|
static void t15_check_text(const char *txt, int fs, int ls, int pw, int ph, const char *tag)
|
||||||
|
{
|
||||||
|
uint32_t *cps = NULL; size_t n = 0;
|
||||||
|
rd_utf8_to_cp((const uint8_t *)txt, strlen(txt), &cps, &n);
|
||||||
|
rd_line *fl = (rd_line *)malloc((n + 1) * sizeof(rd_line));
|
||||||
|
rd_page *fp = (rd_page *)malloc((n + 1) * sizeof(rd_page));
|
||||||
|
rd_line *ml = (rd_line *)malloc((size_t)(ph + 4) * sizeof(rd_line));
|
||||||
|
CHECK(fl && fp && ml, "t15 alloc");
|
||||||
|
rd_layout_in in = { cps, (int32_t)n, fs, ls, pw, ph, fs, meas_mock, NULL };
|
||||||
|
rd_layout_out out;
|
||||||
|
out.lines = fl; out.pages = fp; out.max_lines = (int)n + 1; out.max_pages = (int)n + 1;
|
||||||
|
int rc = rd_layout(&in, &out);
|
||||||
|
CHECK(rc == RD_LAYOUT_OK, tag);
|
||||||
|
if (rc != RD_LAYOUT_OK) { free(cps); free(fl); free(fp); free(ml); return; }
|
||||||
|
for (int p = 0; p < out.npages; p++) {
|
||||||
|
int32_t start = out.pages[p].first_char;
|
||||||
|
int mid = !(start == 0 || cps[start - 1] == '\n');
|
||||||
|
int mnl = -1; int32_t nxt = -1;
|
||||||
|
int mrc = rd_layout_page(&in, start, mid, ml, ph + 3, &mnl, &nxt);
|
||||||
|
CHECK(mrc == RD_LAYOUT_OK, "t15 micro rc");
|
||||||
|
CHECK(mnl == out.pages[p].nlines, "t15 page nlines equal");
|
||||||
|
if (mnl > 0 && mnl == out.pages[p].nlines) {
|
||||||
|
for (int li = 0; li < mnl; li++) {
|
||||||
|
const rd_line *a = &out.lines[out.pages[p].first_line + li];
|
||||||
|
const rd_line *b = &ml[li];
|
||||||
|
CHECK(a->s == b->s && a->e == b->e && a->x == b->x && a->y == b->y &&
|
||||||
|
a->w == b->w && a->para_start == b->para_start && a->para_end == b->para_end,
|
||||||
|
"t15 line-for-line equal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int32_t want_next = (p + 1 < out.npages) ? out.pages[p + 1].first_char : (int32_t)n;
|
||||||
|
CHECK(nxt == want_next, "t15 next_start == next page first_char");
|
||||||
|
}
|
||||||
|
free(cps); free(fl); free(fp); free(ml);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void t15(void)
|
||||||
|
{
|
||||||
|
/* 固定样本:多段/空段/中英混排/标点/长词/末尾换行 */
|
||||||
|
t15_check_text("第一章 开始\n\n这是一段正文,包含 English words 与标点。第二句继续填充,"
|
||||||
|
"直到页面溢出产生多页。\n另一段(左括号测试)右标点,簇!?混合。"
|
||||||
|
"supercalifragilisticexpialidocious longword\n\n\n末段无换行结尾",
|
||||||
|
20, 130, 440, 300, "t15 fixed layout");
|
||||||
|
/* 伪随机长文(确定性种子):40+ 页,覆盖段中页首/页满边界 */
|
||||||
|
size_t cap = 65536;
|
||||||
|
char *buf = (char *)malloc(cap);
|
||||||
|
size_t o = 0;
|
||||||
|
static const char *cjk[] = { "的","一","是","不","了","人","我","在","有","他","这","中","大",
|
||||||
|
"来","上","国","个","到","说","们","为","子","和","你","地","出" };
|
||||||
|
static const char *lat[] = { "a","test","word","x","hello","longwordish","b","qk","reader","zz" };
|
||||||
|
static const char *pnc[] = { "。",",","、","?","!","(",")"," ", " ", ".", "," };
|
||||||
|
g_lcg = 0xC0FFEE;
|
||||||
|
while (o + 16 < cap) {
|
||||||
|
uint32_t r = lcg() % 100;
|
||||||
|
const char *s;
|
||||||
|
if (r < 55) s = cjk[lcg() % (sizeof cjk / sizeof cjk[0])];
|
||||||
|
else if (r < 80) s = lat[lcg() % (sizeof lat / sizeof lat[0])];
|
||||||
|
else if (r < 92) s = pnc[lcg() % (sizeof pnc / sizeof pnc[0])];
|
||||||
|
else if (r < 97) s = "\n";
|
||||||
|
else s = "\n\n";
|
||||||
|
size_t sl = strlen(s);
|
||||||
|
memcpy(buf + o, s, sl); o += sl;
|
||||||
|
}
|
||||||
|
buf[o] = 0;
|
||||||
|
t15_check_text(buf, 20, 130, 440, 640, "t15 random layout A");
|
||||||
|
t15_check_text(buf, 17, 150, 200, 300, "t15 random layout B"); /* 窄页小字:更多断行/页 */
|
||||||
|
free(buf);
|
||||||
|
/* 段中续行起始显式用例:整段一行到底(无 \n),页首必为 mid_para=1 */
|
||||||
|
{
|
||||||
|
char *one = (char *)malloc(16384);
|
||||||
|
size_t k = 0;
|
||||||
|
for (int i = 0; i < 400; i++) { const char *w = "持续不断的正文"; size_t wl = strlen(w); memcpy(one + k, w, wl); k += wl; }
|
||||||
|
one[k] = 0;
|
||||||
|
t15_check_text(one, 20, 130, 440, 640, "t15 single-para layout");
|
||||||
|
free(one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
t1(); t2(); t3(); t4(); t5();
|
t1(); t2(); t3(); t4(); t5();
|
||||||
t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13();
|
t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13();
|
||||||
|
t14(); t15();
|
||||||
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
|
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/* test/turn_repro.c — 复现「翻页跳页」(R3.2 分段流式)。
|
||||||
|
* 模拟真机序列:open → (可选 set_enc 到 UTF-16LE) → 跳页/翻页 → 渲染校验(非空/seg 边界正确)。
|
||||||
|
* 跑于 qemu-mipsel(静态)。
|
||||||
|
* 用法:qemu-mipsel ./test/turn_repro_mipsel <font.otf> <book.txt> <dirprefix> [enc] [turns]
|
||||||
|
*/
|
||||||
|
#include "book.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int render_buf(uint16_t *buf, int W, int H, rd_book *b, int page, uint16_t bg, uint16_t fg)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < W * H; i++) buf[i] = bg;
|
||||||
|
int rc = rd_book_render(b, page, buf, W * 2, bg, fg);
|
||||||
|
if (rc != 0) return -100 + rc;
|
||||||
|
int ink = 0;
|
||||||
|
for (int i = 0; i < W * H; i++) if (buf[i] != bg) ink++;
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
||||||
|
const char *book = (argc > 2) ? argv[2] : "/tmp/rdsample/tongyi.txt";
|
||||||
|
const char *pref = (argc > 3) ? argv[3] : "/tmp/rdsample/repro";
|
||||||
|
int force_enc = (argc > 4) ? atoi(argv[4]) : 0;
|
||||||
|
int turns = (argc > 5) ? atoi(argv[5]) : 200;
|
||||||
|
char segbase[420], idxdir[420];
|
||||||
|
snprintf(segbase, sizeof segbase, "%s/segbase", pref);
|
||||||
|
snprintf(idxdir, sizeof idxdir, "%s/index", pref);
|
||||||
|
rd_book_set_seg_base(segbase);
|
||||||
|
rd_book_set_index_dir(idxdir);
|
||||||
|
|
||||||
|
int fs = 16, ls = 135, pw = 480, ph = 724, mx = 16, my = 20, top = 62;
|
||||||
|
uint16_t *buf = (uint16_t *)malloc((size_t)pw * ph * 2);
|
||||||
|
|
||||||
|
rd_book *b = NULL;
|
||||||
|
int rc = rd_book_open(&b, book, font, fs, ls, pw, ph, mx, my, top);
|
||||||
|
printf("open rc=%d enc=%s\n", rc, rc == 0 ? rd_book_encname(b) : "-");
|
||||||
|
if (rc != 0) return 1;
|
||||||
|
if (force_enc) { printf("set_enc(%d)->rc=%d\n", force_enc, rd_book_set_enc(b, (rd_enc_t)force_enc)); }
|
||||||
|
int np = rd_book_pages(b);
|
||||||
|
printf("npages=%d\n", np);
|
||||||
|
|
||||||
|
/* ① 翻页序列:跨段边界观察 cur 跳变 */
|
||||||
|
rd_book_set_page(b, 2);
|
||||||
|
int prev = rd_book_current(b), jumps = 0;
|
||||||
|
for (int i = 0; i < turns; i++) {
|
||||||
|
int nxt = rd_book_turn(b, 1);
|
||||||
|
int d = nxt - prev;
|
||||||
|
if (d != 1) { printf(" JUMP turn#%d %d->%d d=%d npages=%d\n", i, prev, nxt, d, rd_book_pages(b)); if (d > 1) jumps++; }
|
||||||
|
prev = nxt;
|
||||||
|
if (rd_book_pages(b) < 1) break;
|
||||||
|
}
|
||||||
|
printf("turn jumps>1=%d\n", jumps);
|
||||||
|
|
||||||
|
/* ② 渲染首页/第3页/中间页 校验非空 */
|
||||||
|
int targets[3] = { 0, 2, np > 3 ? np / 2 : 0 };
|
||||||
|
for (int t = 0; t < 3; t++) {
|
||||||
|
if (targets[t] >= rd_book_pages(b)) continue;
|
||||||
|
int ink = render_buf(buf, pw, ph, b, targets[t], 0xFFFF, 0x0000);
|
||||||
|
printf("render page %d -> %s (ink=%d)\n", targets[t], ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ③ set_page 到远页(触发按需补段),渲染校验 + 观察是否落到目标 */
|
||||||
|
int far = 316;
|
||||||
|
int got = rd_book_set_page(b, far);
|
||||||
|
printf("set_page(%d) -> cur=%d npages=%d\n", far, got, rd_book_pages(b));
|
||||||
|
int ink = render_buf(buf, pw, ph, b, got, 0xFFFF, 0x0000);
|
||||||
|
printf("render page %d -> %s (ink=%d)\n", got, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
|
||||||
|
|
||||||
|
/* ④ 跳到更远(超过当前 npages,强制补段到 seg 20) */
|
||||||
|
int far2 = 4000;
|
||||||
|
int got2 = rd_book_set_page(b, far2);
|
||||||
|
printf("set_page(%d) -> cur=%d npages=%d\n", far2, got2, rd_book_pages(b));
|
||||||
|
ink = render_buf(buf, pw, ph, b, got2, 0xFFFF, 0x0000);
|
||||||
|
printf("render page %d -> %s (ink=%d)\n", got2, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
rd_book_close(b);
|
||||||
|
printf("done\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# tools/check_lgpl_release.sh — 发布前合规校验闸(只读,不改任何产物)。
|
||||||
|
#
|
||||||
|
# 用法(在 reader_app 目录外也可):
|
||||||
|
# ./tools/check_lgpl_release.sh [--skip-net]
|
||||||
|
#
|
||||||
|
# 校验项(对齐 devdocs/plan/FUTURE_LIBMOBI.md §4-④ P0):
|
||||||
|
# 1) os.meow.reader.mpk 必含 resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
|
||||||
|
# 2) build_reader.sh 锁定的 libmobi commit 与 NOTICE「Locked version」一致(防锁定值漂移)
|
||||||
|
# 3) [联网] codeload tarball sha256 与 NOTICE 记录指纹一致(未记录则报告,属 P1 补强,不判失败)
|
||||||
|
# 4) [联网] 主仓 git ls-remote 公开可读(LGPL §4(d) 应用源码可达性)
|
||||||
|
#
|
||||||
|
# 退出码:0 = 通过;1 = 硬失败(缺许可证文件 / commit 漂移 / tarball 漂移 / 主仓不可达)。
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.." # 切到 reader_app 根(build_reader.sh/NOTICE/mpk 都在此)
|
||||||
|
|
||||||
|
SKIP_NET=0
|
||||||
|
[ "${1:-}" = "--skip-net" ] && SKIP_NET=1
|
||||||
|
|
||||||
|
MPK="os.meow.reader.mpk"
|
||||||
|
NOTICE="NOTICE"
|
||||||
|
BUILD="build_reader.sh"
|
||||||
|
LIC="resources/licenses"
|
||||||
|
MAIN_REPO="https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
say() { printf '%s\n' "$*"; }
|
||||||
|
hard() { printf 'FAIL %s\n' "$*"; fail=1; }
|
||||||
|
ok() { printf 'ok %s\n' "$*"; }
|
||||||
|
warn() { printf 'warn %s\n' "$*"; }
|
||||||
|
|
||||||
|
say "== 合规校验(reader_app) =="
|
||||||
|
|
||||||
|
# ---------- 1) 包内许可证文件 ----------
|
||||||
|
if [ ! -f "$MPK" ]; then
|
||||||
|
hard "找不到 $MPK —— 请先 ./build_reader.sh 再 ./pack_reader_mpk.sh"
|
||||||
|
else
|
||||||
|
for f in NOTICE GPL-3.0.txt LGPL-3.0.txt FTL.txt; do
|
||||||
|
if tar tzf "$MPK" 2>/dev/null | grep -qx "resources/licenses/$f"; then
|
||||||
|
ok "包内含 resources/licenses/$f"
|
||||||
|
else
|
||||||
|
hard "包内缺 resources/licenses/$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 2) libmobi 锁定 commit 一致性 ----------
|
||||||
|
LM_URL="$(sed -n 's/^LM_URL="\([^"]*\)"/\1/p' "$BUILD" | head -1)"
|
||||||
|
LM_COMMIT="$(printf '%s' "$LM_URL" | sed 's#.*/##')"
|
||||||
|
NOTICE_COMMIT="$(sed -n 's/^[[:space:]]*\([0-9a-f]\{40\}\).*/\1/p' "$NOTICE" | head -1)"
|
||||||
|
if [ -z "$LM_COMMIT" ]; then
|
||||||
|
hard "build_reader.sh 未解析到 LM_URL 锁定 commit"
|
||||||
|
elif [ "$LM_COMMIT" != "$NOTICE_COMMIT" ]; then
|
||||||
|
hard "libmobi 锁定 commit 漂移:build=$LM_COMMIT NOTICE=$NOTICE_COMMIT"
|
||||||
|
else
|
||||||
|
ok "libmobi 锁定 commit 一致 ($LM_COMMIT)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 3) [联网] tarball sha256 ----------
|
||||||
|
NOTICE_SHA="$(sed -n 's/^[[:space:]]*Tarball sha256[^:]*:[[:space:]]*\([0-9a-f]\{64\}\).*/\1/p' "$NOTICE" | head -1)"
|
||||||
|
LIVE_SHA=""
|
||||||
|
if [ "$SKIP_NET" = 1 ]; then
|
||||||
|
warn "跳过在线 tarball 校验 (--skip-net)"
|
||||||
|
if [ -n "$NOTICE_SHA" ]; then ok "NOTICE 已记录 tarball sha256 ($NOTICE_SHA)"; else warn "NOTICE 未记录 tarball sha256(P1 补强项)"; fi
|
||||||
|
else
|
||||||
|
if command -v curl >/dev/null 2>&1 && command -v sha256sum >/dev/null 2>&1 && [ -n "$LM_URL" ]; then
|
||||||
|
LIVE_SHA="$(curl -fsSL "$LM_URL" 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||||
|
if [ -z "$LIVE_SHA" ]; then
|
||||||
|
warn "在线取 tarball 失败(网络不可用)—— 未校验 sha256"
|
||||||
|
elif [ -n "$NOTICE_SHA" ]; then
|
||||||
|
if [ "$LIVE_SHA" = "$NOTICE_SHA" ]; then ok "tarball sha256 与 NOTICE 一致 ($LIVE_SHA)"; else hard "tarball sha256 漂移:live=$LIVE_SHA NOTICE=$NOTICE_SHA"; fi
|
||||||
|
else
|
||||||
|
say "info tarball sha256=$LIVE_SHA (NOTICE 未记录指纹,属 P1 补强项,不判失败)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "无 curl/sha256sum 或未解析 LM_URL —— 跳过在线 tarball 校验"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 4) [联网] 主仓公开可读 ----------
|
||||||
|
if [ "$SKIP_NET" = 1 ]; then
|
||||||
|
warn "跳过主仓可读性校验 (--skip-net)"
|
||||||
|
else
|
||||||
|
if command -v git >/dev/null 2>&1; then
|
||||||
|
if git ls-remote "$MAIN_REPO" HEAD >/dev/null 2>&1; then
|
||||||
|
ok "主仓公开可读 (git ls-remote HEAD)"
|
||||||
|
else
|
||||||
|
hard "主仓 git ls-remote 失败(LGPL §4(d) 应用源码可达性)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "无 git —— 跳过主仓可读性校验"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 5) 合规清单 ----------
|
||||||
|
say "== 合规清单 =="
|
||||||
|
say "libmobi commit : ${LM_COMMIT:-?}"
|
||||||
|
say "libmobi tarball : ${LIVE_SHA:-${NOTICE_SHA:-未校验}}"
|
||||||
|
say "包内许可证 :"
|
||||||
|
if [ -f "$MPK" ]; then tar tzf "$MPK" 2>/dev/null | grep 'resources/licenses/' || true; fi
|
||||||
|
|
||||||
|
if [ "$fail" = 1 ]; then say "结果:FAIL"; exit 1; fi
|
||||||
|
say "结果:PASS"
|
||||||
@@ -75,6 +75,16 @@ static int g_ime_done = 0;
|
|||||||
static int g_ime_result = 0;
|
static int g_ime_result = 0;
|
||||||
static int g_shift = 0;
|
static int g_shift = 0;
|
||||||
|
|
||||||
|
/* WiFi 设置页 bug3(2026-09-15):IME 打开瞬间 TAP 冷却窗。触发打开的那次点按,其「抬起
|
||||||
|
* TAP」(含 goodix 抬起长尾)可能由 SDK 输入线程在打开 drain 之后才读出并入队(子页自有
|
||||||
|
* tfd 与 SDK 输入线程独立读同一 evdev,节奏可滞后 ~20ms+)——阻塞循环把它当「点背景=取消」
|
||||||
|
* 瞬间关键盘(y<KB_TOP)或幻影按键(y≥KB_TOP)。窗口期内的 TAP 一律消费丢弃(镜像
|
||||||
|
* meow_ui_run 的 STARTUP_COOLDOWN_MS 500 先例,取 200 兼顾手感;正常打字第一键必在窗后)。
|
||||||
|
* 防御为双保险:drain 已复位触摸状态机(在途按压不再产 TAP),冷却窗再兜住 drain 前已入队
|
||||||
|
* 未及清/时序竞争残漏的 TAP。 */
|
||||||
|
#define IME_TAP_COOLDOWN_MS 200
|
||||||
|
static uint64_t g_ime_open_ms = 0; /* meow_ui_time_ms() 单调毫秒;0=未记录 */
|
||||||
|
|
||||||
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
|
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
|
||||||
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
|
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
|
||||||
static char g_ime_buf[256];
|
static char g_ime_buf[256];
|
||||||
@@ -570,6 +580,11 @@ int meow_ime_long_press(int x, int y)
|
|||||||
int meow_ime_handle_event(const meow_input_ev *e)
|
int meow_ime_handle_event(const meow_input_ev *e)
|
||||||
{
|
{
|
||||||
if (!g_ime_active || g_ime_done) return 0;
|
if (!g_ime_active || g_ime_done) return 0;
|
||||||
|
/* 打开冷却窗(bug3):窗口内 TAP 一律消费丢弃——防触发打开那次点按的迟到「抬起 TAP」
|
||||||
|
* 被当「点背景=取消」或幻影按键。消费(return 1)而非透传:事件已属 IME 会话期。 */
|
||||||
|
if (e->type == MEOW_EV_TAP && g_ime_open_ms &&
|
||||||
|
meow_ui_time_ms() - g_ime_open_ms < IME_TAP_COOLDOWN_MS)
|
||||||
|
return 1;
|
||||||
switch (e->type) {
|
switch (e->type) {
|
||||||
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
|
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
|
||||||
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
|
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
|
||||||
@@ -605,6 +620,11 @@ void meow_ui_text_input_async(meow_ime_ctx *ctx)
|
|||||||
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
|
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
|
||||||
meow_input_start();
|
meow_input_start();
|
||||||
meow_input_drain();
|
meow_input_drain();
|
||||||
|
/* bug3 加固(对齐 s_dd_modal/subpage 吸收 goodix 抬起长尾的「排空→40ms→再排空」惯例):
|
||||||
|
* 第一遍 drain 后等 40ms 让输入线程读出迟到事件,第二遍 drain 连状态机一并复位。 */
|
||||||
|
usleep(40000);
|
||||||
|
meow_input_drain();
|
||||||
|
g_ime_open_ms = meow_ui_time_ms(); /* TAP 冷却窗起点(见 IME_TAP_COOLDOWN_MS) */
|
||||||
}
|
}
|
||||||
|
|
||||||
int meow_ui_text_input(meow_ime_ctx *ctx)
|
int meow_ui_text_input(meow_ime_ctx *ctx)
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
|
|
||||||
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
|
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
|
||||||
void meow_ui_gesture_end(void);
|
void meow_ui_gesture_end(void);
|
||||||
|
/* 触摸状态机复位(定义在触摸状态机变量之后,meow_input_drain 内调用)。 */
|
||||||
|
void meow_input_touch_reset(void);
|
||||||
|
|
||||||
/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)──
|
/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)──
|
||||||
* 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理
|
* 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理
|
||||||
@@ -212,6 +214,10 @@ void meow_input_drain(void)
|
|||||||
g_qh = 0; g_qt = 0;
|
g_qh = 0; g_qt = 0;
|
||||||
pthread_mutex_unlock(&g_qm);
|
pthread_mutex_unlock(&g_qm);
|
||||||
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
|
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
|
||||||
|
/* WiFi 设置页 bug3(2026-09-15):drain 同时复位触摸状态机——语义自洽:drain=丢弃在途
|
||||||
|
* 手势。否则 drain 前已开始、drain 后才被输入线程读出的「抬起」仍会按残留原点产生
|
||||||
|
* 一个迟到 TAP 入队(IME 打开瞬间被当「点背景=取消」或幻影按键,键盘开即关)。 */
|
||||||
|
meow_input_touch_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void push_ev(int type, int a, int b)
|
static void push_ev(int type, int a, int b)
|
||||||
@@ -260,6 +266,19 @@ static int t_mv_x = -1, t_mv_y = -1;
|
|||||||
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
|
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
|
||||||
static int t_long_fired = 0;
|
static int t_long_fired = 0;
|
||||||
|
|
||||||
|
/* 触摸状态机整体复位:丢弃在途手势(down/原点/坐标/长按触发标记全部归零)。
|
||||||
|
* 调用点:① meow_input_drain(drain=丢弃在途手势,IME/模态打开前);② §10.5 叠加抑制
|
||||||
|
* 进入沿(防抑制期丢失抬手致 t_down 卡死)。效果:复位前已开始、复位后才抬起的按压
|
||||||
|
* 不再产生 TAP/SWIPE(WiFi 设置页 bug3:IME 打开 drain 之后才入队的迟到「抬起 TAP」
|
||||||
|
* 曾把键盘当「点背景=取消」瞬间关闭)。 */
|
||||||
|
void meow_input_touch_reset(void)
|
||||||
|
{
|
||||||
|
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
|
||||||
|
t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0;
|
||||||
|
t_mv_x = -1; t_mv_y = -1;
|
||||||
|
t_long_fired = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
|
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
|
||||||
static int g_long_press_ms = MEOW_LONG_PRESS_MS;
|
static int g_long_press_ms = MEOW_LONG_PRESS_MS;
|
||||||
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
|
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
|
||||||
@@ -592,9 +611,7 @@ static void *input_thread(void *arg)
|
|||||||
if (overlay_input_suppressed()) {
|
if (overlay_input_suppressed()) {
|
||||||
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
|
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
|
||||||
overlay_was = 1;
|
overlay_was = 1;
|
||||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
|
meow_input_touch_reset();
|
||||||
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
|
|
||||||
t_long_fired = 0;
|
|
||||||
}
|
}
|
||||||
if (r > 0)
|
if (r > 0)
|
||||||
for (int i = 0; i < nfd; i++) {
|
for (int i = 0; i < nfd; i++) {
|
||||||
|
|||||||
@@ -207,8 +207,10 @@ void meow_input_start(void);
|
|||||||
int meow_input_pop(meow_input_ev *ev);
|
int meow_input_pop(meow_input_ev *ev);
|
||||||
/* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */
|
/* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */
|
||||||
void meow_input_inject(int type, int a, int b);
|
void meow_input_inject(int type, int a, int b);
|
||||||
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲)。 */
|
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲);同时复位触摸状态机(drain=丢弃在途手势)。 */
|
||||||
void meow_input_drain(void);
|
void meow_input_drain(void);
|
||||||
|
/* 触摸状态机整体复位(丢弃在途手势;drain/叠加抑制进入沿调用;宿主单测可直调)。 */
|
||||||
|
void meow_input_touch_reset(void);
|
||||||
|
|
||||||
/* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ----
|
/* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ----
|
||||||
* launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在
|
* launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
* ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关;
|
* ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关;
|
||||||
* ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上
|
* ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上
|
||||||
* (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。
|
* (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。
|
||||||
|
* ④ bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——打开后立即注入的 stray-TAP
|
||||||
|
* (背景/键区)被吞不取消;冷却窗后背景 TAP=取消语义保留。
|
||||||
*
|
*
|
||||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||||
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \
|
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \
|
||||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||||
|
* meow_ui_anim.c meow_ui_media.c meow_ui_nav.c meow_pinyin.c meow_ui_box.c meow_ui_scroll.c \
|
||||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm;
|
||||||
|
* widget/input/ime/view 后续又引入 anim/media/nav/pinyin/box/scroll 依赖,一并链接)
|
||||||
*/
|
*/
|
||||||
#include "meow_ui.h"
|
#include "meow_ui.h"
|
||||||
#include "meow_ui_internal.h"
|
#include "meow_ui_internal.h"
|
||||||
@@ -54,6 +58,38 @@ static int near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14
|
|||||||
/* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */
|
/* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */
|
||||||
static char g_echo[64] = "";
|
static char g_echo[64] = "";
|
||||||
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
||||||
|
|
||||||
|
/* ④ WiFi 设置页 bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——触发打开那次点按的迟到
|
||||||
|
* 「抬起 TAP」(goodix 长尾,经 SDK 输入线程 drain 后才入队)不得把键盘当「点背景=取消」
|
||||||
|
* 瞬间关掉;冷却窗后点背景=取消的语义保留。 */
|
||||||
|
static void test_stray_tap(void)
|
||||||
|
{
|
||||||
|
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||||
|
c.mode = MEOW_IME_ASCII; c.maxlen = 32; c.on_change = ime_cb;
|
||||||
|
meow_ui_text_input_async(&c);
|
||||||
|
assert(meow_ime_active() == 1);
|
||||||
|
|
||||||
|
/* 打开后立即注入背景 TAP(y<KB_TOP=420):冷却窗内必须被吞——不取消、键盘仍 active */
|
||||||
|
meow_input_ev stray = { MEOW_EV_TAP, 240, 100 };
|
||||||
|
assert(meow_ime_handle_event(&stray) == 1); /* 消费(吞掉) */
|
||||||
|
assert(meow_ime_active() == 1); /* 键盘未被关(修复前此处已 done=取消) */
|
||||||
|
|
||||||
|
/* 窗口内键区 TAP 也丢弃(防幻影按键) */
|
||||||
|
meow_input_ev key = { MEOW_EV_TAP, K_Q_X, K_Q_Y };
|
||||||
|
assert(meow_ime_handle_event(&key) == 1);
|
||||||
|
assert(meow_ime_active() == 1);
|
||||||
|
assert(meow_ime_text()[0] == 0); /* 无幻影字符入缓冲 */
|
||||||
|
|
||||||
|
/* 冷却窗(200ms)后:点背景=取消 语义保留 */
|
||||||
|
usleep(250000);
|
||||||
|
assert(meow_ime_handle_event(&stray) == 1);
|
||||||
|
assert(meow_ime_active() == 0); /* 已取消(g_ime_done) */
|
||||||
|
printf("PASS stray-tap: 窗内背景/键区 TAP 吞掉不取消, 窗后背景 TAP 取消\n"); fflush(stdout);
|
||||||
|
|
||||||
|
meow_ime_close(0); /* 复位(清 g_ime_done/active) */
|
||||||
|
assert(meow_ime_active() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_shift(void)
|
static void test_shift(void)
|
||||||
{
|
{
|
||||||
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||||
@@ -94,8 +130,6 @@ static char g_drv_text[64] = ""; /* shift 序列后 meow_ime_text()(期
|
|||||||
/* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */
|
/* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */
|
||||||
static void dump_ppm(const char *path)
|
static void dump_ppm(const char *path)
|
||||||
{
|
{
|
||||||
unsigned char *bb = meow_ui_back_buf();
|
|
||||||
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
|
|
||||||
int W = meow_ui_width(), H = meow_ui_height();
|
int W = meow_ui_width(), H = meow_ui_height();
|
||||||
FILE *f = fopen(path, "wb");
|
FILE *f = fopen(path, "wb");
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
@@ -134,7 +168,7 @@ static void *driver(void *arg)
|
|||||||
(void)arg;
|
(void)arg;
|
||||||
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
|
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
|
||||||
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
|
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
|
||||||
usleep(160000);
|
usleep(350000); /* 等过 IME 打开 TAP 冷却窗(200ms,bug3)+ drain 40ms */
|
||||||
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
|
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
|
||||||
usleep(90000);
|
usleep(90000);
|
||||||
meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
|
meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
|
||||||
@@ -169,6 +203,7 @@ int main(void)
|
|||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
|
|
||||||
test_shift(); /* ① */
|
test_shift(); /* ① */
|
||||||
|
test_stray_tap(); /* ④ bug3:打开冷却窗吞 stray-TAP */
|
||||||
|
|
||||||
/* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */
|
/* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */
|
||||||
g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII);
|
g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII);
|
||||||
|
|||||||
Reference in new issue
Block a user