Files
MeowOS-SDK/tuku/logic/library.c
T
AskaEth 64336f95d9 图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)
用户反馈四件事一起改:
- 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路
  (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 +
  .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片=
  mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。
- 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内
  缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期
  生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。
  2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次
  (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。
- 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏
  「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。
- 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态
  「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。

集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/
普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比
mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。

reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push,
走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。

回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00

440 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/library.c — 书库文件管理器模型:单层枚举 + 纯内存行合成(纯 C)。
*
* 扫描(mr_library_scan)与 os.meow.files 的 fsx_scan_dir 同构:
* - 一次 opendir/readdir,逐项 lstat(只认目录、受支持图片、.cbz/.zip;符号链接跳过防环);
* - 隐藏项("." 开头,含 . ..)跳过;空目录/无图目录/系统目录照常列出;
* - 排序:目录优先,组内按 mr_natural_cmp 自然升序;
* - 副标题(类型 · 大小)在扫描期生成一次,渲染期不再做任何格式化/IO;
* - 条目到 MR_LIB_SCAN_MAX 后仍有条目 → 截断标注。
*
* 阅读器辅助(与列表模型解耦):mr_library_dir_has_images 单层判据 +
* mr_book_adjacent(_ex) 兄弟册衔接(M7/M11 阅读器翻到相邻册用)。
*/
#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>
#ifdef MR_LIBRARY_TEST_HOOKS
static mr_library_io_stats g_io;
void mr_library_test_io_reset(void)
{
memset(&g_io, 0, sizeof g_io);
}
void mr_library_test_io_get(mr_library_io_stats *out)
{
if (out) *out = g_io;
}
#define IO_OPEN() (g_io.opendirs++)
#define IO_READDIR() (g_io.readdirs++)
#define IO_LSTAT() (g_io.lstats++)
#else
#define IO_OPEN() ((void)0)
#define IO_READDIR() ((void)0)
#define IO_LSTAT() ((void)0)
#endif
/* 系统目录不参与**册间衔接**(M7 兄弟册表);浏览本身已放开(不隐藏)。 */
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_path_is_archive(const char *name)
{
if (!name) return 0;
const char *dot = strrchr(name, '.');
if (!dot || dot == name) return 0;
dot++;
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 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);
}
/* ============================================================ */
/* 扫描 / 排序 / 行合成 */
/* ============================================================ */
static void fmt_size(long long size, char *out, int outsz)
{
if (size < 0) size = 0;
static const char *const unit[] = { "B", "KB", "MB", "GB" };
double v = (double)size;
int i = 0;
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
else snprintf(out, (size_t)outsz, "%.1f %s", v, unit[i]);
}
/* 副标题只在扫描期生成一次(渲染期零格式化)。 */
static void format_subtitle(mr_library_entry *e)
{
char sz[32];
switch (e->kind) {
case MR_LIB_KIND_DIR:
snprintf(e->subtitle, sizeof e->subtitle, "文件夹");
break;
case MR_LIB_KIND_ARCHIVE:
fmt_size(e->size, sz, (int)sizeof sz);
snprintf(e->subtitle, sizeof e->subtitle, "档案册 · %s", sz);
break;
default:
fmt_size(e->size, sz, (int)sizeof sz);
snprintf(e->subtitle, sizeof e->subtitle, "图片 · %s", sz);
break;
}
}
void mr_library_free(mr_library *lib)
{
if (!lib) return;
free(lib->items);
lib->items = NULL;
lib->count = 0;
lib->cap = 0;
lib->truncated = 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;
int ad = (a->kind == MR_LIB_KIND_DIR), bd = (b->kind == MR_LIB_KIND_DIR);
if (ad != bd) return ad ? -1 : 1;
return mr_natural_cmp(a->name, b->name);
}
static int add_entry(mr_library *lib, const char *name, const char *full,
int kind, const struct stat *st)
{
mr_library_entry e;
memset(&e, 0, sizeof e);
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
e.kind = kind;
e.size = (kind == MR_LIB_KIND_DIR) ? 0 : (long long)st->st_size;
format_subtitle(&e);
return lib_add(lib, &e);
}
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
{
(void)max_depth; /* 兼容旧签名:扫描不递归 */
if (!lib) return -1;
mr_library_free(lib);
if (!dir || !dir[0]) return -1;
IO_OPEN();
DIR *d = opendir(dir);
if (!d) return -1;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
IO_READDIR();
const char *name = de->d_name;
if (name[0] == '.') continue; /* . .. 与隐藏项 */
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
IO_LSTAT();
struct stat st;
if (lstat(full, &st) != 0) continue;
if (S_ISLNK(st.st_mode)) continue;
int kind;
if (S_ISDIR(st.st_mode)) kind = MR_LIB_KIND_DIR;
else if (!S_ISREG(st.st_mode)) continue;
else if (mr_path_is_image(name)) kind = MR_LIB_KIND_IMAGE;
else if (mr_path_is_archive(name)) kind = MR_LIB_KIND_ARCHIVE;
else continue;
if (lib->count >= MR_LIB_SCAN_MAX) { lib->truncated = 1; break; }
if (add_entry(lib, name, full, kind, &st) != 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_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
{
static char trunc_sub[48];
static int trunc_ready;
if (!lib || !out) return 0;
if (idx == lib->count && lib->truncated) {
if (!trunc_ready) {
snprintf(trunc_sub, sizeof trunc_sub, "仅显示前 %d 项", MR_LIB_SCAN_MAX);
trunc_ready = 1;
}
memset(out, 0, sizeof *out);
out->title = "已截断";
out->subtitle = trunc_sub;
out->disabled = 1;
out->index = -1;
return 1;
}
if (idx < 0 || idx >= lib->count) return 0;
const mr_library_entry *e = &lib->items[idx];
memset(out, 0, sizeof *out);
out->title = e->name;
out->subtitle = e->subtitle;
out->kind = e->kind;
out->index = idx;
return 1;
}
int mr_library_image_index(const mr_library *lib, int idx)
{
if (!lib || idx < 0 || idx >= lib->count) return -1;
if (lib->items[idx].kind != MR_LIB_KIND_IMAGE) return -1;
int n = 0;
for (int i = 0; i < idx; i++)
if (lib->items[i].kind == MR_LIB_KIND_IMAGE) n++;
return n;
}
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;
}
/* ============================================================ */
/* 册间衔接(M7 / M11) */
/* ============================================================ */
int mr_library_dir_has_images(const char *dir)
{
if (!dir || !dir[0]) return 0;
IO_OPEN();
DIR *d = opendir(dir);
if (!d) return 0;
int found = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
IO_READDIR();
const char *name = de->d_name;
if (name[0] == '.' || !mr_path_is_image(name)) continue;
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
IO_LSTAT();
struct stat st;
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
found = 1;
break;
}
closedir(d);
return found;
}
static int name_ptr_cmp(const void *pa, const void *pb)
{
const char *a = *(const char *const *)pa;
const char *b = *(const char *const *)pb;
return mr_natural_cmp(a, b);
}
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
static int sibling_is_book(const char *full, const struct stat *st,
mr_lib_archive_fn probe, void *ud)
{
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
if (S_ISREG(st->st_mode)) {
const char *slash = strrchr(full, '/');
const char *name = slash ? slash + 1 : full;
if (!mr_path_is_archive(name)) return 0;
if (!probe) return 0;
int n = probe(full, ud);
return n > 0;
}
return 0;
}
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
mr_lib_archive_fn archive_pages, void *ud)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || (delta != 1 && delta != -1)) return -1;
char norm[MR_PATH_MAX];
size_t dl = strlen(dir);
while (dl > 1 && dir[dl - 1] == '/') dl--;
if (dl >= sizeof norm) return -1;
memcpy(norm, dir, dl);
norm[dl] = '\0';
const char *slash = strrchr(norm, '/');
if (!slash || !slash[1]) return -1;
const char *cur = slash + 1;
char parent[MR_PATH_MAX];
if (mr_path_parent(norm, parent, (int)sizeof parent) != 0) return -1;
DIR *d = opendir(parent);
if (!d) return 0;
char **names = NULL;
int n = 0, cap = 0;
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, parent, name) != 0) continue;
struct stat st;
if (lstat(full, &st) != 0) continue;
if (S_ISLNK(st.st_mode)) continue;
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
if (n >= cap) {
int nc = cap ? cap * 2 : 16;
char **nn = (char **)realloc(names, (size_t)nc * sizeof *nn);
if (!nn) break;
names = nn;
cap = nc;
}
size_t len = strlen(name);
names[n] = (char *)malloc(len + 1);
if (!names[n]) break;
memcpy(names[n], name, len + 1);
n++;
}
closedir(d);
int rc = 0;
if (n > 1) qsort(names, (size_t)n, sizeof *names, name_ptr_cmp);
int idx = -1;
for (int i = 0; i < n; i++)
if (strcmp(names[i], cur) == 0) { idx = i; break; }
int target = idx + delta;
if (idx >= 0 && target >= 0 && target < n) {
if (join_path(out, outsz, parent, names[target]) == 0) rc = 1;
}
for (int i = 0; i < n; i++) free(names[i]);
free(names);
return rc;
}
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
{
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
}