feat(ncm): 复古UI重做 (agent-211, 按 NCM_UI_DESIGN.md) — 白红/黑红双套配色(ncm_palette+主题监听实时换肤); 主页红topbar+个人区+双页签(我创建的/我收藏的, is_collected/subscribed已补); 搜索页(cloudsearch,仅确认时发请求); 歌单详情封面头块+播放全部; 播放页黑胶唱盘(行扫圆+封面裁圆+唱针+分带刷新); 播放页tick分带刷新修整页重绘隐患

ncm→90a25cf0; 0 warning; 宿主14张双主题截图目检+core测试全过
This commit is contained in:
MeowOS committed 2026-08-31 10:26:44 +08:00
1 parent 4cec476dd9
commit 8964cd1750
33 files changed
+1333 -320

No files matched your search

+290
View File
@@ -0,0 +1,290 @@
/* 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);
}