Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf.c
T
AskaEth 63ead3ec1e fix(reader): M27 r2 — 扫描深度提到 6 + 按 Books 第一级目录分组
真机真实布局暴露两处问题(评审 P1):
- Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 在 depth 3-4,旧 depth≤2 让 413 本 3.8GB 不可见
  → SHELF_SCAN_MAX_DEPTH=2→6;report 新增 depth_skipped,超深书籍文件计数并在 g_notice 可见提示
  (有 N 个文件因目录层数超过 6 层未显示),不再静默
- 按完整相对路径分组会把 413 本书炸成 410 个每条 1 书的段头
  → 分组键改为 Books/ 第一级目录(shelf_book.top);完整相对路径仍在 group[],
    第一级之后的子路径(分类/作品)放进副题 sub(如 01、玄幻/01、斗破 · TXT · 2.1MB)
  → 真机书库只剩 3 组:400+本高质量完本合集 / kh / 轻小说
- 单测 50 断言:depth3/4 收录、depth7 计入 depth_skipped、四种真实路径的组名、
  a/b 与 a/c 同组 a、副题含子路径、format 回归、根组未分类、path/进度不变
2026-11-09 22:29:46 +08:00

388 lines
16 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.c — 书架:扫 TF 卡 `Books/`,识别 TXT/FB2/EPUB/MOBI/CBZ(R1 仅 TXT 可开,
* 其它显示「R2 支持」占位);MD3 布局(meow_topappbar + list/grid)+ 最近阅读置顶 + 段头分组。
*
* 布局(README §3.2,用户 2026-09-04 拍板):
* - 默认 list(meow_listview_set_container(on) MD3 圆角卡),可切 grid(meow_lazygrid 4 列封面卡);
* 布局存 reader.conf `ui.shelf_layout`(rdconf_get()->shelf);
* - 顶栏 meow_topappbar:标题「书架」+ 动作(布局切换 / 分组切换 / 刷新 / App 设置入口);
* - 无书也渲染完整骨架(顶栏 + 内容区 + 居中空态卡 + 指引 + 支持格式);
* - 封面:MVP 无封面提取(engine F1);无则圆角占位(surfaceContainerHigh + 格式徽标)。
*
* M27 分组(`ui.shelf_group`,默认 format 保持旧行为):
* format = 按格式分段(TXT/FB2/EPUB/MOBI/CBZ);
* folder = 按 Books/ 下的「第一级目录」分段(根下=未分类),更深子路径(分类/作品)进副题。
* 扫描/行序列在 app/shelf_model.c(纯 C,宿主可测);本文件只做控件装配与绘制。
*/
#include "../reader_internal.h"
#include "progress.h"
#include "reader_conf.h"
#include "shelf_model.h"
#include "meow_ui_internal.h" /* meow_gfx_fill_round(宫格卡圆角填充) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SHELF_APPBAR_H 56
#define SHELF_LIST_X 0
#define SHELF_LIST_W 480
static shelf_book g_books[SHELF_MAX_BOOKS];
static int g_nbooks;
static int g_active;
static meow_widget *g_list; /* listview(布局=list) */
static meow_lazygrid *g_grid; /* lazygrid(布局=grid) */
static meow_widget *g_appbar; /* meow_topappbar */
static int g_layout; /* 0=list 1=grid(读 rdconf->shelf) */
static int g_group_folder;/* 0=format 1=folder(读 rdconf->shelf_group) */
static uint32_t g_bg, g_fg, g_dim;
static char g_notice[160]; /* 提示(opendir 失败/超上限截断等) */
static int g_notice_ms; /* 剩余显示 ms(0=无) */
/* 段头行(list 模式):[段头, 书, 书, ..., 段头, ...];SHELF_MAX_ROWS=2×书上限(见 shelf_model.h)。 */
static shelf_row g_rows[SHELF_MAX_ROWS];
static int g_nrows;
static const char *fmt_name(int f) { return shelf_fmt_name(f); }
/* 进度时间戳(分组/排序用;不改 path,故进度键不受分组影响)。 */
static uint64_t shelf_ts_of(const char *path)
{
rdprog p;
return (rdprog_load(path, &p) == 1) ? p.ts : 0;
}
static void scan_books(void)
{
const char *dir = READER_SD_ROOT "/Books";
g_notice[0] = 0;
g_notice_ms = 0;
shelf_scan_report rep;
g_nbooks = shelf_scan_books(dir, g_books, SHELF_MAX_BOOKS, shelf_ts_of, &rep);
if (rep.root_missing) {
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
g_notice_ms = 4000;
} else if (rep.truncated && rep.depth_skipped > 0) {
snprintf(g_notice, sizeof g_notice,
"书籍超过 %d 本(只显示前 %d 本);另有 %d 个文件目录超过 %d 层未显示",
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS, rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
g_notice_ms = 4000;
} else if (rep.truncated) {
snprintf(g_notice, sizeof g_notice, "书籍超过 %d 本,已只显示前 %d 本",
SHELF_MAX_BOOKS, SHELF_MAX_BOOKS);
g_notice_ms = 4000;
} else if (rep.depth_skipped > 0) {
snprintf(g_notice, sizeof g_notice, "有 %d 个文件因目录层数超过 %d 层未显示",
rep.depth_skipped, SHELF_SCAN_MAX_DEPTH);
g_notice_ms = 4000;
}
}
/* 排队行:folder 模式按目录分段,否则按格式(默认)。 */
static void build_rows(void)
{
g_nrows = shelf_build_rows(g_books, g_nbooks, g_group_folder, g_rows, SHELF_MAX_ROWS);
}
/* ---- list 数据源(vsource + 段头;container on) ---- */
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)
{
(void)ctx;
item->icon = NULL;
item->icon_kind = MEOW_LIST_ICON_NONE;
item->right = MEOW_LIST_RIGHT_NONE;
item->on = 0;
item->enabled = 1;
item->user = NULL;
item->title = NULL;
item->subtitle = NULL;
item->section = 0;
if (idx < 0 || idx >= g_nrows) return;
if (g_rows[idx].section) {
item->section = 1;
item->title = g_rows[idx].label ? g_rows[idx].label : fmt_name(g_rows[idx].fmt);
item->enabled = 0;
} else {
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;
item->icon_kind = MEOW_LIST_ICON_FILE;
item->right = MEOW_LIST_RIGHT_ARROW;
}
}
static void shelf_list_tapcb(int idx, int op, void *ctx)
{
(void)op; (void)ctx;
if (idx < 0 || idx >= g_nrows || g_rows[idx].section) return;
int bi = g_rows[idx].book;
if (bi < 0 || bi >= g_nbooks) return;
/* 点书开读:reader_open 打开书(含页位图)→ push ACT_READER 让阅读页成为 view activity,
* 驱动 reader_on_enter/on_leave(重建/释放页位图);「返回书架」/左缘返回走 page_pop 回书架。 */
if (reader_open(g_books[bi].path) == 0) meow_view_page_push(ACT_READER);
}
/* ---- grid 数据源(meow_lazygrid,4 列封面卡) ---- */
static int shelf_grid_count(void *ctx) { (void)ctx; return g_nbooks; }
static void shelf_grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
{
(void)ctx;
item->title = NULL; item->subtitle = NULL; item->icon = NULL;
item->enabled = 1; item->user = NULL;
if (idx < 0 || idx >= g_nbooks) return;
item->title = g_books[idx].title;
item->subtitle = g_books[idx].sub;
}
/* 标题截断(格内防溢出):按字节收窄(UTF-8 截断可能断码,但仅显示用,断码由字库兜底为 tofu;
* 更稳妥在这里按格子宽度自适应——MVP 简单截 8 字节 + "…")。 */
static void grid_title_cut(const char *src, char *out, int outsz)
{
int n = 0;
while (src[n] && n < (int)sizeof g_books[0].title - 1 && n < 7 * 3) n++; /* ≤7 个 3 字节字 */
if ((size_t)n >= strlen(src)) { strncpy(out, src, (size_t)outsz - 1); out[outsz - 1] = 0; return; }
int m = n; while (m > 0 && ((unsigned char)src[m]) > 0x7F && m > n - 2) m--; /* 断码回退 */
if (m < 0) m = 0;
strncpy(out, src, (size_t)m); out[m] = 0;
size_t L = strlen(out);
if (L + 4 < (size_t)outsz) { out[L] = ' '; out[L + 1] = ' '; out[L + 2] = '.'; out[L + 3] = '.'; out[L + 4] = '.';
out[L + 5] = 0; }
}
static void shelf_grid_draw(int index, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx;
const meow_theme *th = meow_theme_get();
if (index < 0 || index >= g_nbooks) return;
const shelf_book *b = &g_books[index];
uint32_t card = th->surfaceContainerLow;
if (state & MEOW_LAZYGRID_ST_SELECTED) card = th->primaryContainer;
meow_gfx_fill_round(x, y, w, h, th->radius_card, card);
int pad = 8;
int cw = w - 2 * pad, chh = h - 2 * pad - meow_ui_text_height(16) - 6;
int cx = x + pad, cy = y + pad;
if (cw > 0 && chh > 0) {
meow_gfx_fill_round(cx, cy, cw, chh, MEOW_SHAPE_SMALL, th->surfaceContainerHigh);
/* 格式徽标(居中) */
char fc[16]; int fs = 24;
snprintf(fc, sizeof fc, "%s", fmt_name(b->fmt));
meow_ui_text_center(cx + cw / 2, cy + (chh - meow_ui_text_height(fs)) / 2, fc, fs, th->primary);
}
char t[128]; grid_title_cut(b->title, t, sizeof t);
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) {
meow_gfx_fill_round_alpha(x, y, w, h, th->radius_card, th->primary, 26);
}
}
static void shelf_grid_tapcb(int idx, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_nbooks) return;
if (reader_open(g_books[idx].path) == 0) meow_view_page_push(ACT_READER);
}
/* ---- 控件装配(按当前布局重建 list / grid) ---- */
static void shelf_build_content(void)
{
const meow_theme *th = meow_theme_get();
int sbh = meow_ui_statusbar_height();
int top = sbh + SHELF_APPBAR_H;
int list_y = top + 22; /* 副题区下 */
int list_h = 720 - list_y;
if (list_h < 0) list_h = 0;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_layout == 1) { /* grid */
int cols = 4, gap = 8;
int cw = (SHELF_LIST_W - (cols + 1) * 8) / cols; /* 16 边距 */
int chh = cw + 30; /* 封面 + 标题 */
g_grid = meow_lazygrid_create(SHELF_LIST_X, list_y, SHELF_LIST_W, list_h, cols, cw, chh, gap);
if (g_grid) {
meow_lazygrid_set_vsource(g_grid, shelf_grid_count, shelf_grid_bind, NULL);
meow_lazygrid_set_item_draw(g_grid, shelf_grid_draw, NULL);
meow_lazygrid_set_on_tap(g_grid, shelf_grid_tapcb, NULL);
}
} else { /* list */
g_list = meow_listview_create(SHELF_LIST_X, list_y, SHELF_LIST_W, list_h,
g_nrows, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) {
meow_listview_set_vsource(g_list, shelf_list_count, shelf_list_bind, NULL);
meow_listview_set_container(g_list, 1);
meow_listview_set_item_tier(g_list, 2);
meow_listview_set_on_tap(g_list, shelf_list_tapcb, NULL);
}
}
(void)th;
}
/* 顶栏动作:0=布局切换 1=分组切换 2=刷新 3=设置。动作标签 = 将切换到的模式。 */
static void shelf_appbar_build(void)
{
int sbh = meow_ui_statusbar_height();
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
g_appbar = meow_topappbar_create(0, sbh, 480, SHELF_APPBAR_H, "书架");
if (!g_appbar) return;
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, "刷新"); /* 2 刷新 */
meow_topappbar_add_action(g_appbar, "设置"); /* 3 设置 */
meow_topappbar_set_item_icon(g_appbar, 2, MEOW_ICON_REFRESH);
meow_topappbar_set_item_icon(g_appbar, 3, MEOW_ICON_SETTINGS);
}
/* view 生命周期 */
static void shelf_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void shelf_on_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
/* 先读配置(build_rows 依赖分组模式),再扫描/排队。 */
const rdconf *c = rdconf_get();
g_layout = (c && c->shelf[0] && strcmp(c->shelf, "grid") == 0) ? 1 : 0;
g_group_folder = (c && strcmp(c->shelf_group, "folder") == 0) ? 1 : 0;
scan_books(); /* 回到书架即重扫(新增/最近读置顶刷新) */
build_rows();
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
shelf_appbar_build();
shelf_build_content();
shelf_redraw();
}
static void shelf_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
static void shelf_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_appbar) { meow_widget_free(g_appbar); g_appbar = NULL; }
}
meow_view *shelf_view_create(void)
{
static meow_view_hooks hooks = { shelf_on_create, shelf_on_enter, shelf_on_leave,
shelf_on_destroy, NULL, NULL, NULL };
return meow_view_create(ACT_SHELF, MEOW_VIEW_ACTIVITY, &hooks, NULL);
}
int shelf_is_active(void) { return g_active; }
/* 切换 list/grid(书架顶栏「布局切换」动作;改 reader.conf 并重建)。 */
void shelf_set_view(int grid)
{
rdconf *c = rdconf_get();
if (!c) return;
strcpy(c->shelf, grid ? "grid" : "list");
rdconf_persist();
g_layout = grid ? 1 : 0;
if (g_active) {
shelf_appbar_build();
shelf_build_content();
shelf_redraw();
}
}
/* 切换 format/folder(顶栏「分组」动作 / 设置页「书架分组」;改 reader.conf 并重建)。
* 只改分组模式:不碰 books[].path,故进度键 rdprog_hash(path) 不变。 */
void shelf_set_group(int folder)
{
rdconf *c = rdconf_get();
if (!c) return;
strcpy(c->shelf_group, folder ? "folder" : "format");
rdconf_persist();
g_group_folder = folder ? 1 : 0;
if (g_active) {
build_rows();
shelf_appbar_build();
shelf_build_content();
shelf_redraw();
}
}
void shelf_redraw(void)
{
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
meow_ui_clear(g_bg);
if (g_appbar) meow_widget_draw(g_appbar);
/* 副题:Books/ · N 本 [· 按目录](分组只作用于列表;宫格仍是平铺封面墙) */
if (g_nbooks > 0) {
char sub[96];
if (g_group_folder && g_layout == 0)
snprintf(sub, sizeof sub, "Books/ · %d 本 · 按目录", g_nbooks);
else snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
int sbh = meow_ui_statusbar_height();
meow_ui_text(24, sbh + SHELF_APPBAR_H + 3, sub, 16, g_dim);
}
if (g_nbooks > 0) {
if (g_list) meow_widget_draw(g_list);
if (g_grid) meow_lazygrid_draw(g_grid, th);
} else {
/* 无书也渲染完整骨架(顶栏已在)+ 居中空态卡 + 指引 + 支持格式 */
int sbh = meow_ui_statusbar_height();
int top = sbh + SHELF_APPBAR_H;
int area_h = 720 - top;
int cw = 360, chh = 200;
int cx = (480 - cw) / 2;
int cy = top + (area_h - chh) / 2;
if (cy < top) cy = top;
meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->surfaceContainer);
meow_ui_text_center(480 / 2, cy + 40, "书架为空", 28, th->fg);
meow_ui_text_center(480 / 2, cy + 76, "请往 TF 卡 Books/ 放书籍", 20, th->onSurfaceVariant);
meow_ui_text_center(480 / 2, cy + 104, "支持 TXT / FB2 / EPUB / MOBI / CBZ", 20, th->onSurfaceVariant);
/* 「去放书」文本按钮(提示) */
meow_ui_text_center(480 / 2, cy + 150, "Books/ 目录", 20, th->primary);
}
if (g_notice_ms > 0) {
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
meow_ui_text(20, 630, g_notice, 20, th->primary);
}
meow_ui_flush();
}
int shelf_on_tap(int x, int y)
{
/* 顶栏动作先命中 */
if (g_appbar) {
int bi = meow_topappbar_hit(g_appbar, x, y);
if (bi == 0) { /* 布局切换 */
shelf_set_view(g_layout ? 0 : 1);
return 1;
} else if (bi == 1) { /* 分组切换:format ↔ folder */
shelf_set_group(g_group_folder ? 0 : 1);
return 1;
} else if (bi == 2) { /* 刷新:重扫 */
scan_books(); build_rows();
shelf_build_content();
shelf_redraw();
return 1;
} else if (bi == 3) { /* App 设置入口 */
meow_view_page_push(ACT_SETTINGS);
return 1;
}
}
if (g_nbooks <= 0) { shelf_redraw(); return 0; }
if (g_layout == 1 && g_grid) {
int idx = meow_lazygrid_tap_at(g_grid, x, y);
if (idx >= 0) shelf_redraw();
return 0;
}
if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0) shelf_redraw();
return 0;
}
return 0;
}
/* 书架的滚动(list/grid;app.c on_swipe 路由到本页时调用)。 */
void shelf_on_swipe(int dir, int dx, int dy)
{
(void)dir; (void)dx;
if (g_nbooks <= 0) return;
if (g_layout == 1 && g_grid) { meow_lazygrid_scroll_by(g_grid, dy); meow_lazygrid_draw(g_grid, meow_theme_get()); meow_ui_flush(); }
else if (g_list) { meow_listview_scroll_by(g_list, dy); meow_widget_draw(g_list); meow_ui_flush(); }
}