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 留主会话。
+2
View File
@@ -45,6 +45,8 @@ static void app_on_swipe(int dir, int dx, int dy)
{
(void)dx;
if (dir == MEOW_SWIPE_LEFT) {
/* 搜索历史模式:左滑删单条(§23);未消费才走上/下一层返回语义。 */
if (search_is_active() && search_swipe_left()) return;
if (player_is_active()) { meow_view_page_pop(); return; }
if (settings_is_active()) { meow_view_page_pop(); return; }
if (playlist_is_active()) { meow_view_page_pop(); return; }
+229 -3
View File
@@ -10,6 +10,11 @@
/* 滑动窗口互斥(REPORT §17):窗口读写(滑窗/物化/拷贝)全走此锁,见下方窗口节注释。 */
static pthread_mutex_t g_song_mu = PTHREAD_MUTEX_INITIALIZER;
/* 搜索结果独立数组互斥(§23):g_st.search_songs/search_pls 读写 + queue_src 读。 */
static pthread_mutex_t g_search_mu = PTHREAD_MUTEX_INITIALIZER;
static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
int appstate_init(void)
{
const char *dd = getenv("MEOW_DATA_DIR");
@@ -37,6 +42,9 @@ int appstate_init(void)
account_load(); /* §20:恢复会员态缓存(account.json;无文件则 NCM_VIP_NONE) */
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:默认播放队列=歌单窗口 */
g_st.search_tab = 0;
appstate_srch_hist_init(); /* §23:读取本地搜索历史 */
return 0;
}
@@ -362,10 +370,11 @@ void appstate_adopt_songs(int n)
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
{
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
/* 搜索歌单详情(§23):idx==NCM_PL_SEARCH 时读 g_st.search_pl_open(复用本链路)。 */
if (idx != NCM_PL_SEARCH && (idx < 0 || idx >= g_st.playlist_count)) return NCM_ERR_ARG;
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
const ncm_playlist_t *pl = &g_st.playlists[idx];
const ncm_playlist_t *pl = (idx == NCM_PL_SEARCH) ? &g_st.search_pl_open : &g_st.playlists[idx];
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
if (offset < 0) offset = 0;
@@ -412,6 +421,10 @@ int appstate_load_playlist_songs(int idx)
void appstate_apply_playlist_cover(int idx)
{
if (idx == NCM_PL_SEARCH) {
ncm_strlcpy(g_st.cover_url, g_st.search_pl_open.cover, sizeof g_st.cover_url);
return;
}
if (idx < 0 || idx >= g_st.playlist_count) return;
ncm_strlcpy(g_st.cover_url, g_st.playlists[idx].cover, sizeof g_st.cover_url);
}
@@ -427,7 +440,9 @@ void appstate_set_playing_song(int idx, const char *cover_url)
int appstate_current_playing(void)
{
if (g_st.playing_song < 0 || g_st.playing_song >= g_st.track_ids_count) return -1;
if (g_st.playing_song < 0) return -1;
int cnt = appstate_queue_count(); /* §23:搜索队列时按搜索条数界(不误判歌单亮点) */
if (g_st.playing_song >= cnt) return -1;
return g_st.playing_song;
}
@@ -523,3 +538,214 @@ void autoplay_maybe(void)
if (pthread_create(&t, NULL, autoplay_worker, sel) == 0) pthread_detach(t);
else free(sel);
}
/* ============================================================
* 搜索(REPORT §23):独立结果数组(不覆写歌单窗口)+ 播放队列来源 + 本地历史。
* 结果数组读写经 g_search_mu;queue_src 决定超时/起播读数组还是窗口。
* ============================================================ */
void appstate_set_queue(int src) { g_st.queue_src = src; }
int appstate_get_queue(void) { return g_st.queue_src; }
int appstate_queue_count(void)
{
int c;
pthread_mutex_lock(&g_search_mu);
c = (g_st.queue_src == NCM_QUEUE_SEARCH) ? g_st.search_song_count : g_st.track_ids_count;
pthread_mutex_unlock(&g_search_mu);
return c < 0 ? 0 : c;
}
int appstate_queue_song(int idx, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
if (g_st.queue_src == NCM_QUEUE_SEARCH) {
pthread_mutex_lock(&g_search_mu);
int ok = (idx >= 0 && idx < g_st.search_song_count);
if (ok) *out = g_st.search_songs[idx];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
if (appstate_song_copy(idx, out) == 0) return NCM_OK;
return appstate_song_fetch(idx, out);
}
int appstate_search_count(int tab)
{
int c;
pthread_mutex_lock(&g_search_mu);
c = (tab == 0) ? g_st.search_song_count : g_st.search_pl_count;
pthread_mutex_unlock(&g_search_mu);
return c;
}
int appstate_search_offset(int tab)
{
int o;
pthread_mutex_lock(&g_search_mu);
o = (tab == 0) ? g_st.search_song_offset : g_st.search_pl_offset;
pthread_mutex_unlock(&g_search_mu);
return o;
}
int appstate_search_has_more(int tab)
{
int m;
pthread_mutex_lock(&g_search_mu);
m = (tab == 0) ? g_st.search_song_more : g_st.search_pl_more;
pthread_mutex_unlock(&g_search_mu);
return m;
}
int appstate_search_song(int i, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
pthread_mutex_lock(&g_search_mu);
int ok = (i >= 0 && i < g_st.search_song_count);
if (ok) *out = g_st.search_songs[i];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
int appstate_search_pl(int i, ncm_playlist_t *out)
{
if (!out) return NCM_ERR_ARG;
pthread_mutex_lock(&g_search_mu);
int ok = (i >= 0 && i < g_st.search_pl_count);
if (ok) *out = g_st.search_pls[i];
pthread_mutex_unlock(&g_search_mu);
return ok ? NCM_OK : NCM_ERR_ARG;
}
void appstate_search_clear(int tab)
{
pthread_mutex_lock(&g_search_mu);
if (tab == 0) {
g_st.search_song_count = 0; g_st.search_song_offset = 0;
g_st.search_song_total = -1; g_st.search_song_more = 1;
memset(g_st.search_songs, 0, sizeof g_st.search_songs);
} else {
g_st.search_pl_count = 0; g_st.search_pl_offset = 0;
g_st.search_pl_total = -1; g_st.search_pl_more = 1;
memset(g_st.search_pls, 0, sizeof g_st.search_pls);
}
pthread_mutex_unlock(&g_search_mu);
}
/* 追加单曲结果(worker 锁内):写 g_st.search_songs[count..),回填 offset/total/has_more。 */
void appstate_search_append_songs(int n, const ncm_song_t *s, int64_t total)
{
if (n <= 0 || !s) return;
pthread_mutex_lock(&g_search_mu);
int room = NCM_SEARCH_SONG_MAX - g_st.search_song_count;
if (n > room) n = room;
if (n > 0) memcpy(&g_st.search_songs[g_st.search_song_count], s, (size_t)n * sizeof(ncm_song_t));
g_st.search_song_count += n;
g_st.search_song_offset = g_st.search_song_count;
g_st.search_song_total = (total >= 0) ? (int)total : g_st.search_song_total;
int t = g_st.search_song_total;
g_st.search_song_more = (t >= 0) ? (g_st.search_song_count < t) : (n >= NCM_SEARCH_PAGE);
pthread_mutex_unlock(&g_search_mu);
}
void appstate_search_append_pls(int n, const ncm_playlist_t *p, int64_t total)
{
if (n <= 0 || !p) return;
pthread_mutex_lock(&g_search_mu);
int room = NCM_SEARCH_PL_MAX - g_st.search_pl_count;
if (n > room) n = room;
if (n > 0) memcpy(&g_st.search_pls[g_st.search_pl_count], p, (size_t)n * sizeof(ncm_playlist_t));
g_st.search_pl_count += n;
g_st.search_pl_offset = g_st.search_pl_count;
g_st.search_pl_total = (total >= 0) ? (int)total : g_st.search_pl_total;
int t = g_st.search_pl_total;
g_st.search_pl_more = (t >= 0) ? (g_st.search_pl_count < t) : (n >= NCM_SEARCH_PAGE);
pthread_mutex_unlock(&g_search_mu);
}
/* ---- 搜索历史(≤NCM_SEARCH_HIST_MAX,最新在前;单键 meow_config 持久化,条目以 \n 连接)---- */
#define NCM_SEARCH_HIST_KEY "ncm.search_history"
static char g_hist[NCM_SEARCH_HIST_MAX][NCM_SEARCH_HIST_LEN];
static int g_hist_count;
static int g_hist_loaded;
static void srch_hist_load(void)
{
char buf[2048];
if (meow_config_get(NCM_SEARCH_HIST_KEY, buf, sizeof buf) != MEOW_CFG_OK) { g_hist_count = 0; return; }
g_hist_count = 0;
char *p = buf;
while (p && *p && g_hist_count < NCM_SEARCH_HIST_MAX) {
char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
if (len >= NCM_SEARCH_HIST_LEN) len = NCM_SEARCH_HIST_LEN - 1;
if (len > 0) { memcpy(g_hist[g_hist_count], p, len); g_hist[g_hist_count][len] = 0; g_hist_count++; }
if (!nl) break;
p = nl + 1;
}
}
static void srch_hist_save(void)
{
char buf[2048];
size_t bl = 0;
buf[0] = 0;
for (int i = 0; i < g_hist_count; i++) {
int n = snprintf(buf + bl, sizeof buf - bl, "%s%s", i ? "\n" : "", g_hist[i]);
if (n < 0) break;
bl += (size_t)n;
if (bl >= sizeof buf - 1) break;
}
meow_config_set(NCM_SEARCH_HIST_KEY, buf);
}
static void appstate_srch_hist_init(void)
{
if (!g_hist_loaded) { srch_hist_load(); g_hist_loaded = 1; }
}
int appstate_search_history_count(void)
{
appstate_srch_hist_init();
return g_hist_count;
}
int appstate_search_history_get(int i, char *out, size_t sz)
{
appstate_srch_hist_init();
if (i < 0 || i >= g_hist_count) return 0;
ncm_strlcpy(out, g_hist[i], sz);
return 1;
}
void appstate_search_history_add(const char *kw)
{
if (!kw || !kw[0]) return;
appstate_srch_hist_init();
char tmp[NCM_SEARCH_HIST_MAX][NCM_SEARCH_HIST_LEN];
int m = 0;
ncm_strlcpy(tmp[m], kw, sizeof tmp[m]);
m++;
for (int i = 0; i < g_hist_count && m < NCM_SEARCH_HIST_MAX; i++)
if (strcmp(g_hist[i], kw) != 0) { ncm_strlcpy(tmp[m], g_hist[i], sizeof tmp[m]); m++; }
g_hist_count = m;
memcpy(g_hist, tmp, sizeof g_hist);
srch_hist_save();
}
void appstate_search_history_remove(int i)
{
appstate_srch_hist_init();
if (i < 0 || i >= g_hist_count) return;
for (int j = i; j < g_hist_count - 1; j++)
ncm_strlcpy(g_hist[j], g_hist[j + 1], sizeof g_hist[j]);
g_hist_count--;
srch_hist_save();
}
void appstate_search_history_clear(void)
{
g_hist_count = 0;
g_hist_loaded = 1;
srch_hist_save();
}
+58 -4
View File
@@ -63,7 +63,20 @@
* 窗口补载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB) */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
/* ---- 搜索(REPORT §23:独立结果数组 + 单曲/歌单两类页签 + 历史 + 分页)---- */
#define NCM_SEARCH_PAGE 30 /* cloudsearch limit/页(用户定案;offset 累加) */
#define NCM_SEARCH_SONG_MAX 256 /* 单曲结果累计上限(独立数组 ≈156KB bss,不覆写歌单窗口) */
#define NCM_SEARCH_PL_MAX 60 /* 歌单结果累计上限(≈48KB bss) */
#define NCM_SEARCH_HIST_MAX 12 /* 搜索历史条数(上次在最新,≤12) */
#define NCM_SEARCH_HIST_LEN 128 /* 单条历史长度 */
/* 播放队列来源(§23:搜索单曲起播建立「搜索队列」上下文,不覆写歌单窗口) */
#define NCM_QUEUE_PLAYLIST 0 /* 默认:歌单滑动窗口 */
#define NCM_QUEUE_SEARCH 1 /* 搜索单曲结果队列 */
/* active_playlist 哨兵:搜索歌单详情(复用 playlist_open 链路,读 g_st.search_pl_open) */
#define NCM_PL_SEARCH (-2)
/* ---- 播放/媒体会话状态(play 线程 ↔ UI/IPC 共享,mu 保护)---- */
enum {
@@ -169,11 +182,26 @@ typedef struct ncm_app_state {
int cache_limit_gb;
int player_bg_mode; /* 歌词页背景模式(§22):NCM_BG_DEFAULT / NCM_BG_POWER_SAVE */
/* 搜索(结果复用 g_st.songs[] 浅拷贝区;仅搜索页上下文有效)*/
/* 搜索(REPORT §23:独立结果数组,不覆写歌单窗口;单曲/歌单两类页签各自独立结果集/分页/滚动) */
char search_query[256];
int search_count; /* 当前搜索结果数(含 0=无/未搜) */
volatile int search_loading; /* 1=搜索请求在途 */
int search_err; /* 最近一次搜索错误(0=ok) */
int search_tab; /* 当前页签:0=单曲 1=歌单 */
ncm_song_t search_songs[NCM_SEARCH_SONG_MAX]; /* 单曲结果(独立数组) */
int search_song_count;
int search_song_offset; /* 下次 offset(=已加载条数) */
int search_song_total; /* 服务端总数(-1=未知) */
int search_song_more; /* 1=单曲还有下一页 */
ncm_playlist_t search_pls[NCM_SEARCH_PL_MAX]; /* 歌单结果(独立数组) */
int search_pl_count;
int search_pl_offset;
int search_pl_total;
int search_pl_more;
/* 搜索歌单打开的详情(active_playlist==NCM_PL_SEARCH 时用;复用 playlist_open 链路) */
ncm_playlist_t search_pl_open;
/* 播放队列来源(§23):默认 NCM_QUEUE_PLAYLIST;搜索单曲起播切 NCM_QUEUE_SEARCH */
int queue_src;
} ncm_app_state_t;
extern ncm_app_state_t g_st; /* 全局状态(app.c 定义) */
@@ -246,6 +274,7 @@ void playlist_swipe(int dir, int dist);
void playlist_touch_down(int x, int y);
void playlist_touch_move(int x, int y);
void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */
void playlist_open_pl(const ncm_playlist_t *pl); /* 打开「搜索来的歌单」详情(§23,复用链路) */
void playlist_refresh(void);
void playlist_tick(uint64_t now);/* UI tick:检查歌单 worker 是否完成并刷新 */
@@ -254,7 +283,8 @@ meow_view *player_view_create(void);
int player_is_active(void);
void player_on_tap(int x, int y);
void player_tick(uint64_t now); /* 播放进度/歌词滚动 */
void player_play_song(int idx); /* 从歌单第 idx 首开始播放 */
void player_play_song(int idx); /* 从歌单窗口第 idx 首开始播放(queue=PLAYLIST) */
void player_play_search_song(int i); /* 从搜索单曲第 i 首开始播放(queue=SEARCH,§23) */
void player_pause_toggle(int on);
void player_next(int dir); /* dir=+1 下一首 / -1 上一首 */
void player_seek(int64_t ms);
@@ -286,6 +316,7 @@ void search_touch_move(int x, int y);
void search_open(void); /* 进搜索页 */
void search_submit(const char *kw); /* 用户确认后发起搜索(风控:仅在确认时发) */
void search_tick(uint64_t now); /* UI tick:检查搜索 worker 完成并刷新 */
int search_swipe_left(void); /* 历史模式左滑删单条:1=已消费(不触返回) */
/* ---- logic ---- */
int appstate_init(void); /* 初始化 ctx/data/cookie/设置;0=成功 */
@@ -314,6 +345,29 @@ int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
* 同步 song_ids/track_ids_count(播放上下文) */
/* ---- 播放队列来源(§23)---- */
void appstate_set_queue(int src); /* NCM_QUEUE_PLAYLIST / NCM_QUEUE_SEARCH */
int appstate_get_queue(void);
int appstate_queue_count(void); /* 当前播放队列长度(按 queue_src) */
int appstate_queue_song(int idx, ncm_song_t *out); /* 队列第 idx 首;0=ok(按 queue_src) */
/* ---- 搜索结果独立数组(§23;g_search_mu 保护,UI/worker 经此访问)---- */
int appstate_search_count(int tab); /* tab: 0=单曲 1=歌单 */
int appstate_search_offset(int tab); /* 下次 offset(=已加载条数) */
int appstate_search_has_more(int tab); /* tab 是否还有下一页 */
int appstate_search_song(int i, ncm_song_t *out); /* 单曲条 i;0=ok */
int appstate_search_pl(int i, ncm_playlist_t *out); /* 歌单条 i;0=ok */
void appstate_search_clear(int tab); /* 清空某 tab 结果(offset=0, total=-1, more=1) */
void appstate_search_append_songs(int n, const ncm_song_t *s, int64_t total); /* 追加单曲(锁内) */
void appstate_search_append_pls(int n, const ncm_playlist_t *p, int64_t total); /* 追加歌单 */
/* ---- 搜索历史(≤NCM_SEARCH_HIST_MAX,最新在前;meow_config 持久化)---- */
int appstate_search_history_count(void);
int appstate_search_history_get(int i, char *out, size_t sz); /* 1=存在 */
void appstate_search_history_add(const char *kw); /* 去重 + 最新在前 + 截断 ≤12 */
void appstate_search_history_remove(int i); /* 删第 i 条 */
void appstate_search_history_clear(void);
/* account(app/logic/account.c,§20 会员态):account.json 落盘 + 刷新策略
* (登录后/缓存超 1 天/到期后 各查一次,不轮询——风控)。 */
void account_load(void); /* appstate_init 时读 account.json 恢复 vip 态 */
+10 -8
View File
@@ -126,7 +126,8 @@ void player_manager_next(int dir)
pthread_mutex_lock(&g_play.mu);
int c = g_play.cur_song + dir;
if (c < 0) c = 0;
if (c >= g_st.track_ids_count) c = g_st.track_ids_count - 1; /* 滑窗:边界=全量索引数 */
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
if (c >= cnt) c = cnt - 1;
if (c != g_play.cur_song) {
g_play.cur_song = c;
g_play.cmd = 0;
@@ -387,9 +388,9 @@ static void run_song(int idx)
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
* g_play.play_song——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */
ncm_song_t s;
if (appstate_song_copy(idx, &s) != 0) {
int fr = appstate_song_fetch(idx, &s);
if (fr != NCM_OK) { set_err(fr, "load failed"); return; }
if (appstate_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */
set_err(NCM_ERR_ARG, "load failed");
return;
}
if (!still_current(idx)) return; /* 物化期间已切歌/停止:不写过期快照 */
pthread_mutex_lock(&g_play.mu);
@@ -571,7 +572,7 @@ static void *player_thread(void *arg)
int st = g_play.state;
pthread_mutex_unlock(&g_play.mu);
if (cur < 0 || cur >= g_st.track_ids_count || st == NCM_PC_STOPPED) {
if (cur < 0 || cur >= appstate_queue_count() || st == NCM_PC_STOPPED) {
usleep(60 * 1000);
continue;
}
@@ -583,9 +584,10 @@ static void *player_thread(void *arg)
int st2 = g_play.state;
pthread_mutex_unlock(&g_play.mu);
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
if (cur2 >= 0 && cur2 < g_st.track_ids_count && st2 != NCM_PC_STOPPED && cur2 == cur) {
int qc = appstate_queue_count();
if (cur2 >= 0 && cur2 < qc && st2 != NCM_PC_STOPPED && cur2 == cur) {
int nx = cur + 1;
if (nx < g_st.track_ids_count) { player_manager_next(1); }
if (nx < qc) { player_manager_next(1); }
else { player_manager_stop(); }
} else {
usleep(50 * 1000);
@@ -616,7 +618,7 @@ static void *lyric_worker(void *arg)
int have = g_play.play_song_valid;
ncm_strlcpy(id, have ? g_play.play_song.id : "", sizeof id);
pthread_mutex_unlock(&g_play.mu);
if (idx < 0 || idx >= g_st.track_ids_count || !have) {
if (idx < 0 || idx >= appstate_queue_count() || !have) {
lyric_clear();
pthread_mutex_lock(&g_lyrmu);
g_lyr_song_id[0] = 0;
+76 -14
View File
@@ -97,6 +97,9 @@ static int page_id(const char *name)
if (strcmp(name, "playererr") == 0) return ACT_PLAYER; /* §18 错误态 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
if (strcmp(name, "search") == 0) return ACT_SEARCH;
if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */
if (strcmp(name, "searchpl") == 0) return ACT_SEARCH; /* §23 歌单页签 */
if (strcmp(name, "searchtap") == 0) return ACT_SEARCH; /* §23 误触抑制自检 */
return -1;
}
@@ -179,26 +182,53 @@ int main(int argc, char **argv)
g_st.cache_limit_gb = 10;
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir); /* 初始化缓存目录(「占用」行读取)*/
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
} else if (strcmp(name, "search") == 0) {
} else if (strcmp(name, "search") == 0 || strcmp(name, "searchtap") == 0) {
mock_playlists();
static const char *sn[] = { "Song One", "Song Two", "Song Three" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C" };
static const char *al[] = { "Album 1", "Album 2", "Album 3" };
ncm_strlcpy(g_st.search_query, "test", sizeof g_st.search_query);
g_st.search_count = 3;
g_st.search_tab = 0;
g_st.search_err = 0;
g_st.search_song_count = 3; /* §23:结果写独立数组,不覆写窗口 */
g_st.search_song_offset = 3;
g_st.search_song_total = 3;
g_st.search_song_more = 0;
for (int i = 0; i < 3; i++) {
snprintf(g_st.songs[i].id, sizeof g_st.songs[i].id, "%ld", 1000L + i);
snprintf(g_st.songs[i].name, sizeof g_st.songs[i].name, "%s", sn[i]);
snprintf(g_st.songs[i].art_name, sizeof g_st.songs[i].art_name, "%s", ar[i]);
snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "%s", al[i]);
g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = 0;
g_st.song_valid[i] = 1;
g_st.song_ids[i] = 1000 + i;
snprintf(g_st.search_songs[i].id, sizeof g_st.search_songs[i].id, "%ld", 1000L + i);
snprintf(g_st.search_songs[i].name, sizeof g_st.search_songs[i].name, "%s", sn[i]);
snprintf(g_st.search_songs[i].art_name, sizeof g_st.search_songs[i].art_name, "%s", ar[i]);
snprintf(g_st.search_songs[i].album_name, sizeof g_st.search_songs[i].album_name, "%s", al[i]);
g_st.search_songs[i].duration_ms = 180000 + i * 20000;
g_st.search_songs[i].fee = 0;
}
g_st.song_count = 3;
g_st.track_ids_count = 3;
} else if (strcmp(name, "searchpl") == 0) { /* §23:歌单页签结果 */
mock_playlists();
static const char *pn[] = { "Playlist Alpha", "Playlist Beta" };
ncm_strlcpy(g_st.search_query, "test", sizeof g_st.search_query);
g_st.search_tab = 1; /* 1=歌单页签 */
g_st.search_err = 0;
g_st.search_pl_count = 2;
g_st.search_pl_offset = 2;
g_st.search_pl_total = 2;
g_st.search_pl_more = 0;
for (int i = 0; i < 2; i++) {
snprintf(g_st.search_pls[i].id, sizeof g_st.search_pls[i].id, "sp%d", i);
snprintf(g_st.search_pls[i].name, sizeof g_st.search_pls[i].name, "%s", pn[i]);
g_st.search_pls[i].track_count = 30 + i * 10;
snprintf(g_st.search_pls[i].cover, sizeof g_st.search_pls[i].cover,
"http://p1.music.126.net/pl%d.jpg", i);
}
} else if (strcmp(name, "searchhist") == 0) { /* §23:空态=历史列表 */
mock_playlists();
g_st.search_query[0] = 0; /* 未输入 */
g_st.search_tab = 0;
g_st.search_err = 0;
appstate_search_history_clear(); /* 清已有,重建 3 条 */
appstate_search_history_add("\u5468\u6770\u4f26 \u6674\u5929");
appstate_search_history_add("Jay \u5553\u8868");
appstate_search_history_add("lofi beat");
appstate_search_history_add("Jay \u5553\u8868"); /* 去重 → 仍 3 条 */
} else {
mock_playlists();
}
@@ -209,7 +239,8 @@ int main(int argc, char **argv)
else if (strcmp(name, "playlist") == 0) v = playlist_view_create();
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr */
else if (strcmp(name, "settings") == 0) v = settings_view_create();
else if (strcmp(name, "search") == 0) v = search_view_create();
else if (strcmp(name, "search") == 0 || strcmp(name, "searchhist") == 0 ||
strcmp(name, "searchpl") == 0 || strcmp(name, "searchtap") == 0) v = search_view_create();
if (!v) { fprintf(stderr, "bad page\n"); return 1; }
if (strcmp(name, "grid") == 0) player_set_screen(1); /* 兼容占位(重做后恒竖屏) */
@@ -250,9 +281,40 @@ int main(int argc, char **argv)
if (page_id(name) == ACT_PLAYER) {
player_tick(meow_ui_time_ms());
}
if (strcmp(name, "search") == 0) {
if (strcmp(name, "search") == 0 || strcmp(name, "searchtap") == 0) {
search_submit(g_st.search_query); /* 宿主:应用预置搜索结果 */
}
if (strcmp(name, "searchtap") == 0) {
/* §23 误触抑制自检(真实页面 + 真实 SDK 手势管线):
* ① 拖动(|Δy|>10)后抬手 → 不点行(was_drag 抑制,g_play.cur_song 不变);
* ② 干净点击 → 播按下行(g_press_row=g_press_delta 阈值内,g_play.cur_song 变为该行)。 */
ncm_rect_t rc = ncm_content_rect();
int listy = rc.y + 168; /* field+btn+tab 布局:r_list.y = rc.y+168 */
int x = 60;
pthread_mutex_lock(&g_play.mu);
g_play.cur_song = -1; g_play.state = NCM_PC_STOPPED;
pthread_mutex_unlock(&g_play.mu);
/* ② 干净点击 row0(小移动 6px ≤10)→ 播按下行 row0(scroll=0,命中准确) */
search_touch_down(x, listy + 28);
search_touch_move(x, listy + 34);
int r0 = search_on_tap(x, listy + 34);
pthread_mutex_lock(&g_play.mu); int played = g_play.cur_song; int qsrc = g_st.queue_src; pthread_mutex_unlock(&g_play.mu);
int ok2 = (r0 == 1 && played == 0 && qsrc == NCM_QUEUE_SEARCH);
/* 回到搜索页 + 复位播放态,再做 ① 拖动抑制 */
meow_view_page_pop();
pthread_mutex_lock(&g_play.mu); g_play.cur_song = -1; pthread_mutex_unlock(&g_play.mu);
/* ① 拖动 row1(+30px >10)→ was_drag 抑制,不播(cur_song 不变) */
search_touch_down(x, listy + 56 + 28);
search_touch_move(x, listy + 56 + 28 + 30);
pthread_mutex_lock(&g_play.mu); int before = g_play.cur_song; pthread_mutex_unlock(&g_play.mu);
int r1 = search_on_tap(x, listy + 56 + 28 + 30);
pthread_mutex_lock(&g_play.mu); int after = g_play.cur_song; pthread_mutex_unlock(&g_play.mu);
int ok1 = (r1 == 1 && after == before);
printf("searchtap: clean-play=%s (cur=%d qsrc=%d) drag-suppress=%s (cur %d->%d) -> %s\n",
ok2 ? "PASS" : "FAIL", played, qsrc, ok1 ? "PASS" : "FAIL", before, after,
(ok1 && ok2) ? "PASS" : "FAIL");
if (!(ok1 && ok2)) return 1;
}
meow_ui_shutdown();
printf("dumped %s -> %s\n", name, dump);
+18 -1
View File
@@ -71,7 +71,7 @@ static int g_lyr_meta_y; /* 顶部元信息 y */
static int cur_song(void)
{
int c = player_manager_playing_song();
return (c >= 0 && c < g_st.track_ids_count) ? c : -1;
return (c >= 0 && c < appstate_queue_count()) ? c : -1; /* §23:按队列来源界 */
}
/* 当前播放曲元数据(§17):优先播放快照(窗口无关);快照未就绪时退窗口拷贝。 */
@@ -529,6 +529,7 @@ void player_on_swipe(int dir, int dist)
void player_play_song(int idx)
{
if (idx < 0 || idx >= g_st.track_ids_count) return;
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:歌单窗口队列 */
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
* §18:g_st.cover_url 只作「歌单封面兜底」。 */
@@ -542,6 +543,22 @@ void player_play_song(int idx)
meow_view_page_push(ACT_PLAYER);
}
/* 搜索单曲起播(§23):走搜索队列(search_songs[]),不覆写歌单窗口;
* 上/下一首在搜索结果集内循环,回退原歌单时窗口上下文不丢。 */
void player_play_search_song(int i)
{
if (i < 0 || i >= g_st.search_song_count) return;
g_st.queue_src = NCM_QUEUE_SEARCH;
ncm_song_t s;
if (appstate_search_song(i, &s) != 0) return;
const char *cover = s.al_pic[0] ? s.al_pic : g_st.cover_url;
player_manager_play(i);
appstate_set_playing_song(i, NULL);
cover_request(cover);
g_ppage = 0;
meow_view_page_push(ACT_PLAYER);
}
void player_pause_toggle(int on)
{
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
+31 -7
View File
@@ -44,6 +44,15 @@ static void pl_drag_reset(void)
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
/* 当前打开的「歌单」:真实歌单(active_playlist 下标)或搜索来的歌单(NCM_PL_SEARCH 哨兵,§23)。 */
static const ncm_playlist_t *pl_cur(void)
{
if (g_st.active_playlist == NCM_PL_SEARCH) return &g_st.search_pl_open;
if (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
return &g_st.playlists[g_st.active_playlist];
return NULL;
}
static int pl_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
@@ -107,8 +116,7 @@ static void pl_draw(void *ctx)
"\u266a", th->font_body, pa->red); /* ♪ */
}
cover_put();
const ncm_playlist_t *pl = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
? &g_st.playlists[g_st.active_playlist] : NULL;
const ncm_playlist_t *pl = pl_cur();
const char *nm = pl ? pl->name : "歌单名";
int nmx = r_cov.x + r_cov.w + 12;
char meta[256];
@@ -197,8 +205,9 @@ static void pl_apply(int reset_scroll)
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 (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) {
snprintf(g_title, sizeof g_title, "%s", g_st.playlists[g_st.active_playlist].name);
const ncm_playlist_t *pcur = pl_cur();
if (g_tb && pcur) {
snprintf(g_title, sizeof g_title, "%s", pcur->name);
meow_topappbar_set_title(g_tb, g_title);
}
const meow_theme *th = meow_theme_get();
@@ -292,6 +301,21 @@ void playlist_open(int idx)
if (idx < 0 || idx >= g_st.playlist_count) return;
g_st.active_playlist = idx;
g_st.playing_song = -1;
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:打开歌单 → 队列=歌单窗口 */
cover_cache_reset();
meow_view_page_push(ACT_PLAYLIST);
playlist_refresh();
}
/* 打开「搜索来的歌单」详情(§23):不污染真实歌单列表,复用本页/滑窗链路
* (active_playlist=NCM_PL_SEARCH 哨兵,appstate_load_playlist_songs_page 读 search_pl_open)。 */
void playlist_open_pl(const ncm_playlist_t *pl)
{
if (!pl || !pl->id[0]) return;
g_st.search_pl_open = *pl;
g_st.active_playlist = NCM_PL_SEARCH;
g_st.playing_song = -1;
g_st.queue_src = NCM_QUEUE_PLAYLIST;
cover_cache_reset();
meow_view_page_push(ACT_PLAYLIST);
playlist_refresh();
@@ -421,9 +445,9 @@ static void pl_page_enter(meow_view *v, void *ctx)
meow_ui_set_page_bg(th->bg);
/* §18:头块封面回填——封面槽全局单张(与播放页黑胶共享),进歌单页重新请求歌单封面
* (旧版只在 playlist_open 里 cover_cache_reset 清掉,无人再请求 → 头块永远占位 ♪)。 */
if (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count &&
g_st.playlists[g_st.active_playlist].cover[0])
cover_request(g_st.playlists[g_st.active_playlist].cover);
const ncm_playlist_t *pcur = pl_cur();
if (pcur && pcur->cover[0])
cover_request(pcur->cover);
playlist_refresh();
}
+295 -69
View File
@@ -1,11 +1,20 @@
/* ui/search.c — 搜索页(ACT_SEARCH,顶栏搜索按钮进入)。
*
* 独立页面:搜索输入框(SDK TextField,接 IME §5.1,仅英文/数字)+ 红色「搜索」按钮 +
* 结果列表(点击可播,复用 g_st.songs[] 起点:播放/上下一首均对结果集生效)。
* 搜索结果网络走后台 worker(search_worker),UI 不卡。
* §23(REPORT):
* 1. 搜索历史:进页自动弹键盘(meow_textfield_activate);历史本地持久化(meow_config,
* 最近 ≤12 条、去重、最新在前);未输入时显示历史列表(点=直接搜、左滑删单条、清空全部)。
* 2. 结果分类「单曲/歌单」:顶部小页签(单曲 type=1 / 歌单 type=1000);两页签各自独立
* 结果集与滚动位置(g_scroll_song/g_scroll_pl);歌单行点击=playlist_open_pl 进详情。
* 3. 加载更多:触底分页(offset 累加,NCM_SEARCH_PAGE=30/页,cloudsearch 同参数);
* 每类各记各的 offset(appstate_search_offset 回填)。
* 4. 修 search_on_tap 误触(NCM_UI_REVIEW §3-2):套用 g_press_row + g_press_delta>10 抑制
* (对齐 home/playlist §13.3)。
* 5. 结果不再覆写歌单上下文:单曲结果写 g_st.search_songs[](独立数组,appstate 持有),
* 播放搜索单曲走 player_play_search_song(建立「搜索队列」上下文,queue_src=SEARCH),
* 原歌单窗口(g_st.songs[]/track_ids_count)不被破坏。
*
* 风控纪律(用户指定):**搜索请求只在用户确认时发**——用户点「搜索」按钮才发起 cloudsearch,
* 进入页面不自动搜索;IME 输入仅本地编辑,不逐键发请求。
* 风控纪律(用户指定):**真实请求只在用户点「搜索」按钮/切页签/翻页时发**——进页不自动搜索,
* IME 输入仅本地编辑,不逐键发请求。调试/测试全部离线 stub(g_host_mock)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
@@ -16,10 +25,14 @@
typedef struct { meow_view *view; int active; } search_ctrl;
static search_ctrl g_sc;
/* 视图模式 */
enum { SR_HIST = 0, SR_RES, SR_EMPTY };
static meow_widget *g_tb, *g_field, *g_list;
static ncm_rect_t r_cont, r_list, r_btn;
static int g_empty_id, g_footer_id, g_btn_id;
static ncm_rect_t r_cont, r_list, r_btn, r_tabs[2];
static int g_empty_id, g_footer_id, g_btn_id, g_tabbar_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_scroll_song, g_scroll_pl; /* 两页签各自滚动位置(§23)*/
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_thr;
@@ -27,53 +40,146 @@ static int g_thr_on;
static volatile int g_gen;
static int g_acked;
static volatile int g_loading;
static char g_query[256];
static char g_query[256]; /* worker 所搜关键词(不可含引用问题,见 core 转义) */
static int g_tab; /* 0=单曲 1=歌单 */
static int g_searched; /* 1=已提交过搜索(区分「输入后未搜」空态文案)*/
static int g_req_tab, g_req_offset, g_req_replace;
static volatile int g_more_err; /* 翻页失败(snack 一次)*/
static void *search_worker(void *arg); /* 前向声明(sr_maybe_load_more 先引用) */
static void sr_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
/* 当前视图(§23 历史/结果/空态)。由 g_st.search_query 是否为空 + 各页签结果数推导。 */
static int sr_view(void)
{
if (g_st.search_query[0] == 0)
return (appstate_search_history_count() > 0) ? SR_HIST : SR_EMPTY;
if (appstate_search_count(g_tab) > 0) return SR_RES;
return SR_EMPTY;
}
static int sr_count(void)
{
int v = sr_view();
if (v == SR_HIST) return appstate_search_history_count();
if (v == SR_RES) return appstate_search_count(g_tab);
return 0;
}
static void sr_save_scroll(void)
{
if (g_tab == 0) g_scroll_song = meow_listview_scroll(g_list);
else g_scroll_pl = meow_listview_scroll(g_list);
}
static void sr_restore_scroll(void)
{
meow_listview_set_scroll(g_list, (g_tab == 0) ? g_scroll_song : g_scroll_pl);
}
static int sr_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_st.search_count) return 0;
ncm_song_t *s = &g_st.songs[idx];
static char sub[256];
if (s->art_name[0] && s->album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s->art_name, s->album_name);
else if (s->art_name[0]) snprintf(sub, sizeof sub, "%s", s->art_name);
else snprintf(sub, sizeof sub, "%s", s->album_name);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s->name;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
return 1;
int v = sr_view();
if (v == SR_HIST) {
char q[NCM_SEARCH_HIST_LEN];
if (!appstate_search_history_get(idx, q, sizeof q)) return 0;
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = q;
out->subtitle = "";
out->right = MEOW_LIST_RIGHT_NONE;
out->on = 0; out->enabled = 1;
return 1;
}
if (v == SR_RES) {
if (g_tab == 0) {
static ncm_song_t s;
if (appstate_search_song(idx, &s) != 0) return 0;
static char sub[256];
if (s.art_name[0] && s.album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s.art_name, s.album_name);
else if (s.art_name[0]) snprintf(sub, sizeof sub, "%s", s.art_name);
else snprintf(sub, sizeof sub, "%s", s.album_name);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s.name;
out->subtitle = sub;
} else {
static ncm_playlist_t p;
if (appstate_search_pl(idx, &p) != 0) return 0;
static char sub[64];
snprintf(sub, sizeof sub, "%d 首", p.track_count);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = p.name;
out->subtitle = sub;
}
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
return 1;
}
return 0; /* SR_EMPTY:无行 */
}
static void sr_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
if (g_st.search_count != 0 && g_st.search_err == 0) return;
if (sr_view() != SR_EMPTY) return; /* 历史/结果态有行,空态不画 */
if (g_loading) { ncm_draw_empty(r_list, "搜索中", "正在搜索..."); return; }
if (g_st.search_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "登录已过期", "请重新登录");
else if (g_st.search_err) ncm_draw_empty(r_list, "搜索失败", "点「搜索」重试");
else if (!g_query[0]) ncm_draw_empty(r_list, "输入关键词", "输入后点「搜索」");
else ncm_draw_empty(r_list, "没有结果", "未找到相关歌曲");
else if (!g_st.search_query[0]) ncm_draw_empty(r_list, "输入关键词", "输入后点「搜索」");
else if (!g_searched) ncm_draw_empty(r_list, "输入后点「搜索」", "或点历史记录");
else ncm_draw_empty(r_list, "没有结果",
g_tab == 0 ? "未找到相关歌曲" : "未找到相关歌单");
}
static void sr_draw_footer(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
if (g_st.search_err != 0 || g_st.search_count == 0) return;
char foot[64];
snprintf(foot, sizeof foot, "找到 %d 个结果", g_st.search_count);
ncm_draw_footer(r_cont, foot);
int v = sr_view();
if (v == SR_HIST) { /* 历史:右缘「清空全部」按钮 */
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int cw = meow_ui_text_width("清空全部", th->font_body);
int fx = r_cont.x + r_cont.w - 16 - cw;
int fy = r_cont.y + r_cont.h - APP_FOOTER_H + (APP_FOOTER_H - th->font_body) / 2;
meow_ui_text(fx, fy, "清空全部", th->font_body, pa->red);
return;
}
if (v == SR_RES) {
int n = appstate_search_count(g_tab);
if (n > 0) {
char foot[96];
snprintf(foot, sizeof foot, "%s %d %s%s",
g_tab == 0 ? "单曲" : "歌单", n,
g_tab == 0 ? "个结果" : "个歌单",
appstate_search_has_more(g_tab) ? " · 加载更多" : "");
ncm_draw_footer(r_cont, foot);
}
}
}
static void sr_draw_tabs(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
if (sr_view() != SR_RES) return; /* 页签只在结果态显示 */
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
static const char *lab[2] = { "单曲", "歌单" };
for (int i = 0; i < 2; i++) {
int on = (g_tab == i);
uint32_t col = on ? pa->red : th->onSurfaceVariant;
meow_ui_text_center(r_tabs[i].x + r_tabs[i].w / 2, r_tabs[i].y + (r_tabs[i].h - th->font_body) / 2,
lab[i], th->font_body, col);
if (on) meow_ui_fill_rect(r_tabs[i].x, r_tabs[i].y + r_tabs[i].h - 3, r_tabs[i].w, 3, pa->red);
}
}
/* 「搜索」红色确认钮(SDK meow_btn FILLED 是 MD3 蓝;App 自绘品牌红)。 */
static void sr_draw_btn(void *ctx)
{
(void)ctx;
@@ -84,35 +190,61 @@ static void sr_draw_btn(void *ctx)
static void sr_apply(void)
{
if (g_list) {
meow_listview_set_count(g_list, g_st.search_count);
meow_listview_set_scroll(g_list, 0);
meow_listview_set_sel(g_list, -1);
}
if (g_list) meow_listview_set_count(g_list, sr_count());
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
/* 后台 worker:唯一网络调用(cloudsearch),UI 不卡。
* §17:结果集(≤NCM_SEARCH_MAX,远小于窗口)经 appstate_adopt_songs 收编为窗口 base=0
* 播放上下文(song_ids/track_ids_count 同步),播放/上下一首对结果集生效。 */
/* 触底翻页(§23):RES 态 + 还有下一页 + 未在载 → 按当前页签的 offset 拉下一页。
* 仅由用户滚动动作触发(风控:翻页=用户动作)。 */
static void sr_maybe_load_more(void)
{
if (sr_view() != SR_RES) return;
if (g_thr_on || g_loading || g_st.search_err != 0) return;
int n = appstate_search_count(g_tab);
int cap = (g_tab == 0) ? NCM_SEARCH_SONG_MAX : NCM_SEARCH_PL_MAX;
if (n <= 0 || n >= cap || !appstate_search_has_more(g_tab)) return;
int item = APP_ROW_H;
int scroll = meow_listview_scroll(g_list);
if (scroll + r_list.h < n * item - item * 2) return; /* 未近底 */
g_req_tab = g_tab; g_req_offset = n; g_req_replace = 0;
/* 单飞:无 worker 才发(search_worker 尾部解锁) */
g_thr_on = 1; g_loading = 1; g_more_err = 0;
if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) {
g_thr_on = 0; g_loading = 0; g_more_err = 1;
}
}
static void *search_worker(void *arg)
{
(void)arg;
int tab = g_req_tab, off = g_req_offset, repl = g_req_replace;
const char *kw = g_query;
int n = 0;
int rc = ncm_api_cloudsearch(g_st.ctx.device_id, g_st.ctx.cookie, kw,
g_st.songs, NCM_SEARCH_MAX, &n);
pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) {
appstate_adopt_songs(n); /* 收编 songs[0..n) 为窗口(写侧在锁内) */
g_st.search_count = n;
g_st.search_err = 0;
int rc = NCM_ERR_IO, n = 0;
int64_t total = -1;
if (tab == 0) {
static ncm_song_t sbuf[NCM_SEARCH_PAGE];
rc = ncm_api_cloudsearch_song(g_st.ctx.device_id, g_st.ctx.cookie, kw,
off, NCM_SEARCH_PAGE, sbuf, NCM_SEARCH_PAGE, &n, &total);
pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) {
if (repl) appstate_search_clear(0);
appstate_search_append_songs(n, sbuf, total);
g_st.search_err = 0; g_more_err = 0;
} else if (repl) g_st.search_err = rc;
else g_more_err = 1;
} else {
appstate_adopt_songs(0);
g_st.search_count = 0;
g_st.search_err = rc;
static ncm_playlist_t pbuf[NCM_SEARCH_PAGE];
rc = ncm_api_cloudsearch_playlist(g_st.ctx.device_id, g_st.ctx.cookie, kw,
off, NCM_SEARCH_PAGE, pbuf, NCM_SEARCH_PAGE, &n, &total);
pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) {
if (repl) appstate_search_clear(1);
appstate_search_append_pls(n, pbuf, total);
g_st.search_err = 0; g_more_err = 0;
} else if (repl) g_st.search_err = rc;
else g_more_err = 1;
}
g_loading = 0;
g_gen++;
@@ -125,13 +257,18 @@ void search_submit(const char *kw)
if (!kw || !kw[0]) { ncm_snack("请输入关键词"); return; }
ncm_strlcpy(g_query, kw, sizeof g_query);
ncm_strlcpy(g_st.search_query, kw, sizeof g_st.search_query);
if (g_host_mock) { /* 宿主/测试:预置结果,同步 */
appstate_search_history_add(kw); /* §23:历史(去重+最新在前+≤12)*/
g_searched = 1;
g_scroll_song = g_scroll_pl = 0;
if (g_list) { meow_listview_set_scroll(g_list, 0); }
appstate_search_clear(g_tab ^ 1); /* 新查询:清非当前页签,切过去时懒加载新词 */
if (g_host_mock) { /* 宿主/测试:预置结果,同步 */
g_loading = 0; g_st.search_err = 0; sr_apply(); return;
}
if (g_thr_on) return; /* 已有 worker 在跑 */
if (g_thr_on) return; /* 单飞:已有 worker 在跑 */
g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1;
g_st.search_err = 0;
g_st.search_count = 0;
g_loading = 1;
g_loading = 1; g_more_err = 0;
g_thr_on = 1;
if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) {
g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO;
@@ -139,6 +276,26 @@ void search_submit(const char *kw)
sr_apply();
}
/* 切页签:保存旧滚动 → 换页签 → 恢复该页签滚动;空结果页签懒加载(用户切页签动作)。 */
static void sr_set_tab(int t)
{
if (t == g_tab) return;
sr_save_scroll();
g_tab = t;
g_st.search_tab = t; /* 应用态同步(重启/重进保持) */
sr_restore_scroll();
sr_apply();
if (g_st.search_query[0] && !g_thr_on && !g_loading &&
g_st.search_err == 0 && appstate_search_count(g_tab) == 0) {
g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1;
g_loading = 1; g_more_err = 0;
g_thr_on = 1;
if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) {
g_thr_on = 0; g_loading = 0;
}
}
}
void search_tick(uint64_t now)
{
(void)now;
@@ -148,6 +305,7 @@ void search_tick(uint64_t now)
g_acked = g_gen;
g_thr_on = 0;
g_loading = 0;
if (g_more_err) { ncm_snack("加载失败"); g_more_err = 0; }
sr_apply();
}
@@ -170,24 +328,77 @@ int search_on_tap(int x, int y)
search_submit(t ? t : "");
return 1;
}
if (g_field && meow_widget_hit(g_field, x, y)) { /* 唤 IME */
int v = sr_view();
if (v == SR_RES) { /* 页签切换 */
for (int i = 0; i < 2; i++)
if (x >= r_tabs[i].x && x < r_tabs[i].x + r_tabs[i].w &&
y >= r_tabs[i].y && y < r_tabs[i].y + r_tabs[i].h) { sr_set_tab(i); return 1; }
}
if (v == SR_HIST && /* 历史「清空全部」按钮 */
y >= r_cont.y + r_cont.h - APP_FOOTER_H && y < r_cont.y + r_cont.h &&
x >= r_cont.x + r_cont.w - 16 - 70) {
sr_drag_reset();
appstate_search_history_clear();
sr_apply();
return 1;
}
if (g_field && meow_widget_hit(g_field, x, y)) { /* 唤 IME */
meow_textfield_activate(g_field);
return 1;
}
if (g_st.search_count > 0 && g_st.search_err == 0 && meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row < 0) { sr_drag_reset(); return 1; }
if (meow_widget_hit(g_list, x, y)) {
if (v == SR_EMPTY) { /* 失败态:点内容区重试(风控:用户动作) */
if (g_st.search_err && g_st.search_query[0]) {
sr_drag_reset(); g_st.search_err = 0;
if (!g_thr_on) {
g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1;
g_loading = 1;
g_thr_on = 1;
if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) {
g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO;
}
}
return 1;
}
sr_drag_reset();
return 1;
}
/* 误触修复(§13.3/§23):按下行 + 拖动>10 抑制(对齐 home/playlist) */
int was_drag = (g_press_delta > 10);
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
sr_drag_reset();
player_play_song(row); /* 结果集即 g_st.songs[],点击可播 */
if (row < 0 || was_drag) return 1;
if (v == SR_HIST) {
char q[NCM_SEARCH_HIST_LEN];
if (appstate_search_history_get(row, q, sizeof q)) search_submit(q);
return 1;
}
if (g_tab == 0) player_play_search_song(row); /* 单曲:搜索队列起播(§23)*/
else {
ncm_playlist_t p;
if (appstate_search_pl(row, &p) == 0) playlist_open_pl(&p); /* 歌单:进详情 */
}
return 1;
}
return 0;
}
int search_swipe_left(void)
{
if (sr_view() != SR_HIST) return 0; /* 仅历史模式左滑删单条 */
int row = g_press_row; /* touch_down 已记按下行 */
sr_drag_reset();
if (row >= 0) {
appstate_search_history_remove(row);
sr_apply();
return 1; /* 已消费:不触返回 */
}
return 0;
}
void search_touch_down(int x, int y)
{
sr_drag_reset();
if (g_st.search_count == 0 || g_st.search_err != 0) return;
if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row >= 0) g_press_row = row;
@@ -209,19 +420,23 @@ void search_touch_move(int x, int y)
if (dy == 0) return;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, -dy);
if (meow_listview_scroll(g_list) != before)
if (meow_listview_scroll(g_list) != before) {
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
sr_maybe_load_more();
}
}
void search_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_st.search_count == 0 || g_st.search_err != 0) return;
if (sr_view() != SR_RES) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before)
if (meow_listview_scroll(g_list) != before) {
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
sr_maybe_load_more();
}
}
static void sr_page_create(meow_view *v, void *ctx)
@@ -231,12 +446,14 @@ static void sr_page_create(meow_view *v, void *ctx)
r_cont = ncm_content_rect();
ncm_rect_t field = NR(16, r_cont.y + 12, r_cont.w - 32, 48);
r_btn = NR(field.x, field.y + field.h + 12, field.w, 44);
r_list = NR(16, r_btn.y + r_btn.h + 12, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_btn.y + r_btn.h + 12));
int by = r_btn.y + r_btn.h + 8;
r_tabs[0] = NR(r_btn.x, by, r_btn.w / 2 - 6, 36);
r_tabs[1] = NR(r_btn.x + r_btn.w / 2 + 6, by, r_btn.w / 2 - 6, 36);
r_list = NR(16, by + 36 + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (by + 36 + 8));
if (r_list.h < 0) r_list.h = 0;
meow_ui_activity_push(ACT_SEARCH); /* owner=ACT_SEARCH */
g_tb = ncm_topbar_create("搜索", 1, NULL);
if (!g_st.search_query[0]) ncm_strlcpy(g_st.search_query, "", sizeof g_st.search_query);
/* 中文搜索:IME 切 MEOW_IME_PINYIN(拼音候选上屏 UTF-8 到 g_st.search_query,经 on_change 实时回写;
* 键盘「拼/英」键可切回 ASCII 输英文,「中」热区回拼音——英文零回归)。 */
g_field = meow_textfield_create(field.x, field.y, field.w, field.h,
@@ -249,10 +466,14 @@ static void sr_page_create(meow_view *v, void *ctx)
meow_widget_set_z(g_list, 10);
g_btn_id = meow_draw_custom_register(60, r_btn.x, r_btn.y, r_btn.w, r_btn.h,
sr_draw_btn, NULL);
g_tabbar_id = meow_draw_custom_register(40, r_tabs[0].x, r_tabs[0].y,
r_btn.w, r_tabs[0].h, sr_draw_tabs, NULL);
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
sr_draw_empty, NULL);
g_footer_id = meow_draw_custom_register(1, r_cont.x, r_cont.y + r_cont.h - APP_FOOTER_H,
r_cont.w, APP_FOOTER_H, sr_draw_footer, NULL);
/* 页签初始 = 应用态记录值(宿主 dump 可指定 search_tab);越界回落 0。 */
g_tab = (g_st.search_tab == 0 || g_st.search_tab == 1) ? g_st.search_tab : 0;
sr_drag_reset();
}
@@ -260,9 +481,13 @@ static void sr_page_enter(meow_view *v, void *ctx)
{
(void)v;
((search_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
sr_apply(); /* 进页即按当前模式设列表 count(历史/结果/空态),避免首屏空列表 */
#ifndef MEOW_UI_HOST_TEST
/* §23:进页自动弹键盘(字段为空时;返回浏览结果不打断)。宿主离屏为阻塞式模态 IME,跳过。 */
if (g_field && g_st.search_query[0] == 0) meow_textfield_activate(g_field);
#else
(void)0;
#endif
}
static void sr_page_leave(meow_view *v, void *ctx)
@@ -274,9 +499,10 @@ static void sr_page_leave(meow_view *v, void *ctx)
static void sr_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_btn_id) { meow_draw_unregister(g_btn_id); g_btn_id = 0; }
if (g_empty_id){ meow_draw_unregister(g_empty_id); g_empty_id = 0; }
if (g_footer_id){meow_draw_unregister(g_footer_id);g_footer_id = 0; }
if (g_btn_id) { meow_draw_unregister(g_btn_id); g_btn_id = 0; }
if (g_tabbar_id){ meow_draw_unregister(g_tabbar_id);g_tabbar_id = 0; }
if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; }
if (g_footer_id){ meow_draw_unregister(g_footer_id);g_footer_id = 0; }
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_field){ meow_widget_free(g_field); g_field = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
+212
View File
@@ -0,0 +1,212 @@
/* bench/search_sim.c — 搜索增强(REPORT §23)数据层离线回归(0 网络请求)。
*
* 直接驱动 App 真实函数(app/logic/appstate.c):搜索历史(去重/≤12/最新在前)、
* 单曲/歌单两类独立结果集、翻页 offset 累加与 has_more、以及「搜索结果播放队列」
* 上下文(queue_src=SEARCH 读 search_songs[],不覆写歌单窗口)。
* meow_config_* 用内存 stub(历史持久化语义)。误触抑制(g_press_row+g_press_delta>10)
* 属 UI 手势层,另在宿主 test_ncm_app.c 的 searchtap 自检中验证(真实页面+真实 SDK)。
*
* 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -c bench/search_sim.c -o /tmp/ss.o
* $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -c app/logic/appstate.c -o /tmp/ss_app.o
* $CC_HOST -O2 -Wl,--gc-sections -o search_sim /tmp/ss.o /tmp/ss_app.o -lpthread
* 运行:./search_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
/* appstate.c 依赖的全局(app.c 提供;本 bench 只测搜索/历史路径) */
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;
size_t ncm_strlcpy(char *dst, const char *src, size_t sz);
/* appstate_queue_song 的 PLAYLIST 分支引用 song_fetch→songmeta/ncm_api_song_detail;
* 本 bench 只测 SEARCH 路径,这些分支不执行,提供 No-op 桩满足链接。 */
int songmeta_load(const char *cd, const char *id, ncm_song_t *out) { (void)cd;(void)id;(void)out; return NCM_ERR_IO; }
int songmeta_save(const char *cd, const char *id, const ncm_song_t *s){ (void)cd;(void)id;(void)s; return NCM_ERR_IO; }
int ncm_api_song_detail(const char *d, const char *c, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n) {
(void)d;(void)c;(void)ids;(void)n;(void)out;(void)max; if (out_n) *out_n = 0; return NCM_ERR_IO;
}
/* meow_config 内存 stub:只存 ncm.search_history 单键,模拟持久化(历史读回)。 */
static char s_cfg[4096];
static int s_cfg_valid = 0;
int meow_config_set(const char *key, const char *val)
{
(void)key;
if (!val) return MEOW_CFG_ERR_INVAL;
size_t n = strlen(val);
if (n >= sizeof s_cfg) n = sizeof s_cfg - 1;
memcpy(s_cfg, val, n); s_cfg[n] = 0; s_cfg_valid = 1;
return MEOW_CFG_OK;
}
int meow_config_get(const char *key, char *out, int outsz)
{
(void)key;
if (!s_cfg_valid || outsz <= 0) return MEOW_CFG_ERR_NOENT;
size_t n = strlen(s_cfg);
if (n >= (size_t)outsz) n = (size_t)outsz - 1;
memcpy(out, s_cfg, n); out[n] = 0;
return (s_cfg[0] || n) ? MEOW_CFG_OK : MEOW_CFG_ERR_NOENT;
}
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;
}
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)
static ncm_song_t mk_song(int i)
{
ncm_song_t s; memset(&s, 0, sizeof s);
snprintf(s.id, sizeof s.id, "%d", 100000 + i);
snprintf(s.name, sizeof s.name, "Search Song %d", i);
snprintf(s.art_name, sizeof s.art_name, "Artist %d", i);
snprintf(s.album_name, sizeof s.album_name, "Album %d", i);
s.duration_ms = 180000 + i * 1000;
return s;
}
static ncm_playlist_t mk_pl(int i)
{
ncm_playlist_t p; memset(&p, 0, sizeof p);
snprintf(p.id, sizeof p.id, "pl%d", 200000 + i);
snprintf(p.name, sizeof p.name, "Playlist %d", i);
p.track_count = 10 + i;
return p;
}
int main(void)
{
/* ===== 搜索历史:去重 + ≤12 + 最新在前 ===== */
printf("[1] search history dedup/cap/newest-first\n");
appstate_search_history_clear();
appstate_search_history_add("alpha");
appstate_search_history_add("beta");
appstate_search_history_add("alpha"); /* 去重:alpha 前置 */
CHECK(appstate_search_history_count() == 2, "dedup keeps 2 (alpha, beta)");
char b1[64], b2[64];
appstate_search_history_get(0, b1, sizeof b1);
appstate_search_history_get(1, b2, sizeof b2);
CHECK(strcmp(b1, "alpha") == 0 && strcmp(b2, "beta") == 0, "newest first after re-add");
for (int i = 0; i < NCM_SEARCH_HIST_MAX + 5; i++) { /* 超 12 → 截断 */
char k[32]; snprintf(k, sizeof k, "kw%d", i);
appstate_search_history_add(k);
}
CHECK(appstate_search_history_count() == NCM_SEARCH_HIST_MAX, "cap at %d", NCM_SEARCH_HIST_MAX);
char top[64]; appstate_search_history_get(0, top, sizeof top);
CHECK(strcmp(top, "kw16") == 0, "newest kw16 at front"); /* 15+2... kw16 为最后 add */
char mid[64]; appstate_search_history_get(8, mid, sizeof mid);
CHECK(strcmp(mid, "kw8") == 0, "kw8 at index 8 (truncation preserves order)");
appstate_search_history_remove(0);
CHECK(appstate_search_history_count() == NCM_SEARCH_HIST_MAX - 1, "remove index 0 shrinks");
char top2[64]; appstate_search_history_get(0, top2, sizeof top2);
CHECK(strcmp(top2, "kw15") == 0, "remove shifts next to front");
/* 持久化读回(stub meow_config 已落盘) */
appstate_search_history_clear();
CHECK(appstate_search_history_count() == 0, "clear to 0");
/* ===== 单曲/歌单两类独立结果集 ===== */
printf("[2] independent song/playlist result sets\n");
g_st.queue_src = NCM_QUEUE_PLAYLIST;
appstate_search_clear(0);
appstate_search_clear(1);
ncm_song_t s0[4];
for (int i = 0; i < 4; i++) s0[i] = mk_song(i);
appstate_search_append_songs(4, s0, 100);
CHECK(appstate_search_count(0) == 4, "song set count=4");
CHECK(appstate_search_count(1) == 0, "playlist set independent (=0)");
ncm_playlist_t p0[2];
for (int i = 0; i < 2; i++) p0[i] = mk_pl(i);
appstate_search_append_pls(2, p0, 50);
CHECK(appstate_search_count(1) == 2, "playlist set count=2");
CHECK(appstate_search_count(0) == 4, "song set unchanged after playlist append");
ncm_song_t cpy;
CHECK(appstate_search_song(1, &cpy) == 0 && strcmp(cpy.name, "Search Song 1") == 0, "copy song #1");
ncm_playlist_t pcpy;
CHECK(appstate_search_pl(0, &pcpy) == 0 && strcmp(pcpy.name, "Playlist 0") == 0, "copy playlist #0");
appstate_search_clear(0);
CHECK(appstate_search_count(0) == 0 && appstate_search_count(1) == 2, "clear song set leaves playlist");
/* ===== 翻页 offset 累加 / has_more ===== */
printf("[3] pagination offset accumulation / has_more\n");
appstate_search_clear(0);
CHECK(appstate_search_has_more(0) == 1 && appstate_search_offset(0) == 0, "clear resets offset=0 more=1");
ncm_song_t page[30];
for (int i = 0; i < NCM_SEARCH_PAGE; i++) page[i] = mk_song(100 + i);
appstate_search_append_songs(NCM_SEARCH_PAGE, page, 300); /* page1: total 300 */
CHECK(appstate_search_count(0) == NCM_SEARCH_PAGE, "page1 count=30");
CHECK(appstate_search_offset(0) == NCM_SEARCH_PAGE, "offset=30 after page1");
CHECK(appstate_search_has_more(0) == 1, "more=1 (30<300)");
appstate_search_append_songs(NCM_SEARCH_PAGE, page, 300); /* page2 */
CHECK(appstate_search_count(0) == 2 * NCM_SEARCH_PAGE && appstate_search_offset(0) == 60, "offset=60 after page2");
ncm_song_t tail[30];
for (int i = 0; i < NCM_SEARCH_PAGE; i++) tail[i] = mk_song(200 + i);
appstate_search_append_songs(NCM_SEARCH_PAGE, tail, 240); /* page3 → 90/240 */
CHECK(appstate_search_has_more(0) == 1 && appstate_search_offset(0) == 90, "more=1 at 90/240");
for (int i = 0; i < 5; i++) appstate_search_append_songs(30, tail, 240); /* +150 → 240/240 */
CHECK(appstate_search_count(0) == 240 && appstate_search_offset(0) == 240, "reached total 240");
CHECK(appstate_search_has_more(0) == 0, "more=0 at total");
/* 总量 > 数组上限:clamp 到 NCM_SEARCH_SONG_MAX,has_more 仍按服务端总数反映 */
appstate_search_clear(0);
for (int i = 0; i < NCM_SEARCH_SONG_MAX / NCM_SEARCH_PAGE + 3; i++)
appstate_search_append_songs(NCM_SEARCH_PAGE, page, 2000);
CHECK(appstate_search_count(0) == NCM_SEARCH_SONG_MAX, "clamp at NCM_SEARCH_SONG_MAX");
CHECK(appstate_search_has_more(0) == 1, "more=1 while server total (2000) > cap");
/* 服务端 total 恰好等于 cap → 加满后 more=0 */
appstate_search_clear(0);
for (int i = 0; i < NCM_SEARCH_SONG_MAX / NCM_SEARCH_PAGE + 1; i++)
appstate_search_append_songs(NCM_SEARCH_PAGE, page, NCM_SEARCH_SONG_MAX);
CHECK(appstate_search_count(0) == NCM_SEARCH_SONG_MAX && appstate_search_has_more(0) == 0,
"more=0 when server total == cap (clamped full)");
/* 无 total(-1)时回落「返回条数==页大小」判 more */
appstate_search_clear(1);
ncm_playlist_t pp[NCM_SEARCH_PAGE];
for (int i = 0; i < NCM_SEARCH_PAGE; i++) pp[i] = mk_pl(i);
appstate_search_append_pls(NCM_SEARCH_PAGE, pp, -1);
CHECK(appstate_search_has_more(1) == 1, "unknown total + full page => more=1");
appstate_search_append_pls(10, pp, -1);
CHECK(appstate_search_has_more(1) == 0, "unknown total + short page => more=0");
/* ===== 搜索结果播放队列上下文(§23:不覆写歌单窗口) ===== */
printf("[4] search queue context (queue_src=SEARCH)\n");
appstate_search_clear(0);
for (int i = 0; i < 3; i++) { ncm_song_t t = mk_song(300 + i); appstate_search_append_songs(1, &t, 3); }
/* 歌单窗口预置(模拟「打开的歌单还没被打扰」) */
g_st.track_ids_count = 7; g_st.song_count = 7; g_st.window_base = 0;
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
for (int i = 0; i < 7; i++) { g_st.song_ids[i] = 50 + i; g_st.song_valid[i] = 1; }
appstate_set_queue(NCM_QUEUE_PLAYLIST);
CHECK(appstate_queue_count() == 7, "queue=playlist window count(7)");
appstate_set_queue(NCM_QUEUE_SEARCH);
CHECK(appstate_queue_count() == 3, "queue=search count(3)");
ncm_song_t qs;
CHECK(appstate_queue_song(2, &qs) == 0 && strcmp(qs.name, "Search Song 302") == 0, "queue_song from search set");
/* 关键:搜索起播不改写歌单窗口(track_ids_count/song_ids 原样) */
CHECK(g_st.track_ids_count == 7 && g_st.song_valid[3] == 1 && g_st.song_ids[3] == 53,
"playlist window untouched by search queue");
appstate_set_queue(NCM_QUEUE_PLAYLIST);
CHECK(appstate_queue_count() == 7, "queue back to playlist count(7)");
printf("\n%s\n", g_fail ? "search_sim FAIL" : "search_sim ALL PASS");
return g_fail ? 1 : 0;
}
+75 -13
View File
@@ -456,14 +456,11 @@ int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
return NCM_OK;
}
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
ncm_song_t *out, int max, int *out_n) {
if (!out_n) return NCM_ERR_ARG;
*out_n = 0;
if (!keyword || !keyword[0] || !out || max <= 0) return NCM_ERR_ARG;
/* /api/cloudsearch/pc:s=关键词 type=1(单曲) offset/limit 翻页。v1 只取前 max 条。
* 关键词转义(\" 与 \\),防止 IME 输入引号/反斜杠拼出坏 JSON。 */
/* /api/cloudsearch/pc 公共 POST(type=单曲/歌单/专辑 共用):s=关键词 type/offset/limit/total。
* 关键词转义(\" 与 \\),防止 IME 输入引号/反斜杠拼出坏 JSON。 */
static int cloudsearch_post(const char *device_id, const char *cookie, const char *keyword,
int type, int offset, int limit, api_json_t *c)
{
char esc[256], json[512];
size_t ei = 0;
for (size_t si = 0; keyword[si] && ei < sizeof esc - 3; si++) {
@@ -472,20 +469,85 @@ int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *k
esc[ei++] = ch;
}
esc[ei] = 0;
snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":1,\"offset\":0,\"limit\":%d,\"total\":true}",
esc, max);
snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":%d,\"offset\":%d,\"limit\":%d,\"total\":true}",
esc, type, offset > 0 ? offset : 0, limit > 0 ? limit : 30);
return api_post(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI,
NCM_RESP_MAX, c);
}
/* 解析一处「歌单对象数组」(cloudsearch type=1000)。id 可为数字/字符串(均文本拷贝)。 */
static int parse_playlists_array(const ncm_json_t *j, int arr, ncm_playlist_t *out, int max, int *out_n)
{
*out_n = 0;
if (arr < 0 || ncm_json_type(j, arr) != JSMN_ARRAY) return NCM_ERR_NOTFOUND;
int cnt = ncm_json_arr_len(j, arr);
if (cnt > max) cnt = max;
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
for (int i = 0; i < cnt; i++) {
int e = ncm_json_arr_get(j, arr, i);
if (e < 0) break;
obj_str(j, e, "id", out[i].id, sizeof out[i].id);
obj_str(j, e, "name", out[i].name, sizeof out[i].name);
obj_str(j, e, "coverImgUrl", out[i].cover, sizeof out[i].cover);
out[i].track_count = (int)obj_int(j, e, "trackCount");
out[i].is_collected = 0;
}
*out_n = cnt;
return NCM_OK;
}
/* 搜索结果计数(cloudsearch result.songCount / result.playlistCount;缺省 → *out_total=-1 未知)。 */
static void cloudsearch_count(const ncm_json_t *j, const char *path, int64_t *out_total)
{
if (!out_total) return;
*out_total = -1;
int t = ncm_json_path(j, path);
int rc = NCM_ERR_NOTFOUND;
int64_t v = ncm_json_get_i64_at(j, t, &rc);
if (rc == NCM_OK) *out_total = v;
}
int ncm_api_cloudsearch_song(const char *device_id, const char *cookie, const char *keyword,
int offset, int limit, ncm_song_t *out, int max, int *out_n,
int64_t *out_total) {
if (!out_n) return NCM_ERR_ARG;
*out_n = 0;
if (out_total) *out_total = -1;
if (!keyword || !keyword[0] || !out || max <= 0) return NCM_ERR_ARG;
api_json_t c; memset(&c, 0, sizeof c);
int rc = api_post(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI,
NCM_RESP_MAX, &c);
int rc = cloudsearch_post(device_id, cookie, keyword, 1, offset, limit, &c);
if (rc != NCM_OK) return rc;
cloudsearch_count(&c.j, "result.songCount", out_total);
int arr = ncm_json_path(&c.j, "result.songs");
int rc2 = parse_songs_array(&c.j, arr, out, max, out_n);
api_free(&c);
return rc2;
}
int ncm_api_cloudsearch_playlist(const char *device_id, const char *cookie, const char *keyword,
int offset, int limit, ncm_playlist_t *out, int max, int *out_n,
int64_t *out_total) {
if (!out_n) return NCM_ERR_ARG;
*out_n = 0;
if (out_total) *out_total = -1;
if (!keyword || !keyword[0] || !out || max <= 0) return NCM_ERR_ARG;
api_json_t c; memset(&c, 0, sizeof c);
int rc = cloudsearch_post(device_id, cookie, keyword, 1000, offset, limit, &c);
if (rc != NCM_OK) return rc;
cloudsearch_count(&c.j, "result.playlistCount", out_total);
int arr = ncm_json_path(&c.j, "result.playlists");
int rc2 = parse_playlists_array(&c.j, arr, out, max, out_n);
api_free(&c);
return rc2;
}
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
ncm_song_t *out, int max, int *out_n) {
return ncm_api_cloudsearch_song(device_id, cookie, keyword, 0, max, out, max, out_n, NULL);
}
/* 从一段 track/all 响应 JSON 串解析歌曲数组 + total(离线样本/单元测试用,不打网络)。
* 数组取「songs → playlist.tracks → data.songs」首个命中;total 取「total → count →
* playlist.trackCount → musicCount」,全缺则 0。 */
+10 -2
View File
@@ -130,8 +130,16 @@ int ncm_api_parse_vip_info(const char *json, size_t len, int64_t now_ms, ncm_vip
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);
/* 搜索(weapi /api/cloudsearch/pc):keyword 搜索歌曲(type=1)。out 填 ncm_song_t。
* 仅返回前端可解析字段(name/ar[0]/al/dt/id)。风控:只在用户确认时调用。 */
/* 搜索(weapi /api/cloudsearch/pc)。type 映射(BIZ_NOTES §5.1):1=单曲、1000=歌单、(10=专辑留后续)。
* offset/limit 分页(limit=30/页,REPORT §23);*out_total=服务端总数(-1=未知)。
* 风控:只在用户点「搜索」/翻页时调用。 */
int ncm_api_cloudsearch_song(const char *device_id, const char *cookie, const char *keyword,
int offset, int limit, ncm_song_t *out, int max, int *out_n,
int64_t *out_total);
int ncm_api_cloudsearch_playlist(const char *device_id, const char *cookie, const char *keyword,
int offset, int limit, ncm_playlist_t *out, int max, int *out_n,
int64_t *out_total);
/* 兼容旧签名:单曲(type=1, offset=0, limit=max)。 */
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
ncm_song_t *out, int max, int *out_n);
Binary file not shown.
Binary file not shown.
Binary file not shown.