Files
MeowOS-SDK/tuku/ui/library/library.c
T
AskaEth 9f34417c49 图库: 单层浏览 + 懒判定与 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
2026-11-05 12:23:31 +08:00

1006 lines
33 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 档案册,与目录自然序混排);
* - 系统目录/空目录/无图目录都列出且可进入;可一路向上到文件系统 '/',
* 左缘返回由 app.c 逐级上溯、到 '/' 才退出 App;
* - 「直接含图目录=一本书」与档案册页数改为后台懒算:行仍在 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_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) {
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); /* 损坏缓存 */
}
}
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; }
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);
/* 可一路向上到文件系统 '/':到 '/' 才隐藏返回(此时左缘返回=退出 App)。 */
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", 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);
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)
{
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 (!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);
}
/* ============================================================ */
/* 事件 */
/* ============================================================ */
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_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) 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;
/* 屏底 28px 是系统「上滑回桌面」热区:不从这里启动列表拖拽,避免拦截系统手势。 */
if (y >= meow_ui_height() - 28) 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;
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);
}
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_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