feat(tuku): CBZ 档案册集成——档案页源/书库列册/解压缓存

- logic/reader/archive_source.c:档案册页源实现 M6 mr_book 契约
  (mz_open/mz_count/mz_entry_at/mz_read_alloc),条目自然序 + 图片后缀 +
  路径安全(`..`/绝对路径/反斜杠/隐藏段/内嵌 NUL)+ 单页尺寸上限过滤;
  meta 按需解压只读图片头(不落缓存,结果按页记忆);mr_book_open_auto
  统一目录册/档案册开册,翻页与条漫两套消费代码零改动。
- logic/reader/page_cache.c/h:解压页落 MeowData/<pkg>/cache,LRU 64 页或
  32MiB,启动清 mrpage_* 孤儿,写入 .part + rename;与缩略图缓存同目录、
  前缀隔离;互斥量保护,解码/缩略图线程可并发。
- logic/library.c/h:扫描直接子文件里的 .cbz/.zip 成册(行=文件名+页数,
  点入直接阅读);档案探测由调用方注入(logic 不依赖 zip 容器),不注入时
  与 M7 行为逐字一致;册间衔接兄弟表=含图目录∪含页档案册,同列自然序。
  条目构造/副标题/角标/兄弟判据拆成小函数,便于 M13 书库重写拼接。
- ui/library:档案册行 + 封面缩略图(解压档内首图)、扫描注入探测、缓存
  目录统一由 page_cache 推导(一份规则)。
- ui/reader:open_book 走 mr_book_open_auto;跨册衔接纳入档案册。
- build_tuku.sh 补 zip/archive/cache 源与 sysroot zlib 头(zip.c include
  <zlib.h>);run_tests.sh 纳入 zip+cbz 用例;run_webtoon_tests.sh 的宿主
  集成构建补新源与 -lz;.gitignore 同步测试产物。
- test/test_cbz.c(267 项断言)+ make_cbz_samples.py 真实 cbz 夹具:
  列档/自然序/解压字节 CRC/路径安全/坏档与 zip 炸弹容错/LRU 与 32MiB 上限/
  启动清孤儿/续读角标/封面首图。
This commit is contained in:
AskaEth committed 2026-11-05 11:58:12 +08:00
1 parent 19620a7957
commit 562cc44931
16 files changed
+2330 -108

No files matched your search

+37 -23
View File
@@ -1,21 +1,24 @@
/* ui/library/library.c — 图库根页(ACT_LIBRARY):
*
* TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。
* 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册)。
* 行=缩略图 + 目录名 + 页数;目录由 logic/library 扫描并自然排序。
* 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册);
* M11 起 .cbz/.zip 也作为「册」与文件夹并列(行=封面+文件名+页数,点入直接阅读)。
* 行数据由 logic/library 扫描并自然排序;档案册页数/首图由 logic/reader 的 zip 页源提供。
*
* 流畅度(用户指令优先):
* - 首帧只绘文字行:bind 不读盘;缩略图请求投递给后台线程(pthread)异步解码,
* 完成后在 on_tick 补位并只 invalidate 列表视口;
* - 缩略图 meow_img_downscale 到 96px,并落磁盘缓存 MeowData/<pkg>/cache
* (BMP 原始缩略图,文件名含源文件 mtime+size,源文件变化自动失效;≥64 张按
* mtime LRU 删除);
* (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 <dirent.h>
#include <errno.h>
@@ -106,20 +109,10 @@ static void cache_init(void)
const char *data = meow_config_dir();
if (!data || !data[0]) return;
int n = snprintf(g_cache_dir, sizeof g_cache_dir, "%s", data);
if (n <= 0 || n >= (int)sizeof g_cache_dir) { g_cache_dir[0] = '\0'; return; }
char *slash = strrchr(g_cache_dir, '/');
if (slash && strcmp(slash, "/data") == 0) *slash = '\0';
size_t len = strlen(g_cache_dir);
n = snprintf(g_cache_dir + len, sizeof g_cache_dir - len, "/cache");
if (n <= 0 || n >= (int)(sizeof g_cache_dir - len)) { g_cache_dir[0] = '\0'; return; }
if (mkdir(g_cache_dir, 0755) != 0 && errno != EEXIST) {
g_cache_dir[0] = '\0';
return;
}
/* 缩略图缓存与解压页缓存同目录(MeowData/<pkg>/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;
}
@@ -220,12 +213,21 @@ static int write_bmp(const char *path, const meow_img *im)
return ok ? 0 : -1;
}
/* 后台线程:缓存命中直接读;否则解码源图 → downscale → 落缓存。 */
/* 后台线程:缓存命中直接读;否则解码源图 → 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);
@@ -239,7 +241,7 @@ static meow_img *thumb_load(const char *cover)
}
}
meow_img *im = meow_img_load_file(cover);
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; }
@@ -380,7 +382,11 @@ static void lib_bind(int idx, meow_lv_item *item, void *ctx)
item->icon = im;
} else {
item->icon_kind = e->has_images ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER;
if (e->cover[0]) thumb_enqueue(idx, e->cover);
/* 文件夹册用缩略图源图;档案册没有图片文件,用档案路径(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);
}
}
@@ -411,7 +417,7 @@ static void list_rebuild(void)
/* SDK 默认 container=0(扁平满幅+分隔线);显式开 MD3 容器模式:
* 每行 surfaceContainer 圆角卡 + 行间空隙 + 卡内 tonal 选中。 */
meow_listview_set_container(g_lv, 1);
meow_listview_set_empty_text(g_lv, "No folders with PNG/JPEG/BMP");
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);
@@ -446,12 +452,20 @@ static void reset_async(void)
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(&g_lib, g_cur_dir, MR_LIB_MAX_DEPTH) < 0) {
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);
}