Files
MeowOS-SDK/tuku/logic/library.c
T
AskaEth ff97f3cee7 feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接
- 条漫: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 一键运行
2026-11-04 21:49:33 +08:00

363 lines
12 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)。
*
* 扫描分两步:
* 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;
}