277 lines
9.6 KiB
C
277 lines
9.6 KiB
C
/* ui/home.c — 我的歌单页(ACT_HOME,根页)。
|
||
*
|
||
* 内容区用 SDK 结构化列表(meow_listview,§5.5 W_LISTVIEW)虚拟化渲染
|
||
* 「图标(folder 占位) + 歌单名 + 曲目数副标题 + 右侧箭头」。行数据源逐行填 meow_list_item;
|
||
* 滚动/选中/命中由 SDK 收口。空态/错误/底部信息行用 meow_draw_custom_register 注册非控件绘制物。
|
||
* 左缘返回 = 退出 App(app.c 处理)。下拉刷新可选:用 topbar 「↻」 动作项。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <pthread.h>
|
||
|
||
typedef struct { meow_view *view; int active; } home_ctrl;
|
||
static home_ctrl g_hc;
|
||
|
||
static meow_widget *g_tb;
|
||
static meow_widget *g_list;
|
||
static ncm_rect_t r_cont, r_list;
|
||
static int g_empty_id, g_footer_id;
|
||
static char g_title[128];
|
||
static volatile int g_load_err; /* 0=ok;负=错误码(worker 写,UI draw 读) */
|
||
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
|
||
|
||
/* 歌单加载后台 worker:网络(ensure_uid + user_playlist)从 UI 线程挪出,
|
||
* 使冷启动/进 home 立即出界面,再异步填列表。gen 代际作“完成”信号(对齐 cover.c)。
|
||
* 单核 MIPS,gen 在数据写完后置位,UI 见 gen 变化才重建列表,避免半成品。 */
|
||
static pthread_mutex_t g_home_mu = PTHREAD_MUTEX_INITIALIZER;
|
||
static pthread_t g_home_thr;
|
||
static int g_home_thr_on; /* 1=worker 在跑(防重复 spawn) */
|
||
static volatile int g_home_gen; /* worker 完成代数 */
|
||
static int g_home_acked; /* UI 已消费的代数 */
|
||
static volatile int g_home_loading; /* 1=加载中(draw 显示 LOADING) */
|
||
|
||
static void home_drag_reset(void)
|
||
{
|
||
g_press_row = -1;
|
||
g_press_delta = 0;
|
||
g_dragging = 0;
|
||
g_drag_last_y = 0;
|
||
}
|
||
|
||
static int home_src2(int idx, meow_list_item *out, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= g_st.playlist_count) return 0;
|
||
ncm_playlist_t *pl = &g_st.playlists[idx];
|
||
static char title[128]; /* 歌单名截断 */
|
||
static char sub[64];
|
||
snprintf(title, sizeof title, "%s", pl->name);
|
||
snprintf(sub, sizeof sub, "%d tracks", pl->track_count);
|
||
out->icon_kind = MEOW_LIST_ICON_FOLDER;
|
||
out->title = title;
|
||
out->subtitle = sub;
|
||
out->right = MEOW_LIST_RIGHT_ARROW;
|
||
out->on = 0;
|
||
out->enabled = 1;
|
||
return 1;
|
||
}
|
||
|
||
static void home_draw_empty(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_HOME) return;
|
||
if (g_load_err == 0 && g_st.playlist_count != 0) return;
|
||
if (g_home_loading) {
|
||
ncm_draw_empty(r_list, "LOADING", "fetching playlists...");
|
||
return;
|
||
}
|
||
if (g_st.logged_in == 0)
|
||
ncm_draw_empty(r_list, "NOT SIGNED IN", "sign in first");
|
||
else if (g_load_err == NCM_ERR_AUTH)
|
||
ncm_draw_empty(r_list, "SESSION EXPIRED", "please sign in again");
|
||
else if (g_load_err)
|
||
ncm_draw_empty(r_list, "NETWORK ERROR", "pull to retry");
|
||
else
|
||
ncm_draw_empty(r_list, "NO PLAYLISTS", "nothing here yet");
|
||
}
|
||
|
||
static void home_draw_footer(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_HOME) return;
|
||
if (g_load_err != 0) return;
|
||
char foot[96];
|
||
if (g_st.playlist_count == 0)
|
||
snprintf(foot, sizeof foot, "0 playlists");
|
||
else
|
||
snprintf(foot, sizeof foot, "%d playlists %s", g_st.playlist_count,
|
||
g_st.nickname[0] ? g_st.nickname : "");
|
||
ncm_draw_footer(r_cont, foot);
|
||
}
|
||
|
||
/* 把当前 playlist_count/昵称刷进列表 + 标题,并整帧重绘(UI 线程)。 */
|
||
static void home_apply(void)
|
||
{
|
||
if (g_list) {
|
||
meow_listview_set_count(g_list, g_st.playlist_count);
|
||
meow_listview_set_scroll(g_list, 0);
|
||
meow_listview_set_sel(g_list, -1);
|
||
}
|
||
const char *name = g_st.nickname[0] ? g_st.nickname : "My Playlists";
|
||
snprintf(g_title, sizeof g_title, "%s", name);
|
||
if (g_tb) meow_topappbar_set_title(g_tb, g_title);
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_ui_set_page_bg(th->bg);
|
||
meow_ui_paint_full();
|
||
}
|
||
|
||
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist),UI 线程不阻塞。 */
|
||
static void *home_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
int rc;
|
||
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
|
||
else rc = appstate_load_playlists();
|
||
pthread_mutex_lock(&g_home_mu);
|
||
g_load_err = rc;
|
||
g_home_loading = 0;
|
||
g_home_gen++;
|
||
pthread_mutex_unlock(&g_home_mu);
|
||
return NULL;
|
||
}
|
||
|
||
void home_reload(void)
|
||
{
|
||
if (g_host_mock) { /* 宿主/测试:跳过网络,用预置数据(同步) */
|
||
g_load_err = 0;
|
||
if (g_load_err != 0) g_st.playlist_count = 0;
|
||
home_apply();
|
||
return;
|
||
}
|
||
if (g_home_thr_on) return; /* 已有 worker 在跑,忽略重复触发 */
|
||
g_load_err = 0;
|
||
g_st.playlist_count = 0; /* 清旧数据,先画 LOADING,worker 完成后回填 */
|
||
g_home_loading = 1;
|
||
g_home_thr_on = 1;
|
||
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
|
||
g_home_thr_on = 0;
|
||
g_home_loading = 0;
|
||
g_load_err = NCM_ERR_IO;
|
||
}
|
||
home_apply(); /* 先画(LOADING/空态),不等网络 */
|
||
}
|
||
|
||
/* UI tick:worker 完成(gen 变化)后重建列表并重绘。 */
|
||
void home_tick(uint64_t now)
|
||
{
|
||
(void)now;
|
||
if (!g_home_thr_on) return;
|
||
if (g_home_gen == g_home_acked) return;
|
||
g_home_acked = g_home_gen; /* 消费本次完成 */
|
||
g_home_thr_on = 0; /* worker 已结束,可再次 reload */
|
||
g_home_loading = 0;
|
||
home_apply();
|
||
}
|
||
|
||
int home_is_active(void) { return g_hc.active; }
|
||
|
||
int home_on_tap(int x, int y)
|
||
{
|
||
if (g_tb) {
|
||
int ah = meow_topappbar_hit(g_tb, x, y);
|
||
if (ah == MEOW_TOPBAR_BACK) { home_drag_reset(); return 1; }
|
||
if (ah >= 0) { home_drag_reset(); home_reload(); return 1; } /* ↻ 刷新 */
|
||
}
|
||
if (g_load_err != 0 || g_st.playlist_count == 0) return 0;
|
||
if (meow_widget_hit(g_list, x, y)) {
|
||
int row = meow_listview_hit(g_list, x, y);
|
||
if (row < 0) { home_drag_reset(); return 1; }
|
||
home_drag_reset();
|
||
playlist_open(row);
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void home_touch_down(int x, int y)
|
||
{
|
||
home_drag_reset();
|
||
if (g_load_err != 0 || g_st.playlist_count == 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;
|
||
meow_listview_set_sel(g_list, 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 home_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; meow_listview_set_sel(g_list, -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 home_swipe(int dir, int dist)
|
||
{
|
||
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||
if (g_load_err != 0 || g_st.playlist_count == 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 home_page_create(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((home_ctrl *)ctx)->view = v;
|
||
r_cont = ncm_content_rect();
|
||
r_list = NR(r_cont.x, r_cont.y, r_cont.w, r_cont.h - APP_FOOTER_H);
|
||
if (r_list.h < 0) r_list.h = 0;
|
||
meow_ui_activity_push(ACT_HOME); /* 让下面控件 owner=ACT_HOME(§6.1 ①) */
|
||
g_tb = ncm_topbar_create("My Playlists", 0, "\xe2\x86\xbb"); /* 无返回;右侧 ↻ */
|
||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||
home_src2, NULL, MEOW_LIST_SEL_SINGLE);
|
||
meow_listview_set_item_h(g_list, APP_ROW_H);
|
||
meow_ui_activity_pop();
|
||
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
||
home_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, home_draw_footer, NULL);
|
||
home_drag_reset();
|
||
}
|
||
|
||
static void home_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((home_ctrl *)ctx)->active = 1;
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_ui_set_page_bg(th->bg);
|
||
home_reload();
|
||
}
|
||
|
||
static void home_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((home_ctrl *)ctx)->active = 0;
|
||
}
|
||
|
||
static void home_page_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
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_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
}
|
||
|
||
meow_view *home_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_create = home_page_create,
|
||
.on_enter = home_page_enter,
|
||
.on_leave = home_page_leave,
|
||
.on_destroy = home_page_destroy,
|
||
};
|
||
return meow_view_create(ACT_HOME, MEOW_VIEW_ACTIVITY, &h, &g_hc);
|
||
}
|