feat(ncm): 播放模式三态(随机/列表循环/单曲循环,播放页三键左)+当前实时播放列表(三键右,歌单滑窗/meow_listview虚拟化分页至5194首,当前曲高亮,点播切歌) — 模式持久化,宿主modetest/queuetest全过

This commit is contained in:
AskaEth committed 2026-09-03 18:51:28 +08:00
1 parent 29e13b2b5e
commit 7ca3e5bd84
8 files changed
+707 -28

No files matched your search

+63 -2
View File
@@ -89,7 +89,11 @@ static ncm_rect_t g_cover_r; /* 封面页静态带(方形大图
static ncm_rect_t g_dyn_r; /* 封面页动态带(元信息/进度/控制/指示点) */
static int g_title_y, g_artist_y, g_qual_y, g_times_y;
static ncm_rect_t g_bar;
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_mode_x, g_queue_x, g_r_side, g_r_play, g_dots_y;
/* §NEW 模式/队列钮:三键左(mode,prev 更左)/ 三键右(queue,next 更右)。间距 60 保不遮挡。 */
#define GAP_SIDE 60
#define MODE_BTN_R (g_r_side) /* 与侧钮同半径(命中取 CTL_RR) */
#define MODE_LBL_SZ 14 /* 模式/队列钮文字字号(两字,居中) */
/* ---- 歌词页几何 ---- */
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
@@ -284,6 +288,18 @@ static void draw_ctrl_btn(int icon, int cx, int cy, int r, uint32_t circle, uint
draw_md3_icon(icon, cx, cy, (r * 4) / 3, fg);
}
/* 文字钮:圆形底 + 居中两字文案(§NEW 模式/队列钮)。无逐帧三角行扫,纯圆形填充。 */
static void draw_text_btn(const char *label, int cx, int cy, int r, uint32_t circle, uint32_t fg)
{
for (int dy = -r; dy <= r; dy++) {
int half = (int)sqrt((double)(r * r - dy * dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2 * half, 1, circle);
}
if (label && label[0])
meow_ui_text_center(cx, cy - MODE_LBL_SZ / 2, label, MODE_LBL_SZ, fg);
}
/* 圆角封面四角裁掉:把方形图四角(圆角外-方形内)盖回背景色(廉价圆角,无 GPU 裁剪)。 */
static void round_corners_bg(int x, int y, int w, int h, int r, uint32_t bg)
{
@@ -335,6 +351,8 @@ static void player_compute_geom(void)
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
g_ctl_y = g_times_y + 64; /* 564 */
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
g_mode_x = g_prev_x - GAP_SIDE; /* 播放模式三态钮(prev 更左) */
g_queue_x = g_next_x + GAP_SIDE; /* 当前播放队列钮(next 更右) */
g_r_side = 24; g_r_play = 34;
g_dots_y = g_ctl_y + 62; /* 626 */
@@ -449,6 +467,12 @@ static void plr_draw_cover_dyn(void *ctx)
g_play_x, g_ctl_y, g_r_play, pa->red, pa->on_red);
draw_ctrl_btn(NCM_I_NEXT, g_next_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
/* §NEW 播放模式三态钮(三键左,prev 更左)+ 当前播放队列钮(三键右,next 更右):
* 文案反映当前模式(随机/列表/单曲);队列钮进入实时播放列表。 */
draw_text_btn(ncm_mode_name(player_manager_play_mode()), g_mode_x, g_ctl_y,
MODE_BTN_R, PLR_CTRL_BG, PLR_CTRL_FG);
draw_text_btn("\u961f\u5217", g_queue_x, g_ctl_y, MODE_BTN_R, PLR_CTRL_BG, PLR_CTRL_FG); /* 队列 */
/* 页面指示点 */
draw_page_dots(g_dots_y);
}
@@ -604,7 +628,9 @@ void player_touch_down(int x, int y)
interactive = 1;
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y) ||
in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y) ||
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y))
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y) ||
in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y) ||
in_circle(g_queue_x, g_ctl_y, CTL_RR, x, y))
interactive = 1;
}
g_touch_on = (x > PLR_EDGE_W) && (y >= meow_ui_statusbar_hotzone()) && !interactive;
@@ -675,6 +701,19 @@ void player_on_tap(int x, int y)
}
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(-1); return; }
if (in_circle(g_next_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(1); return; }
/* §NEW 播放模式三态钮:点击循环切换(SHUFFLE→LISTLOOP→SINGLE→SHUFFLE)+ 持久化 */
if (in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y)) {
int m = player_manager_play_mode();
int nxt = (m + 1) % NCM_MODE_COUNT;
player_manager_set_play_mode(nxt);
appstate_apply_play_mode(nxt); /* 记住模式(退出后保留) */
player_dyn_invalidate();
return;
}
/* §NEW 当前播放队列钮:进实时播放列表页 */
if (in_circle(g_queue_x, g_ctl_y, CTL_RR, x, y)) { queue_open(); return; }
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
int64_t dur = player_manager_dur_ms();
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
@@ -731,6 +770,28 @@ void player_play_search_song(int i)
meow_view_page_push(ACT_PLAYER);
}
/* 从「当前实时播放队列」第 idx 首播放(§NEW 队列页点播):保持当前 queue_src 不变
* (不把队列切回歌单/不覆写歌单窗口)——上/下一首仍在该队列内循环。封面用窗口/搜索数组
* 内的元数据(不触发网络),占位行由播放线程物化后快照接管。 */
void player_play_queue_song(int idx)
{
int cnt = appstate_queue_count();
if (idx < 0 || idx >= cnt) return;
ncm_song_t s;
int have = 0;
if (appstate_get_queue() == NCM_QUEUE_SEARCH) {
if (appstate_search_song(idx, &s) == 0) have = 1;
} else if (appstate_song_copy(idx, &s) == 0) {
have = 1;
}
const char *cover = have ? (s.al_pic[0] ? s.al_pic : g_st.cover_url) : g_st.cover_url;
player_manager_play(idx);
appstate_set_playing_song(idx, NULL);
if (have) cover_request(cover, s.id);
g_ppage = 0;
meow_view_page_push(ACT_PLAYER);
}
void player_pause_toggle(int on)
{
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
+377
View File
@@ -0,0 +1,377 @@
/* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
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 = r_list.y + (i * item - off) + 1, th2 = item - 2;
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 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 = r_list.y + (i * item - off) + 1, th2 = item - 2;
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
int icy = ry + (item - 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, item - 2, pa->red);
meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, item - 2, pa->red, 26);
}
}
}
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);
}