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

468 lines
18 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;**长按=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 <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=无);按下高亮 + 拖动识别(长按已交 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);
}