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

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

No files matched your search

+114
View File
@@ -552,3 +552,117 @@ bcfd75e5eb872a751c166c456eea0184 bench/verify_fixes.c f6bd765a374effaf866784c3
- **`g_st.songs[]/song_count` 由 playlist worker 写、play/IPC 线程读(§10.9 FM4 旧已知风险)**:入播放页仍在同歌单,竞态窗口极小;沿用未加锁(与项目现状一致)。
- **真机真实请求计数**:本批新增 ~20 次(detail_probe 抓样 1 次(5 请求,样本落盘)+ song_probe 1 + app_path_probe 2 + verify_fixes 2 次 11 + 启动验证 2)。已尽量「一次请求存样本、离线重放」,但修复前后复验需真实响应,属必要开销。
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区。**
---
## 11. 网易云 UI 重做(按 NCM_UI_DESIGN.md,2026-08-31)—— 双套配色 / 黑胶播放页 / 质感
> 依据 `devdocs/plan/NCM_UI_DESIGN.md` 将 App 从「文件管理器列表风」重做为**网易云风格**:
> 白红/黑红双套配色、主页双页签「我创建的/我收藏的」、独立搜索页、歌单详情封面头块+「播放全部」、
> 播放页黑胶唱盘(fill_rect 行扫构造圆 + 方盖内切裁圆封面 + 唱针)。仅改动 `patch_v2/ncm_app/`。
> 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机。
### 11.1 双套配色机制(§3.1/§3.3)
- **新增 `app/ui/ncm_colors.c`**:`ncm_pal_t{ncm_palette(dark)}` 两套 App 调色板(白红 / 黑红 +
播放页/登录专属色)+ `g_dark` 全局 + `ncm_on_theme(mode)` 主题侦听。
- 品牌红走 App 侧(SDK 全局 primary 是 MD3 蓝,App 无权改——红只落 App 自绘层)。
- 中性色(surface/onSurface/onSurfaceVariant/outlineVariant/error)一律 `meow_theme_get()`;
**全程语义色 + App 调色板,零硬编码散落**(§3.1 契约)。
- **`app/app.c`**:`meow_theme_add_listener(ncm_on_theme)`(旧代码传 NULL 无效)+ 启动同步
`g_dark = (meow_theme_get_mode()==MEOW_THEME_DARK)`。系统明暗切换 → `ncm_on_theme` → 整 App 重绘实时换肤。
- 色值(§3.2):day red `#C20C0C` / red_lit `#EC4141` / red_ctr `#FBE9E9`;
night red `#EC4141` / red_lit `#FF6A6A` / red_ctr `#3A1919`;登录 hero night `#7A0E0E`;
播放页底 day `#FFFDF9` / night `#0C0D10`;盘体 day `#1A1A1A` / night `#141414`。
验证(宿主抽样像素):day 组头红条 `(198,12,8)`≈`#C20C0C`,night `(239,65,66)`≈`#EC4141`。✅
### 11.2 数据层(§7.0 明示缺口)
- `core/ncm_api.h`:`ncm_playlist_t` 增 **`int is_collected;`**(1=收藏的/订阅他人,0=自己创建)。
- `core/ncm_api.c`:`ncm_api_user_playlist` 解析响应 **`subscribed`**(`obj_bool`)写入 `is_collected`,
支撑「我创建的/我收藏的」分组(原实现只取 id/name/coverImgUrl/trackCount)。
- `core/ncm_api.h/.c`:新增 **`ncm_api_cloudsearch`**(weapi `/api/cloudsearch/pc`,type=1 单曲),
解析 `result.songs[]`(id 用 int64/name/ar[0]/al/dt),关键词 `"` `\` 转义防坏 JSON。
### 11.3 页面改动
- **登录页**(`ui/login.c`):品牌红 hero 带(y=0 起,沉浸式)+ 红点 logo + 「网易云音乐/扫码登录」
标题区 + QR 卡(圆角 16 白底/surfaceContainer + 内描边)+ 状态字(day red/night red_lit)+ 底部提示。
- **主页**(`ui/home.c`):红系 topbar(标题「我的音乐」+ 刷新/设置/搜索动作,搜索按钮最右)+
**个人区卡片**(首字圆形头像 red_ctr + 昵称 + ID + 箭头)+ **双页签「我创建的/我收藏的」**
(`meow_tabbar`,红下划线/红选中字叠层注入)+ **组头红条**(red_ctr 底 + 红字 + 红侧条)+
歌单列表(rounded card 行 + 红 mini-vinyl 缩略 + 红色按压竖条);列表数据源按 `is_collected` 过滤分组。
- `MEOW_LIST_SEL_NONE` + 自定义红态叠层(§4.2:SDK listview MD3 蓝选中不可改,改叠红条)。
- **保留 home_worker 后台异步加载**(§10.11 P1 修复),未回退成同步。
- **搜索页**(`ui/search.c` 新增,`ACT_SEARCH`):TextField(SDK IME,仅英文/数字)+ **红色「搜索」确认钮** +
结果列表(点击可播,复用 `g_st.songs[]` 作为当前播放上下文)。**风控纪律**:搜索只在用户点「搜索」钮时发
(进入页面不自动搜索、不逐键发请求);网络走后台 worker(search_worker)。
- **歌单详情**(`ui/playlist.c`):封面头块(圆角封面/占位 + 歌单名 + 「创建者 · N 首」)+ 操作条
(**红色「播放全部」** FILLED + 「收藏」OUTLINED(v1 置灰→snack) + 「评论」红字+红badge(v1 置灰→snack))+
曲目列表(歌名 + 歌手·专辑 副标题);保留了 §10.11 的后台 `playlist_worker`。
- **播放页**(`ui/player.c`):**黑胶唱盘**(`fill_rect` 行扫构造圆盘 + 环槽减淡 + 方盖内切圆 +
**四角把「圆外-方外」盖回盘色 → 封面成圆**,§5.1 技巧)+ **唱针**(3px 唱臂 + 端头,
播放压盘/暂停微抬 6px)+ 高光弧 + 进度条(已播红)+ prev/play/next 控制钮(play 红)+ 滚动歌词
(当前行红高亮居中,裁剪到歌词带)+ 竖/横布局可切(沿用 player_set_screen)。
- **恒沉浸**:播放页底 day 瓷白 `#FFFDF9` / night 近黑 `#0C0D10`,不随系统明暗整体反转;accent
(进度红/play 钮/歌词高亮)日夜换红明暗(day `#C20C0C` / night `#EC4141`,§5.4)。
- **设置页**(`ui/settings.c`):4 行卡片(音质/缓存上限/退出登录/关于)+ 分组说明;退出登录用 error 色;
音质/缓存 → BottomSheet;退出 → confirm;关于 → snack。行圆角 12,语义色。
- **chrome.c**:`ncm_draw_empty` kind 大字改品牌红;页脚 dim。
### 11.4 性能(§6 专项)
- **播放页分带刷新**:内容拆「盘带 `g_disc_r`」+「动态带 `g_dyn_r`」两个绘制物,bbox 互不重叠。
`player_tick` **只标脏变化的那一条带**——进度/歌词/时间只刷动态带,盘带仅 歌曲/封面/播停态 变化才重画
(修原 `player_tick` 每 tick 刷整 `r_cont` 的隐患,§6.1/§6.2)。
- 黑胶圆盘仅 封面/状态变化 时重画;歌词行裁剪到歌词带(自定义绘制物不按 bbox 裁剪,防溢出叠控制钮)。
- 封面限幅沿用(`NCM_COVER_MAX=288KB`,单张);搜索结果上限 `NCM_SEARCH_MAX=32`(无大缓冲,§6.7)。
### 11.5 验证记录(宿主 + mipsel)
- **宿主离屏 `MEOW_UI_DUMP` 双套配色各截一轮**:7 页 × 2 主题 = 14 张
`app/out/shots/{login,home,playlist,player,lands,settings,search}_{dark,light}.png`(PPM→PNG)。
- 红色抽样:day `#C20C0C`(组头红条)、night `#EC4141` 均正确;播放页恒沉浸(瓷白/近黑)✅。
- 黑胶盘/唱针/封面裁圆/进度/控制/歌词居中 目检正确 ✅;主页双页签、个人卡、组头红条、红缩略 ✅。
- **`test/test_ncm_core_host` 全 PASS**(无回归);**`out/test_ncm_core_mipsel`(qemu)全 PASS**。
- **mipsel `build_ncm.sh` 自有代码 `-Wall -Wextra` 0 warning**;`ncm` 静态,text 803084 / data 824 / bss 928356。
- **qemu-mipsel 冒烟** `timeout 10 qemu-mipsel ./ncm`:`start`→`ipc: no launcher socket`(优雅)→登录页
unikey 重试(rc=-5 无 CA,预期)→`exit` 干净退出,无崩溃。
- 未做(诚实):真机搜索/歌单请求仅留最少验证或不打(风控约束);未真机扫码/播放出声(用户联调项)。
- **真机部署未执行**:本机无可达设备主机(`meowos.local` 不解析;ssh 无设备别名/IP,WORK_STATE 亦标注
热点组播不通)。二进制已就绪(md5 `90a25cf0c193fd965d6685fb099bacb7`),部署命令留用户执行:
`scp ncm root@<device-ip>:/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new && ssh root@<device-ip> 'mv /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm'`(先 .new 再 mv)。
### 11.6 md5(自有源码 + 二进制)
```
90a25cf0c193fd965d6685fb099bacb7 ncm # mipsel 静态 App(新)
c2921983e878ce0cefe1f279f6d16d3f app/app.c 8585677b732420ead899c34d07823a05 app/ncm_app_internal.h
359ad93833d3ef1914c97da28d249d09 app/ui/login.c 970e4ec732036ae8d8de137c960863c4 app/ui/home.c
ac9e1585e96b1a1a336cbbc9a82a8c86 app/ui/playlist.c 3023fb024ea096e458b52fb4255db709 app/ui/player.c
c4f6cd8569d6ddc4dd15d4111752ed77 app/ui/settings.c ba78835b3fbe6166bbe39f2e9ea9041a app/ui/search.c
54f10365ed640130ee33fba2e674b547 app/ui/ncm_colors.c aa93822cd735f3912556df6a301a3580 app/ui/chrome.c
94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c 539252a2ea6a6a675f94356898176b99 core/ncm_api.h
1c1c05b2fada19c40e8a83a2742afbd7 core/ncm_api.c 30ab44e6f6cbc8b07eb477e7b8ee82a3 build_ncm.sh
71ed280698b67cba76a90d19a43f8458 build_ncm_host.sh 2a9663abe58609fafcc22949b8e8771c app/test/test_ncm_app.c
```
> 注:`app/logic/appstate.c` 本批未改(md5 未变 `94d10237...`);`build_ncm_host.sh` 因补链 SDK
> `meow_ui_nav/meow_ui_media/meow_notify_ipc` 而更新(宿主 SDK 源后来新增这些文件,原脚本缺链导致
> `meow_notify_ipc_notif_open` 未定义;属构建脚本适配,非 SDK 改动)。
### 11.7 出入清单(只改 `patch_v2/ncm_app/`)
```
新增:
app/ui/ncm_colors.c # ncm_palette/g_dark/ncm_on_theme/ncm_fill_round/ncm_draw_button/first_glyph/ncm_text_fit
app/ui/search.c # 搜索页(IME + cloudsearch + 结果列表)
修改:
app/ncm_app_internal.h # ncm_pal_t/g_dark/ACT_SEARCH/搜索状态/新页声明/调色板助手声明
app/app.c # 主题侦听 + search 页建/路由
app/ui/{login,home,playlist,player,settings,chrome}.c # 重做/重着色
core/ncm_api.h/.c # is_collected(§7.0) + ncm_api_cloudsearch
app/test/test_ncm_app.c # is_collected mock + NCM_THEME 双主题 + search mock
build_ncm.sh / build_ncm_host.sh# 源列表加 search/ncm_colors;host 补链 SDK nav/media/notify_ipc
(核心库 §9 其余、vendor/、sdk/、meow_launcherd/ 未动;未 commit/push/刷机)
```
+15 -3
View File
@@ -17,7 +17,8 @@ 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 meow_view *g_login_view, *g_home_view, *g_pl_view, *g_plr_view, *g_set_view,
*g_search_view;
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
@@ -36,6 +37,7 @@ static void app_on_tap(int x, int y)
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; }
if (search_is_active()) { search_on_tap(x, y); return; }
home_on_tap(x, y);
}
@@ -46,6 +48,7 @@ static void app_on_swipe(int dir, int dx, int dy)
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; }
if (search_is_active()) { meow_view_page_pop(); return; }
/* home / login = 根 → 退出 */
meow_log(MEOW_LOG_INFO, "exit:swipe");
meow_ui_request_exit();
@@ -53,6 +56,7 @@ static void app_on_swipe(int dir, int dx, int dy)
}
if (home_is_active()) { home_swipe(dir, dy); return; }
if (playlist_is_active()) { playlist_swipe(dir, dy); return; }
if (search_is_active()) { search_swipe(dir, dy); return; }
if (player_is_active()) { player_on_swipe(dir, dy); return; }
}
@@ -61,6 +65,7 @@ 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);
else if (search_is_active()) search_touch_down(x, y);
}
static void app_on_touch_move(int x, int y)
@@ -68,6 +73,7 @@ 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);
else if (search_is_active()) search_touch_move(x, y);
}
static void app_on_swipe_progress(int x_start, int dx)
@@ -80,6 +86,7 @@ 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; }
if (search_is_active()) { search_tick(now); return; }
if (home_is_active()) { home_tick(now); }
if (playlist_is_active()) { playlist_tick(now); }
chrome_tick();
@@ -93,7 +100,10 @@ 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);
/* 主题侦听:登记真实回调(旧代码 pass NULL 无效),系统明暗切换 → ncm_on_theme 实时换肤。
* 初始同步一次 g_dark(listener 只在变化时被调,初次状态需显式对齐)。 */
g_dark = (meow_theme_get_mode() == MEOW_THEME_DARK);
meow_theme_add_listener(ncm_on_theme);
signal(SIGTERM, on_sigterm);
/* 应用数据/核心库上下文(cookie/deviceId/cache/设置) */
@@ -107,12 +117,13 @@ int main(void)
ipc_media_start();
g_net_ready = 1;
/* 建五页 view */
/* 建六页 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();
g_search_view = search_view_create();
/* 进入根页:已登录→home,否则→login */
meow_view_page_push(g_st.logged_in ? ACT_HOME : ACT_LOGIN);
@@ -132,6 +143,7 @@ int main(void)
meow_view_free(g_pl_view);
meow_view_free(g_home_view);
meow_view_free(g_login_view);
meow_view_free(g_search_view);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;
+41
View File
@@ -28,6 +28,7 @@
#define ACT_PLAYLIST 2 /* 歌单详情(曲目列表) */
#define ACT_PLAYER 3 /* 播放页(可横竖屏切,v1 允许切) */
#define ACT_SETTINGS 4 /* 设置页 */
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
/* ---- 布局 ---- */
#define APP_W 480
@@ -44,6 +45,7 @@
#define NCM_SONG_BATCH 64 /* song_detail 分批每批条数 */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
/* ---- 播放/媒体会话状态(play 线程 ↔ UI/IPC 共享,mu 保护)---- */
enum {
@@ -121,6 +123,12 @@ typedef struct ncm_app_state {
/* 设置(持久化 MeowData/os.meow.ncm/data)*/
int level; /* NCM_LEVEL_* */
int cache_limit_gb;
/* 搜索(结果复用 g_st.songs[] 浅拷贝区;仅搜索页上下文有效)*/
char search_query[256];
int search_count; /* 当前搜索结果数(含 0=无/未搜) */
volatile int search_loading; /* 1=搜索请求在途 */
int search_err; /* 最近一次搜索错误(0=ok) */
} ncm_app_state_t;
extern ncm_app_state_t g_st; /* 全局状态(app.c 定义) */
@@ -129,6 +137,28 @@ extern volatile int g_app_quit; /* 请求退出(线程停) */
extern volatile int g_net_ready; /* 网络/核心库线程安全可用 */
extern int g_host_mock; /* 1=宿主/测试:跳过网络,用预置数据 */
/* ---- App 侧调色板(ui/common/ncm_colors.c)----
* 品牌红 + 播放页/登录专属色。中性色由 meow_theme_get() 语义色提供。§3.2/§3.3/§5.4。 */
typedef struct ncm_pal {
uint32_t red; /* 主 accent:day #C20C0C / night #EC4141 */
uint32_t red_lit; /* 亮点变体:day #EC4141 / night #FF6A6A */
uint32_t red_ctr; /* 红底浅块:day #FBE9E9 / night #3A1919 */
uint32_t on_red; /* 红底前景(#FFFFFF) */
uint32_t on_red_dim; /* 红底次前景(白 ~88%,文本用) */
uint32_t hero; /* 登录 hero 深红压场:day #C20C0C / night #7A0E0E */
uint32_t player_bg; /* 播放页沉浸底:day #FFFDF9 / night #0C0D10 */
uint32_t disc; /* 黑胶盘体:day #1A1A1A / night #141414 */
uint32_t disc_edge; /* 盘缘槽纹:day #2A2A2A / night #232323 */
} ncm_pal_t;
extern int g_dark; /* 1=黑红(night) 0=白红(day) */
const ncm_pal_t *ncm_palette(int dark); /* dark==g_dark */
void ncm_on_theme(meow_theme_mode m); /* 主题侦听:明暗变化 → 整 App 重绘 */
void ncm_fill_round(int x, int y, int w, int h, int r, uint32_t color); /* 填充圆角矩形 */
void ncm_draw_button(int x, int y, int w, int h, const char *label,
uint32_t bg, uint32_t fg); /* App 侧品牌红/FILLED 按钮 */
int first_glyph(const char *s, char *out, size_t sz); /* 取首个 UTF-8 字形(头像首字) */
const char *ncm_text_fit(const char *s, int maxw, int size); /* 按宽裁剪("..."结尾;静态缓冲) */
/* ---- 通用矩形 ---- */
typedef struct { int x, y, w, h; } ncm_rect_t;
#define NR(x,y,w,h) ((ncm_rect_t){ (x),(y),(w),(h) })
@@ -197,6 +227,17 @@ void settings_touch_move(int x, int y);
void settings_open(void); /* 进设置页 */
void settings_reload(void);
/* search */
meow_view *search_view_create(void);
int search_is_active(void);
int search_on_tap(int x, int y);
void search_swipe(int dir, int dist);
void search_touch_down(int x, int y);
void search_touch_move(int x, int y);
void search_open(void); /* 进搜索页 */
void search_submit(const char *kw); /* 用户确认后发起搜索(风控:仅在确认时发) */
void search_tick(uint64_t now); /* UI tick:检查搜索 worker 完成并刷新 */
/* ---- logic ---- */
int appstate_init(void); /* 初始化 ctx/data/cookie/设置;0=成功 */
void appstate_load_settings(void);
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+32 -1
View File
@@ -21,13 +21,16 @@ 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 };
static const int coll[] = { 0, 1, 0, 0, 1 }; /* 0=我创建的 1=我收藏的 */
g_st.playlist_count = 5;
g_st.logged_in = 1;
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
ncm_strlcpy(g_st.uid, "408996274", sizeof g_st.uid);
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];
g_st.playlists[i].is_collected = coll[i];
snprintf(g_st.playlists[i].cover, sizeof g_st.playlists[i].cover,
"http://p1.music.126.net/cover%d.jpg", i);
}
@@ -84,6 +87,7 @@ static int page_id(const char *name)
if (strcmp(name, "player") == 0) return ACT_PLAYER;
if (strcmp(name, "lands") == 0) return ACT_PLAYER; /* 横屏布局 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
if (strcmp(name, "search") == 0) return ACT_SEARCH;
return -1;
}
@@ -100,7 +104,13 @@ int main(int argc, char **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);
/* 主题:NCM_THEME=light|dark 切换两套配色(白红/黑红),1=light 时用 meow_theme_set_mode。 */
const char *theme = getenv("NCM_THEME");
int dark = !(theme && strcmp(theme, "light") == 0);
meow_theme_set_mode(dark ? MEOW_THEME_DARK : MEOW_THEME_LIGHT);
g_dark = dark;
meow_theme_add_listener(ncm_on_theme);
ncm_on_theme(dark ? MEOW_THEME_DARK : MEOW_THEME_LIGHT); /* 同步初始 g_dark */
/* mock 状态 */
if (g_st.cache_dir[0] == 0) {
@@ -135,6 +145,23 @@ int main(int argc, char **argv)
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
g_st.level = NCM_LEVEL_HIRES;
g_st.cache_limit_gb = 10;
} else if (strcmp(name, "search") == 0) {
mock_playlists();
static const char *sn[] = { "Song One", "Song Two", "Song Three" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C" };
static const char *al[] = { "Album 1", "Album 2", "Album 3" };
ncm_strlcpy(g_st.search_query, "test", sizeof g_st.search_query);
g_st.search_count = 3;
g_st.search_err = 0;
for (int i = 0; i < 3; 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, "%s", al[i]);
g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = 0;
}
g_st.song_count = 3;
} else {
mock_playlists();
}
@@ -145,6 +172,7 @@ int main(int argc, char **argv)
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();
else if (strcmp(name, "search") == 0) v = search_view_create();
if (!v) { fprintf(stderr, "bad page\n"); return 1; }
if (strcmp(name, "lands") == 0) player_set_screen(1);
@@ -156,6 +184,9 @@ int main(int argc, char **argv)
if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
player_tick(meow_ui_time_ms());
}
if (strcmp(name, "search") == 0) {
search_submit(g_st.search_query); /* 宿主:应用预置搜索结果 */
}
meow_ui_shutdown();
printf("dumped %s -> %s\n", name, dump);
+4 -3
View File
@@ -38,14 +38,15 @@ meow_widget *ncm_topbar_create(const char *title, int back, const char *action_l
return tb;
}
/* 空态/错误:居中两行(kind 大字 + what 小字)。 */
/* 空态/错误:居中两行(kind 大字 + what 小字)。kind 用品牌红(§3.1 品牌元素走 App 调色板)。 */
void ncm_draw_empty(ncm_rect_t r, const char *kind, const char *what)
{
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int cy = r.y + r.h / 2;
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, th->primary);
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, pa->red);
if (what && what[0])
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->dim);
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->onSurfaceVariant);
}
/* 底部信息(右对齐 dim)。 */
+190 -82
View File
@@ -1,9 +1,15 @@
/* ui/home.c — 我的歌单页(ACT_HOME,根页)。
/* 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 「↻」 动作项。
* 网易云风格(NCM_UI_DESIGN §4.2):红系 topbar(标题「我的音乐」+搜索/刷新/设置动作)+
* 个人区卡片(头像/昵称/ID)+ 双页签「我创建的/我收藏的」(meow_tabbar + 各自 meow_listview)+
* 组头红条 + 歌单行(rounded card + 红 mini-vinyl 缩略 + 红色按压竖条)。
*
* 品牌红走 ncm_palette(g_dark)(App 无权改 SDK MD3 蓝 primary),代码零硬编码散落:
* 中性色读 meow_theme_get(),红读 ncm_palette()。SDK listview 的 MD3 蓝选中不可改,
* 故用 MEOW_LIST_SEL_NONE + 自定义红态叠层(§4.2 叠加红条)。
*
* 左缘返回 = 退出 App(app.c)。歌单加载后台 worker(home_worker,保留 REPORT §10.11
* 的异步结构,不回退成同步——P1 启动卡顿已修,勿回退)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
@@ -14,67 +20,160 @@
typedef struct { meow_view *view; int active; } home_ctrl;
static home_ctrl g_hc;
static meow_widget *g_tb;
static meow_widget *g_list;
static ncm_rect_t r_cont, r_list;
static int g_empty_id, g_footer_id;
static char g_title[128];
static volatile int g_load_err; /* 0=ok;负=错误码(worker 写,UI draw 读) */
static meow_widget *g_tb, *g_list, *g_tabs;
static ncm_rect_t r_cont, r_list, r_tabs, r_group;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static volatile int g_load_err;
static int g_tab; /* 0=我创建的 1=我收藏的 */
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单加载后台 worker:网络(ensure_uid + user_playlist)从 UI 线程挪出,
* 使冷启动/进 home 立即出界面,再异步填列表。gen 代际作“完成”信号(对齐 cover.c)。
* 单核 MIPS,gen 在数据写完后置位,UI 见 gen 变化才重建列表,避免半成品。 */
/* 歌单加载后台 worker(P1:从 UI 线程挪出,gen 代数做完成信号)。 */
static pthread_mutex_t g_home_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_home_thr;
static int g_home_thr_on; /* 1=worker 在跑(防重复 spawn) */
static volatile int g_home_gen; /* worker 完成代数 */
static int g_home_acked; /* UI 已消费的代数 */
static volatile int g_home_loading; /* 1=加载中(draw 显示 LOADING) */
static int g_home_thr_on;
static volatile int g_home_gen;
static int g_home_acked;
static volatile int g_home_loading;
static const char *tab_label(int tab) { return tab ? "我收藏的" : "我创建的"; }
/* ---- tab 数据子集(is_collected 过滤)---- */
static int tab_count(int tab)
{
int c = 0;
for (int i = 0; i < g_st.playlist_count; i++)
if ((g_st.playlists[i].is_collected != 0) == (tab == 1)) c++;
return c;
}
static int tab_global_idx(int tab, int k)
{
int c = 0;
for (int i = 0; i < g_st.playlist_count; i++)
if ((g_st.playlists[i].is_collected != 0) == (tab == 1)) {
if (c == k) return i;
c++;
}
return -1;
}
static void home_drag_reset(void)
{
g_press_row = -1;
g_press_delta = 0;
g_dragging = 0;
g_drag_last_y = 0;
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];
int gi = tab_global_idx(g_tab, idx);
if (gi < 0) return 0;
ncm_playlist_t *pl = &g_st.playlists[gi];
static char title[128], 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;
snprintf(sub, sizeof sub, "%d 首", pl->track_count);
out->icon_kind = MEOW_LIST_ICON_NONE; /* 图标槽由红 overlays 画迷你黑胶 */
out->title = title;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = 1;
out->on = 0; out->enabled = 1;
return 1;
}
/* ---- 歌单行 rounded card 背景(z=5,画在 listview 文本之下)---- */
static void home_row_bg(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err != 0 || g_st.playlist_count == 0) return;
const meow_theme *th = meow_theme_get();
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
int n = tab_count(g_tab);
if (last > n) last = n;
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; /* 只画完整落在视口内的行 */
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer);
}
}
/* ---- 主绘制(z=60):个人卡/组头红条/tab 红态/行缩略与按压红条,>listview 红态叠层 ---- */
static void home_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int w = r_cont.w;
/* 个人区·卡片(16, y0, w-32, 72):圆角 16 surfaceContainer。 */
ncm_rect_t pc = NR(16, r_cont.y + 8, w - 32, 72);
ncm_fill_round(pc.x, pc.y, pc.w, pc.h, 16, th->surfaceContainer);
int asz = 40, ax = pc.x + 16, acy = pc.y + (pc.h - asz) / 2;
ncm_fill_round(ax, acy, asz, asz, asz / 2, pa->red_ctr); /* 圆形头像底 */
char glyph[8];
int gn = first_glyph(g_st.nickname, glyph, sizeof glyph);
if (!gn) snprintf(glyph, sizeof glyph, "\u266a"); /* ♪ 无昵称占位 */
meow_ui_text_center(ax + asz / 2, acy + (asz - th->font_body) / 2, glyph, th->font_body, pa->red);
char idtxt[64];
snprintf(idtxt, sizeof idtxt, "ID: %s", g_st.uid[0] ? g_st.uid : "-");
meow_ui_text(pc.x + asz + 28, pc.y + 6, g_st.nickname[0] ? g_st.nickname : "我的歌单",
th->font_body, th->onSurface);
meow_ui_text(pc.x + asz + 28, pc.y + 6 + th->font_body + 4, idtxt, th->font_body, th->onSurfaceVariant);
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
/* 双页签(g_tabs=meow_tabbar widget 建)——红下划线/红选中字叠层。 */
if (g_tabs) {
int iw = r_tabs.w / 2;
int ay = r_tabs.y + (r_tabs.h - meow_ui_text_height(th->font_body)) / 2;
meow_ui_fill_rect(r_tabs.x + g_tab * iw, r_tabs.y + r_tabs.h - 2, iw, 2, pa->red);
meow_ui_text_center(r_tabs.x + g_tab * iw + iw / 2, ay, tab_label(g_tab), th->font_body, pa->red);
}
/* 组头红条(red_ctr 底 + 红字 + 红侧条)。 */
ncm_fill_round(r_group.x, r_group.y, r_group.w, r_group.h, 8, pa->red_ctr);
meow_ui_fill_rect(r_group.x, r_group.y + 4, 4, r_group.h - 8, pa->red);
char ght[64];
snprintf(ght, sizeof ght, "%s · %d 个", tab_label(g_tab), tab_count(g_tab));
meow_ui_text(r_group.x + 16, r_group.y + (r_group.h - th->font_body) / 2,
ght, th->font_body, pa->red);
meow_ui_text_right(r_group.x + r_group.w - 16, r_group.y + (r_group.h - th->font_body) / 2,
"+ 新建", th->font_body, pa->red);
/* 歌单行:红 mini-vinyl 缩略 + 按压红条(文本由 listview 画在 z=10)。 */
if (g_load_err == 0 && g_st.playlist_count > 0) {
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
int isz = 24;
int first = off / item, last = (off + r_list.h) / item + 1;
int n = tab_count(g_tab);
if (last > n) last = n;
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;
int 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);
}
}
}
}
/* 空态 / 错误 / LOADING。 */
static void home_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_HOME) return;
if (g_load_err == 0 && g_st.playlist_count != 0) return;
if (g_home_loading) {
ncm_draw_empty(r_list, "LOADING", "fetching playlists...");
return;
}
if (g_st.logged_in == 0)
ncm_draw_empty(r_list, "NOT SIGNED IN", "sign in first");
else if (g_load_err == NCM_ERR_AUTH)
ncm_draw_empty(r_list, "SESSION EXPIRED", "please sign in again");
else if (g_load_err)
ncm_draw_empty(r_list, "NETWORK ERROR", "pull to retry");
else
ncm_draw_empty(r_list, "NO PLAYLISTS", "nothing here yet");
if (g_home_loading) { ncm_draw_empty(r_list, "LOADING", "正在加载歌单..."); return; }
if (g_st.logged_in == 0) ncm_draw_empty(r_list, "NOT SIGNED IN", "请先登录");
else if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "NETWORK ERROR", "上滑重试");
else ncm_draw_empty(r_list, "NO PLAYLISTS", "还没有歌单");
}
static void home_draw_footer(void *ctx)
@@ -82,32 +181,27 @@ 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,
char foot[192];
snprintf(foot, sizeof foot, "共 %d 个歌单 %s", g_st.playlist_count,
g_st.nickname[0] ? g_st.nickname : "");
ncm_draw_footer(r_cont, foot);
}
/* 把当前 playlist_count/昵称刷进列表 + 标题,并整帧重绘(UI 线程)。 */
/* 把当前 tab 子集刷进列表 + 重绘(UI 线程)。 */
static void home_apply(void)
{
if (g_list) {
meow_listview_set_count(g_list, g_st.playlist_count);
meow_listview_set_count(g_list, tab_count(g_tab));
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);
if (g_tabs) meow_tabbar_set_selected(g_tabs, g_tab);
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist),UI 线程不阻塞。 */
/* 后台 worker:唯一走网络的地方(ensure_uid + user_playlist)。 */
static void *home_worker(void *arg)
{
(void)arg;
@@ -124,33 +218,25 @@ static void *home_worker(void *arg)
void home_reload(void)
{
if (g_host_mock) { /* 宿主/测试:跳过网络,用预置数据(同步) */
if (g_host_mock) { g_load_err = 0; home_apply(); return; }
if (g_home_thr_on) return;
g_load_err = 0;
if (g_load_err != 0) g_st.playlist_count = 0;
home_apply();
return;
}
if (g_home_thr_on) return; /* 已有 worker 在跑,忽略重复触发 */
g_load_err = 0;
g_st.playlist_count = 0; /* 清旧数据,先画 LOADING,worker 完成后回填 */
g_st.playlist_count = 0;
g_home_loading = 1;
g_home_thr_on = 1;
if (pthread_create(&g_home_thr, NULL, home_worker, NULL) != 0) {
g_home_thr_on = 0;
g_home_loading = 0;
g_load_err = NCM_ERR_IO;
g_home_thr_on = 0; g_home_loading = 0; g_load_err = NCM_ERR_IO;
}
home_apply(); /* 先画(LOADING/空态),不等网络 */
home_apply();
}
/* UI tick:worker 完成(gen 变化)后重建列表并重绘。 */
void home_tick(uint64_t now)
{
(void)now;
if (!g_home_thr_on) return;
if (g_home_gen == g_home_acked) return;
g_home_acked = g_home_gen; /* 消费本次完成 */
g_home_thr_on = 0; /* worker 已结束,可再次 reload */
g_home_acked = g_home_gen;
g_home_thr_on = 0;
g_home_loading = 0;
home_apply();
}
@@ -162,14 +248,21 @@ 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 (ah == 0) { home_drag_reset(); home_reload(); return 1; } /* 刷新 */
if (ah == 1) { home_drag_reset(); settings_open(); return 1; } /* 设置 */
if (ah >= 2) { home_drag_reset(); search_open(); return 1; } /* 搜索(最右) */
}
if (g_tabs && meow_widget_hit(g_tabs, x, y)) {
int t = meow_tabbar_hit(g_tabs, x, y);
if (t >= 0 && t != g_tab) { g_tab = t; home_drag_reset(); home_apply(); }
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);
playlist_open(tab_global_idx(g_tab, row));
return 1;
}
return 0;
@@ -181,12 +274,8 @@ void home_touch_down(int x, int y)
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;
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);
}
}
@@ -198,7 +287,7 @@ void home_touch_move(int x, int y)
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); }
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;
@@ -225,14 +314,30 @@ 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);
ncm_rect_t pc = NR(16, r_cont.y + 8, r_cont.w - 32, 72);
r_tabs = NR(16, pc.y + pc.h + 8, r_cont.w - 32, 48);
r_group = NR(16, r_tabs.y + r_tabs.h + 4, r_cont.w - 32, 36);
r_list = NR(16, r_group.y + r_group.h + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_group.y + r_group.h + 8));
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"); /* 无返回;右侧 ↻ */
meow_ui_activity_push(ACT_HOME); /* owner=ACT_HOME */
g_tb = ncm_topbar_create("我的音乐", 0, "\xe2\x86\xbb"); /* 刷新(动作0) */
meow_topappbar_add_action(g_tb, "\xe2\x9a\x99"); /* 设置(动作1) */
meow_topappbar_add_action(g_tb, "\xe2\x8c\x95"); /* 搜索(动作2, 最右) */
g_tabs = meow_tabbar_create(r_tabs.x, r_tabs.y, r_tabs.w, r_tabs.h);
meow_tabbar_add_tab(g_tabs, tab_label(0));
meow_tabbar_add_tab(g_tabs, tab_label(1));
meow_tabbar_set_selected(g_tabs, 0);
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);
home_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_ui_activity_pop();
/* 显式 z:行卡片(5) < listview(10) < tabbar(20) < 红态叠层(60) < topbar(999) */
meow_widget_set_z(g_list, 10);
meow_widget_set_z(g_tabs, 20);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h, home_row_bg, NULL);
g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
home_draw, NULL);
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,
@@ -258,9 +363,12 @@ static void home_page_leave(meow_view *v, void *ctx)
static void home_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_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_tabs) { meow_widget_free(g_tabs); g_tabs = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
+36 -19
View File
@@ -18,7 +18,7 @@ static volatile int g_err; /* 最后一次错误(网络不可达等) *
static int g_active;
static int g_custom_id; /* 绘制物 */
static ncm_rect_t r_cont;
static ncm_rect_t r_scr; /* 整屏(沉浸式,hero 从 y=0 起) */
/* ---- 绘制 ---- */
static void login_draw(void *ctx)
@@ -26,41 +26,57 @@ static void login_draw(void *ctx)
(void)ctx;
if (meow_ui_activity_active() != ACT_LOGIN) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int W = meow_ui_width(), H = meow_ui_height();
/* 标题 */
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);
/* 品牌红 hero 带(沉浸,从 y=0 到 264;day=red / night=hero 深红压场)。 */
meow_ui_fill_rect(0, 0, W, 264, pa->hero);
/* 标题「网易云音乐」+ 红点 logo(用字体大小近似排版,host 字库用 ASCII 替文本)。 */
const char *title = "网易云音乐";
int tw = meow_ui_text_width(title, 34);
meow_ui_text_center(W/2, 112, title, 34, pa->on_red);
/* 24px 红点 logo(标题左前) */
ncm_fill_round(W/2 - tw/2 - 36, 112 + 4, 24, 24, 12, pa->red_lit);
meow_ui_text_center(W/2, 160, "扫码登录 · 发现好音乐", 22, pa->on_red_dim);
/* 弱分隔(hero 与内容衔接) */
meow_ui_fill_rect(0, 264, W, 1, th->outline);
/* QR 卡(128,228,224,224):day 纯白 / night surfaceContainer;圆角 16 + 内描边。 */
int qc = 224, qx = (W - qc) / 2, qy = 228;
ncm_fill_round(qx, qy, qc, qc, 16, th->surfaceContainer);
meow_ui_draw_rect_round(qx, qy, qc, qc, 16, th->outlineVariant);
/* 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);
int qs = sz * 6;
if (qs > qc - 32) qs = (qc - 32) / sz * sz;
int ox = qx + (qc - qs) / 2, oy = qy + (qc - qs) / 2;
meow_ui_fill_rect(ox, oy, qs, qs, 0xFFFFFF);
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,
meow_ui_text_center(W/2, qy + qc/2 - 10,
g_err ? "no network - tap to retry" : "fetching QR key...", 20, th->dim);
}
/* 状态文字 */
/* 状态文字:成功/等待用红(day red / night red_lit),失败红字提示。 */
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);
uint32_t scol = (g_st.login_state == NCM_LOGIN_OK) ? pa->red : th->dim;
meow_ui_text_center(W/2, 476, st, 20, scol);
/* 底部提示 */
meow_ui_text_center(W/2, H - 56, "由 MeowOS 驱动 · 仅本地解码", 18, th->dim);
}
/* 重绘 login 内容(QR+状态变化时)。 */
static void login_repaint(void)
{
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
meow_ui_invalidate(r_scr.x, r_scr.y, r_scr.w, r_scr.h);
}
const char *login_status_str(int state)
@@ -189,8 +205,9 @@ void login_tick(uint64_t now)
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,
r_scr = NR(0, 0, meow_ui_width(), meow_ui_height());
/* 沉浸式:登录页整屏自绘(hero 从 y=0 起,覆盖状态栏区)。只画一次并监听绘制物重绘。 */
g_custom_id = meow_draw_custom_register(1, r_scr.x, r_scr.y, r_scr.w, r_scr.h,
login_draw, NULL);
g_active = 0;
}
+119
View File
@@ -0,0 +1,119 @@
/* ui/common/ncm_colors.c — 网易云 App 侧调色板 + 品牌红绘制助手。
*
* 明暗两套配色(白红 day / 黑红 night),键控 meow_theme_get_mode():
* - 中性色(surface/文字/描边/错误)一律读 meow_theme_get() 语义色(§3.1),本文件不碰;
* - 品牌红走 ncm_palette(dark)(App 无权改 SDK 的 MD3 蓝 primary,红只落 App 自绘层)。
*
* main() 经 meow_theme_add_listener(ncm_on_theme) 登记主题侦听:系统明暗切换 → 更新 g_dark
* → 整 App 重绘实时换肤。页面 draw() 开头取 const ncm_pal_t *pa = ncm_palette(g_dark)。
*/
#include "../ncm_app_internal.h"
#include <string.h>
#include <math.h>
int g_dark = 0; /* 1=黑红(night);0=白红(day)。由 ncm_on_theme/启动初始化维护。 */
/* 两套 App 调色板(白红 / 黑红)。§3.2/§3.3 契约 + 播放页专属(§5.4)。 */
static const ncm_pal_t pal_day = {
.red = 0xC20C0C, /* 品牌红(主 accent) */
.red_lit = 0xEC4141, /* 亮点变体 */
.red_ctr = 0xFBE9E9, /* 红底浅块(收藏 chip/组头红条底) */
.on_red = 0xFFFFFF, /* 红底前景 */
.on_red_dim = 0xE0E0E0, /* 红底次前景(白 ~88%) */
.hero = 0xC20C0C, /* 登录 hero 深红压场 */
.player_bg = 0xFFFDF9, /* 播放页沉浸暖白(瓷白) */
.disc = 0x1A1A1A, /* 黑胶盘体(白底上醒目) */
.disc_edge = 0x2A2A2A, /* 盘缘槽纹 */
};
static const ncm_pal_t pal_night = {
.red = 0xEC4141, /* 黑红下主 accent 改亮红(深红发闷) */
.red_lit = 0xFF6A6A, /* 亮点变体 */
.red_ctr = 0x3A1919, /* 红底浅块(低饱和弱对比) */
.on_red = 0xFFFFFF,
.on_red_dim = 0xE6E6E6,
.hero = 0x7A0E0E, /* 登录 hero 深红压场(night) */
.player_bg = 0x0C0D10, /* 播放页沉浸近黑 */
.disc = 0x141414, /* 盘恒黑 */
.disc_edge = 0x232323,
};
const ncm_pal_t *ncm_palette(int dark) { return dark ? &pal_night : &pal_day; }
/* 主题侦听:明暗变化 → 重绘全 App(实时换肤)。 */
void ncm_on_theme(meow_theme_mode m)
{
int d = (m == MEOW_THEME_DARK);
if (d == g_dark) return; /* 无变化,忽略 */
g_dark = d;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg); /* 背景随系统明暗 */
meow_ui_paint_full();
}
/* 填充圆角矩形(软渲无 fill_round 原语;行扫近似,仅数张卡片/行,代价低)。
* 与 §5.1 黑胶行扫同技法:每行按角半径求出水平内缩,fill_rect 即可。 */
void ncm_fill_round(int x, int y, int w, int h, int r, uint32_t color)
{
if (r <= 0) { meow_ui_fill_rect(x, y, w, h, color); return; }
if (r > w / 2) r = w / 2;
if (r > h / 2) r = h / 2;
for (int dy = 0; dy < h; dy++) {
int inset = 0;
if (dy < r) {
int d = r - dy;
inset = r - (int)sqrt((double)(r * r - d * d));
} else if (dy >= h - r) {
int d = dy - (h - r);
inset = r - (int)sqrt((double)(r * r - d * d));
}
if (inset < 0) inset = 0;
if (inset > w / 2) inset = w / 2;
meow_ui_fill_rect(x + inset, y + dy, w - 2 * inset, 1, color);
}
}
/* 填充按钮(胶囊/圆角):App 侧绘制品牌红钮(SDK meow_btn FILLED 用 MD3 蓝 primary,无权改)。
* 用 ncm_fill_round 画底 + 居中文本。r<=0 时用不小于 h/2 的胶囊半径。 */
void ncm_draw_button(int x, int y, int w, int h, const char *label,
uint32_t bg, uint32_t fg)
{
int r = h / 2;
ncm_fill_round(x, y, w, h, r, bg);
if (label && label[0]) {
int th = meow_ui_text_height(20);
meow_ui_text_center(x + w / 2, y + (h - th) / 2, label, 20, fg);
}
}
/* 取字符串首个 UTF-8 字形(头像首字用)。写 out(含 NUL),返回字节数;空串返回 0。 */
int first_glyph(const char *s, char *out, size_t sz)
{
if (!s || !s[0] || !out || sz == 0) { if (out && sz) out[0] = 0; return 0; }
unsigned char c = (unsigned char)s[0];
int n = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
if ((size_t)n >= sz) n = (int)sz - 1;
memcpy(out, s, (size_t)n);
out[n] = 0;
return n;
}
/* 按宽度裁剪字符串(UTF-8 字符合法边界 + "..." 结尾)。返回静态缓冲,立即用完。size=字号。 */
const char *ncm_text_fit(const char *s, int maxw, int size)
{
static char buf[256];
if (!s) return "";
if (maxw <= 0) return "";
if (meow_ui_text_width(s, size) <= maxw) return s;
size_t i = 0;
for (i = 0; s[i] && i < sizeof buf - 5; ) {
unsigned char c = (unsigned char)s[i];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
if (i + (size_t)ch >= sizeof buf - 5) break;
memcpy(buf, s, i + (size_t)ch); buf[i + (size_t)ch] = 0;
if (meow_ui_text_width(buf, size) > maxw) break;
i += (size_t)ch;
}
memcpy(buf + i, "...", 4);
return buf;
}
+218 -119
View File
@@ -1,10 +1,15 @@
/* ui/player.c — 播放页(ACT_PLAYER)。
/* ui/player.c — 播放页(ACT_PLAYER):黑胶唱盘 + 唱针 + 进度/控制/滚动歌词(竖/横布局可切)。
*
* 布局:封面(后台 cover 线程解码) + 标题/歌手 + 进度条(可拖 seek) + 播放/暂停/上下曲 + 滚动歌词
* (时间轴解析,当前行高亮居中)。播放页横竖屏可切(device LCD 为竖屏 480x720,故为**布局**切换:
* 竖屏=封面在上、歌词在下;横屏=封面在左、信息+歌词在右,仍在 480x720 画布内)。
* 进度/状态每 tick 读 play_manager(跨线程 mu 保护),歌词行由 pos_ms 推出。
* 全部内容画进一个 custom 绘制物;控件钮/进度条/歌词命中用 player_draw 计算的共享几何。
* 黑胶(NCM_UI_DESIGN §5.1,软渲无圆形原语,全 fill_rect 行扫):
* ① 盘体 = 对每行 dy 取 half=√(R²-dy²) 画 fill_rect(同心圆)。
* ② 封面粉圆 = 方盖(S=R×√2)内切于圆 + 四角把「圆外-方外」两段盖回盘色 = 封面成圆。
* ③ 唱针 = 从盘心右上到盘缘一根 3px 唱臂 + 端头;播放压盘/暂停微抬(状态切换才画)。
* 盘恒黑(palette.disc),accent(进度已播/播放钮/歌词高亮)日夜换红明暗(palette.red/red_lit)。
*
* 性能(§6,专项):**分带刷新**——内容拆 盘带(仅圆盘) + 动态带(歌名/进度/控制/歌词/页脚) 两个绘制物,
* 各带独立且互不重叠 bbox。tick 只标脏「变化的那一条带」,盘带仅 歌曲/封面/播停态 变化才重画
* (修 player_tick 每 tick 刷整 r_cont 的隐患)。播放页恒沉浸(day 瓷白 / night 近黑,
* 不随系统明暗整体反转,仅 accent 换红明暗,§5.4)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
@@ -17,16 +22,20 @@ 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;
static int g_custom_disc, g_custom_dyn;
static int g_landscape;
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
/* 共享几何(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 /* 侧钮命中半径 */
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
static ncm_rect_t g_disc_r, g_dyn_r; /* 盘带 / 动态带 bbox(不重叠) */
static int g_cx, g_cy, g_R, g_S; /* 盘心 / 半径 / 封面边长 */
static ncm_rect_t g_bar; /* 进度条 */
static int g_times_y;
static int g_ctl_y, g_cprev_x, g_cplay_x, g_cnext_x, g_cr;
static int g_title_y, g_artist_y, g_info_x, g_info_w;
static ncm_rect_t g_lyr; /* 歌词区 */
#define CTL_RR 30 /* 侧钮命中半径(命中容差) */
static int cur_song(void)
{
@@ -41,7 +50,6 @@ static void fmt_ms(int64_t ms, char *out, size_t sz)
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++) {
@@ -51,7 +59,6 @@ static void tri(int x0, int y0, int h, int w, int dir, uint32_t 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++) {
@@ -61,113 +68,192 @@ static void draw_ctl(int cx, int cy, int r, int kind, uint32_t base, uint32_t fg
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);
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 arm_line(int x0, int y0, int x1, int y1, uint32_t color, int wpx)
{
int dx = x1 - x0, dy = y1 - y0;
int steps = (abs(dx) > abs(dy) ? abs(dx) : abs(dy)) / 2 + 1;
if (steps < 1) steps = 1;
for (int i = 0; i <= steps; i++) {
int x = x0 + dx * i / steps;
int y = y0 + dy * i / steps;
meow_ui_fill_rect(x - wpx / 2, y - wpx / 2, wpx, wpx, color);
}
}
static void player_draw(void *ctx)
/* 几何(竖/横布局)。带 bbox:盘带=圆盘矩形,动态带=其余(互不重叠)。 */
static void player_compute_geom(void)
{
r_cont = ncm_content_rect();
if (g_landscape) {
g_cx = 180; g_cy = 360; g_R = 120; g_S = (int)(g_R * 1.414);
g_info_x = 340; g_info_w = r_cont.x + r_cont.w - 16 - g_info_x;
g_title_y = 150; g_artist_y = 188;
g_bar = NR(g_info_x, 240, 108, 4); g_times_y = 226;
g_ctl_y = 310; g_cprev_x = 348; g_cplay_x = 404; g_cnext_x = 460; g_cr = 26;
g_lyr = NR(300, 372, 164, 280);
} else {
g_cx = 240; g_cy = 272; g_R = 104; g_S = (int)(g_R * 1.414);
g_info_x = 240; g_info_w = r_cont.w;
g_title_y = 388; g_artist_y = 422;
g_bar = NR(56, 456, 368, 4); g_times_y = 436;
g_ctl_y = 508; g_cprev_x = 176; g_cplay_x = 240; g_cnext_x = 304; g_cr = 30;
g_lyr = NR(16, 546, 448, 150);
}
int cb = r_cont.y + r_cont.h - APP_FOOTER_H;
if (g_lyr.y + g_lyr.h > cb) g_lyr.h = cb - g_lyr.y;
if (g_lyr.h < 0) g_lyr.h = 0;
if (g_landscape) {
g_disc_r = NR(g_cx - g_R, g_cy - g_R, 2*g_R, 2*g_R); /* 左盘区 */
int dy0 = g_title_y - 12;
g_dyn_r = NR(g_info_x - 20, dy0, r_cont.x + r_cont.w - (g_info_x - 20), cb - dy0 + 8);
} else {
g_disc_r = NR(0, g_cy - g_R, r_cont.w, 2*g_R); /* 全宽圆盘带 */
g_dyn_r = NR(0, g_cy + g_R, r_cont.w, cb - (g_cy + g_R)); /* 其余(含歌名/进度/歌词) */
}
}
/* 盘带绘制物:盘体 + 方盖内切圆(四角盖色) + 封面 + 唱针。仅 歌曲/封面/播停态 见变化才重画。 */
static void plr_draw_disc(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;
const ncm_pal_t *pa = ncm_palette(g_dark);
int cx = g_cx, cy = g_cy, R = g_R, S = g_S;
int st = player_manager_state();
int paused = (st == NCM_PC_PAUSED);
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, pa->disc);
}
for (int rr = R - 3; rr >= R - 12; rr -= 4) {
for (int dy = -rr; dy <= rr; dy++) {
int half = (int)sqrt((double)(rr*rr - dy*dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->disc_edge);
}
}
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。 */
meow_img *cov = cover_get();
int hS = S / 2;
if (cov) meow_ui_img_cover(cx - hS, cy - hS, S, cov);
else {
for (int dy = -hS; dy <= hS; dy++) {
int half = (int)sqrt((double)(hS*hS - dy*dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->red_ctr);
}
meow_ui_text_center(cx, cy - 10, "\u266a", th->font_body, pa->red);
}
for (int dy = -R; dy <= R; dy++) {
int half = (int)sqrt((double)(R*R - dy*dy));
if (half <= hS) continue;
meow_ui_fill_rect(cx - half, cy + dy, half - hS, 1, pa->disc);
meow_ui_fill_rect(cx + hS, cy + dy, half - hS + 1, 1, pa->disc);
}
meow_ui_fill_rect(cx - 2, cy - 2, 4, 4, pa->player_bg); /* 盘心孔 */
/* 唱针:播放压盘 / 暂停微抬 6px。 */
int arm_top = paused ? 6 : 0;
int ax0 = cx + 40, ay0 = cy - 70 + arm_top;
int ax1 = cx + R - 6, ay1 = cy + 8 + arm_top;
arm_line(ax0, ay0, ax1, ay1, th->outline, 3);
meow_ui_fill_rect(ax1 - 3, ay1 - 3, 6, 6, th->onSurface);
/* 高光(廉价):左下 45° 1px 弧。 */
for (int a = 0; a < 20; a++) {
double t = (a / 19.0) * (3.14159 / 2);
int hx = cx - (int)((R - 6) * 0.71 * cos(t));
int hy = cy + (int)((R - 6) * 0.71 * sin(t));
meow_ui_fill_rect_alpha(hx, hy, 2, 1, 0xFFFFFF, 60);
}
}
/* 动态带绘制物:歌名/歌手 + 进度条/时间 + 控制钮 + 滚动歌词 + 页脚。tick 主要刷此带。 */
static void plr_draw_dyn(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int sc = cur_song();
const ncm_song_t *s = (sc >= 0) ? &g_st.songs[sc] : NULL;
const char *nm = s ? s->name : "nothing playing";
const char *ar = s ? s->art_name : "";
/* 封面区域 */
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);
if (g_landscape) {
meow_ui_text(g_info_x, g_title_y, ncm_text_fit(nm, g_info_w, 28), 28, th->onSurface);
meow_ui_text(g_info_x, g_artist_y, ncm_text_fit(ar, g_info_w, 20), 20, th->onSurfaceVariant);
} else {
meow_ui_text_center(g_info_x, g_title_y, nm, 28, th->onSurface);
meow_ui_text_center(g_info_x, g_artist_y, ar, 20, th->onSurfaceVariant);
}
/* 进度条 + 时间 */
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);
if (dur <= 0 && sc >= 0) dur = (int)(g_st.songs[sc].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;
meow_ui_text(g_bar.x, g_times_y, lp, 20, th->onSurfaceVariant);
meow_ui_text_right(g_bar.x + g_bar.w, g_times_y, rp, 20, th->onSurfaceVariant);
} 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;
meow_ui_text(44, g_times_y, lp, 20, th->onSurfaceVariant);
meow_ui_text_right(r_cont.x + r_cont.w - 44, g_times_y, rp, 20, th->onSurfaceVariant);
}
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);
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, th->surfaceVariant);
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
int st = player_manager_state();
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
draw_ctl(g_cplay_x, g_ctl_y, g_cr, (st == NCM_PC_PAUSED) ? 1 : 2, pa->red, pa->on_red);
draw_ctl(g_cnext_x, g_ctl_y, g_cr - 6, 3, th->surfaceContainer, th->onSurface);
/* 歌词区(分隔线 + 滚动歌词,当前行红高亮居中) */
meow_ui_fill_rect(g_lyr.x, g_lyr.y, g_lyr.w, 1, th->outlineVariant);
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;
}
int line_h = (g_landscape) ? 24 : 26;
int center_y = g_lyr.y + g_lyr.h / 2;
int lcx = g_lyr.x + g_lyr.w / 2;
if (n == 0) { meow_ui_text_center(lcx, center_y - 10, "no lyric", 20, th->onSurfaceVariant); }
else {
int64_t ps = player_manager_pos_ms();
int cur2 = lyric_line_at(ps);
int lw = g_lyr.w - 8; /* 歌词列宽(裁剪,防窄列溢出) */
for (int i = cur2 - 6; i <= cur2 + 5; i++) {
if (i < 0 || i >= n) continue;
int yy = center_y + (i - cur2) * line_h;
if (yy < g_lyr.y || yy + line_h > g_lyr.y + g_lyr.h) continue; /* 裁剪到歌词带(自定义绘制物不按 bbox 裁剪) */
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);
meow_ui_text_center(lcx, yy, ncm_text_fit(lyric_line_text(i), lw, is_cur ? 24 : 20),
is_cur ? 24 : 20, is_cur ? pa->red : th->onSurfaceVariant);
const char *tl = lyric_line_tl(i);
if (is_cur && tl && tl[0])
meow_ui_text_center(lcx, yy + 24, tl, 20, th->secondary);
meow_ui_text_center(lcx, yy + 24, ncm_text_fit(tl, lw, 20), 20, th->secondary);
}
}
/* 页脚提示 */
char foot[64];
snprintf(foot, sizeof foot, "左右滑切歌 · 点顶部切横屏");
meow_ui_text_center(r_cont.x + r_cont.w / 2, r_cont.y + r_cont.h - APP_FOOTER_H,
foot, 20, th->onSurfaceVariant);
}
int player_is_active(void) { return g_plc.active; }
@@ -177,7 +263,9 @@ 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);
player_compute_geom();
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
return old;
}
@@ -186,24 +274,22 @@ 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 (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 (x >= g_cplay_x - g_cr && x <= g_cplay_x + g_cr && y >= g_ctl_y - g_cr && y <= g_ctl_y + g_cr) {
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) {
if (x >= g_cprev_x - CTL_RR && x <= g_cprev_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
player_manager_next(-1); return;
}
if (x >= g_cnext_x - CTL_RR && x <= g_cnext_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
player_manager_next(1); 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);
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
return;
}
}
@@ -228,27 +314,37 @@ 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; }
/* tick:分带刷新——只标脏变化的那一带(§6)。盘带仅在歌曲/封面/播停态变化时重画。 */
void player_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_plc.active) return;
int sc = cur_song();
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 (sc != g_last_song) {
g_last_song = sc;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌名/歌手变 */
}
if (cover_changed()) {
cover_ack();
meow_ui_invalidate(g_cov.x, g_cov.y, g_cov.w, g_cov.h);
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
}
if (pos != g_last_pos_ms || st != g_last_state) {
g_last_pos_ms = pos;
int state_changed = (st != g_last_state);
g_last_state = st;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 进度/控制钮/时间 */
if (state_changed)
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h); /* 唱针抬/压 */
}
if (line != g_last_line) {
g_last_line = line;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌词 */
}
}
@@ -256,21 +352,23 @@ static void plr_page_create(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
player_compute_geom();
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;
g_custom_disc = meow_draw_custom_register(10, g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h,
plr_draw_disc, NULL);
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
plr_draw_dyn, NULL);
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -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);
const ncm_pal_t *pa = ncm_palette(g_dark);
meow_ui_set_page_bg(pa->player_bg); /* 播放页恒沉浸,不随系统明暗整体反转 */
meow_ui_paint_full();
if (!g_host_mock) player_manager_load_lyric();
}
@@ -284,7 +382,8 @@ static void plr_page_leave(meow_view *v, void *ctx)
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_custom_disc) { meow_draw_unregister(g_custom_disc); g_custom_disc = 0; }
if (g_custom_dyn) { meow_draw_unregister(g_custom_dyn); g_custom_dyn = 0; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
+157 -58
View File
@@ -1,8 +1,12 @@
/* ui/playlist.c — 歌单详情页(ACT_PLAYLIST,曲目列表)。
*
* 曲目用 SDK 结构化列表(meow_listview)虚拟化渲染「歌名 + 歌手 副标题」。点击行 → 起播并推播放页。
* 进入时拉歌单详情(trackIds)+ 分批 song_detail(有界 NCM_SONG_MAX)。空态/错误用绘制物。
* 左缘返回 = 弹回主页(app.c)。
* 网易云风格(NCM_UI_DESIGN §4.3):封面头块(圆角封面 + 歌单名 + 元信息)+ 红色「播放全部」
* 操作条(播放全部可用;收藏/评论 v1 置灰 → snack)+ 曲目列表(歌名 + 歌手·专辑 副标题)。
* 点击曲目行 → 起播并推播放页。进入时拉歌单详情(trackIds)+ song_detail 在后台 worker
* (playlist_worker,保留 §10.11 防大歌单冻 UI 的异步结构)。
*
* 品牌红走 ncm_palette(g_dark);中性色读 meow_theme_get()。SDK meow_btn FILLED 是 MD3 蓝,
* 红色「播放全部」用 App 自绘(ncm_draw_button + 手动命中)。左缘返回 = 弹回主页(app.c)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
@@ -15,21 +19,20 @@ 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 ncm_rect_t r_cont, r_list, r_card, r_cov, r_plall, r_fav, r_cmt;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static char g_title[256];
static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单详情加载后台 worker:大歌单 detail+song_detail 在设备上可达数十秒(trackIds 全量 900KB+),
* 不能占 UI 线程。worker 记录它装载的歌单下标,tick 判变;若用户已切到别的歌单则丢弃并重新加载。 */
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。 */
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_plthr;
static int g_plthr_on; /* 1=worker 在跑 */
static volatile int g_plgen; /* worker 完成代数 */
static int g_placked; /* UI 已消费代数 */
static volatile int g_plloading; /* 1=加载中(draw 显示 LOADING) */
static volatile int g_plloaded_idx;/* worker 实际装载的歌单下标 */
static int g_plthr_on;
static volatile int g_plgen;
static int g_placked;
static volatile int g_plloading;
static volatile int g_plloaded_idx;
static void pl_drag_reset(void)
{
@@ -41,31 +44,119 @@ 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];
static char sub[256];
if (s->art_name[0] && s->album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s->art_name, s->album_name);
else if (s->art_name[0]) snprintf(sub, sizeof sub, "%s", s->art_name);
else if (s->album_name[0]) snprintf(sub, sizeof sub, "%s", s->album_name);
else snprintf(sub, sizeof sub, "-");
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->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = 1;
out->on = 0; out->enabled = 1;
return 1;
}
/* 曲目行 rounded card 背景(z=5,画在 listview 文本之下)。 */
static void pl_row_bg(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0 || g_st.song_count == 0) return;
const meow_theme *th = meow_theme_get();
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 > g_st.song_count) last = g_st.song_count;
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; /* 只画完整落在视口内的行 */
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer);
}
}
/* 主绘制(z=60):封面头块 + 操作条 + 行缩略/按压红条。 */
static void pl_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
/* 封面头块卡片 */
ncm_fill_round(r_card.x, r_card.y, r_card.w, r_card.h, 16, th->surfaceContainer);
meow_img *cov = cover_get();
ncm_fill_round(r_cov.x, r_cov.y, r_cov.w, r_cov.h, 12, pa->red_ctr);
if (cov) meow_ui_img_cover(r_cov.x, r_cov.y, r_cov.w, cov);
else {
meow_ui_text_center(r_cov.x + r_cov.w / 2, r_cov.y + (r_cov.h - th->font_body) / 2,
"\u266a", th->font_body, pa->red); /* ♪ */
}
const ncm_playlist_t *pl = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
? &g_st.playlists[g_st.active_playlist] : NULL;
const char *nm = pl ? pl->name : "歌单名";
int nmx = r_cov.x + r_cov.w + 12;
char meta[256];
if (pl) snprintf(meta, sizeof meta, "创建者 · %d 首", pl->track_count);
else snprintf(meta, sizeof meta, "曲目 %d 首", g_st.song_count);
/* 歌单名(截断到卡片右缘:按 UTF-8 字符合法边界逐字累加测宽)。 */
{
int maxw = r_card.x + r_card.w - 16 - nmx;
static char nmbuf[300], tmp[300];
size_t bi = 0;
while (nm[bi] && bi < sizeof nmbuf - 1) {
unsigned char cc = (unsigned char)nm[bi];
int ch = (cc < 0x80) ? 1 : (cc < 0xE0) ? 2 : (cc < 0xF0) ? 3 : 4;
if (bi + (size_t)ch >= sizeof nmbuf) break;
memcpy(tmp, nm, bi + (size_t)ch); tmp[bi + (size_t)ch] = 0;
if (meow_ui_text_width(tmp, th->font_body) > maxw) break;
bi += (size_t)ch;
}
memcpy(nmbuf, nm, bi); nmbuf[bi] = 0;
meow_ui_text(nmx, r_card.y + 16, nmbuf, th->font_body, th->onSurface);
}
meow_ui_text(nmx, r_card.y + 16 + th->font_body + 8, meta, th->font_body, th->onSurfaceVariant);
/* 操作条:播放全部(红 FILLED) + 收藏(OUTLINED) + 评论(红 TEXT+badge) */
ncm_draw_button(r_plall.x, r_plall.y, r_plall.w, r_plall.h, "播放全部", pa->red, pa->on_red);
meow_ui_draw_rect_round(r_fav.x + 1, r_fav.y + 1, r_fav.w - 2, r_fav.h - 2,
r_fav.h / 2 - 1, th->outlineVariant);
meow_ui_text_center(r_fav.x + r_fav.w / 2, r_fav.y + (r_fav.h - th->font_body) / 2,
"收藏", th->font_body, th->onSurface);
meow_ui_text_center(r_cmt.x + r_cmt.w / 2, r_cmt.y + (r_cmt.h - th->font_body) / 2,
"评论", th->font_body, pa->red);
/* 红色小 badge(评论置灰入口) */
meow_ui_fill_rect(r_cmt.x + r_cmt.w / 2 + 34, r_cmt.y + 10, 6, 6, pa->red);
/* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */
if (g_load_err == 0 && g_st.song_count > 0) {
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 > g_st.song_count) last = g_st.song_count;
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 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_plloading) {
ncm_draw_empty(r_list, "LOADING", "fetching tracks...");
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");
if (g_plloading) { ncm_draw_empty(r_list, "LOADING", "正在加载曲目..."); return; }
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "LOAD FAILED", "返回后重试");
else ncm_draw_empty(r_list, "NO TRACKS", "这个歌单是空的");
}
static void pl_draw_footer(void *ctx)
@@ -73,15 +164,13 @@ static void pl_draw_footer(void *ctx)
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0) return;
int total = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
? g_st.playlists[g_st.active_playlist].track_count : g_st.song_count;
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);
snprintf(foot, sizeof foot, "%d/%d 首", g_st.song_count, total);
ncm_draw_footer(r_cont, foot);
}
/* 把当前 song_count 刷进列表 + 标题,并整帧重绘(UI 线程)。 */
static void pl_apply(void)
{
if (g_list) {
@@ -98,11 +187,10 @@ static void pl_apply(void)
meow_ui_paint_full();
}
/* 后台 worker:detail + song_detail 全在独立线程,UI 不卡(大歌单可数十秒)。 */
static void *playlist_worker(void *arg)
{
(void)arg;
int my_idx = g_st.active_playlist; /* 装载目标(UI 已设定) */
int my_idx = g_st.active_playlist;
int rc;
if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG;
@@ -118,26 +206,18 @@ static void *playlist_worker(void *arg)
void playlist_refresh(void)
{
if (g_host_mock) { /* 宿主/测试:预置数据,同步 */
if (g_host_mock) { g_load_err = 0; pl_apply(); return; }
if (g_plthr_on) return;
g_load_err = 0;
if (g_load_err != 0) g_st.song_count = 0;
pl_apply();
return;
}
if (g_plthr_on) return; /* 已有 worker 在跑 */
g_load_err = 0;
g_st.song_count = 0; /* 清旧曲目,先画 LOADING,worker 完成后回填 */
g_st.song_count = 0;
g_plloading = 1;
g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
g_plthr_on = 0;
g_plloading = 0;
g_load_err = NCM_ERR_IO;
g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO;
}
pl_apply(); /* 先画(LOADING/空态),不等网络 */
pl_apply();
}
/* UI tick:worker 完成(gen 变化)后重建列表;若装载目标已切换则丢弃并重新加载。 */
void playlist_tick(uint64_t now)
{
(void)now;
@@ -146,10 +226,7 @@ void playlist_tick(uint64_t now)
g_placked = g_plgen;
g_plthr_on = 0;
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) {
playlist_refresh(); /* 已切别的歌单:丢弃并重新加载当前目标 */
return;
}
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
if (g_load_err != 0) g_st.song_count = 0;
pl_apply();
}
@@ -161,7 +238,7 @@ void playlist_open(int idx)
g_st.playing_song = -1;
cover_cache_reset();
meow_view_page_push(ACT_PLAYLIST);
playlist_refresh(); /* page_enter 也会调用(幂等);这里先拉避免空帧 */
playlist_refresh();
}
int playlist_is_active(void) { return g_pc.active; }
@@ -173,12 +250,20 @@ int playlist_on_tap(int x, int 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 (x >= r_plall.x && x < r_plall.x + r_plall.w &&
y >= r_plall.y && y < r_plall.y + r_plall.h) { pl_drag_reset(); player_play_song(0); return 1; }
if (x >= r_fav.x && x < r_fav.x + r_fav.w && y >= r_fav.y && y < r_fav.y + r_fav.h) {
pl_drag_reset(); ncm_snack("收藏功能开发中"); return 1;
}
if (x >= r_cmt.x && x < r_cmt.x + r_cmt.w && y >= r_cmt.y && y < r_cmt.y + r_cmt.h) {
pl_drag_reset(); ncm_snack("评论功能开发中"); 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); /* 起播并推播放页 */
player_play_song(row);
return 1;
}
return 0;
@@ -190,7 +275,7 @@ void playlist_touch_down(int x, int y)
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); }
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);
}
@@ -203,7 +288,7 @@ void playlist_touch_move(int x, int y)
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); }
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;
@@ -229,14 +314,26 @@ 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);
r_card = NR(16, r_cont.y + 8, r_cont.w - 32, 148);
r_cov = NR(r_card.x + 16, r_card.y + 20, 80, 80);
int by = r_card.y + r_card.h + 12;
r_plall = NR(r_card.x + 16, by, 152, 48);
r_fav = NR(r_card.x + 16 + 152 + 12, by, 120, 48); /* 32+164+12? 修正为绝对 */
r_cmt = NR(r_fav.x + r_fav.w + 12, by, 120, 48);
r_list = NR(16, by + 48 + 12, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (by + 48 + 12));
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_tb = ncm_topbar_create("歌单", 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);
pl_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,
pl_row_bg, NULL);
g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
pl_draw, NULL);
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,
@@ -262,8 +359,10 @@ static void pl_page_leave(meow_view *v, void *ctx)
static void pl_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_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; }
}
+290
View File
@@ -0,0 +1,290 @@
/* ui/search.c — 搜索页(ACT_SEARCH,顶栏搜索按钮进入)。
*
* 独立页面:搜索输入框(SDK TextField,接 IME §5.1,仅英文/数字)+ 红色「搜索」按钮 +
* 结果列表(点击可播,复用 g_st.songs[] 起点:播放/上下一首均对结果集生效)。
* 搜索结果网络走后台 worker(search_worker),UI 不卡。
*
* 风控纪律(用户指定):**搜索请求只在用户确认时发**——用户点「搜索」按钮才发起 cloudsearch,
* 进入页面不自动搜索;IME 输入仅本地编辑,不逐键发请求。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct { meow_view *view; int active; } search_ctrl;
static search_ctrl g_sc;
static meow_widget *g_tb, *g_field, *g_list;
static ncm_rect_t r_cont, r_list, r_btn;
static int g_empty_id, g_footer_id, g_btn_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_thr;
static int g_thr_on;
static volatile int g_gen;
static int g_acked;
static volatile int g_loading;
static char g_query[256];
static void sr_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
static int sr_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_st.search_count) return 0;
ncm_song_t *s = &g_st.songs[idx];
static char sub[256];
if (s->art_name[0] && s->album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s->art_name, s->album_name);
else if (s->art_name[0]) snprintf(sub, sizeof sub, "%s", s->art_name);
else snprintf(sub, sizeof sub, "%s", s->album_name);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s->name;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
return 1;
}
static void sr_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
if (g_st.search_count != 0 && g_st.search_err == 0) return;
if (g_loading) { ncm_draw_empty(r_list, "SEARCHING", "正在搜索..."); return; }
if (g_st.search_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "SESSION EXPIRED", "请重新登录");
else if (g_st.search_err) ncm_draw_empty(r_list, "SEARCH FAILED", "点「搜索」重试");
else if (!g_query[0]) ncm_draw_empty(r_list, "NO KEYWORD", "输入关键词点「搜索」");
else ncm_draw_empty(r_list, "NO RESULTS", "未找到相关歌曲");
}
static void sr_draw_footer(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
if (g_st.search_err != 0 || g_st.search_count == 0) return;
char foot[64];
snprintf(foot, sizeof foot, "找到 %d 个结果", g_st.search_count);
ncm_draw_footer(r_cont, foot);
}
/* 「搜索」红色确认钮(SDK meow_btn FILLED 是 MD3 蓝;App 自绘品牌红)。 */
static void sr_draw_btn(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_SEARCH) return;
const ncm_pal_t *pa = ncm_palette(g_dark);
ncm_draw_button(r_btn.x, r_btn.y, r_btn.w, r_btn.h, "搜索", pa->red, pa->on_red);
}
static void sr_apply(void)
{
if (g_list) {
meow_listview_set_count(g_list, g_st.search_count);
meow_listview_set_scroll(g_list, 0);
meow_listview_set_sel(g_list, -1);
}
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
/* 后台 worker:唯一网络调用(cloudsearch),UI 不卡。 */
static void *search_worker(void *arg)
{
(void)arg;
const char *kw = g_query;
int n = 0;
int rc = ncm_api_cloudsearch(g_st.ctx.device_id, g_st.ctx.cookie, kw,
g_st.songs, NCM_SEARCH_MAX, &n);
pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) {
g_st.search_count = n;
g_st.song_count = n; /* 结果集作为当前播放上下文 */
g_st.search_err = 0;
} else {
g_st.search_count = 0;
g_st.song_count = 0;
g_st.search_err = rc;
}
g_loading = 0;
g_gen++;
pthread_mutex_unlock(&g_mu);
return NULL;
}
void search_submit(const char *kw)
{
if (!kw || !kw[0]) { ncm_snack("请输入关键词"); return; }
ncm_strlcpy(g_query, kw, sizeof g_query);
ncm_strlcpy(g_st.search_query, kw, sizeof g_st.search_query);
if (g_host_mock) { /* 宿主/测试:预置结果,同步 */
g_loading = 0; g_st.search_err = 0; sr_apply(); return;
}
if (g_thr_on) return; /* 已有 worker 在跑 */
g_st.search_err = 0;
g_st.search_count = 0;
g_loading = 1;
g_thr_on = 1;
if (pthread_create(&g_thr, NULL, search_worker, NULL) != 0) {
g_thr_on = 0; g_loading = 0; g_st.search_err = NCM_ERR_IO;
}
sr_apply();
}
void search_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_thr_on) return;
if (g_gen == g_acked) return;
g_acked = g_gen;
g_thr_on = 0;
g_loading = 0;
sr_apply();
}
void search_open(void)
{
meow_view_page_push(ACT_SEARCH);
}
int search_is_active(void) { return g_sc.active; }
int search_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { sr_drag_reset(); meow_view_page_pop(); return 1; }
if (ah >= 0) { sr_drag_reset(); return 1; }
}
if (x >= r_btn.x && x < r_btn.x + r_btn.w && y >= r_btn.y && y < r_btn.y + r_btn.h) {
const char *t = meow_textfield_text(g_field); /* 「搜索」确认(风控:仅此处发) */
search_submit(t ? t : "");
return 1;
}
if (g_field && meow_widget_hit(g_field, x, y)) { /* 唤 IME */
meow_textfield_activate(g_field);
return 1;
}
if (g_st.search_count > 0 && g_st.search_err == 0 && meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row < 0) { sr_drag_reset(); return 1; }
sr_drag_reset();
player_play_song(row); /* 结果集即 g_st.songs[],点击可播 */
return 1;
}
return 0;
}
void search_touch_down(int x, int y)
{
sr_drag_reset();
if (g_st.search_count == 0 || g_st.search_err != 0) return;
if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row >= 0) g_press_row = row;
g_dragging = 1; g_drag_last_y = y;
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
}
void search_touch_move(int x, int y)
{
(void)x;
if (!g_dragging) return;
if (g_press_row >= 0) {
int d = y - g_drag_last_y; if (d < 0) d = -d;
g_press_delta += d;
if (g_press_delta > 10) g_press_row = -1;
}
int dy = y - g_drag_last_y; g_drag_last_y = y;
if (dy == 0) return;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, -dy);
if (meow_listview_scroll(g_list) != before)
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
void search_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_st.search_count == 0 || g_st.search_err != 0) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before)
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
static void sr_page_create(meow_view *v, void *ctx)
{
(void)v;
((search_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
ncm_rect_t field = NR(16, r_cont.y + 12, r_cont.w - 32, 48);
r_btn = NR(field.x, field.y + field.h + 12, field.w, 44);
r_list = NR(16, r_btn.y + r_btn.h + 12, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_btn.y + r_btn.h + 12));
if (r_list.h < 0) r_list.h = 0;
meow_ui_activity_push(ACT_SEARCH); /* owner=ACT_SEARCH */
g_tb = ncm_topbar_create("搜索", 1, NULL);
if (!g_st.search_query[0]) ncm_strlcpy(g_st.search_query, "", sizeof g_st.search_query);
g_field = meow_textfield_create(field.x, field.y, field.w, field.h,
"输入歌曲 / 歌手", g_st.search_query,
sizeof g_st.search_query, MEOW_IME_ASCII);
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
sr_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_btn_id = meow_draw_custom_register(60, r_btn.x, r_btn.y, r_btn.w, r_btn.h,
sr_draw_btn, NULL);
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
sr_draw_empty, NULL);
g_footer_id = meow_draw_custom_register(1, r_cont.x, r_cont.y + r_cont.h - APP_FOOTER_H,
r_cont.w, APP_FOOTER_H, sr_draw_footer, NULL);
sr_drag_reset();
}
static void sr_page_enter(meow_view *v, void *ctx)
{
(void)v;
((search_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
static void sr_page_leave(meow_view *v, void *ctx)
{
(void)v;
((search_ctrl *)ctx)->active = 0;
}
static void sr_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_btn_id) { meow_draw_unregister(g_btn_id); g_btn_id = 0; }
if (g_empty_id){ meow_draw_unregister(g_empty_id); g_empty_id = 0; }
if (g_footer_id){meow_draw_unregister(g_footer_id);g_footer_id = 0; }
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_field){ meow_widget_free(g_field); g_field = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *search_view_create(void)
{
static const meow_view_hooks h = {
.on_create = sr_page_create,
.on_enter = sr_page_enter,
.on_leave = sr_page_leave,
.on_destroy = sr_page_destroy,
};
return meow_view_create(ACT_SEARCH, MEOW_VIEW_ACTIVITY, &h, &g_sc);
}
+26 -15
View File
@@ -34,37 +34,44 @@ static void settings_draw(void *ctx)
(void)ctx;
if (meow_ui_activity_active() != ACT_SETTINGS) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
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");
/* 行:音质 / 缓存上限 / 退出登录 / 关于(色 = 语义色;退出 = error)。 */
struct { const char *title; char sub[160]; int red; } rows[4];
rows[0].title = "音质"; rows[0].red = 0;
rows[1].title = "缓存上限"; rows[1].red = 0;
rows[2].title = "退出登录"; rows[2].red = 1;
rows[3].title = "关于网易云"; rows[3].red = 0;
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s", level_name(g_st.level));
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB", g_st.cache_limit_gb);
snprintf(rows[2].sub, sizeof rows[2].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", "v0.1 · MeowOS");
for (int i = 0; i < 3; i++) {
for (int i = 0; i < 4; 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;
ncm_fill_round(x, yy, w, g_row_h, 12, th->surfaceContainer);
uint32_t tcol = rows[i].red ? th->error : th->onSurface;
uint32_t scol = rows[i].red ? th->error : th->onSurfaceVariant;
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);
meow_ui_text_right(x + w - APP_PAD, yy + 18, ">", 28, rows[i].red ? th->error : th->onSurfaceVariant);
}
/* 分组说明(占位)+ 品牌红小字。 */
int ny = y + 4 * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "以下为占位,v1 不启用", 20, th->onSurfaceVariant);
meow_ui_text(x, ny + 24, "品牌红 © MeowOS · NCM", 20, pa->red);
}
static int row_at(int x, int y)
{
(void)x;
int r = r_cont.y + 16;
for (int i = 0; i < 3; i++) {
for (int i = 0; i < 4; i++) {
int yy = r + i * (g_row_h + 8);
if (y >= yy && y <= yy + g_row_h) return i;
}
@@ -128,6 +135,10 @@ int settings_on_tap(int x, int y)
}
return 1;
}
if (row == 3) { /* 关于(v1 占位) */
ncm_snack("关于页开发中");
return 1;
}
return 0;
}
+2 -1
View File
@@ -18,7 +18,8 @@ CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
core/ncm_cache.c core/ncm_ctx.c"
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/chrome.c app/logic/appstate.c app/logic/lyric.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c"
+3 -2
View File
@@ -19,7 +19,7 @@ INC="-I. -I../sdk -I../meow_launcherd -Icore \
echo "==> host SDK objects"
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
meow_ui_img meow_ui_config"
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc"
for f in $SDK_SRC; do
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
@@ -29,7 +29,8 @@ $CC_HOST -O2 -c -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" "$SDK_DIR/test_ho
echo "==> host app sources (exclude app.c: has main + g_st; test provides them)"
APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/chrome.c app/logic/appstate.c app/logic/lyric.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c"
for f in $APP_SRC; do
+63
View File
@@ -92,6 +92,14 @@ static long obj_int(const ncm_json_t *j, int tok, const char *key) {
return ncm_json_get_int_at(j, vt, &rc);
}
/* 布尔键取值(jsmn 无原生 bool;比较 "true"/"false" 字面量)。 */
static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
int vt = ncm_json_obj_get(j, tok, key);
char s[8];
if (!ncm_json_get_str_at(j, vt, s, sizeof s)) return 0;
return (strcmp(s, "true") == 0);
}
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n) {
if (!out_n) return NCM_ERR_ARG;
@@ -118,6 +126,8 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
obj_str(&c.j, e, "coverImgUrl", out[i].cover, sizeof out[i].cover);
out[i].track_count = (int)obj_int(&c.j, e, "trackCount");
/* subscribed=true = 收藏的(订阅他人);false = 自己创建(§7.0 分组数据依赖) */
out[i].is_collected = obj_bool(&c.j, e, "subscribed") ? 1 : 0;
}
*out_n = n;
api_free(&c);
@@ -269,3 +279,56 @@ int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
api_free(&c);
return NCM_OK;
}
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
ncm_song_t *out, int max, int *out_n) {
if (!out_n) return NCM_ERR_ARG;
*out_n = 0;
if (!keyword || !keyword[0] || !out || max <= 0) return NCM_ERR_ARG;
/* /api/cloudsearch/pc:s=关键词 type=1(单曲) offset/limit 翻页。v1 只取前 max 条。
* 关键词转义(\" 与 \\),防止 IME 输入引号/反斜杠拼出坏 JSON。 */
char esc[256], json[512];
size_t ei = 0;
for (size_t si = 0; keyword[si] && ei < sizeof esc - 3; si++) {
char ch = keyword[si];
if (ch == '"' || ch == '\\') esc[ei++] = '\\';
esc[ei++] = ch;
}
esc[ei] = 0;
snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":1,\"offset\":0,\"limit\":%d,\"total\":true}",
esc, max);
api_json_t c; memset(&c, 0, sizeof c);
int rc = api_post(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI,
NCM_RESP_MAX, &c);
if (rc != NCM_OK) return rc;
int arr = ncm_json_path(&c.j, "result.songs");
if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; }
int cnt = ncm_json_arr_len(&c.j, arr);
if (cnt > max) cnt = max;
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
for (int i = 0; i < cnt; i++) {
int e = ncm_json_arr_get(&c.j, arr, i);
if (e < 0) break;
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
out[i].duration_ms = obj_int(&c.j, e, "dt");
out[i].fee = (int)obj_int(&c.j, e, "fee");
/* id 用 i64:网易云歌曲 id 可 >2^31 */
int vt = ncm_json_obj_get(&c.j, e, "id");
int rc2;
int64_t idv = ncm_json_get_i64_at(&c.j, vt, &rc2);
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)idv);
int ar = ncm_json_obj_get(&c.j, e, "ar");
if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) {
int a0 = ncm_json_arr_get(&c.j, ar, 0);
if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name);
}
int al = ncm_json_obj_get(&c.j, e, "al");
if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name);
}
*out_n = cnt;
api_free(&c);
return NCM_OK;
}
+6
View File
@@ -30,6 +30,7 @@ typedef struct ncm_playlist {
char name[256];
char cover[512];
int track_count;
int is_collected; /* 1=我收藏的(订阅他人);0=我创建的。来自响应 subscribed(§7.0) */
} ncm_playlist_t;
typedef struct ncm_song {
@@ -77,6 +78,11 @@ int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char
int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz);
/* 搜索(weapi /api/cloudsearch/pc):keyword 搜索歌曲(type=1)。out 填 ncm_song_t。
* 仅返回前端可解析字段(name/ar[0]/al/dt/id)。风控:只在用户确认时调用。 */
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
ncm_song_t *out, int max, int *out_n);
#ifdef __cplusplus
}
#endif
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.