/* 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 预判,超过预算跳过并告警)。 * * 性能(M17):解码目标 = **当前 fit/zoom 的显示尺寸**(不是屏长边),所以翻页/滚动热路径 * 只有原生 blit:不做 meow_ui_img_scaled 的每帧重采样,也不再每帧 malloc 1.2MB 临时缓冲。 * fit 变化时窗口按新几何重解一次;拖动/吸附帧只 invalidate 内容区(顶栏静态不重绘)。 * 内存:单页显示缓存 ≤3MB、整窗 ≤9MB(超限淘汰最远页),解码中间峰值 >12MB 的页直接跳过 * 并显示占位——页数增长不会线性吃内存(常驻只与解码窗大小有关)。 * 方向:一律读 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/page_cache.h" #include "logic/reader/nav.h" #include #include #include #include #include #include #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 (8L * 1024 * 1024) /* 非缩解格式原始 RGBA 超此值提示残余风险 */ #define MR_DECODE_PEAK_CAP (12L * 1024 * 1024) /* 单次解码中间缓冲峰值上限(超则跳过,防 OOM 闪退) */ #define MR_PAGE_BYTES_CAP (3L * 1024 * 1024) /* 单页「显示尺寸」缓存字节上限 */ #define MR_RESIDENT_CAP (9L * 1024 * 1024) /* 页图缓存总字节上限(超则淘汰最远页) */ #define MR_SLOT_TOL 2 /* 缓存图与显示框允许的像素偏差(≤2 走原生 blit,不重采样) */ #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 int g_start_at = MR_READER_START_RESUME; /* 下次 on_enter 的显式起始页(-1=续读) */ static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */ static mr_book g_book; static int g_count; /* 0=空册/不可读 */ static int g_open_err; static uint64_t g_open_retry_at; /* 资源类打开失败(ENOMEM/EMFILE…)的自动重试时刻 */ static int g_open_retry_used; /* 1=本次进入已用过自动重试(避免反复重试) */ 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]; /* 每页原始像素尺寸缓存(打开时按 count 分配;0,0=未知)。绘制/排版几何只查这里, * 不在每帧去 probe 文件(dir_ops_meta 是 fopen+读头,每帧一次会直接拖垮翻页)。 */ typedef struct page_meta { int w, h; } page_meta; static page_meta *g_meta; /* 解码预算(屏长边):超过它的显示尺寸无法靠「再解一次」变大,用于避免重解死循环。 */ static int g_decode_budget = 720; 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 往末页方向) */ /* ---- 绘页路径计数(宿主测试观察:翻页热路径应全部是 blit)---- */ #ifdef MR_READER_HOST_TEST static int g_blit_draws; static int g_scaled_draws; #define NOTE_BLIT() (g_blit_draws++) #define NOTE_SCALED() (g_scaled_draws++) #else #define NOTE_BLIT() ((void)0) #define NOTE_SCALED() ((void)0) #endif /* ---- 单击延迟(双击判定窗口)---- */ 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 invalidate_content(void); static page_slot *slot_find(int idx); 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; } /* 目标解码长边:优先=当前 fit/zoom 的显示尺寸(解码后可直接 blit,翻页零重采样), * 再受屏长边预算与单页字节上限约束。pw/ph=原始尺寸。 */ static int decode_long_side(int pw, int ph, int budget) { int tw = 0, th = 0; int long0 = pw > ph ? pw : ph; int short0 = pw > ph ? ph : pw; if (mr_nav_fit_size(g_fit, pw, ph, g_vw, g_vh, &tw, &th) != 0) return budget; if (g_zoom_pct != 100) mr_nav_zoom_size(tw, th, g_zoom_pct, &tw, &th); int l = tw > th ? tw : th; if (l < 64) l = 64; if (budget > 0 && l > budget) l = budget; /* 单页缓存上限:长边按比例收到能装下为止(高度极大的长条漫走这里)。 */ while (l > 64) { int s = (int)((long long)short0 * l / long0); if ((long long)l * s * 4 <= MR_PAGE_BYTES_CAP) break; l = l * 3 / 4; } return l; } static meow_img *reader_load_page(const char *path, void *ud, int maxdim) { (void)ud; if (!path || !path[0]) return NULL; /* 预算预判(M12):先读文件头拿原始尺寸,不解码像素。 * - 解码中间缓冲峰值超上限 → 跳过(页面显示占位),避免真机被 OOM 杀掉(闪退); * - 非缩解格式(PNG/BMP)原始 RGBA 超过告警线 → 记 WARN(残余风险)。 */ int pw = 0, ph = 0, ch = 0; int have = meow_img_probe_file(path, &pw, &ph, &ch) && pw > 0 && ph > 0; int dim = maxdim; if (have) { int l0 = pw > ph ? pw : ph; dim = decode_long_side(pw, ph, maxdim); long long peak; if (path_is_jpeg(path)) { /* 估 stbi 缩解后的输出(与 SDK 的档位选择同式):仍超上限才跳过。 */ int s = 1; while (s < 8 && (l0 / (s * 2)) >= dim) s *= 2; peak = (long long)(pw / s) * (ph / s) * 4; } else { peak = (long long)pw * ph * 4; /* 整幅解码后降采样 */ if (peak > MR_LOAD_RAW_WARN) meow_log(MEOW_LOG_WARN, "reader decode large non-scalable %dx%d (%lldKB): %s", pw, ph, peak / 1024, path); } if (peak > MR_DECODE_PEAK_CAP) { meow_log(MEOW_LOG_WARN, "reader decode skip %dx%d (peak %lldKB > cap): %s", pw, ph, peak / 1024, path); return NULL; } } meow_img *im = meow_img_load_file_dim(path, dim); if (!im) return NULL; /* 兜底:M12 之前的格式/未知格式若没被缩到预算内,再降一次(保持驻留纪律)。 */ if (dim > 0 && (meow_img_w(im) > dim || meow_img_h(im) > dim)) { meow_img *small = meow_img_downscale(im, dim); 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); } /* ============================================================ */ /* 页原始尺寸缓存(绘制几何的唯一来源;每页只 probe 一次) */ /* ============================================================ */ static int page_meta_dims(int idx, int *w, int *h) { if (w) *w = 0; if (h) *h = 0; if (idx < 0 || idx >= g_count) return 0; if (g_meta) { if (g_meta[idx].w > 0 && g_meta[idx].h > 0) { if (w) *w = g_meta[idx].w; if (h) *h = g_meta[idx].h; return 1; } if (g_meta[idx].w < 0) return 0; /* 探测过且未知:别再每帧 fopen 一遍 */ } int sw = 0, sh = 0; if (!mr_book_meta(&g_book, idx, &sw, &sh) || sw <= 0 || sh <= 0) { if (g_meta) { g_meta[idx].w = -1; g_meta[idx].h = 0; } return 0; } if (g_meta) { g_meta[idx].w = sw; g_meta[idx].h = sh; } if (w) *w = sw; if (h) *h = sh; return 1; } /* 显示框(fit + zoom):绘图与点击几何都走这里。未知名按 2:3 竖图占位。 */ static void page_target_size_at(int idx, int *w, int *h) { int sw = 0, sh = 0; if (w) *w = 0; if (h) *h = 0; if (!w || !h) return; if (!page_meta_dims(idx, &sw, &sh)) { /* meta 不可用(未知/坏头):用已解码尺寸兜底(保持真实长宽比), * 都没解出来才按 2:3 竖图占位——排版稳定且不会把页面拉变形。 */ page_slot *s = slot_find(idx); if (s) { sw = s->src_w; sh = s->src_h; } if (sw <= 0 || sh <= 0) { sw = g_vw > 0 ? g_vw : 1; sh = sw * 3 / 2; } } if (mr_nav_fit_size(g_fit, sw, sh, g_vw, g_vh, w, h) != 0) return; if (g_zoom_pct != 100) mr_nav_zoom_size(*w, *h, g_zoom_pct, w, h); } /* ============================================================ */ /* 续读状态(逐册 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 int page_visible(int idx) { if (g_count <= 0 || idx < 0 || idx >= g_count) return 0; if (g_mode == MR_MODE_WEBTOON && g_strip_ok) { int f = mr_strip_page_at(&g_strip, g_woff); int l = mr_strip_page_at(&g_strip, g_woff + g_vh - 1); return idx >= f && idx <= l; } return idx == g_page || (g_slide_delta != 0 && idx == g_page + g_slide_delta); } 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); } /* 缓存图是否就是当前 fit/zoom 的显示尺寸(可直接 blit)。已达解码预算的图也算就绪, * 否则「原始/放大」档会陷入「解不出目标尺寸 → 反复重解」。 */ static int slot_ready(int idx) { page_slot *s = slot_find(idx); if (!s) return 0; int tw = 0, th = 0; page_target_size_at(idx, &tw, &th); if (tw <= 0 || th <= 0) return 1; if (abs(tw - s->src_w) <= MR_SLOT_TOL && abs(th - s->src_h) <= MR_SLOT_TOL) return 1; if (g_decode_budget > 0 && (s->src_w >= g_decode_budget || s->src_h >= g_decode_budget)) return 1; /* 已是原始分辨率(小图不放大,SDK 不做上采样):这就是能拿到的最好结果。 */ int mw = 0, mh = 0; if (page_meta_dims(idx, &mw, &mh) && s->src_w >= mw && s->src_h >= mh) return 1; return 0; } /* 淘汰离焦点页最远的一张(不淘汰 focus 与 except)。返回被清掉的槽下标,无则 -1。 */ static int slot_evict_far(int except) { int focus = focus_page(); int far = -1, far_d = -1; for (int i = 0; i < MR_SLOTS; i++) { if (i == except || !g_slot[i].img) continue; if (g_slot[i].idx == focus) continue; int d = abs(g_slot[i].idx - focus); if (d > far_d) { far_d = d; far = i; } } if (far >= 0) slot_clear(far); return far; } /* 放进缓存;同页旧图直接替换(fit/zoom 变了要换尺寸)。返回 1=已放入。 */ static int slot_put(int idx, meow_img *img) { if (!img || idx < 0) return 0; int i = -1, empty = -1; for (int k = 0; k < MR_SLOTS; k++) { if (g_slot[k].idx == idx && g_slot[k].img) { i = k; break; } if (!g_slot[k].img && empty < 0) empty = k; } if (i < 0) { if (empty >= 0) { i = empty; } else { /* 全满:替换离焦点页最远的一张 */ int focus = focus_page(), far_d = -1; for (int k = 0; k < MR_SLOTS; k++) { int d = abs(g_slot[k].idx - focus); if (d > far_d) { far_d = d; i = k; } } } } if (i < 0) { meow_img_free(img); return 0; } /* 总字节上限:新图放进去前先淘汰最远页,保证常驻图不超过预算。 */ long need = (long)meow_img_w(img) * meow_img_h(img) * 4; for (;;) { long held = resident_bytes(); if (g_slot[i].img) held -= (long)g_slot[i].src_w * g_slot[i].src_h * 4; if (held + need <= MR_RESIDENT_CAP) break; if (slot_evict_far(i) < 0) break; /* 只剩目标槽:这一页必须放进来 */ } 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_ready(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 (page_visible(got[i].idx)) g_fail_paint = 1; continue; } if (in_window(got[i].idx)) { if (slot_put(got[i].idx, got[i].img) && page_visible(got[i].idx)) 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) { (void)ctx; return page_meta_dims(index, w, h); /* 走缓存:重排版不再逐页 fopen probe */ } 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; invalidate_content(); /* 滚动只动内容区,顶栏静态不重绘 */ } 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; } /* ============================================================ */ /* 布局/绘制 */ /* ============================================================ */ /* 内容视口失效区:chrome 可见时顶栏是静态的,拖动/吸附帧只刷其下内容, * 避免每帧重绘顶栏文字(软渲下文字是最贵的绘制物)。 */ static void invalidate_content(void) { if (g_chrome) meow_ui_invalidate(0, MR_TOP_BAR_H, g_vw, g_vh - MR_TOP_BAR_H); else meow_ui_invalidate(0, 0, g_vw, g_vh); } static void current_geom(int *w, int *h) { page_target_size_at(g_page, w, h); if (!w || !h || *w <= 0 || *h <= 0) { if (w) *w = g_vw; if (h) *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(翻页热路径零重采样), * 只在 fit/zoom 变化或极端长页(超单页缓存上限)时才退回最近邻缩放。 */ 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_at(idx, &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 (abs(w - s->src_w) <= MR_SLOT_TOL && abs(h - s->src_h) <= MR_SLOT_TOL) { meow_ui_img(x + (w - s->src_w) / 2, y + (h - s->src_h) / 2, s->img); NOTE_BLIT(); } else { meow_ui_img_scaled(x, y, w, h, s->img); NOTE_SCALED(); } } /* 条漫:按滚动偏移画可见页。典型 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) { /* 解码=显示尺寸时原生 blit:gfx 只处理屏幕上可见的行, * 长条页只露一截时不再整页重采样(M7 finding 的子矩形 blit 诉求)。 */ if (abs(s->src_w - g_vw) <= MR_SLOT_TOL && abs(s->src_h - h) <= MR_SLOT_TOL) { meow_ui_img((g_vw - s->src_w) / 2, y, s->img); NOTE_BLIT(); } else { meow_ui_img_scaled(0, y, g_vw, h, s->img); NOTE_SCALED(); } 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(); invalidate_content(); } 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; invalidate_content(); /* 吸附帧只刷内容区(整屏 minus 静态顶栏) */ } 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 invalidate_content(); 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; invalidate_content(); } 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) invalidate_content(); } 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; invalidate_content(); } } /* 点击触发的翻页(无跟手位移)。首/末页再前进 → 相邻册(无相邻册则轻提示)。 */ 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_close_auto(&g_book, &g_handle); if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; } g_gen++; /* 作废旧册的异步结果 */ g_count = 0; g_open_err = 0; g_open_retry_at = 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; free(g_meta); g_meta = NULL; } /* 打开一本册并恢复模式/起始页。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(); /* reader_page_enter 传入的 dir 就是 g_dir:自拷贝是 snprintf 的未定义行为, * 其它调用(册间衔接)才需要复制。 */ if (dir != g_dir) 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; g_decode_budget = maxdim; /* 目录 → 目录册;.cbz/.zip → 档案册(M11)。两者同一个 mr_book 契约, * 下面的翻页/条漫消费代码完全不区分 provider。 */ int n = mr_book_open_auto(&g_book, &g_handle, g_dir, maxdim, reader_load_page, reader_unload_page, NULL); if (n < 0) { int err = errno; /* opendir/malloc 失败原因(设备排障用) */ g_open_err = 1; worker_start(); update_title(); meow_log(MEOW_LOG_WARN, "reader open failed: %s (errno=%d %s)", g_dir, err, strerror(err)); /* 资源类失败(内存/FD 瞬时紧张)自动重试一次:library 页已在 on_leave 停止 * 后台解码,稍后内存回收后重开即可成功;权限/路径错误不重试。 */ if ((err == ENOMEM || err == EMFILE || err == ENFILE || err == EAGAIN || err == EINTR) && !g_open_retry_used) { g_open_retry_used = 1; g_open_retry_at = meow_ui_time_ms() + 200; } meow_ui_invalidate(0, 0, g_vw, g_vh); return 0; } g_count = n; /* 页尺寸缓存:每页 8 字节(4096 页上限 ≈32KB),换来的是一帧内零 fopen probe。 */ g_meta = (page_meta *)calloc((size_t)g_count, sizeof *g_meta); if (!g_meta) meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count); 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; } /* 册间衔接的档案册探测(M11):兄弟表 = 含图目录 ∪ 含图片页的 .cbz/.zip。 */ static int cbz_pages_probe(const char *path, void *ud) { (void)ud; return mr_zip_count_pages(path); } 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_ex(g_dir, delta > 0 ? 1 : -1, next, (int)sizeof next, cbz_pages_probe, NULL); 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; invalidate_content(); } /* 松手:过阈值且目标页已缓存 → 从当前位移吸附过去;否则回弹。 */ 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) { invalidate_content(); 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); invalidate_content(); 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(); invalidate_content(); } 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; invalidate_content(); 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; /* 只有当落位的页真的在屏幕上(或换了几何的当前页)才重绘,预取邻居不刷屏。 */ if (results_drain() && !g_dragging) invalidate_content(); /* 打开册的瞬时资源失败:稍后自动重试一次(进入本页时已停 library 后台解码)。 */ if (g_open_err && g_open_retry_at && now >= g_open_retry_at) { g_open_retry_at = 0; 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); return; } if (g_fail_paint) { g_fail_paint = 0; if (!g_dragging) invalidate_content(); } 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_open_retry_at = 0; g_open_retry_used = 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); /* 档案册解压缓存(M11):MeowData//cache。library 页通常已初始化, * 这里幂等兜底(进程重启后直接进阅读器、或交付环境只剩 reader 入口)。 */ { const char *data = meow_config_dir(); if (data && data[0]) mr_cache_init(data); } /* 打开册:显式起始页(书库文件管理器模式点第 N 张)优先于续读记录; * 否则恢复该册上次的模式与页码(无记录 → 翻页模式第 1 页)。 */ int mode = mr_cont_get_mode(g_dir, MR_MODE_PAGED); int page = mr_cont_get_page(g_dir, 0); if (g_start_at != MR_READER_START_RESUME) { page = g_start_at; g_start_at = MR_READER_START_RESUME; } 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(); free(g_meta); g_meta = NULL; g_start_at = MR_READER_START_RESUME; mr_book_close_auto(&g_book, &g_handle); 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_at(const char *path, int start_index) { if (!path) return; snprintf(g_dir, sizeof g_dir, "%s", path); /* -1=续读(旧入口语义);其它负数钳到 0;上界在 open_book 里按页数钳。 */ g_start_at = start_index == MR_READER_START_RESUME ? MR_READER_START_RESUME : (start_index < 0 ? 0 : start_index); meow_log(MEOW_LOG_INFO, "reader open: %s (start=%d)", g_dir, g_start_at); meow_view_page_push(ACT_READER); } void mr_reader_open_dir(const char *path) { mr_reader_open_dir_at(path, MR_READER_START_RESUME); } #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_open_err(void) { return g_open_err; } int mr_reader_ui_jump_open(void) { return g_jump_open; } int mr_reader_ui_jump_pending(void) { return mr_jump_active(&g_jump); } long mr_reader_ui_resident_kb(void) { return resident_bytes() / 1024; } int mr_reader_ui_failed_pages(void) { int n = 0; for (int i = 0; i < g_count && i < MR_BOOK_MAX_PAGES; i++) if (page_failed(i)) n++; return n; } int mr_reader_ui_blit_draws(void) { return g_blit_draws; } int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; } #endif