P2-1(必修):解压页缓存与缩略图 LRU 在 *.bmp 上相互踩踏 - ui/library 的 cache_prune 先按 page_cache 的共用判据 mr_cache_is_page_name 跳过 mrpage_*,不再把 BMP 解压页算进缩略图额度或 unlink(含 Vol10.cbz 这类 BMP 档不再出现「被缩略图 LRU 删掉 → 首页解码失败」); - mr_cache_touch 命中时 stat 校验文件存在与大小,缺失/尺寸不符即移出索引并按 未命中返回,调用方重新解压——外部清理/掉电残留/误删都能自愈,不再返回死路径。 P2-2(文档化一次性溢出): - page_cache.h 与 make_room_locked 写明:单条目本身超过 32MiB 时只保留该条、 其它全部淘汰,占用上限受页源单页 64MiB(MR_ZIP_PAGE_MAX_BYTES)约束; 大页仍可读,不因预算拒绝落缓存。 - 新增 33MiB 页夹具(PNG 头 + 可压零填充,deflate 后仅数十 KB)与断言: 大页可读、cache_bytes 允许超 32MiB、小页被淘汰、再来小页时大页按预算淘汰。 顺手:page_cache.h 注释包名 os.meow.picobrowser → os.meow.picbrowser(测试路径同步)。 测试:test_cbz 267 → 296 项全 PASS;run_tests.sh(zip 106)与 run_webtoon_tests.sh 全绿;交叉构建 sdk/tuku 零警告,产物静态 MIPS32r2 ELF。
672 lines
22 KiB
C
672 lines
22 KiB
C
/* ui/library/library.c — 图库根页(ACT_LIBRARY):
|
||
*
|
||
* TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。
|
||
* 任意文件夹可浏览;无元数据但含 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 删除);档案册封面=档内首图,先解压到同一 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>
|
||
#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_REQ_MAX 64
|
||
#define MR_RES_MAX 16
|
||
|
||
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;
|
||
|
||
static mr_page_ctrl g_ctrl;
|
||
static mr_library g_lib;
|
||
static char g_cur_dir[MR_PATH_MAX] = MR_ROOT_DIR;
|
||
|
||
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 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 thumb_req g_req[MR_REQ_MAX];
|
||
static int g_req_head, g_req_tail, g_req_n;
|
||
static char g_inflight[MR_PATH_MAX];
|
||
|
||
static thumb_res g_res[MR_RES_MAX];
|
||
static int g_res_n;
|
||
|
||
/* ---- 磁盘缩略图缓存 ---- */
|
||
static char g_cache_dir[MR_PATH_MAX];
|
||
static int g_cache_on;
|
||
|
||
static int g_drag;
|
||
static int g_dragged;
|
||
static int g_drag_last_y;
|
||
|
||
/* ============================================================ */
|
||
/* 磁盘缓存(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); /* 损坏缓存 */
|
||
}
|
||
}
|
||
|
||
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 *thumb_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
for (;;) {
|
||
thumb_req req;
|
||
pthread_mutex_lock(&g_lock);
|
||
while (!g_worker_stop && g_req_n == 0)
|
||
pthread_cond_wait(&g_cv, &g_lock);
|
||
if (g_worker_stop) { pthread_mutex_unlock(&g_lock); break; }
|
||
req = g_req[g_req_head];
|
||
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
|
||
g_req_n--;
|
||
snprintf(g_inflight, sizeof g_inflight, "%s", req.cover);
|
||
pthread_mutex_unlock(&g_lock);
|
||
|
||
meow_img *im = thumb_load(req.cover);
|
||
|
||
pthread_mutex_lock(&g_lock);
|
||
g_inflight[0] = '\0';
|
||
if (im && !g_worker_stop && g_res_n < MR_RES_MAX) {
|
||
g_res[g_res_n].gen = req.gen;
|
||
g_res[g_res_n].idx = req.idx;
|
||
g_res[g_res_n].img = im;
|
||
g_res_n++;
|
||
} 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, thumb_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_res_n; i++) meow_img_free(g_res[i].img);
|
||
g_res_n = 0;
|
||
g_req_n = 0;
|
||
g_req_head = g_req_tail = 0;
|
||
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_inflight, cover) == 0) { pthread_mutex_unlock(&g_lock); return; }
|
||
for (int i = 0; i < g_req_n; i++) {
|
||
int p = (g_req_head + i) % MR_REQ_MAX;
|
||
if (strcmp(g_req[p].cover, cover) == 0) { pthread_mutex_unlock(&g_lock); return; }
|
||
}
|
||
if (g_req_n >= MR_REQ_MAX) { pthread_mutex_unlock(&g_lock); return; }
|
||
g_req[g_req_tail].gen = g_gen;
|
||
g_req[g_req_tail].idx = idx;
|
||
snprintf(g_req[g_req_tail].cover, sizeof g_req[g_req_tail].cover, "%s", cover);
|
||
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
|
||
g_req_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;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 列表虚拟化数据源 */
|
||
/* ============================================================ */
|
||
static int lib_count(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
return g_lib.count;
|
||
}
|
||
|
||
static void lib_bind(int idx, meow_lv_item *item, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
memset(item, 0, sizeof *item);
|
||
if (idx < 0 || idx >= g_lib.count) return;
|
||
mr_library_entry *e = &g_lib.items[idx];
|
||
item->title = e->name;
|
||
item->subtitle = e->subtitle;
|
||
item->right = MEOW_LIST_RIGHT_ARROW;
|
||
item->enabled = 1;
|
||
item->user = e;
|
||
|
||
meow_img *im = thumb_find(idx);
|
||
if (im) {
|
||
item->icon_kind = MEOW_LIST_ICON_IMG;
|
||
item->icon = im;
|
||
} else {
|
||
item->icon_kind = e->has_images ? MEOW_LIST_ICON_FILE : MEOW_LIST_ICON_FOLDER;
|
||
/* 文件夹册用缩略图源图;档案册没有图片文件,用档案路径(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);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局 / 列表重建 */
|
||
/* ============================================================ */
|
||
static int lib_top_y(void)
|
||
{
|
||
return meow_ui_statusbar_height() + MR_TOPAPPBAR_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;
|
||
|
||
g_lv = meow_listview_create(g_list_x, g_list_y, g_list_w, g_list_h, 0, NULL, NULL,
|
||
MEOW_LIST_SEL_NONE);
|
||
if (!g_lv) return;
|
||
meow_listview_set_vsource(g_lv, lib_count, lib_bind, NULL);
|
||
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, "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);
|
||
}
|
||
|
||
static void update_topbar(void)
|
||
{
|
||
if (!g_tb) return;
|
||
const char *title = "图库";
|
||
if (strcmp(g_cur_dir, MR_ROOT_DIR) != 0) {
|
||
const char *slash = strrchr(g_cur_dir, '/');
|
||
if (slash && slash[1]) title = slash + 1;
|
||
}
|
||
meow_topappbar_set_title(g_tb, title);
|
||
meow_topappbar_set_back(g_tb, strcmp(g_cur_dir, MR_ROOT_DIR) != 0);
|
||
}
|
||
|
||
static void list_invalidate(void)
|
||
{
|
||
meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h);
|
||
}
|
||
|
||
/* 目录切换:作废旧异步结果、释放内存缩略图、重新扫描、重建列表(新池强制重新 bind)。 */
|
||
static void reset_async(void)
|
||
{
|
||
pthread_mutex_lock(&g_lock);
|
||
g_gen++;
|
||
g_req_n = 0;
|
||
g_req_head = g_req_tail = 0;
|
||
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
|
||
g_res_n = 0;
|
||
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_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);
|
||
}
|
||
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());
|
||
}
|
||
|
||
int library_at_root(void)
|
||
{
|
||
return strcmp(g_cur_dir, MR_ROOT_DIR) == 0;
|
||
}
|
||
|
||
int library_go_up(void)
|
||
{
|
||
if (library_at_root()) return 0;
|
||
char parent[MR_PATH_MAX];
|
||
if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0;
|
||
/* 不越过书库根(父级仍须在根目录之下或等于根)。 */
|
||
if (strcmp(parent, MR_ROOT_DIR) != 0 &&
|
||
strncmp(parent, MR_ROOT_DIR "/", sizeof(MR_ROOT_DIR)) != 0)
|
||
snprintf(parent, sizeof parent, "%s", MR_ROOT_DIR);
|
||
load_dir(parent);
|
||
return 1;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 事件 */
|
||
/* ============================================================ */
|
||
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 (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 || idx >= g_lib.count) return;
|
||
mr_library_entry *e = &g_lib.items[idx];
|
||
if (e->has_images) mr_reader_open_dir(e->path);
|
||
else load_dir(e->path);
|
||
}
|
||
|
||
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;
|
||
thumb_res got[MR_RES_MAX];
|
||
int n = 0;
|
||
pthread_mutex_lock(&g_lock);
|
||
n = g_res_n;
|
||
for (int i = 0; i < n; i++) got[i] = g_res[i];
|
||
g_res_n = 0;
|
||
pthread_mutex_unlock(&g_lock);
|
||
|
||
int changed = 0;
|
||
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) {
|
||
int ai = meow_topappbar_add_action(g_tb, "");
|
||
if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_SETTINGS);
|
||
}
|
||
load_dir(MR_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;
|
||
}
|
||
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_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);
|
||
}
|
||
|
||
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);
|
||
}
|