/* ui/browser.c — 目录浏览页(ACT_BROWSER,根页)view + 控制器。 * * 职责: * - 持有目录态(g_cur 当前目录 / g_entries 缓存快照 / g_err 空态码 / g_scroll); * - 内容区用 meow_widget_custom_create 自绘「虚拟化列表」:只画可见行(fsx_visible_range), * 行=图标(目录/文件)+名称+大小/时间+右侧 ⋯ 操作钮;不一次全读进内存(entries 有界缓存); * - 空态/错误(无卡/空目录/权限)由 render_cb 内 app_draw_empty 画; * - 触摸:行 body=进目录/开预览;右侧操作钮或长按 → BottomSheet(删除/重命名/新建/属性); * - 滑动:LEFT=返回上级(App 层处理);UP/DOWN=列表滚动(本页); * - 写操作只经 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_scroll; /* 列表滚动(px) */ static int g_sel; /* 当前选中行(-1=无) */ static int g_press_row; /* 按下行(-1=无);长按检测 */ static uint64_t g_press_ms; static int g_press_tapped; /* 长按已消费本按 */ 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; /* 内容区虚拟化列表(custom widget) */ static app_rect_t r_content; static char g_title[FSX_MAX_PATH]; /* 预览/操作辅助(前向声明) */ static void browser_open_sheet(int row); 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_tapped = 0; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0; } /* ---- 通用文本绘制:宽度限 maxw 内截断(超出加 "...")---- */ static void browser_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) { int full = meow_ui_text_width(s, size); if (full <= maxw) { meow_ui_text(x, y, s, size, col); return; } const char *dots = "..."; int dw = meow_ui_text_width(dots, size); char tmp[FSX_MAX_NAME + 4]; int i; for (i = 0; s[i] && i < FSX_MAX_NAME - 4; i++) { tmp[i] = s[i]; tmp[i + 1] = '\0'; if (meow_ui_text_width(tmp, size) > maxw - dw) break; } if (i >= FSX_MAX_NAME - 4) i = FSX_MAX_NAME - 4; memcpy(tmp + i, dots, 4); meow_ui_text(x, y, tmp, size, col); } /* ---- 列表行图标:目录=filled 矩形+页签;文件=空心矩形+折角 ---- */ static void browser_draw_icon(const meow_theme *th, int x, int y, int is_dir, int sel) { uint32_t col = sel ? 0xFFFFFF : (is_dir ? th->primary : th->onSurfaceVariant); if (is_dir) { meow_ui_fill_rect(x, y + 6, 26, 18, col); /* 主体 */ meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */ } else { meow_ui_draw_rect(x + 4, y + 4, 18, 22, col); /* 页体 */ meow_ui_fill_rect(x + 4, y + 4, 7, 4, col); /* 文件名横条 */ } } /* 行区视口高(底部留页脚,行不压到页脚)。 */ static int browser_vp_h(void) { return r_content.h - APP_FOOTER_H; } /* ---- 列表 render_cb:虚拟化画可见行 + 空态 + 页脚 ---- */ static void browser_render(meow_widget *w, const meow_theme *th, void *ctx) { (void)w; (void)ctx; /* 空态/错误 */ if (g_err != 0) { if (g_err == FSX_ERR_NOENT) app_draw_empty(r_content, "NO STORAGE CARD", "insert a TF card / mount sd_0"); else if (g_err == FSX_ERR_EACCES) app_draw_empty(r_content, "ACCESS DENIED", "cannot read this folder"); else app_draw_empty(r_content, "CANNOT OPEN", "unexpected error"); return; } if (g_n == 0) { app_draw_empty(r_content, "EMPTY FOLDER", g_cur); app_draw_footer(r_content, "0 items"); return; } int vp = browser_vp_h(); int first, last; fsx_visible_range(g_n, APP_ROW_H, g_scroll, vp, &first, &last); int thh = meow_ui_text_height(th->font_body); int namew = r_content.w - APP_PAD - APP_ICON_W - APP_PAD - APP_OP_W - APP_PAD; /* 名称区 */ char size[32], tm[32]; for (int i = first; i < last; i++) { fsx_entry *e = &g_entries[i]; int ry = r_content.y + (i * APP_ROW_H - g_scroll); if (ry + APP_ROW_H <= r_content.y) continue; if (ry >= r_content.y + vp) break; int hilite = (i == g_sel || i == g_press_row); if (hilite) meow_ui_fill_rect(r_content.x, ry, r_content.w, APP_ROW_H, th->primary); browser_draw_icon(th, r_content.x + APP_PAD, ry + (APP_ROW_H - 24) / 2, e->is_dir, hilite); uint32_t namecol = hilite ? 0xFFFFFF : th->onSurface; uint32_t subcol = hilite ? 0xFFFFFF : th->dim; browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6, e->name, namew, th->font_body, namecol); if (e->is_dir) { fsx_fmt_time(e->mtime, tm, sizeof tm); snprintf(size, sizeof size, "Folder %s", tm); } else { fsx_fmt_size(e->size, size, sizeof size); fsx_fmt_time(e->mtime, tm, sizeof tm); snprintf(size, sizeof size, "%s %s", size, tm); } browser_text_trunc(r_content.x + APP_PAD + APP_ICON_W + APP_PAD, ry + 6 + thh + 2, size, namew, 20, subcol); /* 右侧 ⋯ 操作钮(三个点) */ int ox = r_content.x + r_content.w - APP_OP_W / 2 - 1; int oy = ry + APP_ROW_H / 2 - 8; for (int d = 0; d < 3; d++) meow_ui_fill_rect(ox, oy + d * 6, 4, 4, hilite ? 0xFFFFFF : th->onSurfaceVariant); /* 分隔线 */ int sy = ry + APP_ROW_H - 1; if (sy >= r_content.y && sy < r_content.y + r_content.h) meow_ui_fill_rect(r_content.x, sy, r_content.w, 1, th->outlineVariant); } char foot[96]; 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); } /* 列表 hit_cb:落点是否在内容区(行判定在 on_tap 里做)。 */ static int browser_hit(meow_widget *w, int x, int y, void *ctx) { (void)w; (void)ctx; return x >= r_content.x && x < r_content.x + r_content.w && y >= r_content.y && y < r_content.y + r_content.h; } /* ---- 导航 ---- */ 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; g_scroll = 0; g_sel = -1; browser_update_title(); const meow_theme *th = meow_theme_get(); meow_ui_set_page_bg(th->bg); meow_ui_paint_full(); } 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_press_tapped) { browser_drag_reset(); return 1; } 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 op = (x >= r_content.x + r_content.w - APP_OP_W); int row = (y - r_content.y + g_scroll) / APP_ROW_H; if (row < 0 || row >= g_n) { browser_drag_reset(); return 1; } g_sel = row; if (op) browser_open_sheet(row); else { fsx_entry *e = &g_entries[row]; if (e->is_dir) browser_enter_dir(e->path); 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 = (y - r_content.y + g_scroll) / APP_ROW_H; if (row >= 0 && row < g_n) { g_press_row = row; g_press_ms = meow_ui_time_ms(); } g_dragging = 1; g_drag_last_y = y; meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.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; if (d > 0) g_press_delta += d; if (g_press_delta > 10) g_press_row = -1; } int dy = y - g_drag_last_y; g_drag_last_y = y; if (dy == 0) return; int ns = g_scroll - dy; int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h()); if (ns < 0) ns = 0; if (ns > ms) ns = ms; if (ns != g_scroll) { g_scroll = ns; meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.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 ns = g_scroll + del; int ms = fsx_max_scroll(g_n, APP_ROW_H, browser_vp_h()); if (ns < 0) ns = 0; if (ns > ms) ns = ms; if (ns != g_scroll) { g_scroll = ns; meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h); } } /* on_tick:长按(≥500ms 未拖动)→ 打开该行 BottomSheet。 */ void browser_tick(uint64_t now) { if (g_press_row >= 0 && !g_press_tapped && now - g_press_ms >= 500) { g_press_tapped = 1; int row = g_press_row; g_press_row = -1; 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(); strncpy(g_cur, FSX_SD_ROOT, sizeof g_cur - 1); g_cur[sizeof g_cur - 1] = '\0'; g_tb = app_topbar_create("Files", 1, "NEW"); g_list = meow_widget_custom_create(r_content.x, r_content.y, r_content.w, r_content.h, browser_render, browser_hit, NULL); g_sel = -1; g_press_row = -1; browser_reload(); } 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_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); }