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,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