Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/cache.c
T
AskaEth 707bbd8a02 feat(ncm): 断网保持登录态 + 离线模式(可见提示/缓存可播/联网自动回在线)(M9)
用户报告:「一断网就退出到扫码登录界面,不会保持账号态切离线,缓存列表形同虚设」。
上一条已把失败分成 NETWORK/AUTH;本条把结论落到 App 状态与 UI。

- 会话状态机(appstate.c):只有 NCM_ERR_AUTH(401/403/301/302)清 cookie;网络类失败
  (不可达/超时/TLS/5xx/-460)只置 g_st.offline,账号态与 session.enc 一律保留;
  appstate_ensure_uid 同语义(原先任何 AUTH 都清 cookie,网络故障不得进入该分支)。
- 启动(app.c):根页改由 ncm_session_boot_decide 决定——有本地会话就进 home(无网也是),
  只有无本地会话才进扫码;打印 boot 决策日志。
- 离线 UX:首页个人卡「离线模式」红标 + 页脚提示 + 空态「离线模式/可播放已缓存」;
  歌单页/搜索页/播放页/缓存页分别给出离线文案(缓存页点播照旧走 NCM_QUEUE_CACHE,本地可播)。
- 恢复:home 可见时每 30s 静默重试一次(强制绕过接口缓存,成功即回在线并重绘);
  任何真实网络成功(home/playlist worker)也会清离线态。
- 防误判:接口缓存命中(120s 旧响应)不算「回在线」——home_worker 用
  ncm_apicache_hits() 差分识别,避免离线提示被缓存命中闪掉。
2026-11-05 11:45:20 +08:00

414 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[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);
}