- rdsrc 统一文档源抽象:TXT/FB2/EPUB/MOBI/CBZ 都产出「UTF-8 段序列+目录+元数据+图片」, 与 R1 rdtxt 段契约一致;book.c 改喂 rd_src(文本)或图页(CBZ)。 - EPUB:miniz 解 ZIP → 自写 rdxml 回调解析 OPF(spine/manifest)+NCX/nav → 正文 XHTML 用 rdhtml 逐行粗解析(坏标签/未闭合/实体容错,不建 DOM),章节按 RDSRC_SEG_MAX 分片。 - FB2:rdxml 严格解析抽 section 段落 + base64 内嵌图;段题进正文+目录。 - MOBI/AZW3:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml_opt→ flow/resources),HTML 走 rdhtml 同款粗解析;LGPL-3.0 合规见 NOTICE(版权+全文+锁定 commit)。 - CBZ:miniz 解 ZIP → 自然序图片序列,rdimg(stb_image) 解码+降采样,阅读页图页模式。 - 目录显示与跳转(EPUB NCX/FB2 section/MOBI 标题);书架开放四格式;进度按 (段,字符偏移)(TXT/FB2/EPUB/MOBI)/页号(CBZ)。 - 宿主单测 loaders_smoke(四格式抽取/目录/图片/base64/页序)+ qemu-mipsel formats_smoke/book_smoke;R1 test_reader_core 58/0 不回归。 - build_reader.sh 集成 miniz/libmobi/stb/zlib 头;NOTICE 入仓;交叉 -Wall -Wextra 0 warning。 新 reader md5: e29036599300eb277b1c960a75b65dbf
183 lines
6.2 KiB
C
183 lines
6.2 KiB
C
/* app/shelf.c — 书架:扫 TF 卡 `Books/`,识别 TXT/FB2/EPUB/MOBI/CBZ(R1 仅 TXT 可开,
|
||
* 其它显示「R2 支持」占位);卡片列表 + 最近阅读置顶(按 progress ts 降序)。
|
||
* 用 W_LISTVIEW(结构化列表)+ App 自绘标题。空态=清屏提示。
|
||
*/
|
||
#include "../reader_internal.h"
|
||
#include "progress.h"
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
|
||
#define SHELF_LIST_X 0
|
||
#define SHELF_LIST_Y 64
|
||
#define SHELF_LIST_W 480
|
||
#define SHELF_LIST_H (720 - 64)
|
||
|
||
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;
|
||
static uint32_t g_bg, g_fg, g_dim;
|
||
static char g_notice[160]; /* 空态/提示 */
|
||
static int g_notice_ms; /* 剩余显示 ms(0=无) */
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
struct dirent *de;
|
||
while ((de = readdir(dp)) && g_nbooks < 256) {
|
||
if (de->d_name[0] == '.') continue;
|
||
int f = fmt_from_ext(de->d_name);
|
||
if (f < 0) continue;
|
||
shelf_book *b = &g_books[g_nbooks];
|
||
if (snprintf(b->path, sizeof b->path, "%s/%s", dir, de->d_name) >= (int)sizeof b->path) continue;
|
||
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;
|
||
struct stat st;
|
||
b->size = (stat(b->path, &st) == 0) ? (long)st.st_size : 0;
|
||
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);
|
||
qsort(g_books, (size_t)g_nbooks, sizeof(shelf_book), cmp_recent_then_name);
|
||
}
|
||
|
||
/* listview 行源 */
|
||
static int shelf_list_src(int idx, meow_list_item *out, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= g_nbooks) return 0;
|
||
const shelf_book *b = &g_books[idx];
|
||
out->icon_kind = MEOW_LIST_ICON_FILE;
|
||
out->title = b->title;
|
||
out->subtitle = b->sub;
|
||
out->right = MEOW_LIST_RIGHT_ARROW;
|
||
out->enabled = 1;
|
||
out->on = 0;
|
||
return 1;
|
||
}
|
||
|
||
static void shelf_list_tapcb(int idx, int op, void *ctx)
|
||
{
|
||
(void)op; (void)ctx;
|
||
if (idx < 0 || idx >= g_nbooks) return;
|
||
reader_open(g_books[idx].path);
|
||
}
|
||
|
||
/* 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(); /* 回到书架即重扫(新增/最近读置顶刷新) */
|
||
const meow_theme *th = meow_theme_get();
|
||
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
|
||
/* 无控件时重建列表(首次/回到书架) */
|
||
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
|
||
g_list = meow_listview_create(SHELF_LIST_X, SHELF_LIST_Y, SHELF_LIST_W, SHELF_LIST_H,
|
||
g_nbooks, shelf_list_src, NULL, MEOW_LIST_SEL_SINGLE);
|
||
meow_listview_set_on_tap(g_list, shelf_list_tapcb, NULL);
|
||
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; }
|
||
}
|
||
|
||
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; }
|
||
|
||
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);
|
||
meow_ui_text(20, 22, "书架", 28, g_fg);
|
||
/* 副标题:格式化说明 */
|
||
char sub[96]; snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
|
||
meow_ui_text(20, 24 + 34, sub, 20, g_dim);
|
||
|
||
if (g_list) meow_widget_draw(g_list);
|
||
if (g_nbooks == 0) {
|
||
meow_ui_text(30, 320, "书架为空:请往 TF 卡 Books/ 放书籍", 20, g_dim);
|
||
meow_ui_text(30, 348, "支持 TXT / FB2 / EPUB / MOBI / CBZ", 20, g_dim);
|
||
}
|
||
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_list && g_nbooks > 0) {
|
||
int idx = meow_listview_tap_at(g_list, x, y);
|
||
if (idx >= 0) { shelf_redraw(); return 1; } /* 选中已由 SDK 标脏;重绘 */
|
||
}
|
||
return 0;
|
||
}
|