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
@@ -30,8 +30,14 @@ static void app_on_swipe(int dir, int dx, int dy)
|
|||||||
if (settings_is_active()) { return; } /* 设置页滑动不翻页 */
|
if (settings_is_active()) { return; } /* 设置页滑动不翻页 */
|
||||||
if (reader_is_active()) { reader_on_swipe(dir, dx, dy); return; }
|
if (reader_is_active()) { reader_on_swipe(dir, dx, dy); return; }
|
||||||
if (shelf_is_active()) {
|
if (shelf_is_active()) {
|
||||||
/* 书架根页左缘右滑(返回手势)→ 退出 App(镜像 hello_app/files_app 的 LEFT→返回)。 */
|
/* 书架根页左缘右滑(返回手势):
|
||||||
if (dir == MEOW_SWIPE_LEFT) { meow_ui_request_exit(); return; }
|
* folder 模式且不在 Books 根 → 回上一级目录(M33);
|
||||||
|
* 已在根 / format 模式 → 退出 App(与既有行为一致)。 */
|
||||||
|
if (dir == MEOW_SWIPE_LEFT) {
|
||||||
|
if (shelf_back()) return;
|
||||||
|
meow_ui_request_exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
shelf_on_swipe(dir, dx, dy); return;
|
shelf_on_swipe(dir, dx, dy); return;
|
||||||
}
|
}
|
||||||
if (dir == MEOW_SWIPE_LEFT) { meow_ui_request_exit(); }
|
if (dir == MEOW_SWIPE_LEFT) { meow_ui_request_exit(); }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.6","version_code":7,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true}
|
{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1.7","version_code":8,"framework":1,"fonts":"common","author":"MeowOS","icon":"icon.png","volume_page":true}
|
||||||
+216
-30
@@ -10,9 +10,13 @@
|
|||||||
* - 封面:MVP 无封面提取(engine F1);无则圆角占位(surfaceContainerHigh + 格式徽标)。
|
* - 封面:MVP 无封面提取(engine F1);无则圆角占位(surfaceContainerHigh + 格式徽标)。
|
||||||
*
|
*
|
||||||
* M27 分组(`ui.shelf_group`,默认 format 保持旧行为):
|
* M27 分组(`ui.shelf_group`,默认 format 保持旧行为):
|
||||||
* format = 按格式分段(TXT/FB2/EPUB/MOBI/CBZ);
|
* format = 按格式分段(TXT/FB2/EPUB/MOBI/CBZ),与改动前完全一致;
|
||||||
* folder = 按 Books/ 下的「第一级目录」分段(根下=未分类),更深子路径(分类/作品)进副题。
|
* folder = **目录导航**(M33):书架只显示当前目录(cwd)的内容——子目录在前(带子树书数)、
|
||||||
* 扫描/行序列在 app/shelf_model.c(纯 C,宿主可测);本文件只做控件装配与绘制。
|
* 直接属于该层的书在后;点目录进入、点书打开;左缘返回/顶栏返回箭头回上一级,
|
||||||
|
* 到 Books 根再返回才退出 App。行序列在 app/shelf_model.c(纯 C,宿主可测);
|
||||||
|
* 本文件只维护 cwd 并做控件装配/绘制。导航只过滤已扫平的 g_books,不重扫/不 readdir。
|
||||||
|
* M33 状态:folder↔format 切换与 list↔grid 切换都**保留 cwd**(切回 folder 还在原目录);
|
||||||
|
* 顶栏「刷新」重扫后若 cwd 已不存在(目录/书被删),收缩到最近的仍有书的祖先。
|
||||||
*
|
*
|
||||||
* M29 性能(用户反馈「几百本书首页卡得飞起」):
|
* M29 性能(用户反馈「几百本书首页卡得飞起」):
|
||||||
* - **扫描结果进程内缓存**:只在本进程首次进入书架时扫 Books/;从阅读页返回不再重扫
|
* - **扫描结果进程内缓存**:只在本进程首次进入书架时扫 Books/;从阅读页返回不再重扫
|
||||||
@@ -51,6 +55,8 @@ static shelf_book g_books[SHELF_MAX_BOOKS];
|
|||||||
static int g_nbooks;
|
static int g_nbooks;
|
||||||
static int g_scanned; /* 1=本进程已扫过 Books/(进书架缓存;「刷新」动作强制重扫) */
|
static int g_scanned; /* 1=本进程已扫过 Books/(进书架缓存;「刷新」动作强制重扫) */
|
||||||
static int g_active;
|
static int g_active;
|
||||||
|
static char g_cwd[SHELF_GROUP_MAX]; /* M33 folder 模式当前相对目录;""=Books 根 */
|
||||||
|
static char g_pending_enter[SHELF_GROUP_MAX]; /* 目录行点击:等 tap_at 返回后再重建列表 */
|
||||||
/* M30:刚打开过的书(返回书架时只刷这一本的 ts 并重排,避免为「最近阅读置顶」重扫全盘)。 */
|
/* M30:刚打开过的书(返回书架时只刷这一本的 ts 并重排,避免为「最近阅读置顶」重扫全盘)。 */
|
||||||
static char g_just_read[400];
|
static char g_just_read[400];
|
||||||
static meow_widget *g_list; /* listview(布局=list) */
|
static meow_widget *g_list; /* listview(布局=list) */
|
||||||
@@ -68,6 +74,10 @@ static uint64_t g_notice_until; /* 提示到期时刻(meow_ui_time_ms;6s
|
|||||||
static shelf_row g_rows[SHELF_MAX_ROWS];
|
static shelf_row g_rows[SHELF_MAX_ROWS];
|
||||||
static int g_nrows;
|
static int g_nrows;
|
||||||
|
|
||||||
|
/* 控件装配/顶栏定义在本文件后部;目录导航的进/退要重建它们。 */
|
||||||
|
static void shelf_appbar_build(void);
|
||||||
|
static void shelf_build_content(void);
|
||||||
|
|
||||||
static const char *fmt_name(int f) { return shelf_fmt_name(f); }
|
static const char *fmt_name(int f) { return shelf_fmt_name(f); }
|
||||||
|
|
||||||
/* 进度时间戳(分组/排序用;不改 path,故进度键不受分组影响)。 */
|
/* 进度时间戳(分组/排序用;不改 path,故进度键不受分组影响)。 */
|
||||||
@@ -149,10 +159,24 @@ static void scan_books_first(void)
|
|||||||
scan_books_full();
|
scan_books_full();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 排队行:folder 模式按目录分段,否则按格式(默认)。 */
|
/* 排队行:folder 模式 = 当前目录 cwd 的导航视图;否则按格式(默认)。
|
||||||
|
* 纯内存过滤 g_books,不触发扫描(M29/M32 语义不变)。 */
|
||||||
static void build_rows(void)
|
static void build_rows(void)
|
||||||
{
|
{
|
||||||
g_nrows = shelf_build_rows(g_books, g_nbooks, g_group_folder, g_rows, SHELF_MAX_ROWS);
|
g_nrows = shelf_build_rows_at(g_books, g_nbooks, g_group_folder, g_cwd,
|
||||||
|
g_rows, SHELF_MAX_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* folder 模式下 cwd 失效收缩:目录/书被删(重扫后)时回退到最近的仍有书的祖先。
|
||||||
|
* 导航视图只能从「有书的目录」进入,所以 count_under==0 即视为不存在。 */
|
||||||
|
static void nav_validate_cwd(void)
|
||||||
|
{
|
||||||
|
char parent[SHELF_GROUP_MAX];
|
||||||
|
while (g_group_folder && g_cwd[0] &&
|
||||||
|
shelf_nav_count_under(g_books, g_nbooks, g_cwd) == 0) {
|
||||||
|
shelf_nav_up(g_cwd, parent, (int)sizeof parent);
|
||||||
|
snprintf(g_cwd, sizeof g_cwd, "%s", parent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* M30:从阅读页返回时把「刚读完」置顶。只读这一本的进度文件 + 重排内存里的 g_books,
|
/* M30:从阅读页返回时把「刚读完」置顶。只读这一本的进度文件 + 重排内存里的 g_books,
|
||||||
@@ -176,6 +200,8 @@ static void promote_just_read(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- list 数据源(vsource + 段头;container on) ---- */
|
/* ---- list 数据源(vsource + 段头;container on) ---- */
|
||||||
|
static void shelf_nav_enter_dir(const char *name); /* 定义在控件装配之后 */
|
||||||
|
|
||||||
static int shelf_list_count(void *ctx) { (void)ctx; return g_nrows; }
|
static int shelf_list_count(void *ctx) { (void)ctx; return g_nrows; }
|
||||||
static void shelf_list_bind(int idx, meow_lv_item *item, void *ctx)
|
static void shelf_list_bind(int idx, meow_lv_item *item, void *ctx)
|
||||||
{
|
{
|
||||||
@@ -190,23 +216,38 @@ static void shelf_list_bind(int idx, meow_lv_item *item, void *ctx)
|
|||||||
item->subtitle = NULL;
|
item->subtitle = NULL;
|
||||||
item->section = 0;
|
item->section = 0;
|
||||||
if (idx < 0 || idx >= g_nrows) return;
|
if (idx < 0 || idx >= g_nrows) return;
|
||||||
if (g_rows[idx].section) {
|
if (g_rows[idx].section) { /* format 段头 */
|
||||||
item->section = 1;
|
item->section = 1;
|
||||||
item->title = g_rows[idx].label ? g_rows[idx].label : fmt_name(g_rows[idx].fmt);
|
item->title = g_rows[idx].label ? g_rows[idx].label : fmt_name(g_rows[idx].fmt);
|
||||||
item->enabled = 0;
|
item->enabled = 0;
|
||||||
} else {
|
} else if (g_rows[idx].dir) { /* folder 目录行 */
|
||||||
|
item->title = g_rows[idx].name;
|
||||||
|
item->subtitle = g_rows[idx].info; /* "N 本" */
|
||||||
|
item->icon_kind = MEOW_LIST_ICON_FOLDER;
|
||||||
|
item->right = MEOW_LIST_RIGHT_ARROW;
|
||||||
|
} else { /* 书行 */
|
||||||
int bi = g_rows[idx].book;
|
int bi = g_rows[idx].book;
|
||||||
if (bi < 0 || bi >= g_nbooks) return;
|
if (bi < 0 || bi >= g_nbooks) return;
|
||||||
item->title = g_books[bi].title;
|
item->title = g_books[bi].title;
|
||||||
item->subtitle = g_group_folder ? g_books[bi].sub_folder : g_books[bi].sub;
|
/* folder 导航视图里书就在当前层,副题用默认「格式 · 大小 [· 读]」;
|
||||||
|
* 完整深层路径不再重复显示(用户已经逐级走进来了)。format 模式不变。 */
|
||||||
|
item->subtitle = g_books[bi].sub;
|
||||||
item->icon_kind = MEOW_LIST_ICON_FILE;
|
item->icon_kind = MEOW_LIST_ICON_FILE;
|
||||||
item->right = MEOW_LIST_RIGHT_ARROW;
|
item->right = MEOW_LIST_RIGHT_ARROW;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 目录行点击:不能在 SDK 的 tap_at 回调里重建控件(会 free 正在遍历的 list/grid)。
|
||||||
|
* 只记下目标,等 shelf_on_tap 里 tap_at 返回后再进入。 */
|
||||||
|
static void shelf_nav_queue_enter(const char *name)
|
||||||
|
{
|
||||||
|
snprintf(g_pending_enter, sizeof g_pending_enter, "%s", name ? name : "");
|
||||||
|
}
|
||||||
|
|
||||||
static void shelf_list_tapcb(int idx, int op, void *ctx)
|
static void shelf_list_tapcb(int idx, int op, void *ctx)
|
||||||
{
|
{
|
||||||
(void)op; (void)ctx;
|
(void)op; (void)ctx;
|
||||||
if (idx < 0 || idx >= g_nrows || g_rows[idx].section) return;
|
if (idx < 0 || idx >= g_nrows || g_rows[idx].section) return;
|
||||||
|
if (g_rows[idx].dir) { shelf_nav_queue_enter(g_rows[idx].name); return; }
|
||||||
int bi = g_rows[idx].book;
|
int bi = g_rows[idx].book;
|
||||||
if (bi < 0 || bi >= g_nbooks) return;
|
if (bi < 0 || bi >= g_nbooks) return;
|
||||||
/* 点书开读:reader_open 打开书(含页位图)→ push ACT_READER 让阅读页成为 view activity,
|
/* 点书开读:reader_open 打开书(含页位图)→ push ACT_READER 让阅读页成为 view activity,
|
||||||
@@ -217,13 +258,27 @@ static void shelf_list_tapcb(int idx, int op, void *ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- grid 数据源(meow_lazygrid,4 列封面卡) ---- */
|
/* ---- grid 数据源(meow_lazygrid,4 列封面卡) ----
|
||||||
static int shelf_grid_count(void *ctx) { (void)ctx; return g_nbooks; }
|
* format 模式:与 M27 前一致,平铺全部书;folder 模式:当前目录的导航行(目录卡 + 书卡)。 */
|
||||||
|
static int shelf_grid_count(void *ctx) { (void)ctx; return g_group_folder ? g_nrows : g_nbooks; }
|
||||||
static void shelf_grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
|
static void shelf_grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
item->title = NULL; item->subtitle = NULL; item->icon = NULL;
|
item->title = NULL; item->subtitle = NULL; item->icon = NULL;
|
||||||
item->enabled = 1; item->user = NULL;
|
item->enabled = 1; item->user = NULL;
|
||||||
|
if (g_group_folder) {
|
||||||
|
if (idx < 0 || idx >= g_nrows) return;
|
||||||
|
if (g_rows[idx].dir) {
|
||||||
|
item->title = g_rows[idx].name;
|
||||||
|
item->subtitle = g_rows[idx].info;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int bi = g_rows[idx].book;
|
||||||
|
if (bi < 0 || bi >= g_nbooks) return;
|
||||||
|
item->title = g_books[bi].title;
|
||||||
|
item->subtitle = g_books[bi].sub;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (idx < 0 || idx >= g_nbooks) return;
|
if (idx < 0 || idx >= g_nbooks) return;
|
||||||
item->title = g_books[idx].title;
|
item->title = g_books[idx].title;
|
||||||
item->subtitle = g_books[idx].sub;
|
item->subtitle = g_books[idx].sub;
|
||||||
@@ -242,12 +297,47 @@ static void grid_title_cut(const char *src, char *out, int outsz)
|
|||||||
if (L + 4 < (size_t)outsz) { out[L] = ' '; out[L + 1] = ' '; out[L + 2] = '.'; out[L + 3] = '.'; out[L + 4] = '.';
|
if (L + 4 < (size_t)outsz) { out[L] = ' '; out[L + 1] = ' '; out[L + 2] = '.'; out[L + 3] = '.'; out[L + 4] = '.';
|
||||||
out[L + 5] = 0; }
|
out[L + 5] = 0; }
|
||||||
}
|
}
|
||||||
|
/* 宫格标题按可用像素宽截断(folder 模式用:目录名/书名可能比格子宽,居中绘制会溢出到邻格)。 */
|
||||||
|
static void grid_title_fit_px(const char *src, char *out, int outsz, int max_px)
|
||||||
|
{
|
||||||
|
snprintf(out, (size_t)outsz, "%s", src ? src : "");
|
||||||
|
if (meow_ui_text_width(out, 16) <= max_px) return;
|
||||||
|
size_t n = strlen(out);
|
||||||
|
char tmp[160];
|
||||||
|
while (n > 0) {
|
||||||
|
do { n--; } while (n > 0 && ((unsigned char)out[n] & 0xC0) == 0x80);
|
||||||
|
out[n] = 0;
|
||||||
|
snprintf(tmp, sizeof tmp, "%s...", out);
|
||||||
|
if ((int)strlen(tmp) < outsz && meow_ui_text_width(tmp, 16) <= max_px) {
|
||||||
|
snprintf(out, (size_t)outsz, "%s", tmp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(out, (size_t)outsz, "...");
|
||||||
|
}
|
||||||
|
|
||||||
static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, void *ctx)
|
static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
if (index < 0 || index >= g_nbooks) return;
|
int is_dir = 0;
|
||||||
const shelf_book *b = &g_books[index];
|
int fmt = 0;
|
||||||
|
const char *title = NULL;
|
||||||
|
const char *badge = NULL;
|
||||||
|
if (g_group_folder) {
|
||||||
|
if (index < 0 || index >= g_nrows) return;
|
||||||
|
const shelf_row *r = &g_rows[index];
|
||||||
|
if (r->dir) {
|
||||||
|
is_dir = 1; title = r->name; badge = r->info; /* "12 本" */
|
||||||
|
} else {
|
||||||
|
int bi = r->book;
|
||||||
|
if (bi < 0 || bi >= g_nbooks) return;
|
||||||
|
fmt = g_books[bi].fmt; title = g_books[bi].title; badge = fmt_name(fmt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 0 || index >= g_nbooks) return;
|
||||||
|
fmt = g_books[index].fmt; title = g_books[index].title; badge = fmt_name(fmt);
|
||||||
|
}
|
||||||
uint32_t card = th->surfaceContainerLow;
|
uint32_t card = th->surfaceContainerLow;
|
||||||
if (state & MEOW_LAZYGRID_ST_SELECTED) card = th->primaryContainer;
|
if (state & MEOW_LAZYGRID_ST_SELECTED) card = th->primaryContainer;
|
||||||
meow_gfx_fill_round(x, y, w, h, th->radius_card, card);
|
meow_gfx_fill_round(x, y, w, h, th->radius_card, card);
|
||||||
@@ -255,13 +345,16 @@ static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, vo
|
|||||||
int cw = w - 2 * pad, chh = h - 2 * pad - meow_ui_text_height(16) - 6;
|
int cw = w - 2 * pad, chh = h - 2 * pad - meow_ui_text_height(16) - 6;
|
||||||
int cx = x + pad, cy = y + pad;
|
int cx = x + pad, cy = y + pad;
|
||||||
if (cw > 0 && chh > 0) {
|
if (cw > 0 && chh > 0) {
|
||||||
meow_gfx_fill_round(cx, cy, cw, chh, MEOW_SHAPE_SMALL, th->surfaceContainerHigh);
|
/* 目录卡用 secondaryContainer 内块 + 书数徽标,与书卡的格式徽标区分。 */
|
||||||
/* 格式徽标(居中) */
|
meow_gfx_fill_round(cx, cy, cw, chh, MEOW_SHAPE_SMALL,
|
||||||
char fc[16]; int fs = 24;
|
is_dir ? th->secondaryContainer : th->surfaceContainerHigh);
|
||||||
snprintf(fc, sizeof fc, "%s", fmt_name(b->fmt));
|
int fs = 24;
|
||||||
meow_ui_text_center(cx + cw / 2, cy + (chh - meow_ui_text_height(fs)) / 2, fc, fs, th->primary);
|
meow_ui_text_center(cx + cw / 2, cy + (chh - meow_ui_text_height(fs)) / 2, badge, fs,
|
||||||
|
is_dir ? th->onSecondaryContainer : th->primary);
|
||||||
}
|
}
|
||||||
char t[128]; grid_title_cut(b->title, t, sizeof t);
|
char t[128];
|
||||||
|
if (g_group_folder) grid_title_fit_px(title ? title : "", t, sizeof t, w - 8);
|
||||||
|
else grid_title_cut(title ? title : "", t, sizeof t);
|
||||||
meow_ui_text_center(x + w / 2, y + h - meow_ui_text_height(16) - 2, t, 16, th->onSurface);
|
meow_ui_text_center(x + w / 2, y + h - meow_ui_text_height(16) - 2, t, 16, th->onSurface);
|
||||||
if (state & MEOW_LAZYGRID_ST_SELECTED) {
|
if (state & MEOW_LAZYGRID_ST_SELECTED) {
|
||||||
meow_gfx_fill_round_alpha(x, y, w, h, th->radius_card, th->primary, 26);
|
meow_gfx_fill_round_alpha(x, y, w, h, th->radius_card, th->primary, 26);
|
||||||
@@ -270,9 +363,15 @@ static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, vo
|
|||||||
static void shelf_grid_tapcb(int idx, void *ctx)
|
static void shelf_grid_tapcb(int idx, void *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (idx < 0 || idx >= g_nbooks) return;
|
int bi = idx;
|
||||||
if (reader_open(g_books[idx].path) == 0) {
|
if (g_group_folder) {
|
||||||
snprintf(g_just_read, sizeof g_just_read, "%s", g_books[idx].path); /* 返回时置顶用 */
|
if (idx < 0 || idx >= g_nrows) return;
|
||||||
|
if (g_rows[idx].dir) { shelf_nav_queue_enter(g_rows[idx].name); return; }
|
||||||
|
bi = g_rows[idx].book;
|
||||||
|
}
|
||||||
|
if (bi < 0 || bi >= g_nbooks) return;
|
||||||
|
if (reader_open(g_books[bi].path) == 0) {
|
||||||
|
snprintf(g_just_read, sizeof g_just_read, "%s", g_books[bi].path); /* 返回时置顶用 */
|
||||||
meow_view_page_push(ACT_READER);
|
meow_view_page_push(ACT_READER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,13 +416,73 @@ static void shelf_build_content(void)
|
|||||||
(void)th;
|
(void)th;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶栏动作:0=布局切换 1=分组切换 2=刷新 3=设置。动作标签 = 将切换到的模式。 */
|
/* 进入子目录:改 cwd → 内存重排行序列 → 重建顶栏/内容。不扫描、不读盘。 */
|
||||||
|
static void shelf_nav_enter_dir(const char *name)
|
||||||
|
{
|
||||||
|
char next[SHELF_GROUP_MAX];
|
||||||
|
if (!shelf_nav_enter(g_cwd, name, next, (int)sizeof next)) return;
|
||||||
|
snprintf(g_cwd, sizeof g_cwd, "%s", next);
|
||||||
|
build_rows();
|
||||||
|
shelf_appbar_build();
|
||||||
|
shelf_build_content();
|
||||||
|
shelf_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回上一级(左缘返回手势 / 顶栏返回箭头)。1=已回退;0=已在根(调用方决定退出 App)。
|
||||||
|
* folder 模式之外恒返回 0——format 模式保持原有「左滑退出」行为。 */
|
||||||
|
int shelf_back(void)
|
||||||
|
{
|
||||||
|
if (!g_group_folder || !g_cwd[0]) return 0;
|
||||||
|
char parent[SHELF_GROUP_MAX];
|
||||||
|
if (!shelf_nav_up(g_cwd, parent, (int)sizeof parent)) return 0;
|
||||||
|
snprintf(g_cwd, sizeof g_cwd, "%s", parent);
|
||||||
|
build_rows();
|
||||||
|
shelf_appbar_build();
|
||||||
|
shelf_build_content();
|
||||||
|
shelf_redraw();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶栏标题按可用像素宽截断(UTF-8 安全,末尾 "…")。
|
||||||
|
* 动作区固定占右侧约 200px(宫格/格式两枚文字动作 + 刷新/设置两枚 48px 图标格),
|
||||||
|
* 返回箭头占左侧 48px;目录名较长时(如 "400+本高质量完本合集")必须让位,否则压在动作上。 */
|
||||||
|
static void appbar_title_fit(const char *src, char *out, int outsz, int max_px)
|
||||||
|
{
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
int fsz = (th && th->font_title > 0) ? th->font_title : 28;
|
||||||
|
snprintf(out, (size_t)outsz, "%s", src ? src : "");
|
||||||
|
if (meow_ui_text_width(out, fsz) <= max_px) return;
|
||||||
|
size_t n = strlen(out);
|
||||||
|
char tmp[128];
|
||||||
|
while (n > 0) {
|
||||||
|
do { n--; } while (n > 0 && ((unsigned char)out[n] & 0xC0) == 0x80);
|
||||||
|
out[n] = 0;
|
||||||
|
snprintf(tmp, sizeof tmp, "%s…", out);
|
||||||
|
if ((int)strlen(tmp) < outsz && meow_ui_text_width(tmp, fsz) <= max_px) {
|
||||||
|
snprintf(out, (size_t)outsz, "%s", tmp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(out, (size_t)outsz, "…");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶栏动作:0=布局切换 1=分组切换 2=刷新 3=设置。动作标签 = 将切换到的模式。
|
||||||
|
* folder 模式下顶栏标题 = 当前目录名(根="书架"),并有返回箭头;面包屑全路径在副题行。 */
|
||||||
static void shelf_appbar_build(void)
|
static void shelf_appbar_build(void)
|
||||||
{
|
{
|
||||||
int sbh = meow_ui_statusbar_height();
|
int sbh = meow_ui_statusbar_height();
|
||||||
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
|
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
|
||||||
g_appbar = meow_topappbar_create(0, sbh, 480, SHELF_APPBAR_H, "书架");
|
char title[64] = "书架";
|
||||||
|
int show_back = 0;
|
||||||
|
if (g_group_folder && g_cwd[0]) {
|
||||||
|
char name[64];
|
||||||
|
shelf_dir_name(g_cwd, name, (int)sizeof name);
|
||||||
|
appbar_title_fit(name, title, (int)sizeof title, 205);
|
||||||
|
show_back = 1;
|
||||||
|
}
|
||||||
|
g_appbar = meow_topappbar_create(0, sbh, 480, SHELF_APPBAR_H, title);
|
||||||
if (!g_appbar) return;
|
if (!g_appbar) return;
|
||||||
|
if (show_back) meow_topappbar_set_back(g_appbar, 1);
|
||||||
meow_topappbar_add_action(g_appbar, g_layout ? "列表" : "宫格"); /* 0 布局切换 */
|
meow_topappbar_add_action(g_appbar, g_layout ? "列表" : "宫格"); /* 0 布局切换 */
|
||||||
meow_topappbar_add_action(g_appbar, g_group_folder ? "格式" : "目录"); /* 1 分组切换 */
|
meow_topappbar_add_action(g_appbar, g_group_folder ? "格式" : "目录"); /* 1 分组切换 */
|
||||||
meow_topappbar_add_action(g_appbar, "刷新"); /* 2 刷新 */
|
meow_topappbar_add_action(g_appbar, "刷新"); /* 2 刷新 */
|
||||||
@@ -351,6 +510,7 @@ static void shelf_on_enter(meow_view *v, void *ctx)
|
|||||||
* 提示条(超上限截断 / 超深目录)语义不变:索引里存了这两个值,载入时如实重建。 */
|
* 提示条(超上限截断 / 超深目录)语义不变:索引里存了这两个值,载入时如实重建。 */
|
||||||
if (!g_scanned) scan_books_first();
|
if (!g_scanned) scan_books_first();
|
||||||
else promote_just_read();
|
else promote_just_read();
|
||||||
|
nav_validate_cwd(); /* folder 模式:cwd 若已失效(目录/书被删)回退到最近的祖先 */
|
||||||
build_rows();
|
build_rows();
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
|
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
|
||||||
@@ -411,6 +571,9 @@ void shelf_set_group(int folder)
|
|||||||
strcpy(c->shelf_group, folder ? "folder" : "format");
|
strcpy(c->shelf_group, folder ? "folder" : "format");
|
||||||
rdconf_persist();
|
rdconf_persist();
|
||||||
g_group_folder = folder ? 1 : 0;
|
g_group_folder = folder ? 1 : 0;
|
||||||
|
/* M33:folder↔format 切换保留 cwd(切回 folder 仍在原目录);失效则收缩到祖先。
|
||||||
|
* 只改分组模式:不碰 books[].path,也不重扫(M29 g_scanned 语义不变)。 */
|
||||||
|
nav_validate_cwd();
|
||||||
if (g_active) {
|
if (g_active) {
|
||||||
build_rows();
|
build_rows();
|
||||||
shelf_appbar_build();
|
shelf_appbar_build();
|
||||||
@@ -426,12 +589,20 @@ void shelf_redraw(void)
|
|||||||
meow_ui_clear(g_bg);
|
meow_ui_clear(g_bg);
|
||||||
if (g_appbar) meow_widget_draw(g_appbar);
|
if (g_appbar) meow_widget_draw(g_appbar);
|
||||||
|
|
||||||
/* 副题:Books/ · N 本 [· 按目录](分组只作用于列表;宫格仍是平铺封面墙) */
|
/* 副题:format = "Books/ · N 本"(M27 前逐字不变);
|
||||||
|
* folder = 面包屑全路径 + 当前层子树书数,如 "Books/400+…/01、玄幻 · 12 本 · 按目录"。
|
||||||
|
* 顶栏另显示当前目录名,两者配合可看出「在哪 + 下面还有多少本」。 */
|
||||||
if (g_nbooks > 0) {
|
if (g_nbooks > 0) {
|
||||||
char sub[96];
|
char sub[128];
|
||||||
if (g_group_folder && g_layout == 0)
|
if (g_group_folder) {
|
||||||
snprintf(sub, sizeof sub, "Books/ · %d 本 · 按目录", g_nbooks);
|
char bc[400], tmp[512];
|
||||||
else snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
|
int total = shelf_nav_count_under(g_books, g_nbooks, g_cwd);
|
||||||
|
shelf_breadcrumb(g_cwd, bc, (int)sizeof bc);
|
||||||
|
snprintf(tmp, sizeof tmp, "%s,%d 本,按目录", bc, total);
|
||||||
|
shelf_copy_utf8(sub, (int)sizeof sub, tmp);
|
||||||
|
} else {
|
||||||
|
snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
|
||||||
|
}
|
||||||
int sbh = meow_ui_statusbar_height();
|
int sbh = meow_ui_statusbar_height();
|
||||||
meow_ui_text(24, sbh + SHELF_APPBAR_H + 3, sub, 16, g_dim);
|
meow_ui_text(24, sbh + SHELF_APPBAR_H + 3, sub, 16, g_dim);
|
||||||
}
|
}
|
||||||
@@ -471,6 +642,10 @@ int shelf_on_tap(int x, int y)
|
|||||||
/* 顶栏动作先命中 */
|
/* 顶栏动作先命中 */
|
||||||
if (g_appbar) {
|
if (g_appbar) {
|
||||||
int bi = meow_topappbar_hit(g_appbar, x, y);
|
int bi = meow_topappbar_hit(g_appbar, x, y);
|
||||||
|
if (bi == MEOW_TOPBAR_BACK) { /* M33:顶栏返回箭头 = 上一级(根层不显示此钮) */
|
||||||
|
shelf_back();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (bi == 0) { /* 布局切换 */
|
if (bi == 0) { /* 布局切换 */
|
||||||
shelf_set_view(g_layout ? 0 : 1);
|
shelf_set_view(g_layout ? 0 : 1);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -484,6 +659,7 @@ int shelf_on_tap(int x, int y)
|
|||||||
* 刷新也一定重扫;扫描成功后写回新索引(写失败只丢缓存)。 */
|
* 刷新也一定重扫;扫描成功后写回新索引(写失败只丢缓存)。 */
|
||||||
shelf_index_remove(READER_SD_ROOT "/Books");
|
shelf_index_remove(READER_SD_ROOT "/Books");
|
||||||
scan_books_full();
|
scan_books_full();
|
||||||
|
nav_validate_cwd(); /* M33:刷新后 cwd 目录可能已被删 → 收缩到仍有书的祖先 */
|
||||||
build_rows();
|
build_rows();
|
||||||
shelf_build_content();
|
shelf_build_content();
|
||||||
shelf_redraw();
|
shelf_redraw();
|
||||||
@@ -496,12 +672,22 @@ int shelf_on_tap(int x, int y)
|
|||||||
if (g_nbooks <= 0) { shelf_redraw(); return 0; }
|
if (g_nbooks <= 0) { shelf_redraw(); return 0; }
|
||||||
if (g_layout == 1 && g_grid) {
|
if (g_layout == 1 && g_grid) {
|
||||||
int idx = meow_lazygrid_tap_at(g_grid, x, y);
|
int idx = meow_lazygrid_tap_at(g_grid, x, y);
|
||||||
if (idx >= 0) shelf_redraw();
|
if (g_pending_enter[0]) {
|
||||||
|
char name[SHELF_GROUP_MAX];
|
||||||
|
snprintf(name, sizeof name, "%s", g_pending_enter);
|
||||||
|
g_pending_enter[0] = 0;
|
||||||
|
shelf_nav_enter_dir(name); /* tap_at 已返回,重建控件安全 */
|
||||||
|
} else if (idx >= 0) shelf_redraw();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (g_list) {
|
if (g_list) {
|
||||||
int idx = meow_listview_tap_at(g_list, x, y);
|
int idx = meow_listview_tap_at(g_list, x, y);
|
||||||
if (idx >= 0) shelf_redraw();
|
if (g_pending_enter[0]) {
|
||||||
|
char name[SHELF_GROUP_MAX];
|
||||||
|
snprintf(name, sizeof name, "%s", g_pending_enter);
|
||||||
|
g_pending_enter[0] = 0;
|
||||||
|
shelf_nav_enter_dir(name); /* tap_at 已返回,重建控件安全 */
|
||||||
|
} else if (idx >= 0) shelf_redraw();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ void shelf_sort_recent(shelf_book *books, int nbooks)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 重建一本书的两种副题(扫描后与「刚读完置顶」共用同一份格式化逻辑)。
|
/* 重建一本书的两种副题(扫描后与「刚读完置顶」共用同一份格式化逻辑)。
|
||||||
* sub = 格式 · 大小 [· 读](format 模式/宫格)
|
* sub = 格式 · 大小 [· 读](format 模式 / 宫格 / M33 folder 导航视图)
|
||||||
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(folder 模式)
|
* sub_folder = 相对 book.group 第一级之后的子路径 · sub(M33 起导航视图不再显示,保留兼容)
|
||||||
*/
|
*/
|
||||||
void shelf_fmt_sub(shelf_book *b)
|
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 第一级目录名)。长度截到第一个 '/',排序时忽略大小写。 */
|
/* format 模式:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致。 */
|
||||||
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,
|
static int build_rows_format(const shelf_book *books, int nbooks,
|
||||||
shelf_row *rows, int rowcap)
|
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 (books[i].fmt != fmt) continue;
|
||||||
if (!have) {
|
if (!have) {
|
||||||
if (nrows >= rowcap) break;
|
if (nrows >= rowcap) break;
|
||||||
rows[nrows].section = 1; rows[nrows].fmt = fmt;
|
rows[nrows].section = 1; rows[nrows].dir = 0; rows[nrows].fmt = fmt;
|
||||||
rows[nrows].book = -1; rows[nrows].label = NULL;
|
rows[nrows].book = -1; rows[nrows].count = 0;
|
||||||
|
rows[nrows].name[0] = 0; rows[nrows].info[0] = 0;
|
||||||
|
rows[nrows].label = NULL;
|
||||||
nrows++;
|
nrows++;
|
||||||
have = 1;
|
have = 1;
|
||||||
}
|
}
|
||||||
if (nrows >= rowcap) break;
|
if (nrows >= rowcap) break;
|
||||||
rows[nrows].section = 0; rows[nrows].fmt = fmt;
|
rows[nrows].section = 0; rows[nrows].dir = 0; rows[nrows].fmt = fmt;
|
||||||
rows[nrows].book = i; rows[nrows].label = NULL;
|
rows[nrows].book = i; rows[nrows].count = 0;
|
||||||
nrows++;
|
rows[nrows].name[0] = 0; rows[nrows].info[0] = 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
rows[nrows].label = NULL;
|
||||||
nrows++;
|
nrows++;
|
||||||
}
|
}
|
||||||
@@ -316,11 +261,161 @@ static int build_rows_folder(const shelf_book *books, int nbooks,
|
|||||||
return nrows;
|
return nrows;
|
||||||
}
|
}
|
||||||
|
|
||||||
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
/* group 相对 cwd 的剩余路径:group==cwd → ""(书直接在当前层);group 在 cwd 下 → 去掉
|
||||||
shelf_row *rows, int rowcap)
|
* "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 (!books || !rows || nbooks <= 0 || rowcap <= 0) return 0;
|
||||||
if (nbooks > SHELF_MAX_BOOKS) nbooks = SHELF_MAX_BOOKS;
|
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);
|
: 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);
|
||||||
|
}
|
||||||
@@ -3,14 +3,15 @@
|
|||||||
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
|
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
|
||||||
* 扫描计时(test/shelf_scan_bench.c)共用。
|
* 扫描计时(test/shelf_scan_bench.c)共用。
|
||||||
*
|
*
|
||||||
* 目录归属(M27 r2):folder 模式按「Books/ 下的第一级目录」分组(如 "轻小说"、
|
* 目录视图(M33):folder 模式不再是「按第一级目录分段」,而是**逐级进入**——书架只显示
|
||||||
* "400+本高质量完本合集");更深的相对子路径("01、玄幻/01、斗破")进该书的副题,
|
* 当前目录(cwd,相对 Books/,根="")的内容:子目录在前(每个带子树书数)、直接属于该层的
|
||||||
* 不再每组一本书把书架炸成上百个段头。Books/ 根下的书 top="",显示段头 SHELF_GROUP_ROOT。
|
* 书在后。cwd 由 App 维护;本模块的 shelf_build_rows_at() 只在已扫平的 books[].group 上做
|
||||||
* 分组键是「第一级目录名」,完整相对路径保留在 group[] 供副题/调试用。
|
* 前缀过滤,**不读盘、不 readdir**(M32 索引省下的 readdir 不能又被导航用回去)。
|
||||||
* a/b 与 a/c 同属 a 组;a/b 与 b 仍是两个组(不会因为只取末级目录名而混)。
|
* M27 的 top(第一级目录名)仍保留在 shelf_book 里供索引/副题/诊断使用,但不再决定行序列。
|
||||||
|
* 完整相对路径保留在 group[]:path 仍是书文件的完整绝对路径。
|
||||||
*
|
*
|
||||||
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),分组只写 top/group,
|
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),导航只改 cwd,
|
||||||
* 绝不改写 path;两个分组模式间切换不影响进度。
|
* 绝不改写 path;format/folder 两个模式间切换不影响进度。
|
||||||
*/
|
*/
|
||||||
#ifndef SHELF_MODEL_H
|
#ifndef SHELF_MODEL_H
|
||||||
#define SHELF_MODEL_H
|
#define SHELF_MODEL_H
|
||||||
@@ -27,24 +28,29 @@
|
|||||||
#define SHELF_SCAN_MAX_DEPTH 6
|
#define SHELF_SCAN_MAX_DEPTH 6
|
||||||
/* 行数上限:每本书最多贡献 1 行书 + 1 行段头(段头数 ≤ 书数),故 2N 必够。 */
|
/* 行数上限:每本书最多贡献 1 行书 + 1 行段头(段头数 ≤ 书数),故 2N 必够。 */
|
||||||
#define SHELF_MAX_ROWS (SHELF_MAX_BOOKS * 2)
|
#define SHELF_MAX_ROWS (SHELF_MAX_BOOKS * 2)
|
||||||
#define SHELF_GROUP_ROOT "未分类" /* Books/ 根下书籍的段头 */
|
|
||||||
|
|
||||||
typedef struct shelf_book {
|
typedef struct shelf_book {
|
||||||
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
|
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
|
||||||
char title[128];
|
char title[128];
|
||||||
char sub[160]; /* 默认(format/宫格)副题:格式 · 大小 [· 读],与 M27 前逐字一致 */
|
char sub[160]; /* 默认副题:格式 · 大小 [· 读],与 M27 前逐字一致;
|
||||||
char sub_folder[160]; /* folder 模式副题:更深子路径 · 格式 · 大小 [· 读] */
|
* format 模式与 folder 导航视图(书就在当前层)共用 */
|
||||||
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径(供副题);根下="" */
|
char sub_folder[160]; /* 深层子路径 · 格式 · 大小 [· 读](M33 起导航视图不再显示;
|
||||||
char top[SHELF_GROUP_MAX]; /* 分组键:相对路径的第一级目录名;根下="" */
|
* 保留字段供测试/兼容,不要用它改写 path) */
|
||||||
|
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径:folder 导航的前缀键;根下="" */
|
||||||
|
char top[SHELF_GROUP_MAX]; /* 第一级目录名(M27 遗留字段,供索引/诊断;M33 起不决定行序列) */
|
||||||
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
|
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
|
||||||
long size;
|
long size;
|
||||||
uint64_t ts; /* 最近阅读 unix;无=0 */
|
uint64_t ts; /* 最近阅读 unix;无=0 */
|
||||||
} shelf_book;
|
} shelf_book;
|
||||||
|
|
||||||
typedef struct shelf_row {
|
typedef struct shelf_row {
|
||||||
int section; /* 1=段头行 */
|
int section; /* 1=段头行(仅 format 模式) */
|
||||||
|
int dir; /* 1=目录导航行(仅 folder 模式);0=书行 */
|
||||||
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
|
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
|
||||||
int book; /* 书行=books 下标;段头=-1 */
|
int book; /* 书行=books 下标;段头/目录行=-1 */
|
||||||
|
int count; /* 目录行:该子目录下(含更深层)书籍数 */
|
||||||
|
char name[SHELF_GROUP_MAX]; /* 目录行:子目录名(不含父路径) */
|
||||||
|
char info[24]; /* 目录行:展示副题,如 "12 本" */
|
||||||
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
|
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
|
||||||
} shelf_row;
|
} shelf_row;
|
||||||
|
|
||||||
@@ -82,12 +88,36 @@ int shelf_scan_books(const char *root, shelf_book *books, int cap,
|
|||||||
|
|
||||||
/* 生成行序列:
|
/* 生成行序列:
|
||||||
* folder_mode=0:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致;
|
* folder_mode=0:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致;
|
||||||
* folder_mode=1:按 books[].top(Books/ 第一级目录)分组,根组「未分类」置顶,
|
* folder_mode=1:当前目录 cwd(相对 Books/,根="")的导航行——子目录在前(按名排序,
|
||||||
* 其余组按组名排序;组内沿用 books[] 全局顺序(最近阅读优先、其次书名)。
|
* 每个带 count/info),直接属于该层的书在后(沿用 books[] 全局顺序)。
|
||||||
|
* 纯内存:只读 books[].group,不做任何 opendir/readdir/stat。
|
||||||
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
|
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
|
||||||
|
int shelf_build_rows_at(const shelf_book *books, int nbooks, int folder_mode,
|
||||||
|
const char *cwd, shelf_row *rows, int rowcap);
|
||||||
|
|
||||||
|
/* 兼容旧调用:等价于 shelf_build_rows_at(..., cwd=""),即 folder 模式显示 Books/ 根层。 */
|
||||||
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
||||||
shelf_row *rows, int rowcap);
|
shelf_row *rows, int rowcap);
|
||||||
|
|
||||||
|
/* ---- 目录导航(纯字符串/纯内存工具,宿主可测) ---- */
|
||||||
|
|
||||||
|
/* cwd 子树(含 cwd 本层书)的书籍数。cwd 无效/无书返回 0。 */
|
||||||
|
int shelf_nav_count_under(const shelf_book *books, int nbooks, const char *cwd);
|
||||||
|
|
||||||
|
/* 进入子目录:cwd + "/" + name → out。1=成功;0=非法/超长(out 不改)。
|
||||||
|
* 根目录 cwd="" 时结果为 name(无前导 '/')。 */
|
||||||
|
int shelf_nav_enter(const char *cwd, const char *name, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 返回上一级:非根返回 1 并写 out(根父为 "");已在根返回 0。 */
|
||||||
|
int shelf_nav_up(const char *cwd, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 面包屑:根 → "Books/";子目录 → "Books/<cwd>"。UTF-8 安全截断到 outsz;
|
||||||
|
* 超长时保留末尾(当前目录最重要)并加 "…" 前缀。返回写入字节数。 */
|
||||||
|
int shelf_breadcrumb(const char *cwd, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 当前目录名(cwd 最后一级);根返回 ""。返回写入字节数。 */
|
||||||
|
int shelf_dir_name(const char *cwd, char *out, int outsz);
|
||||||
|
|
||||||
/* 工具(单测直接调用)。 */
|
/* 工具(单测直接调用)。 */
|
||||||
int shelf_fmt_from_ext(const char *name);
|
int shelf_fmt_from_ext(const char *name);
|
||||||
const char *shelf_fmt_name(int fmt);
|
const char *shelf_fmt_name(int fmt);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ int shelf_is_active(void);
|
|||||||
int shelf_on_tap(int x, int y);
|
int shelf_on_tap(int x, int y);
|
||||||
void shelf_on_swipe(int dir, int dx, int dy);
|
void shelf_on_swipe(int dir, int dx, int dy);
|
||||||
void shelf_redraw(void);
|
void shelf_redraw(void);
|
||||||
|
int shelf_back(void); /* M33:folder 模式返回上一级;1=已回退 0=已在根 */
|
||||||
void shelf_set_view(int grid); /* 1=grid 0=list;读 reader.conf ui.shelf_layout(A) */
|
void shelf_set_view(int grid); /* 1=grid 0=list;读 reader.conf ui.shelf_layout(A) */
|
||||||
void shelf_set_group(int folder); /* 1=folder 0=format;写 ui.shelf_group(M27) */
|
void shelf_set_group(int folder); /* 1=folder 0=format;写 ui.shelf_group(M27) */
|
||||||
void shelf_on_tick(uint64_t now); /* 提示条 6s 到期消失(书架前台时由 app.c 调) */
|
void shelf_on_tick(uint64_t now); /* 提示条 6s 到期消失(书架前台时由 app.c 调) */
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
/* test/shelf_group_smoke.c — M27 书架目录分组宿主单测(纯 C,gcc 直接跑,不依赖设备/SD 卡)。
|
/* test/shelf_group_smoke.c — M27/M33 书架目录模型宿主单测(纯 C,gcc 直接跑,不依赖设备/SD 卡)。
|
||||||
*
|
*
|
||||||
* 覆盖:
|
* 覆盖:
|
||||||
* G1 扫描:格式识别 / 标题去扩展名 / 目录归属(group=完整相对路径,top=第一级目录)
|
* G1 扫描:格式识别 / 标题去扩展名 / 目录归属(group=完整相对路径,top=第一级目录)
|
||||||
* G2 format 模式 = 改动前行为(格式段头 TXT/FB2/EPUB/MOBI/CBZ,最近阅读优先、其次书名)
|
* G2 format 模式 = 改动前行为(格式段头 TXT/FB2/EPUB/MOBI/CBZ,最近阅读优先、其次书名)
|
||||||
* G3 folder 模式:按 Books/ 第一级目录分段(未分类置顶,其余按组名排序),
|
* G3(M33 改写)folder 模式 = 逐级进入:根层只列子目录(排序/计数)+ 根下书;
|
||||||
* 更深子路径(分类/作品)进副题;a/b 与 a/c 同组 a
|
* 进 400+ → 01、玄幻 → 01、斗破 才能看到书(不再把深层书平铺在顶级)
|
||||||
* G4 空目录不产生段头;depth 3/4 收录,depth > SHELF_SCAN_MAX_DEPTH 计数且不出书
|
* G4 空目录不产生导航行;depth 3/4 收录,depth > SHELF_SCAN_MAX_DEPTH 计数且不出书
|
||||||
* G5 Books/ 根下的书归「未分类」;字面名为「未分类」的目录与根组是两个不同键
|
* G5.1(M33 改写)a/b 与 a/c 合并为目录 a(2),逐级进入后可见书 1/2;
|
||||||
|
* 组件边界(a 不含 ab/c);G5.2 字面名「未分类」的目录与根层书是两个独立行
|
||||||
* G6 UTF-8 安全截断(中文目录名不切出半个字符)
|
* G6 UTF-8 安全截断(中文目录名不切出半个字符)
|
||||||
* G7 超上限不静默:truncated=1(含「cap 卡在目录边界」与「恰好装满不误报」两种);
|
* G7 超上限不静默:truncated=1(含「cap 卡在目录边界」与「恰好装满不误报」两种);
|
||||||
* 行数上限(段头 ≤ 书数 ⇒ 2N 必够)
|
* 行数上限(段头 ≤ 书数 ⇒ 2N 必够)
|
||||||
* G8 进度不受分组影响:path 不变、rdprog_load 前后同值、ts 两种模式一致
|
* G8 进度不受分组影响:path 不变、rdprog_load 前后同值、ts 两种模式一致
|
||||||
* G9 副题格式化的唯一实现 shelf_fmt_sub:扫描与「刚读完置顶」共用
|
* G9 副题格式化的唯一实现 shelf_fmt_sub:扫描与「刚读完置顶」共用
|
||||||
* (ts 变化后重算 → 「· 读」立即出现;folder 子路径前缀随 group 重建)
|
* (ts 变化后重算 → 「· 读」立即出现;folder 子路径前缀随 group 重建)
|
||||||
|
* G10(M33)导航纯函数:虚拟目录(磁盘不存在)也能出行序列(不 readdir);
|
||||||
|
* 进出/面包屑/目录名/rowcap/UTF-8 截断
|
||||||
*/
|
*/
|
||||||
#include "shelf_model.h"
|
#include "shelf_model.h"
|
||||||
#include "progress.h"
|
#include "progress.h"
|
||||||
@@ -61,14 +64,17 @@ static uint64_t ts_of(const char *path)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 把行序列打成一串便于整体比对:S:<段头>|B:<书名>(<目录>)| */
|
/* 把行序列打成一串便于整体比对:S:<段头>|D:<目录>(<书数>)|B:<书名>(<目录>)| */
|
||||||
static void dump_rows(const shelf_row *rows, int nrows, const shelf_book *books,
|
static void dump_rows(const shelf_row *rows, int nrows, const shelf_book *books,
|
||||||
char *out, int outsz)
|
char *out, int outsz)
|
||||||
{
|
{
|
||||||
int off = 0;
|
int off = 0;
|
||||||
out[0] = 0;
|
out[0] = 0;
|
||||||
for (int i = 0; i < nrows && off < outsz; i++) {
|
for (int i = 0; i < nrows && off < outsz; i++) {
|
||||||
if (rows[i].section) {
|
if (rows[i].dir) {
|
||||||
|
off += snprintf(out + off, (size_t)(outsz - off), "D:%s(%d)|",
|
||||||
|
rows[i].name, rows[i].count);
|
||||||
|
} else if (rows[i].section) {
|
||||||
const char *label = rows[i].label ? rows[i].label : shelf_fmt_name(rows[i].fmt);
|
const char *label = rows[i].label ? rows[i].label : shelf_fmt_name(rows[i].fmt);
|
||||||
off += snprintf(out + off, (size_t)(outsz - off), "S:%s|", label);
|
off += snprintf(out + off, (size_t)(outsz - off), "S:%s|", label);
|
||||||
} else {
|
} else {
|
||||||
@@ -192,19 +198,79 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
|||||||
"format 模式段头/顺序与旧行为一致");
|
"format 模式段头/顺序与旧行为一致");
|
||||||
CHECK(nf == 11, "format 行数 = 3 段头 + 8 书");
|
CHECK(nf == 11, "format 行数 = 3 段头 + 8 书");
|
||||||
|
|
||||||
/* G3 folder 模式:按第一级目录分组(未分类置顶 → 400+ → B → D → F2 → kh → 轻小说) */
|
/* G3(M33 改写)folder 模式 = Books 根层导航:子目录在前(排序、带子树书数),
|
||||||
|
* 根下书在后。M27 的「第一级目录段头」语义已被逐级进入取代(断言同等甚至更强:
|
||||||
|
* 不仅可见该书,还锁定了它所在的目录层级与计数)。 */
|
||||||
int nd = shelf_build_rows(books, n, 1, rows, 128);
|
int nd = shelf_build_rows(books, n, 1, rows, 128);
|
||||||
dump_rows(rows, nd, books, got, sizeof got);
|
dump_rows(rows, nd, books, got, sizeof got);
|
||||||
CHECK(strcmp(got,
|
CHECK(strcmp(got,
|
||||||
"S:未分类|B:根下()|"
|
"D:400+本高质量完本合集(1)|D:B(1)|D:D(1)|D:F2(1)|D:kh(1)|D:轻小说(2)|B:根下()|") == 0,
|
||||||
"S:400+本高质量完本合集|B:《斗破》(400+本高质量完本合集/01、玄幻/01、斗破)|"
|
"folder 根层:子目录在前(排序+子树书数),根下书在后");
|
||||||
"S:B|B:5(B/sub)|S:D|B:6(D/E/F)|S:F2|B:9(F2/a/b/c)|S:kh|B:3(kh)|"
|
CHECK(nd == 7, "folder 根层行数 = 6 目录 + 1 根下书");
|
||||||
"S:轻小说|B:1(轻小说)|B:2(轻小说)|") == 0,
|
|
||||||
"folder 模式按第一级目录分组、段头顺序正确");
|
|
||||||
CHECK(nd == 15, "folder 行数 = 7 段头 + 8 书");
|
|
||||||
|
|
||||||
/* G4 空目录不产生段头(C 不在序列里) */
|
/* G4 空目录不产生导航行(C 不在序列里) */
|
||||||
CHECK(strstr(got, "S:C|") == NULL, "空目录 C 无段头");
|
CHECK(strstr(got, "D:C") == NULL, "空目录 C 不出现在根层");
|
||||||
|
|
||||||
|
/* G3.1 逐级进入:400+ 系列(三级分类/作品/书名)不再把 1 本书平铺在顶级,
|
||||||
|
* 而是每层只看到下一层目录;最终进入作品目录才看到书。 */
|
||||||
|
char cwd[SHELF_GROUP_MAX], next[SHELF_GROUP_MAX];
|
||||||
|
cwd[0] = 0;
|
||||||
|
CHECK(shelf_nav_count_under(books, n, "") == 8, "根层子树共 8 本");
|
||||||
|
CHECK(shelf_nav_count_under(books, n, "轻小说") == 2, "轻小说子树 2 本");
|
||||||
|
CHECK(shelf_nav_count_under(books, n, "400+本高质量完本合集") == 1, "400+ 子树 1 本");
|
||||||
|
CHECK(shelf_nav_count_under(books, n, "不存在") == 0, "不存在的目录 0 本");
|
||||||
|
|
||||||
|
CHECK(shelf_nav_enter("", "400+本高质量完本合集", next, sizeof next) == 1 &&
|
||||||
|
strcmp(next, "400+本高质量完本合集") == 0, "从根进入 400+ 目录");
|
||||||
|
snprintf(cwd, sizeof cwd, "%s", next);
|
||||||
|
int n1 = shelf_build_rows_at(books, n, 1, cwd, rows, 128);
|
||||||
|
dump_rows(rows, n1, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:01、玄幻(1)|") == 0,
|
||||||
|
"进 400+ → 只看到分类目录 01、玄幻(不是 1 本书平铺)");
|
||||||
|
|
||||||
|
snprintf(next, sizeof next, "%s", cwd);
|
||||||
|
CHECK(shelf_nav_enter(next, "01、玄幻", cwd, sizeof cwd) == 1, "进入 01、玄幻");
|
||||||
|
int n2 = shelf_build_rows_at(books, n, 1, cwd, rows, 128);
|
||||||
|
dump_rows(rows, n2, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:01、斗破(1)|") == 0, "进 01、玄幻 → 看到作品目录 01、斗破");
|
||||||
|
|
||||||
|
snprintf(next, sizeof next, "%s", cwd);
|
||||||
|
CHECK(shelf_nav_enter(next, "01、斗破", cwd, sizeof cwd) == 1, "进入 01、斗破");
|
||||||
|
int n3 = shelf_build_rows_at(books, n, 1, cwd, rows, 128);
|
||||||
|
dump_rows(rows, n3, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:《斗破》(400+本高质量完本合集/01、玄幻/01、斗破)|") == 0,
|
||||||
|
"进 01、斗破 → 看到书《斗破》(导航后可见该书)");
|
||||||
|
CHECK(n3 == 1 && rows[0].book >= 0 && strcmp(books[rows[0].book].title, "《斗破》") == 0,
|
||||||
|
"书行指向正确的 books[] 下标(点书能打开正确路径)");
|
||||||
|
|
||||||
|
/* 返回逐级正确(与左缘手势/顶栏返回箭头同一函数) */
|
||||||
|
CHECK(shelf_nav_up(cwd, next, sizeof next) == 1 &&
|
||||||
|
strcmp(next, "400+本高质量完本合集/01、玄幻") == 0, "返回一级:01、斗破 → 01、玄幻");
|
||||||
|
CHECK(shelf_nav_up(next, cwd, sizeof cwd) == 1 &&
|
||||||
|
strcmp(cwd, "400+本高质量完本合集") == 0, "再返回:01、玄幻 → 400+");
|
||||||
|
CHECK(shelf_nav_up(cwd, next, sizeof next) == 1 && next[0] == 0, "再返回:400+ → Books 根");
|
||||||
|
CHECK(shelf_nav_up("", next, sizeof next) == 0 && next[0] == 0, "根层再返回 = 不动作(退出交给 App)");
|
||||||
|
|
||||||
|
/* 扁平目录直接出书;只有一层子目录没有书时仍显示该子目录 */
|
||||||
|
int nq = shelf_build_rows_at(books, n, 1, "轻小说", rows, 128);
|
||||||
|
dump_rows(rows, nq, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:1(轻小说)|B:2(轻小说)|") == 0, "轻小说(扁平)→ 直接看到 2 本书");
|
||||||
|
int nb = shelf_build_rows_at(books, n, 1, "B", rows, 128);
|
||||||
|
dump_rows(rows, nb, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:sub(1)|") == 0, "B 层没有书、只有子目录 sub → 仍显示 sub");
|
||||||
|
int nbs = shelf_build_rows_at(books, n, 1, "B/sub", rows, 128);
|
||||||
|
dump_rows(rows, nbs, books, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:5(B/sub)|") == 0, "继续进入 B/sub → 看到书 5");
|
||||||
|
|
||||||
|
/* 面包屑 / 目录名文案 */
|
||||||
|
char bc[64], dn[64];
|
||||||
|
CHECK(shelf_breadcrumb("", bc, sizeof bc) == 6 && strcmp(bc, "Books/") == 0,
|
||||||
|
"根层面包屑 = Books/");
|
||||||
|
CHECK(shelf_breadcrumb("轻小说", bc, sizeof bc) == 15 &&
|
||||||
|
strcmp(bc, "Books/轻小说") == 0, "子目录面包屑 = Books/<cwd>");
|
||||||
|
CHECK(shelf_dir_name("", dn, sizeof dn) == 0, "根层无目录名");
|
||||||
|
CHECK(shelf_dir_name("400+本高质量完本合集/01、玄幻", dn, sizeof dn) > 0 &&
|
||||||
|
strcmp(dn, "01、玄幻") == 0, "目录名取最后一级");
|
||||||
|
|
||||||
/* G7 行数上限:段头 ≤ 书数,2N 足够;rowcap 小时只写 rowcap 行 */
|
/* G7 行数上限:段头 ≤ 书数,2N 足够;rowcap 小时只写 rowcap 行 */
|
||||||
CHECK(SHELF_MAX_ROWS >= SHELF_MAX_BOOKS * 2, "SHELF_MAX_ROWS ≥ 2×书上限");
|
CHECK(SHELF_MAX_ROWS >= SHELF_MAX_BOOKS * 2, "SHELF_MAX_ROWS ≥ 2×书上限");
|
||||||
@@ -249,7 +315,9 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
|||||||
CHECK(shelf_scan_books(nope, small, 2, NULL, &rep3) == 0 && rep3.root_missing == 1,
|
CHECK(shelf_scan_books(nope, small, 2, NULL, &rep3) == 0 && rep3.root_missing == 1,
|
||||||
"根不存在时 root_missing=1");
|
"根不存在时 root_missing=1");
|
||||||
|
|
||||||
/* G5.1 a/b 与 a/c 必须归同一组 a(不是两个段头) */
|
/* G5.1(M33 改写)a/b 与 a/c 在根层合并为同一个目录行 a(计数=2);
|
||||||
|
* 进入 a 才分别看到 b、c,进入 b/c 才看到书——旧断言「同属组 a」被换成
|
||||||
|
* 「导航后可见该书」,并额外锁定目录计数。 */
|
||||||
char root3[400];
|
char root3[400];
|
||||||
snprintf(root3, sizeof root3, "%s/mini2", tmproot);
|
snprintf(root3, sizeof root3, "%s/mini2", tmproot);
|
||||||
snprintf(p, sizeof p, "%s/a/b", root3); mkdirs(p);
|
snprintf(p, sizeof p, "%s/a/b", root3); mkdirs(p);
|
||||||
@@ -259,10 +327,19 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
|||||||
shelf_book mb2[8];
|
shelf_book mb2[8];
|
||||||
shelf_scan_report rep5;
|
shelf_scan_report rep5;
|
||||||
int nm2 = shelf_scan_books(root3, mb2, 8, NULL, &rep5);
|
int nm2 = shelf_scan_books(root3, mb2, 8, NULL, &rep5);
|
||||||
int nd2 = shelf_build_rows(mb2, nm2, 1, rows, 128);
|
int nd2 = shelf_build_rows_at(mb2, nm2, 1, "", rows, 128);
|
||||||
dump_rows(rows, nd2, mb2, got, sizeof got);
|
dump_rows(rows, nd2, mb2, got, sizeof got);
|
||||||
CHECK(strcmp(got, "S:a|B:1(a/b)|B:2(a/c)|") == 0, "a/b 与 a/c 同组 a(一个段头)");
|
CHECK(strcmp(got, "D:a(2)|") == 0, "a/b 与 a/c 在根层合并为目录 a(计数 2)");
|
||||||
CHECK(nd2 == 3, "a/b + a/c = 1 段头 + 2 书");
|
CHECK(nd2 == 1, "根层只有 1 行目录(不再按书平铺)");
|
||||||
|
int nd2a = shelf_build_rows_at(mb2, nm2, 1, "a", rows, 128);
|
||||||
|
dump_rows(rows, nd2a, mb2, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:b(1)|D:c(1)|") == 0, "进入 a → 子目录 b、c 各 1 本");
|
||||||
|
int nd2b = shelf_build_rows_at(mb2, nm2, 1, "a/b", rows, 128);
|
||||||
|
dump_rows(rows, nd2b, mb2, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:1(a/b)|") == 0, "进入 a/b → 导航后可见书 1");
|
||||||
|
int nd2c = shelf_build_rows_at(mb2, nm2, 1, "a/c", rows, 128);
|
||||||
|
dump_rows(rows, nd2c, mb2, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:2(a/c)|") == 0, "进入 a/c → 导航后可见书 2");
|
||||||
CHECK(find_book(mb2, nm2, "1") && strstr(find_book(mb2, nm2, "1")->sub_folder, "b · TXT") != NULL,
|
CHECK(find_book(mb2, nm2, "1") && strstr(find_book(mb2, nm2, "1")->sub_folder, "b · TXT") != NULL,
|
||||||
"a/b 的 folder 副题含 b");
|
"a/b 的 folder 副题含 b");
|
||||||
CHECK(find_book(mb2, nm2, "2") && strstr(find_book(mb2, nm2, "2")->sub_folder, "c · TXT") != NULL,
|
CHECK(find_book(mb2, nm2, "2") && strstr(find_book(mb2, nm2, "2")->sub_folder, "c · TXT") != NULL,
|
||||||
@@ -270,7 +347,24 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
|||||||
CHECK(find_book(mb2, nm2, "1") && strcmp(find_book(mb2, nm2, "1")->sub, "TXT · 1B") == 0,
|
CHECK(find_book(mb2, nm2, "1") && strcmp(find_book(mb2, nm2, "1")->sub, "TXT · 1B") == 0,
|
||||||
"a/b 的 format 副题仍与 base 一致");
|
"a/b 的 format 副题仍与 base 一致");
|
||||||
|
|
||||||
/* G5.2 字面名「未分类」的目录与根组是两个独立键(不按显示名合并) */
|
/* G5.1b 组件边界:a 的子树不包含 ab/c(a/b 与 ab/c 不能因前缀混淆) */
|
||||||
|
shelf_book bnd[3];
|
||||||
|
memset(bnd, 0, sizeof bnd);
|
||||||
|
snprintf(bnd[0].group, sizeof bnd[0].group, "a/b");
|
||||||
|
snprintf(bnd[0].title, sizeof bnd[0].title, "one");
|
||||||
|
snprintf(bnd[1].group, sizeof bnd[1].group, "ab/c");
|
||||||
|
snprintf(bnd[1].title, sizeof bnd[1].title, "two");
|
||||||
|
snprintf(bnd[2].group, sizeof bnd[2].group, "a");
|
||||||
|
snprintf(bnd[2].title, sizeof bnd[2].title, "three");
|
||||||
|
bnd[0].fmt = bnd[1].fmt = bnd[2].fmt = 0;
|
||||||
|
int nbnd = shelf_build_rows_at(bnd, 3, 1, "a", rows, 128);
|
||||||
|
dump_rows(rows, nbnd, bnd, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:b(1)|B:three(a)|") == 0,
|
||||||
|
"cwd=a 只含 a/b(不含 ab/c)且列出直接书 three");
|
||||||
|
CHECK(shelf_nav_count_under(bnd, 3, "a") == 2, "count_under(a)=2(a/b + 直接书,不含 ab/c)");
|
||||||
|
|
||||||
|
/* G5.2(M33 改写)字面名「未分类」是真实目录,与根层书是两个不同实体:
|
||||||
|
* 根层同时出现目录行「未分类」和书 x;进入该目录看到 7(旧断言不合并 → 同等强度)。 */
|
||||||
char root2[400], mini[420];
|
char root2[400], mini[420];
|
||||||
snprintf(root2, sizeof root2, "%s/mini", tmproot);
|
snprintf(root2, sizeof root2, "%s/mini", tmproot);
|
||||||
snprintf(mini, sizeof mini, "%s/未分类", root2);
|
snprintf(mini, sizeof mini, "%s/未分类", root2);
|
||||||
@@ -280,10 +374,16 @@ static void test_scan_and_modes(const char *root, const char *tmproot)
|
|||||||
shelf_book mb[8];
|
shelf_book mb[8];
|
||||||
shelf_scan_report rep4;
|
shelf_scan_report rep4;
|
||||||
int nm = shelf_scan_books(root2, mb, 8, NULL, &rep4);
|
int nm = shelf_scan_books(root2, mb, 8, NULL, &rep4);
|
||||||
int ndm = shelf_build_rows(mb, nm, 1, rows, 128);
|
int ndm = shelf_build_rows_at(mb, nm, 1, "", rows, 128);
|
||||||
dump_rows(rows, ndm, mb, got, sizeof got);
|
dump_rows(rows, ndm, mb, got, sizeof got);
|
||||||
CHECK(strcmp(got, "S:未分类|B:x()|S:未分类|B:7(未分类)|") == 0,
|
CHECK(strcmp(got, "D:未分类(1)|B:x()|") == 0,
|
||||||
"根组与名为「未分类」的目录不合并(两个段头)");
|
"根层书 x 与名为「未分类」的真实目录并存(不按显示名合并)");
|
||||||
|
CHECK(ndm == 2 && rows[0].dir && strcmp(rows[0].name, "未分类") == 0 &&
|
||||||
|
!rows[1].dir && rows[1].book >= 0 && strcmp(mb[rows[1].book].title, "x") == 0,
|
||||||
|
"目录行「未分类」与根层书 x 是两个独立行");
|
||||||
|
int ndm2 = shelf_build_rows_at(mb, nm, 1, "未分类", rows, 128);
|
||||||
|
dump_rows(rows, ndm2, mb, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "B:7(未分类)|") == 0, "进入「未分类」目录 → 看到书 7(未被当特殊值吞掉)");
|
||||||
|
|
||||||
/* G6 UTF-8 安全截断 */
|
/* G6 UTF-8 安全截断 */
|
||||||
char src[200], dst[96];
|
char src[200], dst[96];
|
||||||
@@ -341,6 +441,55 @@ static void test_fmt_sub(void)
|
|||||||
"一级目录书无子路径前缀");
|
"一级目录书无子路径前缀");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* G10(M33):导航纯函数——只在 books[].group 上做前缀过滤,目录在磁盘上**不存在**
|
||||||
|
* 也能出正确行序列(证明导航不引入 opendir/readdir/stat)。 */
|
||||||
|
static void test_nav_pure(void)
|
||||||
|
{
|
||||||
|
shelf_book fake[4];
|
||||||
|
memset(fake, 0, sizeof fake);
|
||||||
|
snprintf(fake[0].group, sizeof fake[0].group, "ghost/a/b");
|
||||||
|
snprintf(fake[0].title, sizeof fake[0].title, "A");
|
||||||
|
snprintf(fake[1].group, sizeof fake[1].group, "ghost/a/c");
|
||||||
|
snprintf(fake[1].title, sizeof fake[1].title, "B");
|
||||||
|
snprintf(fake[2].group, sizeof fake[2].group, "ghost/d");
|
||||||
|
snprintf(fake[2].title, sizeof fake[2].title, "C");
|
||||||
|
snprintf(fake[3].group, sizeof fake[3].group, "ghost");
|
||||||
|
snprintf(fake[3].title, sizeof fake[3].title, "D");
|
||||||
|
for (int i = 0; i < 4; i++) fake[i].fmt = 0;
|
||||||
|
|
||||||
|
shelf_row rows[16];
|
||||||
|
char got[512];
|
||||||
|
int n0 = shelf_build_rows_at(fake, 4, 1, "", rows, 16);
|
||||||
|
dump_rows(rows, n0, fake, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:ghost(4)|") == 0, "虚拟目录(磁盘不存在)也能出导航行:根层 ghost(4)");
|
||||||
|
|
||||||
|
int n1 = shelf_build_rows_at(fake, 4, 1, "ghost", rows, 16);
|
||||||
|
dump_rows(rows, n1, fake, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:a(2)|D:d(1)|B:D(ghost)|") == 0,
|
||||||
|
"ghost 层:子目录按名排序 + 直接书在后");
|
||||||
|
|
||||||
|
int n2 = shelf_build_rows_at(fake, 4, 1, "ghost/a", rows, 16);
|
||||||
|
dump_rows(rows, n2, fake, got, sizeof got);
|
||||||
|
CHECK(strcmp(got, "D:b(1)|D:c(1)|") == 0, "ghost/a 层:b、c 各 1 本");
|
||||||
|
|
||||||
|
int ncap = shelf_build_rows_at(fake, 4, 1, "ghost", rows, 2);
|
||||||
|
CHECK(ncap == 2, "rowcap=2 时只写 2 行且不越界");
|
||||||
|
|
||||||
|
/* 面包屑超长:UTF-8 安全、保留末尾、前缀 "…" */
|
||||||
|
char longcwd[256], out[32];
|
||||||
|
int k = 0;
|
||||||
|
for (int i = 0; i < 60; i++) { memcpy(longcwd + k, "轻", 3); k += 3; }
|
||||||
|
longcwd[k] = 0;
|
||||||
|
int bn = shelf_breadcrumb(longcwd, out, (int)sizeof out);
|
||||||
|
CHECK(bn > 0 && bn < (int)sizeof out && utf8_complete(out), "超长面包屑仍为合法 UTF-8 且不越界");
|
||||||
|
CHECK(strncmp(out, "…", 3) == 0, "超长面包屑以省略号前缀保留当前目录末尾");
|
||||||
|
|
||||||
|
/* 进入目录超长:拒绝且不写 out */
|
||||||
|
char tiny[8] = "keep";
|
||||||
|
CHECK(shelf_nav_enter("abcdefgh", "ij", tiny, (int)sizeof tiny) == 0 &&
|
||||||
|
strcmp(tiny, "keep") == 0, "拼接超长时 shelf_nav_enter 返回 0 且不改 out");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_progress_stability(const char *root)
|
static void test_progress_stability(const char *root)
|
||||||
{
|
{
|
||||||
/* G8:写一份进度,切换分组模式前后 rdprog_load 必须同值,path 不变。 */
|
/* G8:写一份进度,切换分组模式前后 rdprog_load 必须同值,path 不变。 */
|
||||||
@@ -427,6 +576,7 @@ int main(void)
|
|||||||
|
|
||||||
test_scan_and_modes(root, tmp);
|
test_scan_and_modes(root, tmp);
|
||||||
test_fmt_sub();
|
test_fmt_sub();
|
||||||
|
test_nav_pure();
|
||||||
test_progress_stability(root);
|
test_progress_stability(root);
|
||||||
test_conf_default(tmp);
|
test_conf_default(tmp);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user