ncm search: 历史+单曲/歌单分类+分页+误触修复+不覆写歌单上下文 (§23)

- 搜索历史:进页自动弹键盘;meow_config 持久化 ≤12 去重最新在前;空态=历史列表(点搜/左滑删/清空)
- 结果分类单曲/歌单页签:独立结果集+独立滚动;歌单行进详情(playlist_open_pl);单曲行播放
- 触底分页:offset 累加 limit=30/页 cloudsearch 同参数;每类各记 offset
- 误触抑制:g_press_row + g_press_delta>10(对齐 home/playlist §13.3)
- 结果改独立数组(不写 g_st.songs[]);播放搜索单曲建搜索队列(queue_src=SEARCH),不弄丢歌单窗口
- core cloudsearch 扩展 type/offset/limit/total + 歌单解析
- bench/search_sim.c 离线回归(历史/独立集/翻页 offset/队列上下文);host searchtap 误触自检

验证:mipsel -Wall -Wextra 0 warning;core host/mipsel PASS;search_sim PASS;
宿主离屏 search(单曲)/searchpl(歌单)/searchhist(空态历史)/searchtap(误触) 均正常;0 真实 NCM 请求。
This commit is contained in:
MeowOS committed 2026-09-01 19:21:16 +08:00
1 parent d4928224a5
commit 1e97dbf54a
15 files changed
+1152 -121

No files matched your search

+136
View File
@@ -2092,3 +2092,139 @@ fa00ea4f0093cd7e0a2600887106de07 app/ncm_app_internal.h
⟳ 钮一并退役);`player_screen()` 恒 0,宿主 `grid`/`lands` 旧模式不再有横屏布局。
- **歌词读锁残余(既有)**:`lyric_line_text/tl` 解锁后返回静态指针可能被下次 `lyric_parse` 覆盖
(§14.7 已知);本批未改,点行跳转读 `lyric_line_time` 同理(写入仅在切歌瞬间,窗口极小)。
---
---
## 23. 搜索增强:历史 + 单曲/歌单分类 + 分页 + 误触修复 + 不覆写歌单上下文(2026-09-01,agent-273)
> 范围:仅 `patch_v2/ncm_app/`。定案见 `devdocs/plan/NCM_CACHE_DESIGN.md §5`(历史+自动聚焦+「单曲/歌单」两类起步)
> 与 `NCM_UI_REVIEW.md §2.4/§3-2`(搜索缺什么 / 点行误触)。风控:本批 0 次真实 NCM 请求(全离线 stub/
> 宿主 mock/离线样本);真机部署留主会话统一。上一批(播放页重做 §22)已 commit,本批在其上做。
### 23.1 搜索历史(进页自动弹键盘 + 本地持久化)
- **进页自动弹键盘**:`search_open`/`sr_page_enter` 后若 `g_field && search_query` 为空 → `meow_textfield_activate`。
`#ifndef MEOW_UI_HOST_TEST` 包住(宿主离屏为阻塞式模态 IME,避免挂起 dump;真机走阻塞式弹键盘——与既有
「点输入框唤 IME」同构)。
- **持久化**:`appstate.c` 新增历史函数族(`appstate_search_history_add/get/count/remove/clear`),内存数组
`g_hist[12][128]`(静态,bss,最小)+ 单键 `meow_config`(`ncm.search_history`,条目以 `\n` 连接)。
语义:**去重 + 最新在前 + 截断 ≤12**(用户定案)。`search_submit` 成功提交即 `add(kw)`。
- **空态=历史列表**:`sr_view()` 在 `search_query` 为空且历史非空 → `SR_HIST`:列表行=历史(点=直接 `search_submit`)、
右缘「清空全部」按钮、左滑删单条(`search_swipe_left`,见 23.4)。历史为空回落「输入关键词」空态。
### 23.2 结果分类「单曲/歌单」+ 独立页签
- 顶部小页签(`r_tabs[0/1]`,各 `r_btn.w/2-6`):单曲(type=1)/歌单(type=1000)。选中=品牌红下划线高亮。
- **两页签各自独立结果集与滚动位置**:`g_st.search_songs[]`(单曲,`NCM_SEARCH_SONG_MAX=256`)与
`g_st.search_pls[]`(歌单,`NCM_SEARCH_PL_MAX=60`)互不干扰;滚动各存 `g_scroll_song`/`g_scroll_pl`,
`sr_set_tab` 先存旧再恢复新。非当前页签在提交新查询时被清空(`g_tab^1`),切过去懒加载(用户动作)。
- 单曲行点击 → `player_play_search_song(i)`(搜索队列起播);歌单行点击 → `playlist_open_pl(&g_st.search_pls[i])`
(进该歌单详情,见 23.5)。
### 23.3 加载更多(触底分页)
- `appstate_search_count(tab)` 即该页签已加载条数(=下次 offset);`appstate_search_has_more(tab)` 判还有下一页。
- 触底(`scroll + r_list.h >= count*itemH - itemH*2`)且 `more & !g_thr_on` → `search_fetch(tab, offset=count, 0)`。
cloudsearch 同参数 `limit=NCM_SEARCH_PAGE(30)`、`offset` 累加;每类各记各的 `offset`(`search_song_offset`/
`search_pl_offset`,appstate append 回填)。
- `has_more` 判定:`total>=0 ? (count<total) : (本页==limit)`;结果数组到上限仍按服务端总数反映(UI 层
`n>=cap` 停止拉取,内存护栏独立于服务端 total)。
### 23.4 误触抑制(NCM_UI_REVIEW §3-2)
- `search_on_tap` 点行套用 §13.3 修法(对齐 home/playlist):`was_drag = (g_press_delta > 10)`;
`row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(...)`;`row<0 || was_drag` 不点行。
`g_press_row`/`g_press_delta` 在 `search_touch_down/move` 已记录(沿用),历史模式同样生效。
### 23.5 结果不覆写歌单上下文(核心解耦)
- **搜索结果改独立数组**:`search_worker` 不再写 `g_st.songs[]` 也不再 `appstate_adopt_songs`;单曲结果写
`g_st.search_songs[]`(appstate 持 `g_search_mu`,经 `appstate_search_append_songs` 提交),**g_st.songs[]/
song_ids/track_ids_count(歌单窗口)永不被搜索触碰**。
- **播放搜索单曲 = 建立「搜索队列」上下文**:`player_play_search_song(i)` 设 `g_st.queue_src=NCM_QUEUE_SEARCH`
→ `appstate_queue_count()/queue_song()` 改读 `search_songs[]`(`player_manager.c` 的
`run_song/next/player_thread/lyric_worker` 全部改走 `appstate_queue_*`,按 queue_src 分派窗口或搜索)。
`player_manager_next`/`player_thread` 边界 = `appstate_queue_count()`。**从歌单详情退到搜索页时原歌单窗口
不丢**(`track_ids_count/song_ids` 未被改写)。
- **歌单结果进详情**:新增 `playlist_open_pl(const ncm_playlist_t*)`——拷入 `g_st.search_pl_open`、
`active_playlist=NCM_PL_SEARCH` 哨兵;`appstate_load_playlist_songs_page`/`appstate_apply_playlist_cover`/
`pl_cur`(playlist.c)对哨兵改读 `search_pl_open`,复用既有滑窗/封面/分页链路,不污染真实歌单列表。
### 23.6 core:cloudsearch 扩展(type/offset/limit/total)
- `ncm_api_cloudsearch_song`(type=1)/`ncm_api_cloudsearch_playlist`(type=1000):共用
`cloudsearch_post` POST(`s/type/offset/limit/total:true`)+ `cloudsearch_count`
(读 `result.songCount`/`result.playlistCount`,缺省 -1)。新增 `parse_playlists_array`(id/name/coverImgUrl/
trackCount)。旧 `ncm_api_cloudsearch` 保留为单曲 wrapper(type=1, offset=0)。
### 23.7 验证记录(0 真实 NCM 请求)
| 项 | 方式 | 结果 |
|---|---|---|
| 历史去重/截断/最新在前 | `bench/search_sim.c`(离线,驱动真实 appstate 历史函数) | **PASS**:去重 2 条;重add 最新在前;截断 ≤12(kw16 在顶、kw8 在 idx8 顺序保留);remove 前移;clear |
| 单曲/歌单独立结果集 | `bench/search_sim.c` | **PASS**:song 集 4/pl 集 2 互不干扰;clear song 留 pl;song/pl copy 正确 |
| 翻页 offset/has_more | `bench/search_sim.c` | **PASS**:clear 复位 offset=0/more=1;page1 30→offset 30;累计到 total 240→more=0;clamp 至 SONG_MAX 且 total>cap→more=1;无 total(-1)回落页大小判 more |
| 搜索队列上下文(不覆写窗口) | `bench/search_sim.c` | **PASS**:queue=playlist 7/queue=search 3;queue_song 读搜索集;`track_ids_count/song_ids/song_valid` 原样 |
| 误触抑制 | `app/test/test_ncm_app.c` searchtap(真实页面+真实 SDK 手势) | **PASS**:干净点击→播按下行(cur=0,qsrc=SEARCH);拖动(+30px>10)→was_drag 抑制(cur -1→-1) |
| core 回归 | `test_ncm_core_host` / `qemu ./out/test_ncm_core_mipsel` | **PASS** |
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 972518 / data 1608 / bss 1364548`(bss +~1MB 来自 search_songs/pls 独立数组) |
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 空态=历史列表(searchhist 3 行+清空区)/结果态两类页签(search 单曲 3 行下划线在左、searchpl 歌单 2 行下划线在右)均正常,无崩溃 |
### 23.8 md5(本批改动文件 + 产物)
```
30b24372ab9833c0c49f97be3b9418ee ncm # mipsel 静态 App(新)
ca3b451c0be5e5ead7f215cb8d431594 ncm_host # 宿主离屏调试二进制(重编)
b22cc6f5696d778c1a93adcf39ed4847 core/ncm_api.c
14ddc89bf1e1b20afc3c7008377ba928 core/ncm_api.h
330f131d42b9f70ab8792ace2f1a7197 app/ncm_app_internal.h
1e02f90fdd9aae0b3e725548874b19f9 app/logic/appstate.c
21a80495141cec6506e9d5197e56c937 app/ui/search.c
666e18381a6b284b6bb4b1b92c056fec app/ui/player.c
63485f230aca0f20d7bf0a977b36000c app/ui/playlist.c
cd0d99b31f56df698c44e3fbeace95fc app/play/player_manager.c
46d9a5f16e257709010fba79ffe44a61 app/app.c
060bc58975f6f56f6e2a6e87c23b3a1e app/test/test_ncm_app.c
c9c115cda1340b92d1988e330eafbb5b bench/search_sim.c
```
### 23.9 出入清单(只改 `patch_v2/ncm_app/`)
```
修改:
core/ncm_api.c/.h # cloudsearch 扩展:type/offset/limit/total + 歌单解析 + parse_playlists_array
app/ncm_app_internal.h # 搜索常量/状态字段/search_pl_open/queue_src + 新函数原型 + (去 NCM_SEARCH_MAX)
app/logic/appstate.c # 搜索历史(≤12去重) + 独立结果集(append/clear/count/offset/has_more) +
# 播放队列来源(queue_*) + NCM_PL_SEARCH 哨兵支持 + 默认 queue_src=PLAYLIST
app/ui/search.c # 重写:历史(点搜/左滑删/清空) + 单曲/歌单页签 + 触底分页 + 误触抑制 +
# 进页自动弹键盘 + 结果改走 appstate 独立数组
app/ui/player.c # player_play_song(队列=PLAYLIST) + player_play_search_song(队列=SEARCH) +
# cur_song() 按 appstate_queue_count
app/ui/playlist.c # pl_cur() 助手 + playlist_open_pl(搜索歌单) + 封面/标题哨兵处理
app/play/player_manager.c # 播放循环/next/歌词 worker 改走 appstate_queue_count/queue_song
app/app.c # LEFT 左滑先发 search_swipe_left(历史删单条),未消费才返
app/test/test_ncm_app.c # 宿主:search/searchhist/searchpl/searchtap 场景 + mock 改独立数组
新增:
bench/search_sim.c # 搜索数据层离线回归(历史/独立集/翻页 offset/队列上下文;0 网络)
REPORT.md # 本批记录(§23)
(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备系统/恢复区)
```
### 23.10 诚实出入 / 已知限制
- **真机未部署未验**(留主会话统一)。真机验收点:①进搜索页自动弹键盘(拼音 IME 候选上屏)②输入关键词
→点「搜索」出单曲、切「歌单」页签出歌单、滚动触底翻页 ③点搜索单曲播放、上/下一首在搜索结果集内循环、
返回歌单详情原歌单不丢 ④空态历史列表:点=直接搜、左滑删单条、右缘清空全部 ⑤快速横滑列表抬手不误播。
- **歌单结果进详情走 `NCM_PL_SEARCH` 哨兵(active_playlist=-2)**:极少数「歌单列表恰好满 120 且正在浏览」的
场景下此哨兵不与真实下标冲突(哨兵为负,永不在 [0,120) 内);`pl_cur()` 统一分派,未污染的边界已由
bench [4] 的「窗口原样」校验覆盖。
- **`appstate_adopt_songs` 现为不可达死代码**(搜索结果不再收编窗口):保留(公开函数,`-Wall -Wextra` 不告警),
供既有单测/文档引用;后续可清理。
- **历史持久化单键 `ncm.search_history`(条目以 `\n` 连接)**:搜索词不含换行(IME 不产生);若将来支持
粘贴含换行文本需改定界(本项目无粘贴,当前安全)。
- **两页签懒加载**:提交查询只拉当前页签;切到另一页签(其结果为空)才发该页签请求——属用户动作
(点页签),但严格讲不是「点搜索钮」;已避免进页自动搜/逐键发。风控更严可直接在 `search_submit` 双拉。
- **`meow_textfield_activate` 为阻塞式模态 IME**(SDK 既定):进页自动弹键盘会阻塞 UI 直至用户关闭键盘,
与「点输入框唤 IME」一致(既有交互模式);宿主离屏以 `#ifndef MEOW_UI_HOST_TEST` 跳过以免挂起 dump。
- **未动 sdk/、meow_launcherd/、vendor/、core(除 cloudsearch 扩展)**;部署链路同 §13.5 留主会话。