Files

864 lines
25 KiB
C
Raw Permalink 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.
/**
****************************************************************************************************
* @file ui_files.c
* @author HiFi Player Project
* @version V1.0
* @date 2026-01-01
* @brief 文件管理器 —— 浏览 SD 卡上的全部文件,支持进出子文件夹
*
* ┌──────────────────────────────────┐
* │ 📁 FILES 12 SD │ 状态栏
* ├──────────────────────────────────┤
* │ 0:/MUSIC/Album │ 当前路径
* ├──────────────────────────────────┤
* │ 📁 .. <DIR> │ 选中行(高亮 + 描边)
* │ 📁 Album <DIR> │
* │ ♪ song01.flac 24.1M │ 目录在前,文件在后,按名称排序
* │ 📄 notes.txt 128B │
* │ 📄 readme.md 2K │
* ├──────────────────────────────────┤
* │ [◀K0] [▲K1] [▼K2] [✔K3] │
* │ BACK UP DOWN OPEN │
* └──────────────────────────────────┘
*
* 实现要点:
* 1. 不重建控件。界面创建时固定做 FILES_ROWS 个“行”,刷新时把
* [s_top, s_top+FILES_ROWS) 的条目映射到这些行上(滑动窗口),
* 即使目录里有几百个文件也不会撑爆 LVGL 内存。
* 2. 目录里超过 FILES_MAX_ENTRIES 的条目会被截断,避免静态数组过大。
* 3. 非根目录时列表第一行固定是 "..",回车即可返回上级;
* K0 短按同样用于返回上级(根目录时由 ui.c 接管回主界面)。
* 4. 中文文件名是 GBK 编码,LVGL 内置字体无法显示,用 '?' 逐字占位,
* 但进入目录 / 打开文件仍使用原始字节,不影响功能。
****************************************************************************************************
*/
#include "ui_files.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "esp_log.h"
#include "ff.h"
#include "spi_sd.h"
#include "ui_common.h"
#include "app.h"
#define TAG "ui_files"
/* ---------------- 浏览参数 ---------------- */
#define FILES_ROOT "0:" /* SD 卡挂载点下的根目录 */
#define FILES_MAX_ENTRIES 96 /* 单目录最多显示多少条 */
#define FILES_NAME_MAX 96 /* 单条文件名缓冲 */
#define FILES_ROWS 5 /* 列表可见行数 */
#define FILES_ROW_H 28 /* 每行高度 */
#define FILES_PATH_MAX 300 /* 当前路径缓冲 */
/* ---------------- 屏幕布局 ---------------- */
/* 注意:不能用 LIST_H 这类名字,FreeRTOS 的 list.h 用 LIST_H 作为 include guard */
#define FILES_HEADER_H 30
#define FILES_PATH_Y 32
#define FILES_PATH_H 20
#define FILES_LIST_Y 54
#define FILES_LIST_H (FILES_ROWS * FILES_ROW_H) /* 140 → 底边 194 */
#define FILES_TOAST_Y 150
/* 每行内部的横向布局 */
#define ROW_NAME_X 22 /* 名称相对行左边的偏移 */
#define ROW_NAME_W 224 /* 名称最大宽度(超出滚动) */
#define ROW_SIZE_W 44 /* 右侧大小 / <DIR> 宽度 */
/* FATFS 卷标属性(本版本 ff.h 未提供 AM_VOL) */
#define FILES_AM_VOL 0x08
/* ---------------- 条目 ---------------- */
typedef struct {
char name[FILES_NAME_MAX]; /* 原始文件 / 目录名(GBK 原样保留) */
uint32_t size; /* 文件字节数,目录为 0 */
bool is_dir;
} files_entry_t;
/* ---------------- 行控件(固定池) ---------------- */
typedef struct {
lv_obj_t *row;
lv_obj_t *icon;
lv_obj_t *name;
lv_obj_t *size;
} files_row_t;
/* ---------------- 界面控件 ---------------- */
static lv_obj_t *s_scr = NULL;
static lv_obj_t *s_lbl_path = NULL;
static lv_obj_t *s_lbl_count = NULL;
static lv_obj_t *s_lbl_empty = NULL;
static lv_obj_t *s_toast = NULL;
static lv_obj_t *s_toast_lbl = NULL;
static files_row_t s_rows[FILES_ROWS];
static ui_key_bar_t s_bar;
static ui_flash_t s_flash;
/* ---------------- 浏览状态 ---------------- */
static files_entry_t s_entries[FILES_MAX_ENTRIES];
static int s_count = 0; /* 有效条目数 */
static int s_sel = 0; /* 选中项下标 */
static int s_top = 0; /* 列表窗口首行对应的条目下标 */
static char s_path[FILES_PATH_MAX] = FILES_ROOT;
static FRESULT s_load_res = FR_OK; /* 最近一次目录读取结果 */
static bool s_sd_ok = false; /* SD 卡是否已可读 */
static bool s_mount_tried = false; /* 是否已经尝试过挂载 */
static uint32_t s_toast_tick = 0;
/* ---------------- 按键提示 ---------------- */
static const char *const s_symbols[APP_KEY_NUM] = {
LV_SYMBOL_LEFT, /* K0:返回上级目录 / 回主界面 */
LV_SYMBOL_UP, /* K1:上一项 */
LV_SYMBOL_DOWN, /* K2:下一项 */
LV_SYMBOL_OK, /* K3:进入目录 */
};
static const char *const s_captions[APP_KEY_NUM] = {
"K0 BACK",
"K1 UP",
"K2 DOWN",
"K3 OPEN",
};
static const bool s_enabled[APP_KEY_NUM] = { true, true, true, true };
/* ================================================================================================
* 小工具
* ============================================================================================== */
/**
* @brief GBK 文件名 → 可在 LVGL 内置字体上显示的字符串
* @note ASCII 原样保留;每个双字节汉字折叠成一个 '?',避免出现乱码方块
*/
static void files_display_name(const char *src, char *dst, size_t size)
{
size_t len = strlen(src);
size_t j = 0;
if (size == 0)
{
return;
}
for (size_t i = 0; (i < len) && (j + 1 < size); i++)
{
unsigned char c = (unsigned char)src[i];
if ((c >= 0x20) && (c < 0x7F))
{
dst[j++] = (char)c;
continue;
}
/* GBK 双字节字符:跳过第二个字节,只输出一个占位符 */
if ((c >= 0x81) && (i + 1 < len))
{
unsigned char t = (unsigned char)src[i + 1];
if ((t >= 0x40) && (t <= 0xFE))
{
i++;
}
}
dst[j++] = '?';
}
dst[j] = '\0';
}
/** 文件大小 → "128B" / "2K" / "24.1M"(纯整数运算,不依赖浮点 printf) */
static void files_format_size(uint32_t bytes, char *out, size_t size)
{
const uint32_t mb = 1024u * 1024u;
if (bytes >= mb)
{
uint32_t whole = bytes / mb;
uint32_t frac = ((bytes % mb) * 10u) / mb;
snprintf(out, size, "%u.%uM", (unsigned)whole, (unsigned)frac);
}
else if (bytes >= 1024u)
{
snprintf(out, size, "%uK", (unsigned)(bytes / 1024u));
}
else
{
snprintf(out, size, "%uB", (unsigned)bytes);
}
}
/** 冒烟测试用的扩展名判断(只影响图标和颜色) */
static bool files_name_has_ext(const char *name, const char *ext)
{
size_t nlen = strlen(name);
size_t elen = strlen(ext);
if (nlen <= elen)
{
return false;
}
const char *tail = name + nlen - elen;
for (size_t i = 0; i < elen; i++)
{
char a = tail[i];
char b = ext[i];
if ((a >= 'A') && (a <= 'Z'))
{
a = (char)(a - 'A' + 'a');
}
if ((b >= 'A') && (b <= 'Z'))
{
b = (char)(b - 'A' + 'a');
}
if (a != b)
{
return false;
}
}
return true;
}
static bool files_is_audio(const char *name)
{
return files_name_has_ext(name, ".wav") ||
files_name_has_ext(name, ".flac");
}
static bool files_is_image(const char *name)
{
return files_name_has_ext(name, ".bmp") ||
files_name_has_ext(name, ".png") ||
files_name_has_ext(name, ".jpg") ||
files_name_has_ext(name, ".jpeg") ||
files_name_has_ext(name, ".gif");
}
/* ================================================================================================
* SD 卡
* ============================================================================================== */
/**
* @brief 确保 SD 卡已挂载
* @note 正常情况下播放器任务在开机时已经挂载好,这里只做“探测”,
* 只有探测失败且从未尝试过时才主动挂载一次,避免和播放器争抢。
*/
static void files_ensure_mounted(void)
{
FF_DIR dir;
if (f_opendir(&dir, FILES_ROOT) == FR_OK)
{
f_closedir(&dir);
if (!s_sd_ok)
{
s_sd_ok = true;
ESP_LOGI(TAG, "sd card ready");
}
return;
}
if (s_mount_tried)
{
return; /* 只主动尝试一次 */
}
s_mount_tried = true;
if (sd_spi_init() == ESP_OK)
{
s_sd_ok = true;
ESP_LOGI(TAG, "sd card mounted by files app");
}
else
{
ESP_LOGW(TAG, "sd card not available");
}
}
/* ================================================================================================
* 目录读取
* ============================================================================================== */
/** qsort 比较:`..` → 目录 → 文件,同类按名称不区分大小写 */
static int files_cmp(const void *a, const void *b)
{
const files_entry_t *x = (const files_entry_t *)a;
const files_entry_t *y = (const files_entry_t *)b;
bool x_up = (strcmp(x->name, "..") == 0);
bool y_up = (strcmp(y->name, "..") == 0);
if (x_up != y_up)
{
return x_up ? -1 : 1;
}
if (x->is_dir != y->is_dir)
{
return x->is_dir ? -1 : 1;
}
return strcasecmp(x->name, y->name);
}
/**
* @brief 读取当前路径下的条目到 s_entries
* @retval 有效条目数;s_load_res 给出 FatFs 的原始结果
*/
static int files_load(void)
{
FF_DIR dir;
FILINFO *fi = NULL;
FRESULT res = FR_OK;
int n = 0;
/* 非根目录:第一行固定插入 "..",回车即返回上级 */
if (strcmp(s_path, FILES_ROOT) != 0)
{
snprintf(s_entries[0].name, sizeof(s_entries[0].name), "..");
s_entries[0].is_dir = true;
s_entries[0].size = 0;
n = 1;
}
fi = (FILINFO *)malloc(sizeof(FILINFO));
if (fi == NULL)
{
ESP_LOGE(TAG, "no memory for FILINFO");
s_load_res = FR_NOT_ENOUGH_CORE;
return n;
}
res = f_opendir(&dir, s_path);
if (res == FR_OK)
{
while (n < FILES_MAX_ENTRIES)
{
res = f_readdir(&dir, fi);
if (res != FR_OK)
{
break;
}
if (fi->fname[0] == '\0')
{
break; /* 目录遍历结束 */
}
/* 隐藏 / 系统 / 卷标 一律不显示 */
if ((fi->fattrib & AM_HID) || (fi->fattrib & AM_SYS) ||
(fi->fattrib & FILES_AM_VOL))
{
continue;
}
if (strcmp(fi->fname, ".") == 0)
{
continue;
}
/* strlcpy 不做格式化,可以避开 -Wformat-truncation;
超过 FILES_NAME_MAX-1 的极端长名会被截断(仅此类目录无法再进入) */
strlcpy(s_entries[n].name, fi->fname, sizeof(s_entries[n].name));
s_entries[n].is_dir = (fi->fattrib & AM_DIR) != 0;
s_entries[n].size = (uint32_t)fi->fsize;
n++;
}
f_closedir(&dir);
}
free(fi);
s_load_res = res;
if (n > 1)
{
qsort(s_entries, (size_t)n, sizeof(files_entry_t), files_cmp);
}
return n;
}
/* ================================================================================================
* 界面刷新
* ============================================================================================== */
/** 让选中项始终落在可见窗口内 */
static void files_keep_visible(void)
{
if (s_sel < s_top)
{
s_top = s_sel;
}
else if (s_sel >= (s_top + FILES_ROWS))
{
s_top = s_sel - FILES_ROWS + 1;
}
int max_top = (s_count > FILES_ROWS) ? (s_count - FILES_ROWS) : 0;
if (s_top > max_top)
{
s_top = max_top;
}
if (s_top < 0)
{
s_top = 0;
}
}
/** K0 的含义随深度变化:根目录是“回主界面”,子目录是“返回上级” */
static void files_update_keybar(void)
{
bool at_root = (strcmp(s_path, FILES_ROOT) == 0);
ui_key_bar_set_caption(&s_bar, APP_KEY_0, at_root ? "K0 HOME" : "K0 UP");
}
static void files_hide_toast(void)
{
lv_obj_add_flag(s_toast, LV_OBJ_FLAG_HIDDEN);
}
static void files_show_toast(const char *text)
{
ui_label_set_if_changed(s_toast_lbl, text);
lv_obj_clear_flag(s_toast, LV_OBJ_FLAG_HIDDEN);
s_toast_tick = lv_tick_get();
}
/** 把当前窗口内的条目画到固定的行控件上 */
static void files_refresh(void)
{
char buf[80];
/* ---- 顶部:路径 + 条目数 ---- */
ui_label_set_if_changed(s_lbl_path, s_path);
snprintf(buf, sizeof(buf), "%d", s_count);
ui_label_set_if_changed(s_lbl_count, buf);
/* ---- 空目录 / 读取失败提示 ---- */
if (s_count == 0)
{
if (s_load_res == FR_OK)
{
ui_label_set_if_changed(s_lbl_empty, "EMPTY FOLDER");
}
else if (s_sd_ok)
{
snprintf(buf, sizeof(buf), "CANNOT READ (%d)", (int)s_load_res);
ui_label_set_if_changed(s_lbl_empty, buf);
}
else
{
ui_label_set_if_changed(s_lbl_empty, "NO SD CARD");
}
lv_obj_clear_flag(s_lbl_empty, LV_OBJ_FLAG_HIDDEN);
}
else
{
lv_obj_add_flag(s_lbl_empty, LV_OBJ_FLAG_HIDDEN);
}
/* ---- 列表行 ---- */
for (int i = 0; i < FILES_ROWS; i++)
{
files_row_t *r = &s_rows[i];
int idx = s_top + i;
if (idx >= s_count)
{
lv_obj_add_flag(r->row, LV_OBJ_FLAG_HIDDEN);
continue;
}
lv_obj_clear_flag(r->row, LV_OBJ_FLAG_HIDDEN);
const files_entry_t *e = &s_entries[idx];
bool sel = (idx == s_sel);
/* 选中:深色底 + 强调色描边 */
lv_obj_set_style_bg_color(r->row,
lv_color_hex(sel ? UI_COL_CARD_SEL : UI_COL_CARD),
LV_PART_MAIN);
lv_obj_set_style_border_width(r->row, sel ? 1 : 0, LV_PART_MAIN);
/* 图标:目录黄色、音频青色、图片紫色、其它灰色 */
const char *icon = LV_SYMBOL_FILE;
uint32_t icon_col = UI_COL_DIM;
if (e->is_dir)
{
icon = LV_SYMBOL_DIRECTORY;
icon_col = UI_COL_WARN;
}
else if (files_is_audio(e->name))
{
icon = LV_SYMBOL_AUDIO;
icon_col = UI_COL_ACCENT;
}
else if (files_is_image(e->name))
{
icon = LV_SYMBOL_IMAGE;
icon_col = UI_COL_ACCENT2;
}
ui_label_set_if_changed(r->icon, icon);
lv_obj_set_style_text_color(r->icon, lv_color_hex(icon_col), LV_PART_MAIN);
/* 名称(非 ASCII 折叠成 '?') */
files_display_name(e->name, buf, sizeof(buf));
ui_label_set_if_changed(r->name, buf);
lv_obj_set_style_text_color(r->name,
lv_color_hex(sel ? UI_COL_TEXT : 0xD5DBEA),
LV_PART_MAIN);
/* 右侧:目录显示 <DIR>,文件显示大小 */
if (e->is_dir)
{
ui_label_set_if_changed(r->size, "<DIR>");
}
else
{
files_format_size(e->size, buf, sizeof(buf));
ui_label_set_if_changed(r->size, buf);
}
}
files_update_keybar();
}
/* ================================================================================================
* 导航
* ============================================================================================== */
/**
* @brief 打开一个目录(失败则退回原目录)
*/
static void files_open_dir(const char *path)
{
char target[FILES_PATH_MAX];
char backup[FILES_PATH_MAX];
int old_sel = s_sel;
int n;
/* 先把目标路径复制出来,避免 path 指向 s_path 自身时 snprintf 重叠 */
strlcpy(target, path, sizeof(target));
snprintf(backup, sizeof(backup), "%s", s_path);
strlcpy(s_path, target, sizeof(s_path));
files_ensure_mounted();
n = files_load();
if ((n == 0) && (s_load_res != FR_OK) && (strcmp(target, FILES_ROOT) != 0))
{
ESP_LOGW(TAG, "cannot open %s (%d)", target, (int)s_load_res);
strlcpy(s_path, backup, sizeof(s_path));
n = files_load();
s_sel = old_sel;
}
else
{
s_sel = 0;
}
s_count = n;
s_top = 0;
files_keep_visible();
files_refresh();
}
/**
* @brief 返回上一级目录
* @retval true 已返回;false 已在根目录
*/
static bool files_go_up(void)
{
if (strcmp(s_path, FILES_ROOT) == 0)
{
return false;
}
char parent[FILES_PATH_MAX];
char *slash = strrchr(s_path, '/');
if (slash == NULL)
{
return false;
}
size_t len = (size_t)(slash - s_path);
if (len == 0)
{
snprintf(parent, sizeof(parent), "%s", FILES_ROOT);
}
else
{
if (len >= sizeof(parent))
{
len = sizeof(parent) - 1;
}
memcpy(parent, s_path, len);
parent[len] = '\0';
}
files_open_dir(parent);
return true;
}
/**
* @brief 打开选中项:目录则进入,文件则弹出详情
*/
static void files_enter(void)
{
if ((s_count <= 0) || (s_sel < 0) || (s_sel >= s_count))
{
return;
}
const files_entry_t *e = &s_entries[s_sel];
if (e->is_dir)
{
if (strcmp(e->name, "..") == 0)
{
files_go_up();
return;
}
char next[FILES_PATH_MAX];
size_t plen = strlen(s_path);
size_t nlen = strlen(e->name);
/* 拼成 "<当前路径>/<子目录名>"(s_path 根目录时是 "0:",同样需要斜杠) */
if ((plen + 1u + nlen + 1u) > sizeof(next))
{
files_show_toast("PATH TOO LONG");
return;
}
memcpy(next, s_path, plen);
next[plen] = '/';
memcpy(next + plen + 1u, e->name, nlen);
next[plen + 1u + nlen] = '\0';
files_open_dir(next);
return;
}
/* 普通文件:显示文件名 + 大小 + 类型 */
char disp[80];
char size_text[24];
char text[128];
files_display_name(e->name, disp, sizeof(disp));
files_format_size(e->size, size_text, sizeof(size_text));
snprintf(text, sizeof(text), "%s\n%s %s", disp, size_text,
files_is_audio(e->name) ? "AUDIO" :
(files_is_image(e->name) ? "IMAGE" : "FILE"));
files_show_toast(text);
}
/* ================================================================================================
* 对外接口
* ============================================================================================== */
/** 创建一行(固定池,不随目录内容变化) */
static void files_create_row(lv_obj_t *parent, int i)
{
files_row_t *r = &s_rows[i];
r->row = ui_card(parent, 0, (lv_coord_t)(i * FILES_ROW_H),
UI_HOR_RES - 16, FILES_ROW_H - 2, UI_COL_CARD, 7);
lv_obj_set_style_pad_hor(r->row, 6, LV_PART_MAIN);
lv_obj_set_style_border_width(r->row, 0, LV_PART_MAIN);
lv_obj_set_style_border_color(r->row, lv_color_hex(UI_COL_ACCENT), LV_PART_MAIN);
r->icon = ui_label(r->row, LV_SYMBOL_FILE, &lv_font_montserrat_14, UI_COL_DIM);
lv_obj_set_width(r->icon, 18);
lv_obj_align(r->icon, LV_ALIGN_LEFT_MID, 0, 0);
r->size = ui_label(r->row, "", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_set_width(r->size, ROW_SIZE_W);
lv_obj_set_style_text_align(r->size, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN);
lv_obj_align(r->size, LV_ALIGN_RIGHT_MID, 0, 0);
r->name = ui_label(r->row, "", &lv_font_montserrat_14, UI_COL_TEXT);
lv_obj_set_size(r->name, ROW_NAME_W, FILES_ROW_H - 4);
lv_label_set_long_mode(r->name, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(r->name, LV_ALIGN_LEFT_MID, ROW_NAME_X, 0);
}
void ui_files_create(void)
{
s_scr = lv_obj_create(NULL);
ui_style_screen(s_scr);
/* ---------------- 标题栏 ---------------- */
lv_obj_t *header = ui_card(s_scr, 0, 0, UI_HOR_RES, FILES_HEADER_H, UI_COL_PANEL, 0);
lv_obj_t *title = ui_label(header, LV_SYMBOL_DIRECTORY " FILES",
&lv_font_montserrat_16, 0x31C27C);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 10, 0);
lv_obj_t *hint = ui_label(header, "SD", &lv_font_montserrat_12, UI_COL_SUB);
lv_obj_align(hint, LV_ALIGN_RIGHT_MID, -44, 0);
s_lbl_count = ui_label(header, "0", &lv_font_montserrat_12, UI_COL_ACCENT);
lv_obj_align(s_lbl_count, LV_ALIGN_RIGHT_MID, -10, 0);
/* ---------------- 当前路径 ---------------- */
lv_obj_t *path_bar = ui_card(s_scr, 8, FILES_PATH_Y, UI_HOR_RES - 16, FILES_PATH_H,
UI_COL_TRACK, 6);
s_lbl_path = ui_label(path_bar, FILES_ROOT, &lv_font_montserrat_12, UI_COL_ACCENT);
lv_obj_set_width(s_lbl_path, UI_HOR_RES - 16 - 12);
lv_label_set_long_mode(s_lbl_path, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(s_lbl_path, LV_ALIGN_LEFT_MID, 6, 0);
/* ---------------- 列表容器(不支持滚轮,靠滑动窗口) ---------------- */
lv_obj_t *list = lv_obj_create(s_scr);
lv_obj_set_pos(list, 8, FILES_LIST_Y);
lv_obj_set_size(list, UI_HOR_RES - 16, FILES_LIST_H);
lv_obj_clear_flag(list, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(list, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(list, 0, LV_PART_MAIN);
for (int i = 0; i < FILES_ROWS; i++)
{
files_create_row(list, i);
}
/* ---------------- 空目录 / 错误提示 ---------------- */
s_lbl_empty = ui_label(s_scr, "NO SD CARD", &lv_font_montserrat_14, UI_COL_DIM);
lv_obj_align(s_lbl_empty, LV_ALIGN_TOP_MID, 0, 116);
/* ---------------- 文件详情浮层(创建在最后,压在最上层) ---------------- */
s_toast = ui_card(s_scr, 8, FILES_TOAST_Y, UI_HOR_RES - 16, 38, UI_COL_PANEL, 8);
lv_obj_set_style_border_width(s_toast, 1, LV_PART_MAIN);
lv_obj_set_style_border_color(s_toast, lv_color_hex(UI_COL_ACCENT), LV_PART_MAIN);
lv_obj_add_flag(s_toast, LV_OBJ_FLAG_HIDDEN);
s_toast_lbl = ui_label(s_toast, "", &lv_font_montserrat_12, UI_COL_TEXT);
lv_obj_set_width(s_toast_lbl, UI_HOR_RES - 16 - 16);
lv_obj_set_style_text_align(s_toast_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_center(s_toast_lbl);
/* ---------------- 底部按键提示 ---------------- */
ui_key_bar_create(s_scr, &s_bar, s_symbols, s_captions, s_enabled);
}
void ui_files_show(void)
{
ui_key_bar_set_highlight(&s_bar, -1);
files_hide_toast();
/* 每次进入应用都从根目录重新开始,行为可预测 */
files_open_dir(FILES_ROOT);
lv_scr_load(s_scr);
}
void ui_files_tick(void)
{
/* 文件详情浮层 2 秒后自动消失 */
if (!lv_obj_has_flag(s_toast, LV_OBJ_FLAG_HIDDEN) &&
(lv_tick_elaps(s_toast_tick) > 2000))
{
files_hide_toast();
}
ui_key_bar_set_highlight(&s_bar, ui_flash_active(&s_flash));
}
void ui_files_flash(int key)
{
ui_flash_trigger(&s_flash, key);
}
void ui_files_handle_key(uint8_t key)
{
ui_flash_trigger(&s_flash, key);
switch (key)
{
case APP_KEY_1: /* 上一项(到顶后跳到底部) */
if (s_count > 0)
{
s_sel = (s_sel <= 0) ? (s_count - 1) : (s_sel - 1);
files_keep_visible();
files_refresh();
}
break;
case APP_KEY_2: /* 下一项 */
if (s_count > 0)
{
s_sel = (s_sel + 1) % s_count;
files_keep_visible();
files_refresh();
}
break;
case APP_KEY_3: /* 进入目录 / 查看文件 */
files_enter();
break;
default:
break;
}
}
bool ui_files_back(void)
{
if (strcmp(s_path, FILES_ROOT) == 0)
{
return false; /* 已在根目录:交给 ui.c 回主界面 */
}
return files_go_up();
}