/* ui/search.c — 搜索页(ACT_SEARCH,顶栏搜索按钮进入)。 * * 独立页面:搜索输入框(SDK TextField,接 IME §5.1,仅英文/数字)+ 红色「搜索」按钮 + * 结果列表(点击可播,复用 g_st.songs[] 起点:播放/上下一首均对结果集生效)。 * 搜索结果网络走后台 worker(search_worker),UI 不卡。 * * 风控纪律(用户指定):**搜索请求只在用户确认时发**——用户点「搜索」按钮才发起 cloudsearch, * 进入页面不自动搜索;IME 输入仅本地编辑,不逐键发请求。 */ #include "../ncm_app_internal.h" #include #include #include #include typedef struct { meow_view *view; int active; } search_ctrl; static search_ctrl g_sc; 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 int g_press_row, g_press_delta, g_dragging, g_drag_last_y; 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]; static void sr_drag_reset(void) { g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0; } 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; } 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 (g_loading) { ncm_draw_empty(r_list, "SEARCHING", "正在搜索..."); return; } if (g_st.search_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录"); else if (g_st.search_err) ncm_draw_empty(r_list, "SEARCH FAILED", "点「搜索」重试"); else if (!g_query[0]) ncm_draw_empty(r_list, "NO KEYWORD", "输入关键词点「搜索」"); else ncm_draw_empty(r_list, "NO RESULTS", "未找到相关歌曲"); } 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); } /* 「搜索」红色确认钮(SDK meow_btn FILLED 是 MD3 蓝;App 自绘品牌红)。 */ 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, g_st.search_count); meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); } const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); } /* 后台 worker:唯一网络调用(cloudsearch),UI 不卡。 */ static void *search_worker(void *arg) { (void)arg; 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) { g_st.search_count = n; g_st.song_count = n; /* 结果集作为当前播放上下文 */ g_st.search_err = 0; } else { g_st.search_count = 0; g_st.song_count = 0; g_st.search_err = rc; } 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); if (g_host_mock) { /* 宿主/测试:预置结果,同步 */ g_loading = 0; g_st.search_err = 0; sr_apply(); return; } if (g_thr_on) return; /* 已有 worker 在跑 */ g_st.search_err = 0; g_st.search_count = 0; 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; } sr_apply(); } 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; 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; } 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; } sr_drag_reset(); player_play_song(row); /* 结果集即 g_st.songs[],点击可播 */ 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; 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); } 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; 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); } 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); 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)); 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); g_field = meow_textfield_create(field.x, field.y, field.w, field.h, "输入歌曲 / 歌手", g_st.search_query, sizeof g_st.search_query, MEOW_IME_ASCII); 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_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); sr_drag_reset(); } 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(); } 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_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); }