- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页); 排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测) - 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化 (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码 - 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」 - 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出 - 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示) - 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判, 超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险) - 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的 阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
363 lines
12 KiB
C
363 lines
12 KiB
C
/* logic/library.c — 书库目录扫描 + 自然排序(纯 C)。
|
||
*
|
||
* 扫描分两步:
|
||
* 1) scan_info():递归探测一个目录是否「最终含图」,并带回直接图片数 / 含图子目录数 /
|
||
* 缩略图源(优先本目录自然序第一张,其次第一个含图子目录);
|
||
* 2) mr_library_scan():只对目标目录的**直接子目录**调用 scan_info(),把 nav=1 的目录
|
||
* 收进结果并自然排序——因此根视图不会列出后代条目,进入子目录时再重新扫描该层。
|
||
*
|
||
* 内存:条目数组倍增分配;路径/名称定长,随 mr_library_free() 释放。
|
||
*
|
||
* M7 追加(不改扫描语义):
|
||
* - 续读角标:含图册的副标题后缀「· 续读 Pn」(读 logic/reader 的每册 last page);
|
||
* - 册间衔接:mr_book_adjacent 给出同父目录下按书库自然序的相邻「含图册」,
|
||
* 供阅读器末页前进/首页后退跨册;mr_library_dir_has_images 是共享的「是不是一本书」判据。
|
||
*/
|
||
#include "library.h"
|
||
#include "logic/reader.h" /* mr_cont_badge:书库行续读角标(M7) */
|
||
|
||
#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");
|
||
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
|
||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||
if (e.has_images) {
|
||
char badge[24];
|
||
if (mr_cont_badge(e.path, badge, (int)sizeof badge)) {
|
||
int used = (int)strlen(e.subtitle);
|
||
if (used < (int)sizeof e.subtitle - 2)
|
||
snprintf(e.subtitle + used, sizeof e.subtitle - (size_t)used,
|
||
" · %s", badge);
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 册间衔接(M7) */
|
||
/* ============================================================ */
|
||
int mr_library_dir_has_images(const char *dir)
|
||
{
|
||
if (!dir || !dir[0]) return 0;
|
||
DIR *d = opendir(dir);
|
||
if (!d) return 0;
|
||
|
||
int found = 0;
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) != NULL) {
|
||
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;
|
||
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);
|
||
}
|
||
|
||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||
{
|
||
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 || S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||
if (!mr_library_dir_has_images(full)) 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;
|
||
}
|