feat(ncm): 网易云客户端 v1 UI+播放链路 (agent-205) — os.meow.ncm: 登录页(真实unikey二维码+轮询状态机)/我的歌单(listview)/歌单详情/播放页(封面+seek+滚动歌词+竖横屏切换)/设置(音质默认Hi-res+缓存上限默认10G); 播放线程(缓存LRU+minimp3→ALSA UAPI直开hw:0,0); 媒体会话MSG_META上报+PLAYCTL接收(首真实媒体源)

ncm 1e331964 (text 764KB); 0 warning; 宿主离屏6页截图目检+真实QR可扫+qemu-mipsel干净启动+解码446xRT
真机待联调: ALSA时序/803扫码闭环/媒体会话联动/gbk大字库渲染
This commit is contained in:
MeowOS committed 2026-08-31 07:24:59 +08:00
1 parent 32543ac616
commit 21519ce2e4
23 files changed
+3255

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
/* app.c — os.meow.ncm 应用装配层:main + 组装 + 事件分发。
*
* 职责(对齐 hello_app/app.c):main 只做初始化/建页/跑事件循环/清理;把 meow_ui_run 的平铺输入
* 回调(key/tap/swipe/tick/touch)路由到当前活动页。页面切换统一走 meow_view_page_push/pop/replace。
* 播放线程(player_manager)+ 媒体会话 IPC 线程(ipc_media)在 main 起动、退出时停。
*
* 铁律(APPS.md §3.4.1):物理键禁映射——on_key 不处理任何键语义;返回=左缘左滑/返回钮/request_exit。
* 左缘返回:播放页→歌单→主页→退出(登录页→退出)。
*/
#include "ncm_app_internal.h"
#include <signal.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0;
static meow_view *g_login_view, *g_home_view, *g_pl_view, *g_plr_view, *g_set_view;
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
/* ============================================================ */
/* 事件分发:meow_ui_run 平铺回调 → 当前页 */
/* ============================================================ */
static void app_on_key(int keycode, int is_down)
{
(void)keycode; (void)is_down; /* 物理键系统语义;App 不映射(§3.4.1) */
}
static void app_on_tap(int x, int y)
{
if (login_is_active()) { login_on_tap(x, y); return; }
if (player_is_active()) { player_on_tap(x, y); return; }
if (settings_is_active()) { settings_on_tap(x, y); return; }
if (playlist_is_active()) { playlist_on_tap(x, y); return; }
home_on_tap(x, y);
}
static void app_on_swipe(int dir, int dx, int dy)
{
(void)dx;
if (dir == MEOW_SWIPE_LEFT) {
if (player_is_active()) { meow_view_page_pop(); return; }
if (settings_is_active()) { meow_view_page_pop(); return; }
if (playlist_is_active()) { meow_view_page_pop(); return; }
/* home / login = 根 → 退出 */
meow_log(MEOW_LOG_INFO, "exit:swipe");
meow_ui_request_exit();
return;
}
if (home_is_active()) { home_swipe(dir, dy); return; }
if (playlist_is_active()) { playlist_swipe(dir, dy); return; }
if (player_is_active()) { player_on_swipe(dir, dy); return; }
}
static void app_on_touch_down(int x, int y)
{
if (home_is_active()) home_touch_down(x, y);
else if (playlist_is_active()) playlist_touch_down(x, y);
else if (settings_is_active()) settings_touch_down(x, y);
}
static void app_on_touch_move(int x, int y)
{
if (home_is_active()) home_touch_move(x, y);
else if (playlist_is_active()) playlist_touch_move(x, y);
else if (settings_is_active()) settings_touch_move(x, y);
}
static void app_on_swipe_progress(int x_start, int dx)
{
meow_ui_swipe_hint(x_start + dx);
}
static void app_on_tick(void)
{
uint64_t now = meow_ui_time_ms();
if (login_is_active()) { login_tick(now); return; }
if (player_is_active()) { player_tick(now); return; }
chrome_tick();
}
/* ============================================================ */
/* main */
/* ============================================================ */
int main(void)
{
meow_log(MEOW_LOG_INFO, "start");
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
meow_theme_add_listener(NULL);
signal(SIGTERM, on_sigterm);
/* 应用数据/核心库上下文(cookie/deviceId/cache/设置) */
appstate_init();
/* 播放 + 媒体会话线程(UI 不卡;无 launcher socket 时 ipc 优雅跳过) */
player_manager_start();
ipc_media_start();
g_net_ready = 1;
/* 建五页 view */
g_login_view = login_view_create();
g_home_view = home_view_create();
g_pl_view = playlist_view_create();
g_plr_view = player_view_create();
g_set_view = settings_view_create();
/* 进入根页:已登录→home,否则→login */
meow_view_page_push(g_st.logged_in ? ACT_HOME : ACT_LOGIN);
meow_ui_events ev = {
app_on_key, app_on_tap, app_on_swipe, app_on_tick, app_on_swipe_progress,
app_on_touch_down, app_on_touch_move,
};
meow_ui_run(&ev);
/* 清理:停线程 → 释放页 */
g_app_quit = 1;
player_manager_stop_thread();
ipc_media_stop();
meow_view_free(g_set_view);
meow_view_free(g_plr_view);
meow_view_free(g_pl_view);
meow_view_free(g_home_view);
meow_view_free(g_login_view);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;
}
+165
View File
@@ -0,0 +1,165 @@
/* logic/appstate.c — 应用全局状态:ctx/登录/歌单/歌曲/设置。仅依赖核心库 + SDK meow_config_*。 */
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data"
int appstate_init(void)
{
const char *dd = getenv("MEOW_DATA_DIR");
if (!dd || !dd[0]) dd = NCM_DEFAULT_DATA;
strncpy(g_st.data_dir, dd, sizeof g_st.data_dir - 1);
g_st.data_dir[sizeof g_st.data_dir - 1] = 0;
ncm_ctx_init(&g_st.ctx, g_st.data_dir); /* device_id + cookie 载入 + cache */
if (g_st.ctx.cache.dir[0])
strncpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir - 1);
else
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
appstate_load_settings();
g_st.logged_in = (g_st.ctx.cookie[0] != 0);
g_st.active_playlist = -1;
g_st.playing_song = -1;
g_st.qr_size = 0;
g_st.login_state = NCM_LOGIN_PENDING;
/* uid/nickname 从配置恢复(登录时落盘);uid 缺失时不在此阻塞校验——由 home_reload 拉歌单时
* appstate_ensure_uid 触发 login_status_check(启动快,避免首屏长时间网络阻塞)。 */
meow_config_get("ncm.uid", g_st.uid, sizeof g_st.uid);
meow_config_get("ncm.nickname", g_st.nickname, sizeof g_st.nickname);
return 0;
}
void appstate_load_settings(void)
{
g_st.cache_limit_gb = meow_config_int("ncm.cache_limit_gb", 10);
if (g_st.cache_limit_gb < 1) g_st.cache_limit_gb = 1;
if (g_st.cache_limit_gb > 100) g_st.cache_limit_gb = 100;
g_st.level = meow_config_int("ncm.level", NCM_LEVEL_HIRES);
if (g_st.level < NCM_LEVEL_STANDARD || g_st.level > NCM_LEVEL_HIRES)
g_st.level = NCM_LEVEL_HIRES;
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
}
void appstate_save_settings(void)
{
char b[16];
snprintf(b, sizeof b, "%d", g_st.cache_limit_gb);
meow_config_set("ncm.cache_limit_gb", b);
snprintf(b, sizeof b, "%d", g_st.level);
meow_config_set("ncm.level", b);
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
}
void appstate_set_logged_in(const char *uid, const char *nickname)
{
if (uid && uid[0]) { ncm_strlcpy(g_st.uid, uid, sizeof g_st.uid); meow_config_set("ncm.uid", uid); }
if (nickname && nickname[0]) {
ncm_strlcpy(g_st.nickname, nickname, sizeof g_st.nickname);
meow_config_set("ncm.nickname", nickname);
}
g_st.logged_in = 1;
}
void appstate_logout(void)
{
ncm_ctx_clear_cookie(&g_st.ctx);
g_st.logged_in = 0;
g_st.uid[0] = 0;
g_st.nickname[0] = 0;
g_st.active_playlist = -1;
g_st.playing_song = -1;
g_st.song_count = 0;
meow_config_remove("ncm.uid");
meow_config_remove("ncm.nickname");
cover_cache_reset();
}
/* 若 uid 缺失(cookie 在但没存 uid),用 login_status_check 校验并取回。返回 0=成功。 */
int appstate_ensure_uid(void)
{
if (!g_st.logged_in || !g_st.ctx.cookie[0]) return NCM_ERR_AUTH;
if (g_st.uid[0]) return NCM_OK;
char uid[32], nick[128];
int rc = ncm_login_status_check(g_st.ctx.device_id, g_st.ctx.cookie, uid, sizeof uid,
nick, sizeof nick);
if (rc == NCM_OK) {
appstate_set_logged_in(uid, nick);
return NCM_OK;
}
/* 会话失效:清 cookie → 未登录 */
if (rc == NCM_ERR_AUTH) {
ncm_ctx_clear_cookie(&g_st.ctx);
g_st.logged_in = 0;
}
return rc;
}
int appstate_load_playlists(void)
{
if (!g_st.logged_in) return NCM_ERR_AUTH;
int rc = appstate_ensure_uid();
if (rc != NCM_OK) return rc;
memset(g_st.playlists, 0, sizeof g_st.playlists);
int n = 0;
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0;
return rc;
}
int appstate_load_playlist_songs(int idx)
{
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
g_st.active_playlist = idx;
const ncm_playlist_t *pl = &g_st.playlists[idx];
int idn = 0;
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
NULL, 0, NULL, 0, g_st.song_ids, NCM_SONG_MAX, &idn);
if (rc != NCM_OK) { g_st.song_count = 0; return rc; }
memset(g_st.songs, 0, sizeof g_st.songs);
int outn = 0;
for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
int batch = idn - off;
if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
int bn = 0;
rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn);
if (rc != NCM_OK) break;
outn += bn;
if (bn < batch) break;
}
g_st.song_count = outn;
appstate_apply_playlist_cover(idx);
return NCM_OK;
}
void appstate_apply_playlist_cover(int idx)
{
if (idx < 0 || idx >= g_st.playlist_count) return;
ncm_strlcpy(g_st.cover_url, g_st.playlists[idx].cover, sizeof g_st.cover_url);
}
/* 设置播放页当前歌曲 + 封面 URL(player 页 UI 读)。idx=-1 表示清除。 */
void appstate_set_playing_song(int idx, const char *cover_url)
{
g_st.playing_song = idx;
if (cover_url && cover_url[0])
ncm_strlcpy(g_st.cover_url, cover_url, sizeof g_st.cover_url);
}
int appstate_current_playing(void)
{
if (g_st.playing_song < 0 || g_st.playing_song >= g_st.song_count) return -1;
return g_st.playing_song;
}
+141
View File
@@ -0,0 +1,141 @@
/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。
*
* 内存纪律:下载限幅 NCM_COVER_MAX(~288KB);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。
* 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经
* cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。
* 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
static meow_img *g_cover_img;
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */
static pthread_t g_thr;
static int g_thr_on;
static int g_quit;
static volatile int g_gen; /* 每次换入 +1 */
static int g_acked; /* UI 已消费的版本 */
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
static unsigned char g_buf[NCM_COVER_MAX]; /* 单张下载缓冲(常驻) */
static meow_img *decode_cover(const char *url, size_t *out_len)
{
if (!url || !url[0]) return NULL;
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = url;
req.method = "GET";
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 10000;
ncm_http_resp_t resp;
int rc = ncm_http_request(&req, (char *)g_buf, sizeof g_buf, &resp);
if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) return NULL;
if (out_len) *out_len = resp.body_len;
return meow_img_load_mem((const void *)resp.body, (int)resp.body_len);
}
static void save_cover_raw(const char *url, size_t len)
{
if (!url || len == 0) return;
if (g_st.cache_dir[0]) {
char path[512];
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
ncm_store_write_file(path, g_buf, len);
pthread_mutex_lock(&g_cm);
ncm_strlcpy(g_cover_path, path, sizeof g_cover_path);
pthread_mutex_unlock(&g_cm);
}
(void)url;
}
static void *cover_worker(void *arg)
{
(void)arg;
while (!g_quit) {
pthread_mutex_lock(&g_cm);
if (!g_tgt_url[0]) { pthread_mutex_unlock(&g_cm); usleep(250 * 1000); continue; }
char url[512];
ncm_strlcpy(url, g_tgt_url, sizeof url);
pthread_mutex_unlock(&g_cm);
size_t blen = 0;
meow_img *img = decode_cover(url, &blen);
if (!img) { usleep(500 * 1000); continue; }
pthread_mutex_lock(&g_cm);
/* 目标仍是 url 才换入(期间未提交更早) */
if (g_tgt_url[0] && strcmp(g_tgt_url, url) == 0) {
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
save_cover_raw(url, blen);
g_gen++;
} else {
meow_img_free(img);
}
pthread_mutex_unlock(&g_cm);
}
return NULL;
}
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
void cover_request(const char *url)
{
if (!url || !url[0]) return;
pthread_mutex_lock(&g_cm);
if (g_cover_url[0] && strcmp(g_cover_url, url) == 0) { pthread_mutex_unlock(&g_cm); return; }
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url);
pthread_mutex_unlock(&g_cm);
if (!g_thr_on) {
g_thr_on = 1;
if (pthread_create(&g_thr, NULL, cover_worker, NULL) == 0)
pthread_detach(g_thr);
else
g_thr_on = 0;
}
}
/* UI 线程:读取当前封面(不拥有)。 */
meow_img *cover_get(void)
{
meow_img *im;
pthread_mutex_lock(&g_cm);
im = g_cover_img;
pthread_mutex_unlock(&g_cm);
return im;
}
/* 是否有新封面待绘制(UI tick 判变)。 */
int cover_changed(void) { return g_gen != g_acked; }
/* UI 消费一次:标记已见当前版本。 */
void cover_ack(void) { g_acked = g_gen; }
/* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无。 */
const char *cover_current_path(void)
{
const char *p;
pthread_mutex_lock(&g_cm);
p = g_cover_path[0] ? g_cover_path : NULL;
pthread_mutex_unlock(&g_cm);
return p;
}
void cover_cache_reset(void)
{
pthread_mutex_lock(&g_cm);
if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; }
g_cover_url[0] = 0;
g_tgt_url[0] = 0;
g_cover_path[0] = 0;
pthread_mutex_unlock(&g_cm);
g_gen++;
g_acked = g_gen;
}
+139
View File
@@ -0,0 +1,139 @@
/* logic/lyric.c — LRC 时间轴解析 + 当前行定位。
*
* 输入:ncm_api_lyric 返回的 lrc(原文)与 tlrc(翻译)。两者均为标准 [mm:ss.xx] 行。
* 解析:把所有带时间戳的行按时间升序排序,存 timestamp(ms)/text;翻译按相同时间戳合并到
* 对应原文行(存 tl)。无匹配的翻译行忽略。流式、有界:NCM_LYRIC_LINES 行,超限截断。
* 纯 C 无 UI,仅依赖标准库。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LRC_MAX_TEXT 192
static long s_t[NCM_LYRIC_LINES];
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
static int s_n;
static void lrc_parse(const char *lrc, int is_trans)
{
if (!lrc) return;
const char *p = lrc;
while (*p) {
/* 找行首 '[' */
while (*p && *p != '[') p++;
if (!*p) break;
/* 解析一个或多个时间标签 */
int have = 0;
long first_ms = 0;
while (*p == '[') {
int m = 0, s = 0, cs = 0;
if (sscanf(p, "[%d:%d.%d]", &m, &s, &cs) >= 2) {
long ms = (long)m * 60000L + (long)s * 1000L + (long)cs * 10L;
if (!have) { first_ms = ms; have = 1; }
/* 跳到本标签结尾 */
const char *q = strchr(p, ']');
if (!q) break;
p = q + 1;
} else {
/* 非时间标签([ti:..] 等)——跳过到行尾 */
const char *q = strchr(p, ']');
if (!q) return;
p = q + 1;
/* 若行内还有时间标签继续解析(极少),否则整行的正文以第一个文本为准 */
break;
}
}
if (have) {
/* 正文 = 到行尾(去 \r\n) */
const char *e = p;
while (*e && *e != '\n' && *e != '\r') e++;
size_t len = (size_t)(e - p);
if (len > LRC_MAX_TEXT - 1) len = LRC_MAX_TEXT - 1;
if (is_trans) {
/* 翻译行:按 first_ms 匹配到已有原文行 */
for (int i = 0; i < s_n; i++) {
if (s_t[i] == first_ms) {
memcpy(s_tl[i], p, len); s_tl[i][len] = 0;
break;
}
}
} else {
if (s_n < NCM_LYRIC_LINES) {
s_t[s_n] = first_ms;
memcpy(s_txt[s_n], p, len); s_txt[s_n][len] = 0;
s_tl[s_n][0] = 0;
s_n++;
}
}
}
while (*p && *p != '\n') p++; /* 跳到下一行 */
if (*p == '\n') p++;
}
}
static int cmp_lines(const void *a, const void *b)
{
const int *ia = (const int *)a, *ib = (const int *)b;
if (s_t[*ia] < s_t[*ib]) return -1;
if (s_t[*ia] > s_t[*ib]) return 1;
return 0;
}
int lyric_parse(const char *lrc, const char *tlrc)
{
lyric_clear();
lrc_parse(lrc, 0);
lrc_parse(tlrc, 1);
/* 按时间排序(用索引数组) */
int *idx = (int *)malloc(sizeof(int) * (size_t)(s_n ? s_n : 1));
if (!idx) return s_n;
for (int i = 0; i < s_n; i++) idx[i] = i;
qsort(idx, (size_t)s_n, sizeof(int), cmp_lines);
long ttmp[NCM_LYRIC_LINES];
char txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
char tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
memcpy(ttmp, s_t, sizeof(s_t));
memcpy(txt, s_txt, sizeof(s_txt));
memcpy(tl, s_tl, sizeof(s_tl));
for (int i = 0; i < s_n; i++) {
s_t[i] = ttmp[idx[i]];
memcpy(s_txt[i], txt[idx[i]], LRC_MAX_TEXT);
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
}
free(idx);
return s_n;
}
int lyric_line_at(int64_t ms)
{
int line = 0;
for (int i = 0; i < s_n; i++) {
if (s_t[i] <= ms) line = i;
else break;
}
return line;
}
const char *lyric_line_text(int i)
{
if (i < 0 || i >= s_n) return "";
return s_txt[i];
}
const char *lyric_line_tl(int i)
{
if (i < 0 || i >= s_n) return "";
return s_tl[i];
}
int lyric_count(void) { return s_n; }
void lyric_clear(void)
{
s_n = 0;
memset(s_t, 0, sizeof(s_t));
memset(s_txt, 0, sizeof(s_txt));
memset(s_tl, 0, sizeof(s_tl));
}
+259
View File
@@ -0,0 +1,259 @@
/* ncm_app_internal.h — os.meow.ncm 应用内部共享头(UI 各页 + app 装配层 + 宿主测试面)。
*
* 层级(对齐 hello_app/app.c 批② + files_app):
* - app.c main + 装配 + 事件分发(把 meow_ui_run 的平铺回调路由到各页)
* - app/ui/<page>.c 每页一个 view 文件(login/home/playlist/player/settings)
* - app/ui/common/chrome.c 共享 chrome(topbar / 内容区几何 / 空态 / 提示)
* - app/logic/ appstate(全局 ctx/歌单/歌曲/播放态)+ lyric(LRC 解析) + cover(封面缓存)
* - app/play/ player_manager(播放线程) + audio_out(ALSA/文件输出) + ipc_media(媒体会话 IPC)
*
* 铁律(APPS.md §3.4.1 / WORK_STATE.md):物理键禁映射——on_key 不处理任何键语义;
* 返回=左缘左滑(on_swipe LEFT)/返回钮/meow_ui_request_exit;左缘返回:播放页→歌单→主页→退出。
* 播放/网络/IPC 用独立线程,UI 不卡。媒体会话经 meow_ipc.h 帧格式最小客户端上报
* MSG_META + 收 MSG_PLAYCTL(SDK 尚无媒体封装,App 内实现)。
*/
#ifndef NCM_APP_INTERNAL_H
#define NCM_APP_INTERNAL_H
#include <stdint.h>
#include <stddef.h>
#include <pthread.h>
#include "meow_ui.h"
#include "meow_ui_view.h"
#include "ncm.h" /* 核心库伞头(ctx/api/login/cache/...) */
/* ---- Activity 归属(§6.1 ①)---- */
#define ACT_LOGIN 0 /* 扫码登录页(根,未登录) */
#define ACT_HOME 1 /* 我的歌单(根/主页,已登录) */
#define ACT_PLAYLIST 2 /* 歌单详情(曲目列表) */
#define ACT_PLAYER 3 /* 播放页(可横竖屏切,v1 允许切) */
#define ACT_SETTINGS 4 /* 设置页 */
/* ---- 布局 ---- */
#define APP_W 480
#define APP_H 720
#define APP_TOPBAR_H 56
#define APP_ROW_H 56 /* 列表行高 */
#define APP_PAD 16
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
#define APP_FOOTER_H 26
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
#define NCM_SONG_MAX 256 /* 单个歌单曲目上限(大歌单截断前 256 条) */
#define NCM_SONG_BATCH 64 /* song_detail 分批每批条数 */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
/* ---- 播放/媒体会话状态(play 线程 ↔ UI/IPC 共享,mu 保护)---- */
enum {
NCM_PC_STOPPED = 0,
NCM_PC_PLAYING = 1,
NCM_PC_PAUSED = 2,
NCM_PC_BUFFER = 3, /* 缓冲/加载中 */
};
/* 播放器命令(IPC MSG_PLAYCTL cmd 一一对应)*/
enum {
NCM_CTL_PLAY = 1,
NCM_CTL_PAUSE = 2,
NCM_CTL_PREV = 3,
NCM_CTL_NEXT = 4,
NCM_CTL_SEEK = 5,
};
/* 跨线程播放运行时状态(main 只读展示;play 线程写,IPC 线程读)。 */
typedef struct ncm_play_run {
pthread_mutex_t mu;
int state; /* NCM_PC_* */
int cur_song; /* 当前曲目在 g_st.songs[] 下标;-1=无 */
int64_t pos_ms; /* 当前播放位置(播放进程推进,seek 写此) */
int64_t dur_ms; /* 当前曲目标称时长 */
int cmd; /* 暂存最新内部命令(play/pause/prev/next/seek)*/
int64_t cmd_seek_ms; /* cmd==SEEK 目标 */
int seq; /* 播放代际(每次 start 递增,用于区分过期信号)*/
int err; /* 最近一次播放错误(0=无) */
char errmsg[96];
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
} ncm_play_run_t;
/* ---- 音质档位字符串(设置页 + 持久化用)---- */
extern const char *media; /* 占位 */
/* ---- App 全局状态(main 线程拥有;play 线程经 ncm_play_run 共享)---- */
typedef struct ncm_app_state {
ncm_ctx_t ctx; /* 核心库上下文(device_id/cookie/cache) */
char data_dir[512]; /* 数据目录(MeowData/os.meow.ncm/data) */
char cache_dir[512];
/* 登录 */
char unikey[64];
char qr_url[256];
unsigned char qr_mods[177*177]; /* QR 位图矩阵(1=暗) */
int qr_size;
int login_state; /* NCM_LOGIN_* */
int logged_in;
/* 账号 */
char uid[32];
char nickname[128];
/* 我的歌单 */
ncm_playlist_t playlists[NCM_PLAYLIST_MAX];
int playlist_count;
int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目 */
long song_ids[NCM_SONG_MAX];
ncm_song_t songs[NCM_SONG_MAX];
int song_count;
/* 播放页视图态 */
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
meow_img *cover_img; /* 当前封面(解码后;UI 持有,play 线程不碰) */
char cover_url[512]; /* 当前封面 URL(playlist/song cover) */
/* 歌词(播放器加载后由 play 线程填充,UI 读)*/
pthread_mutex_t lyric_mu;
int lyric_line_count;
int cur_lyric_line;
/* 设置(持久化 MeowData/os.meow.ncm/data)*/
int level; /* NCM_LEVEL_* */
int cache_limit_gb;
} ncm_app_state_t;
extern ncm_app_state_t g_st; /* 全局状态(app.c 定义) */
extern ncm_play_run_t g_play; /* 播放运行态(app.c 定义) */
extern volatile int g_app_quit; /* 请求退出(线程停) */
extern volatile int g_net_ready; /* 网络/核心库线程安全可用 */
extern int g_host_mock; /* 1=宿主/测试:跳过网络,用预置数据 */
/* ---- 通用矩形 ---- */
typedef struct { int x, y, w, h; } ncm_rect_t;
#define NR(x,y,w,h) ((ncm_rect_t){ (x),(y),(w),(h) })
/* ---- chrome(ui/common/chrome.c)---- */
int ncm_statusbar(void);
ncm_rect_t ncm_topbar_rect(void);
ncm_rect_t ncm_content_rect(void);
meow_widget *ncm_topbar_create(const char *title, int back, const char *action_label);
void ncm_draw_empty(ncm_rect_t r, const char *kind, const char *what);
void ncm_draw_footer(ncm_rect_t r, const char *text);
void ncm_snack(const char *text); /* 底部 2s 提示条(非阻塞) */
void chrome_tick(void); /* 周期检查 snack 到期清除 */
/* ---- 页面(ui/<page>.c)---- */
/* login */
meow_view *login_view_create(void);
int login_is_active(void);
void login_on_tap(int x, int y);
void login_tick(uint64_t now);
int login_start(void); /* 触发 unikey 获取(宿主测试:无网络则返回) */
const char *login_status_str(int state);
/* home */
meow_view *home_view_create(void);
int home_is_active(void);
int home_on_tap(int x, int y);
void home_swipe(int dir, int dist);
void home_touch_down(int x, int y);
void home_touch_move(int x, int y);
void home_reload(void); /* 刷新我的歌单 */
int home_go_up(void); /* 左缘返回:到底则退出 */
/* playlist */
meow_view *playlist_view_create(void);
int playlist_is_active(void);
int playlist_on_tap(int x, int y);
void playlist_swipe(int dir, int dist);
void playlist_touch_down(int x, int y);
void playlist_touch_move(int x, int y);
void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */
void playlist_refresh(void);
/* player */
meow_view *player_view_create(void);
int player_is_active(void);
void player_on_tap(int x, int y);
void player_tick(uint64_t now); /* 播放进度/歌词滚动 */
void player_play_song(int idx); /* 从歌单第 idx 首开始播放 */
void player_pause_toggle(int on);
void player_next(int dir); /* dir=+1 下一首 / -1 上一首 */
void player_seek(int64_t ms);
void player_on_swipe(int dir, int dist); /* 横屏切换/返回 */
int player_set_screen(int landscape); /* 1=横屏 0=竖屏;返回之前 */
int player_screen(void);
/* settings */
meow_view *settings_view_create(void);
int settings_is_active(void);
int settings_on_tap(int x, int y);
void settings_swipe(int dir, int dist);
void settings_touch_down(int x, int y);
void settings_touch_move(int x, int y);
void settings_open(void); /* 进设置页 */
void settings_reload(void);
/* ---- logic ---- */
int appstate_init(void); /* 初始化 ctx/data/cookie/设置;0=成功 */
void appstate_load_settings(void);
void appstate_save_settings(void);
void appstate_set_logged_in(const char *uid, const char *nickname);
void appstate_logout(void);
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
int appstate_load_playlists(void); /* 拉我的歌单 */
int appstate_load_playlist_songs(int idx); /* 拉歌单详情+曲目详情 */
void appstate_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
/* lyric */
int lyric_parse(const char *lrc, const char *tlrc); /* 填充 g_lyric_* */
int lyric_line_at(int64_t ms); /* 当前行 */
const char *lyric_line_text(int i);
const char *lyric_line_tl(int i);
int lyric_count(void);
void lyric_clear(void);
/* cover */
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
meow_img *cover_get(void); /* 当前封面(不拥有;NULL=无) */
int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */
void cover_ack(void); /* UI 消费一次(已见当前版本) */
const char *cover_current_path(void); /* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无 */
void cover_cache_reset(void);
/* ---- play(app/play/)---- */
/* audio_out */
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功 */
int audio_out_write(const short *samples, int frames); /* <0=错误/结束 */
void audio_out_close(void);
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
int audio_out_buffered_ms(void);
/* player_manager:播放线程(下载+解码+输出)驱动 */
int player_manager_start(void); /* 起播放线程;0=成功 */
void player_manager_stop(void); /* 停播(清曲目/置 STOPPED) */
void player_manager_stop_thread(void);/* 停线程(app 退出时 join) */
void player_manager_play(int song_idx); /* 加载并播放 */
void player_manager_pause(int on); /* on=1 暂停 */
void player_manager_next(int dir); /* dir=+1/-1 */
void player_manager_seek(int64_t ms);
void player_manager_set_quality(int level, int cache_gb);
int player_manager_state(void); /* NCM_PC_* */
int player_manager_playing_song(void);
int64_t player_manager_pos_ms(void);
int64_t player_manager_dur_ms(void);
int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */
int player_manager_error(void);
/* ipc_media:媒体会话上报/收指令 */
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
void ipc_media_stop(void);
/* ---- 宿主测试面(app/test/test_ncm_app.c 驱动真实页面)---- */
void app_host_set_mock(int mode); /* 0=无 1=home 2=playlist 3=player 4=settings 5=login */
void app_host_paint_page(int page); /* 构建指定页并整帧绘制到 back_buf(供 dump)*/
#endif /* NCM_APP_INTERNAL_H */
+166
View File
@@ -0,0 +1,166 @@
/* play/audio_out.c — 音频输出抽象:ALSA 直开 hw:0,0(44.1k/16bit/立体声)或 FILE/NULL sink。
*
* 设备(mipsel musl 无 libasound):用 ALSA 内核 UAPI(sound/asound.h)直开 /dev/snd/pcmC0D0p,
* 非 mmap RW 模式下 ioctl 设 hw_params + write() 写交织帧。布局/时序真机待验证(本层只保证跨编译 +
* 宿主回退正确)。
* 宿主/qemu 验证:无 /dev/snd/pcmC0D0p → 回退 FILE sink(写 WAV,env NCM_AUDIO_OUT 指定路径,
* 缺省 /tmp/ncm_out.wav)或 NULL(env NCM_AUDIO_SINK=null,丢弃,仅测解码 RT 用)。
* 线程模型:仅由 player_manager 播放线程调用(单写者);audio_out_close 可被 UI 线程调用打断阻塞写。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#ifdef __linux__
#include <sound/asound.h>
#include <sys/ioctl.h>
#define NCM_HAVE_ALSA 1
#endif
enum { SINK_ALSADEV = 0, SINK_FILE = 1, SINK_NULL = 2 };
static int g_sink = -1;
static int g_fd = -1; /* alsa or file fd */
static int g_rate, g_ch, g_bits;
static const char *g_path = NULL;
#define NCM_DEFAULT_OUT "/tmp/ncm_out.wav"
void audio_out_set_target(int ms) { (void)ms; }
int audio_out_buffered_ms(void) { return 0; }
#ifdef NCM_HAVE_ALSA
/* ALSA 非 mmap 直装:开设备 → hw_params(access=rw_interleaved, S16_LE, rate, 2ch, s16)→ write。
* 注意:musl 的 sound/asound.h 中 masks/intervals 是**重索引**数组:
* masks[k] ↔ hw_param (FIRST_MASK + k); intervals[k] ↔ hw_param (FIRST_INTERVAL + k)。
* 故访问时须减 FIRST_MASK/FIRST_INTERVAL,否则越界/错位。 */
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
static void pivset(struct snd_interval *iv, unsigned val)
{
iv->min = iv->max = val;
iv->integer = 1;
iv->openmin = iv->openmax = 0;
iv->empty = 0;
}
static int alsa_open(int rate, int ch, int bits)
{
if (bits != 16) return -1;
int fd = open("/dev/snd/pcmC0D0p", O_RDWR);
if (fd < 0) return -1;
struct snd_pcm_hw_params p;
memset(&p, 0, sizeof p);
p.flags = 0;
pmask(&p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK],
SNDRV_PCM_ACCESS_RW_INTERLEAVED);
pmask(&p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK],
SNDRV_PCM_FORMAT_S16_LE);
pmask(&p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK],
SNDRV_PCM_SUBFORMAT_STD);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL],
(unsigned)ch);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL],
(unsigned)rate);
p.rmask = ~0u;
p.info = 0;
p.msbits = 0;
p.rate_num = 0;
p.rate_den = 0;
if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p) != 0) { close(fd); return -1; }
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0);
return fd;
}
#endif
/* 写 WAV 头(44 字节标准 PCM 布局:RIFF/WAVE/fmt(16)/data)。
* fmt(16) = format(2) ch(2) sample_rate(4) byte_rate(4) block_align(2) bits(2)。 */
static void file_write_header(int fd, int rate, int ch, int bits, uint32_t data_len)
{
unsigned char h[44];
memset(h, 0, sizeof h);
memcpy(h+0, "RIFF", 4);
uint32_t le = 36 + data_len; memcpy(h+4, &le, 4);
memcpy(h+8, "WAVE", 4);
memcpy(h+12, "fmt ", 4);
le = 16; memcpy(h+16, &le, 4);
uint16_t s16 = 1; memcpy(h+20, &s16, 2); /* PCM */
s16 = (uint16_t)ch; memcpy(h+22, &s16, 2);
le = (uint32_t)rate; memcpy(h+24, &le, 4); /* sample rate */
le = (uint32_t)(rate * ch * bits / 8); memcpy(h+28, &le, 4); /* byte rate */
s16 = (uint16_t)(ch * bits / 8); memcpy(h+32, &s16, 2); /* block align */
s16 = (uint16_t)bits; memcpy(h+34, &s16, 2); /* bits/sample */
memcpy(h+36, "data", 4);
le = data_len; memcpy(h+40, &le, 4);
if (write(fd, h, 44) != 44) { /* 忽略 */ }
}
int audio_out_open(int sample_rate, int channels, int bits)
{
g_rate = sample_rate; g_ch = channels; g_bits = bits;
const char *env = getenv("NCM_AUDIO_SINK");
g_path = getenv("NCM_AUDIO_OUT");
if (!g_path || !g_path[0]) g_path = NCM_DEFAULT_OUT;
/* 显式 null(测 RT) */
if (env && strcmp(env, "null") == 0) { g_sink = SINK_NULL; g_fd = -1; return 0; }
/* 尝试 ALSA(仅未显式指定 file;宿主无设备自动回退) */
if (!(env && strcmp(env, "file") == 0)) {
#ifdef NCM_HAVE_ALSA
int fd = alsa_open(sample_rate, channels, bits);
if (fd >= 0) { g_sink = SINK_ALSADEV; g_fd = fd; return 0; }
#endif
}
/* FILE sink */
int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { g_sink = SINK_NULL; g_fd = -1; return 0; } /* 无路可写 → null */
file_write_header(fd, sample_rate, channels, bits, 0);
g_sink = SINK_FILE;
g_fd = fd;
return 0;
}
int audio_out_write(const short *samples, int frames)
{
if (g_app_quit) return -1;
if (g_sink == SINK_NULL || g_sink < 0) return frames;
size_t n = (size_t)frames * (size_t)g_ch * (size_t)(g_bits / 8);
if (g_sink == SINK_ALSADEV) {
/* 阻塞尽力写满 - EINTR/EAGAIN 重试;错误返回 */
const unsigned char *p = (const unsigned char *)samples;
size_t off = 0;
while (off < n) {
ssize_t w = write(g_fd, p + off, n - off);
if (w > 0) { off += (size_t)w; continue; }
if (w < 0 && (errno == EINTR || errno == EAGAIN)) continue;
return -1;
}
return frames;
}
if (g_sink == SINK_FILE) {
if (write(g_fd, samples, n) != (ssize_t)n) return -1;
return frames;
}
return frames;
}
void audio_out_close(void)
{
if (g_sink == SINK_FILE && g_fd >= 0) {
/* 回填:offset 4 = RIFF 总长-36;offset 40 = data 长度 */
off_t end = lseek(g_fd, 0, SEEK_END);
if (end > 44) {
uint32_t data_len = (uint32_t)(end - 44);
lseek(g_fd, 4, SEEK_SET);
uint32_t le = 36 + data_len;
if (write(g_fd, &le, 4) != 4) { }
lseek(g_fd, 40, SEEK_SET);
if (write(g_fd, &data_len, 4) != 4) { }
}
}
if (g_fd >= 0) close(g_fd);
g_fd = -1;
g_sink = -1;
}
+212
View File
@@ -0,0 +1,212 @@
/* play/ipc_media.c — 媒体会话 IPC 最小客户端(SDK 尚无媒体封装,App 内用 meow_ipc.h 帧格式直连)。
*
* 连接:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。HELLO(app_id,
* MF_PLAYBACK) 注册为媒体源;之后**同一连接**周期发 MSG_META(~1s,进度随播放推进)并阻塞等
* launcher 下发 MSG_PLAYCTL(播放/暂停/上下曲/seek)。launcher 经该连接 fd 回发(meow_media.c
* send_playctl 用最近活跃会话的 fd)。无 socket/launcher 不跑 → 优雅跳过(打日志不崩)。
* 线程模型:独立线程(main 起了 meow_ui_run 阻塞;播放线程归 player_manager,本线程只做 IPC 收发)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "meow_ipc.h"
static pthread_t g_thr;
static int g_thr_on;
static int g_quit;
static const char *sock_path(void)
{
const char *e = getenv("MEOW_IPC_SOCK");
return (e && e[0]) ? e : MEOW_IPC_PATH;
}
static int connect_path(const char *path)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
return fd;
}
/* 发送一帧(头+负载)。 */
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) return -1;
off += (size_t)w;
}
if (len && payload) {
off = 0;
while (off < len) {
ssize_t w = write(fd, (const uint8_t *)payload + off, len - off);
if (w <= 0) return -1;
off += (size_t)w;
}
}
return 0;
}
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
size_t off = 0;
while (off < sizeof hdr) {
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
if (r <= 0) return -1;
off += (size_t)r;
}
uint32_t magic, len; uint16_t ver;
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
memcpy(&len, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
off = 0;
while (off < len) {
ssize_t r = read(fd, rbuf + off, len - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return (int)len;
}
/* 组并发送一条 META。 */
static void send_meta(int fd, uint32_t req_id)
{
m_meta_t mm;
memset(&mm, 0, sizeof mm);
ncm_strlcpy(mm.app_id, "os.meow.ncm", sizeof mm.app_id);
int idx = player_manager_playing_song();
pthread_mutex_lock(&g_play.mu);
int pos = (int)g_play.pos_ms;
int dur = (int)g_play.dur_ms;
int state = g_play.state;
int err = g_play.err;
pthread_mutex_unlock(&g_play.mu);
static char title[128], artist[128], album[128];
title[0] = artist[0] = album[0] = 0;
if (idx >= 0 && idx < g_st.song_count) {
snprintf(title, sizeof title, "%s", g_st.songs[idx].name);
snprintf(artist, sizeof artist, "%s", g_st.songs[idx].art_name);
snprintf(album, sizeof album, "%s", g_st.songs[idx].album_name);
}
snprintf(mm.title, sizeof mm.title, "%s", title);
snprintf(mm.artist, sizeof mm.artist, "%s", artist);
snprintf(mm.album, sizeof mm.album, "%s", album);
mm.pos_ms = (uint32_t)(pos < 0 ? 0 : pos);
mm.dur_ms = (uint32_t)(dur < 0 ? 0 : dur);
uint32_t st = 0;
if (err != 0) st = 0;
else if (state == NCM_PC_PLAYING) st = 1;
else if (state == NCM_PC_PAUSED) st = 2;
else if (state == NCM_PC_BUFFER) st = 3;
else st = 0;
mm.state = st;
/* 封面:若已落盘(cover 缓存文件)则提供路径 */
mm.flags = 0;
const char *cp = cover_current_path();
if (cp && cp[0]) {
snprintf(mm.artwork, sizeof mm.artwork, "%s", cp);
mm.flags |= MF_ART | MF_COVER;
}
if (lyric_count() > 0) mm.flags |= MF_LYRIC;
send_frame(fd, MSG_META, req_id, &mm, sizeof mm);
}
static void apply_playctl(const m_playctl_t *c)
{
switch (c->cmd) {
case NCM_CTL_PLAY: player_manager_pause(0); break;
case NCM_CTL_PAUSE: player_manager_pause(1); break;
case NCM_CTL_PREV: player_manager_next(-1); break;
case NCM_CTL_NEXT: player_manager_next(1); break;
case NCM_CTL_SEEK: player_manager_seek(c->seek_ms); break;
default: break;
}
}
static void *ipc_thread(void *arg)
{
(void)arg;
int fd = connect_path(sock_path());
if (fd < 0) { meow_log(MEOW_LOG_WARN, "ipc: no launcher socket"); return NULL; }
m_hello_t h;
memset(&h, 0, sizeof h);
ncm_strlcpy(h.app_id, "os.meow.ncm", sizeof h.app_id);
h.version = 1;
h.flags = MF_PLAYBACK;
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) != 0) { close(fd); return NULL; }
uint8_t hb[256];
uint16_t htype;
if (read_frame(fd, &htype, hb, sizeof hb) < 0) { close(fd); return NULL; }
send_meta(fd, 2); /* 首条注册媒体源(state=0 未播也登记,start 播后自动成源) */
uint64_t last_meta = 0;
while (!g_quit && !g_app_quit) {
struct pollfd p;
p.fd = fd; p.events = POLLIN;
int pr = poll(&p, 1, 200);
if (pr > 0 && (p.revents & POLLIN)) {
uint8_t rbuf[MEOW_IPC_MAX];
uint16_t type;
int n = read_frame(fd, &type, rbuf, sizeof rbuf);
if (n < 0) break;
if (type == MSG_PLAYCTL && n >= (int)sizeof(m_playctl_t)) {
m_playctl_t c;
memcpy(&c, rbuf, sizeof c);
apply_playctl(&c);
}
} else if (pr < 0 && errno != EINTR) {
break;
}
uint64_t now = meow_ui_time_ms();
if (now - last_meta >= 1000) {
send_meta(fd, (uint32_t)(now & 0x7FFFFFFF));
last_meta = now;
}
}
close(fd);
return NULL;
}
int ipc_media_start(void)
{
if (g_thr_on) return 0;
g_quit = 0;
if (pthread_create(&g_thr, NULL, ipc_thread, NULL) != 0) return -1;
pthread_detach(g_thr);
g_thr_on = 1;
return 0;
}
void ipc_media_stop(void)
{
g_quit = 1;
/* 线程在 200ms poll 内退出(detach,不 join)*/
g_thr_on = 0;
}
+4
View File
@@ -0,0 +1,4 @@
/* play/minimp3_impl.c — minimp3(x) 唯一实现 TU(header-only:define 实现一次)。
* 编译时 -I<vendor>/minimp3。宿主与 mipsel 共用。 */
#define MINIMP3_IMPLEMENTATION
#include "minimp3_ex.h"
+321
View File
@@ -0,0 +1,321 @@
/* play/player_manager.c — 播放线程:URL 获取(Hi-res 逐档降级)→缓存(命中直接播/否则流式落盘)→
* minimp3 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
*
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
* seek=重开解码器+输出并 seek 到目标 ms(字节 offset 近似);下一/上一首=打断当前 run_song 返回外层。
* 所有跨线程共享状态在 g_play (mu)。g_st.ctx 的 device_id/cookie/cache 在播放期间稳定(登出会先停播)。
* 返回码/错误经 g_play.err/errmsg 供 UI 提示。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "minimp3_ex.h"
static pthread_t g_thr;
static int g_thr_on;
static int g_want_run = 1; /* 线程运行标志(quit=0) */
#define PCM_CHUNK 8192 /* 每块交织样本数(双声道 → 4096 帧) */
/* ---- 线程内部一次性歌的状态 ---- */
static char g_path[512]; /* 本地 mp3 路径(缓存) */
static int64_t g_file_bytes;
static int g_level; /* 音质档(来自设置) */
static int g_cache_gb;
int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; }
int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; }
int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; }
int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; }
int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; }
void player_manager_set_quality(int level, int cache_gb)
{
g_level = level;
g_cache_gb = cache_gb;
pthread_mutex_lock(&g_play.mu);
g_play.err = 0;
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_play(int song_idx)
{
pthread_mutex_lock(&g_play.mu);
g_play.cur_song = song_idx;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
g_play.pos_ms = 0;
g_play.seq++;
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_pause(int on)
{
pthread_mutex_lock(&g_play.mu);
if (on) { if (g_play.state == NCM_PC_PLAYING) g_play.state = NCM_PC_PAUSED; }
else { if (g_play.state == NCM_PC_PAUSED) g_play.state = NCM_PC_PLAYING; }
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_seek(int64_t ms)
{
pthread_mutex_lock(&g_play.mu);
g_play.cmd = NCM_CTL_SEEK;
g_play.cmd_seek_ms = ms;
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_next(int dir)
{
pthread_mutex_lock(&g_play.mu);
int c = g_play.cur_song + dir;
if (c < 0) c = 0;
if (c >= g_st.song_count) c = g_st.song_count - 1;
if (c != g_play.cur_song) {
g_play.cur_song = c;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
g_play.pos_ms = 0;
g_play.seq++;
}
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_stop(void)
{
pthread_mutex_lock(&g_play.mu);
g_play.cur_song = -1;
g_play.state = NCM_PC_STOPPED;
g_play.pos_ms = 0;
g_play.seq++;
pthread_mutex_unlock(&g_play.mu);
}
/* ---- 每首歌的播放体(在外层循环内调用)---- */
static int download_cb(void *ctx, const unsigned char *data, size_t len)
{
return ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
}
/* 得到本地 mp3 路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
* 返回 NCM_OK 或负错误码。 */
static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes)
{
const ncm_song_t *s = &g_st.songs[idx];
const char *id = s->id;
/* 缓存命中 → 离线直接播 */
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) == NCM_OK) {
struct stat st;
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
}
/* 播放地址:从 g_level 降级,只收 mp3 */
ncm_play_url_t u;
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, g_level, &u);
if (rc != NCM_OK) return rc;
if (!u.url[0]) return NCM_ERR_NOTFOUND;
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
ncm_cache_writer_t *w = NULL;
rc = ncm_cache_open(&g_st.ctx.cache, id, ".mp3", &w);
if (rc != NCM_OK) return rc;
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = u.url;
req.method = "GET";
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 30000;
ncm_http_resp_t resp;
size_t maxb = (u.size > 0 && u.size < (64LL << 20)) ? (size_t)u.size : (64u << 20);
rc = ncm_http_download(&req, download_cb, w, maxb, &resp);
if (rc != NCM_OK || resp.status != 200) {
ncm_cache_abort(w);
return (rc != NCM_OK) ? rc : NCM_ERR_HTTP;
}
rc = ncm_cache_commit(w);
if (rc != 0) return rc;
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) != NCM_OK)
return NCM_ERR_NOENT;
struct stat st;
if (stat(path, &st) == 0) *bytes = (int64_t)st.st_size;
return NCM_OK;
}
static void set_err(int err, const char *msg)
{
pthread_mutex_lock(&g_play.mu);
g_play.err = err;
if (msg) { ncm_strlcpy(g_play.errmsg, msg, sizeof g_play.errmsg); }
g_play.state = NCM_PC_STOPPED;
pthread_mutex_unlock(&g_play.mu);
}
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
static void run_song(int idx)
{
int64_t file_bytes = 0;
char path[512];
int rc = song_local_file(idx, path, sizeof path, &file_bytes);
if (rc != NCM_OK) {
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
set_err(rc, m);
return;
}
ncm_strlcpy(g_path, path, sizeof g_path);
g_file_bytes = file_bytes;
const ncm_song_t *s = &g_st.songs[idx];
int64_t dur = s->duration_ms > 0 ? s->duration_ms : 0;
mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "decode open failed"); return; }
/* 音频输出:解码器输出 S16_LE(minimp3 默认);用 44100/2ch 直开 */
if (audio_out_open(44100, 2, 16) != 0) { mp3dec_ex_close(&dec); set_err(NCM_ERR_IO, "audio open failed"); return; }
/* 状态→播放 */
pthread_mutex_lock(&g_play.mu);
g_play.state = NCM_PC_PLAYING;
g_play.pos_ms = 0;
g_play.dur_ms = dur;
g_play.err = 0;
pthread_mutex_unlock(&g_play.mu);
mp3d_sample_t pcm[PCM_CHUNK];
int64_t frames_done = 0; /* 已输出帧(每通道) */
int need_seek_ms = -1;
for (;;) {
if (g_app_quit) break;
/* 取命令 */
pthread_mutex_lock(&g_play.mu);
int cmd = g_play.cmd; g_play.cmd = 0;
int64_t seek_ms = g_play.cmd_seek_ms;
int cur = g_play.cur_song;
int st = g_play.state;
pthread_mutex_unlock(&g_play.mu);
if (cur != idx) break; /* 切歌 → 回外层 */
if (st == NCM_PC_STOPPED) break;
if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; }
if (st == NCM_PC_PAUSED) { usleep(40 * 1000); continue; } /* 冻结 */
if (need_seek_ms >= 0) {
/* 重开并 seek:关闭后重开(字节 offset 近似) */
audio_out_close();
mp3dec_ex_close(&dec);
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "seek reopen failed"); break; }
if (audio_out_open(44100, 2, 16) != 0) { set_err(NCM_ERR_IO, "audio reopen failed"); break; }
int64_t target = (g_file_bytes > 0 && dur > 0)
? (int64_t)g_file_bytes * need_seek_ms / dur : 0;
mp3dec_ex_seek(&dec, (uint64_t)(target > 0 ? target : 0));
frames_done = (int64_t)need_seek_ms * 44100 / 1000;
need_seek_ms = -1;
continue;
}
size_t got = mp3dec_ex_read(&dec, pcm, PCM_CHUNK);
if (got == 0) break; /* 播完 */
int frames = (int)(got / 2);
if (audio_out_write(pcm, frames) != frames) break;
frames_done += frames;
/* 更新进度(~200ms 节流)*/
int64_t pos = frames_done * 1000 / 44100;
pthread_mutex_lock(&g_play.mu);
g_play.pos_ms = pos;
if (dur > 0 && pos >= dur) g_play.pos_ms = dur;
pthread_mutex_unlock(&g_play.mu);
}
audio_out_close();
mp3dec_ex_close(&dec);
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
}
/* 外层播放线程循环 */
static void *player_thread(void *arg)
{
(void)arg;
while (g_want_run && !g_app_quit) {
pthread_mutex_lock(&g_play.mu);
int cur = g_play.cur_song;
int st = g_play.state;
pthread_mutex_unlock(&g_play.mu);
if (cur < 0 || cur >= g_st.song_count || st == NCM_PC_STOPPED) {
usleep(60 * 1000);
continue;
}
/* 以本地快照播放(idx 被外层线程独占,无需再锁) */
run_song(cur);
pthread_mutex_lock(&g_play.mu);
int cur2 = g_play.cur_song;
int st2 = g_play.state;
pthread_mutex_unlock(&g_play.mu);
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
if (cur2 >= 0 && cur2 < g_st.song_count && st2 != NCM_PC_STOPPED && cur2 == cur) {
int nx = cur + 1;
if (nx < g_st.song_count) { player_manager_next(1); }
else { player_manager_stop(); }
} else {
usleep(50 * 1000);
}
}
return NULL;
}
/* 为当前曲目拉歌词并解析(UI 线程调):buf 有界,解析后存 lyric 模块。 */
int player_manager_load_lyric(void)
{
lyric_clear();
int idx = player_manager_playing_song();
if (idx < 0 || idx >= g_st.song_count) return NCM_ERR_ARG;
const ncm_song_t *s = &g_st.songs[idx];
char *lrc = (char *)malloc(64 * 1024);
char *tl = (char *)malloc(32 * 1024);
if (!lrc || !tl) { free(lrc); free(tl); return NCM_ERR_OOM; }
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, s->id, lrc, 64 * 1024, tl, 32 * 1024);
if (rc == NCM_OK) lyric_parse(lrc, tl);
free(lrc);
free(tl);
return rc;
}
int player_manager_start(void)
{
if (g_thr_on) return 0;
g_want_run = 1;
g_level = g_st.level;
g_cache_gb = g_st.cache_limit_gb;
pthread_mutex_init(&g_play.mu, NULL);
if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1;
g_thr_on = 1;
return 0;
}
void player_manager_stop_thread(void)
{
g_want_run = 0;
g_app_quit = 1;
audio_out_close(); /* 打断阻塞写 */
if (g_thr_on) {
pthread_join(g_thr, NULL);
g_thr_on = 0;
}
}
+67
View File
@@ -0,0 +1,67 @@
/* test/test_decode.c — 播放解码链路验证(宿主):minimp3 解码 mp3 → audio_out(FILE sink) 写 WAV。
* 不依赖网络/登录(输入本地 mp3)。验证:解码无错、WAV 非空、时长合理、RT 因子。
* 运行:NCM_AUDIO_SINK=file NCM_AUDIO_OUT=/tmp/ncm_decode.wav ./test_decode <path.mp3>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../ncm_app_internal.h" /* 引 audio_out / minimp3 声明 */
#include "minimp3_ex.h"
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
static double now_s(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + ts.tv_nsec / 1e9;
}
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "vendor/minimp3/vectors/performance/MIPSTest.mp3";
mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { fprintf(stderr, "open failed\n"); return 1; }
static mp3d_sample_t pcm[8192 * 2];
/* 读首帧确定采样率/声道(read 会填 dec.info)*/
size_t got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
int sr = dec.info.hz;
int ch = dec.info.channels;
if (sr <= 0 || ch <= 0) { fprintf(stderr, "bad stream\n"); return 1; }
printf("mp3: %s %dHz x%d detected_samples=%lld bitrate=%dkbps\n",
path, sr, ch, (long long)dec.samples, dec.info.bitrate_kbps);
if (audio_out_open(sr, ch, 16) != 0) { fprintf(stderr, "audio_out open failed\n"); return 1; }
long long frames = 0;
double t0 = now_s();
if (got > 0) {
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); return 1; }
frames += fr;
}
for (;;) {
if (g_app_quit) break;
got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
if (got == 0) break;
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); break; }
frames += fr;
}
double dt = now_s() - t0;
audio_out_close();
mp3dec_ex_close(&dec);
double secs = (double)frames / (double)sr;
double rt = (dt > 0) ? secs / dt : 0;
printf("decoded %lld frames = %.2f s audio (%d Hz) in %.3f s => %.1fx RT\n",
frames, secs, sr, dt, rt);
printf("OK: %s\n", (frames > 0) ? "decode chain works" : "EMPTY");
return (frames > 0) ? 0 : 1;
}
+163
View File
@@ -0,0 +1,163 @@
/* test/test_ncm_app.c — 宿主验证:真实页面 view + 真实 SDK 绘制,MEOW_UI_DUMP 导 PPM 截图目检。
*
* 编译见 build_ncm_host.sh。运行:NCM_PAGE=login|home|playlist|player|lands|settings \
* MEOW_UI_DUMP=/tmp/ncm_<page>.ppm ./ncm_host
* 不依赖 /dev/fb0(SDK 宿主导出 480x720 离屏);meow_font 打桩(ASCII 5x7;中文不渲染,仅验布局)。
* 数据全 mock(g_host_mock=1,页面跳过网络)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
static void mock_playlists(void)
{
static const char *names[] = { "My Favorites", "Focus Mix", "Commute", "Workout", "Sleep" };
static const int counts[] = { 128, 56, 34, 22, 15 };
g_st.playlist_count = 5;
g_st.logged_in = 1;
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
for (int i = 0; i < 5; i++) {
snprintf(g_st.playlists[i].id, sizeof g_st.playlists[i].id, "pl%d", i);
snprintf(g_st.playlists[i].name, sizeof g_st.playlists[i].name, "%s", names[i]);
g_st.playlists[i].track_count = counts[i];
snprintf(g_st.playlists[i].cover, sizeof g_st.playlists[i].cover,
"http://p1.music.126.net/cover%d.jpg", i);
}
}
static void mock_songs(void)
{
static const char *sn[] = { "Song One", "Song Two", "Song Three",
"Song Four", "Song Five", "Song Six" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C",
"Artist D", "Artist E", "Artist F" };
g_st.song_count = 6;
g_st.active_playlist = 0;
for (int i = 0; i < 6; i++) {
snprintf(g_st.songs[i].id, sizeof g_st.songs[i].id, "%ld", 1000L + i);
snprintf(g_st.songs[i].name, sizeof g_st.songs[i].name, "%s", sn[i]);
snprintf(g_st.songs[i].art_name, sizeof g_st.songs[i].art_name, "%s", ar[i]);
snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "Album %d", i + 1);
g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = (i == 3) ? 1 : 0;
}
ncm_strlcpy(g_st.cover_url, "http://p1.music.126.net/cover0.jpg", sizeof g_st.cover_url);
g_st.playing_song = 0;
}
static void mock_lyric(void)
{
static const char *lrc =
"[00:00.00]Intro\n[00:05.00]First line of the song\n"
"[00:10.50]Second line here\n[00:15.20]Third line with translation\n"
"[00:20.80]Fourth line\n[00:26.10]Fifth\n[00:31.40]Sixth line\n";
static const char *tl =
"[00:10.50]Second translation\n[00:15.20]Third translation\n";
lyric_parse(lrc, tl);
}
static void mock_qr(void)
{
g_st.qr_size = 29;
for (int y = 0; y < 29; y++)
for (int x = 0; x < 29; x++) {
int border = (x < 3 || x >= 26 || y < 3 || y >= 26);
int finder = (x < 7 && y < 7) || (x >= 22 && y < 7) || (x < 7 && y >= 22);
g_st.qr_mods[y * 29 + x] = border || finder || ((x ^ y) & 1);
}
g_st.login_state = NCM_LOGIN_PENDING;
}
static int page_id(const char *name)
{
if (strcmp(name, "login") == 0) return ACT_LOGIN;
if (strcmp(name, "home") == 0) return ACT_HOME;
if (strcmp(name, "playlist") == 0) return ACT_PLAYLIST;
if (strcmp(name, "player") == 0) return ACT_PLAYER;
if (strcmp(name, "lands") == 0) return ACT_PLAYER; /* 横屏布局 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
return -1;
}
int main(int argc, char **argv)
{
const char *name = getenv("NCM_PAGE");
if (!name || !name[0]) name = "home";
char dump[512];
const char *dp = getenv("MEOW_UI_DUMP");
if (!dp || !dp[0]) snprintf(dump, sizeof dump, "/tmp/ncm_%s.ppm", name);
else snprintf(dump, sizeof dump, "%s", dp);
setenv("MEOW_UI_DUMP", dump, 1);
(void)argc; (void)argv;
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_add_listener(NULL);
/* mock 状态 */
if (g_st.cache_dir[0] == 0) {
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_host_cache");
}
if (strcmp(name, "login") == 0) mock_qr();
else if (strcmp(name, "loginlive") == 0) { /* 真实 unikey → 真实二维码 */
ncm_ctx_init(&g_st.ctx, "/tmp/ncm_host_login");
char unikey[64];
int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, sizeof unikey);
if (rc != NCM_OK) { fprintf(stderr, "unikey fetch failed rc=%d\n", rc); return 1; }
ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url);
int sz = 0;
if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, 177, &sz) != NCM_OK) {
fprintf(stderr, "qr matrix failed\n"); return 1;
}
g_st.qr_size = sz;
g_st.login_state = NCM_LOGIN_PENDING;
printf("live unikey=%s qr_url=%s size=%d\n", unikey, g_st.qr_url, sz);
}
else if (strcmp(name, "playlist") == 0) { mock_playlists(); mock_songs(); }
else if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
mock_playlists(); mock_songs(); mock_lyric();
pthread_mutex_lock(&g_play.mu);
g_play.state = NCM_PC_PLAYING;
g_play.cur_song = 0;
g_play.pos_ms = 94000;
g_play.dur_ms = 180000;
pthread_mutex_unlock(&g_play.mu);
} else if (strcmp(name, "settings") == 0) {
g_st.logged_in = 1;
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
g_st.level = NCM_LEVEL_HIRES;
g_st.cache_limit_gb = 10;
} else {
mock_playlists();
}
meow_view *v = NULL;
if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create();
else if (strcmp(name, "home") == 0) v = home_view_create();
else if (strcmp(name, "playlist") == 0) v = playlist_view_create();
else if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) v = player_view_create();
else if (strcmp(name, "settings") == 0) v = settings_view_create();
if (!v) { fprintf(stderr, "bad page\n"); return 1; }
if (strcmp(name, "lands") == 0) player_set_screen(1);
int pid = (strcmp(name, "loginlive") == 0) ? ACT_LOGIN : page_id(name);
meow_view_page_push(pid);
/* player 页:其 on_tick 通常驱动进度;这里直接再画一次确保状态就绪 */
meow_ui_paint_full();
if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
player_tick(meow_ui_time_ms());
}
meow_ui_shutdown();
printf("dumped %s -> %s\n", name, dump);
return 0;
}
+104
View File
@@ -0,0 +1,104 @@
/* ui/common/chrome.c — os.meow.ncm 共享 chrome(TopAppBar 建置 / 几何 / 空态&页脚 / 底部提示条)。
*
* 与 files_app/ui/common/chrome.c 同理:把「页面 chrome 固定不随内容重建」分出去。绘制全部读
* meow_theme_get() 语义色(§8.1),不硬编码配色。topbar 由 meow_topappbar 建,自动注册进绘制物
* 注册表(§6.2),owner=创建时 active activity。
*/
#include "../ncm_app_internal.h"
#include <string.h>
int ncm_statusbar(void)
{
int sb = meow_ui_statusbar_height();
return sb < 0 ? 0 : sb;
}
ncm_rect_t ncm_topbar_rect(void)
{
int sb = ncm_statusbar();
return NR(0, sb, meow_ui_width(), APP_TOPBAR_H);
}
ncm_rect_t ncm_content_rect(void)
{
int sb = ncm_statusbar();
int y = sb + APP_TOPBAR_H;
int h = meow_ui_height() - y;
if (h < 0) h = 0;
return NR(0, y, meow_ui_width(), h);
}
meow_widget *ncm_topbar_create(const char *title, int back, const char *action_label)
{
ncm_rect_t r = ncm_topbar_rect();
meow_widget *tb = meow_topappbar_create(r.x, r.y, r.w, r.h, title);
meow_topappbar_set_back(tb, back);
if (action_label) meow_topappbar_add_action(tb, action_label);
meow_widget_set_z(tb, 999); /* chrome 置顶(§6.2:> 一切自动 z) */
return tb;
}
/* 空态/错误:居中两行(kind 大字 + what 小字)。 */
void ncm_draw_empty(ncm_rect_t r, const char *kind, const char *what)
{
const meow_theme *th = meow_theme_get();
int cy = r.y + r.h / 2;
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, th->primary);
if (what && what[0])
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->dim);
}
/* 底部信息(右对齐 dim)。 */
void ncm_draw_footer(ncm_rect_t r, const char *text)
{
const meow_theme *th = meow_theme_get();
if (!text || !text[0]) return;
meow_ui_text_right(r.x + r.w - APP_PAD, r.y + r.h - 26, text, 20, th->dim);
}
/* 底部提示条(非阻塞):画在内容区底部之上,2s 后由 tick 清除。
* 只画一次并标脏;g_snack_until 为过期时间(0=无)。 */
static char s_snack[192];
static uint64_t s_snack_until; /* monotonic ms 过期时刻 */
static ncm_rect_t s_snack_r;
static int s_snack_id; /* <0=未注册 */
static void snack_draw(void *ctx)
{
(void)ctx;
if (meow_ui_time_ms() >= s_snack_until && s_snack_until != 0) {
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
s_snack_until = 0;
return;
}
const meow_theme *th = meow_theme_get();
/* 深色底 + 白字 */
meow_ui_fill_rect(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h, th->inverseSurface);
meow_ui_text_center(s_snack_r.x + s_snack_r.w / 2, s_snack_r.y + (s_snack_r.h - 26) / 2 + 4,
s_snack, 20, th->inverseOnSurface);
}
void ncm_snack(const char *text)
{
if (!text) return;
strncpy(s_snack, text, sizeof s_snack - 1);
s_snack[sizeof s_snack - 1] = 0;
s_snack_until = meow_ui_time_ms() + 2000;
int w = meow_ui_width() - 2 * APP_PAD;
s_snack_r = NR(APP_PAD, meow_ui_height() - 56, w, 40);
if (s_snack_id < 0)
s_snack_id = meow_draw_custom_register(999, s_snack_r.x, s_snack_r.y, s_snack_r.w,
s_snack_r.h, snack_draw, NULL);
else
meow_draw_set_z(s_snack_id, 999);
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
}
/* 周期检查:snack 到期清除。可由各页 tick 调。 */
void chrome_tick(void)
{
if (s_snack_until && meow_ui_time_ms() >= s_snack_until) {
meow_ui_invalidate(s_snack_r.x, s_snack_r.y, s_snack_r.w, s_snack_r.h);
s_snack_until = 0;
}
}
+220
View File
@@ -0,0 +1,220 @@
/* 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>
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 int g_load_err; /* 0=ok;负=错误码 */
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
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_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);
}
void home_reload(void)
{
g_load_err = 0;
int rc = 0;
if (g_host_mock) { /* 宿主/测试:跳过网络,用预置数据 */
rc = 0;
} else if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists();
g_load_err = rc;
if (g_load_err != 0) g_st.playlist_count = 0;
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();
}
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);
}
+215
View File
@@ -0,0 +1,215 @@
/* ui/login.c — 扫码登录页(ACT_LOGIN,根页)。
*
* 流程:进入即起后台 worker(login_worker):
* unikey(eapi) → 本地二维码矩阵(qrcodegen) → 轮询(801/802/803) → 803 捕获 set-cookie 加密落盘
* → login_status 拿 uid/nickname → 置 g_login_done。
* UI 线程只在 tick 读 g_st.qr 状态与 login_state 重绘并检查 g_login_done → 切入 home。
* 800(过期)自动刷新二维码。UI 不卡:网络全在 worker 线程。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static pthread_t g_thread;
static volatile int g_polling; /* worker 运行中 */
static volatile int g_done; /* 登录成功(UI tick 切入 home) */
static volatile int g_err; /* 最后一次错误(网络不可达等) */
static int g_active;
static int g_custom_id; /* 绘制物 */
static ncm_rect_t r_cont;
/* ---- 绘制 ---- */
static void login_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_LOGIN) return;
const meow_theme *th = meow_theme_get();
/* 标题 */
meow_ui_text_center(r_cont.x + r_cont.w/2, r_cont.y + 24, "NetEase Cloud Music", 28, th->fg);
meow_ui_text_center(r_cont.x + r_cont.w/2, r_cont.y + 64, "scan to sign in", 20, th->dim);
/* QR 位图 */
int sz = g_st.qr_size;
if (sz > 0) {
int qs = sz * 6; /* 每模块 6px → 41x41=246,33x33=198 */
if (qs > 320) qs = sz * 4;
int ox = r_cont.x + (r_cont.w - qs) / 2;
int oy = r_cont.y + 108;
/* 白色底 + 边框 */
meow_ui_fill_rect(ox - 8, oy - 8, qs + 16, qs + 16, 0xFFFFFF);
meow_ui_draw_rect(ox - 8, oy - 8, qs + 16, qs + 16, th->outline);
for (int y = 0; y < sz; y++)
for (int x = 0; x < sz; x++)
if (g_st.qr_mods[y*sz + x])
meow_ui_fill_rect(ox + x*6, oy + y*6, 6, 6, 0x000000);
} else {
meow_ui_text_center(r_cont.x + r_cont.w/2, r_cont.y + 180,
g_err ? "no network - tap to retry" : "fetching QR key...", 20, th->dim);
}
/* 状态文字 */
const char *st = login_status_str(g_st.login_state);
if (g_st.login_state == NCM_LOGIN_OK && g_done) st = "success";
meow_ui_text_center(r_cont.x + r_cont.w/2, r_cont.y + 108 + 320 + 44,
st, 20, (g_st.login_state == NCM_LOGIN_OK) ? th->primary : th->dim);
}
/* 重绘 login 内容(QR+状态变化时)。 */
static void login_repaint(void)
{
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
const char *login_status_str(int state)
{
switch (state) {
case NCM_LOGIN_PENDING: return "waiting for scan...";
case NCM_LOGIN_SCANNED: return "scanned - confirm on phone";
case NCM_LOGIN_OK: return "signed in!";
case NCM_LOGIN_EXPIRED: return "QR expired - refreshing";
default: return "error, tap to retry";
}
}
static int get_unikey_and_qr(char *unikey, size_t usz)
{
int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, usz);
if (rc != NCM_OK) return rc;
ncm_strlcpy(g_st.unikey, unikey, sizeof g_st.unikey);
ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url);
int size = 0;
if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, sizeof g_st.qr_mods / 177, &size) != NCM_OK)
g_st.qr_size = 0;
else
g_st.qr_size = size;
g_st.login_state = NCM_LOGIN_PENDING;
g_err = 0;
return NCM_OK;
}
static void *login_worker(void *arg)
{
(void)arg;
char unikey[64];
if (get_unikey_and_qr(unikey, sizeof unikey) != NCM_OK) { g_err = 1; return NULL; }
char cookie[NCM_COOKIE_MAX];
ncm_strlcpy(cookie, g_st.ctx.cookie, sizeof cookie);
while (!g_app_quit) {
ncm_login_poll_t r;
char out_cookie[NCM_COOKIE_MAX];
int rc = ncm_login_poll(g_st.ctx.device_id, unikey, cookie, &r,
out_cookie, sizeof out_cookie);
if (rc != NCM_OK) { g_err = 1; usleep(2 * 1000 * 1000); continue; }
g_st.login_state = r.state;
if (r.state == NCM_LOGIN_OK) {
ncm_strlcpy(cookie, out_cookie, sizeof cookie);
ncm_ctx_set_cookie(&g_st.ctx, cookie);
/* 拿 uid/nickname */
char uid[32], nick[128];
if (ncm_login_status_check(g_st.ctx.device_id, cookie, uid, sizeof uid,
nick, sizeof nick) == NCM_OK)
appstate_set_logged_in(uid, nick);
else
appstate_set_logged_in("", "");
g_st.logged_in = 1;
g_done = 1;
break;
}
if (r.state == NCM_LOGIN_EXPIRED) {
/* 刷新二维码 */
usleep(500 * 1000);
get_unikey_and_qr(unikey, sizeof unikey);
continue;
}
usleep(1500 * 1000);
}
return NULL;
}
int login_start(void)
{
if (g_polling) return 0;
g_polling = 1;
g_done = 0;
g_err = 0;
if (pthread_create(&g_thread, NULL, login_worker, NULL) != 0) {
g_polling = 0;
return -1;
}
pthread_detach(g_thread);
return 0;
}
int login_is_active(void) { return g_active; }
void login_on_tap(int x, int y)
{
(void)x; (void)y;
if (g_st.login_state == NCM_LOGIN_OK && g_done) return;
if (g_err) { login_start(); login_repaint(); } /* 网络失败重试 */
}
/* UI tick:重绘 QR/状态变化;登录成功 → 切 home。 */
void login_tick(uint64_t now)
{
(void)now;
chrome_tick();
static int last_state = -1, last_qr = 0;
if (g_st.login_state != last_state || g_st.qr_size != last_qr) {
last_state = g_st.login_state;
last_qr = g_st.qr_size;
login_repaint();
}
if (g_done)
meow_view_page_replace(ACT_HOME);
}
/* ---- 生命周期 ---- */
static void login_page_create(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
r_cont = ncm_content_rect();
g_custom_id = meow_draw_custom_register(1, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
login_draw, NULL);
g_active = 0;
}
static void login_page_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
if (!g_host_mock) login_start(); /* 进入即获取二维码 + 轮询 */
}
static void login_page_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 0;
}
static void login_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_custom_id) { meow_draw_unregister(g_custom_id); g_custom_id = 0; }
}
meow_view *login_view_create(void)
{
static const meow_view_hooks h = {
.on_create = login_page_create,
.on_enter = login_page_enter,
.on_leave = login_page_leave,
.on_destroy = login_page_destroy,
};
return meow_view_create(ACT_LOGIN, MEOW_VIEW_ACTIVITY, &h, NULL);
}
+300
View File
@@ -0,0 +1,300 @@
/* ui/player.c — 播放页(ACT_PLAYER)。
*
* 布局:封面(后台 cover 线程解码) + 标题/歌手 + 进度条(可拖 seek) + 播放/暂停/上下曲 + 滚动歌词
* (时间轴解析,当前行高亮居中)。播放页横竖屏可切(device LCD 为竖屏 480x720,故为**布局**切换:
* 竖屏=封面在上、歌词在下;横屏=封面在左、信息+歌词在右,仍在 480x720 画布内)。
* 进度/状态每 tick 读 play_manager(跨线程 mu 保护),歌词行由 pos_ms 推出。
* 全部内容画进一个 custom 绘制物;控件钮/进度条/歌词命中用 player_draw 计算的共享几何。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
typedef struct { meow_view *view; int active; } player_ctrl;
static player_ctrl g_plc;
static meow_widget *g_tb;
static ncm_rect_t r_cont;
static int g_custom_id;
static int g_landscape; /* 0=竖屏 1=横屏(布局) */
static int g_last_pos_ms, g_last_state, g_last_line;
/* 共享几何(player_draw 计算 → player_on_tap 命中用) */
static ncm_rect_t g_cov; /* 封面 */
static int g_bar_x, g_bar_y, g_bar_w;
static int g_cprev_x, g_cplay_x, g_cnext_x, g_cby, g_cr;
static int g_lyr_x, g_lyr_y, g_lyr_w, g_lyr_h; /* 歌词区 */
#define CTL_RR 28 /* 侧钮命中半径 */
static int cur_song(void)
{
int c = player_manager_playing_song();
return (c >= 0 && c < g_st.song_count) ? c : -1;
}
static void fmt_ms(int64_t ms, char *out, size_t sz)
{
if (ms < 0) ms = 0;
int sec = (int)(ms / 1000);
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
}
/* 实心三角近似:right=向右指(顶点在右),left=向左指(顶点在左)。 */
static void tri(int x0, int y0, int h, int w, int dir, uint32_t color)
{
for (int i = 0; i < h; i++) {
int wi = 1 + i * w / h;
if (dir == 1) meow_ui_fill_rect(x0, y0 + i, wi, 1, color);
else meow_ui_fill_rect(x0 - wi + 1, y0 + i, wi, 1, color);
}
}
/* 圆形按钮 + 图标。kind: 0=|<<(prev) 1=>play 2=||pause 3=>>|(next)。 */
static void draw_ctl(int cx, int cy, int r, int kind, uint32_t base, uint32_t fg)
{
for (int i = 0; i < 2 * r; i++) {
int dy = i - r;
int half = (int)sqrt((double)(r * r - dy * dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy - r + i, 2 * half, 1, base);
}
int t = r / 2;
if (kind == 0) { /* |<< */
meow_ui_fill_rect(cx - r - 2, cy - t, 4, 2*t, fg);
tri(cx + r - 4, cy - t, 2*t, 2*t, -1, fg);
} else if (kind == 1) { /* > */
tri(cx - t, cy - t, 2*t, 2*t, 1, fg);
} else if (kind == 2) { /* || */
meow_ui_fill_rect(cx - t - 2, cy - t, 4, 2*t, fg);
meow_ui_fill_rect(cx + t - 2, cy - t, 4, 2*t, fg);
} else { /* >>| */
tri(cx - r + 4, cy - t, 2*t, 2*t, 1, fg);
meow_ui_fill_rect(cx + r - 2, cy - t, 4, 2*t, fg);
}
}
static void player_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER) return;
const meow_theme *th = meow_theme_get();
int cx = r_cont.x + r_cont.w / 2;
int sc = cur_song();
const ncm_song_t *s = (sc >= 0) ? &g_st.songs[sc] : NULL;
/* 封面区域 */
int covsz = 220;
if (g_landscape) g_cov = NR(r_cont.x + 24, r_cont.y + 60, covsz, covsz);
else g_cov = NR(cx - covsz/2, r_cont.y + 10, covsz, covsz);
meow_ui_fill_rect(g_cov.x, g_cov.y, g_cov.w, g_cov.h, th->surfaceContainerHigh);
meow_ui_draw_rect(g_cov.x, g_cov.y, g_cov.w, g_cov.h, th->outlineVariant);
meow_img *cov = cover_get();
if (cov) meow_ui_img_cover(g_cov.x, g_cov.y, covsz, cov);
else meow_ui_text_center(g_cov.x + g_cov.w/2, g_cov.y + g_cov.h/2 - 10, "no artwork", 20, th->dim);
int info_x = (g_landscape) ? g_cov.x + g_cov.w + 28 : cx;
int info_w = (g_landscape) ? (r_cont.x + r_cont.w - 16 - info_x) : r_cont.w;
int ty = (g_landscape) ? g_cov.y + 8 : g_cov.y + g_cov.h + 8;
meow_ui_text_center(info_x, ty, s ? s->name : "nothing playing", 28, th->fg);
meow_ui_text_center(info_x, ty + 34, s ? s->art_name : "", 20, th->dim);
/* 进度条 + 时间 */
g_bar_x = (g_landscape) ? info_x - info_w/2 : 32;
g_bar_w = (g_landscape) ? info_w - 24 : r_cont.w - 64;
g_bar_y = ty + 78;
int pos = (int)player_manager_pos_ms();
int dur = (int)player_manager_dur_ms();
if (dur <= 0 && s) dur = (int)(s->duration_ms);
int frac = dur > 0 ? (int)((int64_t)pos * 1000 / dur) : 0;
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
char lp[16], rp[16];
fmt_ms(pos, lp, sizeof lp);
fmt_ms(dur, rp, sizeof rp);
meow_ui_text(g_bar_x, g_bar_y - 20, lp, 20, th->dim);
meow_ui_text_right(g_bar_x + g_bar_w, g_bar_y - 20, rp, 20, th->dim);
meow_ui_fill_rect(g_bar_x, g_bar_y, g_bar_w, 4, th->surfaceVariant);
meow_ui_fill_rect(g_bar_x, g_bar_y, (int)((int64_t)g_bar_w * frac / 1000), 4, th->primary);
/* 控制钮 */
g_cby = g_bar_y + 40;
int cw = 56, gap = (g_landscape) ? 34 : 26;
g_cr = cw/2 + 4;
g_cprev_x = info_x - cw - gap/2 - 4;
g_cplay_x = info_x;
g_cnext_x = info_x + cw + gap/2 + 4;
draw_ctl(g_cprev_x, g_cby, cw/2, 0, th->secondaryContainer, th->onSecondaryContainer);
int st = player_manager_state();
draw_ctl(g_cplay_x, g_cby, g_cr, (st == NCM_PC_PAUSED) ? 1 : 2, th->primary, th->onPrimary);
draw_ctl(g_cnext_x, g_cby, cw/2, 3, th->secondaryContainer, th->onSecondaryContainer);
/* 歌词区 */
if (g_landscape) {
g_lyr_x = r_cont.x + 24;
g_lyr_y = g_cov.y + g_cov.h + 12;
g_lyr_w = r_cont.w - 48;
g_lyr_h = r_cont.y + r_cont.h - g_lyr_y;
} else {
g_lyr_x = r_cont.x;
g_lyr_y = g_cby + g_cr + 10;
g_lyr_w = r_cont.w;
g_lyr_h = r_cont.y + r_cont.h - g_lyr_y;
}
if (g_lyr_h < 0) g_lyr_h = 0;
/* 背景(歌词区隔断) */
meow_ui_fill_rect(g_lyr_x, g_lyr_y, g_lyr_w, 1, th->surfaceVariant);
int n = lyric_count();
int line_h = 28;
int center_y = g_lyr_y + (g_lyr_h > line_h * 11 ? g_lyr_h / 2 : 140);
int lcx = g_lyr_x + g_lyr_w / 2;
if (n == 0) {
meow_ui_text_center(lcx, center_y - 10, "no lyric", 20, th->dim);
return;
}
int64_t ps = player_manager_pos_ms();
int cur2 = lyric_line_at(ps);
for (int i = cur2 - 6; i <= cur2 + 5; i++) {
if (i < 0 || i >= n) continue;
int yy = center_y + (i - cur2) * line_h;
int is_cur = (i == cur2);
meow_ui_text_center(lcx, yy, lyric_line_text(i), is_cur ? 24 : 20,
is_cur ? th->primary : th->dim);
const char *tl = lyric_line_tl(i);
if (is_cur && tl && tl[0])
meow_ui_text_center(lcx, yy + 24, tl, 20, th->secondary);
}
}
int player_is_active(void) { return g_plc.active; }
int player_screen(void) { return g_landscape; }
int player_set_screen(int landscape)
{
int old = g_landscape;
g_landscape = landscape ? 1 : 0;
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
return old;
}
void player_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
if (ah >= 0) { player_set_screen(!g_landscape); return; } /* ⟳ 布局切换 */
}
if (g_bar_w <= 0) return;
/* 控制钮 */
int r = g_cr;
if (x >= g_cplay_x - r && x <= g_cplay_x + r && y >= g_cby - r && y <= g_cby + r) {
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
else player_manager_pause(1);
return;
}
if (x >= g_cprev_x - CTL_RR && x <= g_cprev_x + CTL_RR &&
y >= g_cby - CTL_RR && y <= g_cby + CTL_RR) { player_manager_next(-1); return; }
if (x >= g_cnext_x - CTL_RR && x <= g_cnext_x + CTL_RR &&
y >= g_cby - CTL_RR && y <= g_cby + CTL_RR) { player_manager_next(1); return; }
/* 进度条 seek */
if (y >= g_bar_y - 8 && y <= g_bar_y + 12 && 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);
return;
}
}
void player_play_song(int idx)
{
if (idx < 0 || idx >= g_st.song_count) return;
player_manager_play(idx);
appstate_set_playing_song(idx, g_st.cover_url);
cover_request(g_st.cover_url);
meow_view_page_push(ACT_PLAYER);
}
void player_pause_toggle(int on)
{
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
else player_manager_pause(1);
(void)on;
}
void player_next(int dir) { player_manager_next(dir); }
void player_seek(int64_t ms) { player_manager_seek(ms); }
void player_on_swipe(int dir, int dist) { (void)dir; (void)dist; }
void player_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_plc.active) return;
int pos = (int)player_manager_pos_ms();
int st = player_manager_state();
int line = (lyric_count() > 0) ? lyric_line_at(pos) : -1;
if (pos != g_last_pos_ms || st != g_last_state) {
g_last_pos_ms = pos;
g_last_state = st;
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
} else if (line != g_last_line) {
g_last_line = line;
meow_ui_invalidate(g_lyr_x, g_lyr_y, g_lyr_w, g_lyr_h);
}
if (cover_changed()) {
cover_ack();
meow_ui_invalidate(g_cov.x, g_cov.y, g_cov.w, g_cov.h);
}
}
static void plr_page_create(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
meow_ui_activity_push(ACT_PLAYER); /* owner=ACT_PLAYER */
g_tb = ncm_topbar_create("Now Playing", 1, "\xe2\x86\xbb"); /* 返回 + ⟳ 布局 */
meow_ui_activity_pop();
g_custom_id = meow_draw_custom_register(1, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
player_draw, NULL);
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1;
}
static void plr_page_enter(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
if (!g_host_mock) player_manager_load_lyric();
}
static void plr_page_leave(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->active = 0;
}
static void plr_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_custom_id) { meow_draw_unregister(g_custom_id); g_custom_id = 0; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *player_view_create(void)
{
static const meow_view_hooks h = {
.on_create = plr_page_create,
.on_enter = plr_page_enter,
.on_leave = plr_page_leave,
.on_destroy = plr_page_destroy,
};
return meow_view_create(ACT_PLAYER, MEOW_VIEW_ACTIVITY, &h, &g_plc);
}
+216
View File
@@ -0,0 +1,216 @@
/* ui/playlist.c — 歌单详情页(ACT_PLAYLIST,曲目列表)。
*
* 曲目用 SDK 结构化列表(meow_listview)虚拟化渲染「歌名 + 歌手 副标题」。点击行 → 起播并推播放页。
* 进入时拉歌单详情(trackIds)+ 分批 song_detail(有界 NCM_SONG_MAX)。空态/错误用绘制物。
* 左缘返回 = 弹回主页(app.c)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct { meow_view *view; int active; } playlist_ctrl;
static playlist_ctrl g_pc;
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[256];
static int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static void pl_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
static int pl_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_st.song_count) return 0;
ncm_song_t *s = &g_st.songs[idx];
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s->name;
out->subtitle = (s->art_name[0] || s->album_name[0])
? (s->art_name[0] ? s->art_name : s->album_name) : "-";
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = 1;
return 1;
}
static void pl_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err == 0 && g_st.song_count != 0) return;
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, "LOAD FAILED", "tap back and retry");
else
ncm_draw_empty(r_list, "NO TRACKS", "this playlist is empty");
}
static void pl_draw_footer(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0) return;
char foot[96];
snprintf(foot, sizeof foot, "%d/%d tracks",
g_st.song_count,
(g_st.playlist_count > 0 && g_st.active_playlist >= 0)
? g_st.playlists[g_st.active_playlist].track_count : g_st.song_count);
ncm_draw_footer(r_cont, foot);
}
void playlist_refresh(void)
{
g_load_err = 0;
int rc = NCM_ERR_ARG;
if (!g_host_mock && g_st.active_playlist >= 0)
rc = appstate_load_playlist_songs(g_st.active_playlist);
else if (g_host_mock)
rc = 0;
g_load_err = rc;
if (g_load_err != 0) g_st.song_count = 0;
if (g_list) {
meow_listview_set_count(g_list, g_st.song_count);
meow_listview_set_scroll(g_list, 0);
meow_listview_set_sel(g_list, -1);
}
if (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) {
snprintf(g_title, sizeof g_title, "%s", g_st.playlists[g_st.active_playlist].name);
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();
}
void playlist_open(int idx)
{
if (idx < 0 || idx >= g_st.playlist_count) return;
g_st.active_playlist = idx;
g_st.playing_song = -1;
cover_cache_reset();
meow_view_page_push(ACT_PLAYLIST);
playlist_refresh(); /* page_enter 也会调用(幂等);这里先拉避免空帧 */
}
int playlist_is_active(void) { return g_pc.active; }
int playlist_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { pl_drag_reset(); meow_view_page_pop(); return 1; }
if (ah >= 0) { pl_drag_reset(); playlist_refresh(); return 1; }
}
if (g_load_err != 0 || g_st.song_count == 0) return 0;
if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row < 0) { pl_drag_reset(); return 1; }
pl_drag_reset();
player_play_song(row); /* 起播并推播放页 */
return 1;
}
return 0;
}
void playlist_touch_down(int x, int y)
{
pl_drag_reset();
if (g_load_err != 0 || g_st.song_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 playlist_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 playlist_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_load_err != 0 || g_st.song_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 pl_page_create(meow_view *v, void *ctx)
{
(void)v;
((playlist_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_PLAYLIST); /* owner=ACT_PLAYLIST */
g_tb = ncm_topbar_create("Playlist", 1, NULL);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
pl_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,
pl_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, pl_draw_footer, NULL);
pl_drag_reset();
}
static void pl_page_enter(meow_view *v, void *ctx)
{
(void)v;
((playlist_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
playlist_refresh();
}
static void pl_page_leave(meow_view *v, void *ctx)
{
(void)v;
((playlist_ctrl *)ctx)->active = 0;
}
static void pl_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 *playlist_view_create(void)
{
static const meow_view_hooks h = {
.on_create = pl_page_create,
.on_enter = pl_page_enter,
.on_leave = pl_page_leave,
.on_destroy = pl_page_destroy,
};
return meow_view_create(ACT_PLAYLIST, MEOW_VIEW_ACTIVITY, &h, &g_pc);
}
+193
View File
@@ -0,0 +1,193 @@
/* ui/settings.c — 设置页(ACT_SETTINGS)。
*
* 行项:音质(Hi-res/较高/标准,默认 Hi-res)+ 缓存上限(1~100GB,默认 10GB)+ 退出登录。
* 设置持久化到 MeowData/os.meow.ncm/data(meow_config_*;缓存上限键 ncm.cache_limit_gb 与核心库
* ncm_cache_load_setting 对齐)。音质/上限经 BottomSheet 选择;退出登录经 confirm 弹窗。
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 3 行,无需列表滚动)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct { meow_view *view; int active; } settings_ctrl;
static settings_ctrl g_sc;
static meow_widget *g_tb;
static ncm_rect_t r_cont;
static int g_custom_id;
static int g_row_h; /* 行高 */
static const char *level_name(int lv)
{
switch (lv) {
case NCM_LEVEL_HIRES: return "Hi-Res";
case NCM_LEVEL_LOSSLESS:return "Lossless";
case NCM_LEVEL_EXHIGH: return "Exhigh";
case NCM_LEVEL_HIGHER: return "Higher";
default: return "Standard";
}
}
static void settings_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SETTINGS) return;
const meow_theme *th = meow_theme_get();
g_row_h = 60;
int y = r_cont.y + 16;
int w = r_cont.w - 2 * APP_PAD;
int x = APP_PAD;
/* 行:音质 / 缓存上限 / 退出登录 */
struct { const char *title; char sub[48]; } rows[3];
rows[0].title = "Music Quality";
rows[1].title = "Cache Limit";
rows[2].title = "Sign Out";
snprintf(rows[0].sub, sizeof rows[0].sub, "current: %s", level_name(g_st.level));
snprintf(rows[1].sub, sizeof rows[1].sub, "current: %d GB", g_st.cache_limit_gb);
snprintf(rows[2].sub, sizeof rows[2].sub, "%s", g_st.nickname[0] ? g_st.nickname : "sign out");
for (int i = 0; i < 3; i++) {
int yy = y + i * (g_row_h + 8);
meow_ui_fill_rect(x, yy, w, g_row_h, th->surfaceContainer);
uint32_t tcol = (i == 2) ? th->error : th->fg;
uint32_t scol = (i == 2) ? th->error : th->dim;
meow_ui_text(x + APP_PAD, yy + 12, rows[i].title, 20, tcol);
meow_ui_text(x + APP_PAD, yy + 34, rows[i].sub, 20, scol);
/* 右侧箭头 (draw) */
meow_ui_text_right(x + w - APP_PAD, yy + 18, ">", 28, th->dim);
}
}
static int row_at(int x, int y)
{
(void)x;
int r = r_cont.y + 16;
for (int i = 0; i < 3; i++) {
int yy = r + i * (g_row_h + 8);
if (y >= yy && y <= yy + g_row_h) return i;
}
return -1;
}
int settings_is_active(void) { return g_sc.active; }
int settings_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
if (ah >= 0) { return 1; }
}
if (x < APP_PAD || x > r_cont.w - APP_PAD) return 0;
int row = row_at(x, y);
if (row < 0) return 0;
if (row == 0) { /* 音质 */
meow_sheet *s = meow_sheet_create("Music Quality", 0);
meow_sheet_add_item(s, "Hi-Res");
meow_sheet_add_item(s, "Higher");
meow_sheet_add_item(s, "Standard");
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op >= 0) {
g_st.level = (op == 0) ? NCM_LEVEL_HIRES : (op == 1) ? NCM_LEVEL_HIGHER : NCM_LEVEL_STANDARD;
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
appstate_save_settings();
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
return 1;
}
if (row == 1) { /* 缓存上限 */
static const int gbs[] = { 1, 2, 5, 10, 20, 30, 50, 100 };
static const char *labels[] = { "1 GB", "2 GB", "5 GB", "10 GB",
"20 GB", "30 GB", "50 GB", "100 GB" };
meow_sheet *s = meow_sheet_create("Cache Limit", 0);
for (int i = 0; i < 8; i++) meow_sheet_add_item(s, labels[i]);
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op >= 0 && op < 8) {
g_st.cache_limit_gb = gbs[op];
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
appstate_save_settings();
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
return 1;
}
if (row == 2) { /* 退出登录 */
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Sign out?");
const char *msgs[1];
msgs[0] = "Playback will stop.\nYou'll need to scan a QR code again.";
meow_dialog_set_items(d, msgs, 1, -1);
int yes = meow_dialog_confirm(d);
meow_dialog_free(d);
if (yes) {
appstate_logout();
meow_view_page_replace(ACT_LOGIN);
}
return 1;
}
return 0;
}
void settings_swipe(int dir, int dist) { (void)dir; (void)dist; }
void settings_touch_down(int x, int y) { (void)x; (void)y; }
void settings_touch_move(int x, int y) { (void)x; (void)y; }
void settings_open(void)
{
g_st.playing_song = -1;
meow_view_page_push(ACT_SETTINGS);
}
void settings_reload(void)
{
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
static void st_page_create(meow_view *v, void *ctx)
{
(void)v;
((settings_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
meow_ui_activity_push(ACT_SETTINGS); /* owner=ACT_SETTINGS */
g_tb = ncm_topbar_create("Settings", 1, NULL);
meow_ui_activity_pop();
g_row_h = 60;
g_custom_id = meow_draw_custom_register(1, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
settings_draw, NULL);
}
static void st_page_enter(meow_view *v, void *ctx)
{
(void)v;
((settings_ctrl *)ctx)->active = 1;
settings_reload();
}
static void st_page_leave(meow_view *v, void *ctx)
{
(void)v;
((settings_ctrl *)ctx)->active = 0;
}
static void st_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_custom_id) { meow_draw_unregister(g_custom_id); g_custom_id = 0; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *settings_view_create(void)
{
static const meow_view_hooks h = {
.on_create = st_page_create,
.on_enter = st_page_enter,
.on_leave = st_page_leave,
.on_destroy = st_page_destroy,
};
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_sc);
}