图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
P1-1(会静默开错图):取消「书库从列表推导图片下标」。 - 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的 全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确, 再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。 - 删除 mr_library_image_index(不再有跨枚举的隐式契约)。 - 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。 - 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/ library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411); 评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**, 2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。 连带处理(评审 r1): 1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth); 顺手删掉 library_page_create 里同款死写。 2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted, book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册; 根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、 rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。 3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子 mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。 4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。 回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例) 与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
This commit is contained in:
1 parent
64336f95d9
commit
4befbfe14c
17 files changed
+523
-112
No files matched your search
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* 浏览语义(M18,用户要求「完全像文件管理器」):
|
||||
* - 列表 = 当前目录一层的「子目录 + 受支持图片 + .cbz/.zip 档案册」,目录优先、组内自然序;
|
||||
* 点目录=进入(空目录也进),点图片=从该图开始读(mr_reader_open_dir_at(目录, 图片下标)),
|
||||
* 点目录=进入(空目录也进),点图片=从该图开始读(mr_reader_open_dir_at_name(目录, 文件名),
|
||||
* 由阅读器在全量目录册里定位起始页;列表被截断也不会点错图),
|
||||
* 点档案册=打开档案册(第 1 页/续读);
|
||||
* - 首帧只做**单层**枚举(一次 opendir/readdir+lstat):没有「目录含图=一本书」的探测、
|
||||
* 页数、封面、角标,也没有懒算线程/缩略图解码——行副标题在扫描期生成一次;
|
||||
@@ -268,8 +269,7 @@ int library_go_up(void)
|
||||
char parent[MR_PATH_MAX];
|
||||
if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0;
|
||||
/* 防御:父目录若越出浏览根(理论上不会发生),钳回根。 */
|
||||
size_t rl = strlen(g_root_dir);
|
||||
if (strlen(parent) < rl || strncmp(parent, g_root_dir, rl) != 0)
|
||||
if (!mr_path_within_root(parent, g_root_dir))
|
||||
snprintf(parent, sizeof parent, "%s", g_root_dir);
|
||||
if (!load_dir(parent)) {
|
||||
/* 父目录不可读 → 提示并停留(不越根、不丢当前列表)。 */
|
||||
@@ -289,9 +289,9 @@ static void library_open_entry(int idx)
|
||||
return;
|
||||
}
|
||||
if (e->kind == MR_LIB_KIND_IMAGE) {
|
||||
/* 下标由 logic 用与阅读器目录册相同的过滤器/比较器数出,保证「点 A 开 A」。 */
|
||||
int start = mr_library_image_index(&g_lib, idx);
|
||||
mr_reader_open_dir_at(g_cur_dir, start < 0 ? 0 : start);
|
||||
/* 按文件名定位起始页:由阅读器用自己的全量目录册算下标。
|
||||
* 书库列表可能被 MR_LIB_SCAN_MAX 截断,绝不能从列表推导下标(会点 A 开 B)。 */
|
||||
mr_reader_open_dir_at_name(g_cur_dir, e->name);
|
||||
return;
|
||||
}
|
||||
if (e->kind == MR_LIB_KIND_ARCHIVE) {
|
||||
@@ -401,8 +401,8 @@ void library_on_tick(void)
|
||||
/* ============================================================ */
|
||||
static void library_page_create(meow_view *v, void *ctx)
|
||||
{
|
||||
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
|
||||
c->view = v;
|
||||
(void)v;
|
||||
(void)ctx;
|
||||
|
||||
int w = meow_ui_width();
|
||||
int sb = meow_ui_statusbar_height();
|
||||
@@ -525,6 +525,14 @@ int library_test_src(int idx, meow_list_item *item)
|
||||
return lib_src(idx, item, NULL);
|
||||
}
|
||||
|
||||
/* 直接打开第 idx 行(等价点击行体;测试里避免滚动/坐标干扰)。 */
|
||||
int library_test_open_row(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_lib.count) return 0;
|
||||
library_open_entry(idx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int library_test_hint_active(void)
|
||||
{
|
||||
return g_hint[0] != '\0';
|
||||
|
||||
+70
-10
@@ -21,7 +21,7 @@
|
||||
*
|
||||
* 模式(M7):翻页/条漫是一等公民。阅读 chrome 的图标按钮一键切换;每册的模式与 last page
|
||||
* 经 meow_config 持久化(键 reader.mode/page.<册标识>,见 logic/reader.c),下次打开同一册
|
||||
* 自动恢复。书库行角标由 logic/library.c 扫描时读取同一份状态(副标题后缀「续读 Pn」)。
|
||||
* 自动恢复。书库列表是文件管理器模型(M18),不再显示续读角标。
|
||||
*
|
||||
* 跳页:chrome 内 meow_slider 选目标页,松手后先把目标页解码进缓存再落位(不白屏不闪烁;
|
||||
* 2.5s 超时则降级落位并提示)。
|
||||
@@ -86,6 +86,11 @@
|
||||
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=续读) */
|
||||
/* 下次 on_enter 的「按文件名定位起始页」(书库点图;只对目录册生效,open_book 消费后清空)。
|
||||
* 用文件名而不是下标:书库列表可能被 MR_LIB_SCAN_MAX 截断,下标不可信。 */
|
||||
static char g_start_name[MR_NAME_MAX];
|
||||
/* 浏览根(生产=MR_ROOT_DIR,宿主测试可改):册间衔接不得越过它。 */
|
||||
static char g_root[MR_PATH_MAX] = MR_ROOT_DIR;
|
||||
|
||||
static mr_book_handle g_handle; /* 目录册或档案册(M11 统一开册) */
|
||||
static mr_book g_book;
|
||||
@@ -1433,10 +1438,28 @@ static void open_book_reset_state(void)
|
||||
g_meta = NULL;
|
||||
}
|
||||
|
||||
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。 */
|
||||
/* 在目录册里按文件名定位页下标:先精确,再大小写不敏感(书库展示名与磁盘名一致,
|
||||
* 兜底只为容错)。-1=没找到。 */
|
||||
static int dir_book_find_index(const mr_dir_book *db, const char *name)
|
||||
{
|
||||
if (!db || !name || !name[0] || db->count <= 0) return -1;
|
||||
for (int i = 0; i < db->count; i++)
|
||||
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
|
||||
for (int i = 0; i < db->count; i++)
|
||||
if (db->names[i] && strcasecmp(db->names[i], name) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 打开一本册并恢复模式/起始页。start_page<0 = 末页(上一册衔接)。1=成功。
|
||||
* 若 g_start_name 非空(书库点图),优先用**刚枚举出来的目录册**按 basename 定位起始页:
|
||||
* 枚举源与翻页完全一致,且不受书库 MR_LIB_SCAN_MAX 截断影响。 */
|
||||
static int open_book(const char *dir, int start_page, int mode)
|
||||
{
|
||||
if (!dir || !dir[0]) return 0;
|
||||
char want_name[MR_NAME_MAX];
|
||||
snprintf(want_name, sizeof want_name, "%s", g_start_name);
|
||||
g_start_name[0] = '\0'; /* 只在本次打开生效 */
|
||||
|
||||
open_book_reset_state();
|
||||
/* reader_page_enter 传入的 dir 就是 g_dir:自拷贝是 snprintf 的未定义行为,
|
||||
* 其它调用(册间衔接)才需要复制。 */
|
||||
@@ -1472,6 +1495,12 @@ static int open_book(const char *dir, int start_page, int mode)
|
||||
meow_log(MEOW_LOG_WARN, "reader meta cache unavailable (%d pages)", g_count);
|
||||
|
||||
int page = start_page < 0 ? g_count - 1 : start_page;
|
||||
/* 书库点图:按文件名在刚打开的目录册里定位(与翻页同一份枚举/比较器)。
|
||||
* 找不到(文件已改名/删除)才退回 start_page/续读。 */
|
||||
if (want_name[0] && g_handle.kind == MR_BOOK_KIND_DIR) {
|
||||
int idx = dir_book_find_index(&g_handle.u.dir, want_name);
|
||||
if (idx >= 0) page = idx;
|
||||
}
|
||||
page = mr_nav_clamp_page(page, g_count);
|
||||
if (page < 0) page = 0;
|
||||
|
||||
@@ -1525,8 +1554,9 @@ 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);
|
||||
/* 根锁定:相邻册必须仍在浏览根内,绝不跳到 SD 卡挂载点以上。 */
|
||||
int r = mr_book_adjacent_rooted(g_dir, delta > 0 ? 1 : -1, g_root,
|
||||
cbz_pages_probe, NULL, next, (int)sizeof next);
|
||||
if (r != 1) {
|
||||
toast(delta > 0 ? "已是最后一册" : "已是第一册");
|
||||
return 0;
|
||||
@@ -2040,7 +2070,6 @@ static void reader_state_reset(void)
|
||||
static void reader_page_enter(meow_view *v, void *ctx)
|
||||
{
|
||||
mr_page_ctrl *c = (mr_page_ctrl *)ctx;
|
||||
c->view = v;
|
||||
c->active = 1;
|
||||
g_quitting = 0;
|
||||
|
||||
@@ -2120,6 +2149,7 @@ static void reader_page_leave(meow_view *v, void *ctx)
|
||||
free(g_meta);
|
||||
g_meta = NULL;
|
||||
g_start_at = MR_READER_START_RESUME;
|
||||
g_start_name[0] = '\0'; /* 未消费的文件名定位不跨页残留 */
|
||||
mr_book_close_auto(&g_book, &g_handle);
|
||||
|
||||
if (g_slider) { meow_widget_free(g_slider); g_slider = NULL; }
|
||||
@@ -2160,7 +2190,9 @@ const char *mr_reader_dir(void)
|
||||
return g_dir;
|
||||
}
|
||||
|
||||
void mr_reader_open_dir_at(const char *path, int start_index)
|
||||
/* 打开入口共用部分:已在本页时先 pop(完整 leave/enter 生命周期),再设目标并 push。
|
||||
* start_name 非空 = 按文件名定位(只对目录册),start_index 忽略。 */
|
||||
static void reader_open_common(const char *path, int start_index, const char *start_name)
|
||||
{
|
||||
if (!path) return;
|
||||
/* 已在本页(书库再次点另一张图/另一本册时可能仍是 ACT_READER):
|
||||
@@ -2172,13 +2204,31 @@ void mr_reader_open_dir_at(const char *path, int start_index)
|
||||
if (reopen) meow_view_page_pop();
|
||||
|
||||
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);
|
||||
g_start_name[0] = '\0';
|
||||
if (start_name && start_name[0]) {
|
||||
/* 按文件名定位:由 open_book 在刚枚举出的目录册里找下标(不受列表截断影响)。 */
|
||||
snprintf(g_start_name, sizeof g_start_name, "%s", start_name);
|
||||
g_start_at = MR_READER_START_RESUME;
|
||||
meow_log(MEOW_LOG_INFO, "reader open: %s (name=%s)", g_dir, g_start_name);
|
||||
} else {
|
||||
/* -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_at(const char *path, int start_index)
|
||||
{
|
||||
reader_open_common(path, start_index, NULL);
|
||||
}
|
||||
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name)
|
||||
{
|
||||
reader_open_common(dir, MR_READER_START_RESUME, name);
|
||||
}
|
||||
|
||||
void mr_reader_open_dir(const char *path)
|
||||
{
|
||||
mr_reader_open_dir_at(path, MR_READER_START_RESUME);
|
||||
@@ -2205,4 +2255,14 @@ int mr_reader_ui_scaled_draws(void) { return g_scaled_draws; }
|
||||
int mr_reader_ui_decode_count(void) { return g_decode_count; }
|
||||
int mr_reader_ui_fit(void) { return g_fit; }
|
||||
int mr_reader_ui_chrome(void) { return g_chrome; }
|
||||
/* 直接切模式(避开顶栏 tap 命中时序):与顶栏动作走同一个 mode_set。 */
|
||||
void mr_reader_test_toggle_mode(void)
|
||||
{
|
||||
mode_set(g_mode == MR_MODE_PAGED ? MR_MODE_WEBTOON : MR_MODE_PAGED, -1);
|
||||
}
|
||||
void mr_reader_test_set_root(const char *root)
|
||||
{
|
||||
if (root && root[0]) snprintf(g_root, sizeof g_root, "%s", root);
|
||||
}
|
||||
const char *mr_reader_test_root(void) { return g_root; }
|
||||
#endif
|
||||
@@ -19,6 +19,14 @@ void reader_on_touch_move(int x, int y);
|
||||
* 显式下标优先于续读记录:用户在列表里点了某张图,就不该被上次的页码带走。 */
|
||||
#define MR_READER_START_RESUME (-1)
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||
|
||||
/* 书库 → 阅读器(按文件名定位起始页;M18 评审 P1-1 的首选入口):
|
||||
* 打开 dir 后,由阅读器自己的目录册枚举(logic/reader/page_source.c: mr_dir_book_init,
|
||||
* 与翻页用的是同一份全量枚举)按 basename 找起始页(先大小写敏感,再大小写不敏感兜底)。
|
||||
* 找不到则退回续读记录/首页。这样书库不需要(也不能)从可能被 MR_LIB_SCAN_MAX 截断的
|
||||
* 列表里推导下标——目录条目超过 2000 时也不会「点 A 开 B」。name 为 NULL/空则等价续读。 */
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name);
|
||||
|
||||
/* 保留旧入口:等价于 mr_reader_open_dir_at(dir, MR_READER_START_RESUME)。 */
|
||||
void mr_reader_open_dir(const char *dir);
|
||||
|
||||
@@ -36,6 +44,11 @@ int mr_reader_ui_scaled_draws(void);/* 累计重采样绘页次数(每帧重
|
||||
int mr_reader_ui_decode_count(void);/* 累计解码次数(查重解抖动/活锁) */
|
||||
int mr_reader_ui_fit(void); /* 当前 fit 档(MR_FIT_*) */
|
||||
int mr_reader_ui_chrome(void); /* 1=顶栏可见 */
|
||||
/* 直接切模式(绕过顶栏 tap 的命中时序;测试用,产品路径仍走顶栏动作)。 */
|
||||
void mr_reader_test_toggle_mode(void);
|
||||
/* 浏览根(生产=MR_ROOT_DIR):册间衔接不得越过它。测试可指向合成夹具根。 */
|
||||
void mr_reader_test_set_root(const char *root);
|
||||
const char *mr_reader_test_root(void);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
Reference in new issue
Block a user