Merge branch 'feat/mangareader-skeleton-and-library'

This commit is contained in:
AskaEth committed 2026-11-04 20:20:34 +08:00
commit 429535e4d9
17 files changed
+1701

No files matched your search

+6
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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;
}
+3
View File
@@ -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}
+18
View File
@@ -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
+247
View File
@@ -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;
}
+63
View File
@@ -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 */
+14
View File
@@ -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;
}
+23
View File
@@ -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 */
+14
View File
@@ -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
+8
View File
@@ -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
+223
View File
@@ -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;
}
+64
View File
@@ -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 */
+653
View File
@@ -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);
}
+65
View File
@@ -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);
}
+91
View File
@@ -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);
}
+61
View File
@@ -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);
}