Files
MeowOS-SDK/tuku/ui/library/library.c
T
AskaEth 3cbcaaaac1 图库: 修系统返回语义与打开册资源路径(M15)
- 返回手势:书库列表排除左缘 24px 系统返回带并清空残留拖拽态;library_go_up
  到 TF 根返回 0(app.c 退出 App),不再上溯系统目录;顶栏返回改为显式
  library_go_parent(访问放开,可越过 TF 根到 /)。lock 页继续自行消费返回。
- 打开册:缩略图源图改用预算解码(meow_img_load_file_dim,峰值数十 MB→~1MB);
  离开书库页停止后台探测/缩略图线程,避免与 open_book 争内存/FD;打开失败按
  errno 记日志,ENOMEM/EMFILE/ENFILE/EAGAIN/EINTR 自动重试一次;修 open_book
  自拷贝 snprintf 的未定义行为。
- 测试:test_browse_ui_host 增加 TF 根退出/顶栏上级/左缘不拖拽断言;新增
  test_open_book_host(封面图+含图子目录的父目录 → reader 打开的完整 UI 链路)。
2026-11-05 13:03:20 +08:00

1078 lines
36 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ui/library/library.c — 图库浏览页(ACT_LIBRARY):
*
* TopAppBar(标题 + 返回 + 「TF卡」回根 + ⚙)+ meow_listview(src2 行数据源)。
*
* 浏览语义(M13,用户要求:性能 + 访问放开):
* - 首帧只做**单层**枚举(logic/library.c:一次 opendir/readdir+lstat),
* 不做任何递归「子树含图」探测;列表行 = 当前目录的直接子目录(M11 起还包括
* .cbz/.zip 档案册,与目录自然序混排);
* - 系统目录/空目录/无图目录都列出且可进入;左缘返回按系统语义逐级上移,
* **到 TF 根即退出 App**(不再上溯系统目录);TF 根以外的上级浏览走顶栏「上级」动作;
* - 「直接含图目录=一本书」与档案册页数改为后台懒算:行仍在 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/<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"
#include "logic/reader/page_cache.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define MR_THUMB_MAX 24
#define MR_THUMB_DIM 96
#define MR_THUMB_DECODE_DIM 256 /* 缩略图源图预算解码边长(峰值 ~1MB;整幅解码可达数十 MB) */
#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/<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;
}
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) {
/* 解压页缓存(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];
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); /* 损坏缓存 */
}
}
/* 预算解码:封面可能是 12MP 手机照片,整幅解码峰值 ~48MB,会在低内存设备上
* 挤掉随后 reader 打开所需的 opendir/malloc(用户报告「点开父文件夹=无法打开此册」)。
* 缩略图最终只要 96px,先解到 ≤MR_THUMB_DECODE_DIM 再精修,视觉无损、峰值 ~1MB。 */
meow_img *im = meow_img_load_file_dim(src, MR_THUMB_DECODE_DIM);
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);
/* 顶栏「上级」是显式浏览动作(访问放开,可越过 TF 根上溯到 '/');
* 左缘返回是系统返回语义,TF 根=退出 App(见 library_go_up)。到 '/' 才隐藏。 */
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 (errno=%d %s)", target,
errno, strerror(errno));
/* 启动首帧根目录不可读:仍建一个空列表,页面可交互(返回/回根/上溯)。 */
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)
{
/* 系统返回语义:TF 根 = 退出 App(app.c 收到 0 后 request_exit),
* 不再上溯系统目录;文件系统 "/" 同理。 */
if (strcmp(g_cur_dir, g_root_dir) == 0) return 0;
if (strcmp(g_cur_dir, "/") == 0) return 0;
return library_go_parent();
}
/* 顶栏「上级」:显式浏览动作,访问放开——可越过 TF 根一路到 '/'。 */
int library_go_parent(void)
{
if (strcmp(g_cur_dir, "/") == 0) return 0;
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 {
meow_log(MEOW_LOG_WARN, "library probe failed: %s (errno=%d %s)", e->path,
errno, strerror(errno));
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_parent(); 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;
/* 每次按下都作废上一次手势的拖拽状态:热区/左缘提前返回时不能残留 g_drag,
* 否则系统返回手势的 move 仍会驱动列表滚动。 */
g_drag = 0;
g_dragged = 0;
/* 屏底 28px 是系统「上滑回桌面」热区:不从这里启动列表拖拽,避免拦截系统手势。 */
if (y >= meow_ui_height() - 28) return;
/* 左缘 24px 是系统返回手势带(与 reader 同口径):列表不启动拖拽,把滑动留给系统返回。 */
if (x < 24) 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;
worker_start(); /* 离开时停止(见 leave),回页恢复探测/缩略图 */
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);
/* 停止后台解码:进 reader 前释放/停止潜在的大图解码,避免与 open_book 的
* opendir/malloc 抢内存(设备上「点开父文件夹=无法打开此册」的诱因之一)。
* worker_stop 会 join,保证 page_push 的 on_enter 打开册时线程已退出。 */
worker_stop();
}
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_has_list(void)
{
return g_lv != NULL;
}
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();
}
int library_test_go_parent(void)
{
return library_go_parent();
}
/* 跟手拖拽是否在本次手势中被启动(左缘/屏底热区排除的回归观测口)。 */
int library_test_drag_active(void)
{
return g_drag;
}
/* 后台探测/缩略图线程是否在跑(离开书库页应停止,回页恢复)。 */
int library_test_worker_on(void)
{
return g_worker_on;
}
/* 缩略图 LRU 接缝(M11 P2-1 回归):指向当前缓存目录并同步跑一次 prune。 */
const char *library_test_cache_dir(void)
{
return g_cache_dir;
}
int library_test_cache_on(void)
{
return g_cache_on;
}
void library_test_cache_prune(void)
{
cache_prune();
}
#endif