Files
MeowOS-SDK/tuku/logic/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

185 lines
6.1 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.
/* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)+ 每册续读状态(M7)。
*
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
* 之后走纯内存快路径(不再读配置)。
* M7:阅读模式(翻页/条漫)与每册 last page 同为 meow_config 小键(reader.mode/page.<册标识>,
* 册标识=规范化目录路径的 FNV-1a64);书库行角标经 mr_cont_badge 读取同一份状态。
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
*/
#include "reader.h"
#include "include/meow_ui_config.h"
#include "include/meow_ui_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MR_DIR_CFG_KEY "reader.dir"
#define MR_DIR_CFG_LTR "ltr"
#define MR_DIR_CFG_RTL "rtl"
static int g_direction = MR_DIR_RTL;
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
static void reader_restore_once(void)
{
char buf[8];
if (g_restored) return;
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
/* 其它值:忽略,保持默认 RTL */
}
int mr_reader_get_direction(void)
{
reader_restore_once();
return g_direction;
}
void mr_reader_set_direction(int dir)
{
if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
if (g_direction == dir) return; /* 无变化不写盘 */
g_direction = dir;
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
!= MEOW_CFG_OK)
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
}
#ifdef MR_READER_TEST_HOOKS
void mr_reader_test_reset(void)
{
g_direction = MR_DIR_RTL;
g_restored = 0;
}
#endif
/* ============================================================ */
/* 阅读模式 + 每册续读状态(M7) */
/* ============================================================ */
int mr_mode_valid(int mode)
{
return mode == MR_MODE_PAGED || mode == MR_MODE_WEBTOON;
}
const char *mr_mode_name(int mode)
{
return mode == MR_MODE_WEBTOON ? "webtoon" : "paged";
}
int mr_mode_parse(const char *s)
{
if (!s) return -1;
if (strcmp(s, "paged") == 0) return MR_MODE_PAGED;
if (strcmp(s, "webtoon") == 0) return MR_MODE_WEBTOON;
return -1;
}
/* 目录规范化:去掉末尾 '/'(根 "/" 保留),保证同一册跨调用/跨启动得到同一键。 */
static void cont_norm_dir(const char *dir, char *out, int outsz)
{
if (!out || outsz <= 0) return;
out[0] = '\0';
if (!dir || !dir[0]) return;
int n = (int)strlen(dir);
while (n > 1 && dir[n - 1] == '/') n--;
if (n >= outsz) n = outsz - 1;
memcpy(out, dir, (size_t)n);
out[n] = '\0';
}
static unsigned long long cont_fnv1a64(const char *s)
{
unsigned long long h = 1469598103934665603ULL;
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h ^= (unsigned long long)*p;
h *= 1099511628211ULL;
}
return h;
}
int mr_cont_key(const char *dir, const char *field, char *out, int outsz)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || !field || !field[0]) return -1;
char norm[MR_CONT_PATH_MAX];
cont_norm_dir(dir, norm, (int)sizeof norm);
if (!norm[0]) return -1;
int n = snprintf(out, (size_t)outsz, "reader.%s.%016llx", field, cont_fnv1a64(norm));
if (n <= 0 || n >= outsz) { out[0] = '\0'; return -1; }
return 0;
}
int mr_cont_get_page(const char *dir, int def)
{
char key[64];
char buf[24];
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return def;
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
char *end = NULL;
long v = strtol(buf, &end, 10);
if (end == buf || (end && *end != '\0')) return def;
if (v < 0 || v > MR_CONT_PAGE_MAX) return def;
return (int)v;
}
void mr_cont_set_page(const char *dir, int page)
{
char key[64];
char val[24];
if (page < 0) page = 0;
if (page > MR_CONT_PAGE_MAX) page = MR_CONT_PAGE_MAX;
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return;
snprintf(val, sizeof val, "%d", page);
if (meow_config_set(key, val) != MEOW_CFG_OK)
meow_log(MEOW_LOG_WARN, "reader: save page failed (best-effort)");
}
int mr_cont_get_mode(const char *dir, int def)
{
char key[64];
char buf[16];
if (!mr_mode_valid(def)) def = MR_MODE_PAGED;
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return def;
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
int m = mr_mode_parse(buf);
return m >= 0 ? m : def;
}
void mr_cont_set_mode(const char *dir, int mode)
{
char key[64];
if (!mr_mode_valid(mode)) return;
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return;
if (meow_config_set(key, mr_mode_name(mode)) != MEOW_CFG_OK)
meow_log(MEOW_LOG_WARN, "reader: save mode failed (best-effort)");
}
void mr_cont_save(const char *dir, int page, int mode)
{
if (!dir || !dir[0]) return;
mr_cont_set_page(dir, page);
mr_cont_set_mode(dir, mode);
}
int mr_cont_badge(const char *dir, char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
int page = mr_cont_get_page(dir, -1);
if (page <= 0) return 0;
snprintf(out, (size_t)outsz, "续读 P%d", page + 1);
return 1;
}