- 新增顶层 tuku/ 工程(package dev.tuku.app、二进制 tuku):app.c/internal 头/logic/ui 分层、app.json、build/pack 脚本 - 书库:从 TF 卡根浏览文件夹,含图目录=一本书;logic/library 纯 C 扫描+自然排序(page2<page10) - 流式缩略图:后台线程解码+meow_img_downscale(96),首帧/滚动不阻塞;磁盘缓存 MeowData/<pkg>/cache(mtime 失效+LRU) - 列表 meow_listview 虚拟化 + meow_scroll fling/snap;行=缩略图+目录名+页数 - 主题:全部语义色 + meow_theme_add_listener 即时重绘;明暗/风格族两态可读 - 导航:meow_view page push/pop、TopAppBar ⚙→设置;reader/settings/lock 最小桩 - 接缝:mr_reader_get/set_direction(默认 RTL)、lock_view_should_show()(桩 0) - 宿主 gcc 测试 46 项 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
651 lines
20 KiB
C
651 lines
20 KiB
C
/* ui/library/library.c — 图库根页(ACT_LIBRARY):
|
||
*
|
||
* TopAppBar(标题 + 返回 + ⚙ 位图动作)+ meow_listview 虚拟化目录列表。
|
||
* 任意文件夹可浏览;无元数据但含 PNG/JPEG/BMP 的文件夹即一本书(含图目录=册)。
|
||
* 行=缩略图 + 目录名 + 页数;目录由 logic/library 扫描并自然排序。
|
||
*
|
||
* 流畅度(用户指令优先):
|
||
* - 首帧只绘文字行:bind 不读盘;缩略图请求投递给后台线程(pthread)异步解码,
|
||
* 完成后在 on_tick 补位并只 invalidate 列表视口;
|
||
* - 缩略图 meow_img_downscale 到 96px,并落磁盘缓存 MeowData/<pkg>/cache
|
||
* (BMP 原始缩略图,文件名含源文件 mtime+size,源文件变化自动失效;≥64 张按
|
||
* mtime LRU 删除);
|
||
* - 列表滚动用 meow_scroll_wrap:跟手 track + 松手 fling + 行高 snap。
|
||
*
|
||
* 绘制/颜色:SDK 注册表非保留绘制,全部取 meow_theme_get() 语义色;主题切换由 app.c 的
|
||
* listener 整帧 invalidate。缩略图缓存只在列表重建时释放(避免池内悬垂指针),上限 24 张。
|
||
*/
|
||
#include "tuku_internal.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;
|
||
|
||
int n = snprintf(g_cache_dir, sizeof g_cache_dir, "%s", data);
|
||
if (n <= 0 || n >= (int)sizeof g_cache_dir) { g_cache_dir[0] = '\0'; return; }
|
||
|
||
char *slash = strrchr(g_cache_dir, '/');
|
||
if (slash && strcmp(slash, "/data") == 0) *slash = '\0';
|
||
|
||
size_t len = strlen(g_cache_dir);
|
||
n = snprintf(g_cache_dir + len, sizeof g_cache_dir - len, "/cache");
|
||
if (n <= 0 || n >= (int)(sizeof g_cache_dir - len)) { g_cache_dir[0] = '\0'; return; }
|
||
|
||
if (mkdir(g_cache_dir, 0755) != 0 && errno != EEXIST) {
|
||
g_cache_dir[0] = '\0';
|
||
return;
|
||
}
|
||
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 → 落缓存。 */
|
||
static meow_img *thumb_load(const char *cover)
|
||
{
|
||
struct stat st;
|
||
if (stat(cover, &st) != 0 || !S_ISREG(st.st_mode)) return NULL;
|
||
|
||
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(cover);
|
||
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;
|
||
if (e->cover[0]) thumb_enqueue(idx, e->cover);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局 / 列表重建 */
|
||
/* ============================================================ */
|
||
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);
|
||
meow_listview_set_empty_text(g_lv, "No folders with PNG/JPEG/BMP");
|
||
|
||
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);
|
||
}
|
||
|
||
static void load_dir(const char *dir)
|
||
{
|
||
snprintf(g_cur_dir, sizeof g_cur_dir, "%s", dir);
|
||
reset_async();
|
||
thumbs_free();
|
||
if (mr_library_scan(&g_lib, g_cur_dir, MR_LIB_MAX_DEPTH) < 0) {
|
||
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);
|
||
}
|