/* 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 卡」+ 顶栏刷新重试; * 重进本页也会自动重扫。子目录不可读 → 停留原目录 + 顶部提示横幅。 * * 行绑定(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 #include #include #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_refresh = -1; static int g_act_settings = -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); } 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(); update_topbar(); 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(); update_topbar(); 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_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; int w = meow_ui_width(); int sb = meow_ui_statusbar_height(); g_tb = meow_topappbar_create(0, sb, w, MR_TOPAPPBAR_H, "图库"); if (g_tb) { 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); } 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); update_topbar(); 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_refresh = g_act_settings = -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; } /* 当前列表空态文案(无 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