Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_model.c
T
AskaEth dc9bfdbdaf perf(reader): M31 字体路径记忆化(开书字体查找 35 目录 walk → 1 次 stat)+ 「· 读」徽标即时刷新
字体:find_font_rec/resolve_font 从 reader.c 抽到 app/font_resolve.c 并加进程内记忆化。
只缓存成功路径;命中后仍做一次 access() 校验(字库被删即重扫);没找到不缓存,
用户之后拷入的字库下次开书即可找到。顶栏「刷新」额外调 font_cache_invalidate(),
给「新拷入字库、旧字库仍在」一条显式失效路径。宿主实测:35 目录树 walk 0.026~0.54ms
(=300+ 次系统调用)vs 命中 0.001ms(1 次 access);开书路径每次省一次完整 walk。

徽标:副题格式化抽成 shelf_fmt_sub()(扫描与「刚读完置顶」共用一份实现),
promote_just_read() 改 ts 后立即重算这本的 sub/sub_folder,「· 读」不再等下次全量刷新。

回归:test_reader_core 6846、shelf_group_smoke 63(+8)、font_resolve_smoke 14 全绿;
build_reader.sh 0 warning;扫描缓存/进度语义未改。app.json 0.1.3/code4 → 0.1.4/code5。
2026-11-10 13:39:32 +08:00

303 lines
11 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.
/* app/shelf_model.c — 书架数据模型实现(见 app/shelf_model.h)。 */
#include "shelf_model.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int shelf_fmt_from_ext(const char *name)
{
const char *d = strrchr(name, '.');
if (!d) return -1;
if (strcasecmp(d, ".txt") == 0) return 0;
if (strcasecmp(d, ".fb2") == 0) return 1;
if (strcasecmp(d, ".epub") == 0) return 2;
if (strcasecmp(d, ".mobi") == 0) return 3;
if (strcasecmp(d, ".cbz") == 0) return 4;
return -1;
}
const char *shelf_fmt_name(int f)
{
switch (f) {
case 0: return "TXT";
case 1: return "FB2";
case 2: return "EPUB";
case 3: return "MOBI";
case 4: return "CBZ";
default: return "?";
}
}
void shelf_fmt_size(char *out, int outsz, long size)
{
if (!out || outsz <= 0) return;
if (size >= 1 << 20) snprintf(out, (size_t)outsz, "%.1fMB", (double)size / (1 << 20));
else if (size >= 1 << 10) snprintf(out, (size_t)outsz, "%.0fKB", (double)size / (1 << 10));
else snprintf(out, (size_t)outsz, "%ldB", size);
}
int shelf_copy_utf8(char *dst, int cap, const char *src)
{
if (!dst || cap <= 0) return 0;
if (!src) { dst[0] = 0; return 0; }
int limit = cap - 1;
int n = 0;
while (src[n] && n < limit) n++;
if (src[n]) {
/* 截断:若 src[n] 是续字节(10xxxxxx),回退到该字符首字节之前,整段丢弃。 */
while (n > 0 && ((unsigned char)src[n] & 0xC0) == 0x80) n--;
}
memcpy(dst, src, (size_t)n);
dst[n] = 0;
return n;
}
static int cmp_recent_then_name(const void *a, const void *b)
{
const shelf_book *x = (const shelf_book *)a, *y = (const shelf_book *)b;
if (x->ts != y->ts) return (x->ts < y->ts) ? 1 : -1; /* 最近在前 */
return strcasecmp(x->title, y->title);
}
/* 最近阅读排序(扫描后 / 刚读完置顶共用同一口径)。 */
void shelf_sort_recent(shelf_book *books, int nbooks)
{
if (!books || nbooks < 2) return;
qsort(books, (size_t)nbooks, sizeof(shelf_book), cmp_recent_then_name);
}
/* 重建一本书的两种副题(扫描后与「刚读完置顶」共用同一份格式化逻辑)。
* sub = 格式 · 大小 [· 读](format 模式/宫格)
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(folder 模式)
*/
void shelf_fmt_sub(shelf_book *b)
{
if (!b) return;
char sz[24];
shelf_fmt_size(sz, sizeof sz, b->size);
/* 默认副题:与 M27 前逐字一致(format 模式/宫格都用它)。 */
snprintf(b->sub, sizeof b->sub, "%s · %s%s", shelf_fmt_name(b->fmt), sz,
b->ts ? " · 读" : "");
/* folder 副题:第一级之后的子路径(分类/作品)前置,其余同默认副题。 */
const char *rest = strchr(b->group, '/');
if (rest) rest++;
if (rest && rest[0]) {
char pre[100];
shelf_copy_utf8(pre, (int)sizeof pre, rest);
snprintf(b->sub_folder, sizeof b->sub_folder, "%s · %s", pre, b->sub);
} else {
snprintf(b->sub_folder, sizeof b->sub_folder, "%s", b->sub);
}
}
typedef struct scan_ctx {
shelf_book *books;
int cap;
int n;
int truncated;
int depth_skipped;
shelf_ts_fn ts_fn;
} scan_ctx;
/* 统计 dir 子树里还能识别的书籍文件数(只数不收集)。自身留 8 层硬上限,
* 防符号链接/异常深目录把计数也拖死。返回书籍文件数。 */
static int count_books_under(const char *dir, int depth)
{
if (depth > SHELF_SCAN_MAX_DEPTH + 8) return 0;
int n = 0;
DIR *dp = opendir(dir);
if (!dp) return 0;
struct dirent *de;
while ((de = readdir(dp))) {
if (de->d_name[0] == '.') continue;
char path[400];
if (snprintf(path, sizeof path, "%s/%s", dir, de->d_name) >= (int)sizeof path) continue;
struct stat st;
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) { n += count_books_under(path, depth + 1); continue; }
if (shelf_fmt_from_ext(de->d_name) >= 0) n++;
}
closedir(dp);
return n;
}
/* 递归收集。rel = 相对 root 的完整目录路径(root 自身为 ""),
* top = 第一级目录名(root 自身/根下文件为 "")。 */
static void scan_dir(scan_ctx *c, const char *dir, int depth, const char *rel, const char *top)
{
if (depth > SHELF_SCAN_MAX_DEPTH) return; /* 防深嵌套/循环 */
if (c->n >= c->cap) return; /* 已满:不再进新目录 */
DIR *dp = opendir(dir);
if (!dp) return;
struct dirent *de;
while ((de = readdir(dp))) {
if (de->d_name[0] == '.') continue;
char path[400];
if (snprintf(path, sizeof path, "%s/%s", dir, de->d_name) >= (int)sizeof path) continue;
struct stat st;
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
if (depth + 1 > SHELF_SCAN_MAX_DEPTH) {
c->depth_skipped += count_books_under(path, depth + 1); /* 超深:计数→可见提示 */
continue;
}
if (c->n >= c->cap) {
/* 上限卡在目录边界:探一下这个子树是否真有书,有书才算被截断。
* 只探空目录不置位,避免“正好装满且后面全是空目录”误报。 */
if (!c->truncated && count_books_under(path, depth + 1) > 0) c->truncated = 1;
if (c->truncated) break; /* 已确认丢书,后面的目录/文件不必再跑 */
continue;
}
char subrel[SHELF_GROUP_MAX * 16];
snprintf(subrel, sizeof subrel, "%s%s%s", rel, rel[0] ? "/" : "", de->d_name);
/* 第一级目录名 = top;更深层沿用。de->d_name 在递归返回前有效。 */
scan_dir(c, path, depth + 1, subrel, depth == 0 ? de->d_name : top);
continue;
}
int f = shelf_fmt_from_ext(de->d_name);
if (f < 0) continue;
if (c->n >= c->cap) { c->truncated = 1; break; } /* 超限:可见提示,不静默 */
shelf_book *b = &c->books[c->n];
shelf_copy_utf8(b->path, (int)sizeof b->path, path);
shelf_copy_utf8(b->title, (int)sizeof b->title, de->d_name);
char *dot = strrchr(b->title, '.');
if (dot) *dot = 0; /* 标题去扩展名 */
shelf_copy_utf8(b->group, (int)sizeof b->group, rel);
shelf_copy_utf8(b->top, (int)sizeof b->top, top);
b->fmt = f;
b->size = (long)st.st_size;
b->ts = c->ts_fn ? c->ts_fn(b->path) : 0;
shelf_fmt_sub(b); /* 副题:格式 · 大小 [· 读](+ folder 子路径) */
c->n++;
}
closedir(dp);
}
int shelf_scan_books(const char *root, shelf_book *books, int cap,
shelf_ts_fn ts_fn, shelf_scan_report *rep)
{
if (rep) { rep->nbooks = 0; rep->truncated = 0; rep->depth_skipped = 0; rep->root_missing = 0; }
if (!root || !books || cap <= 0) {
if (rep) rep->root_missing = 1;
return 0;
}
DIR *dp = opendir(root);
if (!dp) {
if (rep) rep->root_missing = 1;
return 0;
}
closedir(dp);
scan_ctx c;
c.books = books; c.cap = cap; c.n = 0; c.truncated = 0; c.depth_skipped = 0; c.ts_fn = ts_fn;
scan_dir(&c, root, 0, "", "");
shelf_sort_recent(books, c.n);
if (rep) {
rep->nbooks = c.n;
rep->truncated = c.truncated;
rep->depth_skipped = c.depth_skipped;
}
return c.n;
}
/* ---- 行序列 ---- */
/* 分组键 = top(Books 第一级目录名)。长度截到第一个 '/',排序时忽略大小写。 */
static int top_len(const char *s)
{
const char *p = strchr(s, '/');
return p ? (int)(p - s) : (int)strlen(s);
}
static int top_cmp(const char *a, const char *b)
{
int la = top_len(a), lb = top_len(b);
if (!la != !lb) return la ? 1 : -1; /* 根组 "" 恒置顶 */
int m = la < lb ? la : lb;
int c = strncasecmp(a, b, (size_t)m);
if (c) return c;
return la - lb;
}
static int top_eq(const char *a, const char *b) { return top_cmp(a, b) == 0; }
/* 段头排序:根组(top=="")恒置顶,其余按第一级目录名不区分大小写排序。 */
static int cmp_group_key(const void *pa, const void *pb)
{
return top_cmp(*(const char *const *)pa, *(const char *const *)pb);
}
static int build_rows_format(const shelf_book *books, int nbooks,
shelf_row *rows, int rowcap)
{
static const int fmts[] = { 0, 1, 2, 3, 4 };
int nrows = 0;
for (size_t fi = 0; fi < sizeof fmts / sizeof fmts[0]; fi++) {
int fmt = fmts[fi];
int have = 0;
if (nrows >= rowcap) break;
for (int i = 0; i < nbooks; i++) {
if (books[i].fmt != fmt) continue;
if (!have) {
if (nrows >= rowcap) break;
rows[nrows].section = 1; rows[nrows].fmt = fmt;
rows[nrows].book = -1; rows[nrows].label = NULL;
nrows++;
have = 1;
}
if (nrows >= rowcap) break;
rows[nrows].section = 0; rows[nrows].fmt = fmt;
rows[nrows].book = i; rows[nrows].label = NULL;
nrows++;
}
}
return nrows;
}
static int build_rows_folder(const shelf_book *books, int nbooks,
shelf_row *rows, int rowcap)
{
/* 去重分组键(books[].top,""=根组)。键指向 books 内字符串,生存期覆盖 rows。 */
static const char *keys[SHELF_MAX_BOOKS];
int nkeys = 0;
for (int i = 0; i < nbooks; i++) {
const char *k = books[i].top;
int seen = 0;
for (int j = 0; j < nkeys; j++) {
if (top_eq(keys[j], k)) { seen = 1; break; }
}
if (!seen) keys[nkeys++] = k;
}
qsort(keys, (size_t)nkeys, sizeof keys[0], cmp_group_key);
int nrows = 0;
for (int ki = 0; ki < nkeys; ki++) {
if (nrows >= rowcap) break;
rows[nrows].section = 1;
rows[nrows].fmt = -1;
rows[nrows].book = -1;
rows[nrows].label = keys[ki][0] ? keys[ki] : SHELF_GROUP_ROOT;
nrows++;
for (int i = 0; i < nbooks; i++) {
if (!top_eq(books[i].top, keys[ki])) continue;
if (nrows >= rowcap) break;
rows[nrows].section = 0;
rows[nrows].fmt = books[i].fmt;
rows[nrows].book = i;
rows[nrows].label = NULL;
nrows++;
}
}
return nrows;
}
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
shelf_row *rows, int rowcap)
{
if (!books || !rows || nbooks <= 0 || rowcap <= 0) return 0;
if (nbooks > SHELF_MAX_BOOKS) nbooks = SHELF_MAX_BOOKS;
return folder_mode ? build_rows_folder(books, nbooks, rows, rowcap)
: build_rows_format(books, nbooks, rows, rowcap);
}