图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)

rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
This commit is contained in:
AskaEth committed 2026-11-05 12:23:31 +08:00
1 parent c9cff99af3
commit 9f34417c49
12 files changed
+2299 -358

No files matched your search

+458 -124
View File
@@ -1,20 +1,27 @@
/* ui/library/library.c — 图库根页(ACT_LIBRARY):
/* ui/library/library.c — 图库浏览页(ACT_LIBRARY):
*
* TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。
* 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册);
* M11 起 .cbz/.zip 也作为「册」与文件夹并列(行=封面+文件名+页数,点入直接阅读)。
* 行数据由 logic/library 扫描并自然排序;档案册页数/首图由 logic/reader 的 zip 页源提供。
* TopAppBar(标题 + 返回 + 「TF卡」回根 + ⚙)+ meow_listview(src2 行数据源)。
*
* 流畅度(用户指令优先):
* - 首帧只绘文字行:bind 不读盘;缩略图请求投递给后台线程(pthread)异步解码,
* 完成后在 on_tick 补位并只 invalidate 列表视口;
* - 缩略图 meow_img_downscale 到 96px,并落磁盘缓存 MeowData/<pkg>/cache
* (BMP 原始缩略图,文件名含源 mtime+size,源文件变化自动失效;≥64 张按
* mtime LRU 删除);档案册封面=档内首图,先解压到同一 cache(page_cache 管 LRU);
* - 列表滚动用 meow_scroll_wrap:跟手 track + 松手 fling + 行高 snap。
* 浏览语义(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。
*
* 绘制/颜色:SDK 注册表非保留绘制,全部取 meow_theme_get() 语义色;主题切换由 app.c 的
* listener 整帧 invalidate。缩略图缓存只在列表重建时释放(避免池内悬垂指针),上限 24 张。
* 行绑定(列表 src2 模式):meow_listview_create(..., rows, lib_src, ...) 每帧/每次
* invalidate 对可见行重新取行;lib_src 只读内存里的 g_lib + 缩略图缓存,并把「待探测/
* 待缩略图」请求投递到有界队列(不做 IO),因此懒算结果补位后下一帧即可上屏。
*
* 缩略图:后台线程 meow_img_downscale 到 96px,落磁盘缓存 MeowData/<pkg>/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"
@@ -37,9 +44,13 @@
#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_REQ_MAX 64
#define MR_RES_MAX 16
#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;
@@ -53,14 +64,32 @@ typedef struct thumb_res {
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;
@@ -68,7 +97,7 @@ static struct {
} 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;
@@ -76,21 +105,41 @@ 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 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 thumb_res g_res[MR_RES_MAX];
static int g_res_n;
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 做失效) */
/* ============================================================ */
@@ -137,10 +186,6 @@ static void cache_prune(void)
time_t oldest_t = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
/* 解压页缓存(M11)与缩略图同目录:mrpage_* 归 page_cache 自己的
* 64 页/32MiB LRU 管,这里绝不能把它们当缩略图统计或删除——否则
* 档案页会被缩略图 LRU 清掉(P2-1)。判据与 page_cache 共用。 */
if (mr_cache_is_page_name(de->d_name)) continue;
const char *dot = strrchr(de->d_name, '.');
if (!dot || strcmp(dot, ".bmp") != 0) continue;
char full[MR_PATH_MAX + 96];
@@ -256,34 +301,71 @@ static meow_img *thumb_load(const char *cover)
return im;
}
static void *thumb_worker(void *arg)
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 (;;) {
thumb_req req;
int kind = 0; /* 1=探测 2=缩略图 */
probe_job pj;
thumb_req tr;
pthread_mutex_lock(&g_lock);
while (!g_worker_stop && g_req_n == 0)
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; }
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);
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;
}
@@ -292,7 +374,7 @@ 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;
if (pthread_create(&g_worker, NULL, lib_worker, NULL) == 0) g_worker_on = 1;
}
static void worker_stop(void)
@@ -306,30 +388,57 @@ static void worker_stop(void)
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;
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);
}
/* 请求缩略图:bind 只投递,不阻塞首绘/滚动。 */
/* 探测请求:只投递(有界队列 + 去重),不做 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_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 (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_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++;
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);
}
@@ -360,38 +469,38 @@ static int thumb_put(int idx, meow_img *im)
}
/* ============================================================ */
/* 列表虚拟化数据源 */
/* 行数据源(src2:每帧按需取可见行;零 IO,懒算请求只入队) */
/* ============================================================ */
static int lib_count(void *ctx)
{
(void)ctx;
return g_lib.count;
}
static void lib_bind(int idx, meow_lv_item *item, void *ctx)
static int lib_src(int idx, meow_list_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);
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 = e->has_images ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER;
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 (e->has_images && mr_path_is_archive(e->path)) src = e->path;
if (src) thumb_enqueue(idx, src);
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;
}
/* ============================================================ */
@@ -402,6 +511,14 @@ 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; }
@@ -412,16 +529,16 @@ static void list_rebuild(void)
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);
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_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");
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);
@@ -431,12 +548,15 @@ static void update_topbar(void)
{
if (!g_tb) return;
const char *title = "图库";
if (strcmp(g_cur_dir, MR_ROOT_DIR) != 0) {
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);
meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, MR_ROOT_DIR) != 0);
/* 可一路向上到文件系统 '/':到 '/' 才隐藏返回(此时左缘返回=退出 App)。 */
meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, "/") != 0);
}
static void list_invalidate(void)
@@ -444,61 +564,179 @@ static void list_invalidate(void)
meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h);
}
/* 目录切换:作废旧异步结果、释放内存缩略图、重新扫描、重建列表(新池强制重新 bind)。 */
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_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;
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);
}
/* 书库扫描的档案册探测(M11 注入):logic/library 不依赖 zip 容器。 */
static int cbz_archive_pages(const char *path, void *ud)
/* 目录切换:成功 1;不可读/不存在 0(提示;首次加载时仍建空列表)。 */
static int load_dir(const char *dir)
{
(void)ud;
return mr_zip_count_pages(path);
}
/* 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);
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());
return 1;
}
int library_at_root(void)
{
return strcmp(g_cur_dir, MR_ROOT_DIR) == 0;
return strcmp(g_cur_dir, g_root_dir) == 0;
}
int library_go_up(void)
{
if (library_at_root()) return 0;
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 (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);
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);
}
/* ============================================================ */
/* 事件 */
/* ============================================================ */
@@ -511,14 +749,15 @@ void library_on_tap(int x, int y)
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 || 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);
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)
@@ -567,19 +806,47 @@ void library_on_touch_move(int x, int y)
list_invalidate();
}
/* 16ms tick:收下后台解码好的缩略图并局部补位。 */
/* 16ms tick:收下后台探测/缩略图结果并局部补位;提示横幅到期自动清除。 */
void library_on_tick(void)
{
if (!g_ctrl.active) return;
thumb_res got[MR_RES_MAX];
int n = 0;
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);
n = g_res_n;
for (int i = 0; i < n; i++) got[i] = g_res[i];
g_res_n = 0;
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))
@@ -605,10 +872,15 @@ static void library_page_create(meow_view *v, void *ctx)
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);
}
load_dir(MR_ROOT_DIR);
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)
@@ -632,6 +904,7 @@ static void library_page_enter(meow_view *v, void *ctx)
g_list_w = meow_ui_width();
g_list_h = h;
}
hint_place();
g_drag = 0;
g_dragged = 0;
update_topbar();
@@ -652,11 +925,13 @@ 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)
@@ -669,3 +944,62 @@ meow_view *library_view_create(void)
};
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