图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(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;交叉构建零警告。
This commit is contained in:
1 parent
d07b99056f
commit
64336f95d9
16 files changed
+940
-1927
No files matched your search
+64
-322
@@ -1,28 +1,19 @@
|
||||
/* logic/library.c — 书库目录浏览:单层枚举 + 后台懒判定(纯 C)。
|
||||
/* logic/library.c — 书库文件管理器模型:单层枚举 + 纯内存行合成(纯 C)。
|
||||
*
|
||||
* 扫描(mr_library_scan / mr_library_scan_ex)与 os.meow.files 的 fsx_scan_dir 同构:
|
||||
* - 一次 opendir/readdir,逐项 lstat(只认目录与档案文件;符号链接跳过防环);
|
||||
* - 隐藏项("." 开头,含 . ..)跳过;系统目录/空目录/无图目录一律列出;
|
||||
* - 目录名/档案文件名按 mr_natural_cmp 自然升序;条目到 MR_LIB_SCAN_MAX 后仍有条目 →
|
||||
* 截断标注。不再对子目录做任何递归/探测——首帧成本只与本层条目数线性相关。
|
||||
* 扫描(mr_library_scan)与 os.meow.files 的 fsx_scan_dir 同构:
|
||||
* - 一次 opendir/readdir,逐项 lstat(只认目录、受支持图片、.cbz/.zip;符号链接跳过防环);
|
||||
* - 隐藏项("." 开头,含 . ..)跳过;空目录/无图目录/系统目录照常列出;
|
||||
* - 排序:目录优先,组内按 mr_natural_cmp 自然升序;
|
||||
* - 副标题(类型 · 大小)在扫描期生成一次,渲染期不再做任何格式化/IO;
|
||||
* - 条目到 MR_LIB_SCAN_MAX 后仍有条目 → 截断标注。
|
||||
*
|
||||
* 懒判定(probe):
|
||||
* - mr_library_probe_dir 只读一层(图片项 lstat 校验普通文件),给出直接图片数与自然序首图;
|
||||
* - mr_library_probe_path 按行类型探测:目录走上面的单层扫描;档案册调调用方注入的
|
||||
* archive_pages(M11,logic 不依赖 zip)。两者共用 mtime(秒+纳秒)+size 失效的 256 项
|
||||
* LRU 缓存 + 互斥锁,可被后台线程并发调用;探测期间目录/档案发生变化则不写缓存。
|
||||
* - mr_library_apply_probe 把结果补回行;副标题由 mr_library_format_subtitle 统一生成,
|
||||
* 书行再拼 M7 的「· 续读 Pn」角标。
|
||||
*
|
||||
* M11 档案册:注入 archive_pages 时 .cbz/.zip 直接子文件也成行(懒探测页数);
|
||||
* 未注入时只列目录(M7 书库语义)。册间衔接的兄弟表 = 含图目录 ∪ 含图片页的档案册。
|
||||
* 阅读器辅助(与列表模型解耦):mr_library_dir_has_images 单层判据 +
|
||||
* mr_book_adjacent(_ex) 兄弟册衔接(M7/M11 阅读器翻到相邻册用)。
|
||||
*/
|
||||
#include "library.h"
|
||||
#include "logic/reader.h" /* mr_cont_badge:书库行续读角标(M7) */
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -138,244 +129,39 @@ int mr_natural_cmp(const char *a, const char *b)
|
||||
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
/* 档案册探测(UI 注入;NULL=纯目录书库,档案文件不成行)。 */
|
||||
static int archive_pages_at(mr_lib_archive_fn probe, void *ud, const char *path)
|
||||
{
|
||||
if (!probe || !path || !path[0]) return 0;
|
||||
int n = probe(path, ud);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 单层探测 + mtime/size 失效缓存 */
|
||||
/* 扫描 / 排序 / 行合成 */
|
||||
/* ============================================================ */
|
||||
int mr_library_probe_dir(const char *dir, mr_library_probe *out)
|
||||
static void fmt_size(long long size, char *out, int outsz)
|
||||
{
|
||||
if (!out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
if (!dir || !dir[0]) return -1;
|
||||
|
||||
IO_OPEN();
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
char best[MR_NAME_MAX];
|
||||
best[0] = '\0';
|
||||
out->empty = 1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
IO_READDIR();
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.') continue; /* 隐藏项不算内容 */
|
||||
out->empty = 0;
|
||||
if (!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;
|
||||
|
||||
out->pages++;
|
||||
if (best[0] == '\0' || mr_natural_cmp(name, best) < 0) {
|
||||
copy_str(best, (int)sizeof best, name);
|
||||
copy_str(out->cover, (int)sizeof out->cover, full);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return 0;
|
||||
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]);
|
||||
}
|
||||
|
||||
#define MR_PROBE_CACHE_MAX 256
|
||||
|
||||
typedef struct probe_cache_entry {
|
||||
char path[MR_PATH_MAX];
|
||||
long long mtime_ns;
|
||||
long long size;
|
||||
mr_library_probe probe;
|
||||
unsigned long long last_use;
|
||||
} probe_cache_entry;
|
||||
|
||||
static probe_cache_entry *g_cache[MR_PROBE_CACHE_MAX];
|
||||
static unsigned long long g_cache_clock;
|
||||
static pthread_mutex_t g_cache_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static long long stat_mtime_ns(const struct stat *st)
|
||||
/* 副标题只在扫描期生成一次(渲染期零格式化)。 */
|
||||
static void format_subtitle(mr_library_entry *e)
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
return (long long)st->st_mtimespec.tv_sec * 1000000000LL + st->st_mtimespec.tv_nsec;
|
||||
#else
|
||||
return (long long)st->st_mtim.tv_sec * 1000000000LL + st->st_mtim.tv_nsec;
|
||||
#endif
|
||||
}
|
||||
|
||||
static probe_cache_entry *cache_find_locked(const char *path)
|
||||
{
|
||||
for (int i = 0; i < MR_PROBE_CACHE_MAX; i++) {
|
||||
if (!g_cache[i]) continue;
|
||||
if (strcmp(g_cache[i]->path, path) == 0) return g_cache[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void cache_put_locked(const char *path, long long mtime_ns, long long size,
|
||||
const mr_library_probe *p)
|
||||
{
|
||||
probe_cache_entry *e = cache_find_locked(path);
|
||||
if (!e) {
|
||||
int slot = -1;
|
||||
for (int i = 0; i < MR_PROBE_CACHE_MAX; i++)
|
||||
if (!g_cache[i]) { slot = i; break; }
|
||||
if (slot < 0) { /* 满:淘汰最久未用项 */
|
||||
int lru = 0;
|
||||
unsigned long long oldest = g_cache[0]->last_use;
|
||||
for (int i = 1; i < MR_PROBE_CACHE_MAX; i++) {
|
||||
if (g_cache[i]->last_use < oldest) {
|
||||
oldest = g_cache[i]->last_use;
|
||||
lru = i;
|
||||
}
|
||||
}
|
||||
free(g_cache[lru]);
|
||||
g_cache[lru] = NULL;
|
||||
slot = lru;
|
||||
}
|
||||
e = (probe_cache_entry *)malloc(sizeof *e);
|
||||
if (!e) return; /* 缓存只是加速:失败就每次现算 */
|
||||
if (copy_str(e->path, (int)sizeof e->path, path) != 0) {
|
||||
free(e);
|
||||
return;
|
||||
}
|
||||
g_cache[slot] = e;
|
||||
}
|
||||
e->mtime_ns = mtime_ns;
|
||||
e->size = size;
|
||||
e->probe = *p;
|
||||
e->last_use = ++g_cache_clock;
|
||||
}
|
||||
|
||||
void mr_library_probe_cache_clear(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_cache_lock);
|
||||
for (int i = 0; i < MR_PROBE_CACHE_MAX; i++) {
|
||||
free(g_cache[i]);
|
||||
g_cache[i] = NULL;
|
||||
}
|
||||
g_cache_clock = 0;
|
||||
pthread_mutex_unlock(&g_cache_lock);
|
||||
}
|
||||
|
||||
/* 无缓存探测:目录=单层目录扫描;档案册=注入回调取页数。 */
|
||||
static int probe_path_uncached(const char *path, int is_archive,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
mr_library_probe *out)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
if (is_archive) {
|
||||
if (!archive_pages || !S_ISREG(st.st_mode)) return -1;
|
||||
out->pages = archive_pages_at(archive_pages, ud, path);
|
||||
out->empty = 0;
|
||||
out->cover[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode)) return -1;
|
||||
return mr_library_probe_dir(path, out);
|
||||
}
|
||||
|
||||
int mr_library_probe_path(const char *path, int is_archive,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
mr_library_probe *out)
|
||||
{
|
||||
if (!out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
if (!path || !path[0]) return -1;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
if (is_archive ? !S_ISREG(st.st_mode) : !S_ISDIR(st.st_mode)) return -1;
|
||||
|
||||
long long mtime_ns = stat_mtime_ns(&st);
|
||||
long long size = (long long)st.st_size;
|
||||
|
||||
pthread_mutex_lock(&g_cache_lock);
|
||||
probe_cache_entry *hit = cache_find_locked(path);
|
||||
if (hit && hit->mtime_ns == mtime_ns && hit->size == size) {
|
||||
*out = hit->probe;
|
||||
hit->last_use = ++g_cache_clock;
|
||||
pthread_mutex_unlock(&g_cache_lock);
|
||||
return 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_cache_lock);
|
||||
|
||||
mr_library_probe p;
|
||||
memset(&p, 0, sizeof p);
|
||||
if (probe_path_uncached(path, is_archive, archive_pages, ud, &p) != 0) return -1;
|
||||
|
||||
/* 评审 P2:只把「探测前后完全一致」的结果写入缓存。
|
||||
* 探测期间目录/档案的 mtime 或 size 变了(新增/替换文件),说明结果可能不完整,
|
||||
* 不能绑定到新 mtime 上当「新鲜」——本次直接返回,下次调用重新探测。 */
|
||||
struct stat st2;
|
||||
if (stat(path, &st2) == 0 && stat_mtime_ns(&st2) == mtime_ns &&
|
||||
(long long)st2.st_size == size) {
|
||||
pthread_mutex_lock(&g_cache_lock);
|
||||
cache_put_locked(path, mtime_ns, size, &p);
|
||||
pthread_mutex_unlock(&g_cache_lock);
|
||||
}
|
||||
|
||||
*out = p;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_library_probe_cached(const char *dir, mr_library_probe *out)
|
||||
{
|
||||
return mr_library_probe_path(dir, 0, NULL, NULL, out);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 行状态:副标题 / 补位 / 行合成 */
|
||||
/* ============================================================ */
|
||||
/* 基础副标题(不含 M7 续读角标)。 */
|
||||
static void entry_subtitle(mr_library_entry *e)
|
||||
{
|
||||
switch (e->probe_state) {
|
||||
case MR_LIB_READY:
|
||||
if (e->pages > 0)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d page%s", e->pages,
|
||||
e->pages == 1 ? "" : "s");
|
||||
else if (!e->is_archive && e->empty)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "空目录");
|
||||
else
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "无图片");
|
||||
char sz[32];
|
||||
switch (e->kind) {
|
||||
case MR_LIB_KIND_DIR:
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "文件夹");
|
||||
break;
|
||||
case MR_LIB_FAILED:
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "无法读取");
|
||||
case MR_LIB_KIND_ARCHIVE:
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "档案册 · %s", sz);
|
||||
break;
|
||||
default:
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "…");
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "图片 · %s", sz);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 续读角标(M7):有 last page 记录的书行,在副标题后缀「· 续读 Pn」。
|
||||
* 档案册以档案路径为册标识,同一套逻辑复用。
|
||||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||||
static void entry_badge(mr_library_entry *e)
|
||||
{
|
||||
if (e->has_images != 1) return;
|
||||
char badge[24];
|
||||
if (!mr_cont_badge(e->path, badge, (int)sizeof badge)) return;
|
||||
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);
|
||||
}
|
||||
|
||||
void mr_library_format_subtitle(mr_library_entry *e)
|
||||
{
|
||||
if (!e) return;
|
||||
entry_subtitle(e);
|
||||
entry_badge(e);
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
{
|
||||
if (!lib) return;
|
||||
@@ -400,59 +186,32 @@ static int lib_add(mr_library *lib, const mr_library_entry *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);
|
||||
}
|
||||
|
||||
/* 文件夹册行(M13):一律列出、初始待探测;是否「书」由后台 probe 决定。 */
|
||||
static int add_dir_entry(mr_library *lib, const char *name, const char *full)
|
||||
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.pages = -1;
|
||||
e.has_images = -1;
|
||||
e.probe_state = MR_LIB_PENDING;
|
||||
mr_library_format_subtitle(&e);
|
||||
e.kind = kind;
|
||||
e.size = (kind == MR_LIB_KIND_DIR) ? 0 : (long long)st->st_size;
|
||||
format_subtitle(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
/* 档案册(.cbz/.zip)行:文件即一本书,点入直接阅读。
|
||||
* pages<0=待后台探测(M13 懒算);>=0=已知页数(扫描期注入的旧路径/测试用)。
|
||||
* cover 留空——缩略图由 UI 解压档内首图(mr_zip_cover_path)生成。 */
|
||||
static int add_archive_entry(mr_library *lib, const char *name, const char *full,
|
||||
int pages)
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
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.is_archive = 1;
|
||||
if (pages < 0) {
|
||||
e.pages = -1;
|
||||
e.has_images = -1;
|
||||
e.probe_state = MR_LIB_PENDING;
|
||||
} else {
|
||||
e.pages = pages;
|
||||
e.has_images = (pages > 0) ? 1 : 0;
|
||||
e.probe_state = MR_LIB_READY;
|
||||
}
|
||||
mr_library_format_subtitle(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 单层扫描 */
|
||||
/* ============================================================ */
|
||||
int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
(void)max_depth; /* 兼容旧签名:扫描不再递归 */
|
||||
(void)ud;
|
||||
(void)max_depth; /* 兼容旧签名:扫描不递归 */
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
if (!dir || !dir[0]) return -1;
|
||||
@@ -475,15 +234,15 @@ int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
int is_dir = S_ISDIR(st.st_mode);
|
||||
int is_arch = (!is_dir && archive_pages && S_ISREG(st.st_mode) &&
|
||||
mr_path_is_archive(name));
|
||||
if (!is_dir && !is_arch) continue;
|
||||
if (lib->count >= MR_LIB_SCAN_MAX) { lib->truncated = 1; break; }
|
||||
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;
|
||||
|
||||
int rc = is_dir ? add_dir_entry(lib, name, full)
|
||||
: add_archive_entry(lib, name, full, -1);
|
||||
if (rc != 0) {
|
||||
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;
|
||||
@@ -496,37 +255,6 @@ int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
return mr_library_scan_ex(lib, dir, max_depth, NULL, NULL);
|
||||
}
|
||||
|
||||
int mr_library_apply_probe(mr_library *lib, int idx, const mr_library_probe *p)
|
||||
{
|
||||
if (!lib || !p || idx < 0 || idx >= lib->count) return 0;
|
||||
mr_library_entry *e = &lib->items[idx];
|
||||
e->pages = p->pages;
|
||||
e->empty = p->empty;
|
||||
e->has_images = (p->pages > 0) ? 1 : 0;
|
||||
e->probe_state = MR_LIB_READY;
|
||||
copy_str(e->cover, (int)sizeof e->cover, p->cover);
|
||||
mr_library_format_subtitle(e);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_library_mark_error(mr_library *lib, int idx)
|
||||
{
|
||||
if (!lib || idx < 0 || idx >= lib->count) return 0;
|
||||
mr_library_entry *e = &lib->items[idx];
|
||||
e->pages = -1;
|
||||
e->empty = 0;
|
||||
e->has_images = 0;
|
||||
e->probe_state = MR_LIB_FAILED;
|
||||
e->cover[0] = '\0';
|
||||
mr_library_format_subtitle(e);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
|
||||
{
|
||||
static char trunc_sub[48];
|
||||
@@ -551,12 +279,21 @@ int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = e->name;
|
||||
out->subtitle = e->subtitle;
|
||||
out->is_book = (e->has_images == 1);
|
||||
out->is_archive = e->is_archive;
|
||||
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;
|
||||
@@ -582,16 +319,19 @@ int mr_path_parent(const char *path, char *out, int outsz)
|
||||
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;
|
||||
@@ -618,7 +358,9 @@ static int sibling_is_book(const char *full, const struct stat *st,
|
||||
const char *slash = strrchr(full, '/');
|
||||
const char *name = slash ? slash + 1 : full;
|
||||
if (!mr_path_is_archive(name)) return 0;
|
||||
return archive_pages_at(probe, ud, full) > 0;
|
||||
if (!probe) return 0;
|
||||
int n = probe(full, ud);
|
||||
return n > 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+46
-106
@@ -1,25 +1,18 @@
|
||||
/* logic/library.h — 书库目录浏览(纯 C,不依赖 meow_ui.h)。
|
||||
/* logic/library.h — 书库文件管理器模型(纯 C,不依赖 meow_ui.h)。
|
||||
*
|
||||
* 分层契约同 examples/hello:logic 层只含文件系统与纯数据,宿主 gcc 可直接裸测。
|
||||
*
|
||||
* M13 浏览语义(用户要求:性能 + 访问放开;参考 os.meow.files 的 fsx_scan_dir):
|
||||
* - mr_library_scan / mr_library_scan_ex 只枚举**当前目录一层**:一次 opendir/readdir +
|
||||
* 逐项 lstat,列出全部子目录(自然升序),不做任何递归「子树含图」探测;
|
||||
* - 访问放开:系统目录(MeowAPPs/MeowData/MeowOS/lost+found)不再隐藏,
|
||||
* 空目录/无图目录一律列出并可进入;仅跳过 "."/".." 与其它 "." 开头的隐藏项、
|
||||
* 符号链接(防环);
|
||||
* - 「直接含图目录=一本书」的判定改为**后台懒算**:mr_library_probe_dir 只读一层,
|
||||
* mr_library_probe_path 按行类型(目录/档案册)走同一套 mtime 失效缓存(线程安全),
|
||||
* 结果经 mr_library_apply_probe 补回行(页数/首图/副标题)。首帧/滚动不做子目录 IO;
|
||||
* - 单目录条目上限 MR_LIB_SCAN_MAX(默认 2000):到上限后若还有条目则截断并置
|
||||
* lib->truncated=1(UI 用 mr_library_row_at 末尾的提示行标注)。
|
||||
* M18 浏览语义(用户要求「完全像文件管理器」):
|
||||
* - 列表 = 当前目录一层的「子目录 + 受支持图片(png/jpg/jpeg/bmp) + 档案册(.cbz/.zip)」;
|
||||
* 目录优先,组内自然序;其它普通文件不列;隐藏项("." 开头)与符号链接跳过;
|
||||
* - **没有**「目录含图=一本书」的判定:不探测、不算页数、不取封面、不拼续读角标;
|
||||
* 点目录就是进目录(空目录也进),点图片从该图开始读,点档案册打开档案册;
|
||||
* - 扫描只做一次 opendir/readdir + 逐项 lstat;副标题(类型 · 大小)在扫描时生成一次,
|
||||
* 渲染期零 IO、零字符串格式化(UI row 回调直接取指针);
|
||||
* - 单目录条目上限 MR_LIB_SCAN_MAX:到上限后若还有条目则截断并置 lib->truncated=1。
|
||||
*
|
||||
* M11 档案册(.cbz/.zip):注入 archive_pages 回调时,当前层的档案文件也成行(与目录同列
|
||||
* 自然序混排);档案册的页数同样走懒探测(mr_library_probe_path + 注入回调),logic 层
|
||||
* 不依赖 zip 容器。未注入时(archive_pages==NULL)只列目录,等价 M7 书库。
|
||||
*
|
||||
* M7 续读角标 / 册间衔接:mr_library_format_subtitle 在探测完成后拼「· 续读 Pn」
|
||||
* (mr_cont_badge);mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变。
|
||||
* 仍保留的阅读器辅助:mr_library_dir_has_images / mr_book_adjacent(_ex)
|
||||
* (阅读到册末时翻到同父目录的相邻册,与书库列表模型无关)。
|
||||
*/
|
||||
#ifndef MR_LIBRARY_H
|
||||
#define MR_LIBRARY_H
|
||||
@@ -30,149 +23,96 @@ extern "C" {
|
||||
|
||||
#define MR_PATH_MAX 512
|
||||
#define MR_NAME_MAX 256
|
||||
#define MR_LIB_MAX_DEPTH 8 /* 兼容旧调用:扫描已不递归,该参数被忽略 */
|
||||
#define MR_LIB_MAX_DEPTH 8 /* 兼容旧调用:扫描不递归,该参数被忽略 */
|
||||
|
||||
/* 单目录列出的条目上限(超大目录截断 + UI 提示;可用 -D 覆写便于宿主测试)。 */
|
||||
#ifndef MR_LIB_SCAN_MAX
|
||||
#define MR_LIB_SCAN_MAX 2000
|
||||
#endif
|
||||
|
||||
/* 行懒判定状态:0=待探测(副标题占位「…」)/1=已探测/2=探测失败(不可读)。 */
|
||||
#define MR_LIB_PENDING 0
|
||||
#define MR_LIB_READY 1
|
||||
#define MR_LIB_FAILED 2
|
||||
/* 行类型(列表图标/点击行为由 UI 按此分派)。 */
|
||||
#define MR_LIB_KIND_DIR 0 /* 子目录:点=进入 */
|
||||
#define MR_LIB_KIND_IMAGE 1 /* 受支持图片:点=从该图开始阅读 */
|
||||
#define MR_LIB_KIND_ARCHIVE 2 /* .cbz/.zip 档案册:点=打开档案册 */
|
||||
|
||||
typedef struct mr_library_entry {
|
||||
char name[MR_NAME_MAX]; /* 目录名/档案文件名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径(导航/进 reader/探测用) */
|
||||
char cover[MR_PATH_MAX]; /* 后台探测填:自然序第一张直接子图(档案册留空,UI 解压首图) */
|
||||
char subtitle[48]; /* 占位「…」;探测后 "N pages"/"空目录"/"无图片"/"无法读取" */
|
||||
int pages; /* -1=未判定;>=0=直接图片数(档案册=档内图片页数) */
|
||||
int subdirs; /* 兼容字段(M11 及旧读者):新扫描不统计,恒 0 */
|
||||
int has_images; /* 1=书;0=非书;-1=未判定 */
|
||||
int probe_state; /* MR_LIB_PENDING / MR_LIB_READY / MR_LIB_FAILED */
|
||||
int empty; /* 1=空目录(目录且 probe_state==READY 后有效) */
|
||||
int is_archive; /* 1=.cbz/.zip 档案册行 */
|
||||
char name[MR_NAME_MAX]; /* 文件名/目录名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径 */
|
||||
char subtitle[64]; /* 扫描时预生成一次:「文件夹」/「图片 · 1.2 MB」/「档案册 · 1.2 MB」 */
|
||||
long long size; /* 普通文件字节数;目录=0 */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
} mr_library_entry;
|
||||
|
||||
typedef struct mr_library {
|
||||
mr_library_entry *items;
|
||||
int count; /* 当前层列出的条目数 */
|
||||
int count; /* 当前层列出的条目数 */
|
||||
int cap;
|
||||
int truncated; /* 1=达到 MR_LIB_SCAN_MAX 被截断 */
|
||||
int truncated; /* 1=达到 MR_LIB_SCAN_MAX 被截断 */
|
||||
} mr_library;
|
||||
|
||||
/* 单行探测结果(目录=本层直接图片数;档案册=档内图片页数;均不递归)。 */
|
||||
typedef struct mr_library_probe {
|
||||
int pages; /* 受支持图片数(png/jpg/jpeg/bmp;档案册=档内页数) */
|
||||
int empty; /* 1=目录没有可见子项(档案册恒 0) */
|
||||
char cover[MR_PATH_MAX]; /* 目录的自然序第一张图(档案册留空) */
|
||||
} mr_library_probe;
|
||||
|
||||
/* 行数据(纯内存合成;UI 的每帧 src 回调直接取用,零 IO)。 */
|
||||
/* 行数据(纯内存合成;UI 的每帧 src 回调直接取用,零 IO、零格式化)。 */
|
||||
typedef struct mr_library_row {
|
||||
const char *title; /* 主标题 */
|
||||
const char *subtitle; /* 副标题(可空串) */
|
||||
int is_book; /* 1=书(显示页数/缩略图,点入阅读,无进入箭头) */
|
||||
int is_archive; /* 1=档案册行 */
|
||||
int disabled; /* 1=提示行(不可点,如截断标注) */
|
||||
int index; /* 对应 mr_library.items 下标;提示行=-1 */
|
||||
const char *title; /* 主标题 */
|
||||
const char *subtitle; /* 副标题(预生成,永不为 NULL) */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
int disabled; /* 1=提示行(不可点,如截断标注) */
|
||||
int index; /* 对应 mr_library.items 下标;提示行=-1 */
|
||||
} mr_library_row;
|
||||
|
||||
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
|
||||
int mr_path_is_image(const char *name);
|
||||
|
||||
/* 档案册扩展名判断:cbz / zip(大小写不敏感)。1=档案册。
|
||||
* 注意这只是路径判据;能不能当书读还要看档案里有没有图片页(探测回调)。 */
|
||||
/* 档案册扩展名判断:cbz / zip(大小写不敏感)。1=档案册。 */
|
||||
int mr_path_is_archive(const char *name);
|
||||
|
||||
/* 自然排序比较:数字段按数值比(page2 < page10),数字相等时前导零少的在前;
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序。 */
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序(结果确定,无抖动)。 */
|
||||
int mr_natural_cmp(const char *a, const char *b);
|
||||
|
||||
/* 扫描 dir 的**直接子目录**(单层;不读子目录内容),写入 lib(先释放 lib 旧内容)。
|
||||
/* 扫描 dir 的**直接子项**(单层):子目录 + 受支持图片 + 档案册,写入 lib(先释放旧内容)。
|
||||
* 返回条目数(>=0);dir 不可读/不存在/不是目录返回 -1(lib 仍为空且可安全 free)。
|
||||
* max_depth 仅为兼容旧调用保留,已忽略(不再递归)。 */
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz/.zip 当一本书,与文件夹册同列 */
|
||||
/* ------------------------------------------------------------
|
||||
* logic 层不依赖 zip 容器(保持宿主裸测/链接面干净):档案册探测由 UI 注入。
|
||||
* archive_pages(path, ud) 返回档内图片页数(>0=是一本书),0=不是/坏档。
|
||||
* 传 NULL 时行为与 M7 完全一致(纯目录书库,档案文件不成行)。
|
||||
* 档案册行:name=文件名、path=档案路径、is_archive=1、cover 留空(UI 解压首图)、
|
||||
* 页数/书标记由后台 probe 经同一注入回调懒算。
|
||||
* ============================================================ */
|
||||
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
|
||||
|
||||
int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
/* 释放条目数组并清零(可重复调用)。 */
|
||||
void mr_library_free(mr_library *lib);
|
||||
|
||||
/* 取父目录路径(去掉末尾 '/' 后截断最后一段)。根 "/" 返回 "/"。
|
||||
* 成功 0;参数非法/输出缓冲不足 -1。可一路向上到文件系统 '/'。 */
|
||||
* 成功 0;参数非法/输出缓冲不足 -1。纯路径工具,不做根目录限制(限制在 UI 导航层)。 */
|
||||
int mr_path_parent(const char *path, char *out, int outsz);
|
||||
|
||||
/* ---- 后台懒判定(单层探测;可由工作线程调用) ---- */
|
||||
|
||||
/* 单层探测目录 dir:直接图片数 + 自然序首图 + 是否空目录。不递归、不跟随符号链接。
|
||||
* 0=成功;-1=参数非法/不可读(out 清零)。 */
|
||||
int mr_library_probe_dir(const char *dir, mr_library_probe *out);
|
||||
|
||||
/* 带缓存探测目录:按目录 mtime(秒+纳秒)+ size 失效;内部互斥锁,可多线程并发调用。
|
||||
* 0=成功(可能来自缓存);-1=不可读(失败不缓存)。 */
|
||||
int mr_library_probe_cached(const char *dir, mr_library_probe *out);
|
||||
|
||||
/* 按行探测(目录或档案册),带同一套缓存:
|
||||
* is_archive=0 → 目录(等价 mr_library_probe_cached);
|
||||
* is_archive=1 → 调 archive_pages(path,ud) 取档内页数(cover 留空、empty=0);
|
||||
* archive_pages 为 NULL 时返回 -1。
|
||||
* 0=成功;-1=参数非法/不可读。探测期间 mtime/size 变化则不写入缓存(避免假新鲜)。 */
|
||||
int mr_library_probe_path(const char *path, int is_archive,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
mr_library_probe *out);
|
||||
|
||||
/* 清空探测缓存(目录整体切换/测试用)。 */
|
||||
void mr_library_probe_cache_clear(void);
|
||||
|
||||
/* 把探测结果补回第 idx 行(pages/has_images/cover/subtitle/empty)。
|
||||
* 1=已应用;0=参数非法/越界。 */
|
||||
int mr_library_apply_probe(mr_library *lib, int idx, const mr_library_probe *p);
|
||||
|
||||
/* 标记第 idx 行探测失败(不可读):副标题「无法读取」,视为非书。1=已应用;0=非法。 */
|
||||
int mr_library_mark_error(mr_library *lib, int idx);
|
||||
|
||||
/* 按行当前状态重算副标题(占位/页数/空目录/无图片/无法读取),
|
||||
* 并在书行(has_images==1)后缀「· 续读 Pn」(M7 角标)。 */
|
||||
void mr_library_format_subtitle(mr_library_entry *e);
|
||||
|
||||
/* 行合成(纯内存、零 IO、O(1);UI 线程每帧调用):idx 越界返回 0。
|
||||
* idx==count 且 lib->truncated 时输出末尾提示行(disabled=1)。
|
||||
* 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out);
|
||||
|
||||
/* 目录是否**直接**含受支持图片(= 书库语义里的「一本书」,可直接进 reader)。
|
||||
* 1=有;0=没有/不可读。 */
|
||||
/* 图片行在该目录图片自然序中的下标(0 基);idx 不是图片行/越界返回 -1。
|
||||
*
|
||||
* ⚠️ 这是书库→阅读器的契约:阅读器的目录册(logic/reader/page_source.c)只枚举
|
||||
* 「非隐藏 + mr_path_is_image + 普通文件」并按 mr_natural_cmp 排序。书库扫描用的是
|
||||
* 同一套过滤器与比较器,因此本函数数出的下标就是 mr_reader_open_dir_at() 的起始页。 */
|
||||
int mr_library_image_index(const mr_library *lib, int idx);
|
||||
|
||||
/* 目录是否**直接**含受支持图片。1=有;0=没有/不可读。
|
||||
* 仅用于阅读器的「相邻册」判定,不再是书库行语义。 */
|
||||
int mr_library_dir_has_images(const char *dir);
|
||||
|
||||
/* 册间衔接(M7):同父目录下按书库自然序排列的「直接含图」兄弟册。
|
||||
/* 册间衔接(M7):同父目录下按自然序排列的「直接含图」兄弟册。
|
||||
* delta=+1 下一册 / -1 上一册。1=找到并写入 out 完整路径;
|
||||
* 0=没有相邻册(当前册是首/末册,或当前册不在兄弟表中);-1=参数非法。
|
||||
* 语义与书库一致:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
|
||||
* 语义:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
|
||||
* 只算文件夹册(直接含图目录)——M7 口径不变。 */
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz);
|
||||
|
||||
/* 册间衔接 + 档案册(M11):兄弟表 = 「直接含图目录」∪「含图片页的 .cbz/.zip」,
|
||||
* 两者同列按名称自然序混排(跨册衔接按同父目录自然序)。
|
||||
* archive_pages 传 NULL 时与 mr_book_adjacent 完全等价。 */
|
||||
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
#ifdef MR_LIBRARY_TEST_HOOKS
|
||||
/* 宿主测试用 IO 计数(scan/probe_dir 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
|
||||
/* 宿主测试用 IO 计数(scan/dir_has_images 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
|
||||
typedef struct mr_library_io_stats {
|
||||
long opendirs;
|
||||
long readdirs;
|
||||
|
||||
Reference in new issue
Block a user