Files
MeowOS-For-R3P2/patch_v2/files_app/ui/browser.c
T
MeowOS c187b7bce3 feat(files): 新增外置系统文件管理器 os.meow.files (agent-183) — 目录浏览(虚拟化列表/空态)+文本预览(≤256KB截断)+删除/重命名/新建/属性(BottomSheet+IME);路径写操作限 TF 卡内(拒 .. 越界);多文件结构(app.c+ui/{browser,viewer}.c+ui/common/chrome.c+logic/fsx.c) 按批次②范本
files md5 6f6b0a6d; 0 warning; 宿主 test_fsx 全过(路径安全/格式化/排序/虚拟化/写操作/文本嗅探); qemu 离屏冒烟通过
SDK 缺口记录: listview 仅单行 label(本 App 用 custom 画布自绘行); common 档字库中文缺字(未声明 fonts:full)
真机待验证: 列表渲染/滚动手感/IME 重命名/弹窗交互
2026-08-31 04:03:56 +08:00

505 lines
17 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/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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
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);
}