/* ui/browser.c — 目录浏览页(ACT_BROWSER,根页)view + 控制器。 * * 职责: * - 持有目录态(g_cur 当前目录 / g_entries 缓存快照 / g_err 空态码); * - 内容区用 SDK 结构化列表(meow_listview,§5.5 W_LISTVIEW)虚拟化渲染 * 「图标 + 名称 + 大小/时间 + 右侧 ⋯ 操作钮」行:行数据源 browser_src2 逐行填充 * meow_list_item(icon_kind=目录/文件符号、副标题=大小/时间、right=⋯)。滚动/选中命中由 * SDK 收口(视口裁剪、局部 dirty、disabled 不响应),App 只在 on_tap/touch_move 驱动 * scroll/sel 并分发:行体=进目录/开预览;右侧 ⋯ → BottomSheet;**长按=SDK 通用长按事件 * (§6.3,≥500ms 未抬手且位移≤容忍)** 由 SDK 判定后经 on_long_press 回调带坐标交给本页, * App 据此行命中打开同一 BottomSheet(App 层长按计时逻辑已删,交给 SDK); * - 空态/错误(无卡/空目录/权限)与底部信息行用 meow_draw_custom_register 注册为非控件绘制物 * (app_draw_empty / app_draw_footer)——列表视口不含页脚,行不压页脚;invalidate/paint_full * 自动重画它们; * - 写操作只经 logic(fsx_*),且在卡内校验后执行。 * * 颜色全部读 meow_theme_get() 语义色(§8.1),不硬编码。 */ #include "files_internal.h" #include #include #include #include typedef struct { meow_view *view; int active; } browser_ctrl; static browser_ctrl g_bc; static fsx_entry g_entries[FSX_MAX_ENTRIES]; static int g_n; /* 当前目录项数 */ static char g_cur[FSX_MAX_PATH]; static int g_err; /* fsx_scan_dir 返回(0=ok,<0=空态码) */ static int g_press_row; /* 按下行(-1=无);按下高亮 + 拖动识别(长按已交 SDK §6.3) */ static int g_press_delta; /* 按下后累计位移(>阈值清除按下高亮) */ static int g_dragging; /* 本次触摸起点是否在列表(拖动滚列表) */ static int g_drag_last_y; /* 拖拽上一帧 y */ static meow_widget *g_tb; /* TopAppBar(chrome) */ static meow_widget *g_list; /* 内容区结构化列表(meow_listview,W_LISTVIEW) */ static app_rect_t r_content; /* 顶部栏之下 → 屏底(含页脚带) */ static app_rect_t r_list; /* 列表视口 = 内容区去掉页脚带(行不压页脚) */ static int g_empty_id; /* 空态/错误绘制物(非控件) */ static int g_footer_id; /* 底部信息行绘制物(非控件) */ static char g_title[FSX_MAX_PATH]; /* 预览/操作辅助(前向声明) */ static void browser_open_sheet(int row); void browser_on_long_press(int x, int y); /* 跨 TU(app.c 经 §6.3 回调调用);files_internal.h */ static void browser_enter_dir(const char *path); static void browser_open_file(const char *path); static void browser_rename_entry(const fsx_entry *e); static void browser_delete_entry(const fsx_entry *e); static void browser_props_entry(const fsx_entry *e); static void browser_new_folder_in(const char *dirpath); static void browser_report_ret(int r); /* 本轮按下状态复位(每次 on_tap / on_touch_down 前清)。 */ static void browser_drag_reset(void) { g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0; } /* ---- 列表行数据源:逐行填 meow_list_item(图标/名称/大小时间/⋯ ),轻量不 IO(§5.5)---- */ static int browser_src2(int idx, meow_list_item *out, void *ctx) { (void)ctx; if (idx < 0 || idx >= g_n) return 0; fsx_entry *e = &g_entries[idx]; static char sub[FSX_MAX_NAME + 48]; /* 行内瞬时字符串(绘制即刻用,重复填充无碍) */ char sz[32], tm[32]; out->icon_kind = e->is_dir ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_FILE; out->title = e->name; if (e->is_dir) { fsx_fmt_time(e->mtime, tm, sizeof tm); snprintf(sub, sizeof sub, "Folder %s", tm); } else { fsx_fmt_size(e->size, sz, sizeof sz); fsx_fmt_time(e->mtime, tm, sizeof tm); snprintf(sub, sizeof sub, "%s %s", sz, tm); } out->subtitle = sub; out->right = MEOW_LIST_RIGHT_MORE; /* 右侧 ⋯ 详情钮(行右 1/3 判操作) */ out->on = 0; out->enabled = 1; return 1; } /* ---- 空态/错误绘制物(非控件;非空态时无输出)---- * ⚠️ custom 绘制物无 owner 闸门(不同于 widget,见 meow_ui_draw.c draw_order): * 必须显式按「本页是否为当前 active activity」守门,否则切到 viewer 页会跨页泄漏绘制。 */ static void browser_draw_empty(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_BROWSER) return; /* 非本页 active 不画(防跨页泄漏) */ if (g_err == 0 && g_n != 0) return; if (g_err == FSX_ERR_NOENT) app_draw_empty(r_list, "NO STORAGE CARD", "insert a TF card / mount sd_0"); else if (g_err == FSX_ERR_EACCES) app_draw_empty(r_list, "ACCESS DENIED", "cannot read this folder"); else if (g_err) app_draw_empty(r_list, "CANNOT OPEN", "unexpected error"); else app_draw_empty(r_list, "EMPTY FOLDER", g_cur); } /* ---- 底部信息行绘制物(非控件):项数/路径/截断提示 ---- */ static void browser_draw_footer(void *ctx) { (void)ctx; if (meow_ui_activity_active() != ACT_BROWSER) return; /* 非本页 active 不画(防跨页泄漏) */ if (g_err != 0) return; char foot[96]; if (g_n == 0) snprintf(foot, sizeof foot, "0 items"); else if (g_n >= FSX_MAX_ENTRIES) snprintf(foot, sizeof foot, "%d items (showing first %d)", g_n, FSX_MAX_ENTRIES); else snprintf(foot, sizeof foot, "%d items %s", g_n, g_cur); app_draw_footer(r_content, foot); } /* ---- 导航 ---- */ static void browser_update_title(void) { const char *name = fsx_basename(g_cur); if (fsx_is_root(g_cur)) snprintf(g_title, sizeof g_title, "SD Card"); else snprintf(g_title, sizeof g_title, "%s", name); if (g_title[0] == '\0') snprintf(g_title, sizeof g_title, "Files"); if (g_tb) meow_topappbar_set_title(g_tb, g_title); } void browser_reload(void) { int r = fsx_scan_dir(g_cur, g_entries, FSX_MAX_ENTRIES, &g_n); g_err = (r < 0) ? r : 0; if (g_err != 0) g_n = 0; if (g_list) { meow_listview_set_count(g_list, g_n); /* 行数动态更新(§5.5) */ meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); } browser_update_title(); const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); /* 重画全注册绘制物(行/空态/页脚/topbar) */ } void browser_enter_dir(const char *path) { if (!path) return; strncpy(g_cur, path, sizeof g_cur - 1); g_cur[sizeof g_cur - 1] = '\0'; browser_reload(); } int browser_go_up(void) { if (fsx_is_root(g_cur)) return 0; char up[FSX_MAX_PATH]; if (fsx_dirname(g_cur, up, sizeof up) != 0) return 0; browser_enter_dir(up); return 1; } int browser_at_root(void) { return fsx_is_root(g_cur); } int browser_is_active(void) { return g_bc.active; } const char *browser_current_dir(void) { return g_cur; } int browser_entry_count(void) { return g_n; } /* ---- 行操作 ---- */ static void browser_report_ret(int r) { if (r == 0) return; const char *msg = strerror(-r); meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, "Action failed"); const char *msgs[1]; msgs[0] = msg ? msg : "error"; meow_dialog_set_items(d, msgs, 1, -1); meow_dialog_show(d); meow_dialog_free(d); } static void browser_rename_entry(const fsx_entry *e) { meow_ime_ctx ime; memset(&ime, 0, sizeof ime); ime.mode = MEOW_IME_ASCII; strncpy(ime.buf, e->name, sizeof ime.buf - 1); ime.maxlen = 120; meow_ui_text_input(&ime); char nb[FSX_MAX_NAME]; if (fsx_sanitize_name(ime.buf, nb, sizeof nb) == 0 && strcmp(ime.buf, e->name) != 0) { int r = fsx_rename_into(e->path, nb); browser_report_ret(r); } } static void browser_delete_entry(const fsx_entry *e) { char m[192]; snprintf(m, sizeof m, "Really delete \"%s\"%s?", e->name, e->is_dir ? " (folder must be empty)" : ""); meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Delete"); const char *msgs[1]; msgs[0] = m; meow_dialog_set_items(d, msgs, 1, -1); int yes = meow_dialog_confirm(d); meow_dialog_free(d); if (yes) { int r = fsx_delete(e->path); browser_report_ret(r); } } static void browser_props_entry(const fsx_entry *e) { fsx_entry st; if (fsx_stat_path(e->path, &st) != 0) return; char size[32], tm[32], l1[64], l2[64], l3[64]; fsx_fmt_size(st.size, size, sizeof size); fsx_fmt_time(st.mtime, tm, sizeof tm); snprintf(l1, sizeof l1, "Type: %s", st.is_dir ? "Directory" : "File"); snprintf(l2, sizeof l2, "Size: %s", st.is_dir ? "-" : size); snprintf(l3, sizeof l3, "Modified: %s", tm); const char *msgs[3]; msgs[0] = l1; msgs[1] = l2; msgs[2] = l3; meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, st.name); meow_dialog_set_items(d, msgs, 3, -1); meow_dialog_show(d); meow_dialog_free(d); } static void browser_new_folder_in(const char *dirpath) { meow_ime_ctx ime; memset(&ime, 0, sizeof ime); ime.mode = MEOW_IME_ASCII; ime.maxlen = 120; meow_ui_text_input(&ime); char nb[FSX_MAX_NAME]; if (fsx_sanitize_name(ime.buf, nb, sizeof nb) == 0) { int r = fsx_mkdir(dirpath, nb); browser_report_ret(r); if (r == 0 && browser_is_active()) browser_reload(); } } static void browser_new_folder(void) /* 当前目录下新建 */ { browser_new_folder_in(g_cur); } /* BottomSheet:按行类型建不同选项清单。 */ static void browser_open_sheet(int row) { fsx_entry *e = &g_entries[row]; char title[128]; snprintf(title, sizeof title, "%s: %s", e->is_dir ? "Folder" : "File", e->name); meow_sheet *s = meow_sheet_create(title, 0); if (e->is_dir) { meow_sheet_add_item(s, "Open"); meow_sheet_add_item(s, "New Folder"); meow_sheet_add_item(s, "Rename"); meow_sheet_add_item(s, "Delete"); meow_sheet_add_item(s, "Properties"); } else { meow_sheet_add_item(s, "Open"); meow_sheet_add_item(s, "Rename"); meow_sheet_add_item(s, "Delete"); meow_sheet_add_item(s, "Properties"); } int op = meow_sheet_open(s); meow_sheet_free(s); if (op < 0) return; if (op == 0) { /* Open */ if (e->is_dir) browser_enter_dir(e->path); else browser_open_file(e->path); } else if (e->is_dir) { if (op == 1) browser_new_folder_in(e->path); else if (op == 2) browser_rename_entry(e); else if (op == 3) browser_delete_entry(e); else if (op == 4) browser_props_entry(e); } else { if (op == 1) browser_rename_entry(e); else if (op == 2) browser_delete_entry(e); else if (op == 3) browser_props_entry(e); } if (browser_is_active()) browser_reload(); /* 写操作后重扫(Open 文件时 viewer 前台则跳过) */ } /* 打开文件:文本→预览页;否则提示。 */ static void browser_open_file(const char *path) { if (fsx_is_text_path(path)) { if (viewer_open_file(path) == 0) return; /* 已 push 预览页 */ browser_report_ret(-EIO); return; } meow_dialog *d = meow_dialog_create(MEOW_DLG_INFO, "Preview"); const char *msgs[1]; msgs[0] = "This is not a text file.\nNo preview available."; meow_dialog_set_items(d, msgs, 1, -1); meow_dialog_show(d); meow_dialog_free(d); } /* ---- 输入分发 ---- */ int browser_on_tap(int x, int y) { if (g_tb) { int ah = meow_topappbar_hit(g_tb, x, y); if (ah == MEOW_TOPBAR_BACK) { browser_drag_reset(); browser_go_up(); return 1; } if (ah >= 0) { browser_drag_reset(); browser_new_folder(); return 1; } /* NEW */ } if (g_err != 0 || g_n == 0) return 0; if (meow_widget_hit(g_list, x, y)) { int row = meow_listview_hit(g_list, x, y); /* 行号(含滚动换算;禁用行 -1) */ if (row < 0) { browser_drag_reset(); return 1; } /* 命中列表但无有效行(空隙) */ int op = meow_listview_hit_op(g_list, x, y); /* 行右 1/3 = 操作区 */ if (op) { if (!picker_active()) browser_open_sheet(row); /* §6.2 二期:picker 模式不弹 sheet */ } else { fsx_entry *e = &g_entries[row]; if (e->is_dir) browser_enter_dir(e->path); else if (picker_active()) picker_return_file(e->path); /* picker:点文件=返回路径 */ else browser_open_file(e->path); } browser_drag_reset(); return 1; } return 0; } /* 触摸拖拽滚动(滚列表不滚页;topbar 不拖)。 */ void browser_touch_down(int x, int y) { browser_drag_reset(); if (g_err != 0 || g_n == 0) return; if (meow_widget_hit(g_list, x, y)) { int row = meow_listview_hit(g_list, x, y); if (row >= 0) { g_press_row = row; meow_listview_set_sel(g_list, row); /* 按下高亮(§5.8.3 selected) */ } g_dragging = 1; g_drag_last_y = y; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } } void browser_touch_move(int x, int y) { (void)x; if (!g_dragging) return; if (g_press_row >= 0) { /* 拖动超阈值:清除按下高亮 */ int d = y - g_drag_last_y; if (d < 0) d = -d; g_press_delta += d; if (g_press_delta > 10) { g_press_row = -1; meow_listview_set_sel(g_list, -1); /* 拖动清除按下高亮 */ } } int dy = y - g_drag_last_y; g_drag_last_y = y; if (dy == 0) return; int before = meow_listview_scroll(g_list); meow_listview_scroll_by(g_list, -dy); /* set_scroll 内部 clamp 到 [0,max] */ if (meow_listview_scroll(g_list) != before) meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } void browser_swipe(int dir, int dist) { if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; if (g_err != 0 || g_n == 0) return; int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 上滑→显示更低内容 */ int before = meow_listview_scroll(g_list); meow_listview_scroll_by(g_list, del); if (meow_listview_scroll(g_list) != before) meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); } /* SDK 通用长按(§6.3,≥阈值未抬手且位移≤容忍)→ 打开该行 BottomSheet。 * App 层长按计时已删(g_press_ms/g_press_tapped 移除);长按触发后 SDK 已抑制其抬手 * TAP(不再触发行体/打开动作),故这里无需再吞后续 tap;打开前清按下高亮与拖动态。 */ void browser_on_long_press(int x, int y) { if (picker_active()) return; /* §6.2 二期:picker 模式无长按 BottomSheet */ if (g_err != 0 || g_n == 0) return; if (!meow_widget_hit(g_list, x, y)) return; int row = meow_listview_hit(g_list, x, y); if (row < 0) return; g_press_row = -1; g_dragging = 0; meow_listview_set_sel(g_list, -1); /* 清除按下高亮(sheet 打开在即) */ browser_open_sheet(row); /* 内部:写操作后自行 reload */ } /* ---- 页 view 生命周期 ---- */ static void browser_page_create(meow_view *v, void *ctx) { (void)v; ((browser_ctrl *)ctx)->view = v; r_content = app_content_rect(); r_list = AR(r_content.x, r_content.y, r_content.w, r_content.h - APP_FOOTER_H); if (r_list.h < 0) r_list.h = 0; strncpy(g_cur, FSX_SD_ROOT, sizeof g_cur - 1); g_cur[sizeof g_cur - 1] = '\0'; /* §6.2 二期:picker 模式 topbar 标题「Pick a file」且无 NEW 动作(点文件即选)。 */ if (picker_active()) g_tb = app_topbar_create("Pick a file", 1, NULL); else g_tb = app_topbar_create("Files", 1, "NEW"); /* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */ g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0, browser_src2, NULL, MEOW_LIST_SEL_SINGLE); meow_listview_set_item_h(g_list, APP_ROW_H); /* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */ g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h, browser_draw_empty, NULL); g_footer_id = meow_draw_custom_register(1, r_content.x, r_content.y + r_content.h - APP_FOOTER_H, r_content.w, APP_FOOTER_H, browser_draw_footer, NULL); g_press_row = -1; browser_reload(); /* 扫首个目录(count/scroll/sel 由 reload 写入 listview) */ } static void browser_page_enter(meow_view *v, void *ctx) { (void)v; ((browser_ctrl *)ctx)->active = 1; const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); } static void browser_page_leave(meow_view *v, void *ctx) { (void)v; ((browser_ctrl *)ctx)->active = 0; } static void browser_page_destroy(meow_view *v, void *ctx) { (void)v; (void)ctx; if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; } if (g_footer_id) { meow_draw_unregister(g_footer_id); g_footer_id = 0; } if (g_list) { meow_widget_free(g_list); g_list = NULL; } if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; } } meow_view *browser_view_create(void) { static const meow_view_hooks h = { .on_create = browser_page_create, .on_enter = browser_page_enter, .on_leave = browser_page_leave, .on_destroy = browser_page_destroy, }; return meow_view_create(ACT_BROWSER, MEOW_VIEW_ACTIVITY, &h, &g_bc); }