- 页源契约(logic/reader):count/meta/open/close + 目录册(文件夹即书)为第一个实现; meta 由纯 C 图片头探测(PNG IHDR/JPEG SOF/BMP)提供原始尺寸,条漫可先排版再取图, zip 册(M9/M11)只需另写 mr_book_ops,两种消费方式零改动复用 - 翻页引擎:pthread 后台解码窗口=当前±1 页(目录册 downscale 到屏幕长边),翻页只 blit 缓存页图 + meow_anim 150ms 滑动吸附;未命中立即切页显示加载态、不在翻页路径解码; 只 flush 内容区;decode/flip 首帧/翻页完成/进出内存全部 meow_log 打点 - 阅读页 UI:fit 三档(适宽/适高/原始,TopAppBar 动作循环,标题显示页码 x/y)+ 双击以点击点为锚放大(临时缓冲超上限自动降倍率)+ 单指拖拽/滑动步进平移; tap 三分区方向感知(RTL 默认:左=下一页,LTR 反之),中区切 chrome; 左缘返回交给 app.c、屏底热区交给 SDK 回桌面,两者都不拦截 - 跟手:app.c 仅补 2 处极小路由(ACT_READER 的 down/move),reader 自行算位移; 屏底 28px/左缘 24px 不进拖动,竖向主导放弃横滑 - 内存纪律:驻留 ≤3 张 downscale 页图;放大时窗口收缩到当前页;on_leave 停解码线程、 释放全部页图/册索引/TopAppBar,并记录 VmRSS/MemAvailable 前后对比 - 测试/构建:新增 test_reader_core(契约边界/空册/越界/重复 open-close/首末页/头探测/ 方向语义/fit/平移夹取,108 项);run_tests.sh 与 build_tuku.sh 源列表接线; 宿主 46+108 全 PASS,交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
1067 lines
35 KiB
C
1067 lines
35 KiB
C
/* ui/reader/reader.c — 阅读器页(ACT_READER,全屏):左右翻页模式。
|
||
*
|
||
* 用户口径:优先流畅度,其次功能。翻页路径不解码、不重排:
|
||
* - 后台 pthread 解码窗口(当前 ±1 页),主循环只取已备好的图;
|
||
* - 跟手拖动只 blit 两张已缓存页图,松手后 meow_anim 一次短滑动吸附(150ms);
|
||
* - 只 flush 受影响区(本页=整屏内容区);翻页/解码/驻留耗时都 meow_log 打点。
|
||
*
|
||
* 页源:logic/reader/page_source 的 mr_book 契约(count/meta/open/close)。目录册
|
||
* (文件夹即书)是第一个实现;zip 册与条漫只需另写 provider / 消费方式,本页零改动。
|
||
* 方向:一律读 M3 的 mr_reader_get_direction()(默认 RTL),不另立全局。
|
||
*
|
||
* 手势(与 SDK 语义对齐,见 sdk/meow_ui_input.c touch_release):
|
||
* - 左缘(x<24)向右划 = 系统返回(MEOW_SWIPE_LEFT)→ 不消费,app.c page_pop 退出;
|
||
* - 屏底 28px 上滑 = 系统回桌面(MEOW_EV_HOME),SDK 不分发给 App,本页不起手;
|
||
* - 中部横拖 = 跟手翻页(本页用 app.c 路由来的原始 down/move 自己算位移);
|
||
* - tap 分区方向感知:屏左/右 1/3 = 上/下一页,中 1/3 = chrome 显隐;双击 = 放大。
|
||
* - 物理键不映射(app.c 只记日志)。
|
||
*
|
||
* 节拍:app.c 的 on_tick 只转给 library_on_tick(不在本页 scope)。本页用一条常驻
|
||
* meow_anim “ticker”(SDK 主循环每轮 meow_anim_tick)驱动结果收取与双击判定超时,
|
||
* 不新增常驻轮询线程,也不改 on_tick 路由。翻页动画本身也走同一节拍。
|
||
*
|
||
* 生命周期:on_enter 建状态 + 建 TopAppBar + 预取首页;on_leave 全量释放(停解码线程、
|
||
* 释放全部页图/册索引/控件)。控件在 on_enter 建 → owner=ACT_READER(非根页防泄漏)。
|
||
*/
|
||
#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>
|
||
|
||
#define MR_WIN 3 /* 预解码窗口:当前 ±1 页 */
|
||
#define MR_TOPBAR_H 56
|
||
#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_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_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;
|
||
|
||
/* ---- 页图缓存(≤3 张 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_WIN];
|
||
|
||
/* ---- 后台解码队列 ---- */
|
||
typedef struct page_req { int idx; unsigned gen; } page_req;
|
||
typedef struct page_res { int idx; unsigned gen; meow_img *img; } 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=放大平移 */
|
||
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_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;
|
||
|
||
/* ============================================================ */
|
||
/* 页源解码回调(注入 logic 层的 mr_book 契约;可在后台线程调用) */
|
||
/* ============================================================ */
|
||
static meow_img *reader_load_page(const char *path, void *ud, int maxdim)
|
||
{
|
||
(void)ud;
|
||
meow_img *im = meow_img_load_file(path);
|
||
if (!im) return NULL;
|
||
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_WIN; i++)
|
||
if (g_slot[i].img) n++;
|
||
return n;
|
||
}
|
||
|
||
static long resident_bytes(void)
|
||
{
|
||
long b = 0;
|
||
for (int i = 0; i < MR_WIN; 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);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 页图缓存 */
|
||
/* ============================================================ */
|
||
static page_slot *slot_find(int idx)
|
||
{
|
||
if (idx < 0) return NULL;
|
||
for (int i = 0; i < MR_WIN; 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_WIN) 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_WIN; i++) slot_clear(i);
|
||
}
|
||
|
||
/* 放进缓存;缓存满时淘汰离当前页最远的一张。返回 1=已放入。 */
|
||
static int slot_put(int idx, meow_img *img)
|
||
{
|
||
if (!img || idx < 0) return 0;
|
||
int empty = -1, far = -1, far_d = -1;
|
||
for (int i = 0; i < MR_WIN; 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 - g_page);
|
||
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;
|
||
}
|
||
|
||
/* 当前窗口:正常=当前±1;放大时只驻留当前页(给缩放临时缓冲让内存)。 */
|
||
static int in_window(int idx)
|
||
{
|
||
if (g_count <= 0 || idx < 0 || idx >= g_count) return 0;
|
||
if (g_zoom_pct > 100) return idx == g_page;
|
||
return idx >= g_page - 1 && idx <= 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)) 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 (img && !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_n++;
|
||
img = 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 && 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)
|
||
{
|
||
for (int i = 0; i < MR_WIN; i++)
|
||
if (g_slot[i].idx >= 0 && !in_window(g_slot[i].idx)) slot_clear(i);
|
||
queue_prune();
|
||
|
||
queue_page(g_page);
|
||
if (g_page - 1 >= 0) queue_page(g_page - 1);
|
||
if (g_page + 1 < g_count) queue_page(g_page + 1);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局/绘制 */
|
||
/* ============================================================ */
|
||
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);
|
||
}
|
||
|
||
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_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);
|
||
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);
|
||
return;
|
||
}
|
||
|
||
draw_page_at(g_page, 0, 0);
|
||
if (!slot_find(g_page)) {
|
||
meow_ui_text_center(w / 2, h / 2, "加载中…", th->font_body, th->onSurfaceVariant);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 状态动作 */
|
||
/* ============================================================ */
|
||
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) 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);
|
||
}
|
||
|
||
static void set_fit(int fit)
|
||
{
|
||
g_fit = (fit < 0 || fit >= MR_FIT_COUNT) ? MR_FIT_WIDTH : fit;
|
||
g_zoom_pct = 100;
|
||
g_pan_x = g_pan_y = 0;
|
||
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_TOPBAR_H, g_vw, MR_TOPBAR_H);
|
||
meow_ui_invalidate(0, 0, g_vw, MR_TOPBAR_H);
|
||
}
|
||
|
||
/* 平移(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();
|
||
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) 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);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 跟手拖动 */
|
||
/* ============================================================ */
|
||
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 (target != g_page && mr_nav_commit(dx, g_vw)) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
void reader_on_touch_down(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || g_quitting) return;
|
||
if (g_count <= 0) return;
|
||
if (y >= g_vh - 28) return; /* 屏底热区:系统回桌面,勿劫持 */
|
||
if (x < 24) return; /* 左缘:系统返回手势,勿劫持 */
|
||
if (g_chrome && y < MR_TOPBAR_H) return; /* chrome 上的点击交给 tap */
|
||
if (g_snap || g_bounce) 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;
|
||
g_touch_moved = 0;
|
||
}
|
||
|
||
void reader_on_touch_move(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || !g_dragging || g_quitting) 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) {
|
||
meow_view_page_pop();
|
||
return;
|
||
}
|
||
if (hit >= 0) set_fit(mr_nav_fit_next(g_fit));
|
||
}
|
||
|
||
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_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_TOPBAR_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;
|
||
|
||
/* 跟手手势收尾:移动过就按拖动处理(过阈值翻页 / 否则回弹),不再当点击。 */
|
||
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_TOPBAR_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(g_page) != NULL;
|
||
if (results_drain() && !had_current && slot_find(g_page) && !g_dragging)
|
||
meow_ui_invalidate(0, 0, g_vw, g_vh);
|
||
|
||
if (g_tap_pending && now - g_tap_ms >= MR_DOUBLE_MS) tap_flush();
|
||
}
|
||
|
||
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;
|
||
return 0;
|
||
}
|
||
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_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_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_TOPBAR_H, "");
|
||
if (g_tb) {
|
||
meow_topappbar_set_back(g_tb, 1);
|
||
int ai = meow_topappbar_add_action(g_tb, "");
|
||
if (ai >= 0) meow_topappbar_set_item_icon(g_tb, ai, MEOW_ICON_REFRESH);
|
||
meow_widget_set_rect(g_tb, 0, 0, g_vw, MR_TOPBAR_H);
|
||
}
|
||
|
||
/* 页源:目录册(文件夹即书)。maxdim 取屏幕长边,页图驻留 ≈ 屏宽×屏高×4。 */
|
||
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) {
|
||
g_open_err = 1;
|
||
meow_log(MEOW_LOG_WARN, "reader open failed: %s", g_dir);
|
||
} else {
|
||
g_count = n;
|
||
}
|
||
mr_dir_book_bind(&g_book, g_count > 0 ? &g_db : NULL);
|
||
|
||
worker_start();
|
||
if (!g_worker_on && g_count > 0) {
|
||
/* 线程创建失败:退化为主线程同步解第一页(保证不是纯白屏)。 */
|
||
meow_img *first = mr_book_open(&g_book, 0);
|
||
if (first) slot_put(0, first);
|
||
meow_log(MEOW_LOG_WARN, "reader worker unavailable, sync first page");
|
||
}
|
||
window_update(); /* 当前页优先进队 */
|
||
ticker_start();
|
||
update_title();
|
||
|
||
meow_log(MEOW_LOG_INFO, "reader enter dir=%s pages=%d fit=%d", g_dir, g_count, g_fit);
|
||
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;
|
||
|
||
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(); /* 先停线程,再释放册/图 */
|
||
slots_free_all();
|
||
mr_book_destroy(&g_book);
|
||
mr_dir_book_free(&g_db);
|
||
|
||
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
|
||
g_dragging = 0;
|
||
g_drag_mode = 0;
|
||
g_drag_off = 0;
|
||
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);
|
||
}
|