图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)

rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
This commit is contained in:
AskaEth committed 2026-11-05 12:23:31 +08:00
1 parent c9cff99af3
commit 9f34417c49
12 files changed
+2299 -358

No files matched your search

+354 -122
View File
@@ -1,38 +1,57 @@
/* logic/library.c — 书库目录扫描 + 自然排序(纯 C)。
/* logic/library.c — 书库目录浏览:单层枚举 + 后台懒判定(纯 C)。
*
* 扫描分两步:
* 1) scan_info():递归探测一个目录是否「最终含图」,并带回直接图片数 / 含图子目录数 /
* 缩略图源(优先本目录自然序第一张,其次第一个含图子目录);
* 2) mr_library_scan():只对目标目录的**直接子目录**调用 scan_info(),把 nav=1 的目录
* 收进结果并自然排序——因此根视图不会列出后代条目,进入子目录时再重新扫描该层。
* 扫描(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_free() 释放。
* 懒判定(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」角标。
*
* M7 追加(不改扫描语义):
* - 续读角标:含图册的副标题后缀「· 续读 Pn」(读 logic/reader 的每册 last page);
* - 册间衔接:mr_book_adjacent 给出同父目录下按书库自然序的相邻「含图册」,
* 供阅读器末页前进/首页后退跨册;mr_library_dir_has_images 是共享的「是不是一本书」判据。
*
* M11 追加(档案册,仍然不改目录扫描的主体语义):
* - 目标目录的**直接子文件**里 .cbz/.zip 也成行(行=文件名+档内页数,has_images=1);
* - 档案册探测由调用方注入(mr_lib_archive_fn),logic 层不依赖 zip 容器;
* 不注入时所有 *_ex 入口与 M7 行为逐字一致;
* - 册间衔接的兄弟表从「含图目录」扩成「含图目录 ∪ 含图片页的档案册」,同列自然序。
* 小函数边界(add_dir_entry / add_archive_entry / entry_subtitle / entry_badge /
* sibling_is_book)刻意留给 M13 的书库重写复用。
* M11 档案册:注入 archive_pages 时 .cbz/.zip 直接子文件也成行(懒探测页数);
* 未注入时只列目录(M7 书库语义)。册间衔接的兄弟表 = 含图目录 ∪ 含图片页的档案册。
*/
#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>
#include <strings.h>
#include <sys/stat.h>
/* 系统目录不参与书库(SD 根上的 MeowOS 目录树)。 */
#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" };
@@ -119,16 +138,7 @@ int mr_natural_cmp(const char *a, const char *b)
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
}
typedef struct scan_info {
int pages; /* 直接图片数 */
int subdirs; /* 含图子目录数 */
int archives; /* 直接档案册数(.cbz/.zip 且探测到图片页) */
int nav; /* 1=本目录最终含书(可直接看或可导航到书) */
char cover[MR_PATH_MAX]; /* 缩略图源 */
char first_name[MR_NAME_MAX]; /* 本目录自然序第一张图片的文件名(比较用) */
} scan_info;
/* 档案册探测(UI 注入;NULL=纯目录书库,行为与 M7 一致)。 */
/* 档案册探测(UI 注入;NULL=纯目录书库,档案文件不成行)。 */
static int archive_pages_at(mr_lib_archive_fn probe, void *ud, const char *path)
{
if (!probe || !path || !path[0]) return 0;
@@ -136,48 +146,234 @@ static int archive_pages_at(mr_lib_archive_fn probe, void *ud, const char *path)
return n > 0 ? n : 0;
}
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si,
mr_lib_archive_fn probe, void *ud)
/* ============================================================ */
/* 单层探测 + mtime/size 失效缓存 */
/* ============================================================ */
int mr_library_probe_dir(const char *dir, mr_library_probe *out)
{
memset(si, 0, sizeof *si);
DIR *d = opendir(path);
if (!d) return;
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 (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (ignored_dir_name(name)) continue;
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, path, name) != 0) continue;
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;
if (lstat(full, &st) != 0 || !S_ISREG(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, probe, ud);
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)) {
if (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);
}
} else if (mr_path_is_archive(name)) {
if (archive_pages_at(probe, ud, full) > 0) si->archives++;
}
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);
si->nav = (si->pages > 0 || si->subdirs > 0 || si->archives > 0);
return 0;
}
#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)
{
#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, "无图片");
break;
case MR_LIB_FAILED:
snprintf(e->subtitle, sizeof e->subtitle, "无法读取");
break;
default:
snprintf(e->subtitle, sizeof e->subtitle, "…");
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)
@@ -187,6 +383,7 @@ void mr_library_free(mr_library *lib)
lib->items = NULL;
lib->count = 0;
lib->cap = 0;
lib->truncated = 0;
}
static int lib_add(mr_library *lib, const mr_library_entry *e)
@@ -210,51 +407,22 @@ static int entry_cmp(const void *pa, const void *pb)
return mr_natural_cmp(a->name, b->name);
}
/* 副标题(M11 扩展):直接含图 → pages;只有档案册 → books;否则 folders。 */
static void entry_subtitle(mr_library_entry *e, int archive_books)
{
if (e->has_images)
snprintf(e->subtitle, sizeof e->subtitle, "%d page%s", e->pages,
e->pages == 1 ? "" : "s");
else if (archive_books > 0)
snprintf(e->subtitle, sizeof e->subtitle, "%d book%s", archive_books,
archive_books == 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,副标题保持原样。 */
static void entry_badge(mr_library_entry *e)
{
if (!e->has_images) 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);
}
/* 文件夹册(含可导航中间目录)一行。 */
static int add_dir_entry(mr_library *lib, const char *name, const char *full,
const scan_info *si)
/* 文件夹册行(M13):一律列出、初始待探测;是否「书」由后台 probe 决定。 */
static int add_dir_entry(mr_library *lib, const char *name, const char *full)
{
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;
copy_str(e.cover, (int)sizeof e.cover, si->cover);
e.pages = si->pages;
e.subdirs = si->subdirs;
e.has_images = (si->pages > 0);
entry_subtitle(&e, si->archives);
entry_badge(&e);
e.pages = -1;
e.has_images = -1;
e.probe_state = MR_LIB_PENDING;
mr_library_format_subtitle(&e);
return lib_add(lib, &e);
}
/* 档案册(.cbz/.zip)一行:文件即一本书,点入直接阅读。
/* 档案册(.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)
@@ -263,52 +431,58 @@ static int add_archive_entry(mr_library *lib, const char *name, const char *full
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 = pages;
e.subdirs = 0;
e.has_images = 1;
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", pages, pages == 1 ? "" : "s");
entry_badge(&e);
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;
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;
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 (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (ignored_dir_name(name)) continue;
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 rc = 0;
if (S_ISDIR(st.st_mode)) {
scan_info si;
scan_dir(full, 1, max_depth, &si, archive_pages, ud);
if (!si.nav) continue;
rc = add_dir_entry(lib, name, full, &si);
} else if (S_ISREG(st.st_mode) && mr_path_is_archive(name)) {
int pages = archive_pages_at(archive_pages, ud, full);
if (pages <= 0) continue; /* 坏档/无图片页:不当书 */
rc = add_archive_entry(lib, name, full, pages);
} else {
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 rc = is_dir ? add_dir_entry(lib, name, full)
: add_archive_entry(lib, name, full, -1);
if (rc != 0) {
closedir(d);
mr_library_free(lib);
@@ -317,7 +491,8 @@ int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
}
closedir(d);
if (lib->count > 1) qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
if (lib->count > 1)
qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
return lib->count;
}
@@ -326,6 +501,62 @@ 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];
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->is_book = (e->has_images == 1);
out->is_archive = e->is_archive;
out->index = idx;
return 1;
}
int mr_path_parent(const char *path, char *out, int outsz)
{
if (!path || !out || outsz <= 0) return -1;
@@ -346,7 +577,7 @@ int mr_path_parent(const char *path, char *out, int outsz)
}
/* ============================================================ */
/* 册间衔接(M7) */
/* 册间衔接(M7 / M11) */
/* ============================================================ */
int mr_library_dir_has_images(const char *dir)
{
@@ -427,7 +658,8 @@ int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
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)) continue;
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) {