feat(ncm §22): 播放页重做——封面页/歌词页双页(NCM_CACHE_DESIGN §6 定案)

- 去标题栏(不再用 ncm_topbar);返回=左缘手势/系统返回钮语义
- 双页结构:封面页/歌词页,屏幕正中左/右滑切换(与左缘≤24px返回、顶部通知分流),
  页面指示点显示当前页;中部横滑=on_touch_move 锁存 page-drag + on_tap/on_swipe 判向
- 封面页:方形大图+圆角(无旋转动画)+进度条(点 seek)+MD3 矢量控制钮(prev/play/pause/next,
  新增 app/ui/ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)+元信息+实际音质档
- 歌词页:LRC 滚动(当前行居中高亮、上下渐隐)+点行跳转(新增 lyric_line_time);
  默认=封面模糊置底(封面铺底+主题 scrim),省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode)
- 日夜间:主题配色只映射到半透明叠加层/scrim/accent(红),不动沉浸底(NCM_PLAYER_BG 恒定)
- 分带刷新沿用:封面页=封面静态带+动态带(互不重叠 bbox,tick 只刷动态带);歌词页换行(~5s)才重画
- 性能:宿主 4 页 PPM 核验+手势自检 PASS;mipsel -Wall -Wextra 0 warning;qemu core 回归 PASS;
  0 真实请求(封面/歌词用本地样本注入)。未部署真机(主会话统一)

md5: ncm 730e0b6e / ncm_host 500ac85 / player.c 00934de / ncm_icons.h 20f0bba
This commit is contained in:
MeowOS committed 2026-09-01 18:52:14 +08:00
1 parent ed2ae10320
commit 7bd6962ccb
13 files changed
+795 -343

No files matched your search

+104
View File
@@ -1988,3 +1988,107 @@ a80ee35f243ac28379cd1adeb61548ed build_ncm_host.sh # +stream_src.c
首帧);实测 44.1k 向量 400kbps 首开声 3.82s(vs mp3 2.60s)即此因。
- UI 菊花/缓冲提示未接 g_play.stalled(字段已备,UI 批再做)。
- 断续网 underrun 聚合断言为时序容忍口径(§21.6),非判据放松——PCM 金标准不变。
---
## 22. 播放页重做:封面页/歌词页双页 + MD3 矢量图标 + 点行跳转 + 背景模式(2026-09-01,按 NCM_CACHE_DESIGN §6 定案)
> 范围:仅 `patch_v2/ncm_app/`。未动 `sdk/`(sb_icons.h 只作图标范式参照,未改)、`meow_launcherd/`、
> `core/`(缓存/解码/网络底座零改动)。风控纪律:本批真实 NCM 请求 **0 次**(封面/歌词/手势全走
> 宿主 mocK 与本地样本注入)。部署/真机验收留主会话统一。上一批(§21 边下边播)已 commit `9e25db7`,
> 本批在其上做。
### 22.1 设计定案(NCM_CACHE_DESIGN §6)与实现映射
| 定案 | 实现 |
|---|---|
| 去掉现有标题栏(播放页不再用 ncm_topbar) | 删除 `player.c` 的 `g_tb`/topbar 创建与 `MEOW_TOPBAR_BACK` 命中;返回 = 左缘手势(`SWIPE_LEFT`→`app_on_swipe`→`page_pop`)/ 系统返回钮语义,`app.c` 不改 |
| 双页结构:封面页/歌词页,屏幕正中左/右滑切换 | 新增内部页 `g_ppage`(0=封面、1=歌词);`player_set_page/player_page/player_page_go`;页面指示点(2 点,当前页红色) |
| 手势分流(左缘≤24px=返回、顶部=通知、中部=页切换) | 不抢 SDK 左缘(`PLR_EDGE_W=24` 与 SDK `EDGE_W` 一致)与顶部热区(`y>=statusbar_hotzone()`);中部横滑用 `on_touch_down/move` 锁存 `page-drag` + `on_tap`(中部左滑落 TAP)判向 → 切页;中部右滑由 SDK `on_swipe(RIGHT)` → 切页(左滑/右滑都覆盖) |
| 封面页:方形大图+圆角,无旋转动画;进度条(可点 seek)+控制钮+元信息+实际音质档 | `meow_ui_img_cover` 方形 + `round_corners_bg` 四角裁圆角;进度条点击 seek;控制钮 = MD3 图标;元信息(歌名/歌手)+ 实际生效档/降级标注 |
| 按钮全部用 MD3 矢量图标(32×32 alpha,参照 sb_icons.h 范式) | 新增 `app/ui/ncm_icons.h`(PREV/PLAY/PAUSE/NEXT 四枚 32×32 单通道 alpha,几何近似+8× 超采样抗锯齿,参照 sb_icons.h 存储范式);`draw_md3_icon` 最近邻缩放 + 逐像素 `fill_rect_alpha` |
| 歌词页:LRC 滚动(当前行居中高亮、上下渐隐)+ 点行跳转 | 沿用 `lyric.c`;新增 `lyric_line_time(i)`(点行 seek 用);当前行 `size=24` 红高亮、上下行 `size=20` 按距中心 `mix_c` 渐隐;点行 → `player_seek(lyric_line_time(i))` |
| 歌词页默认=封面模糊置底;省电=封面背景+黑色半透明叠加层(设置 `ncm.player_bg_mode`) | 默认:封面 `meow_ui_img_scaled` 铺满 + 主题 `th->scrim` α150;省电:铺满 + `0x000000` α195;设置页新增行「歌词页背景」sheet 二选一,持久化 `ncm.player_bg_mode` |
| 日夜间:主题配色只映射到半透明叠加层,不动沉浸底 | 新增 `NCM_PLAYER_BG` 固定沉浸底;`ncm_on_theme` 在 ACT_PLAYER 时 page_bg 恒 `NCM_PLAYER_BG`(不改底),其余页面仍跟随系统明暗;歌词 scrim/accent 随 `g_dark`/主题变化 |
| 缓冲「缓冲中…N%」/错误条(256 那套)沿用 | 封面页:缓冲文案替时间行、错误文案(VIP/无音源/登录过期/网络)+「点播放键重试」;`ncm_snack` 错误沿沿用 |
| 退出=后台播放(M4 background_audio,退出语义不动) | `app.json` 已声明 `background_audio:1`;播放线程/退出路径未动 |
### 22.2 性能(分带刷新纪律沿用)
- **封面页**拆「封面静态带 `g_cover_r`(z8)」+「动态带 `g_dyn_r`(z9)」两个绘制物,bbox 互不重叠:
tick 只标脏动态带(进度/控制/元信息/指示点);封面带仅 歌曲/封面 变化(`player_static_invalidate`)
才重画 → 不整页重绘。
- **歌词页**为整屏带状物:仅在 进入/切歌/封面变化/换背景模式/换行(~5s 一次,歌词按行进非逐帧)
时整屏重画;pos tick 不重画歌词页(`g_last_line` 沿驱动,行间静止)。
- 模糊底图 = 封面按屏最近邻铺底 + scrim;区域重画时 SDK 绘制 clip 收敛(`meow_ui_draw.c`,
`g_clip_stack`),只算相交区,不整屏 overdraw(§5.8 记忆点)。
- 控制钮由「逐帧行扫三角」改为 **MD3 图标位图 blit**(扫描 + 每像素 blend565),code 更简单、观感对齐 Material。
### 22.3 验证记录(0 真实 NCM 请求)
| 层 | 手段 | 结果 |
|---|---|---|
| mipsel 交叉 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 963042 / data 1608 / bss 1113988`(bss 增量仅来自 icons/ncm 段,均常量) |
| 宿主离屏 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | 自有代码 0 warning(仅 SDK `meow_ui_vol.c` 既有 `system()` 告警) |
| 四页 PPM 核验 | `NCM_PAGE=plcover/pllyric × NCM_THEME=light/dark` + `NCM_COVER=本地样本注入` | **4 页全 rc=0**:封面页=方形圆角封面+歌名/歌手+时间+红进度条(52%)+prev/暂停/next MD3 图标+指示点;歌词页=封面模糊置底+顶部「歌名·歌手」+当前行红高亮居中+上下渐隐+指示点;日/暗两套底恒 `NCM_PLAYER_BG`(不动沉浸底)、accent/scrim 随主题 |
| 缓冲/错误态 | `NCM_PAGE=playerbuf/playererr` | 全 rc=0:缓冲=「缓冲中… N%」文案+空进度条+暂停钮;错误=错误文案+「重试/切下一首」+播放钮(STOPPED→play 重试) |
| 省电背景 | `NCM_PAGE=pllyric NCM_BGMODE=power_save` | 全 rc=0:封面背景更暗(`0x000000` α195),与 default 明显区分 |
| 手势切换自检 | `NCM_PAGE=gestest`(宿主直调 touch_down/move/on_tap/on_swipe) | **PASS**:p0=0 → 中部左滑=1(歌词) → 中部右滑=0(封面);进度条上按下拖拽不锁存(不被误判翻页) |
| qemu core 回归 | `timeout 120 qemu-mipsel ./out/test_ncm_core_mipsel` | **RESULT: PASS**(core 全量,本批未动 core) |
| 宿主 core | `test/test_ncm_core_host` | **PASS** |
| 设置页 | `NCM_PAGE=settings` | rc=0,6 行(新增「歌词页背景」行存在) |
### 22.4 md5(本批产物 + 改动文件)
```
730e0b6eaa0b3f626f828bbaa51dc5a8 ncm # mipsel 静态 App(新)
500ac859618a58bc2a641553a383a10f ncm_host # 宿主离屏调试二进制(新)
00934dec4452f2f31e6f473c64bd311f app/ui/player.c
20f0bbac74099d07638d4f92a340eb82 app/ui/ncm_icons.h # 新:MD3 prev/play/pause/next 32x32 alpha
aedd5ff24e227eeda94341e63eac4064 app/ui/settings.c
31ae666b0f7497bb3c3391da9ce4636a app/ui/ncm_colors.c
b1c8f029886a60288e96bcf54476d7bf app/logic/lyric.c
63127b670604ff67dd176224088b16c4 app/logic/cover.c
aa5a5f22e10662c2247ece166c13ea6e app/logic/appstate.c
e2a7062b0033234649fe03767a4c009d app/app.c
fa00ea4f0093cd7e0a2600887106de07 app/ncm_app_internal.h
2c484e5e920c0b05c6da107d15fd8a6f app/test/test_ncm_app.c
```
### 22.5 出入清单(只改 `patch_v2/ncm_app/`)
```
修改:
app/ui/player.c # 播放页重做:去 topbar + 封面/歌词双页(中滑切换) + MD3 图标控制钮 +
# 方盖圆角 + 点行 seek + 封面模糊/省电背景 + 分带刷新 + 手势分流
app/ui/ncm_icons.h # 新增:MD3 prev/play/pause/next 32x32 单通道 alpha 位图(参照 sb_icons.h)
app/ui/settings.c # 新增「歌词页背景」行(default/省电 sheet 二选一)
app/ui/ncm_colors.c # ncm_on_theme:ACT_PLAYER 沉浸底恒 NCM_PLAYER_BG(不动沉浸底)
app/logic/lyric.c # 新增 lyric_line_time(i):点行跳转
app/logic/cover.c # 宿主测试桩 cover_host_inject(#ifdef MEOW_UI_HOST_TEST,产品不编)
app/logic/appstate.c # 设置持久化 ncm.player_bg_mode(load/save + 越界回落 default)
app/app.c # on_touch_down/move 播发页路由(中部横滑页切换跟踪)
app/ncm_app_internal.h # NCM_PLAYER_BG/NCM_BG_* + player_bg_mode 字段 + player_set_page/
# player_page/player_touch_* + lyric_line_time 声明
app/test/test_ncm_app.c # 宿主:plcover/pllyric/gestest 模式 + NCM_BGMODE/NCM_COVER 注入
ncm / ncm_host # 重编产物
(未动 sdk/、meow_launcherd/、vendor/、core/;REPORT.md 本批记录 §22)
```
### 22.6 诚实出入 / 已知限制
- **真机未部署未验**(留主会话统一)。真机验收点:①双页中滑流畅(跟手感/指示点)②点行 seek
命中时间 ③封面模糊置底观感(默认)vs 省电黑色叠加(对比明显)④设置页「歌词页背景」持久化、
重启生效 ⑤返回=左缘右滑仍正确(不误触页切换)⑥播放中退出=后台播放(M4,未动)。
- **歌词页「当前行居中」为行进式**(按 LRC 行切换,非逐帧平滑滚动):这是 MIPS 软帧下
「流畅性廉价可靠」取舍(§11);当前行高亮 + 上下行渐隐已提供视觉焦点。
- **封面模糊 = 封面按屏最近邻铺底 + scrim(廉价近似)**:无 GPU 模糊/降采样原语,非真高斯模糊;
非均匀铺底(480×720)会轻微拉伸封面(居中裁方可解但需另建 thumbnail 缓冲,本批不做)。
- **歌词页顶部元信息/整屏重在换行时重画**:换行频率 ≈ 每行(数秒)一次,非逐帧;pos tick 不刷
歌词页(只在其换行时刷),故 MIPS 上歌词页代价可忽略。
- **页面对比色固定浅色系**(播放页为沉浸深色介质,文本若随主题 onSurface 在浅色主题下为近黑会
隐形):这是「不动沉浸底」定案的必然——播放页文本/控件用固定浅色,仅 accent(红)/scrim 随日夜间。
- **`player_set_screen` 保留为兼容占位(恒竖屏)**:重做后播放页为竖屏双页(旧「横屏布局」随顶栏
⟳ 钮一并退役);`player_screen()` 恒 0,宿主 `grid`/`lands` 旧模式不再有横屏布局。
- **歌词读锁残余(既有)**:`lyric_line_text/tl` 解锁后返回静态指针可能被下次 `lyric_parse` 覆盖
(§14.7 已知);本批未改,点行跳转读 `lyric_line_time` 同理(写入仅在切歌瞬间,窗口极小)。
+4 -2
View File
@@ -62,7 +62,8 @@ static void app_on_swipe(int dir, int dx, int dy)
static void app_on_touch_down(int x, int y)
{
if (home_is_active()) home_touch_down(x, y);
if (player_is_active()) player_touch_down(x, y); /* §22 中部横滑页切换跟踪 */
else 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);
@@ -70,7 +71,8 @@ static void app_on_touch_down(int x, int y)
static void app_on_touch_move(int x, int y)
{
if (home_is_active()) home_touch_move(x, y);
if (player_is_active()) player_touch_move(x, y);
else 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);
+7
View File
@@ -52,6 +52,11 @@ void appstate_load_settings(void)
if (g_st.level < NCM_LEVEL_STANDARD || g_st.level > NCM_LEVEL_UI_MAX)
g_st.level = NCM_LEVEL_EXHIGH;
/* §22 歌词页背景模式:default(模糊置底) / power_save(封面+黑叠加层);越界回落 default */
g_st.player_bg_mode = meow_config_int("ncm.player_bg_mode", NCM_BG_DEFAULT);
if (g_st.player_bg_mode != NCM_BG_DEFAULT && g_st.player_bg_mode != NCM_BG_POWER_SAVE)
g_st.player_bg_mode = NCM_BG_DEFAULT;
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
}
@@ -63,6 +68,8 @@ void appstate_save_settings(void)
meow_config_set("ncm.cache_limit_gb", b);
snprintf(b, sizeof b, "%d", g_st.level);
meow_config_set("ncm.level", b);
snprintf(b, sizeof b, "%d", g_st.player_bg_mode);
meow_config_set("ncm.player_bg_mode", b);
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
/* 手动调低上限时立即按 LRU 淘汰(超上限删最旧);未超限则为无操作 */
+18
View File
@@ -230,3 +230,21 @@ void cover_cache_reset(void)
g_gen++;
g_acked = g_gen;
}
#ifdef MEOW_UI_HOST_TEST
/* 宿主测试桩(仅宿主构建;产品构建无此符号):注入一张本地封面图(不经网络),
* 验证封面页方形圆角大图与歌词页模糊置底路径(§22)。0 真实请求纪律不变。 */
void cover_host_inject(const char *path)
{
if (!path || !path[0]) return;
meow_img *img = meow_img_load_file(path);
if (!img) { meow_log(MEOW_LOG_WARN, "cover host inject: load failed %s", path); return; }
pthread_mutex_lock(&g_cm);
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, path, sizeof g_cover_url);
g_cover_path[0] = 0;
g_gen++;
pthread_mutex_unlock(&g_cm);
}
#endif
+9
View File
@@ -174,6 +174,15 @@ const char *lyric_line_tl(int i)
return p;
}
/* 第 i 行时间戳(ms)。越界返回 -1。供播放页「点行跳转」(§22:seek 到该行时间)。 */
long lyric_line_time(int i)
{
pthread_mutex_lock(&g_lyric_mu);
long t = (i < 0 || i >= s_n) ? -1 : s_t[i];
pthread_mutex_unlock(&g_lyric_mu);
return t;
}
int lyric_count(void)
{
pthread_mutex_lock(&g_lyric_mu);
+20 -2
View File
@@ -39,6 +39,15 @@
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
#define APP_FOOTER_H 26
/* ---- 播放页(§22 重做:封面页/歌词页双页,NCM_CACHE_DESIGN §6)----
* 沉浸底恒 NCM_PLAYER_BG(不随系统明暗反转——「不动沉浸底」);日夜间主题配色只映射到
* 半透明叠加层(歌词页 scrim)与 accent(红)。 */
#define NCM_PLAYER_BG 0x101014 /* 播放页固定沉浸底(中性近黑) */
/* 歌词页背景模式(设置 ncm.player_bg_mode:default/power_save,§22) */
#define NCM_BG_DEFAULT 0 /* default:封面模糊置底(封面缩放铺底 + 主题 scrim 叠加) */
#define NCM_BG_POWER_SAVE 1 /* power_save:封面背景 + 黑色半透明叠加层(更暗,省电观感) */
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
/* 滑动窗口(REPORT §17,三期第三批,取代 §14 的 NCM_SONG_MAX=1000 硬截断):
@@ -158,6 +167,7 @@ typedef struct ncm_app_state {
/* 设置(持久化 MeowData/dev.player.ncm/data)*/
int level; /* NCM_LEVEL_* */
int cache_limit_gb;
int player_bg_mode; /* 歌词页背景模式(§22):NCM_BG_DEFAULT / NCM_BG_POWER_SAVE */
/* 搜索(结果复用 g_st.songs[] 浅拷贝区;仅搜索页上下文有效)*/
char search_query[256];
@@ -248,8 +258,12 @@ 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=竖屏;返回之前 */
void player_on_swipe(int dir, int dist); /* 中部横滑页切换(DIR=RIGHT→封面页;PPAGE 由 touch 拖拽+on_tap 判向) */
int player_set_page(int page); /* 0=封面页 1=歌词页;返回旧页。拖动/程序切页用 */
int player_page(void); /* 当前页(0/1) */
void player_touch_down(int x, int y); /* 中部横滑跟踪(与左缘返回/顶部通知分流,§22) */
void player_touch_move(int x, int y);
int player_set_screen(int landscape); /* 兼容:重做后恒竖屏(返回旧值 0) */
int player_screen(void);
/* settings */
@@ -319,6 +333,7 @@ const char *lyric_line_tl(int i);
int lyric_count(void);
void lyric_clear(void);
int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能 0 行=无词) */
long lyric_line_time(int i);/* 第 i 行时间戳(ms;越界 -1)。播放页点行跳转(§22) */
/* cover */
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
@@ -328,6 +343,9 @@ 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);
#ifdef MEOW_UI_HOST_TEST
void cover_host_inject(const char *path); /* 宿主测试桩:本地封面注入(验证封面/模糊底图) */
#endif
/* ---- play(app/play/)---- */
/* audio_out(后端:默认 SINK_AUDIOD=meow_audiod 独占服务;env NCM_AUDIO_SINK=file/null/alsa
+37 -3
View File
@@ -89,7 +89,10 @@ static int page_id(const char *name)
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, "plcover") == 0) return ACT_PLAYER; /* §22 封面页(双页重做) */
if (strcmp(name, "pllyric") == 0) return ACT_PLAYER; /* §22 歌词页(双页重做) */
if (strcmp(name, "gestest") == 0) return ACT_PLAYER; /* §22 手势切换自检 */
if (strcmp(name, "grid") == 0) return ACT_PLAYER; /* 保留:旧横屏(重做后恒竖屏) */
if (strcmp(name, "playerbuf") == 0) return ACT_PLAYER; /* §18 缓冲态 */
if (strcmp(name, "playererr") == 0) return ACT_PLAYER; /* §18 错误态 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
@@ -138,7 +141,9 @@ int main(int argc, char **argv)
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) {
else if (strcmp(name, "player") == 0 || strcmp(name, "plcover") == 0 ||
strcmp(name, "pllyric") == 0 || strcmp(name, "gestest") == 0 ||
strcmp(name, "grid") == 0) {
mock_playlists(); mock_songs(); mock_lyric();
pthread_mutex_lock(&g_play.mu);
g_play.state = NCM_PC_PLAYING;
@@ -207,10 +212,39 @@ int main(int argc, char **argv)
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);
if (strcmp(name, "grid") == 0) player_set_screen(1); /* 兼容占位(重做后恒竖屏) */
int pid = (strcmp(name, "loginlive") == 0) ? ACT_LOGIN : page_id(name);
meow_view_page_push(pid);
/* §22:歌词页模式注入 + 本地封面注入(0 真实请求) */
if (strcmp(name, "pllyric") == 0) player_set_page(1);
const char *bg = getenv("NCM_BGMODE");
if (bg && strcmp(bg, "power_save") == 0) g_st.player_bg_mode = NCM_BG_POWER_SAVE;
const char *cover = getenv("NCM_COVER");
if (cover && cover[0]) cover_host_inject(cover);
/* §22 手势切换自检(宿主直调,不依赖 SDK 触摸管线):封面页中部左滑→歌词页、右滑→封面页。
* 起点取封面区(避开进度条/控制钮),验证 page-drag 锁存与判向。 */
if (strcmp(name, "gestest") == 0) {
int p0 = player_page();
player_touch_down(240, 200); /* 中部,封面区 */
player_touch_move(140, 200); /* 左移 100px → 锁存 */
player_on_tap(140, 200); /* 抬手落 tap → 判为页切换 */
int p1 = player_page();
player_touch_down(240, 200);
player_touch_move(340, 200); /* 右移 100px */
player_on_swipe(MEOW_SWIPE_RIGHT, 100); /* SDK 中部右滑 → SWIPE_RIGHT */
int p2 = player_page();
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,504)=进度条行 */
player_touch_down(240, 504);
player_touch_move(140, 504);
player_on_tap(140, 504); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
int latched_change = player_page();
int ok = (p0 == 0 && p1 == 1 && p2 == 0 && latched_change == 0);
printf("gesture: p0=%d p1(左滑)=%d p2(右滑)=%d 省电锁存=%d -> %s\n",
p0, p1, p2, latched_change, ok ? "PASS" : "FAIL");
if (!ok) return 1;
}
/* player 页:其 on_tick 通常驱动进度;这里直接再画一次确保状态就绪 */
meow_ui_paint_full();
if (page_id(name) == ACT_PLAYER) {
+7 -1
View File
@@ -47,7 +47,13 @@ void ncm_on_theme(meow_theme_mode m)
if (d == g_dark) return; /* 无变化,忽略 */
g_dark = d;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg); /* 背景随系统明暗 */
/* §22:播放页沉浸底恒 NCM_PLAYER_BG(日夜间「不动沉浸底」——主题配色只映射到
* 半透明叠加层(歌词页 scrim)与 accent(红),由播放页绘制读取 g_dark 变化体现;
* 其余页面背景仍随系统明暗)。 */
if (meow_ui_activity_active() == ACT_PLAYER)
meow_ui_set_page_bg(NCM_PLAYER_BG);
else
meow_ui_set_page_bg(th->bg);
meow_ui_paint_full();
}
+73
View File
@@ -0,0 +1,73 @@
/* ncm_icons.h — 网易云播放页 MD3 矢量控制图标(Material Symbols Rounded 风格)。
* 单通道 alpha 位图 32x32(每像素 0..255 亮度/alpha),范式参照 sdk/sb_icons.h。
* ★ 日/夜间预留:位图只存亮度;绘制颜色由调用方(ncm_palette/meow_theme)提供。
* 生成:/tmp/gen_md3_icons.py(几何近似 + 8x 超采样抗锯齿;未提交生成器,仅提交位图头)。
*/
#ifndef NCM_ICONS_H
#define NCM_ICONS_H
#define NCM_ICON_SIZE 32
typedef struct { const unsigned char *data; } ncm_icon_t;
static const unsigned char NCM_ICON_PREV[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,0,0,0,0,8,148,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,0,0,0,48,215,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,
0,4,116,251,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,28,191,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,88,239,255,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,16,163,255,255,255,255,255,
255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,32,191,255,255,255,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,4,120,251,255,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,48,215,255,255,255,255,32,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,0,12,148,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,0,0,0,76,235,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,159,0,0,0,0,0,0,0,20,179,32,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,32,20,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char NCM_ICON_PLAY[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
179,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,243,108,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,199,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,
251,132,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,215,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,155,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,
255,231,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,247,116,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,187,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,235,96,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,163,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,223,72,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,251,140,12,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,207,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char NCM_ICON_PAUSE[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,
255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,
0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,
255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,32,0,0,0,255,255,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,32,32,32,4,0,0,0,32,32,32,32,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char NCM_ICON_NEXT[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,171,16,0,0,0,0,0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,227,68,0,0,0,0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,140,8,0,
0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,207,44,0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,247,112,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,183,28,0,
255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,207,48,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,144,8,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,227,68,0,0,0,0,255,255,32,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,171,20,0,0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,243,100,0,0,0,0,0,0,0,255,255,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,199,32,0,0,0,0,0,0,0,0,255,255,32,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,32,32,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const ncm_icon_t ncm_icons[] = {
{ NCM_ICON_PREV }, { NCM_ICON_PLAY }, { NCM_ICON_PAUSE }, { NCM_ICON_NEXT },
};
/* 枚举索引(与 ncm_icons[] 对应) */
enum { NCM_I_PREV=0, NCM_I_PLAY=1, NCM_I_PAUSE=2, NCM_I_NEXT=3 };
#endif /* NCM_ICONS_H */
+485 -321
View File
@@ -1,44 +1,72 @@
/* ui/player.c — 播放页(ACT_PLAYER):黑胶唱盘 + 唱针 + 进度/控制/滚动歌词(竖/横布局可切)。
/* ui/player.c — 播放页(ACT_PLAYER)重做:封面页/歌词页双页(屏幕正中左/右滑切换)。
*
* 黑胶(NCM_UI_DESIGN §5.1,软渲无圆形原语,全 fill_rect 行扫):
* ① 盘体 = 对每行 dy 取 half=√(R²-dy²) 画 fill_rect(同心圆)。
* ② 封面粉圆 = 方盖(S=R×√2)内切于圆 + 四角把「圆外-方外」两段盖回盘色 = 封面成圆。
* ③ 唱针 = 从盘心右上到盘缘一根 3px 唱臂 + 端头;播放压盘/暂停微抬(状态切换才画)。
* 盘恒黑(palette.disc),accent(进度已播/播放钮/歌词高亮)日夜换红明暗(palette.red/red_lit)。
* 设计(NCM_CACHE_DESIGN §6 定案 2026-09-01):
* - 去掉标题栏(不再用 ncm_topbar);返回仍走左缘手势(SWIPE_LEFT → app_on_swipe → page_pop)
* /系统返回钮语义。
* - 双页结构:封面页 / 歌词页。屏幕正中左/右滑切换。手势分流:左缘 ≤24px 归系统返回
* (SDK SWIPE_LEFT),顶部热区归通知中心,中部横滑归页切换(页面指示点显示当前页)。
* - 封面页:方形大图+圆角(cover.c LRU,无旋转动画)+ 进度条(可点 seek)+ MD3 矢量控制钮
* (prev/play-pause/next,ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)
* + 元信息 + 实际音质档 + 页面指示点;缓冲/错误态沿用(「缓冲中…N%」/中文错误条+重试)。
* - 歌词页:LRC 滚动(当前行高亮居中,上下行渐隐)+ 点行跳转;默认=封面模糊置底,
* 省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode: default/power_save)。
* - 日夜间切换:主题配色只映射到半透明叠加层(歌词页 scrim)与 accent(红),
* 不动沉浸底(恒 NCM_PLAYER_BG)。
*
* 性能(§6,专项):**分带刷新**——内容拆 盘带(仅圆盘) + 动态带(歌名/进度/控制/歌词/页脚) 两个绘制物,
* 各带独立且互不重叠 bbox。tick 只标脏「变化的那一条带」,盘带仅 歌曲/封面/播停态 变化才重画
* (修 player_tick 每 tick 刷整 r_cont 的隐患)。播放页恒沉浸(day 瓷白 / night 近黑,
* 不随系统明暗整体反转,仅 accent 换红明暗,§5.4)。
* 性能(§6 专项,分带刷新纪律沿用):
* - 封面页拆「封面静态带 g_cover_r」+「动态带 g_dyn_r」(互不重叠 bbox):tick 只标脏
* 动态带(进度/控制/元信息/指示点),封面带仅 歌曲/封面 变化才重画(不整页重绘)。
* - 歌词页为「整屏带状物」(bg 置底 + 歌词叠加):仅在 进入/切歌/封面变化/换背景模式/
* 换行(~5s 一次)时整屏重画;pos tick 不重画歌词页(歌词按行进,非逐帧滚动)。
* - 模糊底图 = 封面按屏幕铺底(meow_ui_img_scaled 最近邻)+ scrim 叠加;区域重画时绘制
* clip 收敛(meow_ui_draw.c),只算相交区,避免 overdraw。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include "ncm_icons.h"
/* 播放页固定视觉色(沉浸底恒 NCM_PLAYER_BG;文本/控件用固定浅色 —— 日夜间仅 accent 红
* 与歌词 scrim 随 g_dark/主题变化,不动底。参照 §8.1「语义色」但播放页为沉浸深色介质,
* 文本若跟主题 onSurface(浅色主题为近黑)会隐形,故播放页用固定浅前景。 */
#define PLR_TEXT 0xEDEDED /* 主文本 */
#define PLR_TEXT_DIM 0xA6A6AC /* 次文本/时间 */
#define PLR_CTRL_BG 0x2A2A30 /* prev/next 圆形钮底 */
#define PLR_CTRL_FG 0xE8E8E8
#define PLR_TRACK_BG 0x33333A /* 进度条未播轨 */
#define PLR_EDGE_W 24 /* 左缘返回热区(与 SDK EDGE_W 一致,防抢返回) */
#define PLR_SWIPE_MS 65 /* 中部横滑判页阈值(与 SDK SWIPE_THRESH 一致) */
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_disc, g_custom_dyn;
static int g_landscape;
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
static int g_last_dl, g_last_err; /* 缓冲进度粒度值 / 错误沿(§18) */
static int g_last_eff; /* 生效档沿(§20:降级标注出现/消失刷动态带) */
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
static int g_ppage; /* 0=封面页 1=歌词页 */
static int g_pstatic, g_pdyn; /* 自定义绘制物 id(按当前页注册) */
static int g_W, g_H; /* 屏尺寸(沉浸:0,0,W,H) */
static int g_last_pos_ms, g_last_state, g_last_song, g_last_line, g_last_eff;
static int g_last_dl, g_last_err;
static char g_last_meta_id[24];
/* 运行期几何(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; /* 歌词区 */
/* 中部横滑跟踪(on_touch_down/move 记录;resolved in on_tap/on_swipe) */
static int g_touch_on;
static int g_touch_sx, g_touch_sy;
static int g_drag_latched, g_drag_dx;
#define CTL_RR 30 /* 侧钮命中半径(命中容差) */
/* ---- 封面页几何 ---- */
static ncm_rect_t g_cover_r; /* 封面页静态带(方形大图) */
static ncm_rect_t g_dyn_r; /* 封面页动态带(元信息/进度/控制/指示点) */
static int g_title_y, g_artist_y, g_qual_y, g_times_y;
static ncm_rect_t g_bar;
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
/* ---- 歌词页几何 ---- */
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
static int g_lyr_lh; /* 行高 */
static int g_lyr_meta_y; /* 顶部元信息 y */
#define CTL_RR (g_r_side + 8) /* 侧钮命中容差(圆外一点仍可点) */
static int cur_song(void)
{
@@ -62,317 +90,455 @@ static void fmt_ms(int64_t ms, char *out, size_t sz)
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
}
static void tri(int x0, int y0, int h, int w, int dir, uint32_t color)
/* 颜色插值(t=0..255 混向 b)。播放页歌词渐隐/叠加用(无 GPU,逐通道整数混合)。 */
static uint32_t mix_c(uint32_t a, uint32_t b, int t)
{
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);
if (t < 0) t = 0; if (t > 255) t = 255;
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
int r = ar + ((br - ar) * t + 128) / 255;
int g = ag + ((bg - ag) * t + 128) / 255;
int bl = ab + ((bb - ab) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
}
/* MD3 图标 blit:把 ncm_icons.h 的 32x32 单通道 alpha 位图最近邻放大到 size×size,居中于
* (cx,cy),逐像素以 color/alpha 混到 back_buf(fill_rect_alpha blend565)。 */
static void draw_md3_icon(int idx, int cx, int cy, int size, uint32_t color)
{
const unsigned char *src = ncm_icons[idx].data;
if (size <= 0) return;
int x0 = cx - size / 2, y0 = cy - size / 2;
for (int y = 0; y < size; y++) {
int sy = (y * NCM_ICON_SIZE) / size;
if (sy >= NCM_ICON_SIZE) sy = NCM_ICON_SIZE - 1;
for (int x = 0; x < size; x++) {
int sx = (x * NCM_ICON_SIZE) / size;
if (sx >= NCM_ICON_SIZE) sx = NCM_ICON_SIZE - 1;
int a = src[sy * NCM_ICON_SIZE + sx];
if (a > 4) meow_ui_fill_rect_alpha(x0 + x, y0 + y, 1, 1, color, a);
}
}
}
static void draw_ctl(int cx, int cy, int r, int kind, uint32_t base, uint32_t fg)
/* 控制钮:圆形底 + MD3 图标(无逐帧三角行扫)。icon 尺寸按底半径缩放(~4r/3 使 24 内容入圈)。 */
static void draw_ctrl_btn(int icon, int cx, int cy, int r, uint32_t circle, uint32_t fg)
{
for (int i = 0; i < 2 * r; i++) {
int dy = i - r;
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 - r + i, 2 * half, 1, base);
meow_ui_fill_rect(cx - half, cy + dy, 2 * half, 1, circle);
}
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); }
draw_md3_icon(icon, cx, cy, (r * 4) / 3, fg);
}
static void arm_line(int x0, int y0, int x1, int y1, uint32_t color, int wpx)
/* 圆角封面四角裁掉:把方形图四角(圆角外-方形内)盖回背景色(廉价圆角,无 GPU 裁剪)。 */
static void round_corners_bg(int x, int y, int w, int h, int r, uint32_t bg)
{
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);
if (r > w / 2) r = w / 2;
if (r > h / 2) r = h / 2;
for (int dy = 0; dy < r; dy++) {
int inset = r - (int)sqrt((double)(r * r - (r - dy) * (r - dy)));
if (inset < 0) inset = 0;
if (inset > w / 2) inset = w / 2;
meow_ui_fill_rect(x, y + dy, inset, 1, bg);
meow_ui_fill_rect(x + w - inset, y + dy, inset, 1, bg);
meow_ui_fill_rect(x, y + h - 1 - dy, inset, 1, bg);
meow_ui_fill_rect(x + w - inset, y + h - 1 - dy, inset, 1, bg);
}
}
/* 几何(竖/横布局)。带 bbox:盘带=圆盘矩形,动态带=其余(互不重叠)。 */
static void draw_page_dots(int y)
{
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
for (int i = 0; i < 2; i++) {
int cx = g_W / 2 - 13 + i * 26;
uint32_t c = (i == g_ppage) ? pa->red : th->outlineVariant;
for (int dy = -4; dy <= 4; dy++) {
int half = (int)sqrt((double)(16 - dy * dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, y + dy, 2 * half, 1, c);
}
}
}
/* 几何(沉浸:0,0,W,H)。封面页/歌词页各自布局。 */
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)); /* 其余(含歌名/进度/歌词) */
}
g_W = meow_ui_width();
g_H = meow_ui_height();
int W = g_W, H = g_H;
int cs = 304; /* 封面边长(方形) */
if (cs > W - 2 * APP_PAD) cs = W - 2 * APP_PAD;
g_cover_r = NR((W - cs) / 2, 64, cs, cs); /* 64..368 */
g_title_y = g_cover_r.y + g_cover_r.h + 32; /* 400 */
g_artist_y = g_title_y + 32; /* 432 */
g_qual_y = g_artist_y + 26; /* 458 */
g_times_y = g_qual_y + 26; /* 484 */
g_bar = NR(48, g_times_y + 20, W - 96, 4); /* 504 (h=4) */
g_ctl_y = g_bar.y + 64; /* 568 */
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
g_r_side = 24; g_r_play = 34;
g_dots_y = g_ctl_y + 62; /* 630 */
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
g_lyr_cx = H / 2; /* 360 */
g_lyr_lh = 30;
g_lyr_meta_y = 22;
}
/* 盘带绘制物:盘体 + 方盖内切圆(四角盖色) + 封面 + 唱针。仅 歌曲/封面/播停态 见变化才重画。 */
static void plr_draw_disc(void *ctx)
/* ============================================================ */
/* 绘制物 */
/* ============================================================ */
/* 封面页静态带:方形大图 + 圆角 + 兜底占位。仅 歌曲/封面 变化才重画。 */
static void plr_draw_cover_static(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER) return;
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 0) return;
const meow_theme *th = meow_theme_get();
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);
}
}
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。(cover_get 持锁,绘完 cover_put,§18) */
const ncm_pal_t *pa = ncm_palette(g_dark);
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);
if (cov) {
meow_ui_img_cover(g_cover_r.x, g_cover_r.y, g_cover_r.w, cov);
round_corners_bg(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h, 16, NCM_PLAYER_BG);
} else {
ncm_fill_round(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h, 16, pa->red_ctr);
meow_ui_text_center(g_cover_r.x + g_cover_r.w / 2,
g_cover_r.y + g_cover_r.h / 2 - 12, "\u266a", th->font_body, pa->red);
}
cover_put();
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)
/* 封面页动态带:元信息 + 实际音质档 + 进度条/时间 + 控制钮 + 指示点 + 缓冲/错误态。
* tick 主要刷此带(进度/控制/元数据变化)。 */
static void plr_draw_cover_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);
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 0) return;
const ncm_pal_t *pa = ncm_palette(g_dark);
int W = g_W;
ncm_song_t sm;
int have = (cur_song_meta(&sm) == 0);
const char *nm = have ? sm.name : "暂无播放";
const char *ar = have ? sm.art_name : "";
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);
}
/* 进度条 + 时间 */
int pos = (int)player_manager_pos_ms();
int dur = (int)player_manager_dur_ms();
if (dur <= 0 && have) dur = (int)sm.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);
if (g_landscape) {
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 {
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);
}
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);
/* §20:实际生效音质档(与用户档不同时小字标注「已降级至 xx」;缓冲期未定不显示)。
* 位置:时间行中部(左右是时间戳,中央留白),竖/横屏同一条带。 */
{
int eff = player_manager_eff_level();
if (have && eff >= 0 && player_manager_state() != NCM_PC_STOPPED) {
char qb[72];
if (eff != g_st.level)
snprintf(qb, sizeof qb, "已降级至 %s", ncm_level_name(eff));
else
snprintf(qb, sizeof qb, "%s", ncm_level_name(eff));
int qcx = g_landscape ? (g_bar.x + g_bar.w / 2) : (r_cont.x + r_cont.w / 2);
uint32_t qc = (eff != g_st.level) ? pa->red : th->onSurfaceVariant;
meow_ui_text_center(qcx, g_times_y, qb, 18, qc);
}
}
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 || st == NCM_PC_STOPPED) ? 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 = (g_landscape) ? 24 : 26;
int center_y = g_lyr.y + g_lyr.h / 2;
int lcx = g_lyr.x + g_lyr.w / 2;
int perr = player_manager_error();
/* 元信息(歌名/歌手) */
meow_ui_text_center(W / 2, g_title_y, ncm_text_fit(nm, W - 32, 28), 28, PLR_TEXT);
meow_ui_text_center(W / 2, g_artist_y, ncm_text_fit(ar, W - 32, 20), 20, PLR_TEXT_DIM);
/* §20 实际生效音质档(与用户档不同→红字「已降级至 xx」);缓冲/错误态不显示 */
int eff = player_manager_eff_level();
if (have && eff >= 0 && st != NCM_PC_STOPPED) {
char qb[72];
if (eff != g_st.level)
snprintf(qb, sizeof qb, "已降级至 %s", ncm_level_name(eff));
else
snprintf(qb, sizeof qb, "%s", ncm_level_name(eff));
uint32_t qc = (eff != g_st.level) ? pa->red_lit : PLR_TEXT_DIM;
meow_ui_text_center(W / 2, g_qual_y, qb, 18, qc);
}
/* 缓冲/错误态(沿用 §18 三态;错误时用文案替代时间行) */
if (perr != 0) {
/* §18:播放错误明示(旧版 err 无 UI 消费者 → 失败无声死等) */
const char *em = (perr == NCM_ERR_VIP) ? "VIP 专享,开通会员后可播放"
: (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
: "加载失败,请检查网络";
meow_ui_text_center(lcx, center_y - 20, em, 20, pa->red);
meow_ui_text_center(lcx, center_y + 10, "点播放键重试 / 切下一首", 20, th->onSurfaceVariant);
}
else if (st == NCM_PC_BUFFER) {
/* §18:缓冲反馈(song_url 获取 + 下载全程不再无提示) */
int64_t dd = 0, dt = 0;
player_manager_dl(&dd, &dt);
char bbuf[48];
if (dt > 0) {
int pct = (int)(dd * 100 / dt);
if (pct > 100) pct = 100;
snprintf(bbuf, sizeof bbuf, "缓冲中… %d%%", pct);
} else if (dd > 0) {
snprintf(bbuf, sizeof bbuf, "缓冲中… %lldKB", (long long)(dd / 1024));
meow_ui_text_center(W / 2, g_times_y - 8, em, 20, pa->red_lit);
meow_ui_text_center(W / 2, g_times_y + 18, "点播放键重试 / 切下一首", 18, PLR_TEXT_DIM);
} else {
/* 进度条 + 时间 */
int pos = (int)player_manager_pos_ms();
int dur = (int)player_manager_dur_ms();
if (dur <= 0 && have) dur = (int)sm.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);
if (st == NCM_PC_BUFFER) {
int64_t dd = 0, dt = 0;
player_manager_dl(&dd, &dt);
char bbuf[48];
if (dt > 0) {
int pct = (int)(dd * 100 / dt);
if (pct > 100) pct = 100;
snprintf(bbuf, sizeof bbuf, "缓冲中… %d%%", pct);
} else if (dd > 0) {
snprintf(bbuf, sizeof bbuf, "缓冲中… %lldKB", (long long)(dd / 1024));
} else {
snprintf(bbuf, sizeof bbuf, "缓冲中…");
}
meow_ui_text_center(W / 2, g_times_y - 8, bbuf, 20, PLR_TEXT_DIM);
} else {
snprintf(bbuf, sizeof bbuf, "缓冲中…");
}
meow_ui_text_center(lcx, center_y - 10, bbuf, 20, th->onSurfaceVariant);
}
else if (n == 0) {
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
meow_ui_text_center(lcx, center_y - 10,
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…",
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, 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, ncm_text_fit(tl, lw, 20), 20, th->secondary);
meow_ui_text(48, g_times_y, lp, 20, PLR_TEXT_DIM);
meow_ui_text_right(W - 48, g_times_y, rp, 20, PLR_TEXT_DIM);
}
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, PLR_TRACK_BG);
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
}
/* 页脚提示(用「・」U+30FB 而非 U+00B7「·」:common 字库按 GB2312 解码 A1A4 得 U+30FB,
* 不含 U+00B7 → 后者在设备上是豆腐块。REPORT §11 二期。) */
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);
/* 控制钮(MD3 图标) */
draw_ctrl_btn(NCM_I_PREV, g_prev_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
draw_ctrl_btn((st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? NCM_I_PLAY : NCM_I_PAUSE,
g_play_x, g_ctl_y, g_r_play, pa->red, pa->on_red);
draw_ctrl_btn(NCM_I_NEXT, g_next_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
/* 页面指示点 */
draw_page_dots(g_dots_y);
}
/* 歌词页(整屏带状物):封面模糊置底 + scrim + 顶部元信息 + 滚动歌词(当前行高亮居中 +
* 上下行渐隐)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
static void plr_draw_lyrics(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 1) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int W = g_W, H = g_H;
/* 底:封面铺满(最近邻,模糊近似)。无封面 → 沉浸底。
* ⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏 → 下次 paint 自锁死,§18)。 */
meow_img *cov = cover_get();
if (cov) meow_ui_img_scaled(0, 0, W, H, cov);
else meow_ui_fill_rect(0, 0, W, H, NCM_PLAYER_BG);
cover_put();
/* 半透明叠加层:default=主题 scrim(日/夜跟随主题);power_save=黑色更重(§22)。 */
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 195);
else
meow_ui_fill_rect_alpha(0, 0, W, H, th->scrim, 150);
/* 顶部元信息(title · artist,Apple Music 式顶部小字) */
ncm_song_t sm;
if (cur_song_meta(&sm) == 0) {
char mb[300];
snprintf(mb, sizeof mb, "%s · %s", sm.name, sm.art_name);
meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT);
}
/* 歌词:当前行居中高亮;上下行渐隐(距中心越远越接近底色)。 */
int n = lyric_count();
int64_t ps = player_manager_pos_ms();
int cur = (n > 0) ? lyric_line_at(ps) : -1;
int cy = g_lyr_cx;
int lo = g_lyr_meta_y + 44, hi = g_dots_y - 6; /* 歌词窗口(避顶部元信息/底部指示点) */
for (int i = cur - 7; i <= cur + 6; i++) {
if (i < 0 || i >= n) continue;
int yy = cy + (i - cur) * g_lyr_lh;
if (yy < lo || yy + g_lyr_lh > hi) continue;
int is_cur = (i == cur);
int dist = (i - cur) < 0 ? (cur - i) : (i - cur);
int fade = dist * 42; /* 0..255,越远越淡 */
if (fade > 214) fade = 214; /* 远行仍隐约可见,不全隐 */
uint32_t base = is_cur ? pa->red_lit : PLR_TEXT;
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? 0x000000 : th->scrim;
uint32_t col = mix_c(base, bgcol, fade);
int size = is_cur ? 24 : 20;
meow_ui_text_center(W / 2, yy, ncm_text_fit(lyric_line_text(i), W - 24, size), size, col);
if (is_cur) {
const char *tl = lyric_line_tl(i);
if (tl && tl[0])
meow_ui_text_center(W / 2, yy + 28, ncm_text_fit(tl, W - 24, 18), 18,
mix_c(pa->red_lit, bgcol, 120));
}
}
if (n == 0) {
meow_ui_text_center(W / 2, cy - 10,
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
}
draw_page_dots(g_dots_y);
}
/* 按当前页注册两个绘制物(封面页=静态带+动态带;歌词页=整屏带状物)。 */
static void player_apply_bands(void)
{
if (g_pstatic) { meow_draw_unregister(g_pstatic); g_pstatic = 0; }
if (g_pdyn) { meow_draw_unregister(g_pdyn); g_pdyn = 0; }
if (g_ppage == 0) {
g_pstatic = meow_draw_custom_register(8, g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h,
plr_draw_cover_static, NULL);
g_pdyn = meow_draw_custom_register(9, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
plr_draw_cover_dyn, NULL);
} else {
g_pdyn = meow_draw_custom_register(9, 0, 0, g_W, g_H, plr_draw_lyrics, NULL);
}
}
static void player_static_invalidate(void)
{
if (g_ppage == 0 && g_pstatic)
meow_ui_invalidate(g_cover_r.x, g_cover_r.y, g_cover_r.w, g_cover_r.h);
else if (g_ppage == 1 && g_pdyn)
meow_ui_invalidate(0, 0, g_W, g_H); /* 封面/背景变化 → 整屏重画歌词页 */
}
static void player_dyn_invalidate(void)
{
if (g_ppage == 0 && g_pdyn)
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
}
/* 切页:只改页面 + 重新注册带状物 + 整屏重画(切页低频,可全量)。 */
static void player_page_go(int dir)
{
int target = g_ppage + dir;
if (target < 0) target = 0;
if (target > 1) target = 1;
if (target == g_ppage) return;
g_ppage = target;
player_apply_bands();
meow_ui_paint_full();
}
int player_is_active(void) { return g_plc.active; }
int player_screen(void) { return g_landscape; }
int player_screen(void) { return 0; } /* 重做后恒竖屏 */
int player_page(void) { return g_ppage; }
int player_set_screen(int landscape)
int player_set_screen(int landscape) { (void)landscape; return 0; }
int player_set_page(int page)
{
int old = g_landscape;
g_landscape = landscape ? 1 : 0;
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);
int old = g_ppage;
if (page < 0) page = 0;
if (page > 1) page = 1;
if (page != g_ppage) {
g_ppage = page;
player_apply_bands();
meow_ui_paint_full();
}
return old;
}
/* ============================================================ */
/* 输入 */
/* ============================================================ */
static int in_circle(int cx, int cy, int r, int x, int y); /* 前向声明(供 touch_down 判交互区) */
/* 中部横滑跟踪:起点不在左缘(>24)且不在顶部热区才记录(左缘归返回、顶部归通知中心)。
* 封面页在「进度条/控制钮」上按下不锁 page-drag(否则拖进度条被误判为翻页,§22)。 */
void player_touch_down(int x, int y)
{
g_touch_sx = x; g_touch_sy = y;
int interactive = 0;
if (g_ppage == 0) {
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)
interactive = 1;
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y) ||
in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y) ||
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y))
interactive = 1;
}
g_touch_on = (x > PLR_EDGE_W) && (y >= meow_ui_statusbar_hotzone()) && !interactive;
g_drag_latched = 0; g_drag_dx = 0;
}
void player_touch_move(int x, int y)
{
if (!g_touch_on) return;
int dx = x - g_touch_sx, dy = y - g_touch_sy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
if (adx >= PLR_SWIPE_MS && adx > ady) { g_drag_latched = 1; g_drag_dx = dx; }
}
static void player_reset_touch(void)
{
g_touch_on = 0; g_drag_latched = 0; g_drag_dx = 0;
}
/* 歌词页:点行 seek(把 tap y 归到最近一行的下标)。 */
static int lyrics_line_tap(int y)
{
int n = lyric_count();
if (n <= 0) return -1;
int64_t ps = player_manager_pos_ms();
int cur = lyric_line_at(ps);
int off = y - g_lyr_cx;
int dioff = (off >= 0) ? (off + g_lyr_lh / 2) / g_lyr_lh : (off - g_lyr_lh / 2) / g_lyr_lh;
int i = cur + dioff;
if (i < 0) i = 0;
if (i >= n) i = n - 1;
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。 */
if (y < g_lyr_meta_y + 44 || y > g_dots_y - 6) return -1;
return i;
}
static int in_circle(int cx, int cy, int r, int x, int y)
{
int dx = x - cx, dy = y - cy;
return (dx * dx + dy * dy) <= (r * r);
}
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 (x >= g_cplay_x - g_cr && x <= g_cplay_x + g_cr && y >= g_ctl_y - g_cr && y <= g_ctl_y + g_cr) {
int st = player_manager_state();
if (st == NCM_PC_PAUSED) player_manager_pause(0);
else if (st == NCM_PC_STOPPED && cur_song() >= 0) player_manager_play(cur_song()); /* 出错后点播放=重试(§18) */
else player_manager_pause(1);
/* 中部横滑已被 touch_move 锁存:on_tap 是「尾部抬手(中部左滑落 TAP)」→ 判页切换。 */
if (g_drag_latched) {
int dir = (g_drag_dx < 0) ? 1 : -1; /* 左滑→下一页(歌词);右滑→上一页(封面) */
player_reset_touch();
player_page_go(dir);
return;
}
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);
return;
player_reset_touch();
if (g_ppage == 0) {
/* 播放/暂停:PAUSED/STOPPED(出错难重试)→ 播放;否则暂停 */
if (in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y)) {
int st = player_manager_state();
if (st == NCM_PC_PAUSED) player_manager_pause(0);
else if (st == NCM_PC_STOPPED && cur_song() >= 0) player_manager_play(cur_song());
else player_manager_pause(1);
return;
}
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(-1); return; }
if (in_circle(g_next_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(1); return; }
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);
return;
}
} else {
int line = lyrics_line_tap(y);
if (line >= 0) {
long t = lyric_line_time(line);
if (t >= 0) player_seek(t);
return;
}
}
}
void player_on_swipe(int dir, int dist)
{
(void)dist;
player_reset_touch();
if (dir == MEOW_SWIPE_RIGHT) player_page_go(-1); /* 中部右滑 → 封面页(上一页) */
/* MEOW_SWIPE_UP/DOWN 与左缘返回(SWIPE_LEFT 在 app_on_swipe 层已消费)不处理 */
}
void player_play_song(int idx)
{
if (idx < 0 || idx >= g_st.track_ids_count) return;
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
* §18:g_st.cover_url 只作「歌单封面兜底」——不再把本曲 al_pic 写进去
* (旧写法污染兜底值:al_pic 缺失的歌会回落成**上一首的封面**,且撞上
* cover_request 去重 → 封面永远停在上一首,切歌不更新的确定性子根因)。 */
* §18:g_st.cover_url 只作「歌单封面兜底」。 */
ncm_song_t s;
const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0])
? s.al_pic : g_st.cover_url;
player_manager_play(idx);
appstate_set_playing_song(idx, NULL);
cover_request(cover);
g_ppage = 0; /* 新曲 → 回封面页 */
meow_view_page_push(ACT_PLAYER);
}
@@ -385,85 +551,85 @@ void player_pause_toggle(int 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; }
/* tick:分带刷新——只标脏变化的那一带(§6)。盘带仅在歌曲/封面/播停态变化时重画。 */
/* tick:封面页只标脏「动态带」;歌词页仅在 换行/切歌/封面 变化才整屏重画(不逐帧滚动)。
* snack/错误沿用 chrome_tick + 错误沿。 */
void player_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_plc.active) return;
int sc = cur_song();
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;
int st = player_manager_state();
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); /* 歌名/歌手变 */
}
/* 元数据 id 变化(含快照滞后 settle:切歌瞬间快照仍是旧曲,play 线程更新后此处纠正)→
* 请求新曲专辑封面(黑胶盘换图)+ 刷动态带。 */
/* 元数据 id 变化(含快照滞后 settle)→ 请求新曲封面 + 刷静态带(封面)与动态带(歌名) */
ncm_song_t s2;
if (sc >= 0 && cur_song_meta(&s2) == 0 && strcmp(s2.id, g_last_meta_id) != 0) {
ncm_strlcpy(g_last_meta_id, s2.id, sizeof g_last_meta_id);
cover_request(s2.al_pic[0] ? s2.al_pic : g_st.cover_url);
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
player_static_invalidate();
player_dyn_invalidate();
}
if (sc != g_last_song) {
g_last_song = sc;
player_dyn_invalidate();
}
if (cover_changed()) {
cover_ack();
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
player_static_invalidate(); /* 封面页重画封面;歌词页整屏重画背景 */
}
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); /* 歌词 */
}
/* §18:缓冲进度前进(~2KB 粒度)刷动态带(「缓冲中… N%」);仅缓冲态需要。 */
int64_t dd = 0, dtot = 0;
player_manager_dl(&dd, &dtot);
int dq = (int)(dd / 2048);
if (dq != g_last_dl) {
g_last_dl = dq;
if (st == NCM_PC_BUFFER)
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
}
/* §18:错误沿——错误条出现/消失刷动态带 + 新错误弹 snack(旧版 err 无任何 UI 出口)。 */
int perr = player_manager_error();
if (perr != g_last_err) {
g_last_err = perr;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
if (perr == NCM_ERR_VIP) ncm_snack("VIP 专享歌曲");
else if (perr) ncm_snack("播放失败");
player_dyn_invalidate();
}
/* §20:生效档变化(降级判定完成/切歌重置)刷动态带。 */
int eff = player_manager_eff_level();
if (eff != g_last_eff) {
g_last_eff = eff;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
if (g_ppage == 0) {
if (pos != g_last_pos_ms || st != g_last_state) {
g_last_pos_ms = pos;
g_last_state = st;
player_dyn_invalidate();
}
int eff = player_manager_eff_level();
if (eff != g_last_eff) {
g_last_eff = eff;
player_dyn_invalidate();
}
int64_t dd = 0, dtot = 0;
player_manager_dl(&dd, &dtot);
int dq = (int)(dd / 2048);
if (dq != g_last_dl && st == NCM_PC_BUFFER) {
g_last_dl = dq;
player_dyn_invalidate();
}
} else {
/* 歌词页:仅在「当前行」变化时整屏重画(bg+歌词;行间静止,不逐帧刷)。 */
int line = (lyric_count() > 0) ? lyric_line_at(pos) : -1;
if (line != g_last_line) {
g_last_line = line;
meow_ui_invalidate(0, 0, g_W, g_H);
}
}
}
/* ============================================================ */
/* 页面生命周期 */
/* ============================================================ */
static void plr_page_create(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->view = v;
player_compute_geom();
meow_ui_activity_push(ACT_PLAYER); /* owner=ACT_PLAYER */
g_tb = ncm_topbar_create("正在播放", 1, "\xe2\x86\xbb"); /* 返回 + ⟳ 布局 */
meow_ui_activity_pop();
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_ppage = 0;
player_apply_bands();
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
g_last_dl = -1; g_last_err = 0; g_last_eff = -2; /* -2: 与 eff=-1(未定) 区分,首 tick 必刷 */
g_last_meta_id[0] = 0;
@@ -473,13 +639,12 @@ static void plr_page_enter(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->active = 1;
const ncm_pal_t *pa = ncm_palette(g_dark);
meow_ui_set_page_bg(pa->player_bg); /* 播放页恒沉浸,不随系统明暗整体反转 */
/* §18 自愈:重进播放页强制重估(g_last_meta_id 只在 create 清过一次——离开期间
* 切歌/封面失败后回来没有重评估路径;清零后首个 tick 按当前快照重请求封面+刷歌名)。 */
meow_ui_set_page_bg(NCM_PLAYER_BG); /* 播放页沉浸底恒 NCM_PLAYER_BG(§22 不动沉浸底) */
/* §18 自愈:重进播放页强制重估(切歌/封面失败后回来无重评估路径) */
g_last_meta_id[0] = 0;
g_last_song = -1;
g_last_dl = -1;
player_apply_bands();
meow_ui_paint_full();
if (!g_host_mock) player_manager_load_lyric();
}
@@ -493,9 +658,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_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; }
if (g_pstatic) { meow_draw_unregister(g_pstatic); g_pstatic = 0; }
if (g_pdyn) { meow_draw_unregister(g_pdyn); g_pdyn = 0; }
}
meow_view *player_view_create(void)
+31 -14
View File
@@ -24,7 +24,7 @@ static int g_custom_id;
static int g_row_h; /* 行高 */
static uint64_t g_cache_used; /* 缓存占用字节(进设置页时算一次,清理后清零) */
#define SETTINGS_ROWS 5
#define SETTINGS_ROWS 6
/* 八档音质 UI 表(用户手机 App 口径为权威,NCM_CACHE_DESIGN §3):显示顺序高→低。
* HIGHER(较高)与 DOLBY(杜比全景声)不进 UI 清单(映射保留,见 ncm_api.h)。 */
@@ -71,13 +71,14 @@ static void settings_draw(void *ctx)
char usetxt[24];
fmt_bytes(g_cache_used, usetxt, sizeof usetxt);
/* 行:音质 / 缓存上限 / 清理缓存 / 退出登录 / 关于。 */
/* 行:音质 / 歌词页背景 / 缓存上限 / 清理缓存 / 退出登录 / 关于。 */
struct { const char *title; char sub[160]; int red; } rows[SETTINGS_ROWS];
rows[0].title = "音质"; rows[0].red = 0;
rows[1].title = "缓存上限"; rows[1].red = 0;
rows[2].title = "清理缓存"; rows[2].red = 0;
rows[3].title = "退出登录"; rows[3].red = 1;
rows[4].title = "关于网易云"; rows[4].red = 0;
rows[1].title = "歌词页背景"; rows[1].red = 0;
rows[2].title = "缓存上限"; rows[2].red = 0;
rows[3].title = "清理缓存"; rows[3].red = 0;
rows[4].title = "退出登录"; rows[4].red = 1;
rows[5].title = "关于网易云"; rows[5].red = 0;
{
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
int gate = ncm_api_level_gate(g_st.level);
@@ -91,10 +92,13 @@ static void settings_draw(void *ctx)
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s)",
ncm_level_name(g_st.level), ncm_level_spec(g_st.level));
}
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb);
snprintf(rows[2].sub, sizeof rows[2].sub, "占用: %s", usetxt);
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
snprintf(rows[4].sub, sizeof rows[4].sub, "%s", "v0.1 · MeowOS");
/* §22:歌词页背景模式(default=封面模糊置底 / power_save=封面+黑叠加层) */
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %s",
g_st.player_bg_mode == NCM_BG_POWER_SAVE ? "省电模式" : "默认(模糊置底)");
snprintf(rows[2].sub, sizeof rows[2].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb);
snprintf(rows[3].sub, sizeof rows[3].sub, "占用: %s", usetxt);
snprintf(rows[4].sub, sizeof rows[4].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
snprintf(rows[5].sub, sizeof rows[5].sub, "%s", "v0.1 · MeowOS");
for (int i = 0; i < SETTINGS_ROWS; i++) {
int yy = y + i * (g_row_h + 8);
@@ -186,7 +190,20 @@ int settings_on_tap(int x, int y)
}
return 1;
}
if (row == 1) { /* 缓存上限:预设 + 自定义(数字输入 1~100) */
if (row == 1) { /* §22 歌词页背景:default(封面模糊置底)/ power_save(省电) */
meow_sheet *s = meow_sheet_create("歌词页背景", 0);
meow_sheet_add_item(s, "默认 (模糊置底)");
meow_sheet_add_item(s, "省电模式 (黑叠加层)");
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op == 0 || op == 1) {
g_st.player_bg_mode = (op == 1) ? NCM_BG_POWER_SAVE : NCM_BG_DEFAULT;
appstate_save_settings();
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);
}
return 1;
}
if (row == 2) { /* 缓存上限:预设 + 自定义(数字输入 1~100) */
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" };
@@ -204,7 +221,7 @@ int settings_on_tap(int x, int y)
}
return 1;
}
if (row == 2) { /* 清理缓存 */
if (row == 3) { /* 清理缓存 */
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "清理缓存?");
char msg[160];
char usetxt[24];
@@ -223,7 +240,7 @@ int settings_on_tap(int x, int y)
}
return 1;
}
if (row == 3) { /* 退出登录 */
if (row == 4) { /* 退出登录 */
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "退出登录?");
const char *msgs[1];
msgs[0] = "将停止播放并清除登录态,\n下次使用需重新扫码。";
@@ -236,7 +253,7 @@ int settings_on_tap(int x, int y)
}
return 1;
}
if (row == 4) { /* 关于 */
if (row == 5) { /* 关于 */
ncm_snack("网易云音乐 v0.1 · MeowOS 移植版");
return 1;
}
Binary file not shown.
Binary file not shown.