- 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
391 lines
15 KiB
C
391 lines
15 KiB
C
/* app/reader.c — 阅读页:W_CUSTOM 画布 + 三栏交互 + 中央菜单占位 + 顶栏进度/章节名 + 进度持久化。
|
||
*
|
||
* 渲染:W_CUSTOM 画布覆盖全屏;render_cb 画帧 = 填底色 → 翻页 blit(FreeType 页位图缓存 /
|
||
* 无字体则 SDK 位图字库兜底直绘)→ 顶栏 → 菜单覆盖层。翻页 = 局部脏区(flush_region)。
|
||
* 交互:点击左/右 1/3 翻页;中央唤菜单(字号±/行距±/返回);左滑=下一页、右滑=上一页。
|
||
* 进度:打开自动续读;换字号重排回锚(段,字符偏移);离开/翻页时保存。
|
||
*/
|
||
#include "../reader_internal.h"
|
||
#include "meow_ui_internal.h" /* back_buf/pitch/bpp 访问器(决策见报告) */
|
||
#include "book.h"
|
||
#include "progress.h"
|
||
#include <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <time.h>
|
||
|
||
#define RD_MENU_H 64
|
||
#define RD_MENU_BTN 6
|
||
|
||
static rd_book *g_book;
|
||
static int g_reader_active;
|
||
static meow_widget *g_canvas;
|
||
static int g_pw, g_ph, g_top, g_mx, g_my;
|
||
static int g_fs, g_ls;
|
||
#define RD_PAGE_CACHE 3
|
||
#define RD_PAGE_SLOT_CUR 1
|
||
static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=prev [1]=cur [2]=next(FreeType 页位图) */
|
||
static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */
|
||
static int g_menu_open;
|
||
static int g_dir_open; /* 目录覆盖层 */
|
||
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
|
||
static char g_font_path[400];
|
||
static int g_notice_ms;
|
||
|
||
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
|
||
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
|
||
|
||
#define RD_READER_DEFAULT_FS 20
|
||
#define RD_READER_DEFAULT_LS 130
|
||
|
||
/* 前向声明(reader_reflow* 依赖 reader_refill/redraw,定义在后) */
|
||
static void reader_redraw(void);
|
||
static int reader_refill_page(void);
|
||
static void reader_reflow_persist(void);
|
||
|
||
/* ---------- 工具 ---------- */
|
||
static uint16_t rgb565(uint32_t c)
|
||
{
|
||
int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF;
|
||
return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
|
||
}
|
||
|
||
/* 递归(深度≤3)扫 MeowOS/fonts/ 找第一个 .otf/.ttf。0=找到。 */
|
||
static int find_font_rec(const char *dir, int depth)
|
||
{
|
||
if (depth > 3) return 0;
|
||
DIR *dp = opendir(dir);
|
||
if (!dp) return 0;
|
||
struct dirent *de;
|
||
int found = 0;
|
||
while ((de = readdir(dp)) && !found) {
|
||
if (de->d_name[0] == '.') continue;
|
||
char full[480];
|
||
if (snprintf(full, sizeof full, "%s/%s", dir, de->d_name) >= (int)sizeof full) continue;
|
||
struct stat st;
|
||
if (stat(full, &st) != 0) continue;
|
||
if (S_ISDIR(st.st_mode)) { found = find_font_rec(full, depth + 1); }
|
||
else {
|
||
const char *d = strrchr(de->d_name, '.');
|
||
if (d && (strcasecmp(d, ".otf") == 0 || strcasecmp(d, ".ttf") == 0)) {
|
||
if (strlen(full) < sizeof g_font_path) {
|
||
strcpy(g_font_path, full);
|
||
found = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
closedir(dp);
|
||
return found;
|
||
}
|
||
static const char *resolve_font(void)
|
||
{
|
||
g_font_path[0] = 0;
|
||
if (find_font_rec(READER_SD_ROOT "/MeowOS/fonts", 0)) return g_font_path;
|
||
return NULL;
|
||
}
|
||
|
||
static void chapter_name(void)
|
||
{
|
||
/* TXT 无章节 → 文件名;R1 固定。 */
|
||
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
|
||
}
|
||
|
||
static void reader_reflow(void)
|
||
{
|
||
if (!g_book) return;
|
||
int seg = 0; int32_t ch = 0;
|
||
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
|
||
rd_book_jump_to_anchor(g_book, seg, ch, g_fs, g_ls);
|
||
reader_refill_page();
|
||
reader_redraw();
|
||
reader_reflow_persist();
|
||
}
|
||
|
||
static void reader_reflow_persist(void)
|
||
{
|
||
if (!g_book) return;
|
||
int seg = 0; int32_t ch = 0;
|
||
rd_book_anchor(g_book, &seg, &ch);
|
||
rdprog p;
|
||
p.valid = 1; p.seg = seg; p.ch = ch; p.fs = g_fs; p.ls = g_ls; p.ts = (uint64_t)time(NULL);
|
||
rdprog_save(rd_book_path(g_book), &p);
|
||
}
|
||
|
||
/* ---------- 渲染(W_CUSTOM render_cb) ---------- */
|
||
static void reader_draw_line_atlas(int x, int y, const char *utf8, int len, void *ctx)
|
||
{
|
||
(void)ctx; (void)len;
|
||
meow_ui_text(x, y, utf8, (g_fs <= 20) ? 20 : 28, g_page_fg);
|
||
}
|
||
|
||
static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||
{
|
||
(void)w; (void)ctx;
|
||
meow_ui_clear(g_page_bg);
|
||
if (g_book) {
|
||
int page = rd_book_current(g_book);
|
||
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
|
||
/* 翻页 blit:行拷贝当前页位图 → back_buf(局部脏区由 flush_region 上屏) */
|
||
unsigned char *bb = meow_ui_back_buf();
|
||
int pitch = meow_ui_back_buf_pitch();
|
||
int bpp = meow_ui_back_buf_bpp();
|
||
uint16_t *src = g_page_buf[RD_PAGE_SLOT_CUR];
|
||
if (bb && pitch > 0 && bpp == 16) {
|
||
for (int y = 0; y < g_ph; y++)
|
||
memcpy(bb + (size_t)y * pitch, src + (size_t)y * g_pw, (size_t)g_pw * 2);
|
||
}
|
||
} else {
|
||
rd_book_render_text(g_book, page, reader_draw_line_atlas, NULL);
|
||
}
|
||
}
|
||
/* 顶栏:进度% + 章节名(避开系统状态栏) */
|
||
int sbh = meow_ui_statusbar_height();
|
||
int pages = rd_book_pages(g_book);
|
||
int page = rd_book_current(g_book);
|
||
char pct[16];
|
||
int p = pages > 0 ? (page * 100 / pages) : 0;
|
||
snprintf(pct, sizeof pct, "%d%%", p);
|
||
meow_ui_fill_rect(0, sbh, g_pw, (g_top - 4 - sbh), g_page_bg);
|
||
meow_ui_text(16, sbh + 4, g_chapter[0] ? g_chapter : "阅读", 20, th->fg);
|
||
meow_ui_text_right(g_pw - 16, sbh + 4, pct, 20, th->fg);
|
||
meow_ui_draw_rect(0, g_top - 5, g_pw, 1, th->outline);
|
||
|
||
if (g_menu_open) {
|
||
int y = g_ph - RD_MENU_H;
|
||
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
|
||
/* 6 按钮:返回 | 目录 | 字− | 字+ | 距− | 距+ */
|
||
int bw = g_pw / RD_MENU_BTN;
|
||
const char *labels[RD_MENU_BTN] = { "返回", "目录", "字-", "字+", "距-", "距+" };
|
||
for (int i = 0; i < RD_MENU_BTN; i++) {
|
||
int bx = i * bw;
|
||
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
|
||
meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2 - 6, labels[i], 20, th->fg);
|
||
}
|
||
}
|
||
if (g_dir_open && g_book) {
|
||
/* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */
|
||
int n = rd_book_toc_n(g_book);
|
||
if (n > 0) {
|
||
int top = g_top + 8;
|
||
int row = 34;
|
||
int maxshow = (g_ph - top - 70) / row;
|
||
if (maxshow < 1) maxshow = 1;
|
||
if (maxshow > n) maxshow = n;
|
||
g_dir_top = top; g_dir_row_h = row;
|
||
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
|
||
char t[96]; int sg; int32_t cch;
|
||
int page = rd_book_current(g_book);
|
||
for (int i = 0; i < maxshow; i++) {
|
||
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
|
||
int y0 = top + i * row;
|
||
if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
|
||
meow_ui_text(24, y0, t, 20, th->fg);
|
||
meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
|
||
}
|
||
meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
|
||
} else {
|
||
g_dir_open = 0;
|
||
meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void reader_redraw(void)
|
||
{
|
||
if (!g_canvas) return;
|
||
meow_widget_draw(g_canvas); /* → render_cb 画帧 */
|
||
meow_ui_flush_region(0, 0, g_pw, g_ph);
|
||
}
|
||
|
||
static void reader_free_page_bufs(void)
|
||
{
|
||
for (int i = 0; i < RD_PAGE_CACHE; i++) { free(g_page_buf[i]); g_page_buf[i] = NULL; }
|
||
}
|
||
|
||
/* 渲染当前页 + 前后各一页到 3 槽缓存(对齐设计「当前±前±后」;动画档为 无/滑动/淡入 R1 默认无)。 */
|
||
static int reader_refill_page(void)
|
||
{
|
||
if (!g_book) return -1;
|
||
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
|
||
uint16_t bg = rgb565(g_page_bg), fg = rgb565(g_page_fg);
|
||
int cur = rd_book_current(g_book);
|
||
int np = rd_book_pages(g_book);
|
||
int slots[RD_PAGE_CACHE] = { cur - 1, cur, cur + 1 };
|
||
for (int i = 0; i < RD_PAGE_CACHE; i++) {
|
||
int p = slots[i];
|
||
if (p >= 0 && p < np) rd_book_render(g_book, p, g_page_buf[i], g_pw * 2, bg, fg);
|
||
else for (int y = 0; y < g_ph; y++) /* 空白槽填背景 */
|
||
for (int x = 0; x < g_pw; x++) g_page_buf[i][(size_t)y * g_pw + x] = bg;
|
||
}
|
||
return 0;
|
||
}
|
||
return 0; /* 兜底路径:render_text 直绘 */
|
||
}
|
||
|
||
/* ---------- 打开 ---------- */
|
||
void reader_open(const char *path)
|
||
{
|
||
if (!path || !path[0]) return;
|
||
if (g_book) { rd_book_close(g_book); g_book = NULL; }
|
||
reader_free_page_bufs();
|
||
|
||
const char *fp = resolve_font();
|
||
g_has_font = (fp != NULL);
|
||
g_fs = RD_READER_DEFAULT_FS; g_ls = RD_READER_DEFAULT_LS;
|
||
|
||
/* 进度(含字号/行距偏好) */
|
||
rdprog prog;
|
||
int hasprog = (rdprog_load(path, &prog) == 1) && prog.valid;
|
||
if (hasprog) { if (prog.fs >= 14 && prog.fs <= 40) g_fs = prog.fs; if (prog.ls >= 100 && prog.ls <= 200) g_ls = prog.ls; }
|
||
|
||
int sbh = meow_ui_statusbar_height();
|
||
g_pw = meow_ui_width(); g_ph = meow_ui_height();
|
||
g_top = sbh + 34; g_mx = 16; g_my = 20;
|
||
|
||
const meow_theme *th = meow_theme_get();
|
||
g_page_bg = th->surface; g_page_fg = th->onSurface;
|
||
|
||
/* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */
|
||
int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top);
|
||
if (rc != 0) {
|
||
meow_log(MEOW_LOG_WARN, "book open failed rc=%d path=%s", rc, path);
|
||
g_book = NULL; g_has_font = 0;
|
||
g_notice_ms = 3000;
|
||
return;
|
||
}
|
||
chapter_name();
|
||
|
||
if (g_has_font || rd_book_is_image(g_book)) {
|
||
reader_free_page_bufs();
|
||
for (int i = 0; i < RD_PAGE_CACHE; i++)
|
||
g_page_buf[i] = (uint16_t *)malloc((size_t)g_pw * (size_t)g_ph * 2);
|
||
if (!g_page_buf[RD_PAGE_SLOT_CUR]) { reader_free_page_bufs(); g_has_font = 0; }
|
||
}
|
||
|
||
/* 续读:进度锚(段,字符偏移)→ 跳页 */
|
||
if (hasprog && g_book) {
|
||
rd_book_jump_to_anchor(g_book, prog.seg, prog.ch, g_fs, g_ls);
|
||
}
|
||
|
||
reader_refill_page();
|
||
|
||
g_menu_open = 0; g_dir_open = 0;
|
||
if (g_reader_active) reader_redraw();
|
||
}
|
||
|
||
/* ---------- 视图生命周期 ---------- */
|
||
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
|
||
static void reader_on_enter(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
g_reader_active = 1;
|
||
if (g_book) { reader_refill_page(); chapter_name(); }
|
||
reader_redraw();
|
||
}
|
||
static void reader_on_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
reader_reflow_persist(); /* 离开保存进度 */
|
||
g_reader_active = 0;
|
||
}
|
||
static void reader_on_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
if (g_book) rd_book_close(g_book);
|
||
g_book = NULL;
|
||
reader_free_page_bufs();
|
||
g_menu_open = 0; g_dir_open = 0; g_reader_active = 0;
|
||
}
|
||
|
||
meow_view *reader_view_create(void)
|
||
{
|
||
static meow_view_hooks hooks = { reader_on_create, reader_on_enter, reader_on_leave,
|
||
reader_on_destroy, NULL, NULL, NULL };
|
||
meow_view *v = meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &hooks, NULL);
|
||
g_canvas = meow_widget_custom_create(0, 0, 480, 720, reader_canvas_render, NULL, NULL);
|
||
meow_view_add_widget(v, g_canvas);
|
||
return v;
|
||
}
|
||
|
||
int reader_is_active(void) { return g_reader_active; }
|
||
|
||
void reader_on_tap(int x, int y)
|
||
{
|
||
if (!g_book) { return; }
|
||
if (g_dir_open) {
|
||
/* 目录覆盖层:点条目跳转,点空白关闭 */
|
||
int n = rd_book_toc_n(g_book);
|
||
if (n > 0 && x >= 0 && y >= g_dir_top) {
|
||
int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
|
||
int idx = (y - g_dir_top) / row;
|
||
if (idx < n && idx >= 0) {
|
||
char t[96]; int sg; int32_t cch;
|
||
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
|
||
if (g_book && sg >= 0) {
|
||
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
|
||
g_dir_open = 0; g_menu_open = 0;
|
||
snprintf(g_chapter, sizeof g_chapter, "%s", t);
|
||
reader_refill_page();
|
||
reader_redraw();
|
||
reader_reflow_persist();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
g_dir_open = 0; reader_redraw();
|
||
return;
|
||
}
|
||
if (g_menu_open) {
|
||
/* 菜单按钮命中 */
|
||
int y0 = g_ph - RD_MENU_H;
|
||
if (y >= y0) {
|
||
int bw = g_pw / RD_MENU_BTN;
|
||
int idx = x / bw;
|
||
switch (idx) {
|
||
case 0: /* 返回书架 */
|
||
g_menu_open = 0;
|
||
reader_reflow_persist();
|
||
meow_view_page_pop();
|
||
return;
|
||
case 1: /* 目录 */
|
||
if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
|
||
else { g_menu_open = 0; reader_redraw(); }
|
||
return;
|
||
case 2: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
|
||
case 3: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
|
||
case 4: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
|
||
case 5: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
|
||
}
|
||
if (y >= y0) return;
|
||
}
|
||
g_menu_open = 0; /* 点菜单外关闭 */
|
||
reader_redraw();
|
||
return;
|
||
}
|
||
/* 三栏:左 1/3 上一页、右 1/3 下一页、中央唤菜单 */
|
||
if (x < g_pw / 3) { rd_book_turn(g_book, -1); reader_refill_page(); reader_redraw(); }
|
||
else if (x > g_pw * 2 / 3) { rd_book_turn(g_book, 1); reader_refill_page(); reader_redraw(); }
|
||
else { g_menu_open = 1; reader_redraw(); }
|
||
}
|
||
|
||
void reader_on_swipe(int dir, int dx, int dy)
|
||
{
|
||
(void)dx; (void)dy;
|
||
if (!g_book) return;
|
||
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
|
||
if (dir == MEOW_SWIPE_LEFT) { rd_book_turn(g_book, 1); }
|
||
else if (dir == MEOW_SWIPE_RIGHT) { rd_book_turn(g_book, -1); }
|
||
else return;
|
||
reader_refill_page(); reader_redraw();
|
||
}
|
||
|
||
void reader_on_tick(uint64_t now)
|
||
{
|
||
(void)now;
|
||
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
|
||
}
|