/* ui/library/library.c — 图库根页(ACT_LIBRARY): * * TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。 * 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册); * M11 起 .cbz/.zip 也作为「册」与文件夹并列(行=封面+文件名+页数,点入直接阅读)。 * 行数据由 logic/library 扫描并自然排序;档案册页数/首图由 logic/reader 的 zip 页源提供。 * * 流畅度(用户指令优先): * - 首帧只绘文字行:bind 不读盘;缩略图请求投递给后台线程(pthread)异步解码, * 完成后在 on_tick 补位并只 invalidate 列表视口; * - 缩略图 meow_img_downscale 到 96px,并落磁盘缓存 MeowData//cache * (BMP 原始缩略图,文件名含源 mtime+size,源文件变化自动失效;≥64 张按 * mtime LRU 删除);档案册封面=档内首图,先解压到同一 cache(page_cache 管 LRU); * - 列表滚动用 meow_scroll_wrap:跟手 track + 松手 fling + 行高 snap。 * * 绘制/颜色:SDK 注册表非保留绘制,全部取 meow_theme_get() 语义色;主题切换由 app.c 的 * listener 整帧 invalidate。缩略图缓存只在列表重建时释放(避免池内悬垂指针),上限 24 张。 */ #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_REQ_MAX 64 #define MR_RES_MAX 16 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; static mr_page_ctrl g_ctrl; static mr_library g_lib; static char g_cur_dir[MR_PATH_MAX] = MR_ROOT_DIR; 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 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 thumb_req g_req[MR_REQ_MAX]; static int g_req_head, g_req_tail, g_req_n; static char g_inflight[MR_PATH_MAX]; static thumb_res g_res[MR_RES_MAX]; static int g_res_n; /* ---- 磁盘缩略图缓存 ---- */ static char g_cache_dir[MR_PATH_MAX]; static int g_cache_on; static int g_drag; static int g_dragged; static int g_drag_last_y; /* ============================================================ */ /* 磁盘缓存(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 *thumb_worker(void *arg) { (void)arg; for (;;) { thumb_req req; pthread_mutex_lock(&g_lock); while (!g_worker_stop && g_req_n == 0) pthread_cond_wait(&g_cv, &g_lock); if (g_worker_stop) { pthread_mutex_unlock(&g_lock); break; } req = g_req[g_req_head]; g_req_head = (g_req_head + 1) % MR_REQ_MAX; g_req_n--; snprintf(g_inflight, sizeof g_inflight, "%s", req.cover); pthread_mutex_unlock(&g_lock); meow_img *im = thumb_load(req.cover); pthread_mutex_lock(&g_lock); g_inflight[0] = '\0'; if (im && !g_worker_stop && g_res_n < MR_RES_MAX) { g_res[g_res_n].gen = req.gen; g_res[g_res_n].idx = req.idx; g_res[g_res_n].img = im; g_res_n++; } 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, thumb_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_res_n; i++) meow_img_free(g_res[i].img); g_res_n = 0; g_req_n = 0; g_req_head = g_req_tail = 0; 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_inflight, cover) == 0) { pthread_mutex_unlock(&g_lock); return; } for (int i = 0; i < g_req_n; i++) { int p = (g_req_head + i) % MR_REQ_MAX; if (strcmp(g_req[p].cover, cover) == 0) { pthread_mutex_unlock(&g_lock); return; } } if (g_req_n >= MR_REQ_MAX) { pthread_mutex_unlock(&g_lock); return; } g_req[g_req_tail].gen = g_gen; g_req[g_req_tail].idx = idx; snprintf(g_req[g_req_tail].cover, sizeof g_req[g_req_tail].cover, "%s", cover); g_req_tail = (g_req_tail + 1) % MR_REQ_MAX; g_req_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; } /* ============================================================ */ /* 列表虚拟化数据源 */ /* ============================================================ */ static int lib_count(void *ctx) { (void)ctx; return g_lib.count; } static void lib_bind(int idx, meow_lv_item *item, void *ctx) { (void)ctx; memset(item, 0, sizeof *item); if (idx < 0 || idx >= g_lib.count) return; mr_library_entry *e = &g_lib.items[idx]; item->title = e->name; item->subtitle = e->subtitle; item->right = MEOW_LIST_RIGHT_ARROW; item->enabled = 1; item->user = e; meow_img *im = thumb_find(idx); if (im) { item->icon_kind = MEOW_LIST_ICON_IMG; item->icon = im; } else { item->icon_kind = e->has_images ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER; /* 文件夹册用缩略图源图;档案册没有图片文件,用档案路径(worker 解压首图)。 */ const char *src = NULL; if (e->cover[0]) src = e->cover; else if (e->has_images && mr_path_is_archive(e->path)) src = e->path; if (src) thumb_enqueue(idx, src); } } /* ============================================================ */ /* 布局 / 列表重建 */ /* ============================================================ */ static int lib_top_y(void) { return meow_ui_statusbar_height() + MR_TOPAPPBAR_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; g_lv = meow_listview_create(g_list_x, g_list_y, g_list_w, g_list_h, 0, NULL, NULL, MEOW_LIST_SEL_NONE); if (!g_lv) return; meow_listview_set_vsource(g_lv, lib_count, lib_bind, NULL); 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, "No folders or CBZ books"); 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, MR_ROOT_DIR) != 0) { const char *slash = strrchr(g_cur_dir, '/'); if (slash && slash[1]) title = slash + 1; } meow_topappbar_set_title(g_tb, title); meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, MR_ROOT_DIR) != 0); } static void list_invalidate(void) { meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h); } /* 目录切换:作废旧异步结果、释放内存缩略图、重新扫描、重建列表(新池强制重新 bind)。 */ static void reset_async(void) { pthread_mutex_lock(&g_lock); g_gen++; g_req_n = 0; g_req_head = g_req_tail = 0; for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img); g_res_n = 0; pthread_mutex_unlock(&g_lock); } /* 书库扫描的档案册探测(M11 注入):logic/library 不依赖 zip 容器。 */ static int cbz_archive_pages(const char *path, void *ud) { (void)ud; return mr_zip_count_pages(path); } static void load_dir(const char *dir) { snprintf(g_cur_dir, sizeof g_cur_dir, "%s", dir); reset_async(); thumbs_free(); if (mr_library_scan_ex(&g_lib, g_cur_dir, MR_LIB_MAX_DEPTH, cbz_archive_pages, NULL) < 0) { mr_library_free(&g_lib); meow_log(MEOW_LOG_WARN, "library scan failed: %s", g_cur_dir); } 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()); } int library_at_root(void) { return strcmp(g_cur_dir, MR_ROOT_DIR) == 0; } int library_go_up(void) { if (library_at_root()) return 0; char parent[MR_PATH_MAX]; if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0; /* 不越过书库根(父级仍须在根目录之下或等于根)。 */ if (strcmp(parent, MR_ROOT_DIR) != 0 && strncmp(parent, MR_ROOT_DIR "/", sizeof(MR_ROOT_DIR)) != 0) snprintf(parent, sizeof parent, "%s", MR_ROOT_DIR); load_dir(parent); return 1; } /* ============================================================ */ /* 事件 */ /* ============================================================ */ 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 (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 || idx >= g_lib.count) return; mr_library_entry *e = &g_lib.items[idx]; if (e->has_images) mr_reader_open_dir(e->path); else load_dir(e->path); } 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; thumb_res got[MR_RES_MAX]; int n = 0; pthread_mutex_lock(&g_lock); n = g_res_n; for (int i = 0; i < n; i++) got[i] = g_res[i]; g_res_n = 0; pthread_mutex_unlock(&g_lock); int changed = 0; 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) { int ai = meow_topappbar_add_action(g_tb, ""); if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_SETTINGS); } load_dir(MR_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; } 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_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); } 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); }