脏工作区现场收编: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 与容器一致)。
1381 lines
57 KiB
C
1381 lines
57 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 "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(); }
|
||
}
|