291 lines
10 KiB
C
291 lines
10 KiB
C
/* 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 <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <pthread.h>
|
||
|
||
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);
|
||
}
|