Files
MeowOS-For-R3P2/patch_v2/reader_app/app/reader.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

1381 lines
57 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/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 "reader_conf.h"
#include "rdchapter.h" /* RDCHAP_TITLE_MAX(章节标题) */
#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 5 /* 返回 | 目录 | 跳转 | 界面 | 设置(R3.3 加「跳转」) */
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */
#define RD_UI_H 268 /* 「界面」排版面板高 */
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 2
#define RD_PAGE_SLOT_CUR 0
static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=cur [1]=next(FreeType 页位图;B.1 降 3→2 槽,省 1 页 0.69MB) */
static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */
static int g_menu_open;
static int g_ui_open; /* 「界面」排版面板(B) */
static int g_dir_open; /* 目录覆盖层 */
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
static int g_jump_open; /* 跳转覆盖层(R3.3) */
static int g_jump_mode; /* 1=章节模式; 2=文件/段模式; 0=未定 */
static int g_jump_sel; /* 当前选中章节/段下标 */
static int g_jump_scroll; /* 列表滚动(首行项下标) */
static char g_jump_buf[8]; /* 章内/段内页码输入缓冲(ASCII 数字) */
static int g_chap_cache_idx = -1; /* 章节范围缓存(章起/止页;随字号/行距重排失效) */
static int g_chap_cache_start, g_chap_cache_end;
static char g_font_path[400];
static int g_notice_ms;
/* ---- R3 翻页动画(轻量水平滑入) ---- */
static meow_anim *g_anim; /* 活动翻页动画;NULL=无 */
static uint16_t *g_anim_old; /* 动画「滑出」页位图(切页前快照) */
static int g_anim_dir; /* 1=下一页(旧左移/新右入);-1=上一页(旧右移/新左入) */
/* ---- R3 段刮擦(右缘拖动快速定位 + 预览) ---- */
#define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */
static int g_scrub; /* 1=刮擦中 */
static int g_scrub_page; /* 刮擦目标页(实时预览用) */
static int g_scrub_moved; /* 本次刮擦是否发生了拖动(无拖动=点按,不当翻页跳页) */
/* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */
static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
static char g_chapter[RDCHAP_TITLE_MAX]; /* 顶栏章节名(TXT 无章节=文件名) */
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
static uint64_t g_prog_last_save_ms; /* 上次进度落盘时刻(tick 节流) */
#define RD_READER_DEFAULT_FS 20
#define RD_READER_DEFAULT_LS 130
/* R3:编码循环(显式编码;Auto 由打开时探测决定,不在此列)。
* -1 哨兵不用;TXT 初始 = 打开探测结果(GB18030 兜底)。 */
static const rd_enc_t g_enc_cycle[] = { RD_ENC_GB18030, RD_ENC_BIG5, RD_ENC_UTF8,
RD_ENC_UTF16LE, RD_ENC_UTF16BE };
#define RD_ENC_CYCLE_N ((int)(sizeof g_enc_cycle / sizeof g_enc_cycle[0]))
static const char *enc_cycle_name(int idx)
{
switch (g_enc_cycle[idx]) {
case RD_ENC_GB18030: return "GB18030";
case RD_ENC_BIG5: return "Big5";
case RD_ENC_UTF8: return "UTF-8";
case RD_ENC_UTF16LE: return "UTF-16LE";
case RD_ENC_UTF16BE: return "UTF-16BE";
default: return "?";
}
}
static int enc_to_cycle_idx(rd_enc_t e)
{
for (int i = 0; i < RD_ENC_CYCLE_N; i++) if (g_enc_cycle[i] == e) return i;
return -1;
}
/* 前向声明(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));
}
/* 按 rdconf.bg/theme 解析阅读页底/字色(默认纯黑,B 定案)。 */
static void reader_resolve_bg(uint32_t *bg, uint32_t *fg)
{
const rdconf *c = rdconf_get();
if (!c) { *bg = 0x000000; *fg = 0x9AA4C8; return; }
if (strcmp(c->bg, "black") == 0) { *bg = 0x000000; *fg = 0x9AA4C8; return; }
if (strcmp(c->bg, "white") == 0) { *bg = 0xFFFFFF; *fg = 0x1A1A2A; return; }
if (strcmp(c->bg, "sepia") == 0) { *bg = 0xF5EFDC; *fg = 0x3B3A32; return; }
/* auto:跟随主题(rd.theme=light/dark 时强制该明暗的 surface/onSurface) */
meow_theme_mode m = MEOW_THEME_AUTO;
if (strcmp(c->theme, "light") == 0) m = MEOW_THEME_LIGHT;
else if (strcmp(c->theme, "dark") == 0) m = MEOW_THEME_DARK;
if (m == MEOW_THEME_AUTO) {
const meow_theme *th = meow_theme_get();
*bg = th->surface; *fg = th->onSurface;
} else {
meow_theme t = meow_theme_defaults(m);
*bg = t.surface; *fg = t.onSurface;
}
}
/* 「界面」面板改背景(写 rdconf.bg + 重解析颜色 + 重排版页缓冲)。 */
static void reader_set_bg(const char *bg)
{
rdconf *c = rdconf_get();
if (!c) return;
strncpy(c->bg, bg, sizeof c->bg - 1); c->bg[sizeof c->bg - 1] = 0;
if (strcmp(c->bg, "black") && strcmp(c->bg, "white") &&
strcmp(c->bg, "sepia") && strcmp(c->bg, "auto")) strcpy(c->bg, "black");
rdconf_persist();
reader_resolve_bg(&g_page_bg, &g_page_fg);
reader_refill_page();
reader_redraw();
}
/* 递归(深度≤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 固定)。
* 仅在章节已就绪时用章名(rd_book_chapter_ready 不触发扫描),首屏/未解析时用文件名,
* 避免开书即整书扫描占 UI 线程。 */
if (!g_book) { g_chapter[0] = 0; return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int page = rd_book_current(g_book);
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
char t[RDCHAP_TITLE_MAX];
if (rd_book_chapter(g_book, idx, t, sizeof t, NULL, NULL, NULL, NULL) == 0) {
snprintf(g_chapter, sizeof g_chapter, "%s", t);
return;
}
}
}
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
}
static void reader_reflow(void)
{
if (!g_book) return;
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
g_chap_cache_idx = -1; /* 页索引随字号/行距变 → 章范围缓存失效 */
int seg = 0; int32_t ch = 0;
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
g_prog_dirty = 1;
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.enc = (g_book && !rd_book_is_image(g_book)) ? (int)g_enc_cycle[g_enc_idx] : -1;
p.ts = (uint64_t)time(NULL);
rdprog_save(rd_book_path(g_book), &p);
g_prog_dirty = 0;
}
/* ---------- 渲染(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);
}
/* 正文内容区几何:y0=top(顶栏下沿),y1=ph-my(下边距)。 */
static int rect_y0(void) { return g_top; }
static int rect_y1(void) { return g_ph - g_my; }
/* 把一页位图的行区间 [y0,y1) 以水平偏移 dx 拷到 back_buf(裁剪到屏幕)。动画/局部重绘共用。 */
static void blit_rows(uint16_t *src, int dx, int y0, int y1)
{
unsigned char *bb = meow_ui_back_buf();
int pitch = meow_ui_back_buf_pitch();
int bpp = meow_ui_back_buf_bpp();
if (!bb || pitch <= 0 || bpp != 16) return;
int sx0 = dx < 0 ? -dx : 0;
int dx0 = dx > 0 ? dx : 0;
int w = g_pw - (dx < 0 ? -dx : dx);
if (w <= 0) return;
for (int y = y0; y < y1; y++) {
if (y < 0 || y >= g_ph) continue;
memcpy(bb + (size_t)y * pitch + (size_t)dx0 * 2,
src + (size_t)y * g_pw + sx0, (size_t)w * 2);
}
}
/* 子矩形直拷(同屏幕坐标):用于右缘刮擦条区恢复/局部重绘。 */
static void blit_sub(uint16_t *src, int sx, int sy, int w, int h)
{
unsigned char *bb = meow_ui_back_buf();
int pitch = meow_ui_back_buf_pitch();
int bpp = meow_ui_back_buf_bpp();
if (!bb || pitch <= 0 || bpp != 16) return;
for (int y = sy; y < sy + h; y++) {
if (y < 0 || y >= g_ph) continue;
memcpy(bb + (size_t)y * pitch + (size_t)sx * 2, src + (size_t)y * g_pw + sx, (size_t)w * 2);
}
}
/* R3 右缘刮擦指示条:轨道 + 拇指(当前/目标位置)+ 刮擦中预览框。render_cb 调用。 */
static void reader_draw_scrub(const meow_theme *th)
{
int np = g_book ? rd_book_pages(g_book) : 0;
if (np <= 0) return;
int y0 = rect_y0(), y1 = rect_y1(), h = y1 - y0;
if (h <= 0) return;
int page = g_scrub ? g_scrub_page : rd_book_current(g_book);
if (page < 0) page = 0;
if (page >= np) page = np - 1;
int x = g_pw - 10;
meow_ui_fill_rect(x - 1, y0, 3, h, th->dim); /* 轨道 */
int denom = np > 1 ? (np - 1) : 1;
int ty = y0 + (int)((long long)h * (np > 1 ? page : 0) / denom);
meow_ui_fill_rect(x - 3, ty - 4, 7, 8, th->primary); /* 拇指 */
if (g_scrub) {
char pv[24]; snprintf(pv, sizeof pv, "%d/%d", page + 1, np);
int bw = 58, bh = 22;
int px = x - bw - 6;
int py = ty - bh / 2;
if (py < y0) py = y0;
if (py + bh > y1) py = y1 - bh;
meow_ui_fill_rect(px, py, bw, bh, th->surfaceContainer);
meow_ui_text_center(px + bw / 2, py + (bh - 20) / 2, pv, 16, th->fg);
}
}
/* ---------------- R3.3 跳转弹层(章节模式 / 文件·段模式) ----------------
* 复用 g_dir 系列「半透明底 + 条目列表 + 当前高亮」;顶部提供:当前章节/段信息、页码输入
* (自绘数字键盘 0-9 + ⌫ + 跳)、快捷(首 / ±10 / ±100 / 末)。只对 TXT(seg 模式)启用:
* rd_book_chapter_n>0 → 章节模式;否则 rd_book_seg_n>0 → 文件/段模式。 */
#define J_SHORTCUT_N 6
#define J_SHORTCUT_X0 16
#define J_SHORTCUT_GAP 6
#define J_SHORTCUT_H 32
#define J_KEY_W 64
#define J_KEY_H 42
#define J_KEY_GAP 8
#define J_ROW 30
enum {
J_NONE = 0,
J_GO_FIRST = 1, J_GO_M100 = 2, J_GO_M10 = 3,
J_GO_P10 = 4, J_GO_P100 = 5, J_GO_LAST = 6,
J_DIG_0 = 10, J_DIG_1 = 11, J_DIG_2 = 12, J_DIG_3 = 13, J_DIG_4 = 14,
J_DIG_5 = 15, J_DIG_6 = 16, J_DIG_7 = 17, J_DIG_8 = 18, J_DIG_9 = 19,
J_BACK = 30, J_JUMP = 31,
J_ITEM = 100, /* 100 + i = 列表第 i 项 */
};
static int jump_hdr_y(void) { return g_top; }
static int jump_info_y(void) { return g_top + 30; }
static int jump_shortcut_y(void){ return g_top + 56; }
static int jump_input_y(void) { return g_top + 92; }
static int jump_pad_y(void) { return g_top + 128; }
static int jump_list_y(void) { return g_top + 128 + 4 * (J_KEY_H + J_KEY_GAP) + 10; }
static int chapter_range(int idx, int *start, int *end)
{
if (!g_book || idx < 0) return -1;
if (g_chap_cache_idx == idx) { if (start) *start = g_chap_cache_start; if (end) *end = g_chap_cache_end; return 0; }
int64_t co = 0;
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) != 0) return -1;
int s = rd_book_page_for_char(g_book, co);
if (s < 0) return -1;
int n = rd_book_chapter_n(g_book);
int e;
if (idx + 1 < n) {
int64_t co2 = 0;
if (rd_book_chapter(g_book, idx + 1, NULL, 0, &co2, NULL, NULL, NULL) != 0) return -1;
e = rd_book_page_for_char(g_book, co2) - 1;
} else {
e = rd_book_pages(g_book) - 1;
}
if (e < s) e = s;
g_chap_cache_idx = idx; g_chap_cache_start = s; g_chap_cache_end = e;
if (start) *start = s;
if (end) *end = e;
return 0;
}
/* 跳转选中项 idx 的页区间(章节模式=章范围;文件模式=段范围)。 */
static int jump_region(int idx, int *start, int *end, int *inp, int *ofp)
{
if (!g_book) return -1;
int s, e;
if (g_jump_mode == 1) {
if (chapter_range(idx, &s, &e) != 0) return -1;
} else if (g_jump_mode == 2) {
int nseg = rd_book_seg_n(g_book);
if (idx < 0 || idx >= nseg) return -1;
s = rd_book_seg_page(g_book, idx);
if (s < 0) return -1;
e = s + rd_book_seg_page_count(g_book, idx) - 1;
if (e < s) e = s;
} else return -1;
int cur = rd_book_current(g_book);
if (cur < s) cur = s;
if (cur > e) cur = e;
if (start) *start = s;
if (end) *end = e;
if (inp) *inp = cur - s + 1;
if (ofp) *ofp = e - s + 1;
return 0;
}
/* 页底条右侧文本:章内 y/Z · 全书 x/M(章节就绪且范围可算时),否则 全书 x/M。 */
static void footer_progress(char *out, int outsz)
{
int np = rd_book_pages(g_book);
int page = rd_book_current(g_book);
if (np <= 0) { snprintf(out, outsz, "0/0"); return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
int s = -1, e = -1;
if (chapter_range(idx, &s, &e) == 0 && e >= s) {
int inp = page - s + 1, ofp = e - s + 1;
if (inp > 0 && ofp > 0) { snprintf(out, outsz, "%d/%d · %d/%d", inp, ofp, page + 1, np); return; }
}
}
}
snprintf(out, outsz, "%d/%d", page + 1, np);
}
static void jump_chip(int x, int y, int w, int h, const char *label, int active, const meow_theme *th)
{
if (active) meow_ui_fill_rect(x, y, w, h, th->primaryContainer);
else meow_ui_fill_rect(x, y, w, h, th->surfaceContainerHighest);
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(x + w / 2, y + (h - 20) / 2, label, 20, tc);
}
static void reader_draw_jump(const meow_theme *th)
{
if (!g_book) return;
int page = rd_book_current(g_book);
int np = rd_book_pages(g_book);
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
/* 头栏 */
int y0 = jump_hdr_y();
meow_ui_fill_rect(0, y0, g_pw, 30, th->surfaceContainer);
char hdr[48];
snprintf(hdr, sizeof hdr, "跳转 · %s", (g_jump_mode == 1) ? "章节" : "文件/段");
meow_ui_text(16, y0 + 5, hdr, 20, th->onSurface);
meow_ui_draw_rect(0, y0 + 29, g_pw, 1, th->outlineVariant);
if (n <= 0) {
meow_ui_text(20, jump_info_y() + 12, "本书无可用跳转目标", 20, th->dim);
meow_ui_text(20, g_ph - 30, "点空白关闭", 16, th->dim);
return;
}
/* 信息行 */
char info[256]; info[0] = 0;
int s, e, inp, ofp;
if (g_jump_mode == 1) {
char lab[RDCHAP_TITLE_MAX];
int num = -1;
if (rd_book_chapter(g_book, g_jump_sel, lab, sizeof lab, NULL, NULL, NULL, &num) == 0 &&
jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
char prefix[24]; prefix[0] = 0;
if (num >= 0) snprintf(prefix, sizeof prefix, "第%d章 ", num);
snprintf(info, sizeof info, "%s%s · 章内 %d/%d · 全书 %d/%d", prefix, lab, inp, ofp, page + 1, np);
}
} else {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0)
snprintf(info, sizeof info, "段 %d/%d · 段内 %d/%d · 全书 %d/%d", g_jump_sel + 1, n, inp, ofp, page + 1, np);
}
meow_ui_text(16, jump_info_y() + 3, info[0] ? info : "—", 16, th->fg);
/* 快捷 chips */
static const char *sc_lbl[J_SHORTCUT_N] = { "首", "-100", "-10", "+10", "+100", "末" };
int x = J_SHORTCUT_X0;
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
for (int i = 0; i < J_SHORTCUT_N; i++) {
jump_chip(x, jump_shortcut_y(), cw, J_SHORTCUT_H, sc_lbl[i], 0, th);
x += cw + J_SHORTCUT_GAP;
}
/* 输入行 */
meow_ui_text(16, jump_input_y() + 5, (g_jump_mode == 1) ? "章内页" : "段内页", 20, th->fg);
char ib[48];
snprintf(ib, sizeof ib, "%s", g_jump_buf[0] ? g_jump_buf : "_");
meow_ui_text(120, jump_input_y() + 3, ib, 24, th->primary);
meow_ui_text_right(g_pw - 16, jump_input_y() + 7, "输页码点「跳」 · ⌫退格", 16, th->dim);
/* 数字键盘 1..9 / ⌫ 0 跳 */
static const char *pad[4][3] = {
{ "1", "2", "3" }, { "4", "5", "6" }, { "7", "8", "9" }, { "⌫", "0", "跳" },
};
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
int ky = jump_pad_y() + r * (J_KEY_H + J_KEY_GAP);
jump_chip(kx, ky, J_KEY_W, J_KEY_H, pad[r][c], (r == 3 && c == 2), th);
}
}
/* 条目列表(当前选中高亮) */
int top = jump_list_y();
int maxshow = (g_ph - top - 8) / J_ROW;
if (maxshow < 1) maxshow = 1;
/* 让当前选中项进入可视区 */
if (g_jump_sel >= 0) {
if (g_jump_sel < g_jump_scroll) g_jump_scroll = g_jump_sel;
else if (g_jump_sel >= g_jump_scroll + maxshow) g_jump_scroll = g_jump_sel - maxshow + 1;
}
if (g_jump_scroll + maxshow > n) g_jump_scroll = (n - maxshow < 0 ? 0 : n - maxshow);
if (g_jump_scroll < 0) g_jump_scroll = 0;
for (int i = 0; i < maxshow; i++) {
int item = g_jump_scroll + i;
if (item >= n) break;
char t[96];
if (g_jump_mode == 1) {
if (rd_book_chapter(g_book, item, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
} else {
snprintf(t, sizeof t, "段 %d", item + 1);
}
int yy = top + i * J_ROW;
if (item == g_jump_sel) meow_ui_fill_rect(0, yy, g_pw, J_ROW - 4, th->surfaceContainer);
meow_ui_text(24, yy + 4, t, 20, th->fg);
meow_ui_draw_rect(12, yy + J_ROW - 6, g_pw - 24, 1, th->outline);
}
meow_ui_text(20, g_ph - 30, "点条目跳转 · 点空白关闭", 16, th->dim);
}
static int reader_jump_hit(int x, int y)
{
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
if (n <= 0) return J_NONE;
/* 快捷 chips */
int sy = jump_shortcut_y();
if (y >= sy && y < sy + J_SHORTCUT_H) {
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
if (x >= J_SHORTCUT_X0) {
int k = (x - J_SHORTCUT_X0) / (cw + J_SHORTCUT_GAP);
if (k >= 0 && k < J_SHORTCUT_N) return J_GO_FIRST + k;
}
return J_NONE;
}
/* 键盘 */
int py = jump_pad_y();
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
int ky = py + r * (J_KEY_H + J_KEY_GAP);
if (y >= ky && y < ky + J_KEY_H) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
if (x >= kx && x < kx + J_KEY_W) {
if (r == 3) {
if (c == 2) return J_JUMP;
if (c == 0) return J_BACK;
return J_DIG_0; /* 第三行 c==1 → 数字 0 */
}
return J_DIG_0 + (r * 3 + c) + 1; /* 0..2 行:1 2 3 / 4 5 6 / 7 8 9 */
}
}
return J_NONE;
}
}
/* 条目列表 */
int top = jump_list_y();
if (y >= top) {
int idx = (y - top) / J_ROW + g_jump_scroll;
if (idx < n) return J_ITEM + idx;
return J_NONE;
}
return J_NONE;
}
static void reader_jump_to(int page)
{
if (!g_book) return;
int np = rd_book_pages(g_book);
if (np <= 0) return;
int cur = rd_book_current(g_book);
(void)cur;
rd_book_set_page(g_book, page);
g_prog_dirty = 1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
static void reader_jump_action(int a)
{
if (a == J_NONE) { g_jump_open = 0; reader_redraw(); return; }
int s, e, inp, ofp;
if (a >= J_GO_FIRST && a <= J_GO_LAST) {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) != 0) return;
int target = rd_book_current(g_book);
switch (a) {
case J_GO_FIRST: target = s; break;
case J_GO_M100: target -= 100; break;
case J_GO_M10: target -= 10; break;
case J_GO_P10: target += 10; break;
case J_GO_P100: target += 100; break;
case J_GO_LAST: target = e; break;
}
if (target < s) target = s;
if (target > e) target = e;
reader_jump_to(target);
return;
}
if (a >= J_DIG_0 && a <= J_DIG_9) {
size_t len = strlen(g_jump_buf);
if (len + 1 < sizeof g_jump_buf) { g_jump_buf[len] = (char)('0' + (a - J_DIG_0)); g_jump_buf[len + 1] = 0; }
reader_redraw();
return;
}
if (a == J_BACK) {
size_t len = strlen(g_jump_buf);
if (len > 0) g_jump_buf[len - 1] = 0;
reader_redraw();
return;
}
if (a == J_JUMP) {
if (g_jump_buf[0]) {
int tpage = atoi(g_jump_buf);
if (tpage < 1) tpage = 1;
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
int t = s + (tpage - 1);
if (t < s) t = s;
if (t > e) t = e;
reader_jump_to(t);
}
}
return;
}
if (a >= J_ITEM) {
int item = a - J_ITEM;
if (g_jump_mode == 1) {
if (jump_region(item, &s, &e, &inp, &ofp) == 0) {
g_jump_sel = item;
reader_jump_to(s);
}
} else {
int sp = rd_book_seg_page(g_book, item);
if (sp >= 0) { g_jump_sel = item; reader_jump_to(sp); }
}
return;
}
g_jump_open = 0; reader_redraw();
}
/* 打开跳转弹层:解析章节(首次会扫描+缓存),按是否有章节选章节/文件·段模式。 */
static void reader_jump_open(void)
{
if (!g_book) return;
g_jump_buf[0] = 0;
g_jump_scroll = 0;
int nc = rd_book_chapter_n(g_book);
if (nc > 0) {
g_jump_mode = 1;
int idx = -1;
rd_book_chapter_for_page(g_book, rd_book_current(g_book), &idx);
g_jump_sel = (idx >= 0) ? idx : 0;
} else if (rd_book_seg_n(g_book) > 0) {
g_jump_mode = 2;
int seg = rd_book_seg_of_page(g_book, rd_book_current(g_book));
g_jump_sel = (seg >= 0) ? seg : 0;
} else {
g_jump_mode = 2;
g_jump_sel = 0;
}
g_jump_open = 1;
g_menu_open = 0;
reader_redraw();
}
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
* 几何 draw/hit 共用(常量)。 */
#define UI_LABEL_X 20
#define UI_CTRL_X 130
#define UI_CTRL_W (480 - UI_CTRL_X - 16)
#define UI_ROW_H 34
static int ui_panel_y(void) { return g_ph - RD_UI_H; }
/* 单个「值按钮」:[-]/[+] 或文本钮。cx=控件区起点;返回该按钮中点 x。 */
static void ui_draw_step(int x, int w, int y, const char *tag, int active, const meow_theme *th)
{
if (active) meow_ui_fill_rect(x, y, w, 28, th->primaryContainer);
else meow_ui_fill_rect(x, y, w, 28, th->surfaceContainerHighest);
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(x + w / 2, y + 4, tag, 20, tc);
}
static void ui_draw_chips(const char *const *labels, int n, int cur, int x, int y, int w, const meow_theme *th)
{
int gap = 4;
int cw = (w - (n - 1) * gap) / n;
for (int i = 0; i < n; i++) {
int cx = x + i * (cw + gap);
if (i == cur) meow_gfx_fill_round(cx, y, cw, 28, 14, th->primaryContainer);
else meow_gfx_fill_round(cx, y, cw, 28, 14, th->surfaceContainerHighest);
uint32_t tc = (i == cur) ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(cx + cw / 2, y + 4, labels[i], 20, tc);
}
}
static void reader_draw_ui_panel(const meow_theme *th)
{
int y0 = ui_panel_y();
int top = y0 + 30;
int ry;
const rdconf *c = rdconf_get();
/* 面板底 + scrim */
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
meow_ui_fill_rect(0, y0, g_pw, RD_UI_H, th->surfaceContainer);
meow_ui_draw_rect(0, y0, g_pw, 1, th->outlineVariant);
meow_ui_text_center(240, y0 + 5, "界面 · 排版", 20, th->fg);
/* 0 字号 */
ry = top + 0 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "字号", 20, th->fg);
char v[16]; snprintf(v, sizeof v, "%d", g_fs);
ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th);
meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg);
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th);
/* 1 行距 */
ry = top + 1 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "行距", 20, th->fg);
snprintf(v, sizeof v, "%d", g_ls);
ui_draw_step(UI_CTRL_X, 40, ry + 3, "-", 0, th);
meow_ui_text_center(UI_CTRL_X + 40 + (UI_CTRL_W - 80) / 2, ry + 6, v, 20, th->fg);
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 40, 40, ry + 3, "+", 0, th);
/* 2 背景(4 档:黑/白/米黄/自动;当前=标亮) */
ry = top + 2 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "背景", 20, th->fg);
static const char *bg_lbl[4] = { "黑", "白", "米黄", "自动" };
int bgcur = 0;
if (c) { if (!strcmp(c->bg, "white")) bgcur = 1; else if (!strcmp(c->bg, "sepia")) bgcur = 2;
else if (!strcmp(c->bg, "auto")) bgcur = 3; }
ui_draw_chips(bg_lbl, 4, bgcur, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 3 顶栏显隐(开/关) */
ry = top + 3 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "顶栏", 20, th->fg);
static const char *yn[2] = { "开", "关" };
ui_draw_chips(yn, 2, g_show_header ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 4 底栏显隐 */
ry = top + 4 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "底栏", 20, th->fg);
ui_draw_chips(yn, 2, g_show_footer ? 0 : 1, UI_CTRL_X, ry + 3, UI_CTRL_W, th);
/* 5 编码(TXT;显示当前,点循环) */
ry = top + 5 * UI_ROW_H;
meow_ui_text(UI_LABEL_X, ry + 6, "编码", 20, th->fg);
if (g_book && !rd_book_is_image(g_book)) {
ui_draw_step(UI_CTRL_X + UI_CTRL_W - 120, 120, ry + 3, enc_cycle_name(g_enc_idx), 0, th);
} else {
meow_ui_text(UI_CTRL_X, ry + 6, "—", 20, th->dim);
}
/* 6 返回阅读 */
ry = top + 6 * UI_ROW_H;
ui_draw_step(UI_CTRL_X, UI_CTRL_W, ry + 2, "返回阅读", 1, th);
}
/* 「界面」面板命中;返回动作码(见下方 enum)。0=无。 */
enum {
UI_NONE = 0, UI_FS_DEC, UI_FS_INC, UI_LS_DEC, UI_LS_INC,
UI_BG_0, UI_BG_1, UI_BG_2, UI_BG_3, UI_HDR_TOGGLE, UI_FTR_TOGGLE, UI_ENC, UI_CLOSE
};
static int reader_ui_hit(int x, int y)
{
int y0 = ui_panel_y();
if (y < y0) return UI_NONE;
int top = y0 + 30;
int ry;
int cx = UI_CTRL_X, cw = UI_CTRL_W;
/* 0 字号 */
ry = top + 0 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H) {
if (x >= cx && x < cx + 40) return UI_FS_DEC;
if (x >= cx + cw - 40 && x < cx + cw) return UI_FS_INC;
return UI_NONE;
}
/* 1 行距 */
ry = top + 1 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H) {
if (x >= cx && x < cx + 40) return UI_LS_DEC;
if (x >= cx + cw - 40 && x < cx + cw) return UI_LS_INC;
return UI_NONE;
}
/* 2 背景 4 chips */
ry = top + 2 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw) {
int gap = 4, cwp = (cw - 3 * gap) / 4;
int k = (x - cx) / (cwp + gap);
if (k < 0) k = 0;
if (k > 3) k = 3;
return UI_BG_0 + k;
}
/* 3 顶栏 */
ry = top + 3 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_HDR_TOGGLE;
/* 4 底栏 */
ry = top + 4 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_FTR_TOGGLE;
/* 5 编码 */
ry = top + 5 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_ENC;
/* 6 返回阅读 */
ry = top + 6 * UI_ROW_H;
if (y >= ry && y < ry + UI_ROW_H && x >= cx && x < cx + cw)
return UI_CLOSE;
return UI_NONE;
}
static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)w; (void)ctx;
meow_ui_clear(g_page_bg);
chapter_name(); /* 每帧同步顶栏/页底章节名(就绪后取章名,否则文件名) */
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 上屏) */
blit_rows(g_page_buf[RD_PAGE_SLOT_CUR], 0, 0, g_ph);
} else {
rd_book_render_text(g_book, page, reader_draw_line_atlas, NULL);
}
}
/* 页头条(书名 + 章节名;可选,rd.header) */
int sbh = meow_ui_statusbar_height();
const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
if (g_show_header) {
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
meow_ui_text(16, sbh + 6, bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, sbh + 6, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg);
meow_ui_draw_rect(0, sbh + RD_HEADER_H - 1, g_pw, 1, th->outlineVariant);
}
/* 页底条(章节 + 页码;可选,rd.footer) */
if (g_show_footer) {
char ft[64];
footer_progress(ft, sizeof ft);
int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
}
/* R3 刮擦指示条(非目录/面板/跳转覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open && !g_ui_open && !g_jump_open) reader_draw_scrub(th);
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);
/* 5 按钮:返回 | 目录 | 跳转 | 界面 | 设置 */
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, labels[i], 20, th->fg);
}
}
if (g_ui_open && g_book) reader_draw_ui_panel(th);
if (g_jump_open && g_book) reader_draw_jump(th);
if (g_dir_open && g_book) {
/* 目录覆盖层:TXT 有章节 → 章节列表(复用 rd_book 章节数据,与跳转共享);否则 rdsrc 目录;无则「无章节」。 */
int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int ntoc = (nchap == 0) ? rd_book_toc_n(g_book) : 0;
int n = (nchap > 0) ? nchap : ntoc;
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[160];
int page = rd_book_current(g_book);
int curchap = -1;
if (nchap > 0) rd_book_chapter_for_page(g_book, page, &curchap);
for (int i = 0; i < maxshow; i++) {
int hl = 0;
if (nchap > 0) {
if (rd_book_chapter(g_book, i, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
hl = (i == curchap);
} else {
int sg = -1; int32_t cch = 0;
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
hl = (page == sg);
}
int y0 = top + i * row;
if (hl) 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, (rd_book_chapter_n(g_book) == 0) ? "本书无章节" : "本书无目录", 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; }
if (g_anim_old) { free(g_anim_old); g_anim_old = NULL; }
}
/* 分配 FreeType 页位图槽(cur+next 两槽)。已备当前槽则幂等返回(开书/重入共用)。
* 动画快照 g_anim_old 不在此常驻:翻页时懒分配、动画完成即释放(见 turn_animated/anim_done)。
* 分配失败 → g_has_font=0,走字库兜底路径。 */
static void reader_alloc_page_bufs(void)
{
if (!(g_has_font || rd_book_is_image(g_book))) return;
if (g_page_buf[RD_PAGE_SLOT_CUR]) return; /* 已备当前槽(重入)→ 幂等 */
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; }
}
/* 渲染当前页 + 下一预取页到 2 槽缓存(B.1:降 3→2,去 prev 槽;动画档 无/滑动/淡入 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, 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 直绘 */
}
/* ---------- R3 翻页动画(重载页位图,轻量水平滑动,仅内容区分帧) ---------- */
static void reader_anim_update(meow_anim *a, float v, void *ctx)
{
(void)a; (void)ctx;
if (!g_book || !g_anim_old || !g_page_buf[RD_PAGE_SLOT_CUR]) return;
int off = (int)(v * g_pw);
int y0 = rect_y0(), y1 = rect_y1();
meow_ui_fill_rect(0, y0, g_pw, y1 - y0, g_page_bg); /* 内容区清底色 */
int old_x, new_x;
if (g_anim_dir == 1) { old_x = -off; new_x = g_pw - off; } /* 下一页:旧左移/新右入 */
else { old_x = off; new_x = off - g_pw; } /* 上一页:旧右移/新左入 */
blit_rows(g_anim_old, old_x, y0, y1);
blit_rows(g_page_buf[RD_PAGE_SLOT_CUR], new_x, y0, y1);
meow_ui_flush_region(0, y0, g_pw, y1 - y0);
}
static void reader_anim_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)ctx;
g_anim = NULL;
/* 动画完成/取消即释放旧页快照位图,不驻留;下一翻页懒分配(见 reader_turn_animated)。 */
if (g_anim_old) { free(g_anim_old); g_anim_old = NULL; }
if (!canceled) { reader_redraw(); reader_reflow_persist(); } /* 顶栏 n/N+% 更新 + 进度保存 */
}
static void reader_turn_animated(int dir)
{
if (!g_book) return;
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 取消进行中动画,重开 */
/* 无页位图(atlas 兜底):瞬切 */
if (!(g_has_font || rd_book_is_image(g_book)) || !g_page_buf[RD_PAGE_SLOT_CUR]) {
rd_book_turn(g_book, dir);
g_prog_dirty = 1;
reader_refill_page(); reader_redraw(); reader_reflow_persist();
return;
}
if (!g_anim_old) g_anim_old = (uint16_t *)malloc((size_t)g_pw * g_ph * 2);
if (!g_anim_old) { rd_book_turn(g_book, dir); g_prog_dirty = 1; reader_refill_page(); reader_redraw(); return; }
memcpy(g_anim_old, g_page_buf[RD_PAGE_SLOT_CUR], (size_t)g_pw * g_ph * 2);
rd_book_turn(g_book, dir);
g_prog_dirty = 1;
reader_refill_page();
g_anim_dir = dir;
meow_anim_desc d = { 0.0f, 1.0f, 140, MEOW_EASE_OUT, reader_anim_update, reader_anim_done, NULL };
g_anim = meow_anim_start(&d);
if (!g_anim) reader_redraw(); /* 池满等:瞬时落定 */
}
/* ---------- R3 段刮擦(右缘拖动快速定位 + 预览) ---------- */
static int scrub_page_for_y(int y)
{
int y0 = rect_y0(), y1 = rect_y1(), h = y1 - y0;
int np = g_book ? rd_book_pages(g_book) : 0;
if (h <= 0 || np <= 0) return g_book ? rd_book_current(g_book) : 0;
int cy = y - y0; if (cy < 0) cy = 0; if (cy > h) cy = h;
return (int)((long long)cy * (np - 1) / h);
}
static void reader_scrub_jump(int page)
{
if (!g_book) return;
int np = rd_book_pages(g_book);
if (np <= 0) return;
rd_book_set_page(g_book, page);
g_prog_dirty = 1;
reader_refill_page();
reader_redraw(); /* 整页落定:正文 + 顶栏 + 刮擦条 */
reader_reflow_persist();
}
/* 拖动中局部重绘(不动正文页,仅页底页码 + 右缘条/预览)。内容页保持(预览语义)。 */
static void reader_scrub_preview(int page)
{
g_scrub_page = page;
if (!g_book) return;
const meow_theme *th = meow_theme_get();
int pages = rd_book_pages(g_book);
int y0 = rect_y0(), y1 = rect_y1();
char ft[48];
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", g_scrub_page + 1, pages);
else snprintf(ft, sizeof ft, "0/0");
/* 页底条页码重画(页码已移到底条) */
if (g_show_footer) {
int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : "阅读", 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
}
/* 右缘条区清残留(恢复页面内容)再画条 + 预览框;无页位图(字库兜底)则跳过恢复 */
if (g_page_buf[RD_PAGE_SLOT_CUR]) blit_sub(g_page_buf[RD_PAGE_SLOT_CUR], g_pw - 120, y0, 120, y1 - y0);
reader_draw_scrub(th);
if (g_show_footer) meow_ui_flush_region(0, g_ph - RD_FOOT_H, g_pw, RD_FOOT_H);
meow_ui_flush_region(g_pw - 120, y0, 120, y1 - y0);
}
static void reader_scrub_begin(int x, int y)
{
(void)x;
if (!g_book || g_menu_open || g_dir_open || g_jump_open) return;
if (g_pw - x > RD_SCRUB_W) return;
if (y < rect_y0() || y >= rect_y1()) return;
int np = rd_book_pages(g_book);
if (np <= 1) return;
g_scrub = 1;
g_scrub_moved = 0; /* 每次触控重置;拖动(touch_move)才置 1,区分点按/刮擦 */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
reader_scrub_preview(scrub_page_for_y(y));
}
/* ---------- 打开 ---------- */
/* R3.1:页索引缓存目录 = <TF>/MeowData/os.meow.reader/index。由 meow_config_dir
* (<TF>/MeowData/<pkg>/data) 剥尾 "/data" → <TF>/MeowData/<pkg>,接 "index",mkdir -p 后注入。
* 同基座派生 R3.2 分段流式缓存根 <TF>/MeowData/os.meow.reader(book.c 再接 /seg/<hash>)。 */
static void reader_setup_index_dir(void)
{
const char *data = meow_config_dir();
if (!data || !data[0]) return; /* 无卡/宿主:不持久(book.c 直排) */
char base[420];
size_t n = strlen(data);
const char *suf = "/data"; size_t sl = strlen(suf);
size_t keep = (n >= sl && strcmp(data + n - sl, suf) == 0) ? n - sl : n;
if (keep >= sizeof base) keep = sizeof base - 1;
memcpy(base, data, keep); base[keep] = 0;
char dir[440];
if (snprintf(dir, sizeof dir, "%s/index", base) >= (int)sizeof dir) return;
/* mkdir -p 幂等(progress 同款) */
char t[440];
strncpy(t, dir, sizeof t - 1); t[sizeof t - 1] = 0;
for (char *c = t + 1; *c; c++) if (*c == '/') { *c = 0; mkdir(t, 0755); *c = '/'; }
mkdir(t, 0755);
rd_book_set_index_dir(dir);
rd_book_set_seg_base(base); /* R3.2:分段缓存根(book.c 派生 /seg/<hash>) */
}
int reader_open(const char *path)
{
if (!path || !path[0]) return -1;
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);
/* 全局默认字号/行距(reader.conf;进度里的每书偏好随后覆盖) */
const rdconf *rdc = rdconf_get();
g_fs = (rdc && rdc->fs >= 14 && rdc->fs <= 40) ? rdc->fs : RD_READER_DEFAULT_FS;
g_ls = (rdc && rdc->ls >= 100 && rdc->ls <= 200) ? rdc->ls : RD_READER_DEFAULT_LS;
g_show_header = rdc ? (rdc->header ? 1 : 0) : 1;
g_show_footer = rdc ? (rdc->footer ? 1 : 0) : 1;
/* 进度(含字号/行距偏好) */
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 + RD_HEADER_H; g_mx = 16; g_my = 20;
/* 阅读页底/字色:rd.bg(默认纯黑,B)→ 覆盖 th->surface */
reader_resolve_bg(&g_page_bg, &g_page_fg);
/* 页索引缓存(建好目录并注入;命中则开书免整书重排,秒开) */
reader_setup_index_dir();
/* 开书(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 -1;
}
chapter_name();
if (g_has_font || rd_book_is_image(g_book)) {
reader_free_page_bufs(); /* 丢弃上一本书的页位图(重开/换书) */
reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */
}
/* 复位动画/刮擦/覆盖层状态(跨书) */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0;
g_chap_cache_idx = -1;
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
if (g_book && !rd_book_is_image(g_book)) {
int idx = enc_to_cycle_idx(rd_book_enc_get(g_book));
g_enc_idx = idx >= 0 ? idx : 0;
if (hasprog && prog.enc >= 0) {
int pi = enc_to_cycle_idx((rd_enc_t)prog.enc);
if (pi >= 0) {
if (g_enc_cycle[pi] != rd_book_enc_get(g_book)) {
if (rd_book_set_enc(g_book, g_enc_cycle[pi]) == 0) { g_enc_idx = pi; chapter_name(); }
} else g_enc_idx = pi;
}
}
} else g_enc_idx = 0;
/* 续读:进度锚(段,字符偏移)→ 跳页 */
if (hasprog && g_book) {
rd_book_jump_to_anchor(g_book, prog.seg, prog.ch, g_fs, g_ls);
}
reader_refill_page();
if (g_reader_active) reader_redraw();
return 0;
}
/* ---------- R3.4 章节重解析(app/settings.c 调用) ---------- */
int reader_has_chapter_book(void)
{
if (!g_book || rd_book_is_image(g_book)) return 0;
/* TXT + 分段流式(seg 模式)即支持章节解析;用 rd_book_seg_n(不触发扫描)判定。 */
return (rd_book_seg_n(g_book) > 0) ? 1 : 0;
}
int reader_reparse_chapters(void)
{
if (!g_book) return -1;
int rc = rd_book_reparse_chapters(g_book);
if (rc >= 0) g_chap_cache_idx = -1; /* 章节列表变化 → 章范围缓存失效 */
if (g_reader_active) reader_redraw();
return rc;
}
/* ---------- 视图生命周期 ---------- */
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_alloc_page_bufs(); /* 离开已释放页位图 → 重建;未释放则幂等返回 */
reader_refill_page();
chapter_name();
}
reader_redraw();
}
static void reader_on_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
reader_reflow_persist(); /* 离开保存进度 */
reader_free_page_bufs(); /* 离开阅读页释放页位图(cur+next 两槽+动画快照)→ on_enter 重建 */
g_reader_active = 0;
}
static void reader_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
if (g_book) rd_book_close(g_book);
g_book = NULL;
reader_free_page_bufs();
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0; g_scrub = 0; g_reader_active = 0;
g_chap_cache_idx = -1;
}
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)
{
(void)x;
if (!g_book) { return; }
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾(仅真实拖动过才跳;点按右缘=翻下一页) */
g_scrub = 0;
if (g_scrub_moved) { reader_scrub_jump(g_scrub_page); return; }
/* 未拖动:当作点按,落到下方三区翻页逻辑(右 1/3 下一页),避免右缘误触跳远页 */
}
if (g_jump_open) {
/* 跳转弹层:快捷/键盘/条目 → 执行;点空白关闭 */
int a = reader_jump_hit(x, y);
reader_jump_action(a);
return;
}
if (g_dir_open) {
/* 目录覆盖层:TXT 章节(复用 rd_book 章节数据)或 rdsrc 目录;点条目跳转,点空白关闭 */
int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int n = (nchap > 0) ? nchap : 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) {
if (nchap > 0) {
int64_t co = 0;
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) == 0) {
int sp = rd_book_page_for_char(g_book, co);
if (sp >= 0) {
rd_book_set_page(g_book, sp);
g_dir_open = 0; g_menu_open = 0;
g_chap_cache_idx = -1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
return;
}
}
} else {
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_ui_open) {
int a = reader_ui_hit(x, y);
switch (a) {
case UI_FS_DEC: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
case UI_FS_INC: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
case UI_LS_DEC: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
case UI_LS_INC: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
case UI_BG_0: reader_set_bg("black"); break;
case UI_BG_1: reader_set_bg("white"); break;
case UI_BG_2: reader_set_bg("sepia"); break;
case UI_BG_3: reader_set_bg("auto"); break;
case UI_HDR_TOGGLE:
g_show_header = !g_show_header;
if (rdconf_get()) { rdconf_get()->header = g_show_header; rdconf_persist(); }
reader_redraw();
break;
case UI_FTR_TOGGLE:
g_show_footer = !g_show_footer;
if (rdconf_get()) { rdconf_get()->footer = g_show_footer; rdconf_persist(); }
reader_redraw();
break;
case UI_ENC: /* 编码切换(TXT/Big5 繁体书) */
if (g_book && !rd_book_is_image(g_book)) {
int ni = (g_enc_idx + 1) % RD_ENC_CYCLE_N;
if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) {
g_enc_idx = ni;
chapter_name();
g_prog_dirty = 1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
}
break;
case UI_CLOSE: g_ui_open = 0; reader_redraw(); break;
default: if (a == UI_NONE) { g_ui_open = 0; reader_redraw(); } break;
}
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: /* 目录(TXT 章节 → 章节列表;无章节显示「本书无章节」) */
g_dir_open = 1; g_menu_open = 0; reader_redraw();
return;
case 2: /* 跳转(章节/文件·段模式) */
if (rd_book_chapter_n(g_book) > 0 || rd_book_seg_n(g_book) > 0) { reader_jump_open(); }
else 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 3: /* 界面(阅读排版设置) */
g_ui_open = 1; g_menu_open = 0; reader_redraw();
return;
case 4: /* 设置 → App 设置页 */
reader_reflow_persist();
meow_view_page_push(ACT_SETTINGS);
return;
}
if (y >= y0) return;
}
g_menu_open = 0; /* 点菜单外关闭 */
reader_redraw();
return;
}
/* 三栏:左 1/3 上一页、右 1/3 下一页、中央唤菜单 */
if (x < g_pw / 3) { reader_turn_animated(-1); }
else if (x > g_pw * 2 / 3) { reader_turn_animated(1); }
else { if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } 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_scrub) g_scrub = 0; /* 刮擦中被滑走 → 退出刮擦 */
/* 水平滑 = 返回语义(镜像菜单「返回书架」,见上文 g_menu_open 分支)。SDK 把「左缘右滑
* (返回手势)」判为 MEOW_SWIPE_LEFT 下发;旧语义拿去翻下一页,会与 SDK 自绘的左缘返回
* 指示条错位(M6.40 真机 bug:有指示条却翻页/不返回)。故水平滑统一「先关一层 overlay,
* 否则回书架」。翻页不再交给水平滑——保留三区点按(左 1/3 上页 / 右 1/3 下页)与右缘
* 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; }
if (g_jump_open) { g_jump_open = 0; reader_redraw(); return; }
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) {
reader_reflow_persist();
meow_view_page_pop();
}
}
/* ---- R3 段刮擦触控(app.c on_touch_down/on_touch_move 路由) ---- */
void reader_on_touch_down(int x, int y) { reader_scrub_begin(x, y); }
void reader_on_touch_move(int x, int y)
{
(void)x;
if (g_scrub && g_book) { g_scrub_moved = 1; reader_scrub_preview(scrub_page_for_y(y)); }
}
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
* 真正翻页在 UI 线程 reader_on_tick 消费(避免跨线程直接改页面状态)。 */
void reader_page_key(int dir)
{
int p = g_volpage_pending + dir;
if (p > 2) p = 2;
if (p < -2) p = -2;
g_volpage_pending = p;
}
void reader_on_tick(uint64_t now)
{
(void)now;
if (g_volpage_pending) {
int dir = g_volpage_pending > 0 ? 1 : -1;
g_volpage_pending = 0;
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
reader_turn_animated(dir);
}
/* Phase 2C:空闲预取下一段(距段界 ≤2% 时提前解码进窗口,翻段界零突发) */
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
rd_book_prefetch(g_book);
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
g_prog_last_save_ms = now;
reader_reflow_persist();
}
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
}