feat(reader): M33 目录模式逐级下钻 — 子目录/书分层显示(416 本不再平铺)
- folder 模式从「按第一级目录分段」改为当前目录视图:子目录在前(带子树书数), 直接书在后;进入/返回只过滤已扫平的 g_books,不 readdir、不重扫(M32 索引不动)。 - 纯函数 shelf_build_rows_at(books,n,folder,cwd,...) + nav 工具(enter/up/breadcrumb/ count)全部宿主可测;format 模式行序列/副题逐字不变,path 仍是完整路径(进度键不变)。 - 交互:点目录进入;顶栏返回箭头/左缘返回手势回上一级,根层再返回退出 App;顶栏标题= 当前目录名(按像素宽截断),副题行=面包屑全路径+当前层书数;list/grid 均支持导航。 - 状态:folder↔format、list↔grid 切换保留 cwd;刷新重扫后 cwd 失效则收缩到有书的祖先。 - 边界:空目录不显示(导航只读 g_books,不额外 readdir);深层目录逐级进入;真实名为 「未分类」的目录按普通目录处理,不与根层书合并。 - 测试:shelf_group_smoke 63→99 断言;M27 的按第一级分组断言改写为「逐级进入后可见该书」 (同等强度),新增进入/返回/面包屑/计数/空目录/纯内存/rowcap/UTF-8 截断断言。 - 交付 0.1.7/code8(M32 已占用任务书里的 0.1.6/code7);真机 0.1.4 未被覆盖。
This commit is contained in:
1 parent
190041cd67
commit
2881ce908a
7 files changed
+609
-141
No files matched your search
@@ -70,8 +70,8 @@ void shelf_sort_recent(shelf_book *books, int nbooks)
|
||||
}
|
||||
|
||||
/* 重建一本书的两种副题(扫描后与「刚读完置顶」共用同一份格式化逻辑)。
|
||||
* sub = 格式 · 大小 [· 读](format 模式/宫格)
|
||||
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(folder 模式)
|
||||
* sub = 格式 · 大小 [· 读](format 模式 / 宫格 / M33 folder 导航视图)
|
||||
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(M33 起导航视图不再显示,保留兼容)
|
||||
*/
|
||||
void shelf_fmt_sub(shelf_book *b)
|
||||
{
|
||||
@@ -229,29 +229,7 @@ int shelf_scan_books(const char *root, shelf_book *books, int cap,
|
||||
|
||||
/* ---- 行序列 ---- */
|
||||
|
||||
/* 分组键 = 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);
|
||||
}
|
||||
|
||||
/* format 模式:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致。 */
|
||||
static int build_rows_format(const shelf_book *books, int nbooks,
|
||||
shelf_row *rows, int rowcap)
|
||||
{
|
||||
@@ -265,50 +243,17 @@ static int build_rows_format(const shelf_book *books, int nbooks,
|
||||
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;
|
||||
rows[nrows].section = 1; rows[nrows].dir = 0; rows[nrows].fmt = fmt;
|
||||
rows[nrows].book = -1; rows[nrows].count = 0;
|
||||
rows[nrows].name[0] = 0; rows[nrows].info[0] = 0;
|
||||
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].section = 0; rows[nrows].dir = 0; rows[nrows].fmt = fmt;
|
||||
rows[nrows].book = i; rows[nrows].count = 0;
|
||||
rows[nrows].name[0] = 0; rows[nrows].info[0] = 0;
|
||||
rows[nrows].label = NULL;
|
||||
nrows++;
|
||||
}
|
||||
@@ -316,11 +261,161 @@ static int build_rows_folder(const shelf_book *books, int nbooks,
|
||||
return nrows;
|
||||
}
|
||||
|
||||
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
||||
shelf_row *rows, int rowcap)
|
||||
/* group 相对 cwd 的剩余路径:group==cwd → ""(书直接在当前层);group 在 cwd 下 → 去掉
|
||||
* "cwd/" 前缀的剩余部分;不在 cwd 子树内 → NULL。只做组件边界匹配(a/b 不匹配 a/bc)。 */
|
||||
static const char *rel_under_cwd(const char *group, const char *cwd)
|
||||
{
|
||||
if (!cwd || !cwd[0]) return group;
|
||||
size_t n = strlen(cwd);
|
||||
if (strncmp(group, cwd, n) != 0) return NULL;
|
||||
if (group[n] == '/') return group + n + 1;
|
||||
if (group[n] == 0) return group + n; /* 直接属于 cwd:剩余为空串 */
|
||||
return NULL; /* "a/bc" 不属于 "a/b" */
|
||||
}
|
||||
|
||||
/* 目录行排序:子目录名不区分大小写;同名前缀再按字节序(与 M27 的组名排序口径一致)。 */
|
||||
static int nav_name_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const shelf_row *a = (const shelf_row *)pa, *b = (const shelf_row *)pb;
|
||||
int c = strcasecmp(a->name, b->name);
|
||||
if (c) return c;
|
||||
return strcmp(a->name, b->name);
|
||||
}
|
||||
|
||||
/* folder 模式:当前目录视图(纯内存,只读 books[].group)。
|
||||
* 步骤:①把所有「更深的书」按第一段子目录名各记一行(count 先记 1)②按名排序
|
||||
* ③相邻同名合并(count 累加 = 子树书数)④追加直接属于 cwd 的书(沿用 books[] 全局顺序)。
|
||||
* 目录行借用 rows[] 自身做排序/合并,不额外占内存,也不读盘。 */
|
||||
static int build_rows_folder_at(const shelf_book *books, int nbooks, const char *cwd,
|
||||
shelf_row *rows, int rowcap)
|
||||
{
|
||||
int nd = 0;
|
||||
for (int i = 0; i < nbooks && nd < rowcap; i++) {
|
||||
const char *rel = rel_under_cwd(books[i].group, cwd);
|
||||
if (!rel || !rel[0]) continue; /* 不在本层/直接属于本层的书后处理 */
|
||||
const char *slash = strchr(rel, '/');
|
||||
int len = slash ? (int)(slash - rel) : (int)strlen(rel);
|
||||
if (len <= 0) continue;
|
||||
if (len >= (int)sizeof rows[nd].name) len = (int)sizeof rows[nd].name - 1;
|
||||
shelf_row *r = &rows[nd];
|
||||
memcpy(r->name, rel, (size_t)len);
|
||||
r->name[len] = 0;
|
||||
r->section = 0; r->dir = 1; r->fmt = -1; r->book = -1;
|
||||
r->count = 1; r->info[0] = 0; r->label = NULL;
|
||||
nd++;
|
||||
}
|
||||
qsort(rows, (size_t)nd, sizeof rows[0], nav_name_cmp);
|
||||
|
||||
/* 合并同名(exFAT 大小写不敏感;扫描可能把同一目录的不同拼写带进来)。 */
|
||||
int w = 0;
|
||||
for (int i = 0; i < nd; i++) {
|
||||
if (w > 0 && strcasecmp(rows[w - 1].name, rows[i].name) == 0) {
|
||||
rows[w - 1].count += rows[i].count;
|
||||
} else {
|
||||
if (w != i) rows[w] = rows[i];
|
||||
w++;
|
||||
}
|
||||
}
|
||||
nd = w;
|
||||
for (int i = 0; i < nd; i++)
|
||||
snprintf(rows[i].info, sizeof rows[i].info, "%d 本", rows[i].count);
|
||||
|
||||
/* 本层直接书:book 下标指回 books[],path/进度键不受影响。 */
|
||||
for (int i = 0; i < nbooks && nd < rowcap; i++) {
|
||||
const char *rel = rel_under_cwd(books[i].group, cwd);
|
||||
if (!rel || rel[0]) continue;
|
||||
shelf_row *r = &rows[nd];
|
||||
r->section = 0; r->dir = 0; r->fmt = books[i].fmt; r->book = i;
|
||||
r->count = 0; r->name[0] = 0; r->info[0] = 0; r->label = NULL;
|
||||
nd++;
|
||||
}
|
||||
return nd;
|
||||
}
|
||||
|
||||
int shelf_build_rows_at(const shelf_book *books, int nbooks, int folder_mode,
|
||||
const char *cwd, 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)
|
||||
return folder_mode ? build_rows_folder_at(books, nbooks, cwd ? cwd : "", rows, rowcap)
|
||||
: build_rows_format(books, nbooks, rows, rowcap);
|
||||
}
|
||||
|
||||
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
||||
shelf_row *rows, int rowcap)
|
||||
{
|
||||
return shelf_build_rows_at(books, nbooks, folder_mode, "", rows, rowcap);
|
||||
}
|
||||
|
||||
/* ---- 目录导航工具 ---- */
|
||||
|
||||
int shelf_nav_count_under(const shelf_book *books, int nbooks, const char *cwd)
|
||||
{
|
||||
if (!books || nbooks <= 0) return 0;
|
||||
if (nbooks > SHELF_MAX_BOOKS) nbooks = SHELF_MAX_BOOKS;
|
||||
int n = 0;
|
||||
for (int i = 0; i < nbooks; i++)
|
||||
if (rel_under_cwd(books[i].group, cwd ? cwd : "")) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
int shelf_nav_enter(const char *cwd, const char *name, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
if (!name || !name[0]) return 0;
|
||||
size_t lc = (cwd && cwd[0]) ? strlen(cwd) : 0;
|
||||
size_t ln = strlen(name);
|
||||
size_t need = lc + (lc ? 1 : 0) + ln + 1;
|
||||
if (need > (size_t)outsz) return 0; /* 超长:不写 out(不产生半截路径) */
|
||||
int n;
|
||||
if (lc) n = snprintf(out, (size_t)outsz, "%s/%s", cwd, name);
|
||||
else n = snprintf(out, (size_t)outsz, "%s", name);
|
||||
return (n > 0 && n < outsz) ? 1 : 0;
|
||||
}
|
||||
|
||||
int shelf_nav_up(const char *cwd, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
if (!cwd || !cwd[0]) { out[0] = 0; return 0; }
|
||||
const char *slash = strrchr(cwd, '/');
|
||||
int len = slash ? (int)(slash - cwd) : 0;
|
||||
if (len >= outsz) len = outsz - 1;
|
||||
memcpy(out, cwd, (size_t)len);
|
||||
out[len] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int shelf_breadcrumb(const char *cwd, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
char full[400];
|
||||
int n;
|
||||
if (cwd && cwd[0]) n = snprintf(full, sizeof full, "Books/%s", cwd);
|
||||
else n = snprintf(full, sizeof full, "Books/");
|
||||
if (n < 0) { out[0] = 0; return 0; }
|
||||
if (n < outsz) {
|
||||
memcpy(out, full, (size_t)n + 1);
|
||||
return n;
|
||||
}
|
||||
if (outsz <= 4) { out[0] = 0; return 0; }
|
||||
/* 超长:保留末尾(当前目录更重要),前缀 "…"(3 字节 UTF-8)。 */
|
||||
int keep = outsz - 1 - 3;
|
||||
int start = n - keep;
|
||||
if (start < 0) start = 0;
|
||||
while (start < n && ((unsigned char)full[start] & 0xC0) == 0x80) start++;
|
||||
int len = n - start;
|
||||
if (len > outsz - 1 - 3) len = outsz - 1 - 3;
|
||||
memcpy(out, "…", 3);
|
||||
memcpy(out + 3, full + start, (size_t)len);
|
||||
out[3 + len] = 0;
|
||||
return 3 + len;
|
||||
}
|
||||
|
||||
int shelf_dir_name(const char *cwd, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = 0;
|
||||
if (!cwd || !cwd[0]) return 0;
|
||||
const char *slash = strrchr(cwd, '/');
|
||||
return shelf_copy_utf8(out, outsz, slash ? slash + 1 : cwd);
|
||||
}
|
||||
Reference in new issue
Block a user