Files
MeowOS-For-R3P2/patch_v2/files_app/ui/browser.c
T

459 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 空态码);
* - 内容区用 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(删除/重命名/新建/属性);
* - 空态/错误(无卡/空目录/权限)与底部信息行用 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 <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_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; /* 内容区结构化列表(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);
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;
}
/* ---- 列表行数据源:逐行填 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_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 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) 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 = meow_listview_hit(g_list, x, y);
if (row >= 0) {
g_press_row = row;
g_press_ms = meow_ui_time_ms();
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;
if (d > 0) 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);
}
/* 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();
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';
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);
}