diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 8d3fd28..bb5ded9 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -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@:/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new && ssh root@ '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/刷机) +``` diff --git a/patch_v2/ncm_app/app/app.c b/patch_v2/ncm_app/app/app.c index cab3aff..dee7969 100644 --- a/patch_v2/ncm_app/app/app.c +++ b/patch_v2/ncm_app/app/app.c @@ -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; diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 6061cd6..e37311c 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -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); diff --git a/patch_v2/ncm_app/app/out/shots/home_dark.png b/patch_v2/ncm_app/app/out/shots/home_dark.png new file mode 100644 index 0000000..9554d5a Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/home_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/home_light.png b/patch_v2/ncm_app/app/out/shots/home_light.png new file mode 100644 index 0000000..ea9fc9e Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/home_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/lands_dark.png b/patch_v2/ncm_app/app/out/shots/lands_dark.png new file mode 100644 index 0000000..85ae6d3 Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/lands_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/lands_light.png b/patch_v2/ncm_app/app/out/shots/lands_light.png new file mode 100644 index 0000000..3dcf33d Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/lands_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/login_dark.png b/patch_v2/ncm_app/app/out/shots/login_dark.png new file mode 100644 index 0000000..fc86f61 Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/login_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/login_light.png b/patch_v2/ncm_app/app/out/shots/login_light.png new file mode 100644 index 0000000..578381a Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/login_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/player_dark.png b/patch_v2/ncm_app/app/out/shots/player_dark.png new file mode 100644 index 0000000..002180e Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/player_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/player_light.png b/patch_v2/ncm_app/app/out/shots/player_light.png new file mode 100644 index 0000000..f70b8b2 Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/player_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/playlist_dark.png b/patch_v2/ncm_app/app/out/shots/playlist_dark.png new file mode 100644 index 0000000..5854ffd Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/playlist_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/playlist_light.png b/patch_v2/ncm_app/app/out/shots/playlist_light.png new file mode 100644 index 0000000..2565204 Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/playlist_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/search_dark.png b/patch_v2/ncm_app/app/out/shots/search_dark.png new file mode 100644 index 0000000..da81774 Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/search_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/search_light.png b/patch_v2/ncm_app/app/out/shots/search_light.png new file mode 100644 index 0000000..3221d5b Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/search_light.png differ diff --git a/patch_v2/ncm_app/app/out/shots/settings_dark.png b/patch_v2/ncm_app/app/out/shots/settings_dark.png new file mode 100644 index 0000000..efe69ae Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/settings_dark.png differ diff --git a/patch_v2/ncm_app/app/out/shots/settings_light.png b/patch_v2/ncm_app/app/out/shots/settings_light.png new file mode 100644 index 0000000..e0b843a Binary files /dev/null and b/patch_v2/ncm_app/app/out/shots/settings_light.png differ diff --git a/patch_v2/ncm_app/app/test/test_ncm_app.c b/patch_v2/ncm_app/app/test/test_ncm_app.c index b13b7b6..a4929ac 100644 --- a/patch_v2/ncm_app/app/test/test_ncm_app.c +++ b/patch_v2/ncm_app/app/test/test_ncm_app.c @@ -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); diff --git a/patch_v2/ncm_app/app/ui/chrome.c b/patch_v2/ncm_app/app/ui/chrome.c index 9cc9cf1..e87d140 100644 --- a/patch_v2/ncm_app/app/ui/chrome.c +++ b/patch_v2/ncm_app/app/ui/chrome.c @@ -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)。 */ diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index e6edbd0..64a5abc 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -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 @@ -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, - g_st.nickname[0] ? g_st.nickname : ""); + 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) { /* 宿主/测试:跳过网络,用预置数据(同步) */ - g_load_err = 0; - if (g_load_err != 0) g_st.playlist_count = 0; - home_apply(); - return; - } - if (g_home_thr_on) return; /* 已有 worker 在跑,忽略重复触发 */ + if (g_host_mock) { g_load_err = 0; home_apply(); return; } + if (g_home_thr_on) return; 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,16 +314,32 @@ 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(); - g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h, - home_draw_empty, NULL); + /* 显式 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, r_cont.w, APP_FOOTER_H, home_draw_footer, NULL); home_drag_reset(); @@ -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_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_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; } + if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_id = 0; } + if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; } + if (g_footer_id){ meow_draw_unregister(g_footer_id);g_footer_id = 0; } if (g_list) { meow_widget_free(g_list); g_list = NULL; } + if (g_tabs) { meow_widget_free(g_tabs); g_tabs = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } } diff --git a/patch_v2/ncm_app/app/ui/login.c b/patch_v2/ncm_app/app/ui/login.c index 0767f9e..35fa715 100644 --- a/patch_v2/ncm_app/app/ui/login.c +++ b/patch_v2/ncm_app/app/ui/login.c @@ -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; } diff --git a/patch_v2/ncm_app/app/ui/ncm_colors.c b/patch_v2/ncm_app/app/ui/ncm_colors.c new file mode 100644 index 0000000..bc9d22c --- /dev/null +++ b/patch_v2/ncm_app/app/ui/ncm_colors.c @@ -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 +#include + +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; +} diff --git a/patch_v2/ncm_app/app/ui/player.c b/patch_v2/ncm_app/app/ui/player.c index 7122c90..1b6f0ab 100644 --- a/patch_v2/ncm_app/app/ui/player.c +++ b/patch_v2/ncm_app/app/ui/player.c @@ -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 @@ -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; - } - int64_t ps = player_manager_pos_ms(); - int cur2 = lyric_line_at(ps); - for (int i = cur2 - 6; i <= cur2 + 5; i++) { - if (i < 0 || i >= n) continue; - int yy = center_y + (i - cur2) * line_h; - int is_cur = (i == cur2); - meow_ui_text_center(lcx, yy, lyric_line_text(i), is_cur ? 24 : 20, - is_cur ? th->primary : th->dim); - const char *tl = lyric_line_tl(i); - if (is_cur && tl && tl[0]) - meow_ui_text_center(lcx, yy + 24, tl, 20, th->secondary); + int 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, 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); + } } + + /* 页脚提示 */ + 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; } } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index b8f7019..4adc48b 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -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 @@ -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->right = MEOW_LIST_RIGHT_ARROW; - out->on = 0; - out->enabled = 1; + out->subtitle = sub; + out->right = MEOW_LIST_RIGHT_ARROW; + 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) { /* 宿主/测试:预置数据,同步 */ - g_load_err = 0; - if (g_load_err != 0) g_st.song_count = 0; - pl_apply(); - return; - } - if (g_plthr_on) return; /* 已有 worker 在跑 */ + if (g_host_mock) { g_load_err = 0; pl_apply(); return; } + if (g_plthr_on) return; 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; @@ -228,17 +313,29 @@ 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_cont = ncm_content_rect(); + 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(); - g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h, - pl_draw_empty, NULL); + 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, r_cont.w, APP_FOOTER_H, pl_draw_footer, NULL); pl_drag_reset(); @@ -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_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_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; } + if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_id = 0; } + if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; } + if (g_footer_id){ meow_draw_unregister(g_footer_id);g_footer_id = 0; } if (g_list) { meow_widget_free(g_list); g_list = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } } diff --git a/patch_v2/ncm_app/app/ui/search.c b/patch_v2/ncm_app/app/ui/search.c new file mode 100644 index 0000000..7eebe52 --- /dev/null +++ b/patch_v2/ncm_app/app/ui/search.c @@ -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 +#include +#include +#include + +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); +} diff --git a/patch_v2/ncm_app/app/ui/settings.c b/patch_v2/ncm_app/app/ui/settings.c index 096efdd..f0bb48b 100644 --- a/patch_v2/ncm_app/app/ui/settings.c +++ b/patch_v2/ncm_app/app/ui/settings.c @@ -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; } diff --git a/patch_v2/ncm_app/build_ncm.sh b/patch_v2/ncm_app/build_ncm.sh index c9f2d51..7b0331a 100755 --- a/patch_v2/ncm_app/build_ncm.sh +++ b/patch_v2/ncm_app/build_ncm.sh @@ -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" diff --git a/patch_v2/ncm_app/build_ncm_host.sh b/patch_v2/ncm_app/build_ncm_host.sh index 31b464f..70831d5 100755 --- a/patch_v2/ncm_app/build_ncm_host.sh +++ b/patch_v2/ncm_app/build_ncm_host.sh @@ -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 diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 7536aa7..e6f4cf0 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -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; +} diff --git a/patch_v2/ncm_app/core/ncm_api.h b/patch_v2/ncm_app/core/ncm_api.h index caf516f..559c971 100644 --- a/patch_v2/ncm_app/core/ncm_api.h +++ b/patch_v2/ncm_app/core/ncm_api.h @@ -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 diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 4ad4e01..085fcea 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host new file mode 100755 index 0000000..4c6724f Binary files /dev/null and b/patch_v2/ncm_app/ncm_host differ diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index 5a3eaa8..408cd93 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ