/* ui/search.c — 搜索页(ACT_SEARCH,顶栏搜索按钮进入)。 * * §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)不被破坏。 * * 风控纪律(用户指定):**真实请求只在用户点「搜索」按钮/切页签/翻页时发**——进页不自动搜索, * IME 输入仅本地编辑,不逐键发请求。调试/测试全部离线 stub(g_host_mock)。 */ #include "../ncm_app_internal.h" #include #include #include #include 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, 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; static int g_thr_on; static volatile int g_gen; static int g_acked; static volatile int g_loading; 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; 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 (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_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; 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); } } static void sr_draw_btn(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_SEARCH) return; const ncm_pal_t *pa = ncm_palette(g_dark); ncm_draw_button(r_btn.x, r_btn.y, r_btn.w, r_btn.h, "搜索", pa->red, pa->on_red); } static void sr_apply(void) { 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(); } /* 触底翻页(§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 (ncm_thread_create(&g_thr, 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 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 { 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++; pthread_mutex_unlock(&g_mu); return NULL; } 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); 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 在跑 */ g_req_tab = g_tab; g_req_offset = 0; g_req_replace = 1; g_st.search_err = 0; g_loading = 1; g_more_err = 0; g_thr_on = 1; if (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO; } 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 (ncm_thread_create(&g_thr, search_worker, NULL) != 0) { g_thr_on = 0; g_loading = 0; } } } void search_tick(uint64_t now) { (void)now; chrome_tick(); if (!g_thr_on) return; if (g_gen == g_acked) return; g_acked = g_gen; g_thr_on = 0; g_loading = 0; if (g_more_err) { ncm_snack("加载失败"); g_more_err = 0; } sr_apply(); } void search_open(void) { meow_view_page_push(ACT_SEARCH); } int search_is_active(void) { return g_sc.active; } int search_on_tap(int x, int y) { if (g_tb) { int ah = meow_topappbar_hit(g_tb, x, y); if (ah == MEOW_TOPBAR_BACK) { sr_drag_reset(); meow_view_page_pop(); return 1; } if (ah >= 0) { sr_drag_reset(); return 1; } } if (x >= r_btn.x && x < r_btn.x + r_btn.w && y >= r_btn.y && y < r_btn.y + r_btn.h) { const char *t = meow_textfield_text(g_field); /* 「搜索」确认(风控:仅此处发) */ search_submit(t ? t : ""); return 1; } 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 (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 (ncm_thread_create(&g_thr, 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(); 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 (meow_widget_hit(g_list, x, y)) { int row = meow_listview_hit(g_list, x, y); if (row >= 0) g_press_row = row; g_dragging = 1; g_drag_last_y = y; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } void search_touch_move(int x, int y) { (void)x; if (!g_dragging) return; if (g_press_row >= 0) { int d = y - g_drag_last_y; if (d < 0) d = -d; g_press_delta += d; if (g_press_delta > 10) g_press_row = -1; } int dy = y - g_drag_last_y; g_drag_last_y = 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) { 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 (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) { 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) { (void)v; ((search_ctrl *)ctx)->view = v; 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); 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); /* 中文搜索: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, "输入歌曲 / 歌手", g_st.search_query, sizeof g_st.search_query, MEOW_IME_PINYIN); g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, sr_src2, NULL, MEOW_LIST_SEL_NONE); meow_listview_set_item_h(g_list, APP_ROW_H); meow_ui_activity_pop(); 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(); } static void sr_page_enter(meow_view *v, void *ctx) { (void)v; ((search_ctrl *)ctx)->active = 1; 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) { (void)v; ((search_ctrl *)ctx)->active = 0; } 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_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; } } meow_view *search_view_create(void) { static const meow_view_hooks h = { .on_create = sr_page_create, .on_enter = sr_page_enter, .on_leave = sr_page_leave, .on_destroy = sr_page_destroy, }; return meow_view_create(ACT_SEARCH, MEOW_VIEW_ACTIVITY, &h, &g_sc); }