/* ui/login.c — 扫码登录页(ACT_LOGIN,根页)。 * * 流程:进入即起后台 worker(login_worker): * unikey(eapi) → 本地二维码矩阵(qrcodegen) → 轮询(801/802/803) → 803 捕获 set-cookie 加密落盘 * → login_status 拿 uid/nickname → 置 g_login_done。 * UI 线程只在 tick 读 g_st.qr 状态与 login_state 重绘并检查 g_login_done → 切入 home。 * 800(过期)自动刷新二维码。UI 不卡:网络全在 worker 线程。 */ #include "../ncm_app_internal.h" #include #include #include static pthread_t g_thread; static volatile int g_polling; /* worker 运行中 */ static volatile int g_done; /* 登录成功(UI tick 切入 home) */ static volatile int g_err; /* 最后一次错误(网络不可达等) */ static int g_active; static int g_custom_id; /* 绘制物 */ static ncm_rect_t r_cont; /* ---- 绘制 ---- */ static void login_draw(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_LOGIN) return; const meow_theme *th = meow_theme_get(); /* 标题 */ 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); /* 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); 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, g_err ? "no network - tap to retry" : "fetching QR key...", 20, th->dim); } /* 状态文字 */ 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); } /* 重绘 login 内容(QR+状态变化时)。 */ static void login_repaint(void) { meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h); } const char *login_status_str(int state) { switch (state) { case NCM_LOGIN_PENDING: return "waiting for scan..."; case NCM_LOGIN_SCANNED: return "scanned - confirm on phone"; case NCM_LOGIN_OK: return "signed in!"; case NCM_LOGIN_EXPIRED: return "QR expired - refreshing"; default: return "error, tap to retry"; } } static int get_unikey_and_qr(char *unikey, size_t usz) { int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, usz); if (rc != NCM_OK) { meow_log(MEOW_LOG_ERROR, "login: unikey fetch failed rc=%d", rc); return rc; } meow_log(MEOW_LOG_INFO, "login: unikey ok, qr ready"); ncm_strlcpy(g_st.unikey, unikey, sizeof g_st.unikey); ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url); int size = 0; if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, sizeof g_st.qr_mods / 177, &size) != NCM_OK) g_st.qr_size = 0; else g_st.qr_size = size; g_st.login_state = NCM_LOGIN_PENDING; g_err = 0; return NCM_OK; } static void *login_worker(void *arg) { (void)arg; char unikey[64]; /* 自动重试:unikey 失败(网络未就绪/抖动)时 5s 退避重试,不再要求用户点重试 */ int retry = 0; while (get_unikey_and_qr(unikey, sizeof unikey) != NCM_OK) { g_err = 1; if (g_app_quit) return NULL; meow_log(MEOW_LOG_WARN, "login: unikey retry in 5s (attempt %d)", ++retry); usleep(5 * 1000 * 1000); } meow_log(MEOW_LOG_INFO, "login: poll worker start"); char cookie[NCM_COOKIE_MAX]; ncm_strlcpy(cookie, g_st.ctx.cookie, sizeof cookie); while (!g_app_quit) { ncm_login_poll_t r; char out_cookie[NCM_COOKIE_MAX]; int rc = ncm_login_poll(g_st.ctx.device_id, unikey, cookie, &r, out_cookie, sizeof out_cookie); if (rc != NCM_OK) { meow_log(MEOW_LOG_WARN, "login: poll failed rc=%d", rc); g_err = 1; usleep(2 * 1000 * 1000); continue; } if (r.state != g_st.login_state) meow_log(MEOW_LOG_INFO, "login: state -> %d", (int)r.state); g_st.login_state = r.state; if (r.state == NCM_LOGIN_OK) { meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie"); ncm_strlcpy(cookie, out_cookie, sizeof cookie); ncm_ctx_set_cookie(&g_st.ctx, cookie); /* 先落登录态并放行跳页(真机联调 2026-08-31:status_check 在 weapi 读响应时 * 挂死(HTTP 读循环超时未生效),把跳页卡在登录页。昵称是装饰信息, * 先跳 home(歌单请求会独立验证 cookie),status_check 留待修复后恢复。) */ appstate_set_logged_in("", ""); g_st.logged_in = 1; g_done = 1; meow_log(MEOW_LOG_INFO, "login: -> home (status_check deferred)"); break; } if (r.state == NCM_LOGIN_EXPIRED) { /* 刷新二维码 */ usleep(500 * 1000); get_unikey_and_qr(unikey, sizeof unikey); continue; } usleep(1500 * 1000); } return NULL; } int login_start(void) { if (g_polling) return 0; g_polling = 1; g_done = 0; g_err = 0; if (pthread_create(&g_thread, NULL, login_worker, NULL) != 0) { g_polling = 0; return -1; } pthread_detach(g_thread); return 0; } int login_is_active(void) { return g_active; } void login_on_tap(int x, int y) { (void)x; (void)y; if (g_st.login_state == NCM_LOGIN_OK && g_done) return; if (g_err) { login_start(); login_repaint(); } /* 网络失败重试 */ } /* UI tick:重绘 QR/状态变化;登录成功 → 切 home。 */ void login_tick(uint64_t now) { (void)now; chrome_tick(); static int last_state = -1, last_qr = 0; if (g_st.login_state != last_state || g_st.qr_size != last_qr) { last_state = g_st.login_state; last_qr = g_st.qr_size; login_repaint(); } if (g_done) meow_view_page_replace(ACT_HOME); } /* ---- 生命周期 ---- */ 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, login_draw, NULL); g_active = 0; } static void login_page_enter(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 1; const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); if (!g_host_mock) login_start(); /* 进入即获取二维码 + 轮询 */ } static void login_page_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; } static void login_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; } } meow_view *login_view_create(void) { static const meow_view_hooks h = { .on_create = login_page_create, .on_enter = login_page_enter, .on_leave = login_page_leave, .on_destroy = login_page_destroy, }; return meow_view_create(ACT_LOGIN, MEOW_VIEW_ACTIVITY, &h, NULL); }