feat(tuku): 图库 M3 骨架——书库浏览/主题同步/阅读器与锁屏接缝

- 新增顶层 tuku/ 工程(package dev.tuku.app、二进制 tuku):app.c/internal 头/logic/ui 分层、app.json、build/pack 脚本
- 书库:从 TF 卡根浏览文件夹,含图目录=一本书;logic/library 纯 C 扫描+自然排序(page2<page10)
- 流式缩略图:后台线程解码+meow_img_downscale(96),首帧/滚动不阻塞;磁盘缓存 MeowData/<pkg>/cache(mtime 失效+LRU)
- 列表 meow_listview 虚拟化 + meow_scroll fling/snap;行=缩略图+目录名+页数
- 主题:全部语义色 + meow_theme_add_listener 即时重绘;明暗/风格族两态可读
- 导航:meow_view page push/pop、TopAppBar ⚙→设置;reader/settings/lock 最小桩
- 接缝:mr_reader_get/set_direction(默认 RTL)、lock_view_should_show()(桩 0)
- 宿主 gcc 测试 46 项 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
This commit is contained in:
AskaEth committed 2026-11-04 19:44:18 +08:00
1 parent c0396e40ce
commit bed6a95485
17 files changed
+1676

No files matched your search

+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 */