/* ui/cache.c — 已缓存列表页(ACT_CACHE,首页顶栏「已缓存」钮进入)。 * * 数据源:songs/ 音频池(键=歌曲ID+请求档位,NCM_CACHE_DESIGN §2.5)。扫描在 worker: * ncm_cache_scan(读目录 + 文件头 tag + 音频参数探测)→ appstate 快照; * UI 线程只读快照(零磁盘 IO),列表由 meow_listview 虚拟化。 * 行:歌名 / 歌手 · **实际档位** · 大小。**档位显示实际档而非请求档**:缓存名里的档位段只是 * 请求档(真机存在 song__hires.mp3 = 无损请求被降级链落到 exhigh mp3),实际档由 * 文件头(FLAC bps/采样率、MP3 首帧码率)推断。 * 交互:点行=点播(NCM_QUEUE_CACHE,上/下一首在缓存列表内循环);行右「删除」=单曲删除; * 顶栏「清空」=清空缓存池(确认框,与设置页「清理缓存」同语义)。 * 主题/控件惯例同 queue.c:圆角卡 + 红 mini-vinyl + 按压红条 + 当前播放行高亮。 * 左缘返回=弹回首页(app.c)。 */ #include "../ncm_app_internal.h" #include #include #include #include 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[128]; fmt_size(total, sz, sizeof sz); snprintf(foot, sizeof foot, "%d 首 · 合计 %s%s", n, sz, appstate_is_offline() ? " · 离线模式(可播放)" : ""); 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); }