/* ui/queue.c — 当前实时播放队列页(ACT_QUEUE,§NEW:播放页「队列」钮进入)。 * * 展示**当前播放队列**(player_manager 的 queue_src —— 歌单滑动窗口 或 搜索单曲结果集), * 含当前曲高亮、可点某行切到那首播放。数据源与分页复用歌单滑动窗口(REPORT §17): * - count = appstate_queue_count()(全量:track_ids_count 或 search_song_count,可 5194 首); * - 行内容 = src2 逐行按 queue_src 取(PLAYLIST→滑窗拷贝,SEARCH→搜索数组),未物化槽显示 * ".../加载中" 占位,视口 ±24 行未物化 → 后台 worker ensure_range 补载(盘缓存优先,批≤NCM_SONG_BATCH); * - 列表本身由 meow_listview 虚拟化(只画视口内行),不整歌单常驻内存 → 任意长度不卡死。 * - SEARCH 队列行全在内存(≤256),无需 ensure;PLAYLIST 队列走滑窗+ensure(含网络)。 * 当前曲高亮 = 播放页三态钮旁队列页以红色左条 + 红色轻染标记 player_manager_playing_song() * 所在行(queue idx 与播放队列下标一致,两种 queue_src 通用)。 * * 布局(无封面头块,纯列表):topbar「播放列表」+ 曲目列表 + 底部 `first-last / N 首` 页脚。 * 左缘返回 = 弹回上一页(播放页,app.c);点击行 → player_play_queue_song(保持当前 queue_src)。 */ #include "../ncm_app_internal.h" #include #include #include #include typedef struct { meow_view *view; int active; } queue_ctrl; static queue_ctrl g_qc; static meow_widget *g_tb; static meow_widget *g_list; static ncm_rect_t r_cont, r_list; static int g_bg_id, g_draw_id, g_empty_id, g_footer_id; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_last_count, g_last_cur; /* 队列补载(PLAYLIST)后台 worker 参数:目标区间 [lo,hi)(全局下标)。SEARCH 无需补载。 */ static pthread_mutex_t g_qmu = PTHREAD_MUTEX_INITIALIZER; static pthread_t g_qthr; static int g_qthr_on; static volatile int g_ens_lo, g_ens_hi; static volatile int g_ens_err; static volatile int g_ens_done; /* worker 已完成的代次(tick 消费) */ static volatile int g_ens_acked; static void q_drag_reset(void) { g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0; } /* 队列第 idx 行的 ncm_song_t:按 queue_src 取。返回 0=已物化(out 有效);-1=未物化/占位。 * PLAYLIST:滑窗拷贝(UI 线程不触发网络);SEARCH:搜索数组(内存)。 */ static int q_song_get(int idx, ncm_song_t *out) { if (appstate_get_queue() == NCM_QUEUE_SEARCH) return (appstate_search_song(idx, out) == 0) ? 0 : -1; return (appstate_song_copy(idx, out) == 0) ? 0 : -1; } /* 列表数据源(meow_list2_source):逐行按需填充(不做 IO,§5.5)。未物化槽显示占位行。 */ static int q_src2(int idx, meow_list_item *out, void *ctx) { (void)ctx; int cnt = appstate_queue_count(); if (idx < 0 || idx >= cnt) return 0; static ncm_song_t s_s; static char sub[256]; if (q_song_get(idx, &s_s) != 0) { out->icon_kind = MEOW_LIST_ICON_NONE; out->title = "..."; out->subtitle = "加载中"; out->right = MEOW_LIST_RIGHT_NONE; out->on = 0; out->enabled = 1; return 1; } if (s_s.art_name[0] && s_s.album_name[0]) snprintf(sub, sizeof sub, "%s / %s", s_s.art_name, s_s.album_name); else if (s_s.art_name[0]) snprintf(sub, sizeof sub, "%s", s_s.art_name); else if (s_s.album_name[0]) snprintf(sub, sizeof sub, "%s", s_s.album_name); else snprintf(sub, sizeof sub, "-"); out->icon_kind = MEOW_LIST_ICON_NONE; out->title = s_s.name; out->subtitle = sub; out->right = MEOW_LIST_RIGHT_NONE; out->on = 0; out->enabled = 1; return 1; } /* 行背景(z=5,画在 listview 文本之下):每行 surfaceContainer 圆角卡 + 「当前播放行」红色左条/轻染。 */ static void q_row_bg(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_QUEUE) return; int cnt = appstate_queue_count(); if (cnt <= 0) return; const meow_theme *th = meow_theme_get(); const ncm_pal_t *pa = ncm_palette(g_dark); int item = APP_ROW_H; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; int cur = player_manager_playing_song(); for (int i = first; i < last; i++) { int ry = ncm_row_card_y(item, i, off, r_list.y); int th2 = ncm_row_card_h(item); if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */ if (i == cur) { /* 当前播放行:红色左条 + 红色轻染(高亮),底用 red_ctr 浅红 */ ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, pa->red_ctr); meow_ui_fill_rect(r_list.x + 2, ry + 4, 4, th2 - 8, pa->red); } else { ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer); } } } /* 主绘制(z=60):当前行红色 mini-vinyl 缩略座标(文本由 listview z=10 画)。 */ static void q_draw(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_QUEUE) return; const meow_theme *th = meow_theme_get(); const ncm_pal_t *pa = ncm_palette(g_dark); int cnt = appstate_queue_count(); if (cnt <= 0) return; int item = APP_ROW_H, isz = 24; int off = meow_listview_scroll(g_list); int first = off / item, last = (off + r_list.h) / item + 1; if (last > cnt) last = cnt; for (int i = first; i < last; i++) { int ry = ncm_row_card_y(item, i, off, r_list.y); int th2 = ncm_row_card_h(item); if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; int icy = ry + (th2 - isz) / 2, bx = r_list.x + 16; ncm_fill_round(bx, icy, isz, isz, 6, pa->red); meow_ui_fill_rect(bx + isz / 2 - 2, icy + isz / 2 - 2, 4, 4, pa->on_red); if (g_press_row == i) { meow_ui_fill_rect(r_list.x, ry, 4, th2, pa->red); meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, th2, pa->red, 26); } /* 擦除 z=10 的 SDK 行分隔线(现落进卡片间空隙):以页底色填空隙带;不越出列表视口底。 */ int gy = ry + th2, gh = NCM_ROW_GAP; if (gy + gh > r_list.y + r_list.h) gh = r_list.y + r_list.h - gy; if (gh > 0) meow_ui_fill_rect(r_list.x, gy, r_list.w, gh, th->bg); } } static void q_draw_empty(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_QUEUE) return; if (appstate_queue_count() != 0) return; ncm_draw_empty(r_list, "暂无播放队列", "播放一首歌曲后在此查看列表"); } static void q_draw_footer(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_QUEUE) return; int total = appstate_queue_count(); if (total <= 0) return; int first = meow_listview_scroll(g_list) / APP_ROW_H + 1; int last = first + r_list.h / APP_ROW_H; if (last > total) last = total; char foot[96]; snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total); ncm_draw_footer(r_cont, foot); } static void q_apply(void) { if (g_list) { int cnt = appstate_queue_count(); meow_listview_set_count(g_list, cnt); if (cnt != g_last_count) { g_last_count = cnt; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); } static void *queue_worker(void *arg) { (void)arg; int lo, hi; pthread_mutex_lock(&g_qmu); lo = g_ens_lo; hi = g_ens_hi; pthread_mutex_unlock(&g_qmu); int rc = 0; if (!g_host_mock) { /* §26:滑窗补载(盘缓存优先,批网格补载);非致命(部分失败留占位行) */ rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK; } pthread_mutex_lock(&g_qmu); g_ens_err = rc; g_ens_done++; pthread_mutex_unlock(&g_qmu); return NULL; } /* 视口驱动滑窗补载(仅 PLAYLIST 队列):视口 ±24 行有未物化槽 → 发 ensure worker。 * SEARCH 队列全在内存,无需补载。 */ static void q_maybe_ensure(void) { if (appstate_get_queue() != NCM_QUEUE_PLAYLIST) return; if (g_qthr_on || g_host_mock) return; int total = appstate_queue_count(); if (total <= 0) return; int scroll = meow_listview_scroll(g_list); int first = scroll / APP_ROW_H; int last = (scroll + r_list.h) / APP_ROW_H + 1; int lo = first - 24, hi = last + 24; if (lo < 0) lo = 0; if (hi > total) hi = total; if (lo >= hi) return; if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作 */ pthread_mutex_lock(&g_qmu); g_ens_lo = lo; g_ens_hi = hi; pthread_mutex_unlock(&g_qmu); g_qthr_on = 1; if (ncm_thread_create(&g_qthr, queue_worker, NULL) != 0) { g_qthr_on = 0; ncm_snack("加载失败"); } } void queue_open(void) { /* 从播放页「队列」钮进入:push 本页后按当前 queue 刷新(首屏立刻有 count/行)。 */ meow_view_page_push(ACT_QUEUE); q_apply(); meow_ui_paint_full(); q_maybe_ensure(); } int queue_is_active(void) { return g_qc.active; } int queue_on_tap(int x, int y) { if (g_tb) { int ah = meow_topappbar_hit(g_tb, x, y); if (ah == MEOW_TOPBAR_BACK) { q_drag_reset(); meow_view_page_pop(); return 1; } } if (meow_widget_hit(g_list, x, y)) { int was_drag = (g_press_delta > 10); int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y); q_drag_reset(); if (row < 0 || was_drag) return 1; player_play_queue_song(row); /* 点播:保持当前 queue_src,占位行由播放线程物化 */ return 1; } return 0; } void queue_touch_down(int x, int y) { q_drag_reset(); if (appstate_queue_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; g_dragging = 1; g_drag_last_y = y; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } void queue_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); q_maybe_ensure(); } } void queue_swipe(int dir, int dist) { if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; if (appstate_queue_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); q_maybe_ensure(); } } void queue_tick(uint64_t now) { (void)now; chrome_tick(); if (!g_qc.active) return; /* worker 补载完成 → 刷新列表(占位行自愈) */ if (g_qthr_on) { int done, acked; pthread_mutex_lock(&g_qmu); done = g_ens_done; acked = g_ens_acked; pthread_mutex_unlock(&g_qmu); if (done != acked) { pthread_mutex_lock(&g_qmu); g_ens_acked = done; pthread_mutex_unlock(&g_qmu); g_qthr_on = 0; if (g_ens_err != 0) ncm_snack("加载失败"); q_apply(); meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } /* 当前播放曲变化(切歌/顺延)→ 重画高亮行 */ int cur = player_manager_playing_song(); if (cur != g_last_cur) { g_last_cur = cur; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } static void q_page_create(meow_view *v, void *ctx) { (void)v; ((queue_ctrl *)ctx)->view = v; r_cont = ncm_content_rect(); r_list = NR(16, r_cont.y + 8, r_cont.w - 32, r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8)); if (r_list.h < 0) r_list.h = 0; meow_ui_activity_push(ACT_QUEUE); g_tb = ncm_topbar_create("播放列表", 1, NULL); g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, q_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_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, q_row_bg, NULL); g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h, q_draw, NULL); g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h, q_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, q_draw_footer, NULL); q_drag_reset(); } static void q_page_enter(meow_view *v, void *ctx) { (void)v; ((queue_ctrl *)ctx)->active = 1; const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); g_last_count = -1; g_last_cur = -2; q_apply(); meow_ui_paint_full(); q_maybe_ensure(); } static void q_page_leave(meow_view *v, void *ctx) { (void)v; ((queue_ctrl *)ctx)->active = 0; } static void q_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; if (g_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; } if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_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_tb) { meow_widget_free(g_tb); g_tb = NULL; } } meow_view *queue_view_create(void) { static const meow_view_hooks h = { .on_create = q_page_create, .on_enter = q_page_enter, .on_leave = q_page_leave, .on_destroy = q_page_destroy, }; return meow_view_create(ACT_QUEUE, MEOW_VIEW_ACTIVITY, &h, &g_qc); }