- 返回手势:书库列表排除左缘 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 链路)。
1078 lines
36 KiB
C
1078 lines
36 KiB
C
/* 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
|