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);
}
+25 -11
View File
@@ -39,6 +39,7 @@
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "logic/reader/page_source.h"
#include "logic/reader/page_cache.h"
#include "logic/reader/nav.h"
#include <pthread.h>
@@ -74,7 +75,7 @@
static mr_page_ctrl g_ctrl;
static char g_dir[MR_PATH_MAX];
static mr_dir_book g_db;
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
static mr_book g_book;
static int g_count; /* 0=空册/不可读 */
static int g_open_err;
@@ -765,7 +766,7 @@ static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
int w = g_vw, h = g_vh;
if (g_open_err) {
meow_ui_text_center(w / 2, h / 2 - 20, "无法打开目录", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 - 20, "无法打开此册", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 + 24, g_dir, th->font_body, th->onSurfaceVariant);
return;
}
@@ -1152,8 +1153,7 @@ static void open_book_reset_state(void)
mr_strip_free(&g_strip);
g_strip_ok = 0;
slots_free_all();
mr_book_destroy(&g_book);
mr_dir_book_free(&g_db);
mr_book_close_auto(&g_book, &g_handle);
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
g_gen++; /* 作废旧册的异步结果 */
g_count = 0;
@@ -1183,10 +1183,11 @@ static int open_book(const char *dir, int start_page, int mode)
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
int maxdim = g_vw > g_vh ? g_vw : g_vh;
int n = mr_dir_book_init(&g_db, g_dir, maxdim,
reader_load_page, reader_unload_page, NULL);
/* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约,
* 下面的翻页/条漫消费代码完全不区分 provider。 */
int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim,
reader_load_page, reader_unload_page, NULL);
if (n < 0) {
mr_dir_book_bind(&g_book, NULL);
g_open_err = 1;
worker_start();
update_title();
@@ -1195,7 +1196,6 @@ static int open_book(const char *dir, int start_page, int mode)
return 0;
}
g_count = n;
mr_dir_book_bind(&g_book, g_count > 0 ? &g_db : NULL);
int page = start_page < 0 ? g_count - 1 : start_page;
page = mr_nav_clamp_page(page, g_count);
@@ -1233,12 +1233,20 @@ static int open_book(const char *dir, int start_page, int mode)
return 1;
}
/* 册间衔接的档案册探测(M11):兄弟表 = 含图目录 ∪ 含图片页的 .cbz/.zip。 */
static int cbz_pages_probe(const char *path, void *ud)
{
(void)ud;
return mr_zip_count_pages(path);
}
static int book_switch(int delta)
{
if (delta == 0) return 0;
if (g_count <= 0) return 0;
char next[MR_PATH_MAX];
int r = mr_book_adjacent(g_dir, delta > 0 ? 1 : -1, next, (int)sizeof next);
int r = mr_book_adjacent_ex(g_dir, delta > 0 ? 1 : -1, next, (int)sizeof next,
cbz_pages_probe, NULL);
if (r != 1) {
toast(delta > 0 ? "已是最后一册" : "已是第一册");
return 0;
@@ -1770,6 +1778,13 @@ static void reader_page_enter(meow_view *v, void *ctx)
}
if (g_tb) meow_widget_set_rect(g_tb, 0, 0, g_vw, MR_TOP_BAR_H);
/* 档案册解压缓存(M11):MeowData/<pkg>/cache。library 页通常已初始化,
* 这里幂等兜底(进程重启后直接进阅读器、或交付环境只剩 reader 入口)。 */
{
const char *data = meow_config_dir();
if (data && data[0]) mr_cache_init(data);
}
/* 打开册:恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
int page = mr_cont_get_page(g_dir, 0);
@@ -1801,8 +1816,7 @@ static void reader_page_leave(meow_view *v, void *ctx)
mr_strip_free(&g_strip);
g_strip_ok = 0;
slots_free_all();
mr_book_destroy(&g_book);
mr_dir_book_free(&g_db);
mr_book_close_auto(&g_book, &g_handle);
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }