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:
MeowOS committed 2026-08-31 04:03:56 +08:00
1 parent b5038f8673
commit c187b7bce3
12 files changed
+1741

No files matched your search

+281
View File
@@ -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);
}