用户要求:文件夹拆开可进之后仍保留进度,但入口只在**进入了该文件夹**之后才出现, 不在上层列表的文件夹行加角标/按钮。 - ui/library/library.c:顶栏按 mr_cont_resume_page(g_cur_dir, 图片数) 判定,有进度才加 MEOW_ICON_PLAY_ARROW「续读」动作(label 保留为语义名);点按 = mr_reader_open_dir_at (g_cur_dir, MR_READER_START_RESUME)。SDK 的 TopAppBar 动作创建期固定、无隐藏 API, 因此可见性变化时整条顶栏重建:无进度是真的 2 个动作(没有空占位/死点击区), 续读插最左,刷新/设置的下标与命中区不变。 - 可见性实时重判:load_dir(进入/返回/刷新)与 library_page_enter(从阅读器返回, 进度可能刚写盘)都调 resume_refresh;根目录有进度时同样显示。 - logic/reader.c/.h:新增 mr_cont_resume_page(dir, pages)——无记录/空目录 → -1(不显示); 保存页超页数(文件被删)→ 钳到末页并 INFO 日志;页数未知(列表被 2000 截断)不夹持, 上界交给阅读器 open_book 自己的 mr_nav_clamp_page。 - 不改列表行语义(不重新引入 M18 删掉的「目录自动当书」)。 - 宿主回归(test_browse_ui_host U8):有进度目录 3 个动作/无进度 2 个、三个动作的点击区 互不遮挡、点续读从当前目录续读页打开、超页数钳到末页、空目录不显示、上层/进入/返回 与阅读器返回后的可见性切换;test_library T5 覆盖 mr_cont_resume_page 的全部边界。
685 lines
25 KiB
C
685 lines
25 KiB
C
/* ui/library/library.c — 图库浏览页(ACT_LIBRARY):文件管理器模型。
|
||
*
|
||
* TopAppBar(标题 + 返回 + [续读] + 刷新 + ⚙)+ meow_listview(src2 行数据源);
|
||
* [续读] 只在当前目录有阅读进度时出现(见下「目录内续读入口」)。
|
||
*
|
||
* 浏览语义(M18,用户要求「完全像文件管理器」):
|
||
* - 列表 = 当前目录一层的「子目录 + 受支持图片 + .cbz/.zip 档案册」,目录优先、组内自然序;
|
||
* 点目录=进入(空目录也进),点图片=从该图开始读(mr_reader_open_dir_at_name(目录, 文件名),
|
||
* 由阅读器在全量目录册里定位起始页;列表被截断也不会点错图),
|
||
* 点档案册=打开档案册(第 1 页/续读);
|
||
* - 首帧只做**单层**枚举(一次 opendir/readdir+lstat):没有「目录含图=一本书」的探测、
|
||
* 页数、封面、角标,也没有懒算线程/缩略图解码——行副标题在扫描期生成一次;
|
||
* - 根目录锁定 TF 卡根(MR_ROOT_DIR):顶栏返回在根隐藏,library_go_up 在根返回 0
|
||
* (app.c 据此退出 App),不存在越过根上溯的导航;
|
||
* - 根目录不可访问(未插卡/挂载失败)→ 列表空态「未检测到 SD 卡」+ 顶栏刷新重试;
|
||
* 重进本页也会自动重扫。子目录不可读 → 停留原目录 + 顶部提示横幅。
|
||
*
|
||
* 目录内续读入口(M33,用户要求):顶栏在**当前所在目录有阅读进度**时多一个「续读」动作
|
||
* (MEOW_ICON_PLAY_ARROW),点按 = mr_reader_open_dir_at(g_cur_dir, MR_READER_START_RESUME)
|
||
* 直接回到上次退出那页。定位是**进入该文件夹之后才有**——不在上层列表的目录行上加角标、
|
||
* 按钮或「目录自动当书」语义(M18 明确删掉的模型,勿 reintroduce):上层列表仍然只是
|
||
* 文件管理器行,只有真进来了才知道这层有没有进度。
|
||
* 可见性随导航实时重判:load_dir(进入/返回/下拉刷新)与 library_page_enter(从阅读器返回,
|
||
* 进度可能刚被写入)都调 resume_refresh;根目录也是目录,有进度时同样显示。
|
||
* 顶栏动作是创建期固定的(SDK 无「动态隐藏 action」API),因此可见性变化时整条顶栏重建:
|
||
* 无进度时是真的 2 个动作(刷新/设置),没有空占位或死点击区;「续读」插在最左,
|
||
* 刷新/设置的下标与命中区不变。
|
||
* 边界:保存页超出当前目录图片数(文件被删)→ 钳到末页并 INFO 日志(mr_cont_resume_page);
|
||
* 空目录/无图片/目录不可读 → 不显示入口。
|
||
*
|
||
* 行绑定(src2 模式):meow_listview_create(..., lib_src, ...) 每帧对可见行取一次行数据;
|
||
* lib_src 只读 g_lib 内存(指针直出,不做 IO/格式化),滚动与点击路径零解码零文件访问。
|
||
*
|
||
* 绘制/颜色全部取 meow_theme_get() 语义色;主题切换由 app.c 的 listener 整帧 invalidate。
|
||
*/
|
||
#include "tuku_internal.h"
|
||
#include "ui/reader/reader.h"
|
||
|
||
#include <errno.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#define MR_TOPAPPBAR_H 56
|
||
#define MR_HINT_H 44
|
||
#define MR_HINT_MS 2500
|
||
|
||
static mr_page_ctrl g_ctrl;
|
||
static mr_library g_lib;
|
||
static char g_cur_dir[MR_PATH_MAX] = MR_ROOT_DIR;
|
||
static char g_root_dir[MR_PATH_MAX] = MR_ROOT_DIR; /* 浏览根(生产=MR_ROOT_DIR,宿主测试可改) */
|
||
|
||
static meow_widget *g_tb;
|
||
static meow_widget *g_lv;
|
||
static meow_scroll *g_scroll;
|
||
static int g_list_x, g_list_y, g_list_w, g_list_h;
|
||
static int g_act_resume = -1; /* 顶栏「续读」(-1=当前目录无进度,动作不存在) */
|
||
static int g_act_refresh = -1;
|
||
static int g_act_settings = -1;
|
||
static int g_resume_on; /* 1=当前目录有可用续读记录(顶栏 3 个动作) */
|
||
static int g_resume_page = -1; /* 「续读」将打开的页(已夹持;-1=无入口) */
|
||
static int g_tb_back; /* 顶栏返回箭头是否显示(根目录隐藏) */
|
||
static int g_sd_missing; /* 1=根目录不可访问(未检测到 SD 卡的空态) */
|
||
|
||
/* ---- 顶部提示横幅(子目录不可读等) ---- */
|
||
static int g_hint_id;
|
||
static char g_hint[160];
|
||
static uint64_t g_hint_until;
|
||
static int g_hint_x, g_hint_y, g_hint_w, g_hint_h;
|
||
|
||
static int g_drag;
|
||
static int g_dragged;
|
||
static int g_drag_last_y;
|
||
static int g_press_row; /* 按下行(-1=无):按下高亮,拖动超阈值清除 */
|
||
static int g_press_delta;
|
||
|
||
/* 清除按下高亮(抬手/拖动/切目录时调用)。 */
|
||
static void press_clear(void)
|
||
{
|
||
if (g_lv) meow_listview_set_sel(g_lv, -1);
|
||
g_press_row = -1;
|
||
g_press_delta = 0;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 行数据源(src2:每帧按需取可见行;零 IO、零格式化) */
|
||
/* ============================================================ */
|
||
static int lib_src(int idx, meow_list_item *item, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
memset(item, 0, sizeof *item);
|
||
|
||
mr_library_row row;
|
||
if (!mr_library_row_at(&g_lib, idx, &row)) return 0;
|
||
|
||
item->title = row.title;
|
||
item->subtitle = row.subtitle;
|
||
if (row.disabled) {
|
||
item->enabled = 0;
|
||
return 1;
|
||
}
|
||
item->enabled = 1;
|
||
if (row.kind == MR_LIB_KIND_DIR) {
|
||
item->icon_kind = MEOW_LIST_ICON_FOLDER;
|
||
item->right = MEOW_LIST_RIGHT_ARROW; /* 目录=可进入 */
|
||
} else {
|
||
item->icon_kind = MEOW_LIST_ICON_FILE; /* 图片/档案册=文件符号 */
|
||
item->right = MEOW_LIST_RIGHT_NONE; /* 点开即读,无子级 */
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局 */
|
||
/* ============================================================ */
|
||
static int lib_top_y(void)
|
||
{
|
||
return meow_ui_statusbar_height() + MR_TOPAPPBAR_H;
|
||
}
|
||
|
||
static void hint_place(void)
|
||
{
|
||
g_hint_x = 0;
|
||
g_hint_y = lib_top_y();
|
||
g_hint_w = meow_ui_width();
|
||
g_hint_h = MR_HINT_H;
|
||
}
|
||
|
||
static const char *empty_text_for_state(void)
|
||
{
|
||
if (g_sd_missing) return "未检测到 SD 卡(插卡后点右上角刷新)";
|
||
return "空文件夹";
|
||
}
|
||
|
||
static void list_rebuild(void)
|
||
{
|
||
if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; }
|
||
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
|
||
g_list_x = 0;
|
||
g_list_y = lib_top_y();
|
||
g_list_w = meow_ui_width();
|
||
g_list_h = meow_ui_height() - g_list_y;
|
||
if (g_list_h < 1) g_list_h = 1;
|
||
|
||
int rows = g_lib.count + (g_lib.truncated ? 1 : 0);
|
||
g_lv = meow_listview_create(g_list_x, g_list_y, g_list_w, g_list_h, rows,
|
||
lib_src, NULL, MEOW_LIST_SEL_SINGLE);
|
||
if (!g_lv) return;
|
||
meow_listview_set_item_renderer(g_lv, 2, 1, 1);
|
||
meow_listview_set_item_tier(g_lv, 2);
|
||
/* SDK 默认 container=0(扁平满幅+分隔线);显式开 MD3 容器模式(与文件管理器一致)。 */
|
||
meow_listview_set_container(g_lv, 1);
|
||
meow_listview_set_empty_text(g_lv, empty_text_for_state());
|
||
|
||
g_scroll = meow_scroll_wrap(g_lv, 1);
|
||
/* 不设 snap:文件管理器手感是自由滚动,逐行吸附会发黏。 */
|
||
}
|
||
|
||
static void update_topbar(void)
|
||
{
|
||
if (!g_tb) return;
|
||
const char *title = "图库";
|
||
if (strcmp(g_cur_dir, g_root_dir) != 0) {
|
||
const char *slash = strrchr(g_cur_dir, '/');
|
||
if (slash && slash[1]) title = slash + 1;
|
||
}
|
||
meow_topappbar_set_title(g_tb, title);
|
||
/* 根目录锁定:根=浏览天花板,顶栏不显示返回/上级。 */
|
||
g_tb_back = (strcmp(g_cur_dir, g_root_dir) != 0);
|
||
meow_topappbar_set_back(g_tb, g_tb_back);
|
||
}
|
||
|
||
/* 重建顶栏:动作集由 g_resume_on 决定(3=含续读 / 2=只有刷新+设置)。
|
||
* SDK 的 TopAppBar 动作是 add_action 创建期固定、没有隐藏/删除 API,所以用「重建」实现
|
||
* 真正的按需显示(不留空占位、不留 12px 死点击区,动作数就是 2 或 3)。
|
||
* 续读放最左:刷新/设置的下标仍是倒数第二/最后一个,命中区与改动前完全一致。 */
|
||
static void topbar_build(void)
|
||
{
|
||
int sb = meow_ui_statusbar_height();
|
||
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
g_act_resume = g_act_refresh = g_act_settings = -1;
|
||
|
||
g_tb = meow_topappbar_create(0, sb, meow_ui_width(), MR_TOPAPPBAR_H, "图库");
|
||
if (!g_tb) return;
|
||
if (g_resume_on) {
|
||
/* label "续读" 在被 set_item_icon 转位图后不绘制,但保留为该动作的语义名。 */
|
||
g_act_resume = meow_topappbar_add_action(g_tb, "续读");
|
||
if (g_act_resume >= 0)
|
||
meow_topappbar_set_item_icon(g_tb, g_act_resume, MEOW_ICON_PLAY_ARROW);
|
||
}
|
||
g_act_refresh = meow_topappbar_add_action(g_tb, "");
|
||
if (g_act_refresh >= 0) meow_topappbar_set_item_icon(g_tb, g_act_refresh, MEOW_ICON_REFRESH);
|
||
g_act_settings = meow_topappbar_add_action(g_tb, "");
|
||
if (g_act_settings >= 0) meow_topappbar_set_item_icon(g_tb, g_act_settings, MEOW_ICON_SETTINGS);
|
||
update_topbar();
|
||
}
|
||
|
||
/* 当前目录的图片页数(与阅读器目录册同一套单层判据:受支持图片、非隐藏、普通文件)。
|
||
* -1=列表被 MR_LIB_SCAN_MAX 截断 → 真实页数 >= 已列出数,未知;此时不做上界夹持,
|
||
* 交给阅读器 open_book(mr_nav_clamp_page)按它自己的全量枚举钳制。 */
|
||
static int cur_page_count(void)
|
||
{
|
||
if (g_lib.truncated) return -1;
|
||
int n = 0;
|
||
for (int i = 0; i < g_lib.count; i++)
|
||
if (g_lib.items[i].kind == MR_LIB_KIND_IMAGE) n++;
|
||
return n;
|
||
}
|
||
|
||
/* 重新判定「续读」入口(load_dir / 从阅读器返回 / 下拉刷新后都走这里)。
|
||
* 只在当前所在目录有可用续读时显示;无进度/空目录/无图片 → 隐藏(零占位)。 */
|
||
static void resume_refresh(void)
|
||
{
|
||
g_resume_page = mr_cont_resume_page(g_cur_dir, cur_page_count());
|
||
int on = (g_resume_page >= 0);
|
||
if (!g_tb || on != g_resume_on) {
|
||
g_resume_on = on;
|
||
topbar_build();
|
||
} else {
|
||
update_topbar();
|
||
}
|
||
}
|
||
|
||
static void list_invalidate(void)
|
||
{
|
||
meow_ui_invalidate(g_list_x, g_list_y, g_list_w, g_list_h);
|
||
}
|
||
|
||
static void hint_invalidate(void)
|
||
{
|
||
meow_ui_invalidate(g_hint_x, g_hint_y, g_hint_w, g_hint_h);
|
||
}
|
||
|
||
static void show_hint(const char *msg)
|
||
{
|
||
snprintf(g_hint, sizeof g_hint, "%s", msg ? msg : "");
|
||
g_hint_until = meow_ui_time_ms() + MR_HINT_MS;
|
||
hint_place();
|
||
hint_invalidate();
|
||
}
|
||
|
||
/* 顶部横幅绘制物:只在提示有效且本页 active 时输出(custom 绘制物无 owner 闸门,必须自守)。 */
|
||
static void library_draw_hint(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_LIBRARY) return;
|
||
if (!g_hint[0]) return;
|
||
|
||
const meow_theme *th = meow_theme_get();
|
||
int x = g_hint_x, y = g_hint_y, w = g_hint_w, h = g_hint_h;
|
||
if (w < 40 || h < 20) return;
|
||
meow_ui_fill_rect(x + 8, y + 4, w - 16, h - 8, th->surfaceContainerHighest);
|
||
meow_ui_draw_rect(x + 8, y + 4, w - 16, h - 8, th->error);
|
||
int ty = y + (h - meow_ui_text_height(th->font_body)) / 2;
|
||
meow_ui_text(x + 20, ty, g_hint, th->font_body, th->error);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 目录加载 */
|
||
/* ============================================================ */
|
||
static void repaint_all(void)
|
||
{
|
||
if (g_ctrl.active)
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||
}
|
||
|
||
/* 切到 target(空/失败列表也会切);成功 1,目录不可读 0。
|
||
* 根目录不可读 → SD 卡空态;子目录不可读 → 停留原目录 + 提示。 */
|
||
static int load_dir(const char *dir)
|
||
{
|
||
/* dir 可能指向 g_lib 里的行路径(点击进入子目录):先拷到本地,
|
||
* 避免切换后 mr_library_free 释放 g_lib,dir 变成悬垂指针。 */
|
||
char target[MR_PATH_MAX];
|
||
if (!dir || !dir[0] || strlen(dir) >= sizeof target) {
|
||
show_hint("路径无效");
|
||
return 0;
|
||
}
|
||
snprintf(target, sizeof target, "%s", dir);
|
||
|
||
int is_root = (strcmp(target, g_root_dir) == 0);
|
||
|
||
mr_library next;
|
||
memset(&next, 0, sizeof next);
|
||
int n = mr_library_scan(&next, target, MR_LIB_MAX_DEPTH);
|
||
if (n < 0) {
|
||
mr_library_free(&next);
|
||
if (is_root) {
|
||
/* 未插卡/挂载失败/根不是目录:友好空态 + 可重试,绝不弹错误。 */
|
||
mr_library_free(&g_lib);
|
||
snprintf(g_cur_dir, sizeof g_cur_dir, "%s", target);
|
||
g_sd_missing = 1;
|
||
list_rebuild();
|
||
resume_refresh(); /* SD 卡空态:无列表/无图片 → 续读入口隐藏 */
|
||
repaint_all();
|
||
meow_log(MEOW_LOG_INFO, "library: storage root unavailable: %s (errno=%d %s)",
|
||
target, errno, strerror(errno));
|
||
return 0;
|
||
}
|
||
const char *slash = strrchr(target, '/');
|
||
const char *name = (slash && slash[1]) ? slash + 1 : target;
|
||
char msg[200];
|
||
snprintf(msg, sizeof msg, "无法读取文件夹:%s", name);
|
||
show_hint(msg);
|
||
meow_log(MEOW_LOG_WARN, "library scan failed: %s (errno=%d %s)", target,
|
||
errno, strerror(errno));
|
||
return 0;
|
||
}
|
||
|
||
mr_library_free(&g_lib);
|
||
g_lib = next;
|
||
snprintf(g_cur_dir, sizeof g_cur_dir, "%s", target);
|
||
g_sd_missing = 0;
|
||
|
||
list_rebuild();
|
||
resume_refresh(); /* 进入/返回/刷新都重判:当前目录有进度才显示续读 */
|
||
g_drag = 0;
|
||
g_dragged = 0;
|
||
repaint_all();
|
||
return 1;
|
||
}
|
||
|
||
/* 顶栏刷新:重扫当前目录(无 SD 卡时就地重试;列表由 load_dir 重建)。 */
|
||
static void refresh_current(void)
|
||
{
|
||
load_dir(g_cur_dir);
|
||
}
|
||
|
||
int library_at_root(void)
|
||
{
|
||
return strcmp(g_cur_dir, g_root_dir) == 0;
|
||
}
|
||
|
||
/* 系统返回语义:逐级上移;到浏览根(TF 卡根)返回 0 → app.c 退出 App。
|
||
* 根目录锁定:不存在越过 MR_ROOT_DIR 的导航。 */
|
||
int library_go_up(void)
|
||
{
|
||
if (library_at_root()) return 0;
|
||
char parent[MR_PATH_MAX];
|
||
if (mr_path_parent(g_cur_dir, parent, (int)sizeof parent) != 0) return 0;
|
||
/* 防御:父目录若越出浏览根(理论上不会发生),钳回根。 */
|
||
if (!mr_path_within_root(parent, g_root_dir))
|
||
snprintf(parent, sizeof parent, "%s", g_root_dir);
|
||
if (!load_dir(parent)) {
|
||
/* 父目录不可读 → 提示并停留(不越根、不丢当前列表)。 */
|
||
return 1;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* 点击行:目录=进入;图片=从该图开始读;档案册=打开档案册。 */
|
||
static void library_open_entry(int idx)
|
||
{
|
||
if (idx < 0 || idx >= g_lib.count) return;
|
||
const mr_library_entry *e = &g_lib.items[idx];
|
||
|
||
if (e->kind == MR_LIB_KIND_DIR) {
|
||
load_dir(e->path);
|
||
return;
|
||
}
|
||
if (e->kind == MR_LIB_KIND_IMAGE) {
|
||
/* 按文件名定位起始页:由阅读器用自己的全量目录册算下标。
|
||
* 书库列表可能被 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) {
|
||
mr_reader_open_dir_at(e->path, MR_READER_START_RESUME);
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 事件 */
|
||
/* ============================================================ */
|
||
void library_on_tap(int x, int y)
|
||
{
|
||
if (!g_ctrl.active) return;
|
||
g_drag = 0;
|
||
g_dragged = 0;
|
||
press_clear();
|
||
|
||
if (g_tb) {
|
||
int hit = meow_topappbar_hit(g_tb, x, y);
|
||
if (hit == MEOW_TOPBAR_BACK) { library_go_up(); return; }
|
||
if (g_act_resume >= 0 && hit == g_act_resume) {
|
||
/* 一键回到本目录上次退出那页:续读页由阅读器按自己的全量目录册再夹持一次。 */
|
||
if (g_resume_page >= 0)
|
||
mr_reader_open_dir_at(g_cur_dir, MR_READER_START_RESUME);
|
||
return;
|
||
}
|
||
if (g_act_refresh >= 0 && hit == g_act_refresh) { refresh_current(); return; }
|
||
if (g_act_settings >= 0 && hit == g_act_settings) {
|
||
meow_view_page_push(ACT_SETTINGS);
|
||
return;
|
||
}
|
||
}
|
||
if (!g_lv) return;
|
||
int idx = meow_listview_hit(g_lv, x, y);
|
||
if (idx < 0) return;
|
||
mr_library_row row;
|
||
if (!mr_library_row_at(&g_lib, idx, &row) || row.disabled) return;
|
||
library_open_entry(row.index);
|
||
}
|
||
|
||
void library_on_swipe(int dir, int dist)
|
||
{
|
||
if (!g_ctrl.active || !g_lv) return;
|
||
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||
if (g_dragged) { /* 本次手势已实时跟手滚动,抬手只补 fling */
|
||
g_dragged = 0;
|
||
g_drag = 0;
|
||
if (g_scroll) meow_scroll_fling(g_scroll);
|
||
return;
|
||
}
|
||
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
|
||
if (del == 0) del = (dir == MEOW_SWIPE_UP) ? 120 : -120;
|
||
if (g_scroll) {
|
||
meow_scroll_track(g_scroll, del, meow_ui_time_ms());
|
||
meow_scroll_fling(g_scroll);
|
||
} else {
|
||
meow_listview_scroll_by(g_lv, del);
|
||
}
|
||
list_invalidate();
|
||
}
|
||
|
||
void library_on_touch_down(int x, int y)
|
||
{
|
||
if (!g_ctrl.active || !g_lv) return;
|
||
/* 每次按下都作废上一次手势的拖拽状态:热区/左缘提前返回时不能残留 g_drag,
|
||
* 否则系统返回手势的 move 仍会驱动列表滚动。 */
|
||
g_drag = 0;
|
||
g_dragged = 0;
|
||
press_clear();
|
||
/* 屏底 28px 是系统「上滑回桌面」热区:不从这里启动列表拖拽,避免拦截系统手势。 */
|
||
if (y >= meow_ui_height() - 28) return;
|
||
/* 左缘 24px 是系统返回手势带(与 reader 同口径):列表不启动拖拽,把滑动留给系统返回。 */
|
||
if (x < 24) return;
|
||
if (x < g_list_x || x >= g_list_x + g_list_w ||
|
||
y < g_list_y || y >= g_list_y + g_list_h) return;
|
||
g_drag = 1;
|
||
g_dragged = 0;
|
||
g_drag_last_y = y;
|
||
/* 按下高亮(与文件管理器一致;拖动超阈值即清除)。 */
|
||
g_press_row = meow_listview_hit(g_lv, x, y);
|
||
if (g_press_row >= 0) meow_listview_set_sel(g_lv, g_press_row);
|
||
}
|
||
|
||
void library_on_touch_move(int x, int y)
|
||
{
|
||
(void)x;
|
||
if (!g_ctrl.active || !g_drag || !g_lv) return;
|
||
int dy = g_drag_last_y - y;
|
||
if (dy == 0) return;
|
||
if (dy > 1 || dy < -1) g_dragged = 1;
|
||
if (g_press_row >= 0) {
|
||
g_press_delta += dy > 0 ? dy : -dy;
|
||
if (g_press_delta > 10) press_clear(); /* 拖动:清除按下高亮 */
|
||
}
|
||
g_drag_last_y = y;
|
||
if (g_scroll) meow_scroll_track(g_scroll, dy, meow_ui_time_ms());
|
||
else meow_listview_scroll_by(g_lv, dy);
|
||
list_invalidate();
|
||
}
|
||
|
||
/* 16ms tick:只剩提示横幅到期清除(列表同步扫描,无后台补位)。 */
|
||
void library_on_tick(void)
|
||
{
|
||
if (!g_ctrl.active) return;
|
||
if (g_hint[0] && meow_ui_time_ms() >= g_hint_until) {
|
||
g_hint[0] = '\0';
|
||
hint_invalidate();
|
||
}
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* view 生命周期 */
|
||
/* ============================================================ */
|
||
static void library_page_create(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
(void)ctx;
|
||
|
||
/* 启动时还不知道根目录有没有进度:先按「无续读」建顶栏(2 个动作),
|
||
* load_dir 里的 resume_refresh 会按实际可见性决定是否补上「续读」动作。 */
|
||
topbar_build();
|
||
|
||
hint_place();
|
||
g_hint_id = meow_draw_custom_register(MEOW_Z_TOP, g_hint_x, g_hint_y,
|
||
g_hint_w, g_hint_h, library_draw_hint, NULL);
|
||
load_dir(g_root_dir);
|
||
}
|
||
|
||
static void library_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
((mr_page_ctrl *)ctx)->active = 1;
|
||
v->x = 0;
|
||
v->y = 0;
|
||
v->w = meow_ui_width();
|
||
v->h = meow_ui_height();
|
||
meow_ui_set_fullscreen(0); /* 图库保留状态栏(reader 退出时若未复原也在此兜底) */
|
||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||
|
||
int sb = meow_ui_statusbar_height();
|
||
if (g_tb) meow_widget_set_rect(g_tb, 0, sb, meow_ui_width(), MR_TOPAPPBAR_H);
|
||
if (g_lv) {
|
||
int y = sb + MR_TOPAPPBAR_H;
|
||
int h = meow_ui_height() - y;
|
||
if (h < 1) h = 1;
|
||
meow_widget_set_rect(g_lv, 0, y, meow_ui_width(), h);
|
||
g_list_y = y;
|
||
g_list_w = meow_ui_width();
|
||
g_list_h = h;
|
||
}
|
||
hint_place();
|
||
g_drag = 0;
|
||
g_dragged = 0;
|
||
/* 无 SD 卡空态:重进页面即重扫(用户要求的可重试路径之一)。 */
|
||
if (g_sd_missing) load_dir(g_cur_dir);
|
||
/* 从阅读器/设置页返回:进度可能刚被阅读器写盘(离开阅读页时 cont_save_now),
|
||
* 因此每次进页都重判「续读」可见性(幂等;无变化时只刷新标题/返回箭头)。 */
|
||
resume_refresh();
|
||
mr_nav_report();
|
||
meow_ui_paint_full();
|
||
}
|
||
|
||
static void library_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((mr_page_ctrl *)ctx)->active = 0;
|
||
g_drag = 0;
|
||
g_dragged = 0;
|
||
press_clear();
|
||
if (g_scroll) meow_scroll_stop(g_scroll);
|
||
}
|
||
|
||
static void library_page_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
if (g_hint_id) { meow_draw_unregister(g_hint_id); g_hint_id = 0; }
|
||
if (g_scroll) { meow_scroll_free(g_scroll); g_scroll = NULL; }
|
||
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
|
||
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||
g_act_resume = g_act_refresh = g_act_settings = -1;
|
||
g_resume_on = 0;
|
||
g_resume_page = -1;
|
||
mr_library_free(&g_lib);
|
||
}
|
||
|
||
meow_view *library_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_create = library_page_create,
|
||
.on_enter = library_page_enter,
|
||
.on_leave = library_page_leave,
|
||
.on_destroy = library_page_destroy,
|
||
};
|
||
return meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||
}
|
||
|
||
#ifdef MR_LIBRARY_UI_TEST_HOOKS
|
||
/* 宿主集成测试接缝(只在 -DMR_LIBRARY_UI_TEST_HOOKS 编译时存在;不进交付二进制)。 */
|
||
int library_test_load(const char *dir)
|
||
{
|
||
return load_dir(dir);
|
||
}
|
||
|
||
void library_test_set_root(const char *root)
|
||
{
|
||
if (root && root[0]) snprintf(g_root_dir, sizeof g_root_dir, "%s", root);
|
||
}
|
||
|
||
const char *library_test_root(void)
|
||
{
|
||
return g_root_dir;
|
||
}
|
||
|
||
const char *library_test_dir(void)
|
||
{
|
||
return g_cur_dir;
|
||
}
|
||
|
||
void library_test_set_dir(const char *dir)
|
||
{
|
||
if (dir && dir[0]) snprintf(g_cur_dir, sizeof g_cur_dir, "%s", dir);
|
||
}
|
||
|
||
int library_test_rows(void)
|
||
{
|
||
return g_lib.count + (g_lib.truncated ? 1 : 0);
|
||
}
|
||
|
||
int library_test_has_list(void)
|
||
{
|
||
return g_lv != NULL;
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
const char *library_test_hint(void)
|
||
{
|
||
return g_hint;
|
||
}
|
||
|
||
/* 顶栏返回箭头是否显示(根目录必须隐藏)。 */
|
||
int library_test_topbar_back(void)
|
||
{
|
||
return g_tb_back;
|
||
}
|
||
|
||
/* 顶栏动作数:2=刷新+设置(当前目录无进度),3=再加上「续读」。 */
|
||
int library_test_topbar_actions(void)
|
||
{
|
||
return (g_act_resume >= 0 ? 1 : 0) + (g_act_refresh >= 0 ? 1 : 0) +
|
||
(g_act_settings >= 0 ? 1 : 0);
|
||
}
|
||
|
||
/* 「续读」将打开的页(已按当前页数夹持;-1=没有入口)。 */
|
||
int library_test_resume_page(void)
|
||
{
|
||
return g_resume_page;
|
||
}
|
||
|
||
/* 顶栏命中(走真实 meow_topappbar_hit):0=无 1=返回 2=续读 3=刷新 4=设置。
|
||
* 用于断言三个动作的点击区互不遮挡、且无进度时续读区不存在。 */
|
||
#define LIB_TB_NONE 0
|
||
#define LIB_TB_BACK 1
|
||
#define LIB_TB_RESUME 2
|
||
#define LIB_TB_REFRESH 3
|
||
#define LIB_TB_SETTINGS 4
|
||
int library_test_topbar_hit(int x, int y)
|
||
{
|
||
if (!g_tb) return LIB_TB_NONE;
|
||
int hit = meow_topappbar_hit(g_tb, x, y);
|
||
if (hit == MEOW_TOPBAR_BACK) return LIB_TB_BACK;
|
||
if (hit >= 0 && hit == g_act_resume) return LIB_TB_RESUME;
|
||
if (hit >= 0 && hit == g_act_refresh) return LIB_TB_REFRESH;
|
||
if (hit >= 0 && hit == g_act_settings) return LIB_TB_SETTINGS;
|
||
return LIB_TB_NONE;
|
||
}
|
||
|
||
/* 当前列表空态文案(无 SD 卡/空文件夹)。 */
|
||
const char *library_test_empty_text(void)
|
||
{
|
||
return empty_text_for_state();
|
||
}
|
||
|
||
/* 1=根目录不可访问(未检测到 SD 卡的空态)。 */
|
||
int library_test_sd_missing(void)
|
||
{
|
||
return g_sd_missing;
|
||
}
|
||
|
||
/* 顶栏刷新动作:重扫当前目录(无 SD 卡时就地重试)。 */
|
||
int library_test_retry(void)
|
||
{
|
||
refresh_current();
|
||
return library_test_rows();
|
||
}
|
||
|
||
int library_test_go_up(void)
|
||
{
|
||
return library_go_up();
|
||
}
|
||
|
||
/* 跟手拖拽是否在本次手势中被启动(左缘/屏底热区排除的回归观测口)。 */
|
||
int library_test_drag_active(void)
|
||
{
|
||
return g_drag;
|
||
}
|
||
#endif
|