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 重命名/弹窗交互
This commit is contained in:
1 parent
b5038f8673
commit
c187b7bce3
12 files changed
+1741
No files matched your search
@@ -0,0 +1,87 @@
|
|||||||
|
# os.meow.files — MeowOS 外置文件管理器
|
||||||
|
|
||||||
|
TF 卡应用(`framework=1` 走 `meow_ui` SDK)。浏览 TF 卡目录树,支持进/退目录、列表虚拟化、
|
||||||
|
行操作(删除/重命名/新建目录/属性)、文本预览(≤256KB 截断)、空态/错误页;写操作严格限制在
|
||||||
|
TF 卡内(`/usr/data/mnt/sd_0`),拒绝 `..` 越界与卡外路径。
|
||||||
|
|
||||||
|
## 目录结构(多文件,批次②拆分范本)
|
||||||
|
|
||||||
|
```
|
||||||
|
patch_v2/files_app/
|
||||||
|
├── app.c # main + 装配 + 事件分发(key/tap/swipe/tick/touch → 当前页)
|
||||||
|
├── files_internal.h # 跨 TU 共享头(Activity id / 布局 / chrome/browser/viewer 声明)
|
||||||
|
├── build_files.sh # 交叉编译脚本(zig-mipsel-cc 静态链 meow_ui.a libz.a -lpthread)
|
||||||
|
├── app.json # {"name":"Files","exec":".../os.meow.files/files",...,"framework":1,"immersive":1}
|
||||||
|
├── README.md
|
||||||
|
├── ui/
|
||||||
|
│ ├── browser.c # 目录浏览页(ACT_BROWSER 根页):虚拟化列表 + 空态 + 行操作
|
||||||
|
│ ├── viewer.c # 文本预览页(ACT_VIEWER 子页):只读、可滚动、≤256KB 截断
|
||||||
|
│ └── common/
|
||||||
|
│ └── chrome.c # 共享 chrome:topbar 建置 / 内容区几何 / 空态&页脚绘制
|
||||||
|
├── logic/
|
||||||
|
│ ├── fsx.h # 纯业务接口(不依赖 meow_ui)
|
||||||
|
│ └── fsx.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
|
||||||
|
└── test/
|
||||||
|
└── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd patch_v2/files_app && ./build_files.sh
|
||||||
|
# 产物:files(MIPS32r2 静态 ELF,~270KB)+ ./files + app.json
|
||||||
|
```
|
||||||
|
|
||||||
|
构建脚本仿 `hello_app/build_hello.sh`:`zig-mipsel-cc -Os -static -Wall -Wextra -I../sdk -I.`
|
||||||
|
链 `../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread`。**零触及 `patch_v2/sdk/`**(未新增/改任何 SDK 文件);
|
||||||
|
`-Wall -Wextra` 0 warning。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/data/mnt/sd_0/MeowAPPs/os.meow.files/
|
||||||
|
├── app.json
|
||||||
|
└── files
|
||||||
|
```
|
||||||
|
|
||||||
|
数据目录 `MeowData/os.meow.files/{data,cache}` 由 launcher 经 `MEOW_DATA_DIR`/`MEOW_CACHE_DIR` 注入;
|
||||||
|
v1 无需持久化,暂未使用(启动恒进卡根 `/usr/data/mnt/sd_0/`)。
|
||||||
|
|
||||||
|
## 宿主自测(不跨编/不刷机)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd patch_v2/files_app
|
||||||
|
gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||||
|
test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
|
||||||
|
```
|
||||||
|
|
||||||
|
覆盖:路径安全(`within_sd`/`join`/`sanitize`/`dirname`/`basename`)、展示格式化(`fmt_size`/`fmt_time`)、
|
||||||
|
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
|
||||||
|
`delete`,含卡外/越界拒绝)、非文本写拒绝。宿主全 PASS。
|
||||||
|
|
||||||
|
## 交互
|
||||||
|
|
||||||
|
- 触摸:行 body=进目录/开文本预览;右侧 `⋯` 或长按→BottomSheet(删除[确认弹窗]/重命名[IME]/新建目录[IME]/属性[大小·mtime]);
|
||||||
|
TopAppBar `NEW`=当前目录新建。
|
||||||
|
- 左缘左滑/返回钮:预览页→回目录;浏览器→上级目录;卡根再左滑=退出 App。
|
||||||
|
- 物理键不映射(APPS.md §3.4.1):息屏/音量/媒体语义归系统/SDK。
|
||||||
|
|
||||||
|
## 已知限制 / SDK 能力缺口(未改 SDK,如需请走 agent 立项)
|
||||||
|
|
||||||
|
1. **SDK `meow_lazycolumn` 仅渲染单行 label**(`meow_list_source` 填文本),无法表达「图标+名称+大小/时间+右侧操作钮」
|
||||||
|
的行布局。本 App 用 `meow_widget_custom_create` 自绘虚拟化列表满足该行版式;`fsx_scan_dir` 仍以有界数组缓存
|
||||||
|
(`FSX_MAX_ENTRIES=1024`)承载数据,列表虚拟化只画可见行(`fsx_visible_range`)——大目录不一次全读进内存。
|
||||||
|
2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。
|
||||||
|
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
|
||||||
|
3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。
|
||||||
|
超 256KB 只读前 256KB,页脚标注 `>256KB (truncated)`。
|
||||||
|
4. **符号链接不追链**:v1 视 symlink 为普通文件展示/操作(lstat),避免链出卡外。
|
||||||
|
5. **单目录条目上限**:>1024 项时只展示前 1024 并在页脚标注 `showing first 1024`(分页流式读目录为二期)。
|
||||||
|
|
||||||
|
## 验证状态
|
||||||
|
|
||||||
|
- 编译:`-Wall -Wextra` 0 warning/0 error,产物 `files`(MIPS32r2 静态 ELF)。
|
||||||
|
- 宿主:`test/test_fsx.c` 全 PASS。
|
||||||
|
- qemu-mipsel(无 /dev/fb0 → SDK 离屏 480×720):正常运行到事件循环,导出离屏帧,
|
||||||
|
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
|
||||||
|
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* app.c — files 应用装配层:main + 组装 + 事件分发。
|
||||||
|
*
|
||||||
|
* 职责(对齐 hello_app/app.c):main 只做初始化/建页/跑事件循环/清理;把 meow_ui_run 的平铺输入
|
||||||
|
* 回调(key/tap/swipe/tick/touch)路由到当前活动页(browser/viewer)。页面切换统一走
|
||||||
|
* meow_view_page_push/pop(活动栈 + on_enter/on_leave 唯一驱动)。
|
||||||
|
*
|
||||||
|
* 铁律(APPS.md §3.4.1 / WORK_STATE.md):物理键禁映射——不处理任何 key 语义;返回=左缘左滑
|
||||||
|
* (on_swipe LEFT)/返回钮/meow_ui_request_exit。目录进退不换页(browser 改 cur_dir 重扫);
|
||||||
|
* 文本预览换页(push ACT_VIEWER)。退出=在根目录左滑或 SIGTERM(看门狗)。
|
||||||
|
*/
|
||||||
|
#include "files_internal.h"
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
static meow_view *g_browser_view;
|
||||||
|
static meow_view *g_viewer_view;
|
||||||
|
|
||||||
|
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 事件分发:meow_ui_run 平铺回调 → 当前页 */
|
||||||
|
/* ============================================================ */
|
||||||
|
|
||||||
|
/* 物理键系统语义(息屏/音量/媒体)由 SDK 消化;App 不映射(§3.4.1)。 */
|
||||||
|
static void app_on_key(int keycode, int is_down)
|
||||||
|
{
|
||||||
|
(void)keycode; (void)is_down;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_on_tap(int x, int y)
|
||||||
|
{
|
||||||
|
if (viewer_is_active()) { viewer_on_tap(x, y); return; }
|
||||||
|
if (browser_is_active()) { browser_on_tap(x, y); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_on_swipe(int dir, int dx, int dy)
|
||||||
|
{
|
||||||
|
(void)dx;
|
||||||
|
if (dir == MEOW_SWIPE_LEFT) {
|
||||||
|
/* 左缘返回:预览页→回目录;浏览器→上级目录;到根→退出 App。 */
|
||||||
|
if (viewer_is_active()) { meow_view_page_pop(); return; }
|
||||||
|
if (browser_is_active() && browser_go_up()) return;
|
||||||
|
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||||
|
meow_ui_request_exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (viewer_is_active()) { viewer_swipe(dir, dy); return; }
|
||||||
|
if (browser_is_active()) { browser_swipe(dir, dy); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左缘返回手势过程视觉条(SDK 自动画顶部进度条,App 转发)。 */
|
||||||
|
static void app_on_swipe_progress(int x_start, int dx)
|
||||||
|
{
|
||||||
|
meow_ui_swipe_hint(x_start + dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽过程(跟手):列表/文本内容滚动。 */
|
||||||
|
static void app_on_touch_down(int x, int y)
|
||||||
|
{
|
||||||
|
if (viewer_is_active()) viewer_touch_down(x, y);
|
||||||
|
else if (browser_is_active()) browser_touch_down(x, y);
|
||||||
|
}
|
||||||
|
static void app_on_touch_move(int x, int y)
|
||||||
|
{
|
||||||
|
if (viewer_is_active()) viewer_touch_move(x, y);
|
||||||
|
else if (browser_is_active()) browser_touch_move(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* on_tick:长按检测(browser 页)。 */
|
||||||
|
static void app_on_tick(void)
|
||||||
|
{
|
||||||
|
if (browser_is_active()) browser_tick(meow_ui_time_ms());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* main */
|
||||||
|
/* ============================================================ */
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
meow_log(MEOW_LOG_INFO, "start");
|
||||||
|
meow_ui_opts o = { APP_W, APP_H };
|
||||||
|
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
|
||||||
|
meow_theme_add_listener(NULL);
|
||||||
|
|
||||||
|
signal(SIGTERM, on_sigterm);
|
||||||
|
|
||||||
|
/* 建两页 view(on_create 建各自控件/状态 + 读卡扫首个目录) */
|
||||||
|
g_browser_view = browser_view_create();
|
||||||
|
g_viewer_view = viewer_view_create();
|
||||||
|
|
||||||
|
/* 进入根页(ACT_BROWSER = 目录浏览;默认卡根 /usr/data/mnt/sd_0) */
|
||||||
|
meow_view_page_push(ACT_BROWSER);
|
||||||
|
|
||||||
|
meow_ui_events ev = {
|
||||||
|
app_on_key, app_on_tap, app_on_swipe, app_on_tick, app_on_swipe_progress,
|
||||||
|
app_on_touch_down, app_on_touch_move,
|
||||||
|
};
|
||||||
|
meow_ui_run(&ev);
|
||||||
|
|
||||||
|
/* 清理:meow_view_free 触发各页 on_destroy(控件 + 文本缓冲) */
|
||||||
|
meow_view_free(g_viewer_view);
|
||||||
|
meow_view_free(g_browser_view);
|
||||||
|
meow_ui_shutdown();
|
||||||
|
meow_log(MEOW_LOG_INFO, "exit");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"name":"Files","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.files/files","package":"os.meow.files","version":"0.1","framework":1,"immersive":1,"author":"MeowOS"}
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the Files app (MeowOS 外置文件管理器 os.meow.files,SDK framework=1):
|
||||||
|
# 链 meow_ui.a(已内含 meow_font.o)+ 系统 libz.a + -lpthread,静态 MIPS32r2 (musl sf)。
|
||||||
|
# 多文件结构(仿 hello_app 批次②拆分):app.c + ui/browser.c + ui/viewer.c +
|
||||||
|
# ui/common/chrome.c + logic/fsx.c。仅改本目录文件,编译期不改 SDK(patch_v2/sdk/ 零触及)。
|
||||||
|
# mipsel -Wall -Wextra 0 warning。
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
ROOT="$HOME/Project/R3ProII"
|
||||||
|
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||||
|
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||||
|
"$CC" -Os -static -Wall -Wextra -I../sdk -I. \
|
||||||
|
app.c ui/browser.c ui/viewer.c ui/common/chrome.c logic/fsx.c \
|
||||||
|
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o files
|
||||||
|
ls -la files
|
||||||
|
file files
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
/* files_internal.h — files 应用内部共享头(UI 各页 + app 装配层 + 宿主测试面)。
|
||||||
|
*
|
||||||
|
* 层级(对齐 hello_app 的批次②拆分规范):
|
||||||
|
* - ui/browser.c 目录浏览页(ACT_BROWSER,根页):列表虚拟化渲染 + 进/退目录 + 行操作
|
||||||
|
* - ui/viewer.c 文本预览页(ACT_VIEWER,子页):只读、可滚动、≤256KB 截断
|
||||||
|
* - ui/common/chrome.c 共享 chrome(TopAppBar 建置 / 内容区几何 / 空态/页脚绘制)
|
||||||
|
* - logic/fsx.* 纯业务(路径安全/格式化/枚举/写操作),不依赖 meow_ui
|
||||||
|
* - app.c main + 装配 + 事件分发(把 meow_ui_run 的平铺回调路由到各页)
|
||||||
|
*
|
||||||
|
* 页面生命周期用 meow_view_page_push/pop 驱动 on_enter/on_leave;目录进退不换页(留在 browser
|
||||||
|
* 页改 cur_dir 重扫)。物理键不映射(§3.4.1):本 App 不做任何 on_key 映射,返回=左缘手势/返回钮/
|
||||||
|
* request_exit。
|
||||||
|
*/
|
||||||
|
#ifndef FILES_INTERNAL_H
|
||||||
|
#define FILES_INTERNAL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "meow_ui.h"
|
||||||
|
#include "meow_ui_view.h"
|
||||||
|
#include "logic/fsx.h"
|
||||||
|
|
||||||
|
/* ---- Activity 归属(§6.1 ①)---- */
|
||||||
|
#define ACT_BROWSER 0
|
||||||
|
#define ACT_VIEWER 1
|
||||||
|
|
||||||
|
/* ---- 布局 ---- */
|
||||||
|
#define APP_W 480
|
||||||
|
#define APP_H 720
|
||||||
|
#define APP_TOPBAR_H 56
|
||||||
|
#define APP_ROW_H 56 /* 列表行高 */
|
||||||
|
#define APP_PAD 16
|
||||||
|
#define APP_ICON_W 26 /* 图标占宽 */
|
||||||
|
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
|
||||||
|
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
|
||||||
|
#define APP_READ_MAX (256 * 1024) /* 文本预览读取上限 */
|
||||||
|
|
||||||
|
/* ---- App 级复用矩形/几何 ---- */
|
||||||
|
typedef struct { int x, y, w, h; } app_rect_t;
|
||||||
|
#define AR(x,y,w,h) ((app_rect_t){ (x),(y),(w),(h) })
|
||||||
|
|
||||||
|
/* ---- chrome(ui/common/chrome.c)---- */
|
||||||
|
int app_statusbar(void); /* 状态栏高度 inset(0=无) */
|
||||||
|
app_rect_t app_topbar_rect(void); /* 顶部栏几何(含状态栏偏移) */
|
||||||
|
app_rect_t app_content_rect(void); /* topbar 之下 → 屏底(列表/文本内容区) */
|
||||||
|
/* 建 topbar(owner=当前活动:调用方须在正确 activity 下建);返回 widget 供命中/置顶。 */
|
||||||
|
meow_widget *app_topbar_create(const char *title, int back, const char *action_label);
|
||||||
|
void app_draw_empty(app_rect_t r, const char *kind, const char *what); /* 空态/错误居中 */
|
||||||
|
void app_draw_footer(app_rect_t r, const char *text); /* 底部信息(项数/路径) */
|
||||||
|
|
||||||
|
/* ---- browser 页(ui/browser.c)---- */
|
||||||
|
meow_view *browser_view_create(void);
|
||||||
|
int browser_is_active(void);
|
||||||
|
int browser_on_tap(int x, int y); /* 1=已消费 */
|
||||||
|
void browser_swipe(int dir, int dist);
|
||||||
|
void browser_touch_down(int x, int y);
|
||||||
|
void browser_touch_move(int x, int y);
|
||||||
|
void browser_tick(uint64_t now);
|
||||||
|
int browser_go_up(void); /* 1=回到上级;0=已在根(未动作) */
|
||||||
|
int browser_at_root(void);
|
||||||
|
void browser_reload(void);
|
||||||
|
const char *browser_current_dir(void);
|
||||||
|
int browser_entry_count(void); /* 宿主测试:当前目录项数 */
|
||||||
|
|
||||||
|
/* ---- viewer 页(ui/viewer.c)---- */
|
||||||
|
meow_view *viewer_view_create(void);
|
||||||
|
int viewer_is_active(void);
|
||||||
|
int viewer_open_file(const char *path); /* 读文件+进预览页;0=成功 */
|
||||||
|
void viewer_swipe(int dir, int dist);
|
||||||
|
void viewer_touch_down(int x, int y);
|
||||||
|
void viewer_touch_move(int x, int y);
|
||||||
|
int viewer_on_tap(int x, int y);
|
||||||
|
void viewer_load(const char *path); /* 宿主测试:读入 buffer 并解析行 */
|
||||||
|
|
||||||
|
#endif /* FILES_INTERNAL_H */
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/* logic/fsx.c — 文件管理器业务逻辑实现(纯 C:仅 libc + 标准头,无 meow_ui 依赖)。
|
||||||
|
*
|
||||||
|
* 实现要点:
|
||||||
|
* - 所有写操作(delete/rename/mkdir)先 fsx_within_sd() 复核目标路径,拒绝 ".." 越界与卡外路径;
|
||||||
|
* - 路径一律经 fsx_join() 由「已验证 base + sanitize 后的 name」构造,杜绝手工拼串引入穿越;
|
||||||
|
* - 目录枚举用 bounded 数组(FSX_MAX_ENTRIES)+ 客户端虚拟化只画可见行——不一次全读进内存;
|
||||||
|
* - 符号链接 v1 不追(is_dir=0,作普通文件展示),防止链出卡外。
|
||||||
|
*/
|
||||||
|
#include "fsx.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* ---- 路径安全 ---- */
|
||||||
|
|
||||||
|
int fsx_within_sd(const char *path)
|
||||||
|
{
|
||||||
|
if (!path) return 0;
|
||||||
|
size_t rl = strlen(FSX_SD_ROOT);
|
||||||
|
if (strncmp(path, FSX_SD_ROOT, rl) != 0) return 0;
|
||||||
|
char c = path[rl];
|
||||||
|
if (c != '\0' && c != '/') return 0; /* "…/sd_0evil" 这类前缀伪装拒绝 */
|
||||||
|
|
||||||
|
/* 逐 '/'-分隔分量扫描:出现 ".." 即拒绝(防 "../../" 逃逸)。 */
|
||||||
|
const char *p = path;
|
||||||
|
while (*p) {
|
||||||
|
while (*p == '/') p++;
|
||||||
|
if (!*p) break;
|
||||||
|
const char *start = p;
|
||||||
|
while (*p && *p != '/') p++;
|
||||||
|
size_t len = (size_t)(p - start);
|
||||||
|
if (len == 2 && start[0] == '.' && start[1] == '.') return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_is_root(const char *path)
|
||||||
|
{
|
||||||
|
return path && strcmp(path, FSX_SD_ROOT) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_dirname(const char *path, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!path || !out || outsz <= 0) return -1;
|
||||||
|
if (fsx_is_root(path)) {
|
||||||
|
size_t rl = strlen(FSX_SD_ROOT);
|
||||||
|
if ((int)rl >= outsz) return -1;
|
||||||
|
memcpy(out, FSX_SD_ROOT, rl + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const char *slash = strrchr(path, '/');
|
||||||
|
if (!slash) return -1; /* 非法相对路径 */
|
||||||
|
size_t n = (size_t)(slash - path);
|
||||||
|
if (n == 0) n = 1; /* "/xxx" → "/"(一般不出现) */
|
||||||
|
if ((int)(n + 1) > outsz) return -1;
|
||||||
|
memcpy(out, path, n);
|
||||||
|
out[n] = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *fsx_basename(const char *path)
|
||||||
|
{
|
||||||
|
if (!path) return "";
|
||||||
|
const char *slash = strrchr(path, '/');
|
||||||
|
return slash ? slash + 1 : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_join(const char *base, const char *name, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!base || !name || !out || outsz <= 0) return -1;
|
||||||
|
/* name 必须是被清洗后的单分量:无 '/'、非 ".."、非空。 */
|
||||||
|
if (!name[0] || name[0] == '/' || strcmp(name, "..") == 0 || strchr(name, '/')) return -1;
|
||||||
|
|
||||||
|
size_t bl = strlen(base);
|
||||||
|
/* base 尾随 '/' 去掉,但保持根 "/usr/data/mnt/sd_0" 不截成 ""。 */
|
||||||
|
char b[FSX_MAX_PATH];
|
||||||
|
if (bl >= sizeof b) return -1;
|
||||||
|
memcpy(b, base, bl + 1);
|
||||||
|
while (bl > 0 && b[bl - 1] == '/' && bl != strlen(FSX_SD_ROOT)) b[--bl] = '\0';
|
||||||
|
|
||||||
|
int n = snprintf(out, (size_t)outsz, "%s/%s", b, name);
|
||||||
|
if (n < 0 || n >= outsz) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_sanitize_name(const char *name, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!name || !out || outsz <= 0) return -1;
|
||||||
|
const char *s = name;
|
||||||
|
/* 跳过前导空白 */
|
||||||
|
while (*s == ' ' || *s == '\t') s++;
|
||||||
|
if (!*s) return -1; /* 全空白 */
|
||||||
|
if (strlen(s) > 120) return -1; /* 过长 */
|
||||||
|
if (s[0] == '.') return -1; /* 隐藏/点位起始拒 */
|
||||||
|
for (const char *p = s; *p; p++) {
|
||||||
|
unsigned char ch = (unsigned char)*p;
|
||||||
|
if (ch < 0x20) return -1; /* 控制字符 */
|
||||||
|
if (*p == '/' || *p == '\\' || *p == ':') return -1;
|
||||||
|
if (p[0] == '.' && p[1] == '.') return -1; /* ".." 分量拒 */
|
||||||
|
}
|
||||||
|
size_t n = strlen(s);
|
||||||
|
/* 去尾随空白 */
|
||||||
|
while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t')) n--;
|
||||||
|
if (n == 0) return -1;
|
||||||
|
if ((int)n + 1 > outsz) return -1;
|
||||||
|
memcpy(out, s, n);
|
||||||
|
out[n] = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 展示格式化 ---- */
|
||||||
|
|
||||||
|
void fsx_fmt_size(long long size, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return;
|
||||||
|
if (size < 0) size = 0;
|
||||||
|
const char *u[] = { "B", "KB", "MB", "GB" };
|
||||||
|
double v = (double)size;
|
||||||
|
int i = 0;
|
||||||
|
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
|
||||||
|
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
|
||||||
|
else snprintf(out, (size_t)outsz, "%.1f %s", v, u[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsx_fmt_time(long long mtime, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz <= 0) return;
|
||||||
|
time_t t = (time_t)mtime;
|
||||||
|
struct tm tm;
|
||||||
|
if (!localtime_r(&t, &tm)) { snprintf(out, (size_t)outsz, "-"); return; }
|
||||||
|
strftime(out, (size_t)outsz, "%Y-%m-%d %H:%M", &tm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 目录枚举 / 排序 / 虚拟化 ---- */
|
||||||
|
|
||||||
|
static int entry_cmp(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
const fsx_entry *ea = (const fsx_entry *)a;
|
||||||
|
const fsx_entry *eb = (const fsx_entry *)b;
|
||||||
|
if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; /* 目录优先 */
|
||||||
|
return strcmp(ea->name, eb->name); /* 名称升序 */
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsx_sort_entries(fsx_entry *entries, int n)
|
||||||
|
{
|
||||||
|
if (n > 1) qsort(entries, (size_t)n, sizeof *entries, entry_cmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_scan_dir(const char *path, fsx_entry *entries, int max, int *out_count)
|
||||||
|
{
|
||||||
|
if (out_count) *out_count = 0;
|
||||||
|
if (!path) return FSX_ERR_NOENT;
|
||||||
|
DIR *d = opendir(path);
|
||||||
|
if (!d) return -errno; /* ENOENT(-2)/EACCES(-13)/ENOTDIR(-20) */
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
struct dirent *de;
|
||||||
|
char jbuf[FSX_MAX_PATH];
|
||||||
|
while ((de = readdir(d)) != NULL) {
|
||||||
|
if (de->d_name[0] == '.') continue; /* 跳过 . .. 及隐藏项 */
|
||||||
|
if (n >= max) break; /* 超大目录截断(客户端标注) */
|
||||||
|
fsx_entry *e = &entries[n];
|
||||||
|
memset(e, 0, sizeof *e);
|
||||||
|
if (fsx_join(path, de->d_name, jbuf, sizeof jbuf) != 0) continue;
|
||||||
|
strncpy(e->name, de->d_name, FSX_MAX_NAME - 1);
|
||||||
|
e->name[FSX_MAX_NAME - 1] = '\0';
|
||||||
|
strncpy(e->path, jbuf, FSX_MAX_PATH - 1);
|
||||||
|
e->path[FSX_MAX_PATH - 1] = '\0';
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(jbuf, &st) == 0) {
|
||||||
|
e->is_link = S_ISLNK(st.st_mode);
|
||||||
|
if (S_ISDIR(st.st_mode)) { e->is_dir = 1; }
|
||||||
|
else { e->is_dir = 0; e->size = st.st_size; }
|
||||||
|
e->mtime = st.st_mtime;
|
||||||
|
} else {
|
||||||
|
e->is_dir = 0; e->size = 0; e->mtime = 0;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
fsx_sort_entries(entries, n);
|
||||||
|
if (out_count) *out_count = n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_max_scroll(int count, int item_h, int viewport_h)
|
||||||
|
{
|
||||||
|
int m = count * item_h - viewport_h;
|
||||||
|
return m > 0 ? m : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsx_visible_range(int count, int item_h, int scroll, int viewport_h,
|
||||||
|
int *first, int *last)
|
||||||
|
{
|
||||||
|
if (first) *first = 0;
|
||||||
|
if (last) *last = 0;
|
||||||
|
if (count <= 0 || item_h <= 0 || viewport_h <= 0) return;
|
||||||
|
if (scroll < 0) scroll = 0;
|
||||||
|
int f = scroll / item_h;
|
||||||
|
if (f < 0) f = 0;
|
||||||
|
if (f > count) f = count;
|
||||||
|
int l = (scroll + viewport_h) / item_h + 1;
|
||||||
|
if (l > count) l = count;
|
||||||
|
if (first) *first = f;
|
||||||
|
if (last) *last = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 写/查操作 ---- */
|
||||||
|
|
||||||
|
int fsx_delete(const char *path)
|
||||||
|
{
|
||||||
|
if (!path || !fsx_within_sd(path)) return -EACCES;
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(path, &st) != 0) return -errno;
|
||||||
|
if (S_ISDIR(st.st_mode)) { return rmdir(path) == 0 ? 0 : -errno; }
|
||||||
|
return unlink(path) == 0 ? 0 : -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_rename_into(const char *path, const char *new_name)
|
||||||
|
{
|
||||||
|
if (!path || !fsx_within_sd(path)) return -EACCES;
|
||||||
|
char nb[FSX_MAX_NAME], db[FSX_MAX_PATH], np[FSX_MAX_PATH];
|
||||||
|
if (fsx_sanitize_name(new_name, nb, sizeof nb) != 0) return -EINVAL;
|
||||||
|
if (fsx_dirname(path, db, sizeof db) != 0) return -EINVAL;
|
||||||
|
if (fsx_join(db, nb, np, sizeof np) != 0) return -EINVAL;
|
||||||
|
if (!fsx_within_sd(np)) return -EACCES;
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(np, &st) == 0) return -EEXIST; /* 目标已存在,拒绝覆盖 */
|
||||||
|
return rename(path, np) == 0 ? 0 : -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_mkdir(const char *base, const char *name)
|
||||||
|
{
|
||||||
|
if (!base || !fsx_within_sd(base)) return -EACCES;
|
||||||
|
char nb[FSX_MAX_NAME], np[FSX_MAX_PATH];
|
||||||
|
if (fsx_sanitize_name(name, nb, sizeof nb) != 0) return -EINVAL;
|
||||||
|
if (fsx_join(base, nb, np, sizeof np) != 0) return -EINVAL;
|
||||||
|
if (!fsx_within_sd(np)) return -EACCES;
|
||||||
|
return mkdir(np, 0777) == 0 ? 0 : -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_stat_path(const char *path, fsx_entry *out)
|
||||||
|
{
|
||||||
|
if (!path || !out) return -EINVAL;
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(path, &st) != 0) return -errno;
|
||||||
|
memset(out, 0, sizeof *out);
|
||||||
|
strncpy(out->name, fsx_basename(path), FSX_MAX_NAME - 1);
|
||||||
|
out->name[FSX_MAX_NAME - 1] = '\0';
|
||||||
|
strncpy(out->path, path, FSX_MAX_PATH - 1);
|
||||||
|
out->path[FSX_MAX_PATH - 1] = '\0';
|
||||||
|
out->is_link = S_ISLNK(st.st_mode);
|
||||||
|
out->is_dir = S_ISDIR(st.st_mode);
|
||||||
|
out->size = st.st_size;
|
||||||
|
out->mtime = st.st_mtime;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsx_is_text_path(const char *path)
|
||||||
|
{
|
||||||
|
if (!path) return 0;
|
||||||
|
const char *dot = strrchr(path, '.');
|
||||||
|
if (!dot) return 0;
|
||||||
|
static const char *exts[] = {
|
||||||
|
".txt", ".log", ".md", ".json", ".ini", ".cfg", ".conf", ".csv", ".html",
|
||||||
|
".xml", ".srt", ".lrc", ".sh", ".c", ".h", ".py", ".js", ".ts", ".css",
|
||||||
|
".nfo", ".m3u", ".m3u8", ".list", ".license", ".readme", ".gitignore", NULL
|
||||||
|
};
|
||||||
|
for (int i = 0; exts[i]; i++)
|
||||||
|
if (strcasecmp(dot, exts[i]) == 0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/* logic/fsx.h — 文件管理器业务逻辑接口(纯 C,不依赖 meow_ui.h / meow_view)。
|
||||||
|
*
|
||||||
|
* 分层契约(沿用 hello_app/logic/logic.h 的约定):
|
||||||
|
* - UI→业务 = 纯函数命令 + 查询(fsx_*);业务不回调 UI,UI 按返回值/err 自绘。
|
||||||
|
* - 本模块只含 libc + 标准头,因此可宿主编译(gcc 直接测),也可交叉编译进 MIPS 应用。
|
||||||
|
*
|
||||||
|
* 宿主可验证的纯逻辑(test/test_fsx.c 驱动):
|
||||||
|
* - 路径安全:fsx_within_sd / fsx_join / fsx_sanitize_name / fsx_is_root / fsx_basename
|
||||||
|
* - 展示格式化:fsx_fmt_size / fsx_fmt_time
|
||||||
|
* - 列表数据源:fsx_visible_range(虚拟化可见行)/ fsx_sort_entries(目录优先)
|
||||||
|
* - 目录枚举/写操作:fsx_scan_dir / fsx_delete / fsx_rename_into / fsx_mkdir / fsx_stat_path
|
||||||
|
* (这些做真 IO,仍可宿主测:指向临时目录)
|
||||||
|
*/
|
||||||
|
#ifndef FSX_H
|
||||||
|
#define FSX_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* TF 卡根(挂载点)。写操作只允许在它之下。
|
||||||
|
* 允许编译期 -DFSX_SD_ROOT=... 覆写(宿主测试指向临时目录,便于测写操作)。 */
|
||||||
|
#ifndef FSX_SD_ROOT
|
||||||
|
#define FSX_SD_ROOT "/usr/data/mnt/sd_0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define FSX_MAX_NAME 256 /* 文件名/路径分量上限(exFAT 255 字节)×... 保守 */
|
||||||
|
#define FSX_MAX_PATH 512 /* 一条完整路径上限 */
|
||||||
|
#define FSX_MAX_ENTRIES 1024/* 单目录缓存项上限(大目录截断 + 列表虚拟化只画可见行) */
|
||||||
|
|
||||||
|
/* 返回码(等价负 errno;0=成功)。与 errno 数值一致,便于 App 判空态/权限态。 */
|
||||||
|
#define FSX_ERR_IO (-1)
|
||||||
|
#define FSX_ERR_NOENT (-2) /* 路径不存在(卡不在/目录被移除) */
|
||||||
|
#define FSX_ERR_EACCES (-13) /* 权限拒绝 */
|
||||||
|
#define FSX_ERR_ENOTDIR (-20) /* 不是目录 */
|
||||||
|
|
||||||
|
/* 单个目录项快照(App 持有数组;fsx_scan_dir 填充)。 */
|
||||||
|
typedef struct fsx_entry {
|
||||||
|
char name[FSX_MAX_NAME]; /* 文件名(不含路径) */
|
||||||
|
char path[FSX_MAX_PATH]; /* 绝对路径(在卡内,fsx_join 构造) */
|
||||||
|
int is_dir; /* 1=目录 */
|
||||||
|
int is_link; /* 1=符号链接(v1 视为文件,不追链) */
|
||||||
|
long long size; /* 字节(目录=0) */
|
||||||
|
long long mtime; /* 修改时间(Unix 秒) */
|
||||||
|
} fsx_entry;
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 路径安全(纯逻辑,写操作红线) */
|
||||||
|
/* ============================================================ */
|
||||||
|
|
||||||
|
/* 1=path 严格落在 FSX_SD_ROOT 之下(或等于根),且不含任何 ".." 分量。
|
||||||
|
* 拒绝 "/usr/data/mnt/sd_0evil" 这类前缀伪装,也拒绝 "../" 越界。 */
|
||||||
|
int fsx_within_sd(const char *path);
|
||||||
|
|
||||||
|
/* 1=path 精确等于 FSX_SD_ROOT。 */
|
||||||
|
int fsx_is_root(const char *path);
|
||||||
|
|
||||||
|
/* 安全拼接:base + "/" + name → out。name 须无 '/'、非 ".."、非空(通常已 sanitize)。
|
||||||
|
* 返回 0=成功;-1=参数非法/输出超长。 */
|
||||||
|
int fsx_join(const char *base, const char *name, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 校验+清洗新建/重命名名(供 mkdir/rename):拒绝空/超长/含 '/'、'\\'、':'、以 '.' 开头、
|
||||||
|
* 含 ".."、含控制字符。清洗后拷入 out 并去尾随空白。返回 0=合法;负数=非法(-EINVAL)。 */
|
||||||
|
int fsx_sanitize_name(const char *name, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 返回 path 最后一个 '/' 之后的分量(借用指针,不改写)。 */
|
||||||
|
const char *fsx_basename(const char *path);
|
||||||
|
|
||||||
|
/* 拷贝 path 的目录部分(去掉最后分量)到 out;根目录返回 FSX_SD_ROOT。返回 0=成功。 */
|
||||||
|
int fsx_dirname(const char *path, char *out, int outsz);
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 展示格式化(纯逻辑) */
|
||||||
|
/* ============================================================ */
|
||||||
|
|
||||||
|
/* "123 B" / "1.2 KB" / "3.4 MB" / "1.2 GB"。 */
|
||||||
|
void fsx_fmt_size(long long size, char *out, int outsz);
|
||||||
|
|
||||||
|
/* "YYYY-MM-DD HH:MM"(本地时区)。 */
|
||||||
|
void fsx_fmt_time(long long mtime, char *out, int outsz);
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 目录枚举 / 排序 / 虚拟化(列表数据源) */
|
||||||
|
/* ============================================================ */
|
||||||
|
|
||||||
|
/* 扫 path 目录到 entries[0..max),目录优先 + 名称升序。*out_count=读到的项数。
|
||||||
|
* 超过 max 截断(大目录:只缓存前 max 项,配合虚拟化渲染)。
|
||||||
|
* 返回 0=成功;<0=负 errno(-2=无卡/不存在;-13=权限)。空目录=0 且 count=0。 */
|
||||||
|
int fsx_scan_dir(const char *path, fsx_entry *entries, int max, int *out_count);
|
||||||
|
|
||||||
|
/* 目录优先,其次 name 升序(strcmp,字节序)。 */
|
||||||
|
void fsx_sort_entries(fsx_entry *entries, int n);
|
||||||
|
|
||||||
|
/* 虚拟化:给定总行数/行高/滚动/视口高,算可见 [first,last)。越界安全。 */
|
||||||
|
void fsx_visible_range(int count, int item_h, int scroll, int viewport_h,
|
||||||
|
int *first, int *last);
|
||||||
|
|
||||||
|
/* 最大滚动偏移:max(0, count*item_h - viewport_h)。 */
|
||||||
|
int fsx_max_scroll(int count, int item_h, int viewport_h);
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 写/查操作(全部先 fsx_within_sd 校验;0=成功,<0=负 errno) */
|
||||||
|
/* ============================================================ */
|
||||||
|
|
||||||
|
/* 删除:目录(须空)rmdir,否则 unlink。 */
|
||||||
|
int fsx_delete(const char *path);
|
||||||
|
|
||||||
|
/* 重命名 *path 为 *new_name(同名目录下)。目标已存在→-EEXIST。 */
|
||||||
|
int fsx_rename_into(const char *path, const char *new_name);
|
||||||
|
|
||||||
|
/* 在 base 目录下新建子目录 name。 */
|
||||||
|
int fsx_mkdir(const char *base, const char *name);
|
||||||
|
|
||||||
|
/* stat 一个路径填 entry(lstat,不追符号链)。0=成功,<0=负 errno。 */
|
||||||
|
int fsx_stat_path(const char *path, fsx_entry *out);
|
||||||
|
|
||||||
|
/* 简单文本判定:按扩展名白名单(用于读预览)。1=视为文本。 */
|
||||||
|
int fsx_is_text_path(const char *path);
|
||||||
|
|
||||||
|
#endif /* FSX_H */
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/* test/test_fsx.c — 宿主单元自测(不跨编、不刷机)。编译(宿主 gcc,指向临时卡根):
|
||||||
|
* gcc -O2 -Wall -Wextra -DFSX_SD_ROOT="\"/tmp/meowfsx_fsx_test\"" \
|
||||||
|
* -Ilogic test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
|
||||||
|
*
|
||||||
|
* 覆盖 fsx 的纯逻辑与可宿主 IO 逻辑:路径安全 / 格式化 / 枚举排序 / 虚拟化区间 / 写操作。
|
||||||
|
* 返回 0=全 PASS;>0=失败数。
|
||||||
|
*/
|
||||||
|
#include "fsx.h"
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int g_fail; /* 失败计数 */
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) do { \
|
||||||
|
if (cond) { printf(" PASS: %s\n", msg); } \
|
||||||
|
else { printf(" FAIL: %s\n", msg); g_fail++; } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* 创建文件(写几个字节)。 */
|
||||||
|
static void make_file(const char *path, const char *content)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (fd < 0) return;
|
||||||
|
if (content) { size_t n = strlen(content); ssize_t w = write(fd, content, n); (void)w; }
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 递归建目录(如 mkdir -p)。 */
|
||||||
|
static void make_dirs(const char *path)
|
||||||
|
{
|
||||||
|
char tmp[512];
|
||||||
|
snprintf(tmp, sizeof tmp, "%s", path);
|
||||||
|
for (char *p = tmp + 1; *p; p++) {
|
||||||
|
if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; }
|
||||||
|
}
|
||||||
|
mkdir(tmp, 0755);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("== fsx host test ==\n");
|
||||||
|
|
||||||
|
/* ---- 准备临时卡根 ---- */
|
||||||
|
char cmd[600];
|
||||||
|
snprintf(cmd, sizeof cmd, "rm -rf %s", FSX_SD_ROOT);
|
||||||
|
system(cmd);
|
||||||
|
make_dirs(FSX_SD_ROOT);
|
||||||
|
make_dirs(FSX_SD_ROOT "/zeta");
|
||||||
|
make_dirs(FSX_SD_ROOT "/alpha");
|
||||||
|
make_file(FSX_SD_ROOT "/a.txt", "hello");
|
||||||
|
make_file(FSX_SD_ROOT "/B.txt", "WORLD");
|
||||||
|
make_file(FSX_SD_ROOT "/delta.txt", "ddd");
|
||||||
|
make_file(FSX_SD_ROOT "/alpha/inner.txt", "inner");
|
||||||
|
make_file(FSX_SD_ROOT "/zeta/file1.txt", "zz"); /* zeta 非空,用于删除非空目录测试 */
|
||||||
|
|
||||||
|
/* ---- 路径安全 ---- */
|
||||||
|
printf("[path safety]\n");
|
||||||
|
CHECK(fsx_within_sd(FSX_SD_ROOT) == 1, "within_sd(root)=1");
|
||||||
|
CHECK(fsx_within_sd(FSX_SD_ROOT "/zeta") == 1, "within_sd(dev path)=1");
|
||||||
|
CHECK(fsx_within_sd(FSX_SD_ROOT "/zeta/music") == 1, "within_sd(nested)=1");
|
||||||
|
{
|
||||||
|
char p[600];
|
||||||
|
snprintf(p, sizeof p, "%sevil", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_within_sd(p) == 0, "within_sd(spoof prefix)=0");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char p[600];
|
||||||
|
snprintf(p, sizeof p, "%s/../etc", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_within_sd(p) == 0, "within_sd(..)=0");
|
||||||
|
}
|
||||||
|
CHECK(fsx_is_root(FSX_SD_ROOT) == 1, "is_root(root)=1");
|
||||||
|
CHECK(fsx_is_root(FSX_SD_ROOT "/a") == 0, "is_root(child)=0");
|
||||||
|
|
||||||
|
{
|
||||||
|
char out[600];
|
||||||
|
CHECK(fsx_join(FSX_SD_ROOT, "zeta", out, sizeof out) == 0 &&
|
||||||
|
strcmp(out, FSX_SD_ROOT "/zeta") == 0, "join ok");
|
||||||
|
CHECK(fsx_join(FSX_SD_ROOT, "..", out, sizeof out) != 0, "join rejects ..");
|
||||||
|
CHECK(fsx_join(FSX_SD_ROOT, "a/b", out, sizeof out) != 0, "join rejects slash");
|
||||||
|
}
|
||||||
|
CHECK(strcmp(fsx_basename(FSX_SD_ROOT "/zeta/music"), "music") == 0, "basename");
|
||||||
|
CHECK(strcmp(fsx_basename(FSX_SD_ROOT), strrchr(FSX_SD_ROOT, '/') + 1) == 0, "basename(root)");
|
||||||
|
{
|
||||||
|
char out[600];
|
||||||
|
CHECK(fsx_dirname(FSX_SD_ROOT "/zeta/music", out, sizeof out) == 0 &&
|
||||||
|
strcmp(out, FSX_SD_ROOT "/zeta") == 0, "dirname");
|
||||||
|
CHECK(fsx_dirname(FSX_SD_ROOT, out, sizeof out) == 0 &&
|
||||||
|
strcmp(out, FSX_SD_ROOT) == 0, "dirname(root)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 名清洗 ---- */
|
||||||
|
printf("[name sanitize]\n");
|
||||||
|
{
|
||||||
|
char out[100];
|
||||||
|
CHECK(fsx_sanitize_name("my dir", out, sizeof out) == 0 && strcmp(out, "my dir") == 0, "ok name");
|
||||||
|
CHECK(fsx_sanitize_name("..", out, sizeof out) != 0, "reject ..");
|
||||||
|
CHECK(fsx_sanitize_name("a/b", out, sizeof out) != 0, "reject slash");
|
||||||
|
CHECK(fsx_sanitize_name("a\\b", out, sizeof out) != 0, "reject backslash");
|
||||||
|
CHECK(fsx_sanitize_name(".hidden", out, sizeof out) != 0, "reject leading dot");
|
||||||
|
CHECK(fsx_sanitize_name(" ", out, sizeof out) != 0, "reject blank");
|
||||||
|
CHECK(fsx_sanitize_name("good.bin", out, sizeof out) == 0, "ok dotted name");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 格式化 ---- */
|
||||||
|
printf("[format]\n");
|
||||||
|
{
|
||||||
|
char s[32];
|
||||||
|
fsx_fmt_size(512, s, sizeof s);
|
||||||
|
CHECK(strcmp(s, "512 B") == 0, "size 512 B");
|
||||||
|
fsx_fmt_size(2048, s, sizeof s);
|
||||||
|
CHECK(strcmp(s, "2.0 KB") == 0, "size 2.0 KB");
|
||||||
|
fsx_fmt_size(1048576, s, sizeof s);
|
||||||
|
CHECK(strcmp(s, "1.0 MB") == 0, "size 1.0 MB");
|
||||||
|
fsx_fmt_time(0, s, sizeof s);
|
||||||
|
CHECK(s[0] != '\0', "time non-empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 枚举排序 ---- */
|
||||||
|
printf("[scan & sort]\n");
|
||||||
|
{
|
||||||
|
static fsx_entry e[FSX_MAX_ENTRIES];
|
||||||
|
int n = 0;
|
||||||
|
int r = fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
|
||||||
|
CHECK(r == 0, "scan ok");
|
||||||
|
CHECK(n == 5, "scan count 5 (2 dirs+3 files)");
|
||||||
|
/* 目录优先 + 名称升序:alpha, zeta, B.txt, a.txt, delta.txt */
|
||||||
|
if (n == 5) {
|
||||||
|
CHECK(e[0].is_dir && strcmp(e[0].name, "alpha") == 0, "first=dir alpha");
|
||||||
|
CHECK(e[1].is_dir && strcmp(e[1].name, "zeta") == 0, "second=dir zeta");
|
||||||
|
CHECK(!e[2].is_dir && strcmp(e[2].name, "B.txt") == 0, "third=file B.txt");
|
||||||
|
CHECK(!e[3].is_dir && strcmp(e[3].name, "a.txt") == 0, "fourth=file a.txt");
|
||||||
|
CHECK(!e[4].is_dir && strcmp(e[4].name, "delta.txt") == 0, "fifth=file delta.txt");
|
||||||
|
CHECK(e[2].size == 5, "file size=5");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 虚拟化区间 ---- */
|
||||||
|
printf("[virtualize]\n");
|
||||||
|
{
|
||||||
|
int first, last;
|
||||||
|
fsx_visible_range(100, 56, 0, 600, &first, &last);
|
||||||
|
CHECK(first == 0 && last == 11, "range top");
|
||||||
|
fsx_visible_range(100, 56, 5 * 56, 600, &first, &last);
|
||||||
|
CHECK(first == 5 && last == 16, "range mid");
|
||||||
|
fsx_visible_range(5, 56, 0, 600, &first, &last);
|
||||||
|
CHECK(first == 0 && last == 5 && fsx_max_scroll(5, 56, 600) == 0, "range clamped");
|
||||||
|
CHECK(fsx_max_scroll(20, 56, 600) == 520, "max_scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 写操作 ---- */
|
||||||
|
printf("[write ops]\n");
|
||||||
|
{
|
||||||
|
int r = fsx_mkdir(FSX_SD_ROOT, "newdir");
|
||||||
|
CHECK(r == 0, "mkdir ok");
|
||||||
|
/* 建后应在列表里 */
|
||||||
|
static fsx_entry e[FSX_MAX_ENTRIES];
|
||||||
|
int n = 0;
|
||||||
|
fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
|
||||||
|
int found = 0;
|
||||||
|
for (int i = 0; i < n; i++) if (!strcmp(e[i].name, "newdir")) found = 1;
|
||||||
|
CHECK(found, "newdir appears after scan");
|
||||||
|
|
||||||
|
/* 重命名 a.txt → renamed.txt */
|
||||||
|
char oldp[600];
|
||||||
|
snprintf(oldp, sizeof oldp, "%s/a.txt", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_rename_into(oldp, "renamed.txt") == 0, "rename ok");
|
||||||
|
CHECK(fsx_within_sd(FSX_SD_ROOT "/renamed.txt") == 1, "renamed path within sd");
|
||||||
|
CHECK(fsx_rename_into(oldp, "renamed2.txt") != 0, "rename missing src fails");
|
||||||
|
/* 重命名到卡外拒 */
|
||||||
|
{
|
||||||
|
char p[600];
|
||||||
|
snprintf(p, sizeof p, "%s/../../a2.txt", FSX_SD_ROOT);
|
||||||
|
/* fsx_rename_into 以 sanitize + dirname 构造目标,这里验证目标越界被拒 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 改名后旧名没了 */
|
||||||
|
fsx_scan_dir(FSX_SD_ROOT, e, FSX_MAX_ENTRIES, &n);
|
||||||
|
found = 0;
|
||||||
|
for (int i = 0; i < n; i++) if (!strcmp(e[i].name, "a.txt")) found = 1;
|
||||||
|
CHECK(!found, "old name gone after rename");
|
||||||
|
|
||||||
|
/* 删除文件 renamed.txt */
|
||||||
|
char fp[600];
|
||||||
|
snprintf(fp, sizeof fp, "%s/renamed.txt", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_delete(fp) == 0, "delete file ok");
|
||||||
|
|
||||||
|
/* 删除空目录 newdir */
|
||||||
|
char dp[600];
|
||||||
|
snprintf(dp, sizeof dp, "%s/newdir", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_delete(dp) == 0, "delete empty dir ok");
|
||||||
|
|
||||||
|
/* 删除非空目录 zeta(含 inner.txt)→ 应失败(ENOTEMPTY) */
|
||||||
|
char zp[600];
|
||||||
|
snprintf(zp, sizeof zp, "%s/zeta", FSX_SD_ROOT);
|
||||||
|
CHECK(fsx_delete(zp) != 0, "delete non-empty dir fails");
|
||||||
|
|
||||||
|
/* 卡外/越界写操作被拒 */
|
||||||
|
CHECK(fsx_mkdir("/tmp/outside", "x") != 0, "mkdir outside sd rejected");
|
||||||
|
CHECK(fsx_delete("/tmp/outside/file") != 0, "delete outside sd rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 文本判定 ---- */
|
||||||
|
printf("[text detect]\n");
|
||||||
|
CHECK(fsx_is_text_path("a.txt") == 1, "txt is text");
|
||||||
|
CHECK(fsx_is_text_path("a.log") == 1, "log is text");
|
||||||
|
CHECK(fsx_is_text_path("x.mp3") == 0, "mp3 not text");
|
||||||
|
CHECK(fsx_is_text_path("noext") == 0, "no ext not text");
|
||||||
|
|
||||||
|
printf("== %s (%d failure(s)) ==\n", g_fail ? "FAILED" : "ALL PASS", g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
/* 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/* ui/common/chrome.c — files 应用共享 chrome(TopAppBar 建置 / 几何 / 空态&页脚绘制)。
|
||||||
|
*
|
||||||
|
* 与 hello_app/ui/common/chrome.c 同理:把「页面 chrome 固定不随内容重建」分出去;本模块只
|
||||||
|
* 提供几何计算 + topbar 建置 + 非控件直绘辅助(空态/页脚),供 browser/viewer 两页复用。
|
||||||
|
* 绘制全部读 meow_theme_get() 语义色(§8.1),不硬编码配色;topbar 由 meow_topappbar 建,
|
||||||
|
* 自动注册进绘制物注册表(§6.2),owner=创建时 active activity。
|
||||||
|
*/
|
||||||
|
#include "files_internal.h"
|
||||||
|
|
||||||
|
int app_statusbar(void)
|
||||||
|
{
|
||||||
|
int sb = meow_ui_statusbar_height();
|
||||||
|
return sb < 0 ? 0 : sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_rect_t app_topbar_rect(void)
|
||||||
|
{
|
||||||
|
int sb = app_statusbar();
|
||||||
|
return AR(0, sb, meow_ui_width(), APP_TOPBAR_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_rect_t app_content_rect(void)
|
||||||
|
{
|
||||||
|
int sb = app_statusbar();
|
||||||
|
int y = sb + APP_TOPBAR_H;
|
||||||
|
int h = meow_ui_height() - y;
|
||||||
|
if (h < 0) h = 0;
|
||||||
|
return AR(0, y, meow_ui_width(), h);
|
||||||
|
}
|
||||||
|
|
||||||
|
meow_widget *app_topbar_create(const char *title, int back, const char *action_label)
|
||||||
|
{
|
||||||
|
app_rect_t r = app_topbar_rect();
|
||||||
|
meow_widget *tb = meow_topappbar_create(r.x, r.y, r.w, r.h, title);
|
||||||
|
meow_topappbar_set_back(tb, back);
|
||||||
|
if (action_label) meow_topappbar_add_action(tb, action_label);
|
||||||
|
meow_widget_set_z(tb, 999); /* chrome 置顶(§6.2:> 一切自动 z,区域重画最后画它) */
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空态/错误:居中两行(kind 大字 + what 小字)。kind=状态标题,what=说明/路径。 */
|
||||||
|
void app_draw_empty(app_rect_t r, const char *kind, const char *what)
|
||||||
|
{
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
int cy = r.y + r.h / 2;
|
||||||
|
meow_ui_text_center(r.x + r.w / 2, cy - 34, kind ? kind : "", 28, th->primary);
|
||||||
|
if (what && what[0])
|
||||||
|
meow_ui_text_center(r.x + r.w / 2, cy + 4, what, 20, th->dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部信息(右对齐 dim):项数/路径/截断提示。 */
|
||||||
|
void app_draw_footer(app_rect_t r, const char *text)
|
||||||
|
{
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
if (!text || !text[0]) return;
|
||||||
|
meow_ui_text_right(r.x + r.w - APP_PAD, r.y + r.h - 26, text, 20, th->dim);
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
/* ui/viewer.c — 文本预览页(ACT_VIEWER,子页)view + 控制器。
|
||||||
|
*
|
||||||
|
* 只读、可滚动、≤256KB 截断(APP_READ_MAX,超长只读前 256KB)。
|
||||||
|
* 行模型:把读入内容按 '\n' 切成逻辑行(<= VIEWER_MAX_LINES,超长截断并附加提示);
|
||||||
|
* 每逻辑行按可视列宽软换行成「屏幕行」;滚动按屏幕行像素仿真化(只画可见屏幕行)。
|
||||||
|
* 打开时 meow_view_page_push(ACT_VIEWER)(on_enter 置标题/清滚动/整帧重画);返回=左缘左滑/返回钮。
|
||||||
|
*/
|
||||||
|
#include "files_internal.h"
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
typedef struct { meow_view *view; int active; } viewer_ctrl;
|
||||||
|
static viewer_ctrl g_vc;
|
||||||
|
|
||||||
|
#define VIEWER_MAX_LINES 8192 /* 逻辑行上限(超长折行会很多,但按行数界内存) */
|
||||||
|
|
||||||
|
static char *g_buf; /* malloc'd 读入内容(前 256KB,NUL 结尾) */
|
||||||
|
static size_t g_buf_len;
|
||||||
|
static char g_fname[FSX_MAX_NAME];
|
||||||
|
static int *g_line; /* g_nline 个:每逻辑行起始偏移 */
|
||||||
|
static int g_nline;
|
||||||
|
static int *g_rown; /* g_nline 个:每逻辑行对应的首「屏幕行」下标 */
|
||||||
|
static int g_nrows; /* 总屏幕行数(= 末行 rown + 其折行数) */
|
||||||
|
static int g_vscroll; /* 滚动偏移(px) */
|
||||||
|
static int g_vdrag, g_vdrag_last_y;
|
||||||
|
|
||||||
|
static meow_widget *g_tb;
|
||||||
|
static meow_widget *g_vcw; /* 内容区 custom widget(画文本行) */
|
||||||
|
static app_rect_t r_content;
|
||||||
|
|
||||||
|
/* 可视列宽(像素)。 */
|
||||||
|
static int viewer_colw(void) { return r_content.w - 2 * APP_PAD; }
|
||||||
|
/* 文本行区视口高(底部留页脚)。 */
|
||||||
|
static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; }
|
||||||
|
/* 屏幕行高(px)。 */
|
||||||
|
static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; }
|
||||||
|
|
||||||
|
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。 */
|
||||||
|
static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!g_buf || li < 0 || li >= g_nline) return 0;
|
||||||
|
int start = g_line[li];
|
||||||
|
int end = (li + 1 < g_nline) ? g_line[li + 1] : (int)g_buf_len;
|
||||||
|
int len = end - start;
|
||||||
|
while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--;
|
||||||
|
int colw = viewer_colw();
|
||||||
|
int col = 0, row = 0, outlen = 0;
|
||||||
|
char ch[2]; ch[1] = '\0';
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
ch[0] = g_buf[start + i];
|
||||||
|
if (ch[0] == '\r') continue;
|
||||||
|
int w = meow_ui_text_width(ch, 20);
|
||||||
|
if (col + w > colw && col > 0) { row++; col = 0; }
|
||||||
|
if (row == intra && out && outlen < outsz - 1) out[outlen++] = ch[0];
|
||||||
|
col += w;
|
||||||
|
}
|
||||||
|
if (out) out[outlen] = '\0';
|
||||||
|
return row + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读入文件并建行表;0=文本预览可读,-1=非文本/读失败。 */
|
||||||
|
static int viewer_build(const char *path)
|
||||||
|
{
|
||||||
|
free(g_buf); free(g_line); free(g_rown);
|
||||||
|
g_buf = NULL; g_line = NULL; g_rown = NULL;
|
||||||
|
g_nline = 0; g_nrows = 0; g_buf_len = 0;
|
||||||
|
g_fname[0] = '\0';
|
||||||
|
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
size_t cap = APP_READ_MAX + 1;
|
||||||
|
g_buf = (char *)malloc(cap);
|
||||||
|
if (!g_buf) { close(fd); return -1; }
|
||||||
|
ssize_t n = read(fd, g_buf, APP_READ_MAX);
|
||||||
|
close(fd);
|
||||||
|
if (n <= 0) { free(g_buf); g_buf = NULL; return -1; }
|
||||||
|
g_buf_len = (size_t)n;
|
||||||
|
g_buf[g_buf_len] = '\0';
|
||||||
|
|
||||||
|
/* 非文本嗅探:前 512 字节出现 NUL 视为二进制 */
|
||||||
|
int lim = (int)(g_buf_len < 512 ? g_buf_len : 512);
|
||||||
|
for (int i = 0; i < lim; i++) if (g_buf[i] == '\0') { free(g_buf); g_buf = NULL; return -1; }
|
||||||
|
|
||||||
|
/* 文件名 */
|
||||||
|
const char *bn = fsx_basename(path);
|
||||||
|
strncpy(g_fname, bn, FSX_MAX_NAME - 1);
|
||||||
|
g_fname[FSX_MAX_NAME - 1] = '\0';
|
||||||
|
|
||||||
|
/* 切逻辑行(上限 VIEWER_MAX_LINES;超出截断,末行吸收剩余 + 附截断提示) */
|
||||||
|
int cap_l = VIEWER_MAX_LINES;
|
||||||
|
g_line = (int *)malloc((size_t)cap_l * sizeof(int));
|
||||||
|
g_rown = (int *)malloc((size_t)cap_l * sizeof(int));
|
||||||
|
if (!g_line || !g_rown) { free(g_buf); free(g_line); free(g_rown); g_buf = NULL; g_line=NULL; g_rown=NULL; return -1; }
|
||||||
|
g_nline = 0;
|
||||||
|
g_line[g_nline++] = 0;
|
||||||
|
int truncated = 0;
|
||||||
|
for (size_t i = 0; i < g_buf_len; i++) {
|
||||||
|
if (g_buf[i] == '\n') {
|
||||||
|
if (g_nline < cap_l) g_line[g_nline++] = (int)i + 1;
|
||||||
|
else { truncated = 1; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (truncated) {
|
||||||
|
g_line[g_nline - 1] = (int)g_buf_len; /* 让最后一行吸收剩余 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折行累加 → 屏幕行下标 */
|
||||||
|
g_nrows = 0;
|
||||||
|
for (int li = 0; li < g_nline; li++) {
|
||||||
|
g_rown[li] = g_nrows;
|
||||||
|
g_nrows += viewer_wrap_line(li, -1, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 宿主测试/入口兼容:读入 buffer 并解析行。 */
|
||||||
|
void viewer_load(const char *path) { viewer_build(path); }
|
||||||
|
|
||||||
|
/* 内容区 render_cb:虚拟化画可见屏幕行。 */
|
||||||
|
static void viewer_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||||
|
{
|
||||||
|
(void)w; (void)ctx;
|
||||||
|
if (!g_buf || g_nrows <= 0) {
|
||||||
|
app_draw_empty(r_content, "NOT A TEXT FILE", g_fname[0] ? g_fname : "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int rowh = viewer_rowh(th);
|
||||||
|
if (rowh < 1) rowh = 1;
|
||||||
|
int vp = viewer_vp_h();
|
||||||
|
int top = g_vscroll / rowh;
|
||||||
|
int bot = (g_vscroll + vp) / rowh + 1;
|
||||||
|
if (top < 0) top = 0;
|
||||||
|
if (bot > g_nrows) bot = g_nrows;
|
||||||
|
|
||||||
|
char row[512];
|
||||||
|
for (int sr = top; sr < bot; sr++) {
|
||||||
|
/* 二分找逻辑行 li:g_rown[li] <= sr 的最大 li */
|
||||||
|
int lo = 0, hi = g_nline - 1, li = 0;
|
||||||
|
while (lo <= hi) {
|
||||||
|
int mid = (lo + hi) / 2;
|
||||||
|
if (g_rown[mid] <= sr) { li = mid; lo = mid + 1; }
|
||||||
|
else { hi = mid - 1; }
|
||||||
|
}
|
||||||
|
int intra = sr - g_rown[li];
|
||||||
|
row[0] = '\0';
|
||||||
|
viewer_wrap_line(li, intra, row, (int)sizeof row);
|
||||||
|
if (!row[0] && intra == 0) row[0] = ' ', row[1] = '\0';
|
||||||
|
int ry = r_content.y + (sr * rowh - g_vscroll);
|
||||||
|
if (ry < r_content.y) continue;
|
||||||
|
if (ry >= r_content.y + vp) break;
|
||||||
|
meow_ui_text(r_content.x + APP_PAD, ry, row, th->font_body, th->onSurface);
|
||||||
|
}
|
||||||
|
/* 页脚:行数 + 文件名(含截断提示) */
|
||||||
|
char foot[128];
|
||||||
|
if (g_buf_len >= APP_READ_MAX)
|
||||||
|
snprintf(foot, sizeof foot, ">256KB (truncated) %d row(s)", g_nrows);
|
||||||
|
else
|
||||||
|
snprintf(foot, sizeof foot, "%d row(s)", g_nrows);
|
||||||
|
app_draw_footer(r_content, foot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int viewer_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 viewer_clamp_scroll(void)
|
||||||
|
{
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
int rowh = viewer_rowh(th);
|
||||||
|
int content_h = g_nrows * rowh;
|
||||||
|
int maxs = content_h - viewer_vp_h();
|
||||||
|
if (maxs < 0) maxs = 0;
|
||||||
|
if (g_vscroll < 0) g_vscroll = 0;
|
||||||
|
if (g_vscroll > maxs) g_vscroll = maxs;
|
||||||
|
}
|
||||||
|
|
||||||
|
int viewer_open_file(const char *path)
|
||||||
|
{
|
||||||
|
if (viewer_build(path) != 0) return -1;
|
||||||
|
meow_view_page_push(ACT_VIEWER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int viewer_is_active(void) { return g_vc.active; }
|
||||||
|
|
||||||
|
void viewer_swipe(int dir, int dist)
|
||||||
|
{
|
||||||
|
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||||||
|
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
|
||||||
|
g_vscroll += del;
|
||||||
|
viewer_clamp_scroll();
|
||||||
|
meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void viewer_touch_down(int x, int y)
|
||||||
|
{
|
||||||
|
(void)x;
|
||||||
|
g_vdrag = 0; g_vdrag_last_y = 0;
|
||||||
|
if (!g_buf) return;
|
||||||
|
if (meow_widget_hit(g_vcw, x, y)) { g_vdrag = 1; g_vdrag_last_y = y; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void viewer_touch_move(int x, int y)
|
||||||
|
{
|
||||||
|
(void)x;
|
||||||
|
if (!g_vdrag) return;
|
||||||
|
int dy = y - g_vdrag_last_y;
|
||||||
|
g_vdrag_last_y = y;
|
||||||
|
if (dy == 0) return;
|
||||||
|
g_vscroll -= dy;
|
||||||
|
viewer_clamp_scroll();
|
||||||
|
meow_ui_invalidate(r_content.x, r_content.y, r_content.w, r_content.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
int viewer_on_tap(int x, int y)
|
||||||
|
{
|
||||||
|
if (g_tb) {
|
||||||
|
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||||
|
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return 1; }
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 页 view 生命周期 ---- */
|
||||||
|
|
||||||
|
static void viewer_page_create(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((viewer_ctrl *)ctx)->view = v;
|
||||||
|
r_content = app_content_rect();
|
||||||
|
meow_ui_activity_push(ACT_VIEWER); /* 让下面控件 owner=ACT_VIEWER */
|
||||||
|
g_tb = app_topbar_create("Preview", 1, NULL);
|
||||||
|
g_vcw = meow_widget_custom_create(r_content.x, r_content.y, r_content.w, r_content.h,
|
||||||
|
viewer_render, viewer_hit, NULL);
|
||||||
|
meow_ui_activity_pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void viewer_page_enter(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((viewer_ctrl *)ctx)->active = 1;
|
||||||
|
g_vscroll = 0;
|
||||||
|
if (g_tb) meow_topappbar_set_title(g_tb, g_fname[0] ? g_fname : "Preview");
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
meow_ui_set_page_bg(th->bg);
|
||||||
|
meow_ui_paint_full();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void viewer_page_leave(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((viewer_ctrl *)ctx)->active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void viewer_page_destroy(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v; (void)ctx;
|
||||||
|
if (g_vcw) { meow_widget_free(g_vcw); g_vcw = NULL; }
|
||||||
|
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||||||
|
free(g_buf); free(g_line); free(g_rown);
|
||||||
|
g_buf = NULL; g_line = NULL; g_rown = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
meow_view *viewer_view_create(void)
|
||||||
|
{
|
||||||
|
static const meow_view_hooks h = {
|
||||||
|
.on_create = viewer_page_create,
|
||||||
|
.on_enter = viewer_page_enter,
|
||||||
|
.on_leave = viewer_page_leave,
|
||||||
|
.on_destroy = viewer_page_destroy,
|
||||||
|
};
|
||||||
|
return meow_view_create(ACT_VIEWER, MEOW_VIEW_ACTIVITY, &h, &g_vc);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user