Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/cache.c
T
AskaEth f392ee5c48 feat(ncm): 首页顶栏「已缓存」入口 + 已缓存列表页(歌名/歌手/实际档位/大小,点播/单曲删除/清空)
入口:首页 topbar 新增动作 3(MEOW_ICON_DOWNLOAD 位图,原「回播放页」顺延动作 4)→
push ACT_CACHE;左缘返回弹回首页(app.c 路由补齐 tap/swipe/touch/tick)。

列表页 app/ui/cache.c(对齐 queue.c 惯例:圆角卡/红 mini-vinyl/按压红条/当前播放行高亮):
- 行 = 歌名 / 歌手 · 实际档位 · 大小;空态/页脚(N 首 · 合计 X MB)。
- 点行 = 点播(NCM_QUEUE_CACHE 队列,上/下一首在缓存列表内循环);行右「删除」=单曲删除;
  顶栏「清空」=确认框 + ncm_cache_clear(与设置页「清理缓存」同语义)。
- 数据全在 worker:cache_reload → appstate_cache_list_load → ncm_cache_scan(读目录 +
  文件内 tag + 音频头探测)→ 快照;UI 线程零磁盘 IO。快照上限 512 条(最近访问优先,
  ≈116KB bss),更多条目按 mtime 保留最近的。

core:
- ncm_cache_scan / ncm_cache_remove(songs/ 池,song_<id>_<请求档>.<实际格式> 解析,
  跳过 .tmp/.part 与非数字 id;按 mtime 降序;历史双点名删除兜底)。
- ncm_api_level_from_key(缓存键后缀 ↔ 档位枚举,与 ncm_api_level_key 互逆)。
- **档位显示实际档而非请求档**:真机存在 song_171650_hires.mp3(无损请求被降级链落到
  exhigh mp3),故 scan 用 ncm_audio_probe_file 读实际容器参数后只降不升地推断
  (FLAC 16bit→无损 / 24bit→Hi-Res+,MP3 ≤128k→标准 / >128k→极高)。

播放:player_manager 对 NCM_QUEUE_CACHE 用条目真实键(id+请求档+实际扩展名)直读缓存
文件,不按用户当前档键重下;song_local_file/play_is_same_song/queue 页均支持第三种队列。
宿主 mock(NCM_PAGE=cache)造 3 条缓存 + 嵌入 tag + 同步扫描。
2026-11-04 20:49:25 +08:00

413 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui/cache.c — 已缓存列表页(ACT_CACHE,首页顶栏「已缓存」钮进入)。
*
* 数据源:songs/ 音频池(键=歌曲ID+请求档位,NCM_CACHE_DESIGN §2.5)。扫描在 worker:
* ncm_cache_scan(读目录 + 文件头 tag + 音频参数探测)→ appstate 快照;
* UI 线程只读快照(零磁盘 IO),列表由 meow_listview 虚拟化。
* 行:歌名 / 歌手 · **实际档位** · 大小。**档位显示实际档而非请求档**:缓存名里的档位段只是
* 请求档(真机存在 song_<id>_hires.mp3 = 无损请求被降级链落到 exhigh mp3),实际档由
* 文件头(FLAC bps/采样率、MP3 首帧码率)推断。
* 交互:点行=点播(NCM_QUEUE_CACHE,上/下一首在缓存列表内循环);行右「删除」=单曲删除;
* 顶栏「清空」=清空缓存池(确认框,与设置页「清理缓存」同语义)。
* 主题/控件惯例同 queue.c:圆角卡 + 红 mini-vinyl + 按压红条 + 当前播放行高亮。
* 左缘返回=弹回首页(app.c)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct { meow_view *view; int active; } cache_ctrl;
static cache_ctrl g_cc;
static meow_widget *g_tb, *g_list;
static ncm_rect_t r_cont, r_list;
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
static int g_last_count;
/* 扫描 worker(磁盘 IO 不进 UI 线程;gen 计数做完成信号) */
static pthread_mutex_t g_cmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_cthr;
static int g_cthr_on;
static volatile int g_cgen, g_cacked;
static volatile int g_cloading;
static volatile int g_cerr;
static void c_drag_reset(void)
{
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
}
/* 字节数 → "12.3 MB"(整数运算,不引浮点 printf;MIPS 软浮点省体积)。 */
static void fmt_size(uint64_t n, char *out, size_t sz)
{
if (n >= (1024ULL * 1024ULL * 1024ULL)) {
uint64_t t = (n * 10ULL + 512ULL * 1024ULL * 1024ULL) / (1024ULL * 1024ULL * 1024ULL);
snprintf(out, sz, "%llu.%llu GB", (unsigned long long)(t / 10), (unsigned long long)(t % 10));
} else if (n >= 1024ULL * 1024ULL) {
uint64_t t = (n * 10ULL + 512ULL * 1024ULL) / (1024ULL * 1024ULL);
snprintf(out, sz, "%llu.%llu MB", (unsigned long long)(t / 10), (unsigned long long)(t % 10));
} else {
snprintf(out, sz, "%llu KB", (unsigned long long)(n / 1024ULL));
}
}
static const char *level_label(int lv)
{
return (lv >= 0) ? ncm_level_name(lv) : "未知档";
}
/* 列表数据源(逐行快照读取,零 IO)。 */
static int c_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
static ncm_cache_item_t it;
static char sub[192];
static char title[100];
char sz[24];
if (appstate_cache_item(idx, &it) != NCM_OK) return 0;
fmt_size(it.size, sz, sizeof sz);
snprintf(sub, sizeof sub, "%s · %s · %s",
it.artist[0] ? it.artist : "-", level_label(it.actual_level), sz);
snprintf(title, sizeof title, "%s", it.name[0] ? it.name : "(未知歌曲)");
out->icon_kind = MEOW_LIST_ICON_NONE; /* 图标槽由红 overlays 画迷你黑胶 */
out->title = title;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_NONE; /* 右侧「删除」由 z=60 叠层自绘 */
out->on = 0; out->enabled = 1;
return 1;
}
/* 行背景(z=5,画在 listview 文本之下):圆角卡 + 当前播放行红色高亮。 */
static void c_row_bg(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_CACHE) return;
int cnt = appstate_cache_count();
if (cnt <= 0) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
int cur = (appstate_get_queue() == NCM_QUEUE_CACHE) ? player_manager_playing_song() : -1;
for (int i = first; i < last; i++) {
int ry = ncm_row_card_y(item, i, off, r_list.y);
int th2 = ncm_row_card_h(item);
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */
if (i == cur) {
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, pa->red_ctr);
meow_ui_fill_rect(r_list.x + 2, ry + 4, 4, th2 - 8, pa->red);
} else {
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer);
}
}
}
/* 主绘制(z=60):迷你黑胶缩略 + 行右「删除」+ 按压红条;文本由 listview z=10 画。 */
static void c_draw(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_CACHE) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int cnt = appstate_cache_count();
if (cnt <= 0) return;
int item = APP_ROW_H, isz = 24;
int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1;
if (last > cnt) last = cnt;
for (int i = first; i < last; i++) {
int ry = ncm_row_card_y(item, i, off, r_list.y);
int th2 = ncm_row_card_h(item);
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
int icy = ry + (th2 - 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); /* 盘芯 */
/* 行右「删除」(触达区 = 右 48px,见 cache_on_tap) */
meow_ui_text_right(r_list.x + r_list.w - APP_PAD,
ry + (th2 - 18) / 2, "删除", 18, pa->red);
if (g_press_row == i) {
meow_ui_fill_rect(r_list.x, ry, 4, th2, pa->red);
meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, th2, pa->red, 26);
}
/* 擦除 z=10 的 SDK 行分隔线(现落进卡片间空隙):以页底色填空隙带;不越出列表视口底。 */
int gy = ry + th2, gh = NCM_ROW_GAP;
if (gy + gh > r_list.y + r_list.h) gh = r_list.y + r_list.h - gy;
if (gh > 0) meow_ui_fill_rect(r_list.x, gy, r_list.w, gh, th->bg);
}
}
static void c_draw_empty(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_CACHE) return;
if (appstate_cache_count() != 0) return;
if (g_cloading) { ncm_draw_empty(r_list, "LOADING", "正在扫描缓存..."); return; }
if (g_cerr) { ncm_draw_empty(r_list, "ERROR", "缓存目录读取失败"); return; }
ncm_draw_empty(r_list, "暂无缓存", "播放过的歌曲会自动缓存");
}
static void c_draw_footer(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_CACHE) return;
int n = appstate_cache_count();
if (n <= 0) return;
uint64_t total = 0;
ncm_cache_item_t it;
for (int i = 0; i < n; i++)
if (appstate_cache_item(i, &it) == NCM_OK) total += it.size;
char sz[24], foot[96];
fmt_size(total, sz, sizeof sz);
snprintf(foot, sizeof foot, "%d 首 · 合计 %s", n, sz);
ncm_draw_footer(r_cont, foot);
}
static void c_apply(void)
{
if (!g_list) return;
int n = appstate_cache_count();
meow_listview_set_count(g_list, n);
if (n != g_last_count) {
g_last_count = n;
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
}
static void *cache_worker(void *arg)
{
(void)arg;
int rc = appstate_cache_list_load(); /* 扫描 + 文件内 tag + 旁路补名:全部磁盘 IO */
pthread_mutex_lock(&g_cmu);
g_cerr = (rc < 0) ? rc : 0;
g_cloading = 0;
g_cgen++;
pthread_mutex_unlock(&g_cmu);
return NULL;
}
void cache_reload(void)
{
if (g_host_mock) { /* 宿主渲染:同步扫描(无线程/无延时,截图确定) */
int rc = appstate_cache_list_load();
g_cerr = (rc < 0) ? rc : 0;
g_cloading = 0;
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
return;
}
if (g_cthr_on) return;
g_cloading = 1;
g_cthr_on = 1;
if (ncm_thread_create(&g_cthr, cache_worker, NULL) != 0) {
g_cthr_on = 0; g_cloading = 0;
g_cerr = NCM_ERR_IO;
}
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
/* 单曲删除(行右「删除」):删文件 + 摘除快照项 + 提示。 */
static void cache_delete_row(int row)
{
ncm_cache_item_t it;
if (appstate_cache_item(row, &it) != NCM_OK) return;
char name[96], msg[160];
ncm_strlcpy(name, it.name[0] ? it.name : it.id, sizeof name);
if (appstate_cache_remove(row) == 0) {
snprintf(msg, sizeof msg, "已删除:%s", name);
ncm_snack(msg);
c_apply();
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
} else {
ncm_snack("删除失败");
}
}
/* 清空(顶栏「清空」):确认框(设置页「清理缓存」同语义:删除全部音频 + 封面缓存条目)。 */
static void cache_confirm_clear(void)
{
int n = appstate_cache_count();
if (n <= 0) { ncm_snack("缓存已为空"); return; }
uint64_t total = 0;
ncm_cache_item_t it;
for (int i = 0; i < n; i++)
if (appstate_cache_item(i, &it) == NCM_OK) total += it.size;
char sz[24], msg[192];
fmt_size(total, sz, sizeof sz);
snprintf(msg, sizeof msg, "将删除全部 %d 首已缓存歌曲 (%s) 及封面缓存。\n已播放的歌曲需重新下载。",
n, sz);
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "清空缓存?");
const char *msgs[1];
msgs[0] = msg;
meow_dialog_set_items(d, msgs, 1, -1);
int yes = meow_dialog_confirm(d);
meow_dialog_free(d);
if (!yes) return;
appstate_cache_clear();
ncm_snack("缓存已清理");
c_apply();
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
void cache_open(void)
{
meow_view_page_push(ACT_CACHE);
cache_reload();
c_apply();
meow_ui_paint_full();
}
int cache_is_active(void) { return g_cc.active; }
int cache_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { c_drag_reset(); meow_view_page_pop(); return 1; }
if (ah == 0) { c_drag_reset(); cache_confirm_clear(); return 1; } /* 文本动作「清空」 */
}
if (meow_widget_hit(g_list, x, y)) {
int was_drag = (g_press_delta > 10);
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
int del = (x >= r_list.x + r_list.w - APP_OP_W);
c_drag_reset();
if (row < 0 || was_drag) return 1;
if (del) { cache_delete_row(row); return 1; }
player_play_cache_song(row);
return 1;
}
return 0;
}
void cache_touch_down(int x, int y)
{
c_drag_reset();
if (appstate_cache_count() <= 0) return;
if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y);
if (row >= 0) g_press_row = row;
g_dragging = 1; g_drag_last_y = y;
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
}
void cache_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 cache_swipe(int dir, int dist)
{
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (appstate_cache_count() <= 0) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before)
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
void cache_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_cc.active) return;
if (!g_cthr_on) return;
int gen, acked;
pthread_mutex_lock(&g_cmu);
gen = g_cgen; acked = g_cacked;
pthread_mutex_unlock(&g_cmu);
if (gen == acked) return;
pthread_mutex_lock(&g_cmu);
g_cacked = gen;
pthread_mutex_unlock(&g_cmu);
g_cthr_on = 0;
if (g_cerr) ncm_snack("缓存读取失败");
c_apply();
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
}
/* 生命周期 */
static void c_page_create(meow_view *v, void *ctx)
{
(void)v;
((cache_ctrl *)ctx)->view = v;
r_cont = ncm_content_rect();
r_list = NR(16, r_cont.y + 8, r_cont.w - 32,
r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8));
if (r_list.h < 0) r_list.h = 0;
meow_ui_activity_push(ACT_CACHE);
g_tb = ncm_topbar_create("已缓存", 1, "清空"); /* 文本动作(动作0)=清空缓存 */
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
c_src2, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_ui_activity_pop();
meow_widget_set_z(g_list, 10);
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h,
c_row_bg, NULL);
g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
c_draw, NULL);
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
c_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, c_draw_footer, NULL);
c_drag_reset();
}
static void c_page_enter(meow_view *v, void *ctx)
{
(void)v;
((cache_ctrl *)ctx)->active = 1;
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->bg);
g_last_count = -1;
cache_reload(); /* 每次进入重扫(播放/删除后 LRU 变化) */
c_apply();
meow_ui_paint_full();
}
static void c_page_leave(meow_view *v, void *ctx)
{
(void)v;
((cache_ctrl *)ctx)->active = 0;
}
static void c_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; }
if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_id = 0; }
if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; }
if (g_footer_id) { meow_draw_unregister(g_footer_id); g_footer_id = 0; }
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *cache_view_create(void)
{
static const meow_view_hooks h = {
.on_create = c_page_create,
.on_enter = c_page_enter,
.on_leave = c_page_leave,
.on_destroy = c_page_destroy,
};
return meow_view_create(ACT_CACHE, MEOW_VIEW_ACTIVITY, &h, &g_cc);
}