Files
MeowOS-SDK/tuku/ui/library/library.c
T
AskaEth c5985b2c67 图库: 进入文件夹后顶栏显示「续读」,一键回到上次那页(M33)
用户要求:文件夹拆开可进之后仍保留进度,但入口只在**进入了该文件夹**之后才出现,
不在上层列表的文件夹行加角标/按钮。

- 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 的全部边界。
2026-11-06 19:35:43 +08:00

685 lines
25 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.
/* 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