Merge branch 'feat/mangareader-skeleton-and-library'
This commit is contained in:
commit
429535e4d9
17 files changed
+1701
No files matched your search
@@ -44,6 +44,12 @@ test_umbrella
|
||||
examples/hello/hello
|
||||
examples/hello/render_test*.png
|
||||
|
||||
# ---- 图库产物 ----
|
||||
tuku/tuku
|
||||
tuku/*.mpk
|
||||
tuku/build/
|
||||
tuku/test/test_library
|
||||
|
||||
# ---- 杂项 ----
|
||||
*.orig
|
||||
*.log
|
||||
|
||||
@@ -31,6 +31,12 @@ MeowOS-SDK/
|
||||
│ ├── ui/ # 每页一个 view 文件(home/demo/list/full/chrome)
|
||||
│ ├── logic/ # 纯 C 业务逻辑(不依赖 meow_ui.h,宿主可裸测)
|
||||
│ └── build_hello.sh # 构建 hello(链 meow_ui.a + libz.a + -lpthread)
|
||||
├── tuku/ # 图库:漫画书库 App(M3 骨架:主题同步/目录浏览/阅读器桩)
|
||||
│ ├── app.c # main + 装配 + meow_ui_run 回调路由 + 主题监听
|
||||
│ ├── logic/ # 纯 C:目录扫描/自然排序、阅读方向状态(宿主可裸测)
|
||||
│ ├── ui/ # library / reader / settings / lock 每页一个 view
|
||||
│ ├── build_tuku.sh # 交叉构建(SDK 用 ${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk})
|
||||
│ └── pack_tuku.sh # 出包 dev.tuku.app.mpk
|
||||
└── docs/
|
||||
├── APP_DEV_GUIDE.md # 第三方开发上手指南(环境 → 最小 App → 构建打包 → 签名安装 → 调试)
|
||||
└── MEOW_APP_SDK.md # SDK 设计全量规范
|
||||
@@ -46,6 +52,11 @@ cd sdk
|
||||
# 2) 构建示例 App hello
|
||||
cd ../examples/hello
|
||||
./build_hello.sh # 产出 examples/hello/hello(静态 MIPS32r2 ELF)
|
||||
|
||||
# 3) 构建图库(SDK 用绝对路径,可用 MEOW_SDK_DIR 覆盖)
|
||||
cd ../../tuku
|
||||
./build_tuku.sh # 产出 tuku/tuku(静态 MIPS32r2 ELF)
|
||||
./pack_tuku.sh # 产出 tuku/dev.tuku.app.mpk
|
||||
```
|
||||
|
||||
## SDK 能力速览
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/* app.c — 图库装配层:main + 建页 + meow_ui_run 回调路由 + 主题监听。
|
||||
*
|
||||
* 契约(docs/APP_DEV_GUIDE.md §2-§3):
|
||||
* - main 只做 meow_ui_init → 建 view → meow_ui_run → 清理;退出走 meow_ui_request_exit;
|
||||
* - 页面切换统一走 meow_view_page_push/pop(activity 栈 + on_enter/on_leave 唯一入口);
|
||||
* - 物理键不映射返回/确认(on_key 仅日志);屏底上滑由 SDK 系统手势接管,App 不拦截;
|
||||
* - 左缘返回:书库内逐级上移,根目录才退出。
|
||||
*
|
||||
* 主题同步:注册 meow_theme_add_listener;明暗/风格族切换时更新页面底色并整帧 invalidate,
|
||||
* 各页 on_draw / SDK 控件下一帧即用新语义色重绘(不缓存颜色)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "meow_ui_nav.h"
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
static void on_sigterm(int s)
|
||||
{
|
||||
(void)s;
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* 向 launcher 上报当前页栈深/顶层,供系统导航栈跟随。各页 on_enter 调用。 */
|
||||
void mr_nav_report(void)
|
||||
{
|
||||
int depth = meow_ui_activity_depth();
|
||||
if (depth <= 0) depth = 1;
|
||||
int id = meow_ui_activity_active();
|
||||
const char *top = (id == ACT_READER) ? "reader"
|
||||
: (id == ACT_SETTINGS) ? "settings"
|
||||
: (id == ACT_LOCK) ? "lock"
|
||||
: "library";
|
||||
meow_ui_nav_report((uint32_t)depth, top);
|
||||
}
|
||||
|
||||
/* 主题切换:页面底色 + 整帧重绘(控件与 on_draw 全部从 meow_theme_get() 取色)。 */
|
||||
static void app_theme_changed(meow_theme_mode m)
|
||||
{
|
||||
(void)m;
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
}
|
||||
|
||||
static int app_active(void)
|
||||
{
|
||||
return meow_ui_activity_active();
|
||||
}
|
||||
|
||||
static void app_on_key(int keycode, int is_down)
|
||||
{
|
||||
(void)keycode; (void)is_down; /* 物理键是系统语义,不做返回/确认映射 */
|
||||
meow_log(MEOW_LOG_DEBUG, "key");
|
||||
}
|
||||
|
||||
static void app_on_tap(int x, int y)
|
||||
{
|
||||
switch (app_active()) {
|
||||
case ACT_LIBRARY: library_on_tap(x, y); break;
|
||||
case ACT_READER: reader_on_tap(x, y); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_on_swipe(int dir, int dx, int dy)
|
||||
{
|
||||
(void)dy;
|
||||
if (dir == MEOW_SWIPE_LEFT) {
|
||||
if (app_active() == ACT_LIBRARY) {
|
||||
if (!library_go_up()) {
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
} else {
|
||||
meow_view_page_pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* SDK 的 on_swipe 把位移量放在第二参(第三参为 0):UP/DOWN 时 dx=纵向位移。 */
|
||||
if (app_active() == ACT_LIBRARY) library_on_swipe(dir, dx);
|
||||
}
|
||||
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
library_on_tick();
|
||||
}
|
||||
|
||||
static void app_on_swipe_progress(int x_start, int dx)
|
||||
{
|
||||
meow_ui_swipe_hint(x_start + dx);
|
||||
}
|
||||
|
||||
static void app_on_touch_down(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_down(x, y);
|
||||
}
|
||||
|
||||
static void app_on_touch_move(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_move(x, y);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO, "start");
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "init failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_theme_add_listener(app_theme_changed);
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_nav_init();
|
||||
signal(SIGTERM, on_sigterm);
|
||||
|
||||
/* 先建全部页面(on_create 建各自控件/扫描书库),再按启动锁状态进根页。 */
|
||||
meow_view *library = library_view_create();
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view *settings = settings_view_create();
|
||||
meow_view *lock = lock_view_create();
|
||||
|
||||
meow_view_page_push(lock_view_should_show() ? ACT_LOCK : ACT_LIBRARY);
|
||||
|
||||
meow_ui_events ev = {
|
||||
app_on_key, app_on_tap, app_on_swipe, app_on_tick,
|
||||
app_on_swipe_progress, app_on_touch_down, app_on_touch_move,
|
||||
};
|
||||
meow_ui_run(&ev);
|
||||
|
||||
meow_view_free(lock);
|
||||
meow_view_free(settings);
|
||||
meow_view_free(reader);
|
||||
meow_view_free(library);
|
||||
meow_ui_shutdown();
|
||||
meow_log(MEOW_LOG_INFO, "exit");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"name":"图库","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.tuku.app/tuku",
|
||||
"package":"dev.tuku.app","version":"0.1","version_code":1,"framework":1,
|
||||
"fonts":"common","route.reader.fullscreen":1}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# build_tuku.sh — 构建图库(静态 MIPS32r2,链 meow_ui.a + libz.a + -lpthread)。
|
||||
#
|
||||
# SDK 路径用绝对路径(默认 $HOME/Proj/MeowOS-SDK/sdk),可用 MEOW_SDK_DIR 覆盖;
|
||||
# 故意不用 ../../sdk —— 交付时 tuku/ 会搬到独立仓 ~/Proj/Meow-OS-APPs/PicBrowser。
|
||||
# 交叉编译器/sysroot 同 SDK:MEOW_CC / MEOW_SYSROOT 可覆盖。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/.tools/zig-mipsel-cc}"
|
||||
SYSROOT="${MEOW_SYSROOT:-$HOME/.tools/mipsel-linux-musleabi-sysroot}"
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
|
||||
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
|
||||
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
||||
logic/library.c logic/reader.c \
|
||||
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o tuku
|
||||
ls -la tuku
|
||||
file tuku
|
||||
@@ -0,0 +1,247 @@
|
||||
/* logic/library.c — 书库目录扫描 + 自然排序(纯 C)。
|
||||
*
|
||||
* 扫描分两步:
|
||||
* 1) scan_info():递归探测一个目录是否「最终含图」,并带回直接图片数 / 含图子目录数 /
|
||||
* 缩略图源(优先本目录自然序第一张,其次第一个含图子目录);
|
||||
* 2) mr_library_scan():只对目标目录的**直接子目录**调用 scan_info(),把 nav=1 的目录
|
||||
* 收进结果并自然排序——因此根视图不会列出后代条目,进入子目录时再重新扫描该层。
|
||||
*
|
||||
* 内存:条目数组倍增分配;路径/名称定长,随 mr_library_free() 释放。
|
||||
*/
|
||||
#include "library.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* 系统目录不参与书库(SD 根上的 MeowOS 目录树)。 */
|
||||
static int ignored_dir_name(const char *name)
|
||||
{
|
||||
static const char *const skip[] = { "MeowAPPs", "MeowData", "MeowOS", "lost+found" };
|
||||
if (!name || name[0] == '.') return 1;
|
||||
for (unsigned i = 0; i < sizeof skip / sizeof skip[0]; i++)
|
||||
if (strcmp(name, skip[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int copy_str(char *dst, int dstsz, const char *src)
|
||||
{
|
||||
if (!dst || dstsz <= 0) return -1;
|
||||
if (!src) { dst[0] = '\0'; return 0; }
|
||||
int n = (int)strlen(src);
|
||||
if (n >= dstsz) return -1;
|
||||
memcpy(dst, src, (size_t)n + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int join_path(char *out, int outsz, const char *dir, const char *name)
|
||||
{
|
||||
if (!out || outsz <= 0 || !dir || !name) return -1;
|
||||
int n;
|
||||
size_t dl = strlen(dir);
|
||||
if (dl > 0 && dir[dl - 1] == '/')
|
||||
n = snprintf(out, (size_t)outsz, "%s%s", dir, name);
|
||||
else
|
||||
n = snprintf(out, (size_t)outsz, "%s/%s", dir, name);
|
||||
return (n > 0 && n < outsz) ? 0 : -1;
|
||||
}
|
||||
|
||||
int mr_path_is_image(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "png") == 0 || strcasecmp(dot, "jpg") == 0 ||
|
||||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
|
||||
}
|
||||
|
||||
int mr_natural_cmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
const unsigned char *pb = (const unsigned char *)b;
|
||||
if (!a || !b) return (a == b) ? 0 : (a ? 1 : -1);
|
||||
|
||||
while (*pa && *pb) {
|
||||
if (isdigit(*pa) && isdigit(*pb)) {
|
||||
const unsigned char *za = pa, *zb = pb;
|
||||
while (*pa == '0') pa++;
|
||||
while (*pb == '0') pb++;
|
||||
const unsigned char *da = pa, *db = pb;
|
||||
while (isdigit(*pa)) pa++;
|
||||
while (isdigit(*pb)) pb++;
|
||||
|
||||
size_t la = (size_t)(pa - da), lb = (size_t)(pb - db);
|
||||
if (la != lb) return la < lb ? -1 : 1;
|
||||
if (la > 0) {
|
||||
int c = memcmp(da, db, la);
|
||||
if (c != 0) return c < 0 ? -1 : 1;
|
||||
}
|
||||
size_t oa = (size_t)(da - za), ob = (size_t)(db - zb);
|
||||
if (oa != ob) return oa < ob ? -1 : 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int ca = tolower(*pa), cb = tolower(*pb);
|
||||
if (ca != cb) return ca < cb ? -1 : 1;
|
||||
pa++;
|
||||
pb++;
|
||||
}
|
||||
if (*pa) return 1;
|
||||
if (*pb) return -1;
|
||||
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
typedef struct scan_info {
|
||||
int pages; /* 直接图片数 */
|
||||
int subdirs; /* 含图子目录数 */
|
||||
int nav; /* 1=本目录最终含图(可直接看或可导航到图) */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源 */
|
||||
char first_name[MR_NAME_MAX]; /* 本目录自然序第一张图片的文件名(比较用) */
|
||||
} scan_info;
|
||||
|
||||
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si)
|
||||
{
|
||||
memset(si, 0, sizeof *si);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return;
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (ignored_dir_name(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, path, name) != 0) continue;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (depth >= max_depth) continue;
|
||||
scan_info child;
|
||||
scan_dir(full, depth + 1, max_depth, &child);
|
||||
if (!child.nav) continue;
|
||||
si->subdirs++;
|
||||
if (!si->cover[0] && child.cover[0])
|
||||
copy_str(si->cover, (int)sizeof si->cover, child.cover);
|
||||
} else if (S_ISREG(st.st_mode) && mr_path_is_image(name)) {
|
||||
si->pages++;
|
||||
if (!si->first_name[0] || mr_natural_cmp(name, si->first_name) < 0) {
|
||||
copy_str(si->first_name, (int)sizeof si->first_name, name);
|
||||
copy_str(si->cover, (int)sizeof si->cover, full);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
si->nav = (si->pages > 0 || si->subdirs > 0);
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
{
|
||||
if (!lib) return;
|
||||
free(lib->items);
|
||||
lib->items = NULL;
|
||||
lib->count = 0;
|
||||
lib->cap = 0;
|
||||
}
|
||||
|
||||
static int lib_add(mr_library *lib, const mr_library_entry *e)
|
||||
{
|
||||
if (lib->count >= lib->cap) {
|
||||
int cap = lib->cap ? lib->cap * 2 : 16;
|
||||
mr_library_entry *ni = (mr_library_entry *)realloc(lib->items,
|
||||
(size_t)cap * sizeof *ni);
|
||||
if (!ni) return -1;
|
||||
lib->items = ni;
|
||||
lib->cap = cap;
|
||||
}
|
||||
lib->items[lib->count++] = *e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int entry_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const mr_library_entry *a = (const mr_library_entry *)pa;
|
||||
const mr_library_entry *b = (const mr_library_entry *)pb;
|
||||
return mr_natural_cmp(a->name, b->name);
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
if (!dir || !dir[0]) return -1;
|
||||
|
||||
if (max_depth <= 0 || max_depth > MR_LIB_MAX_DEPTH) max_depth = MR_LIB_MAX_DEPTH;
|
||||
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (ignored_dir_name(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||||
|
||||
scan_info si;
|
||||
scan_dir(full, 1, max_depth, &si);
|
||||
if (!si.nav) continue;
|
||||
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) continue;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) continue;
|
||||
copy_str(e.cover, (int)sizeof e.cover, si.cover);
|
||||
e.pages = si.pages;
|
||||
e.subdirs = si.subdirs;
|
||||
e.has_images = (si.pages > 0);
|
||||
if (e.has_images)
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", e.pages,
|
||||
e.pages == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d folder%s", e.subdirs,
|
||||
e.subdirs == 1 ? "" : "s");
|
||||
if (lib_add(lib, &e) != 0) {
|
||||
closedir(d);
|
||||
mr_library_free(lib);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (lib->count > 1) qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_path_parent(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
size_t n = strlen(path);
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n <= 1) {
|
||||
if (outsz < 2) return -1;
|
||||
out[0] = '/';
|
||||
out[1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
while (n > 0 && path[n - 1] != '/') n--;
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n >= (size_t)outsz) return -1;
|
||||
memcpy(out, path, n);
|
||||
out[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* logic/library.h — 漫画书库扫描/排序(纯 C,不依赖 meow_ui.h)。
|
||||
*
|
||||
* 分层契约同 examples/hello:logic 层只含文件系统与纯数据,宿主 gcc 可直接裸测。
|
||||
* 书库语义:
|
||||
* - 从 TF 卡根(MR_ROOT_DIR)起列「直接子目录」;
|
||||
* - 只保留自身含 PNG/JPEG/BMP 的目录,或子树中最终含图的中间目录(可继续导航);
|
||||
* - 行的 pages=该目录直接图片数;subdirs=其下含图子目录数(中间目录用于副标题);
|
||||
* - cover=缩略图源文件:优先该目录自然序第一张图,否则取第一个含图子目录的 cover;
|
||||
* - 条目按名称自然排序(page2 < page10),含前导零与数字/字母混合名。
|
||||
*
|
||||
* 深度上限 MR_LIB_MAX_DEPTH 用于防止坏卡/超大树的无限递归;隐藏目录与系统目录
|
||||
* (. 开头、MeowAPPs/MeowData/MeowOS/lost+found)不参与书库。符号链接跳过(防环)。
|
||||
*/
|
||||
#ifndef MR_LIBRARY_H
|
||||
#define MR_LIBRARY_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_PATH_MAX 512
|
||||
#define MR_NAME_MAX 256
|
||||
#define MR_LIB_MAX_DEPTH 8
|
||||
|
||||
typedef struct mr_library_entry {
|
||||
char name[MR_NAME_MAX]; /* 目录名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径(导航/进 reader 用) */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源图(空=none) */
|
||||
char subtitle[48]; /* 预先格式化好的 "N pages" / "N folders" */
|
||||
int pages; /* 本目录直接图片数 */
|
||||
int subdirs; /* 本目录下含图子目录数 */
|
||||
int has_images; /* 1=本目录直接含图(点入进 reader) */
|
||||
} mr_library_entry;
|
||||
|
||||
typedef struct mr_library {
|
||||
mr_library_entry *items;
|
||||
int count;
|
||||
int cap;
|
||||
} mr_library;
|
||||
|
||||
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
|
||||
int mr_path_is_image(const char *name);
|
||||
|
||||
/* 自然排序比较:数字段按数值比(page2 < page10),数字相等时前导零少的在前;
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序。 */
|
||||
int mr_natural_cmp(const char *a, const char *b);
|
||||
|
||||
/* 扫描 dir 的直接子目录,写入 lib(先释放 lib 旧内容)。
|
||||
* 返回条目数(>=0);dir 不可读返回 -1(lib 仍为空且可安全 mr_library_free)。
|
||||
* max_depth<=0 或超过 MR_LIB_MAX_DEPTH 时按上限处理。 */
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
|
||||
|
||||
/* 释放条目数组并清零(可重复调用)。 */
|
||||
void mr_library_free(mr_library *lib);
|
||||
|
||||
/* 取父目录路径(去掉末尾 '/' 后截断最后一段)。根 "/" 返回 "/"。
|
||||
* 成功 0;参数非法/输出缓冲不足 -1。 */
|
||||
int mr_path_parent(const char *path, char *out, int outsz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_LIBRARY_H */
|
||||
@@ -0,0 +1,14 @@
|
||||
/* logic/reader.c — 阅读方向状态(默认 RTL;M4 的阅读器直接复用)。 */
|
||||
#include "reader.h"
|
||||
|
||||
static int g_direction = MR_DIR_RTL;
|
||||
|
||||
int mr_reader_get_direction(void)
|
||||
{
|
||||
return g_direction;
|
||||
}
|
||||
|
||||
void mr_reader_set_direction(int dir)
|
||||
{
|
||||
if (dir == MR_DIR_RTL || dir == MR_DIR_LTR) g_direction = dir;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* logic/reader.h — 阅读器纯逻辑接缝(M4 填充实现,M3 先落状态 API)。
|
||||
*
|
||||
* 阅读方向默认 RTL(日式漫画从右往左翻);设置页/阅读器可切 LTR。
|
||||
* 本模块保持纯 C(不 include meow_ui.h),宿主可直接测。
|
||||
*/
|
||||
#ifndef MR_READER_H
|
||||
#define MR_READER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_DIR_RTL 1 /* 默认:右到左(日漫) */
|
||||
#define MR_DIR_LTR 0 /* 左到右 */
|
||||
|
||||
int mr_reader_get_direction(void);
|
||||
/* 只接受 MR_DIR_RTL / MR_DIR_LTR;其它值忽略(保持原方向)。 */
|
||||
void mr_reader_set_direction(int dir);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_READER_H */
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# pack_tuku.sh — 出包 dev.tuku.app.mpk(tar.gz,根目录 dev.tuku.app/,含 app.json + tuku)。
|
||||
# .mpk 文件名茎 = app.json.package = 安装目录名(dev.tuku.app)。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
[ -x tuku ] || ./build_tuku.sh
|
||||
|
||||
rm -rf build/dev.tuku.app
|
||||
mkdir -p build/dev.tuku.app
|
||||
cp app.json tuku build/dev.tuku.app/
|
||||
|
||||
rm -f dev.tuku.app.mpk
|
||||
( cd build && tar czf ../dev.tuku.app.mpk dev.tuku.app )
|
||||
ls -la dev.tuku.app.mpk
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库 logic 层测试(不链 SDK、不需要设备)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
gcc -Wall -Wextra -O1 -I. \
|
||||
test/test_library.c logic/library.c logic/reader.c \
|
||||
-o test/test_library
|
||||
./test/test_library
|
||||
@@ -0,0 +1,223 @@
|
||||
/* test_library.c — 图库 logic 层宿主测试(纯 C,无 SDK/硬件)。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写;
|
||||
* T2 扩展名过滤:png/jpg/jpeg/bmp 大小写不敏感,gif/txt 不支持;
|
||||
* T3 目录扫描:只列出含图目录与可导航中间目录;隐藏/系统目录、空目录、无图目录排除;
|
||||
* pages/subdirs/cover/subtitle 正确;父目录路径推导。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_library.c logic/library.c logic/reader.c \
|
||||
* -o test/test_library && ./test/test_library
|
||||
* 或直接 test/run_tests.sh。
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static const mr_library_entry *find_entry(const mr_library *lib, const char *name)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_natural(void)
|
||||
{
|
||||
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
|
||||
CHECK(mr_natural_cmp("page10", "page2") > 0, "page10 > page2");
|
||||
CHECK(mr_natural_cmp("page2", "page02") < 0, "fewer leading zeros first");
|
||||
CHECK(mr_natural_cmp("x001", "x1") > 0, "x1 < x001");
|
||||
CHECK(mr_natural_cmp("Chapter 9", "Chapter 10") < 0, "chapter 9 < 10");
|
||||
CHECK(mr_natural_cmp("img2a", "img10b") < 0, "mixed img2a < img10b");
|
||||
CHECK(mr_natural_cmp("a", "a1") < 0, "prefix shorter first");
|
||||
CHECK(mr_natural_cmp("abc", "abc") == 0, "identical equal");
|
||||
CHECK(mr_natural_cmp("ABC", "abc") != 0, "case-only tie is deterministic");
|
||||
CHECK(mr_natural_cmp("A2", "a10") < 0, "case-insensitive primary then numeric");
|
||||
}
|
||||
|
||||
static void test_ext(void)
|
||||
{
|
||||
CHECK(mr_path_is_image("a.png") == 1, "png");
|
||||
CHECK(mr_path_is_image("a.JPG") == 1, "JPG uppercase");
|
||||
CHECK(mr_path_is_image("a.jpeg") == 1, "jpeg");
|
||||
CHECK(mr_path_is_image("a.BmP") == 1, "BMP mixed case");
|
||||
CHECK(mr_path_is_image("a.gif") == 0, "gif unsupported");
|
||||
CHECK(mr_path_is_image("a.txt") == 0, "txt unsupported");
|
||||
CHECK(mr_path_is_image("png") == 0, "no extension");
|
||||
CHECK(mr_path_is_image(".png") == 0, "dotfile-ish no stem");
|
||||
}
|
||||
|
||||
static void test_parent(void)
|
||||
{
|
||||
char out[MR_PATH_MAX];
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/b", out, sizeof out) == 0 &&
|
||||
strcmp(out, "/usr/data/mnt/sd_0/a") == 0, "parent nested");
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/", out, sizeof out) == 0 &&
|
||||
strcmp(out, "/usr/data/mnt/sd_0") == 0, "parent trailing slash");
|
||||
CHECK(mr_path_parent("/a", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root child");
|
||||
CHECK(mr_path_parent("/", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root");
|
||||
}
|
||||
|
||||
static void test_scan(void)
|
||||
{
|
||||
char tmpl[] = "./mr_test_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 10", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 10/page1.png", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/Series 10/page2.jpg", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 2", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 2/page10.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 02", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 02/page1.bmp", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 3", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 3/page1.gif", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Empty", root); mkdir_p(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Nested/Deeper", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Nested/Deeper/a.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/.hidden", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/.hidden/h.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/MeowAPPs", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/MeowAPPs/m.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p); /* 根目录直接图片不成为行 */
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == 4, "scan count=%d (want 4)", n);
|
||||
CHECK(lib.count == n, "count matches");
|
||||
if (lib.count == 4) {
|
||||
CHECK(strcmp(lib.items[0].name, "Nested") == 0, "sort[0]=Nested got %s", lib.items[0].name);
|
||||
CHECK(strcmp(lib.items[1].name, "Series 2") == 0, "sort[1]=Series 2 got %s", lib.items[1].name);
|
||||
CHECK(strcmp(lib.items[2].name, "Series 02") == 0, "sort[2]=Series 02 got %s", lib.items[2].name);
|
||||
CHECK(strcmp(lib.items[3].name, "Series 10") == 0, "sort[3]=Series 10 got %s", lib.items[3].name);
|
||||
}
|
||||
|
||||
const mr_library_entry *e = find_entry(&lib, "Series 10");
|
||||
CHECK(e && e->pages == 2 && e->has_images == 1 && e->subdirs == 0, "Series 10 pages=2");
|
||||
CHECK(e && strcmp(e->subtitle, "2 pages") == 0, "Series 10 subtitle '%s'", e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->cover, "page1.png") != NULL, "Series 10 cover natural-first '%s'",
|
||||
e ? e->cover : "");
|
||||
|
||||
e = find_entry(&lib, "Series 2");
|
||||
CHECK(e && e->pages == 1 && e->has_images == 1, "Series 2 pages=1");
|
||||
|
||||
e = find_entry(&lib, "Nested");
|
||||
CHECK(e && e->pages == 0 && e->has_images == 0 && e->subdirs == 1, "Nested intermediate");
|
||||
CHECK(e && strcmp(e->subtitle, "1 folder") == 0, "Nested subtitle '%s'", e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->cover, "Deeper/a.png") != NULL, "Nested cover from descendant '%s'",
|
||||
e ? e->cover : "");
|
||||
|
||||
CHECK(find_entry(&lib, "Empty") == NULL, "empty dir excluded");
|
||||
CHECK(find_entry(&lib, "Series 3") == NULL, "gif-only dir excluded");
|
||||
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
||||
CHECK(find_entry(&lib, "MeowAPPs") == NULL, "system dir excluded");
|
||||
|
||||
/* 深度 1:Nested 不再可探测到 Deeper 的图,应被排除。 */
|
||||
mr_library lib1;
|
||||
memset(&lib1, 0, sizeof lib1);
|
||||
CHECK(mr_library_scan(&lib1, root, 1) == 3, "max_depth=1 excludes Nested");
|
||||
mr_library_free(&lib1);
|
||||
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* 不可读目录:返回 -1,lib 可安全 free。 */
|
||||
mr_library bad;
|
||||
memset(&bad, 0, sizeof bad);
|
||||
CHECK(mr_library_scan(&bad, "/no/such/dir/tuku", MR_LIB_MAX_DEPTH) == -1, "missing dir -> -1");
|
||||
CHECK(bad.count == 0, "missing dir empty");
|
||||
mr_library_free(&bad);
|
||||
|
||||
rm_rf(root);
|
||||
}
|
||||
|
||||
static void test_direction(void)
|
||||
{
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "default RTL");
|
||||
mr_reader_set_direction(MR_DIR_LTR);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "switch LTR");
|
||||
mr_reader_set_direction(99);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored");
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_natural();
|
||||
test_ext();
|
||||
test_parent();
|
||||
test_scan();
|
||||
test_direction();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/* tuku_internal.h — 图库应用内部共享头(只 extern 声明,不定义)。
|
||||
*
|
||||
* 分层(照 examples/hello):
|
||||
* - logic/ 纯 C(library 扫描/自然排序、reader 方向状态),不依赖 meow_ui.h,宿主可裸测;
|
||||
* - ui/ 每页一个 view 文件(library/reader/settings/lock),只碰 SDK;
|
||||
* - app.c main + 装配 + meow_ui_run 回调路由 + 主题监听。
|
||||
*
|
||||
* 页面:ACT_LIBRARY(0) 根页(保留状态栏)、ACT_READER(1) 全屏、ACT_SETTINGS(2)、ACT_LOCK(3)。
|
||||
* 并行开发接缝(后续任务只填自己的文件,不改 app.c/internal.h):
|
||||
* - M4 阅读器:ui/reader/reader.c 实现,方向状态用 logic/reader 的 mr_reader_get/set_direction;
|
||||
* 书库点入含图目录经 mr_reader_open_dir(path) 进入本页。
|
||||
* - M5 锁屏/PIN:ui/lock/lock.c 实现 lock_view_should_show();本批桩返回 0(不锁)。
|
||||
*/
|
||||
#ifndef TUKU_INTERNAL_H
|
||||
#define TUKU_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_view.h"
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader.h"
|
||||
|
||||
/* ---- Activity 归属(稳定 id,< MEOW_VIEW_BY_ID_MAX) ---- */
|
||||
#define ACT_LIBRARY 0
|
||||
#define ACT_READER 1
|
||||
#define ACT_SETTINGS 2
|
||||
#define ACT_LOCK 3
|
||||
|
||||
#define MR_ROOT_DIR "/usr/data/mnt/sd_0" /* 书库根 = TF 卡根 */
|
||||
|
||||
/* ---- 视图控制器(biz-UI 绑定点) ---- */
|
||||
typedef struct mr_page_ctrl {
|
||||
meow_view *view;
|
||||
int active;
|
||||
} mr_page_ctrl;
|
||||
|
||||
/* ---- 书库页(ui/library/library.c) ---- */
|
||||
meow_view *library_view_create(void);
|
||||
int library_at_root(void); /* 1=当前在 MR_ROOT_DIR */
|
||||
int library_go_up(void); /* 返回上一级;1=已上移,0=已在根 */
|
||||
void library_on_tap(int x, int y);
|
||||
void library_on_swipe(int dir, int dist); /* dist=SDK on_swipe 的位移量(UP/DOWN 用) */
|
||||
void library_on_touch_down(int x, int y);
|
||||
void library_on_touch_move(int x, int y);
|
||||
void library_on_tick(void); /* 收下后台缩略图结果并局部补位 */
|
||||
|
||||
/* ---- 阅读器页(ui/reader/reader.c,M4 填充) ---- */
|
||||
meow_view *reader_view_create(void);
|
||||
void reader_on_tap(int x, int y);
|
||||
void mr_reader_open_dir(const char *path); /* 书库→阅读器的桩导航接口 */
|
||||
const char *mr_reader_dir(void); /* 当前待打开目录(骨架显示用) */
|
||||
|
||||
/* ---- 设置页(ui/settings/settings.c) ---- */
|
||||
meow_view *settings_view_create(void);
|
||||
|
||||
/* ---- 锁屏页(ui/lock/lock.c,M5 填充) ---- */
|
||||
meow_view *lock_view_create(void);
|
||||
int lock_view_should_show(void); /* 1=启动先显示锁屏;本批桩返回 0 */
|
||||
|
||||
/* ---- App 级(app.c) ---- */
|
||||
void mr_nav_report(void); /* 向 launcher 上报当前页栈深/顶层 */
|
||||
|
||||
#endif /* TUKU_INTERNAL_H */
|
||||
@@ -0,0 +1,653 @@
|
||||
/* 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);
|
||||
/* SDK 默认 container=0(扁平满幅+分隔线);显式开 MD3 容器模式:
|
||||
* 每行 surfaceContainer 圆角卡 + 行间空隙 + 卡内 tonal 选中。 */
|
||||
meow_listview_set_container(g_lv, 1);
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* ui/lock/lock.c — 锁屏页(ACT_LOCK)M3 桩 + M5 接缝。
|
||||
*
|
||||
* M5 在此实现「APP 启动 PIN」:lock_view_should_show() 读配置决定是否锁,
|
||||
* 锁屏页负责 PIN 输入/校验后 page_pop 回书库。本批桩恒返回 0(不锁),页面仅可编译占位。
|
||||
*
|
||||
* 接缝注意(M5):
|
||||
* ① meow_view 的 on_create 在 meow_view_create 内运行(此时 activity 栈空);本页是非根页,
|
||||
* 控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式
|
||||
* meow_ui_activity_push(ACT_LOCK) → 建控件 → meow_ui_activity_pop()
|
||||
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,
|
||||
* 锁屏控件会从书库页被命中(§6.1 跨页泄漏)。
|
||||
* ② 锁屏必须经自身 view 的 on_event 消费 SWIPE:meow_view_dispatch_event 先于 App 的
|
||||
* on_swipe(sdk/meow_ui_view.h:83-88);否则 app.c 对非 library 页的左缘滑动无条件
|
||||
* page_pop,PIN 会被绕过。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
|
||||
static mr_page_ctrl g_ctrl;
|
||||
|
||||
int lock_view_should_show(void)
|
||||
{
|
||||
return 0; /* M5:读 APP 启动 PIN 配置后返回 1 */
|
||||
}
|
||||
|
||||
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
int w = meow_ui_width();
|
||||
int h = meow_ui_height();
|
||||
int cy = h / 2;
|
||||
|
||||
meow_ui_text_center(w / 2, cy - 80, "LOCKED", th->font_title, th->primary);
|
||||
meow_ui_text_center(w / 2, cy - 36, "Startup PIN (M5)", th->font_body, th->onSurface);
|
||||
meow_ui_text_center(w / 2, cy + 8, "PIN entry will be implemented in M5",
|
||||
th->font_body, th->onSurfaceVariant);
|
||||
}
|
||||
|
||||
static void lock_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);
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
mr_nav_report();
|
||||
meow_ui_paint_full();
|
||||
}
|
||||
|
||||
static void lock_page_leave(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v;
|
||||
((mr_page_ctrl *)ctx)->active = 0;
|
||||
}
|
||||
|
||||
meow_view *lock_view_create(void)
|
||||
{
|
||||
static const meow_view_hooks h = {
|
||||
.on_enter = lock_page_enter,
|
||||
.on_leave = lock_page_leave,
|
||||
.on_draw = lock_draw,
|
||||
};
|
||||
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏)M3 骨架。
|
||||
*
|
||||
* M4 在此实现翻页/图片显示;本批只落可编译桩 + 既有接缝:
|
||||
* - 书库经 mr_reader_open_dir(path) 导航进来(app.c/library.c 不改);
|
||||
* - 方向状态走 logic/reader 的 mr_reader_get/set_direction(默认 RTL)。
|
||||
* 绘制走 view on_draw(注册表驱动),主题切换由 app.c 的 listener invalidate 全屏重画。
|
||||
*
|
||||
* 接缝注意(M4):meow_view 的 on_create 在 meow_view_create 内运行(此时 activity 栈空),
|
||||
* 本页是非根页——控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式
|
||||
* meow_ui_activity_push(ACT_READER) → 建控件 → meow_ui_activity_pop()
|
||||
* (examples/hello/ui/demo/demo.c:515-522);否则控件 owner 绑到 ACT_LIBRARY,
|
||||
* 造成跨页命中/绘制泄漏(§6.1)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static mr_page_ctrl g_ctrl;
|
||||
static char g_dir[MR_PATH_MAX];
|
||||
|
||||
const char *mr_reader_dir(void)
|
||||
{
|
||||
return g_dir;
|
||||
}
|
||||
|
||||
void mr_reader_open_dir(const char *path)
|
||||
{
|
||||
if (!path) return;
|
||||
snprintf(g_dir, sizeof g_dir, "%s", path);
|
||||
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
|
||||
meow_view_page_push(ACT_READER);
|
||||
}
|
||||
|
||||
void reader_on_tap(int x, int y)
|
||||
{
|
||||
(void)x; (void)y;
|
||||
if (!g_ctrl.active) return;
|
||||
mr_reader_set_direction(mr_reader_get_direction() == MR_DIR_RTL ? MR_DIR_LTR : MR_DIR_RTL);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
}
|
||||
|
||||
static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
int w = meow_ui_width();
|
||||
const char *dir = g_dir[0] ? g_dir : "(no directory)";
|
||||
size_t n = strlen(dir);
|
||||
if (n > 64) dir += n - 64;
|
||||
|
||||
meow_ui_text_center(w / 2, 160, "READER", th->font_title, th->primary);
|
||||
meow_ui_text_center(w / 2, 200, "(M4 will render pages here)", th->font_body,
|
||||
th->onSurfaceVariant);
|
||||
meow_ui_text_center(w / 2, 280, "Folder:", th->font_body, th->onSurfaceVariant);
|
||||
meow_ui_text_center(w / 2, 308, dir, th->font_body, th->onSurface);
|
||||
meow_ui_text_center(w / 2, 372,
|
||||
mr_reader_get_direction() == MR_DIR_RTL ? "Direction: RTL" : "Direction: LTR",
|
||||
th->font_body, th->onSurface);
|
||||
meow_ui_text_center(w / 2, 420, "tap = toggle direction", th->font_body, th->onSurfaceVariant);
|
||||
meow_ui_text_center(w / 2, 448, "left-edge swipe = back", th->font_body, th->onSurfaceVariant);
|
||||
}
|
||||
|
||||
static void reader_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(1); /* app.json route.reader.fullscreen 的运行时兜底 */
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
mr_nav_report();
|
||||
meow_ui_paint_full();
|
||||
}
|
||||
|
||||
static void reader_page_leave(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v;
|
||||
((mr_page_ctrl *)ctx)->active = 0;
|
||||
meow_ui_set_fullscreen(0);
|
||||
}
|
||||
|
||||
meow_view *reader_view_create(void)
|
||||
{
|
||||
static const meow_view_hooks h = {
|
||||
.on_enter = reader_page_enter,
|
||||
.on_leave = reader_page_leave,
|
||||
.on_draw = reader_draw,
|
||||
};
|
||||
return meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/* ui/settings/settings.c — 设置页(ACT_SETTINGS)M3 骨架。
|
||||
*
|
||||
* M5 在此实现 APP 启动 PIN 等设置项;本批只落可编译桩,入口为书库 TopAppBar ⚙。
|
||||
* 返回:左缘返回(app.c SWIPE_LEFT → page_pop)。
|
||||
*
|
||||
* 接缝注意(M5):meow_view 的 on_create 在 meow_view_create 内运行(activity 栈空);
|
||||
* 本页是非根页,控件必须 lazy 到 on_enter 建,或照 hello demo_init 的模式
|
||||
* meow_ui_activity_push(ACT_SETTINGS) → 建控件 → meow_ui_activity_pop()
|
||||
* (examples/hello/ui/demo/demo.c:515-522);否则 owner 绑到 ACT_LIBRARY,设置控件会从
|
||||
* 书库页被命中(§6.1 跨页泄漏)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
|
||||
static mr_page_ctrl g_ctrl;
|
||||
|
||||
static void settings_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)v; (void)ctx;
|
||||
int w = meow_ui_width();
|
||||
int sb = meow_ui_statusbar_height();
|
||||
|
||||
meow_ui_fill_rect(0, sb, w, 56, th->surfaceContainer);
|
||||
meow_ui_text(w / 2, sb + 16, "Settings", th->font_title, th->onSurface);
|
||||
meow_ui_text_center(w / 2, sb + 100, "APP options", th->font_body, th->onSurfaceVariant);
|
||||
meow_ui_text_center(w / 2, sb + 140, "Startup PIN — M5", th->font_body, th->onSurface);
|
||||
meow_ui_text_center(w / 2, sb + 180, "Reader direction — M4", th->font_body,
|
||||
th->onSurface);
|
||||
meow_ui_text_center(w / 2, sb + 240, "Theme follows the system automatically",
|
||||
th->font_body, th->onSurfaceVariant);
|
||||
meow_ui_text_center(w / 2, sb + 300, "left-edge swipe = back", th->font_body,
|
||||
th->onSurfaceVariant);
|
||||
}
|
||||
|
||||
static void settings_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);
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
mr_nav_report();
|
||||
meow_ui_paint_full();
|
||||
}
|
||||
|
||||
static void settings_page_leave(meow_view *v, void *ctx)
|
||||
{
|
||||
(void)v;
|
||||
((mr_page_ctrl *)ctx)->active = 0;
|
||||
}
|
||||
|
||||
meow_view *settings_view_create(void)
|
||||
{
|
||||
static const meow_view_hooks h = {
|
||||
.on_enter = settings_page_enter,
|
||||
.on_leave = settings_page_leave,
|
||||
.on_draw = settings_draw,
|
||||
};
|
||||
return meow_view_create(ACT_SETTINGS, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||||
}
|
||||
Reference in new issue
Block a user