Files
MeowOS-SDK/tuku/ui/reader/reader.c
T
AskaEth ff97f3cee7 feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接
- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页);
  排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测)
- 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化
  (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码
- 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」
- 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出
- 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示)
- 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判,
  超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险)
- 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的
  阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
2026-11-04 21:49:33 +08:00

1861 lines
63 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.
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏):左右翻页 + 条漫连续滚动。
*
* 用户口径:优先流畅度,其次功能。两条阅读路径都不在翻页/滚动热路径上解码:
* - 后台 pthread 解码窗口(视口 ±1 页,总驻留 ≤ MR_SLOTS 张 downscale 后页图),
* 主循环只取已备好的图;
* - 翻页:跟手拖动只 blit 两张已缓存页图,松手后 meow_anim 一次短滑动吸附(150ms);
* - 条漫:meow_scroll 跟手 track + 松手 fling 惯性,按滚动位置懒解码视口 ±1 页;
* - 只 flush 受影响区(本页/整条可见区);解码/驻留耗时都 meow_log 打点。
*
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。条漫排版用
* meta() 不解码像素;解码入口走 SDK M12 预算解码 meow_img_load_file_dim(JPEG 解码期缩解,
* PNG/BMP 整幅解码后降采样——超大头先 meow_img_probe_file 预判,超过预算跳过并告警)。
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
*
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
* 经 meow_config 持久化(键 reader.mode/page.<册标识>,见 logic/reader.c),下次打开同一册
* 自动恢复。书库行角标由 logic/library.c 扫描时读取同一份状态(副标题后缀「续读 Pn」)。
*
* 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁;
* 2.5s 超时则降级落位并提示)。
*
* 册间衔接:末页前进 → 同父目录下一个含图册(书库自然序)的首页;首页后退 → 上一册末页;
* 无相邻册时停住并轻提示(不误触退出)。条漫在条顶/条底继续拉 MR_BOOK_PULL 像素松手触发。
*
* 手势(与 SDK 语义对齐,见 sdk/meow_ui_input.c touch_release):
* - 左缘(x<24)向右划 = 系统返回(MEOW_SWIPE_LEFT)→ 不消费,app.c page_pop 退出;
* - 屏底 28px 上滑 = 系统回桌面(MEOW_EV_HOME),SDK 不分发给 App,本页不起手;
* - 翻页模式:中部横拖 = 跟手翻页;tap 分区方向感知(左/右 1/3 = 上/下一页,中 1/3 =
* chrome 显隐);双击 = 放大。
* - 条漫模式:竖向拖动 = 整条滚动(fling 惯性);tap = chrome 显隐;双击/横向滑动不动作。
*
* 节拍:app.c 的 on_tick 只转给 library_on_tick(不在本页 scope)。本页用一条常驻
* meow_anim “ticker”(SDK 主循环每轮 meow_anim_tick)驱动结果收取、跳页落位、续读节流
* 保存与单击判定超时;翻页动画与 meow_scroll fling 也走同一节拍。
*
* 生命周期:on_enter 建状态 + 建 TopAppBar/滑块 + 打开册并恢复模式/页码;on_leave 先落盘
* 续读状态,再全量释放(停解码线程、释放页图/册索引/条漫排版/滚动会话/控件)。
*/
#include "tuku_internal.h"
#include "ui/reader/reader.h"
#include "logic/reader/page_source.h"
#include "logic/reader/nav.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#define MR_SLOTS 4 /* 页图缓存槽上限(条漫视口±1 ≤4;翻页只用 3) */
#define MR_TOP_BAR_H 56
#define MR_SLIDER_H 40
#define MR_SLIDE_MS 150 /* 松手后一次短滑动吸附 */
#define MR_BOUNCE_MS 120 /* 未过阈值回弹 */
#define MR_DOUBLE_MS 240 /* 单击等待窗口(区分双击) */
#define MR_ZOOM_PCT 200 /* 双击放大倍率 */
#define MR_TICK_MS 16
#define MR_PAINT_MS 16 /* 跟手拖动重绘节流(≈60fps 上限) */
#define MR_DRAG_SLOP 8 /* 小于此位移算点击 */
#define MR_ZOOM_TEMP_CAP (6 * 1024 * 1024) /* 缩放临时缓冲软上限(>此值降倍率,保内存) */
#define MR_BOOK_PULL 110 /* 条漫到顶/底后再拉这么远,松手进相邻册 */
#define MR_JUMP_TIMEOUT_MS 2500 /* 目标页迟迟解不出来 → 降级落位并提示 */
#define MR_CONT_SAVE_MS 1000 /* 续读状态落盘节流 */
#define MR_TOAST_MS 1400
#define MR_LOAD_RAW_WARN (12L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */
#define MR_LOAD_RAW_CAP (40L * 1024 * 1024) /* 超此值直接跳过(防 OOM;页显示占位) */
#define MR_REQ_MAX 8
#define MR_RES_MAX 4
/* view on_event 的事件号不在公共头(sdk/meow_ui_internal.h §6.4);本页只用到 SWIPE=3。 */
#define MR_EV_SWIPE 3
static mr_page_ctrl g_ctrl;
static char g_dir[MR_PATH_MAX];
static mr_dir_book g_db;
static mr_book g_book;
static int g_count; /* 0=空册/不可读 */
static int g_open_err;
static int g_page; /* 翻页模式当前页;条漫模式=最近落位页 */
static int g_mode = MR_MODE_PAGED;
static int g_vw, g_vh; /* 内容视口 = 全屏 */
static int g_fit = MR_FIT_WIDTH;
static int g_zoom_pct = 100;
static int g_pan_x, g_pan_y;
static int g_chrome = 1;
static meow_widget *g_tb;
static int g_act_mode = -1; /* TopAppBar 动作下标:模式切换 */
static int g_act_jump = -1; /* 动作下标:跳页滑块 */
static int g_act_fit = -1; /* 动作下标:fit 档循环 */
/* ---- 条漫:排版表 + 滚动会话 ---- */
static mr_strip g_strip;
static int g_strip_ok;
static int g_woff; /* 条漫滚动偏移(px) */
static meow_scroll *g_scroll;
/* ---- 跳页(chrome 滑块 + 预解码落位) ---- */
static meow_widget *g_slider;
static int g_jump_open;
static mr_jump g_jump;
/* ---- 轻提示(册边界/模式切换/跳页) ---- */
static char g_toast[64];
static uint64_t g_toast_until;
static int g_title_page = -1; /* 标题里显示的页(条漫滚动停止后同步) */
/* ---- 续读落盘(节流) ---- */
static int g_cont_dirty;
static int g_cont_last_page = -1;
static int g_cont_last_mode = -1;
static uint64_t g_cont_save_ms;
/* ---- 当前解码窗(≤MR_SLOTS 页,按优先级排列) ---- */
static int g_win_idx[MR_SLOTS];
static int g_win_n;
static int g_fail_paint; /* 窗口内出现解码失败页 → tick 重绘错误占位 */
/* ---- 页图缓存(≤4 张 downscale 后的图)---- */
typedef struct page_slot {
int idx; /* -1=空 */
meow_img *img; /* 已 downscale 的页图 */
int src_w, src_h;
} page_slot;
static page_slot g_slot[MR_SLOTS];
/* 解码失败页标记:避免对坏图/超大图无限重试,UI 显示「图片无法显示」而不是一直加载中。 */
static unsigned char g_page_fail[(MR_BOOK_MAX_PAGES + 7) / 8];
static int page_failed(int idx)
{
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return 0;
return (g_page_fail[idx >> 3] >> (idx & 7)) & 1;
}
static void page_mark_failed(int idx)
{
if (idx < 0 || idx >= MR_BOOK_MAX_PAGES) return;
g_page_fail[idx >> 3] |= (unsigned char)(1u << (idx & 7));
}
/* ---- 后台解码队列 ---- */
typedef struct page_req { int idx; unsigned gen; } page_req;
typedef struct page_res { int idx; unsigned gen; meow_img *img; int fail; } page_res;
static pthread_t g_worker;
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
static int g_worker_on, g_stop;
static unsigned g_gen = 1;
static page_req g_req[MR_REQ_MAX];
static int g_req_head, g_req_tail, g_req_n;
static page_res g_res[MR_RES_MAX];
static int g_res_n;
/* ---- 节拍/动画 ---- */
static meow_anim *g_ticker;
static meow_anim *g_snap; /* 翻页滑动吸附 */
static meow_anim *g_bounce; /* 未过阈值回弹 */
static int g_slide_delta; /* 翻页目标增量(动画中) */
static int g_slide_from; /* 进场页起点(px) */
static int g_anim_v; /* 进场页当前位移(px) */
static uint64_t g_flip_t0;
static uint64_t g_last_tick;
static uint64_t g_last_paint;
static int g_quitting; /* on_leave 期间抑制回调 */
/* ---- 跟手拖动 ---- */
static int g_dragging; /* 1=本次按下正在拖动 */
static int g_drag_mode; /* 0=无 1=横滑翻页 2=放大平移 3=滑块 4=条漫滚动 */
static int g_drag_x0, g_drag_y0;
static int g_drag_off; /* 当前页水平位移(px) */
static int g_drag_pan_x0, g_drag_pan_y0;
static int g_touch_moved; /* 本次按下是否已产生 >slop 的位移 */
static int g_wo_last_y; /* 条漫:上一次 move 的 y */
static int g_wo_y0; /* 条漫:按下点 y(slop 判定) */
static int g_wo_pull; /* 条漫:到边后继续拉的累计量(>0 往末页方向) */
/* ---- 单击延迟(双击判定窗口)---- */
static int g_tap_pending;
static int g_tap_x, g_tap_y;
static uint64_t g_tap_ms;
static uint64_t g_last_tap_ms; /* 上一次点击(跨按下保持,双击判定用) */
static int g_last_tap_x, g_last_tap_y;
/* ---- 前向声明(互相调用:册切换/模式/落位) ---- */
static int open_book(const char *dir, int start_page, int mode);
static int book_switch(int delta);
static void mode_set(int mode, int from_page);
static void toast(const char *msg);
static void slider_place(void);
static void update_title(void);
static void cont_touch(void);
static void cont_save_now(void);
static int current_page(void);
/* ============================================================ */
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
/* ============================================================ */
/* 扩展名判断:JPEG 支持解码期缩解(M12),PNG/BMP 只能整幅解码后降采样。 */
static int path_is_jpeg(const char *path)
{
const char *dot = path ? strrchr(path, '.') : NULL;
if (!dot) return 0;
dot++;
return strcasecmp(dot, "jpg") == 0 || strcasecmp(dot, "jpeg") == 0;
}
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
{
(void)ud;
if (!path || !path[0]) return NULL;
/* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。
* - 非缩解格式(PNG/BMP)原始 RGBA 超过预算 → 跳过,避免真机 OOM(页面显示占位);
* - 超过告警线 → 记 WARN(残余风险,按 M12 报告口径如实提示)。 */
int pw = 0, ph = 0, ch = 0;
if (meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0) {
long long raw = (long long)pw * (long long)ph * 4;
if (!path_is_jpeg(path)) {
if (raw > MR_LOAD_RAW_CAP) {
meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (raw %lldKB > cap): %s",
pw, ph, raw / 1024, path);
return NULL;
}
if (raw > MR_LOAD_RAW_WARN)
meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s",
pw, ph, raw / 1024, path);
}
}
meow_img *im = meow_img_load_file_dim(path, maxdim);
if (!im) return NULL;
/* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */
if (maxdim > 0 && (meow_img_w(im) > maxdim || meow_img_h(im) > maxdim)) {
meow_img *small = meow_img_downscale(im, maxdim);
if (small) { meow_img_free(im); im = small; }
}
return im;
}
static void reader_unload_page(meow_img *img)
{
meow_img_free(img);
}
/* ============================================================ */
/* 内存观测(验收用:进出 reader 对比) */
/* ============================================================ */
static long mem_kb(const char *path, const char *key)
{
FILE *f = fopen(path, "r");
if (!f) return -1;
char line[128];
long v = -1;
size_t kl = strlen(key);
while (fgets(line, sizeof line, f)) {
if (strncmp(line, key, kl) == 0) { v = strtol(line + kl, NULL, 10); break; }
}
fclose(f);
return v;
}
static int resident_pages(void)
{
int n = 0;
for (int i = 0; i < MR_SLOTS; i++)
if (g_slot[i].img) n++;
return n;
}
static long resident_bytes(void)
{
long b = 0;
for (int i = 0; i < MR_SLOTS; i++)
if (g_slot[i].img) b += (long)g_slot[i].src_w * g_slot[i].src_h * 4;
return b;
}
static void log_mem(const char *tag)
{
meow_log(MEOW_LOG_INFO, "reader mem[%s]: avail=%ldKB rss=%ldKB pages=%d img=%ldKB",
tag, mem_kb("/proc/meminfo", "MemAvailable:"),
mem_kb("/proc/self/status", "VmRSS:"), resident_pages(), resident_bytes() / 1024);
}
/* ============================================================ */
/* 续读状态(逐册 last page + mode;写入节流,避免滚动时频繁写卡) */
/* ============================================================ */
static int current_page(void)
{
if (g_count <= 0) return 0;
if (g_mode == MR_MODE_WEBTOON && g_strip_ok)
return mr_strip_page_at(&g_strip, g_woff);
return g_page;
}
static void cont_save_now(void)
{
if (!g_dir[0] || g_count <= 0) { g_cont_dirty = 0; return; }
int p = current_page();
if (p == g_cont_last_page && g_mode == g_cont_last_mode) { g_cont_dirty = 0; return; }
mr_cont_save(g_dir, p, g_mode);
g_cont_last_page = p;
g_cont_last_mode = g_mode;
g_cont_dirty = 0;
meow_log(MEOW_LOG_INFO, "reader cont save page=%d/%d mode=%s",
p + 1, g_count, mr_mode_name(g_mode));
}
static void cont_touch(void)
{
g_cont_dirty = 1;
}
/* 只在滚动/翻页停下且距上次写盘超过 MR_CONT_SAVE_MS 时落盘。 */
static void cont_save_throttled(void)
{
if (!g_cont_dirty) return;
if (g_scroll && meow_scroll_active(g_scroll)) return;
if (g_dragging) return;
uint64_t now = meow_ui_time_ms();
if (now - g_cont_save_ms < MR_CONT_SAVE_MS) return;
g_cont_save_ms = now;
cont_save_now();
}
/* ============================================================ */
/* 页图缓存 */
/* ============================================================ */
static int focus_page(void)
{
if (g_count <= 0) return -1;
if (g_mode == MR_MODE_WEBTOON && g_strip_ok)
return mr_strip_page_at(&g_strip, g_woff + g_vh / 2); /* 视口中心页 */
return g_page;
}
static int in_window(int idx)
{
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
for (int i = 0; i < g_win_n; i++)
if (g_win_idx[i] == idx) return 1;
return 0;
}
static page_slot *slot_find(int idx)
{
if (idx < 0) return NULL;
for (int i = 0; i < MR_SLOTS; i++)
if (g_slot[i].idx == idx && g_slot[i].img) return &g_slot[i];
return NULL;
}
static void slot_clear(int i)
{
if (i < 0 || i >= MR_SLOTS) return;
if (g_slot[i].img) meow_img_free(g_slot[i].img);
g_slot[i].img = NULL;
g_slot[i].idx = -1;
g_slot[i].src_w = g_slot[i].src_h = 0;
}
static void slots_free_all(void)
{
for (int i = 0; i < MR_SLOTS; i++) slot_clear(i);
}
/* 放进缓存;缓存满时淘汰离焦点页最远的一张。返回 1=已放入。 */
static int slot_put(int idx, meow_img *img)
{
if (!img || idx < 0) return 0;
int focus = focus_page();
int empty = -1, far = -1, far_d = -1;
for (int i = 0; i < MR_SLOTS; i++) {
if (g_slot[i].idx == idx && g_slot[i].img) { meow_img_free(img); return 0; }
if (!g_slot[i].img && empty < 0) empty = i;
int d = !g_slot[i].img ? -1 : abs(g_slot[i].idx - focus);
if (d > far_d) { far_d = d; far = i; }
}
int i = empty >= 0 ? empty : far;
if (i < 0) { meow_img_free(img); return 0; }
slot_clear(i);
g_slot[i].idx = idx;
g_slot[i].img = img;
g_slot[i].src_w = meow_img_w(img);
g_slot[i].src_h = meow_img_h(img);
return 1;
}
/* ============================================================ */
/* 解码窗计算(≤MR_SLOTS 页,按优先级:跳页目标 > 焦点 > 视口 ±1) */
/* ============================================================ */
static void win_reset(void)
{
g_win_n = 0;
}
static void win_add(int idx)
{
if (idx < 0 || idx >= g_count || g_win_n >= MR_SLOTS) return;
for (int i = 0; i < g_win_n; i++)
if (g_win_idx[i] == idx) return;
g_win_idx[g_win_n++] = idx;
}
static void window_compute(void)
{
win_reset();
if (g_count <= 0) return;
int jt = mr_jump_target(&g_jump);
if (jt >= 0) win_add(jt); /* 跳页目标优先,否则会被窗口淘汰 */
int focus = focus_page();
win_add(focus);
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
int f = 0, l = 0;
int budget = MR_SLOTS - g_win_n; /* 可见±1;剩余槽位给视口 */
if (budget < 1) budget = 1;
mr_strip_window(&g_strip, g_woff, budget, &f, &l);
for (int i = f; i <= l; i++) win_add(i);
/* 视口窗口可能因预算被截:再补焦点两侧各一页,保证滚动方向有前瞻 */
for (int d = 1; d <= MR_SLOTS; d++) {
win_add(focus - d);
win_add(focus + d);
}
} else if (g_zoom_pct > 100) {
win_add(g_page); /* 放大:只驻留当前页,给缩放临时缓冲让内存 */
} else {
win_add(g_page - 1);
win_add(g_page + 1);
}
}
/* ============================================================ */
/* 后台解码 */
/* ============================================================ */
static int req_queued(int idx)
{
for (int i = 0; i < g_req_n; i++) {
int p = (g_req_head + i) % MR_REQ_MAX;
if (g_req[p].idx == idx) return 1;
}
return 0;
}
static void queue_page(int idx)
{
if (!mr_book_valid(&g_book) || idx < 0 || idx >= g_count) return;
if (slot_find(idx) || page_failed(idx)) return;
pthread_mutex_lock(&g_lock);
if (!g_stop && !req_queued(idx) && g_req_n < MR_REQ_MAX) {
g_req[g_req_tail].idx = idx;
g_req[g_req_tail].gen = g_gen;
g_req_tail = (g_req_tail + 1) % MR_REQ_MAX;
g_req_n++;
pthread_cond_signal(&g_cv);
}
pthread_mutex_unlock(&g_lock);
}
static void *reader_worker(void *arg)
{
(void)arg;
for (;;) {
page_req req;
pthread_mutex_lock(&g_lock);
while (!g_stop && g_req_n == 0) pthread_cond_wait(&g_cv, &g_lock);
if (g_stop) { pthread_mutex_unlock(&g_lock); break; }
req = g_req[g_req_head];
g_req_head = (g_req_head + 1) % MR_REQ_MAX;
g_req_n--;
pthread_mutex_unlock(&g_lock);
uint64_t t0 = meow_ui_time_ms();
meow_img *img = mr_book_open(&g_book, req.idx);
uint64_t dt = meow_ui_time_ms() - t0;
int w = img ? meow_img_w(img) : 0;
int h = img ? meow_img_h(img) : 0;
meow_log(MEOW_LOG_INFO, "reader decode page=%d ms=%llu %dx%d %s",
req.idx, (unsigned long long)dt, w, h, img ? "ok" : "fail");
pthread_mutex_lock(&g_lock);
if (!g_stop && g_res_n < MR_RES_MAX) {
g_res[g_res_n].idx = req.idx;
g_res[g_res_n].gen = req.gen;
g_res[g_res_n].img = img;
g_res[g_res_n].fail = img ? 0 : 1;
g_res_n++;
img = NULL; /* 所有权已移交(失败时本就是 NULL) */
}
pthread_mutex_unlock(&g_lock);
if (img) meow_img_free(img);
}
return NULL;
}
static void worker_start(void)
{
if (g_worker_on) return;
g_stop = 0;
if (pthread_create(&g_worker, NULL, reader_worker, NULL) == 0) g_worker_on = 1;
}
static void worker_stop(void)
{
if (!g_worker_on) return;
pthread_mutex_lock(&g_lock);
g_stop = 1;
pthread_cond_broadcast(&g_cv);
pthread_mutex_unlock(&g_lock);
pthread_join(g_worker, NULL);
g_worker_on = 0;
pthread_mutex_lock(&g_lock);
for (int i = 0; i < g_res_n; i++) meow_img_free(g_res[i].img);
g_res_n = 0;
g_req_n = 0;
g_req_head = g_req_tail = 0;
pthread_mutex_unlock(&g_lock);
}
/* 丢弃窗口外的排队请求(主线程调用)。 */
static void queue_prune(void)
{
pthread_mutex_lock(&g_lock);
page_req keep[MR_REQ_MAX];
int n = 0;
for (int i = 0; i < g_req_n; i++) {
page_req r = g_req[(g_req_head + i) % MR_REQ_MAX];
if (r.gen == g_gen && in_window(r.idx)) keep[n++] = r;
}
for (int i = 0; i < n; i++) g_req[i] = keep[i];
g_req_head = 0;
g_req_tail = n;
g_req_n = n;
pthread_mutex_unlock(&g_lock);
}
/* 收取后台结果:只留窗口内新页。返回 1=有图落位。 */
static int results_drain(void)
{
page_res got[MR_RES_MAX];
int n;
pthread_mutex_lock(&g_lock);
n = g_res_n;
for (int i = 0; i < n; i++) got[i] = g_res[i];
g_res_n = 0;
pthread_mutex_unlock(&g_lock);
int changed = 0;
for (int i = 0; i < n; i++) {
if (got[i].gen != g_gen) { meow_img_free(got[i].img); continue; }
if (got[i].fail) {
page_mark_failed(got[i].idx);
if (in_window(got[i].idx)) g_fail_paint = 1;
continue;
}
if (in_window(got[i].idx) && !slot_find(got[i].idx)) {
if (slot_put(got[i].idx, got[i].img)) changed = 1;
} else {
meow_img_free(got[i].img);
}
}
return changed;
}
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
/* 窗口随视口/当前页移动:淘汰窗口外图 → 清掉窗口外请求 → 按优先级补页。 */
static void window_update(void)
{
window_compute();
for (int i = 0; i < MR_SLOTS; i++)
if (g_slot[i].idx >= 0 && !in_window(g_slot[i].idx)) slot_clear(i);
queue_prune();
for (int i = 0; i < g_win_n; i++) queue_page(g_win_idx[i]);
}
/* ============================================================ */
/* 条漫排版 + 滚动会话 */
/* ============================================================ */
static int strip_meta_cb(void *ctx, int index, int *w, int *h)
{
return mr_book_meta((const mr_book *)ctx, index, w, h);
}
static int strip_ensure(void)
{
if (g_strip_ok && g_strip.count == g_count &&
g_strip.vw == g_vw && g_strip.vh == g_vh)
return 1;
mr_strip_free(&g_strip);
g_strip_ok = 0;
if (g_count <= 0) return 0;
uint64_t t0 = meow_ui_time_ms();
if (mr_strip_build(&g_strip, g_count, g_vw, g_vh, strip_meta_cb, &g_book) != 0) {
meow_log(MEOW_LOG_WARN, "reader strip layout failed pages=%d", g_count);
return 0;
}
g_strip_ok = 1;
meow_log(MEOW_LOG_INFO, "reader strip layout pages=%d total=%d ms=%llu",
g_count, mr_strip_total(&g_strip),
(unsigned long long)(meow_ui_time_ms() - t0));
return 1;
}
static int wo_max(void *ctx)
{
(void)ctx;
return mr_strip_max_offset(&g_strip);
}
static int wo_get(void *ctx)
{
(void)ctx;
return g_woff;
}
static void wo_set(void *ctx, int off)
{
(void)ctx;
int m = mr_strip_max_offset(&g_strip);
if (off < 0) off = 0;
if (off > m) off = m;
if (off == g_woff) return;
g_woff = off;
window_update();
cont_touch();
/* 重绘由调用方节流:拖动走 drag_paint(),fling 走 wo_redraw()。 */
}
static void wo_redraw(void *ctx)
{
(void)ctx;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void scroll_ensure(void)
{
if (g_scroll) return;
g_scroll = meow_scroll_create(wo_set, wo_max, wo_get, wo_redraw, NULL);
if (!g_scroll) meow_log(MEOW_LOG_WARN, "reader scroll session unavailable (pool full)");
}
static void scroll_free(void)
{
if (!g_scroll) return;
meow_scroll_free(g_scroll);
g_scroll = NULL;
}
/* ============================================================ */
/* 布局/绘制 */
/* ============================================================ */
static void page_target_size(const page_slot *s, int *w, int *h)
{
*w = *h = 0;
if (!s) return;
if (mr_nav_fit_size(g_fit, s->src_w, s->src_h, g_vw, g_vh, w, h) != 0) return;
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
}
static void current_geom(int *w, int *h)
{
page_slot *s = slot_find(g_page);
if (s) { page_target_size(s, w, h); return; }
/* 页未就绪:用 meta 估算,保证平移/缩放的夹取稳定。 */
int sw = 0, sh = 0;
if (mr_book_meta(&g_book, g_page, &sw, &sh) && sw > 0 && sh > 0) {
mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h);
if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h);
return;
}
*w = g_vw;
*h = g_vh;
}
static void clamp_pan(void)
{
int w, h;
current_geom(&w, &h);
mr_nav_pan_clamp(w, h, g_vw, g_vh, &g_pan_x, &g_pan_y);
}
/* 内容是否超出视口(原始档的方图/宽图即使不放大也需要平移)。 */
static int content_overflow(void)
{
int w, h;
current_geom(&w, &h);
return w > g_vw || h > g_vh;
}
/* 平移模式:放大,或原始档溢出。此时单指拖动=平移,左右 tap=平移步进。 */
static int pan_mode(void)
{
return g_zoom_pct > 100 || content_overflow();
}
/* 画一页(带水平位移);图与目标尺寸相等时走原生 blit(翻页热路径),否则最近邻缩放。 */
static void draw_page_at(int idx, int off_x, int off_y)
{
page_slot *s = slot_find(idx);
if (!s) return;
int w, h;
page_target_size(s, &w, &h);
if (w <= 0 || h <= 0) return;
int x = (g_vw - w) / 2 + g_pan_x + off_x;
int y = (g_vh - h) / 2 + g_pan_y + off_y;
if (w == s->src_w && h == s->src_h) meow_ui_img(x, y, s->img);
else meow_ui_img_scaled(x, y, w, h, s->img);
}
/* 条漫:按滚动偏移画可见页。典型 2:3 页的解码尺寸与显示尺寸相等 → 原生 blit;
* 其余按最近邻缩放(缩放在 SDK 内部申请目标尺寸临时缓冲,单页峰值受控)。 */
static void draw_strip(const meow_theme *th)
{
if (!g_strip_ok) {
meow_ui_text_center(g_vw / 2, g_vh / 2, "正在排版…", th->font_body, th->onSurfaceVariant);
return;
}
int first = mr_strip_page_at(&g_strip, g_woff);
int last = mr_strip_page_at(&g_strip, g_woff + g_vh - 1);
for (int i = first; i <= last; i++) {
int y = mr_strip_top(&g_strip, i) - g_woff;
int h = mr_strip_page_h(&g_strip, i);
if (h <= 0) continue;
page_slot *s = slot_find(i);
if (s) {
if (s->src_w == g_vw && s->src_h == h) meow_ui_img(0, y, s->img);
else meow_ui_img_scaled(0, y, g_vw, h, s->img);
continue;
}
/* 未就绪:只填可见部分(长页不整块清屏),给一帧占位而不是白屏。 */
int y0 = y < 0 ? 0 : y;
int y1 = y + h;
if (y1 > g_vh) y1 = g_vh;
if (y1 <= y0) continue;
meow_ui_fill_rect(0, y0, g_vw, y1 - y0, th->surfaceContainer);
if (y1 - y0 >= 48)
meow_ui_text_center(g_vw / 2, y0 + (y1 - y0) / 2 - 10,
page_failed(i) ? "图片无法显示" : "加载中…",
th->font_body, th->onSurfaceVariant);
}
}
static void draw_toast(const meow_theme *th)
{
if (!g_toast[0] || meow_ui_time_ms() >= g_toast_until) return;
int tw = meow_ui_text_width(g_toast, th->font_body) + 40;
if (tw > g_vw - 16) tw = g_vw - 16;
int x = (g_vw - tw) / 2;
int y = g_vh - 120;
meow_ui_fill_rect_alpha(x, y, tw, 44, th->surfaceContainerHighest, 235);
meow_ui_text_center(g_vw / 2, y + 12, g_toast, th->font_body, th->onSurface);
}
static void reader_draw(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)ctx;
int w = g_vw, h = g_vh;
if (g_open_err) {
meow_ui_text_center(w / 2, h / 2 - 20, "无法打开目录", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 + 24, g_dir, th->font_body, th->onSurfaceVariant);
return;
}
if (g_count <= 0) {
meow_ui_text_center(w / 2, h / 2 - 20, "空册", th->font_title, th->onSurface);
meow_ui_text_center(w / 2, h / 2 + 24, "这个文件夹里没有图片", th->font_body,
th->onSurfaceVariant);
return;
}
if (g_mode == MR_MODE_WEBTOON) {
draw_strip(th);
draw_toast(th);
return;
}
if (g_slide_delta != 0) {
/* 动画帧:进场页从 from 滑到 0,当前页同步让位(只 blit 两张已缓存页图)。 */
draw_page_at(g_page, g_anim_v - g_slide_from, 0);
draw_page_at(g_page + g_slide_delta, g_anim_v, 0);
draw_toast(th);
return;
}
if (g_drag_mode == 1 && g_drag_off != 0) {
/* 跟手:当前页按位移走,相邻页在另一侧同时露出(未就绪就只画当前页)。 */
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, g_drag_off);
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (target != g_page && slot_find(target)) {
int from = mr_nav_slide_from(rtl, delta, g_vw);
draw_page_at(target, g_drag_off + from, 0);
}
draw_page_at(g_page, g_drag_off, 0);
draw_toast(th);
return;
}
draw_page_at(g_page, 0, 0);
if (!slot_find(g_page)) {
meow_ui_text_center(w / 2, h / 2,
page_failed(g_page) ? "图片无法显示" : "加载中…",
th->font_body, th->onSurfaceVariant);
}
draw_toast(th);
}
/* ============================================================ */
/* 状态动作 */
/* ============================================================ */
static void update_title(void)
{
if (!g_tb) return;
static const char *const fit_name[MR_FIT_COUNT] = { "适宽", "适高", "原始" };
const char *fit = (g_fit >= 0 && g_fit < MR_FIT_COUNT) ? fit_name[g_fit] : "适宽";
char buf[64];
if (g_count > 0) {
if (g_mode == MR_MODE_WEBTOON)
snprintf(buf, sizeof buf, "%d / %d 条漫", current_page() + 1, g_count);
else
snprintf(buf, sizeof buf, "%d / %d 翻页 %s", g_page + 1, g_count, fit);
} else {
snprintf(buf, sizeof buf, "0 / 0");
}
meow_topappbar_set_title(g_tb, buf);
g_title_page = (g_count > 0) ? current_page() : -1;
}
static void toast(const char *msg)
{
snprintf(g_toast, sizeof g_toast, "%s", msg ? msg : "");
g_toast_until = meow_ui_time_ms() + MR_TOAST_MS;
meow_ui_invalidate(0, g_vh - 140, g_vw, 140);
}
static void slider_place(void)
{
if (!g_slider) return;
int visible = g_chrome && g_jump_open && g_count > 1;
if (visible)
meow_widget_set_rect(g_slider, 24, MR_TOP_BAR_H + 4, g_vw - 48, MR_SLIDER_H);
else
meow_widget_set_rect(g_slider, 0, -(MR_TOP_BAR_H + MR_SLIDER_H + 8),
g_vw - 48, MR_SLIDER_H);
}
static void slider_build(void)
{
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
if (g_count <= 1) return;
int val = current_page() + 1;
if (val < 1) val = 1;
if (val > g_count) val = g_count;
g_slider = meow_slider_create(0, 0, g_vw - 48, 1, g_count, val);
slider_place();
}
static void slider_sync(void)
{
if (!g_slider || g_count <= 0) return;
int v = current_page() + 1;
if (v < 1) v = 1;
if (v > g_count) v = g_count;
meow_slider_set(g_slider, v);
}
static void set_fit(int fit)
{
if (g_mode == MR_MODE_WEBTOON) {
toast("条漫固定适宽");
return;
}
g_fit = (fit < 0 || fit >= MR_FIT_COUNT) ? MR_FIT_WIDTH : fit;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
window_update();
update_title();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void set_chrome(int on)
{
on = on ? 1 : 0;
if (g_chrome == on) return;
g_chrome = on;
if (g_tb) meow_widget_set_rect(g_tb, 0, on ? 0 : -MR_TOP_BAR_H, g_vw, MR_TOP_BAR_H);
slider_place();
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
}
/* 平移(dx>0 = 内容右移);放大或原始档溢出时可用。 */
static void pan_by(int dx, int dy)
{
if (!pan_mode()) return;
g_pan_x += dx;
g_pan_y += dy;
clamp_pan();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void commit_page(int target)
{
target = mr_nav_clamp_page(target, g_count);
if (target < 0) return;
g_page = target;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
window_update();
update_title();
slider_sync();
cont_touch();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
/* ---- 翻页滑动吸附 ---- */
static void slide_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)ctx;
g_anim_v = (int)value;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void slide_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)ctx;
g_snap = NULL;
if (g_quitting) return;
int delta = g_slide_delta;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
if (!canceled && delta)
commit_page(g_page + delta);
else
meow_ui_invalidate(0, 0, g_vw, g_vh);
meow_log(MEOW_LOG_INFO, "reader flip done page=%d/%d total_ms=%llu",
g_page + 1, g_count,
(unsigned long long)(meow_ui_time_ms() - g_flip_t0));
}
/* 从 start_off(松手时的位移,无跟手时为 0)滑到目标页。1=动画已启动。 */
static int start_slide(int delta, int start_off)
{
if (g_snap || g_bounce) return 0;
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
g_slide_delta = delta;
g_slide_from = mr_nav_slide_from(rtl, delta, g_vw);
g_anim_v = start_off + g_slide_from;
meow_anim_desc d = {
(float)g_anim_v, 0.0f, MR_SLIDE_MS, MEOW_EASE_OUT,
slide_update, slide_done, NULL,
};
uint64_t t0 = meow_ui_time_ms();
g_snap = meow_anim_start(&d); /* start 即回调 update:首帧在这里绘制 */
uint64_t dt = meow_ui_time_ms() - t0;
if (!g_snap) {
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
commit_page(g_page + delta);
return 0;
}
meow_log(MEOW_LOG_INFO, "reader slide first_frame_ms=%llu page=%d->%d",
(unsigned long long)dt, g_page + 1, g_page + delta + 1);
return 1;
}
/* ---- 未过阈值回弹 ---- */
static void bounce_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)ctx;
g_drag_off = (int)value;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void bounce_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)canceled; (void)ctx;
g_bounce = NULL;
g_drag_off = 0;
g_drag_mode = 0;
if (!g_quitting) meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void start_bounce(int from_off)
{
if (g_bounce || g_snap) { g_drag_off = 0; g_drag_mode = 0; return; }
meow_anim_desc d = {
(float)from_off, 0.0f, MR_BOUNCE_MS, MEOW_EASE_OUT,
bounce_update, bounce_done, NULL,
};
g_bounce = meow_anim_start(&d);
if (!g_bounce) { g_drag_off = 0; g_drag_mode = 0; meow_ui_invalidate(0, 0, g_vw, g_vh); }
}
/* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */
static void page_flip(int delta)
{
if (delta == 0 || g_count <= 0) return;
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (target == g_page) { /* 首/末页:册间衔接 */
book_switch(delta > 0 ? 1 : -1);
return;
}
if (g_snap || g_bounce) return; /* 短动画中忽略连点 */
if (g_zoom_pct != 100) { /* 翻页回到基准缩放 */
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
}
results_drain();
g_flip_t0 = meow_ui_time_ms();
if (slot_find(target)) {
if (start_slide(delta, 0))
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit", g_page + 1, target + 1);
else
meow_log(MEOW_LOG_INFO, "reader flip %d->%d hit (no anim slot)", g_page + 1, target + 1);
} else {
meow_log(MEOW_LOG_INFO, "reader flip %d->%d miss (decode pending)", g_page + 1, target + 1);
commit_page(target);
}
}
/* ============================================================ */
/* 跳页(chrome 滑块):目标页预解码完成后落位 */
/* ============================================================ */
static void jump_land(int page, int timed_out)
{
if (g_count <= 0) return;
page = mr_nav_clamp_page(page, g_count);
if (page < 0) page = 0;
if (g_mode == MR_MODE_WEBTOON && g_strip_ok) {
if (g_scroll) meow_scroll_stop(g_scroll);
g_woff = mr_strip_offset_for_page(&g_strip, page);
g_page = page;
} else {
g_page = page;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
}
slider_sync();
window_update();
update_title();
cont_touch();
meow_ui_invalidate(0, 0, g_vw, g_vh);
meow_log(MEOW_LOG_INFO, "reader jump land page=%d/%d%s",
page + 1, g_count, timed_out ? " (timeout)" : "");
if (timed_out) toast("页面解码较慢,已跳到目标页");
else if (page_failed(page)) toast("目标页无法显示");
}
static void jump_commit_from_slider(void)
{
if (!g_slider || g_count <= 0) return;
int page = meow_slider_get(g_slider) - 1;
g_jump_open = 0;
slider_place();
if (page < 0) page = 0;
if (page >= g_count) page = g_count - 1;
if (page == current_page()) { /* 原地:只关面板 */
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
return;
}
if (slot_find(page)) { /* 已缓存:直接落位 */
jump_land(page, 0);
return;
}
mr_jump_begin(&g_jump, page, g_count, meow_ui_time_ms());
window_update(); /* 目标页进解码窗(最高优先级) */
char msg[48];
snprintf(msg, sizeof msg, "跳转到 P%d…", page + 1);
toast(msg);
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void jump_ui_toggle(void)
{
if (g_count <= 1) { toast("只有一页,无需跳转"); return; }
g_jump_open = !g_jump_open;
if (g_jump_open) {
if (!g_slider) slider_build();
slider_sync();
}
slider_place();
meow_ui_invalidate(0, 0, g_vw, MR_TOP_BAR_H + MR_SLIDER_H + 8);
}
static void slider_scrub(int x)
{
if (!g_slider || g_count <= 1) return;
int sx = 24, sw = g_vw - 48;
if (sw < 1) sw = 1;
int v = 1 + (int)((long long)(x - sx) * (g_count - 1) / sw);
if (v < 1) v = 1;
if (v > g_count) v = g_count;
meow_slider_set(g_slider, v);
meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, MR_SLIDER_H + 8);
}
/* ============================================================ */
/* 模式切换(翻页 ⇄ 条漫) */
/* ============================================================ */
static void mode_set(int mode, int from_page)
{
if (!mr_mode_valid(mode) || mode == g_mode) return;
int page = from_page >= 0 ? from_page : current_page();
page = mr_nav_clamp_page(page, g_count);
if (page < 0) page = 0;
if (mode == MR_MODE_WEBTOON) {
if (!strip_ensure()) { toast("条漫排版失败,保持翻页"); return; }
scroll_ensure();
g_page = page;
g_woff = mr_strip_offset_for_page(&g_strip, page);
if (g_scroll) meow_scroll_stop(g_scroll);
} else {
g_page = page;
if (g_scroll) { scroll_free(); }
mr_strip_free(&g_strip);
g_strip_ok = 0;
g_woff = 0;
}
g_mode = mode;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
slider_sync();
window_update();
update_title();
cont_touch();
cont_save_now(); /* 模式切换立即记忆 */
meow_ui_invalidate(0, 0, g_vw, g_vh);
meow_log(MEOW_LOG_INFO, "reader mode=%s page=%d/%d",
mr_mode_name(g_mode), current_page() + 1, g_count);
toast(mode == MR_MODE_WEBTOON ? "条漫模式 · 连续滚动" : "翻页模式");
}
/* ============================================================ */
/* 册间衔接 / 打开册 */
/* ============================================================ */
static void open_book_reset_state(void)
{
worker_stop();
scroll_free();
mr_strip_free(&g_strip);
g_strip_ok = 0;
slots_free_all();
mr_book_destroy(&g_book);
mr_dir_book_free(&g_db);
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
g_gen++; /* 作废旧册的异步结果 */
g_count = 0;
g_open_err = 0;
g_page = 0;
g_woff = 0;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
g_cont_dirty = 0;
g_cont_last_page = -1;
g_cont_last_mode = -1;
g_win_n = 0;
g_fail_paint = 0;
memset(g_page_fail, 0, sizeof g_page_fail);
g_jump_open = 0;
mr_jump_cancel(&g_jump);
g_toast[0] = '\0';
g_title_page = -1;
}
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */
static int open_book(const char *dir, int start_page, int mode)
{
if (!dir || !dir[0]) return 0;
open_book_reset_state();
snprintf(g_dir, sizeof g_dir, "%s", dir);
g_mode = mr_mode_valid(mode) ? mode : MR_MODE_PAGED;
int maxdim = g_vw > g_vh ? g_vw : g_vh;
int n = mr_dir_book_init(&g_db, g_dir, maxdim,
reader_load_page, reader_unload_page, NULL);
if (n < 0) {
mr_dir_book_bind(&g_book, NULL);
g_open_err = 1;
worker_start();
update_title();
meow_log(MEOW_LOG_WARN, "reader open failed: %s", g_dir);
meow_ui_invalidate(0, 0, g_vw, g_vh);
return 0;
}
g_count = n;
mr_dir_book_bind(&g_book, g_count > 0 ? &g_db : NULL);
int page = start_page < 0 ? g_count - 1 : start_page;
page = mr_nav_clamp_page(page, g_count);
if (page < 0) page = 0;
if (g_mode == MR_MODE_WEBTOON) {
if (!strip_ensure()) { /* 排版失败:降级翻页,不白屏 */
meow_log(MEOW_LOG_WARN, "reader strip unavailable, fallback paged");
g_mode = MR_MODE_PAGED;
}
}
if (g_mode == MR_MODE_WEBTOON) {
scroll_ensure();
g_page = page;
g_woff = mr_strip_offset_for_page(&g_strip, page);
} else {
g_page = page;
g_woff = 0;
}
worker_start();
if (!g_worker_on && g_count > 0) {
/* 线程创建失败:退化为主线程同步解起始页(保证不是纯白屏)。 */
meow_img *first = mr_book_open(&g_book, g_page);
if (first) slot_put(g_page, first);
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
}
slider_build();
window_update(); /* 起始页优先进队 */
update_title();
cont_touch();
meow_ui_invalidate(0, 0, g_vw, g_vh);
meow_log(MEOW_LOG_INFO, "reader book=%s pages=%d mode=%s page=%d",
g_dir, g_count, mr_mode_name(g_mode), current_page() + 1);
return 1;
}
static int book_switch(int delta)
{
if (delta == 0) return 0;
if (g_count <= 0) return 0;
char next[MR_PATH_MAX];
int r = mr_book_adjacent(g_dir, delta > 0 ? 1 : -1, next, (int)sizeof next);
if (r != 1) {
toast(delta > 0 ? "已是最后一册" : "已是第一册");
return 0;
}
cont_save_now(); /* 先记忆当前册 */
int mode = mr_cont_get_mode(next, g_mode); /* 相邻册自己的模式;没有则沿用当前 */
if (!open_book(next, delta > 0 ? 0 : -1, mode)) {
toast("无法打开相邻册");
return 0;
}
const char *base = strrchr(g_dir, '/');
char msg[64];
snprintf(msg, sizeof msg, "%s · %s", delta > 0 ? "下一册" : "上一册",
(base && base[1]) ? base + 1 : g_dir);
toast(msg);
return 1;
}
/* ============================================================ */
/* 跟手拖动 */
/* ============================================================ */
static void drag_paint(void)
{
uint64_t now = meow_ui_time_ms();
int gap = g_zoom_pct > 100 ? 33 : MR_PAINT_MS; /* 缩放帧要重采样:30fps 上限 */
if (now - g_last_paint < (uint64_t)gap) return;
g_last_paint = now;
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
/* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */
static void drag_finish(void)
{
if (!g_dragging) return;
g_dragging = 0;
int dx = g_drag_off;
int mode = g_drag_mode;
g_drag_off = 0;
g_drag_mode = 0;
if (mode != 1 || abs(dx) < MR_DRAG_SLOP) {
meow_ui_invalidate(0, 0, g_vw, g_vh);
return;
}
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, dx);
int target = mr_nav_clamp_page(g_page + delta, g_count);
if (mr_nav_commit(dx, g_vw)) {
if (target == g_page) { /* 首/末页越界:进相邻册 */
book_switch(delta > 0 ? 1 : -1);
meow_ui_invalidate(0, 0, g_vw, g_vh);
return;
}
g_flip_t0 = meow_ui_time_ms();
if (slot_find(target) && start_slide(delta, dx)) {
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d hit", g_page + 1, target + 1);
} else {
meow_log(MEOW_LOG_INFO, "reader drag flip %d->%d miss (decode pending)",
g_page + 1, target + 1);
commit_page(target);
}
} else {
g_drag_off = dx; /* start_bounce 从这里起 */
g_drag_mode = 1;
start_bounce(dx);
}
}
/* 条漫松手:到条顶/条底后再拉过阈值 → 进相邻册;否则起 fling 惯性。 */
static void webtoon_release(void)
{
int pull = g_wo_pull;
g_wo_pull = 0;
g_dragging = 0;
g_drag_mode = 0;
if (pull >= MR_BOOK_PULL && book_switch(1)) return;
if (pull <= -MR_BOOK_PULL && book_switch(-1)) return;
if (g_scroll) meow_scroll_fling(g_scroll);
cont_touch();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
void reader_on_touch_down(int x, int y)
{
if (!g_ctrl.active || g_quitting) return;
if (g_count <= 0) return;
g_touch_moved = 0; /* 每次按下都是新手势(chrome 抬手也不残留) */
if (y >= g_vh - 28) return; /* 屏底热区:系统回桌面,勿劫持 */
if (x < 24) return; /* 左缘:系统返回手势,勿劫持 */
if (g_chrome && y < MR_TOP_BAR_H)
return; /* 顶栏点击交给 tap(返回/模式/跳页/fit) */
if (g_chrome && g_jump_open && g_slider && y < MR_TOP_BAR_H + 4 + MR_SLIDER_H) {
g_dragging = 1;
g_drag_mode = 3;
g_touch_moved = 1;
slider_scrub(x);
return;
}
if (g_snap || g_bounce) return; /* 短动画中不接新手势 */
if (g_mode == MR_MODE_WEBTOON) {
g_dragging = 1;
g_drag_mode = 4;
g_wo_y0 = y;
g_wo_last_y = y;
g_wo_pull = 0;
g_tap_pending = 0; /* 条漫没有双击缩放:单击立即生效 */
if (g_scroll) meow_scroll_stop(g_scroll);
return;
}
g_dragging = 1;
g_drag_mode = pan_mode() ? 2 : 1;
g_drag_x0 = x;
g_drag_y0 = y;
g_drag_off = 0;
g_drag_pan_x0 = g_pan_x;
g_drag_pan_y0 = g_pan_y;
}
void reader_on_touch_move(int x, int y)
{
if (!g_ctrl.active || !g_dragging || g_quitting) return;
if (g_drag_mode == 3) { /* 跳页滑块拖动 */
slider_scrub(x);
return;
}
if (g_drag_mode == 4) { /* 条漫:整条滚动 */
int dy = g_wo_last_y - y; /* 手指上移 → 内容上滚 */
g_wo_last_y = y;
if (!g_touch_moved && abs(y - g_wo_y0) >= MR_DRAG_SLOP) g_touch_moved = 1;
if (!g_scroll) return;
int maxo = mr_strip_max_offset(&g_strip);
int before = g_woff;
meow_scroll_track(g_scroll, dy, meow_ui_time_ms());
int after = g_woff;
/* 到边后继续同向拉:累计「越界意图」,松手据此进相邻册。
* 到底后手指继续上移(dy>0)才是「前进」;到顶后继续下移(dy<0)是「后退」。
* 内容不足一屏(maxo==0)时两向都可拉:按手指方向累计。 */
int at_bottom = (before >= maxo && after >= maxo);
int at_top = (before <= 0 && after <= 0);
if (at_bottom && at_top) {
g_wo_pull += dy;
} else if (at_bottom) {
if (dy > 0) g_wo_pull += dy;
} else if (at_top) {
if (dy < 0) g_wo_pull += dy;
} else {
g_wo_pull = 0;
}
drag_paint();
return;
}
int dx = x - g_drag_x0;
int dy = y - g_drag_y0;
int adx = abs(dx), ady = abs(dy);
if (!g_touch_moved && (adx >= MR_DRAG_SLOP || ady >= MR_DRAG_SLOP)) {
g_touch_moved = 1;
g_tap_pending = 0; /* 变成拖动:作废未决单击 */
g_last_tap_ms = 0;
}
if (g_drag_mode == 2) { /* 放大态:单指拖拽平移 */
g_pan_x = g_drag_pan_x0 + dx;
g_pan_y = g_drag_pan_y0 + dy;
clamp_pan();
drag_paint();
return;
}
/* 竖向主导 → 放弃横滑翻页(松手后不算点击,见 reader_on_tap)。 */
if (ady > adx * 3 / 2 && ady > 16) {
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
meow_ui_invalidate(0, 0, g_vw, g_vh);
return;
}
int rtl = mr_reader_get_direction() == MR_DIR_RTL;
int delta = mr_nav_swipe_delta(rtl, dx);
int target = mr_nav_clamp_page(g_page + delta, g_count);
int off = dx;
if (target == g_page) { /* 首/末页:橡皮筋 */
int lim = g_vw / 4;
if (off > lim) off = lim;
if (off < -lim) off = -lim;
}
g_drag_off = off;
drag_paint();
}
/* ============================================================ */
/* 单击 / 双击 */
/* ============================================================ */
static void tap_topbar(int x, int y)
{
if (!g_tb) return;
int hit = meow_topappbar_hit(g_tb, x, y);
if (hit == MEOW_TOPBAR_BACK) {
cont_save_now();
meow_view_page_pop();
return;
}
if (hit < 0) return;
if (hit == g_act_mode) { mode_set(g_mode == MR_MODE_PAGED ? MR_MODE_WEBTOON
: MR_MODE_PAGED, -1); return; }
if (hit == g_act_jump) { jump_ui_toggle(); return; }
if (hit == g_act_fit) { set_fit(mr_nav_fit_next(g_fit)); return; }
}
static void tap_content(int x, int y)
{
(void)y;
if (g_count <= 0) return;
int zone = mr_nav_tap_zone(x, g_vw);
if (zone == MR_ZONE_MIDDLE) {
set_chrome(!g_chrome);
return;
}
if (pan_mode()) {
/* 平移模式:左右分区改为平移步进(配合拖拽,补齐两个方向)。 */
pan_by(zone == MR_ZONE_LEFT ? g_vw / 3 : -g_vw / 3, 0);
return;
}
page_flip(mr_nav_tap_delta(mr_reader_get_direction() == MR_DIR_RTL, zone));
}
static void tap_double(int x, int y)
{
if (g_count <= 0) return;
if (g_mode == MR_MODE_WEBTOON) { /* 条漫不做缩放(保持适宽/内存纪律) */
set_chrome(!g_chrome);
return;
}
if (g_zoom_pct > 100) {
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
} else {
/* 以双击点为锚放大:该点保持在手指下(再夹到可平移范围)。
* 倍率按内存纪律夹取:缩放临时缓冲 = 目标宽×高×4,超过软上限就降档。 */
int bw = 0, bh = 0;
current_geom(&bw, &bh);
int z = MR_ZOOM_PCT;
while (z > 100 && (long long)bw * bh * 4 * z * z / 10000 > MR_ZOOM_TEMP_CAP) z -= 25;
g_zoom_pct = z;
g_pan_x = (g_vw / 2 - x) * (z - 100) / 100;
g_pan_y = (g_vh / 2 - y) * (z - 100) / 100;
clamp_pan();
}
window_update();
meow_ui_invalidate(0, 0, g_vw, g_vh);
}
static void tap_flush(void)
{
if (!g_tap_pending) return;
g_tap_pending = 0;
int x = g_tap_x, y = g_tap_y;
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) tap_topbar(x, y);
else tap_content(x, y);
}
void reader_on_tap(int x, int y)
{
if (!g_ctrl.active || g_quitting) return;
/* 跳页滑块松手:落位到目标页(目标页预解码后由 tick 落位)。 */
if (g_drag_mode == 3) {
g_dragging = 0;
g_drag_mode = 0;
g_touch_moved = 0;
jump_commit_from_slider();
return;
}
if (g_mode == MR_MODE_WEBTOON) {
if (g_dragging) {
if (g_touch_moved) {
g_touch_moved = 0;
webtoon_release();
return;
}
g_dragging = 0;
g_drag_mode = 0;
} else if (g_touch_moved) {
g_touch_moved = 0; /* 竖向拖动被 SDK 报成 TAP:忽略 */
return;
}
g_touch_moved = 0;
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) { tap_topbar(x, y); return; }
if (g_jump_open && g_slider && y < MR_TOP_BAR_H + 4 + MR_SLIDER_H) return;
set_chrome(!g_chrome);
return;
}
/* 跟手手势收尾:移动过就按拖动处理(过阈值翻页 / 否则回弹),不再当点击。 */
if (g_dragging) {
if (g_touch_moved) {
drag_finish();
g_touch_moved = 0;
return;
}
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
} else if (g_touch_moved) {
g_touch_moved = 0; /* 竖向拖动被 SDK 报成 TAP:忽略 */
return;
}
g_touch_moved = 0;
/* chrome 上的点击即时生效,不参与双击判定。 */
if (g_chrome && y >= 0 && y < MR_TOP_BAR_H) {
g_tap_pending = 0;
g_last_tap_ms = 0;
tap_topbar(x, y);
return;
}
uint64_t now = meow_ui_time_ms();
if (g_last_tap_ms && now - g_last_tap_ms <= MR_DOUBLE_MS &&
abs(x - g_last_tap_x) <= 48 && abs(y - g_last_tap_y) <= 48) {
g_tap_pending = 0; /* 取消第一击的单击动作 */
g_last_tap_ms = 0;
tap_double(x, y);
return;
}
tap_flush(); /* 上一击已过期:先执行 */
g_tap_pending = 1;
g_tap_x = x;
g_tap_y = y;
g_tap_ms = now;
g_last_tap_ms = now;
g_last_tap_x = x;
g_last_tap_y = y;
if (!g_ticker) tap_flush(); /* 无节拍器兜底:不延迟单击 */
}
/* ============================================================ */
/* 节拍:收结果 + 跳页落位 + 提示超时 + 续读落盘 */
/* ============================================================ */
static void reader_tick(void)
{
if (!g_ctrl.active || g_quitting) return;
uint64_t now = meow_ui_time_ms();
if (now - g_last_tick < MR_TICK_MS) return;
g_last_tick = now;
int had_current = slot_find(current_page()) != NULL;
if (results_drain() && !had_current && slot_find(current_page()) && !g_dragging)
meow_ui_invalidate(0, 0, g_vw, g_vh);
if (g_fail_paint) {
g_fail_paint = 0;
if (!g_dragging) meow_ui_invalidate(0, 0, g_vw, g_vh);
}
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
if (mr_jump_active(&g_jump)) {
int target = mr_jump_target(&g_jump);
int have = target >= 0 && (slot_find(target) != NULL || page_failed(target));
int timed_out = 0;
if (mr_jump_poll(&g_jump, have, now, MR_JUMP_TIMEOUT_MS, &timed_out))
jump_land(target, timed_out);
}
if (g_toast[0] && now >= g_toast_until) {
g_toast[0] = '\0';
meow_ui_invalidate(0, g_vh - 140, g_vw, 140);
}
/* 条漫滚动停止后同步标题页码(滚动中不每帧更新顶栏文字)。 */
if (g_mode == MR_MODE_WEBTOON && !g_dragging &&
(!g_scroll || !meow_scroll_active(g_scroll))) {
if (current_page() != g_title_page) update_title();
}
cont_save_throttled();
}
static void ticker_update(meow_anim *a, float value, void *ctx)
{
(void)a; (void)value; (void)ctx;
reader_tick();
}
static void ticker_done(meow_anim *a, int canceled, void *ctx)
{
(void)a; (void)ctx;
g_ticker = NULL;
if (canceled || g_quitting || !g_ctrl.active) return;
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
ticker_update, ticker_done, NULL };
g_ticker = meow_anim_start(&d);
}
static void ticker_start(void)
{
if (g_ticker) return;
meow_anim_desc d = { 0.0f, 1.0f, 0xFFFFFFFFu, MEOW_EASE_LINEAR,
ticker_update, ticker_done, NULL };
g_ticker = meow_anim_start(&d);
if (!g_ticker) meow_log(MEOW_LOG_WARN, "reader ticker unavailable (anim pool full)");
}
/* ============================================================ */
/* 输入:滑动手势(view on_event,先于 app.c 的 on_swipe) */
/* ============================================================ */
static int reader_event(meow_view *v, int ev, int a, int b, void *ctx)
{
(void)v; (void)ctx;
if (!g_ctrl.active || g_quitting) return 0;
if (ev != MR_EV_SWIPE) return 0;
if (a == MEOW_SWIPE_LEFT) { /* 左缘返回:交给 app.c page_pop */
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_touch_moved = 0;
g_wo_pull = 0;
cont_save_now(); /* 离开前记忆 */
return 0;
}
if (g_drag_mode == 3) { /* 滑块拖动被 SDK 报成 SWIPE:落位 */
g_dragging = 0;
g_drag_mode = 0;
g_touch_moved = 0;
jump_commit_from_slider();
return 1;
}
if (g_mode == MR_MODE_WEBTOON) {
/* 条漫:竖向滑动 = 滚动收尾(fling/册边界);横向滑动不翻页,统一消费。 */
if (g_dragging) webtoon_release();
return 1;
}
if (a == MEOW_SWIPE_RIGHT) {
if (g_dragging) { drag_finish(); return 1; }
if (g_count <= 0) return 1;
if (pan_mode()) { pan_by(b, 0); return 1; }
page_flip(mr_nav_swipe_delta(mr_reader_get_direction() == MR_DIR_RTL, b));
return 1;
}
if (a == MEOW_SWIPE_UP || a == MEOW_SWIPE_DOWN) {
if (g_dragging) { drag_finish(); return 1; }
if (pan_mode()) { /* 放大 / 原始档溢出:纵向滑动平移 */
pan_by(0, a == MEOW_SWIPE_DOWN ? b : -b);
return 1;
}
return 0; /* 未放大:纵向滑动不归本页 */
}
return 0;
}
/* ============================================================ */
/* view 生命周期 */
/* ============================================================ */
static void reader_state_reset(void)
{
slots_free_all(); /* 静态槽位初值是 idx=0/img=NULL,先清空 */
g_count = 0;
g_open_err = 0;
g_page = 0;
g_mode = MR_MODE_PAGED;
g_fit = MR_FIT_WIDTH;
g_zoom_pct = 100;
g_pan_x = g_pan_y = 0;
g_chrome = 1;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_touch_moved = 0;
g_wo_pull = 0;
g_woff = 0;
g_strip_ok = 0;
g_jump_open = 0;
mr_jump_cancel(&g_jump);
g_toast[0] = '\0';
g_title_page = -1;
g_cont_dirty = 0;
g_cont_last_page = -1;
g_cont_last_mode = -1;
g_win_n = 0;
g_tap_pending = 0;
g_last_tap_ms = 0;
g_quitting = 0;
}
static void reader_page_enter(meow_view *v, void *ctx)
{
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
c->active = 1;
g_quitting = 0;
g_vw = meow_ui_width();
g_vh = meow_ui_height();
v->x = 0;
v->y = 0;
v->w = g_vw;
v->h = g_vh;
meow_ui_set_fullscreen(1);
const meow_theme *th = meow_theme_get();
meow_ui_set_page_bg(th->surface);
reader_state_reset();
g_gen++; /* 作废旧册的异步结果 */
g_last_tick = 0;
g_last_paint = 0;
/* 非根页:控件在 on_enter 建 → owner=ACT_READER(§6.1 跨页泄漏防线)。 */
if (!g_tb) {
g_tb = meow_topappbar_create(0, 0, g_vw, MR_TOP_BAR_H, "");
if (g_tb) {
meow_topappbar_set_back(g_tb, 1);
g_act_mode = meow_topappbar_add_action(g_tb, "模式");
if (g_act_mode >= 0) meow_topappbar_set_item_icon(g_tb, g_act_mode, MEOW_ICON_QUEUE_MUSIC);
g_act_jump = meow_topappbar_add_action(g_tb, "跳页");
if (g_act_jump >= 0) meow_topappbar_set_item_icon(g_tb, g_act_jump, MEOW_ICON_SEARCH);
g_act_fit = meow_topappbar_add_action(g_tb, "适宽");
if (g_act_fit >= 0) meow_topappbar_set_item_icon(g_tb, g_act_fit, MEOW_ICON_REFRESH);
}
}
if (g_tb) meow_widget_set_rect(g_tb, 0, 0, g_vw, MR_TOP_BAR_H);
/* 打开册:恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */
int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED);
int page = mr_cont_get_page(g_dir, 0);
open_book(g_dir, page, mode);
ticker_start();
log_mem("enter");
mr_nav_report();
meow_ui_paint_full();
/* 起始页解码是异步的:先给一帧文字/页码,结果在 tick 里落位。 */
reader_tick();
}
static void reader_page_leave(meow_view *v, void *ctx)
{
(void)v;
((mr_page_ctrl *)ctx)->active = 0;
g_quitting = 1;
cont_save_now(); /* 离开前把最后一页落盘 */
if (g_snap) { meow_anim_cancel(g_snap); g_snap = NULL; }
if (g_bounce) { meow_anim_cancel(g_bounce); g_bounce = NULL; }
if (g_ticker) { meow_anim_cancel(g_ticker); g_ticker = NULL; }
worker_stop(); /* 先停线程,再释放册/图 */
scroll_free();
mr_strip_free(&g_strip);
g_strip_ok = 0;
slots_free_all();
mr_book_destroy(&g_book);
mr_dir_book_free(&g_db);
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
g_act_mode = g_act_jump = g_act_fit = -1;
g_dragging = 0;
g_drag_mode = 0;
g_drag_off = 0;
g_wo_pull = 0;
g_jump_open = 0;
mr_jump_cancel(&g_jump);
g_tap_pending = 0;
g_slide_delta = 0;
g_slide_from = 0;
g_anim_v = 0;
log_mem("leave");
meow_ui_set_fullscreen(0);
}
meow_view *reader_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = reader_page_enter,
.on_leave = reader_page_leave,
.on_draw = reader_draw,
.on_event = reader_event,
};
return meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
}
/* ============================================================ */
/* 书库 → 阅读器导航(app.c / library.c 既有接缝) */
/* ============================================================ */
const char *mr_reader_dir(void)
{
return g_dir;
}
void mr_reader_open_dir(const char *path)
{
if (!path) return;
snprintf(g_dir, sizeof g_dir, "%s", path);
meow_log(MEOW_LOG_INFO, "reader open: %s", g_dir);
meow_view_page_push(ACT_READER);
}
#ifdef MR_READER_HOST_TEST
/* 宿主集成测试观察口(不改变任何状态)。 */
int mr_reader_ui_mode(void) { return g_mode; }
int mr_reader_ui_page(void) { return current_page(); }
int mr_reader_ui_count(void) { return g_count; }
int mr_reader_ui_jump_open(void) { return g_jump_open; }
int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); }
#endif