/* ui/library/library.c — 图库浏览页(ACT_LIBRARY): * * TopAppBar(标题 + 返回 + 「TF卡」回根 + ⚙)+ meow_listview(src2 行数据源)。 * * 浏览语义(M13,用户要求:性能 + 访问放开): * - 首帧只做**单层**枚举(logic/library.c:一次 opendir/readdir+lstat), * 不做任何递归「子树含图」探测;列表行 = 当前目录的直接子目录(M11 起还包括 * .cbz/.zip 档案册,与目录自然序混排); * - 系统目录/空目录/无图目录都列出且可进入;可一路向上到文件系统 '/', * 左缘返回由 app.c 逐级上溯、到 '/' 才退出 App; * - 「直接含图目录=一本书」与档案册页数改为后台懒算:行仍在 PENDING 时副标题「…」, * 后台线程用 mr_library_probe_path(单层 + mtime/size 失效缓存)算出页数/首图, * on_tick 补位后只 invalidate 列表视口——首帧/滚动/命中判定都不读子目录/档案; * - 不可读/不存在的目录:停留在原目录,顶部横幅提示 2.5s。 * * 行绑定(列表 src2 模式):meow_listview_create(..., rows, lib_src, ...) 每帧/每次 * invalidate 对可见行重新取行;lib_src 只读内存里的 g_lib + 缩略图缓存,并把「待探测/ * 待缩略图」请求投递到有界队列(不做 IO),因此懒算结果补位后下一帧即可上屏。 * * 缩略图:后台线程 meow_img_downscale 到 96px,落磁盘缓存 MeowData//cache * (BMP,文件名含源 mtime+size,源变化自动失效;≥64 张按 mtime LRU 删除)。 * 档案册(M11)封面=档内首图:先解压到 page_cache,再按普通图片解码。 * * 绘制/颜色全部取 meow_theme_get() 语义色;主题切换由 app.c 的 listener 整帧 invalidate。 */ #include "tuku_internal.h" #include "logic/reader/page_source.h" #include "logic/reader/page_cache.h" #include #include #include #include #include #include #include #include #include #include #include #define MR_THUMB_MAX 24 #define MR_THUMB_DIM 96 #define MR_TOPAPPBAR_H 56 #define MR_DEFAULT_ROW_H 56 #define MR_CACHE_MAX_FILES 64 #define MR_HINT_H 44 #define MR_HINT_MS 2500 #define MR_TREQ_MAX 64 #define MR_TRES_MAX 16 #define MR_PQ_MAX 128 #define MR_PRES_MAX 48 typedef struct thumb_req { int gen; int idx; char cover[MR_PATH_MAX]; } thumb_req; typedef struct thumb_res { int gen; int idx; meow_img *img; } thumb_res; typedef struct probe_job { int gen; int idx; int is_archive; char path[MR_PATH_MAX]; } probe_job; typedef struct probe_res { int gen; int idx; int is_archive; char path[MR_PATH_MAX]; int ok; mr_library_probe probe; } probe_res; static mr_page_ctrl g_ctrl; static mr_library g_lib; static char g_cur_dir[MR_PATH_MAX] = MR_ROOT_DIR; static char g_root_dir[MR_PATH_MAX] = MR_ROOT_DIR; /* 「TF卡」回根目标(宿主测试可改) */ static meow_widget *g_tb; static meow_widget *g_lv; static meow_scroll *g_scroll; static int g_list_x, g_list_y, g_list_w, g_list_h; static int g_tb_home_idx = -1; static struct { int idx; meow_img *img; } g_thumbs[MR_THUMB_MAX]; static int g_thumb_n; /* ---- 后台工作线程:探测队列(优先)+ 缩略图队列 ---- */ static pthread_t g_worker; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER; static int g_worker_on; static int g_worker_stop; static int g_gen; static probe_job g_pq[MR_PQ_MAX]; static int g_pq_head, g_pq_tail, g_pq_n; static char g_probe_inflight[MR_PATH_MAX]; static probe_res g_pres[MR_PRES_MAX]; static int g_pres_n; static thumb_req g_treq[MR_TREQ_MAX]; static int g_treq_head, g_treq_tail, g_treq_n; static char g_thumb_inflight[MR_PATH_MAX]; static thumb_res g_tres[MR_TRES_MAX]; static int g_tres_n; /* ---- 磁盘缩略图缓存(与解压页缓存同目录) ---- */ static char g_cache_dir[MR_PATH_MAX]; static int g_cache_on; /* ---- 顶部提示横幅(不可读目录等) ---- */ static int g_hint_id; static char g_hint[160]; static uint64_t g_hint_until; static int g_hint_x, g_hint_y, g_hint_w, g_hint_h; static int g_drag; static int g_dragged; static int g_drag_last_y; /* 书库扫描/后台探测的档案册探测(M11 注入):logic/library 不依赖 zip 容器。 */ static int cbz_archive_pages(const char *path, void *ud) { (void)ud; return mr_zip_count_pages(path); } /* ============================================================ */ /* 磁盘缓存(BMP 原始缩略图;文件名含源 mtime+size 做失效) */ /* ============================================================ */ static unsigned long long fnv1a64(const char *s) { unsigned long long h = 1469598103934665603ULL; for (const unsigned char *p = (const unsigned char *)s; *p; p++) { h ^= (unsigned long long)*p; h *= 1099511628211ULL; } return h; } static void cache_init(void) { const char *data = meow_config_dir(); if (!data || !data[0]) return; /* 缩略图缓存与解压页缓存同目录(MeowData//cache)。目录推导、建立与 * 启动清孤儿统一交给 logic/reader/page_cache,避免两处目录规则分叉。 */ if (mr_cache_init(data) != 0 || mr_cache_dir()[0] == '\0') return; snprintf(g_cache_dir, sizeof g_cache_dir, "%s", mr_cache_dir()); g_cache_on = 1; } static void cache_path(char *out, int outsz, const char *cover, const struct stat *st) { unsigned long long h = fnv1a64(cover); snprintf(out, (size_t)outsz, "%s/%016llx_%lld_%lld.bmp", g_cache_dir, h, (long long)st->st_mtime, (long long)st->st_size); } /* LRU:缓存文件数超过 MR_CACHE_MAX_FILES 时删最旧(读命中会 utimes 刷新 mtime)。 * 每次只删一个最旧文件、最多扫 8 轮,避免在 14MB 内存口径下分配大数组。 */ static void cache_prune(void) { if (!g_cache_on || !g_cache_dir[0]) return; for (int pass = 0; pass < 8; pass++) { DIR *d = opendir(g_cache_dir); if (!d) return; int count = 0, found = 0; char oldest[MR_PATH_MAX + 96]; oldest[0] = '\0'; time_t oldest_t = 0; struct dirent *de; while ((de = readdir(d)) != NULL) { const char *dot = strrchr(de->d_name, '.'); if (!dot || strcmp(dot, ".bmp") != 0) continue; char full[MR_PATH_MAX + 96]; int n = snprintf(full, sizeof full, "%s/%s", g_cache_dir, de->d_name); if (n <= 0 || n >= (int)sizeof full) continue; struct stat st; if (stat(full, &st) != 0) continue; count++; if (!found || st.st_mtime < oldest_t) { found = 1; oldest_t = st.st_mtime; snprintf(oldest, sizeof oldest, "%s", full); } } closedir(d); if (count <= MR_CACHE_MAX_FILES || !found) break; unlink(oldest); } } static void put_u16(unsigned char *p, unsigned v) { p[0] = (unsigned char)(v & 0xFF); p[1] = (unsigned char)((v >> 8) & 0xFF); } static void put_u32(unsigned char *p, uint32_t v) { p[0] = (unsigned char)(v & 0xFF); p[1] = (unsigned char)((v >> 8) & 0xFF); p[2] = (unsigned char)((v >> 16) & 0xFF); p[3] = (unsigned char)((v >> 24) & 0xFF); } /* 写 32bpp BI_RGB BMP(stb_image 可读回)。失败返回 -1(缓存只是加速,静默降级)。 */ static int write_bmp(const char *path, const meow_img *im) { int w = meow_img_w(im), h = meow_img_h(im); const unsigned char *px = meow_img_pixels(im); if (!path || w <= 0 || h <= 0 || w > MR_THUMB_DIM || h > MR_THUMB_DIM || !px) return -1; uint32_t stride = (uint32_t)w * 4; uint32_t imgsz = stride * (uint32_t)h; unsigned char fh[14] = { 0 }; unsigned char ih[40] = { 0 }; fh[0] = 'B'; fh[1] = 'M'; put_u32(fh + 2, 14 + 40 + imgsz); put_u32(fh + 10, 14 + 40); put_u32(ih + 0, 40); put_u32(ih + 4, (uint32_t)w); put_u32(ih + 8, (uint32_t)h); put_u16(ih + 12, 1); put_u16(ih + 14, 32); put_u32(ih + 20, imgsz); put_u32(ih + 24, 2835); put_u32(ih + 28, 2835); FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = (fwrite(fh, 1, sizeof fh, f) == sizeof fh && fwrite(ih, 1, sizeof ih, f) == sizeof ih); unsigned char row[MR_THUMB_DIM * 4]; for (int y = h - 1; ok && y >= 0; y--) { const unsigned char *src = px + (size_t)y * (size_t)w * 4; for (int x = 0; x < w; x++) { row[x * 4 + 0] = src[x * 4 + 2]; row[x * 4 + 1] = src[x * 4 + 1]; row[x * 4 + 2] = src[x * 4 + 0]; row[x * 4 + 3] = src[x * 4 + 3]; } if (fwrite(row, 1, stride, f) != stride) ok = 0; } if (fclose(f) != 0) ok = 0; if (!ok) unlink(path); return ok ? 0 : -1; } /* 后台线程:缓存命中直接读;否则解码源图 → downscale → 落缓存。 * 档案册(M11)的 cover 是 .cbz/.zip 路径:先解压档内首图到 page_cache, * 再按普通图片路径解码;缩略图缓存键仍用档案本身(mtime/size 变化即失效)。 */ static meow_img *thumb_load(const char *cover) { struct stat st; if (stat(cover, &st) != 0 || !S_ISREG(st.st_mode)) return NULL; char extracted[MR_CACHE_FILE_MAX]; const char *src = cover; if (mr_path_is_archive(cover)) { if (!mr_zip_cover_path(cover, extracted, (int)sizeof extracted)) return NULL; src = extracted; } char cp[MR_PATH_MAX + 96]; if (g_cache_on && g_cache_dir[0]) { cache_path(cp, (int)sizeof cp, cover, &st); if (access(cp, R_OK) == 0) { meow_img *im = meow_img_load_file(cp); if (im) { utimes(cp, NULL); /* LRU 读命中刷新 */ return im; } unlink(cp); /* 损坏缓存 */ } } meow_img *im = meow_img_load_file(src); if (!im) return NULL; meow_img *small = meow_img_downscale(im, MR_THUMB_DIM); if (small) { meow_img_free(im); im = small; } if (g_cache_on && g_cache_dir[0]) { if (write_bmp(cp, im) == 0) cache_prune(); } return im; } static void probe_push_res_locked(const probe_job *job, int ok, const mr_library_probe *p) { if (g_pres_n >= MR_PRES_MAX) return; probe_res *r = &g_pres[g_pres_n++]; r->gen = job->gen; r->idx = job->idx; r->is_archive = job->is_archive; snprintf(r->path, sizeof r->path, "%s", job->path); r->ok = ok; if (ok && p) r->probe = *p; else memset(&r->probe, 0, sizeof r->probe); } static void thumb_push_res_locked(const thumb_req *req, meow_img *im) { if (g_tres_n >= MR_TRES_MAX) { meow_img_free(im); return; } thumb_res *r = &g_tres[g_tres_n++]; r->gen = req->gen; r->idx = req->idx; r->img = im; } static void *lib_worker(void *arg) { (void)arg; for (;;) { int kind = 0; /* 1=探测 2=缩略图 */ probe_job pj; thumb_req tr; pthread_mutex_lock(&g_lock); while (!g_worker_stop && g_pq_n == 0 && g_treq_n == 0) pthread_cond_wait(&g_cv, &g_lock); if (g_worker_stop) { pthread_mutex_unlock(&g_lock); break; } if (g_pq_n > 0) { /* 探测优先:页数/角标比重缩略图更影响可读性 */ kind = 1; pj = g_pq[g_pq_head]; g_pq_head = (g_pq_head + 1) % MR_PQ_MAX; g_pq_n--; snprintf(g_probe_inflight, sizeof g_probe_inflight, "%s", pj.path); } else { kind = 2; tr = g_treq[g_treq_head]; g_treq_head = (g_treq_head + 1) % MR_TREQ_MAX; g_treq_n--; snprintf(g_thumb_inflight, sizeof g_thumb_inflight, "%s", tr.cover); } pthread_mutex_unlock(&g_lock); if (kind == 1) { mr_library_probe p; int ok = (mr_library_probe_path(pj.path, pj.is_archive, cbz_archive_pages, NULL, &p) == 0); pthread_mutex_lock(&g_lock); g_probe_inflight[0] = '\0'; if (!g_worker_stop) probe_push_res_locked(&pj, ok, &p); pthread_mutex_unlock(&g_lock); } else { meow_img *im = thumb_load(tr.cover); pthread_mutex_lock(&g_lock); g_thumb_inflight[0] = '\0'; if (im && !g_worker_stop) thumb_push_res_locked(&tr, im); else if (im) meow_img_free(im); pthread_mutex_unlock(&g_lock); } } return NULL; } static void worker_start(void) { if (g_worker_on) return; g_worker_stop = 0; if (pthread_create(&g_worker, NULL, lib_worker, NULL) == 0) g_worker_on = 1; } static void worker_stop(void) { if (!g_worker_on) return; pthread_mutex_lock(&g_lock); g_worker_stop = 1; pthread_cond_broadcast(&g_cv); pthread_mutex_unlock(&g_lock); pthread_join(g_worker, NULL); g_worker_on = 0; pthread_mutex_lock(&g_lock); for (int i = 0; i < g_tres_n; i++) meow_img_free(g_tres[i].img); g_tres_n = 0; g_pres_n = 0; g_treq_n = 0; g_treq_head = g_treq_tail = 0; g_pq_n = 0; g_pq_head = g_pq_tail = 0; g_probe_inflight[0] = '\0'; g_thumb_inflight[0] = '\0'; pthread_mutex_unlock(&g_lock); } /* 探测请求:只投递(有界队列 + 去重),不做 IO。 */ static void probe_enqueue(int idx, const char *path, int is_archive) { if (!g_worker_on || !path || !path[0]) return; pthread_mutex_lock(&g_lock); if (g_worker_stop) { pthread_mutex_unlock(&g_lock); return; } if (strcmp(g_probe_inflight, path) == 0) { pthread_mutex_unlock(&g_lock); return; } for (int i = 0; i < g_pq_n; i++) { int p = (g_pq_head + i) % MR_PQ_MAX; if (strcmp(g_pq[p].path, path) == 0) { pthread_mutex_unlock(&g_lock); return; } } if (g_pq_n >= MR_PQ_MAX) { pthread_mutex_unlock(&g_lock); return; } g_pq[g_pq_tail].gen = g_gen; g_pq[g_pq_tail].idx = idx; g_pq[g_pq_tail].is_archive = is_archive; snprintf(g_pq[g_pq_tail].path, sizeof g_pq[g_pq_tail].path, "%s", path); g_pq_tail = (g_pq_tail + 1) % MR_PQ_MAX; g_pq_n++; pthread_cond_signal(&g_cv); pthread_mutex_unlock(&g_lock); } /* 缩略图请求:bind 只投递,不阻塞首绘/滚动。 */ static void thumb_enqueue(int idx, const char *cover) { if (!g_worker_on || !cover || !cover[0]) return; pthread_mutex_lock(&g_lock); if (g_worker_stop) { pthread_mutex_unlock(&g_lock); return; } if (strcmp(g_thumb_inflight, cover) == 0) { pthread_mutex_unlock(&g_lock); return; } for (int i = 0; i < g_treq_n; i++) { int p = (g_treq_head + i) % MR_TREQ_MAX; if (strcmp(g_treq[p].cover, cover) == 0) { pthread_mutex_unlock(&g_lock); return; } } if (g_treq_n >= MR_TREQ_MAX) { pthread_mutex_unlock(&g_lock); return; } g_treq[g_treq_tail].gen = g_gen; g_treq[g_treq_tail].idx = idx; snprintf(g_treq[g_treq_tail].cover, sizeof g_treq[g_treq_tail].cover, "%s", cover); g_treq_tail = (g_treq_tail + 1) % MR_TREQ_MAX; g_treq_n++; pthread_cond_signal(&g_cv); pthread_mutex_unlock(&g_lock); } /* ============================================================ */ /* 缩略图内存缓存 */ /* ============================================================ */ static void thumbs_free(void) { for (int i = 0; i < g_thumb_n; i++) meow_img_free(g_thumbs[i].img); g_thumb_n = 0; } static meow_img *thumb_find(int idx) { for (int i = 0; i < g_thumb_n; i++) if (g_thumbs[i].idx == idx) return g_thumbs[i].img; return NULL; } static int thumb_put(int idx, meow_img *im) { if (g_thumb_n >= MR_THUMB_MAX) return 0; g_thumbs[g_thumb_n].idx = idx; g_thumbs[g_thumb_n].img = im; g_thumb_n++; return 1; } /* ============================================================ */ /* 行数据源(src2:每帧按需取可见行;零 IO,懒算请求只入队) */ /* ============================================================ */ static int lib_src(int idx, meow_list_item *item, void *ctx) { (void)ctx; memset(item, 0, sizeof *item); mr_library_row row; if (!mr_library_row_at(&g_lib, idx, &row)) return 0; item->title = row.title; item->subtitle = row.subtitle; item->enabled = row.disabled ? 0 : 1; if (row.disabled) return 1; int i = row.index; mr_library_entry *e = &g_lib.items[i]; item->right = row.is_book ? MEOW_LIST_RIGHT_NONE : MEOW_LIST_RIGHT_ARROW; meow_img *im = thumb_find(i); if (im) { item->icon_kind = MEOW_LIST_ICON_IMG; item->icon = im; } else { item->icon_kind = row.is_book ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER; /* 文件夹册用缩略图源图;档案册没有图片文件,用档案路径(worker 解压首图)。 */ const char *src = NULL; if (e->cover[0]) src = e->cover; else if (row.is_book && row.is_archive) src = e->path; if (src) thumb_enqueue(i, src); } if (e->probe_state == MR_LIB_PENDING) probe_enqueue(i, e->path, e->is_archive); return 1; } /* ============================================================ */ /* 布局 / 列表重建 */ /* ============================================================ */ static int lib_top_y(void) { return meow_ui_statusbar_height() + MR_TOPAPPBAR_H; } static void hint_place(void) { g_hint_x = 0; g_hint_y = lib_top_y(); g_hint_w = meow_ui_width(); g_hint_h = MR_HINT_H; } static void list_rebuild(void) { if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; } if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; } g_list_x = 0; g_list_y = lib_top_y(); g_list_w = meow_ui_width(); g_list_h = meow_ui_height() - g_list_y; if (g_list_h < 1) g_list_h = 1; int rows = g_lib.count + (g_lib.truncated ? 1 : 0); g_lv = meow_listview_create(g_list_x, g_list_y, g_list_w, g_list_h, rows, lib_src, NULL, MEOW_LIST_SEL_NONE); if (!g_lv) return; meow_listview_set_item_renderer(g_lv, 2, 1, 1); meow_listview_set_item_tier(g_lv, 2); /* SDK 默认 container=0(扁平满幅+分隔线);显式开 MD3 容器模式: * 每行 surfaceContainer 圆角卡 + 行间空隙 + 卡内 tonal 选中。 */ meow_listview_set_container(g_lv, 1); meow_listview_set_empty_text(g_lv, "空目录(无可进入的子目录)"); g_scroll = meow_scroll_wrap(g_lv, 1); if (g_scroll) meow_scroll_set_snap(g_scroll, MR_DEFAULT_ROW_H); } static void update_topbar(void) { if (!g_tb) return; const char *title = "图库"; if (strcmp(g_cur_dir, "/") == 0) { title = "文件系统 /"; } else if (strcmp(g_cur_dir, g_root_dir) != 0) { const char *slash = strrchr(g_cur_dir, '/'); if (slash && slash[1]) title = slash + 1; } meow_topappbar_set_title(g_tb, title); /* 可一路向上到文件系统 '/':到 '/' 才隐藏返回(此时左缘返回=退出 App)。 */ meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, "/") != 0); } static void list_invalidate(void) { meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h); } static void hint_invalidate(void) { meow_ui_invalidate(g_hint_x, g_hint_y, g_hint_w, g_hint_h); } static void show_hint(const char *msg) { snprintf(g_hint, sizeof g_hint, "%s", msg ? msg : ""); g_hint_until = meow_ui_time_ms() + MR_HINT_MS; hint_place(); hint_invalidate(); } /* 顶部横幅绘制物:只在提示有效且本页 active 时输出(custom 绘制物无 owner 闸门,必须自守)。 */ static void library_draw_hint(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_LIBRARY) return; if (!g_hint[0]) return; const meow_theme *th = meow_theme_get(); int x = g_hint_x, y = g_hint_y, w = g_hint_w, h = g_hint_h; if (w < 40 || h < 20) return; meow_ui_fill_rect(x + 8, y + 4, w - 16, h - 8, th->surfaceContainerHighest); meow_ui_draw_rect(x + 8, y + 4, w - 16, h - 8, th->error); int ty = y + (h - meow_ui_text_height(th->font_body)) / 2; meow_ui_text(x + 20, ty, g_hint, th->font_body, th->error); } /* 目录切换:作废旧异步结果、释放内存缩略图、重建列表(新池强制重新取行)。 */ static void reset_async(void) { pthread_mutex_lock(&g_lock); g_gen++; g_pq_n = 0; g_pq_head = g_pq_tail = 0; g_treq_n = 0; g_treq_head = g_treq_tail = 0; g_pres_n = 0; for (int i = 0; i < g_tres_n; i++) meow_img_free(g_tres[i].img); g_tres_n = 0; g_probe_inflight[0] = '\0'; g_thumb_inflight[0] = '\0'; pthread_mutex_unlock(&g_lock); } /* 目录切换:成功 1;不可读/不存在 0(提示;首次加载时仍建空列表)。 */ static int load_dir(const char *dir) { /* dir 可能指向 g_lib 里的行路径(点击进入子目录):先拷到本地, * 避免切换后 mr_library_free 释放 g_lib,dir 变成悬垂指针。 */ char target[MR_PATH_MAX]; if (!dir || !dir[0] || strlen(dir) >= sizeof target) { show_hint("路径无效"); return 0; } snprintf(target, sizeof target, "%s", dir); mr_library next; memset(&next, 0, sizeof next); uint64_t t0 = meow_ui_time_ms(); int n = mr_library_scan_ex(&next, target, MR_LIB_MAX_DEPTH, cbz_archive_pages, NULL); uint64_t ms = meow_ui_time_ms() - t0; if (n < 0) { mr_library_free(&next); const char *slash = strrchr(target, '/'); const char *name = (slash && slash[1]) ? slash + 1 : target; char msg[200]; snprintf(msg, sizeof msg, "无法读取目录:%s", name); show_hint(msg); meow_log(MEOW_LOG_WARN, "library scan failed: %s", target); /* 启动首帧根目录不可读:仍建一个空列表,页面可交互(返回/回根/上溯)。 */ if (!g_lv) { mr_library_free(&g_lib); snprintf(g_cur_dir, sizeof g_cur_dir, "%s", target); reset_async(); thumbs_free(); list_rebuild(); update_topbar(); } return 0; } if (ms > 120) meow_log(MEOW_LOG_WARN, "library scan slow: %s %llums (%d rows)", target, (unsigned long long)ms, n); mr_library_free(&g_lib); g_lib = next; snprintf(g_cur_dir, sizeof g_cur_dir, "%s", target); reset_async(); thumbs_free(); list_rebuild(); update_topbar(); g_drag = 0; g_dragged = 0; if (g_ctrl.active) meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); return 1; } int library_at_root(void) { return strcmp(g_cur_dir, g_root_dir) == 0; } int library_go_up(void) { if (strcmp(g_cur_dir, "/") == 0) return 0; /* 到文件系统根:交给 app.c 退出 */ char parent[MR_PATH_MAX]; if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0; if (!load_dir(parent)) { /* 父目录不可读 → 提示并停留。特例:父目录是文件系统 "/" 时仍切到 "/" 的空列表, * 让下一次返回=退出 App(Android 沙箱可能读不了 "/",否则会卡在最后一个可读目录)。 */ if (strcmp(parent, "/") == 0) { mr_library_free(&g_lib); snprintf(g_cur_dir, sizeof g_cur_dir, "%s", parent); reset_async(); thumbs_free(); list_rebuild(); update_topbar(); if (g_ctrl.active) meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); } return 1; } return 1; } /* TopAppBar「TF卡」:回到书库根(已在根则回到列表顶部)。 */ static void library_go_tf_root(void) { if (strcmp(g_cur_dir, g_root_dir) == 0) { if (g_scroll) meow_scroll_stop(g_scroll); if (g_lv) meow_listview_set_scroll(g_lv, 0); list_invalidate(); return; } load_dir(g_root_dir); } /* 点击行:先确保懒判定完成(单层探测,通常已在缓存里),书=进阅读器,否则进目录。 */ static void library_open_entry(int idx) { if (idx < 0 || idx >= g_lib.count) return; mr_library_entry *e = &g_lib.items[idx]; if (e->probe_state == MR_LIB_PENDING) { mr_library_probe p; if (mr_library_probe_path(e->path, e->is_archive, cbz_archive_pages, NULL, &p) == 0) mr_library_apply_probe(&g_lib, idx, &p); else mr_library_mark_error(&g_lib, idx); list_invalidate(); } if (e->probe_state == MR_LIB_FAILED) { char msg[200]; snprintf(msg, sizeof msg, "无法读取:%s", e->name); show_hint(msg); return; } if (e->has_images == 1) { mr_reader_open_dir(e->path); return; } if (e->is_archive) { /* 档案册但没有图片页(坏档/空档):不导航 */ char msg[200]; snprintf(msg, sizeof msg, "无法阅读(档内没有图片页):%s", e->name); show_hint(msg); return; } load_dir(e->path); } /* ============================================================ */ /* 事件 */ /* ============================================================ */ void library_on_tap(int x, int y) { if (!g_ctrl.active) return; g_drag = 0; g_dragged = 0; if (g_tb) { int hit = meow_topappbar_hit(g_tb, x, y); if (hit == MEOW_TOPBAR_BACK) { library_go_up(); return; } if (g_tb_home_idx >= 0 && hit == g_tb_home_idx) { library_go_tf_root(); return; } if (hit >= 0) { meow_view_page_push(ACT_SETTINGS); return; } } if (!g_lv) return; int idx = meow_listview_hit(g_lv, x, y); if (idx < 0) return; mr_library_row row; if (!mr_library_row_at(&g_lib, idx, &row) || row.disabled) return; library_open_entry(row.index); } void library_on_swipe(int dir, int dist) { if (!g_ctrl.active || !g_lv) return; if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; if (g_dragged) { /* 本次手势已实时跟手滚动,抬手只补 fling */ g_dragged = 0; g_drag = 0; if (g_scroll) meow_scroll_fling(g_scroll); return; } int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; if (del == 0) del = (dir == MEOW_SWIPE_UP) ? 120 : -120; if (g_scroll) { meow_scroll_track(g_scroll, del, meow_ui_time_ms()); meow_scroll_fling(g_scroll); } else { meow_listview_scroll_by(g_lv, del); } list_invalidate(); } void library_on_touch_down(int x, int y) { if (!g_ctrl.active || !g_lv) return; /* 屏底 28px 是系统「上滑回桌面」热区:不从这里启动列表拖拽,避免拦截系统手势。 */ if (y >= meow_ui_height() - 28) return; if (x < g_list_x || x >= g_list_x + g_list_w || y < g_list_y || y >= g_list_y + g_list_h) return; g_drag = 1; g_dragged = 0; g_drag_last_y = y; } void library_on_touch_move(int x, int y) { (void)x; if (!g_ctrl.active || !g_drag || !g_lv) return; int dy = g_drag_last_y - y; if (dy == 0) return; if (dy > 1 || dy < -1) g_dragged = 1; g_drag_last_y = y; if (g_scroll) meow_scroll_track(g_scroll, dy, meow_ui_time_ms()); else meow_listview_scroll_by(g_lv, dy); list_invalidate(); } /* 16ms tick:收下后台探测/缩略图结果并局部补位;提示横幅到期自动清除。 */ void library_on_tick(void) { if (!g_ctrl.active) return; if (g_hint[0] && meow_ui_time_ms() >= g_hint_until) { g_hint[0] = '\0'; hint_invalidate(); } probe_res pres[MR_PRES_MAX]; int pn = 0; pthread_mutex_lock(&g_lock); pn = g_pres_n; for (int i = 0; i < pn; i++) pres[i] = g_pres[i]; g_pres_n = 0; pthread_mutex_unlock(&g_lock); int changed = 0; for (int i = 0; i < pn; i++) { probe_res *r = &pres[i]; if (r->gen != g_gen || r->idx < 0 || r->idx >= g_lib.count) continue; mr_library_entry *e = &g_lib.items[r->idx]; if (strcmp(e->path, r->path) != 0) continue; if (r->ok) { if (mr_library_apply_probe(&g_lib, r->idx, &r->probe) && e->cover[0]) thumb_enqueue(r->idx, e->cover); } else { mr_library_mark_error(&g_lib, r->idx); } changed = 1; } thumb_res got[MR_TRES_MAX]; int n = 0; pthread_mutex_lock(&g_lock); n = g_tres_n; for (int i = 0; i < n; i++) got[i] = g_tres[i]; g_tres_n = 0; pthread_mutex_unlock(&g_lock); for (int i = 0; i < n; i++) { if (got[i].gen == g_gen && got[i].idx >= 0 && got[i].idx < g_lib.count && !thumb_find(got[i].idx) && thumb_put(got[i].idx, got[i].img)) changed = 1; else meow_img_free(got[i].img); } if (changed) list_invalidate(); } /* ============================================================ */ /* view 生命周期 */ /* ============================================================ */ static void library_page_create(meow_view *v, void *ctx) { mr_page_ctrl *c = (mr_page_ctrl *)ctx; c->view = v; cache_init(); worker_start(); int w = meow_ui_width(); int sb = meow_ui_statusbar_height(); g_tb = meow_topappbar_create(0, sb, w, MR_TOPAPPBAR_H, "图库"); if (g_tb) { g_tb_home_idx = meow_topappbar_add_action(g_tb, "TF卡"); int ai = meow_topappbar_add_action(g_tb, ""); if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_SETTINGS); } hint_place(); g_hint_id = meow_draw_custom_register(MEOW_Z_TOP, g_hint_x, g_hint_y, g_hint_w, g_hint_h, library_draw_hint, NULL); load_dir(g_root_dir); } static void library_page_enter(meow_view *v, void *ctx) { ((mr_page_ctrl *)ctx)->active = 1; v->x = 0; v->y = 0; v->w = meow_ui_width(); v->h = meow_ui_height(); meow_ui_set_fullscreen(0); /* 图库保留状态栏(reader 退出时若未复原也在此兜底) */ meow_ui_set_page_bg(meow_theme_get()->bg); int sb = meow_ui_statusbar_height(); if (g_tb) meow_widget_set_rect(g_tb, 0, sb, meow_ui_width(), MR_TOPAPPBAR_H); if (g_lv) { int y = sb + MR_TOPAPPBAR_H; int h = meow_ui_height() - y; if (h < 1) h = 1; meow_widget_set_rect(g_lv, 0, y, meow_ui_width(), h); g_list_y = y; g_list_w = meow_ui_width(); g_list_h = h; } hint_place(); g_drag = 0; g_dragged = 0; update_topbar(); mr_nav_report(); meow_ui_paint_full(); } static void library_page_leave(meow_view *v, void *ctx) { (void)v; ((mr_page_ctrl *)ctx)->active = 0; g_drag = 0; g_dragged = 0; if (g_scroll) meow_scroll_stop(g_scroll); } static void library_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; worker_stop(); if (g_hint_id) { meow_draw_unregister(g_hint_id); g_hint_id = 0; } if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; } if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } thumbs_free(); mr_library_free(&g_lib); mr_library_probe_cache_clear(); } meow_view *library_view_create(void) { static const meow_view_hooks h = { .on_create = library_page_create, .on_enter = library_page_enter, .on_leave = library_page_leave, .on_destroy = library_page_destroy, }; return meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &h, &g_ctrl); } #ifdef MR_LIBRARY_UI_TEST_HOOKS /* 宿主集成测试接缝(只在 -DMR_LIBRARY_UI_TEST_HOOKS 编译时存在;不进交付二进制)。 */ int library_test_load(const char *dir) { return load_dir(dir); } void library_test_set_root(const char *root) { if (root && root[0]) snprintf(g_root_dir, sizeof g_root_dir, "%s", root); } const char *library_test_root(void) { return g_root_dir; } const char *library_test_dir(void) { return g_cur_dir; } void library_test_set_dir(const char *dir) { if (dir && dir[0]) snprintf(g_cur_dir, sizeof g_cur_dir, "%s", dir); } int library_test_rows(void) { return g_lib.count + (g_lib.truncated ? 1 : 0); } int library_test_src(int idx, meow_list_item *item) { return lib_src(idx, item, NULL); } int library_test_hint_active(void) { return g_hint[0] != '\0'; } const char *library_test_hint(void) { return g_hint; } int library_test_go_root(void) { library_go_tf_root(); return strcmp(g_cur_dir, g_root_dir) == 0; } int library_test_go_up(void) { return library_go_up(); } #endif