Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf.c
T

432 lines
17 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 + 格式徽标)。
*/
#include "../reader_internal.h"
#include "progress.h"
#include "reader_conf.h"
#include "meow_ui_internal.h" /* meow_gfx_fill_round(宫格卡圆角填充) */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define SHELF_APPBAR_H 56
#define SHELF_LIST_X 0
#define SHELF_LIST_W 480
typedef struct shelf_book {
char path[400];
char title[128];
char sub[160];
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
long size;
uint64_t ts; /* 最近阅读 unix;无=0 */
} shelf_book;
static shelf_book g_books[256];
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 uint32_t g_bg, g_fg, g_dim;
static char g_notice[160]; /* 提示(opendir 失败等) */
static int g_notice_ms; /* 剩余显示 ms(0=无) */
/* 段头行(list 模式):把扫描结果排队为 [段头(fmt), 书, 书, ..., 段头, ...]。
* 书量小用段头(README §3.2),格式分组(TXT/FB2/EPUB/MOBI/CBZ)。 */
#define SHELF_MAX_ROWS 520
typedef struct shelf_row { char section; int fmt; int book; } shelf_row;
static shelf_row g_rows[SHELF_MAX_ROWS];
static int g_nrows;
static const char *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 "?"; }
}
static int 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;
}
static void fmt_size(char *out, int outsz, long s)
{
if (s >= 1 << 20) snprintf(out, outsz, "%.1fMB", (double)s / (1 << 20));
else if (s >= 1 << 10) snprintf(out, outsz, "%.0fKB", (double)s / (1 << 10));
else snprintf(out, outsz, "%ldB", s);
}
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);
}
/* 递归收集 Books/ 下(含子目录,深度 ≤2)的书。只在 scan_books 里调用。 */
static void scan_dir(const char *dir, int depth)
{
if (depth > 2) return; /* 防深嵌套/循环 */
DIR *dp = opendir(dir);
if (!dp) return;
struct dirent *de;
while ((de = readdir(dp)) && g_nbooks < 256) {
if (de->d_name[0] == '.') continue;
char path[256];
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)) { scan_dir(path, depth + 1); continue; }
int f = fmt_from_ext(de->d_name);
if (f < 0) continue;
shelf_book *b = &g_books[g_nbooks];
snprintf(b->path, sizeof b->path, "%s", path);
strncpy(b->title, de->d_name, sizeof b->title - 1); b->title[sizeof b->title - 1] = 0;
char *dot = strrchr(b->title, '.');
if (dot) *dot = 0; /* 标题去扩展名 */
b->fmt = f;
b->size = (long)st.st_size;
rdprog p;
b->ts = (rdprog_load(b->path, &p) == 1) ? p.ts : 0;
char sz[24]; fmt_size(sz, sizeof sz, b->size);
snprintf(b->sub, sizeof b->sub, "%s · %s%s", fmt_name(f), sz,
b->ts ? " · 读" : "");
g_nbooks++;
}
closedir(dp);
}
static void scan_books(void)
{
const char *dir = READER_SD_ROOT "/Books";
g_nbooks = 0;
DIR *dp = opendir(dir);
if (!dp) {
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
g_notice_ms = 4000;
return;
}
closedir(dp);
scan_dir(dir, 0); /* 递归:含 Books/<子目录>/ 下书籍(用户可能按目录组织) */
qsort(g_books, (size_t)g_nbooks, sizeof(shelf_book), cmp_recent_then_name);
}
/* 排队段头(按格式):为 list 模式生成 [段头(fmt) | 书] 序列。 */
static void build_rows(void)
{
g_nrows = 0;
static const int fmts[] = { 0, 1, 2, 3, 4 };
for (size_t fi = 0; fi < sizeof fmts / sizeof fmts[0]; fi++) {
int fmt = fmts[fi];
int have = 0;
if (g_nrows >= SHELF_MAX_ROWS) break;
for (int i = 0; i < g_nbooks; i++) {
if (g_books[i].fmt != fmt) continue;
if (!have) {
if (g_nrows >= SHELF_MAX_ROWS) break;
g_rows[g_nrows].section = 1; g_rows[g_nrows].fmt = fmt; g_rows[g_nrows].book = -1;
g_nrows++;
have = 1;
}
if (g_nrows >= SHELF_MAX_ROWS) break;
g_rows[g_nrows].section = 0; g_rows[g_nrows].fmt = fmt; g_rows[g_nrows].book = i;
g_nrows++;
}
}
}
/* ---- 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 = 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=设置。布局切换动作标签 = 将切换到的布局。 */
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, "刷新"); /* 1 刷新 */
meow_topappbar_add_action(g_appbar, "设置"); /* 2 设置 */
meow_topappbar_set_item_icon(g_appbar, 1, MEOW_ICON_REFRESH);
meow_topappbar_set_item_icon(g_appbar, 2, 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;
scan_books(); /* 回到书架即重扫(新增/最近读置顶刷新) */
build_rows();
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
/* 布局:读 reader.conf ui.shelf_layout(A) */
const rdconf *c = rdconf_get();
g_layout = (c && c->shelf[0] && strcmp(c->shelf, "grid") == 0) ? 1 : 0;
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();
}
}
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]; 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) { /* 刷新:重扫 */
scan_books(); build_rows();
shelf_build_content();
shelf_redraw();
return 1;
} else if (bi == 2) { /* 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(); }
}