Merge develop into main branch (#137)
* SdCard HAL refactored (#135) - Refactor SdCard HAL - introduce Lockable * Screenshot and FatFS improvements (#136) - Fix screenshots on ESP32 - Improve Screenshot service - Convert Screenshot app to class-based instead of structs - Screenshot app now automatically updates when task is finished - Enable FatFS long filename support * Re-use common log messages (#138) For consistency and binary size reduction * Toolbar spinner should get margin to the right * More TactilityC features (#139) * Rewrote Loader - Simplified Loader by removing custom threa - Created DispatcherThread - Move auto-starting apps to Boot app - Fixed Dispatcher bug where it could get stuck not processing new messages * Hide AP settings if the AP is not saved * Missing from previous commit * Replace LV_EVENT_CLICKED with LV_EVENT_SHORT_CLICKED * Refactored files app and created InputDialog (#140) - Changed Files app so that it has a View and State - Files app now allows for long-pressing on files to perform actions - Files app now has rename and delete actions - Created InputDialog app - Improved AlertDialog app layout
This commit is contained in:
committed by
GitHub
parent
9033daa6dd
commit
50bd6e8bf6
@@ -1,97 +1,99 @@
|
||||
#include "FileUtils.h"
|
||||
#include "TactilityCore.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
#define TAG "file_utils"
|
||||
|
||||
#define SCANDIR_LIMIT 128
|
||||
std::string getChildPath(const std::string& basePath, const std::string& childPath) {
|
||||
// Postfix with "/" when the current path isn't "/"
|
||||
if (basePath.length() != 1) {
|
||||
return basePath + "/" + childPath;
|
||||
} else {
|
||||
return "/" + childPath;
|
||||
}
|
||||
}
|
||||
|
||||
int dirent_filter_dot_entries(const struct dirent* entry) {
|
||||
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right) {
|
||||
bool left_is_dir = (*left)->d_type == TT_DT_DIR || (*left)->d_type == TT_DT_CHR;
|
||||
bool right_is_dir = (*right)->d_type == TT_DT_DIR || (*right)->d_type == TT_DT_CHR;
|
||||
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) {
|
||||
bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR;
|
||||
bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR;
|
||||
if (left_is_dir == right_is_dir) {
|
||||
return strcmp((*left)->d_name, (*right)->d_name);
|
||||
return strcmp(left.d_name, right.d_name) < 0;
|
||||
} else {
|
||||
return (left_is_dir < right_is_dir) ? 1 : -1;
|
||||
return left_is_dir > right_is_dir;
|
||||
}
|
||||
}
|
||||
|
||||
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right) {
|
||||
return strcmp((*left)->d_name, (*right)->d_name);
|
||||
}
|
||||
|
||||
int scandir(
|
||||
const char* path,
|
||||
struct dirent*** output,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filterMethod,
|
||||
ScandirSort _Nullable sortMethod
|
||||
) {
|
||||
DIR* dir = opendir(path);
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
TT_LOG_E(TAG, "Failed to open dir %s", path);
|
||||
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
*output = static_cast<dirent**>(malloc(sizeof(void*) * SCANDIR_LIMIT));
|
||||
if (*output == nullptr) {
|
||||
TT_LOG_E(TAG, "Out of memory");
|
||||
closedir(dir);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent** dirent_array = *output;
|
||||
int next_dirent_index = 0;
|
||||
|
||||
struct dirent* current_entry;
|
||||
bool out_of_memory = false;
|
||||
while ((current_entry = readdir(dir)) != nullptr) {
|
||||
if (filter(current_entry) == 0) {
|
||||
dirent_array[next_dirent_index] = static_cast<dirent*>(malloc(sizeof(struct dirent)));
|
||||
if (dirent_array[next_dirent_index] != nullptr) {
|
||||
memcpy(dirent_array[next_dirent_index], current_entry, sizeof(struct dirent));
|
||||
|
||||
next_dirent_index++;
|
||||
if (next_dirent_index >= SCANDIR_LIMIT) {
|
||||
TT_LOG_E(TAG, "Directory has more than %d files", SCANDIR_LIMIT);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Alloc failed. Aborting and cleaning up.");
|
||||
out_of_memory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-memory clean-up
|
||||
if (out_of_memory && next_dirent_index > 0) {
|
||||
for (int i = 0; i < next_dirent_index; ++i) {
|
||||
TT_LOG_I(TAG, "Cleanup item %d", i);
|
||||
free(dirent_array[i]);
|
||||
}
|
||||
TT_LOG_I(TAG, "Free");
|
||||
free(*output);
|
||||
closedir(dir);
|
||||
return -1;
|
||||
// Empty directory
|
||||
} else if (next_dirent_index == 0) {
|
||||
free(*output);
|
||||
*output = nullptr;
|
||||
} else {
|
||||
if (sort) {
|
||||
qsort(dirent_array, next_dirent_index, sizeof(struct dirent*), (__compar_fn_t)sort);
|
||||
if (filterMethod(current_entry) == 0) {
|
||||
outList.push_back(*current_entry);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return next_dirent_index;
|
||||
|
||||
if (sortMethod != nullptr) {
|
||||
sort(outList.begin(), outList.end(), sortMethod);
|
||||
}
|
||||
|
||||
return (int)outList.size();
|
||||
};
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return filename.ends_with(".elf");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::basic_string<T> lowercase(const std::basic_string<T>& input) {
|
||||
std::basic_string<T> output = input;
|
||||
std::transform(
|
||||
output.begin(),
|
||||
output.end(),
|
||||
output.begin(),
|
||||
[](const T character) { return static_cast<T>(std::tolower(character)); }
|
||||
);
|
||||
return std::move(output);
|
||||
}
|
||||
|
||||
bool isSupportedImageFile(const std::string& filename) {
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return lowercase(filename).ends_with(".png");
|
||||
}
|
||||
|
||||
bool isSupportedTextFile(const std::string& filename) {
|
||||
std::string filename_lower = lowercase(filename);
|
||||
return filename_lower.ends_with(".txt") ||
|
||||
filename_lower.ends_with(".ini") ||
|
||||
filename_lower.ends_with(".json") ||
|
||||
filename_lower.ends_with(".yaml") ||
|
||||
filename_lower.ends_with(".yml") ||
|
||||
filename_lower.ends_with(".lua") ||
|
||||
filename_lower.ends_with(".js") ||
|
||||
filename_lower.ends_with(".properties");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
@@ -26,19 +28,13 @@ enum {
|
||||
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes
|
||||
};
|
||||
|
||||
std::string getChildPath(const std::string& basePath, const std::string& childPath);
|
||||
|
||||
typedef int (*ScandirFilter)(const struct dirent*);
|
||||
|
||||
typedef int (*ScandirSort)(const struct dirent**, const struct dirent**);
|
||||
typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&);
|
||||
|
||||
/**
|
||||
* Alphabetic sorting function for tt_scandir()
|
||||
* @param left left-hand side part for comparison
|
||||
* @param right right-hand side part for comparison
|
||||
* @return 0, -1 or 1
|
||||
*/
|
||||
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right);
|
||||
|
||||
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right);
|
||||
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
|
||||
|
||||
int dirent_filter_dot_entries(const struct dirent* entry);
|
||||
|
||||
@@ -49,16 +45,20 @@ int dirent_filter_dot_entries(const struct dirent* entry);
|
||||
* The caller is responsible for free-ing the memory of these.
|
||||
*
|
||||
* @param[in] path path the scan for files and directories
|
||||
* @param[out] output a pointer to an array of dirent*
|
||||
* @param[out] outList a pointer to vector of dirent
|
||||
* @param[in] filter an optional filter to filter out specific items
|
||||
* @param[in] sort an optional sorting function
|
||||
* @return the amount of items that were stored in "output" or -1 when an error occurred
|
||||
*/
|
||||
int scandir(
|
||||
const char* path,
|
||||
struct dirent*** output,
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
);
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename);
|
||||
bool isSupportedImageFile(const std::string& filename);
|
||||
bool isSupportedTextFile(const std::string& filename);
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
#include "FilesData.h"
|
||||
#include "Files.h"
|
||||
#include "app/AppContext.h"
|
||||
#include "Tactility.h"
|
||||
#include "Assets.h"
|
||||
#include "Check.h"
|
||||
#include "FileUtils.h"
|
||||
#include "StringUtils.h"
|
||||
#include "app/ElfApp.h"
|
||||
#include "app/imageviewer/ImageViewer.h"
|
||||
#include "app/textviewer/TextViewer.h"
|
||||
#include "lvgl.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
@@ -24,231 +11,21 @@ namespace tt::app::files {
|
||||
extern const AppManifest manifest;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
std::shared_ptr<Data> _Nullable optData() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<Data>(app->getData());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive check to see if the given file matches the provided file extension.
|
||||
* @param path the full path to the file
|
||||
* @param extension the extension to look for, including the period symbol, in lower case
|
||||
* @return true on match
|
||||
*/
|
||||
static bool hasFileExtension(const char* path, const char* extension) {
|
||||
size_t postfix_len = strlen(extension);
|
||||
size_t base_len = strlen(path);
|
||||
if (base_len < postfix_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = (int)postfix_len - 1; i >= 0; i--) {
|
||||
if (tolower(path[base_len - postfix_len + i]) != tolower(extension[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isSupportedExecutableFile(const char* filename) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return hasFileExtension(filename, ".elf");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool isSupportedImageFile(const char* filename) {
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return hasFileExtension(filename, ".png");
|
||||
}
|
||||
|
||||
static bool isSupportedTextFile(const char* filename) {
|
||||
return hasFileExtension(filename, ".txt") ||
|
||||
hasFileExtension(filename, ".ini") ||
|
||||
hasFileExtension(filename, ".json") ||
|
||||
hasFileExtension(filename, ".yaml") ||
|
||||
hasFileExtension(filename, ".yml") ||
|
||||
hasFileExtension(filename, ".lua") ||
|
||||
hasFileExtension(filename, ".js") ||
|
||||
hasFileExtension(filename, ".properties");
|
||||
}
|
||||
|
||||
// region Views
|
||||
|
||||
static void updateViews(std::shared_ptr<Data> data);
|
||||
|
||||
static void onNavigateUpPressed(TT_UNUSED lv_event_t* event) {
|
||||
auto files_data = optData();
|
||||
if (files_data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(files_data->current_path, "/") != 0) {
|
||||
TT_LOG_I(TAG, "Navigating upwards");
|
||||
char new_absolute_path[MAX_PATH_LENGTH];
|
||||
if (string::getPathParent(files_data->current_path, new_absolute_path)) {
|
||||
data_set_entries_for_path(files_data, new_absolute_path);
|
||||
}
|
||||
}
|
||||
|
||||
updateViews(files_data);
|
||||
}
|
||||
|
||||
static void viewFile(const char* path, const char* filename) {
|
||||
size_t path_len = strlen(path);
|
||||
size_t filename_len = strlen(filename);
|
||||
char* filepath = static_cast<char*>(malloc(path_len + filename_len + 2));
|
||||
sprintf(filepath, "%s/%s", path, filename);
|
||||
|
||||
// For PC we need to make the path relative to the current work directory,
|
||||
// because that's how LVGL maps its 'drive letter' to the file system.
|
||||
char* processed_filepath;
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
|
||||
TT_LOG_E(TAG, "Failed to get current working directory");
|
||||
return;
|
||||
}
|
||||
if (!strstr(filepath, cwd)) {
|
||||
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
|
||||
return;
|
||||
}
|
||||
char* substr = filepath + strlen(cwd);
|
||||
processed_filepath = substr;
|
||||
} else {
|
||||
processed_filepath = filepath;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Clicked %s", filepath);
|
||||
|
||||
if (isSupportedExecutableFile(filename)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
app::startElfApp(processed_filepath);
|
||||
#endif
|
||||
} else if (isSupportedImageFile(filename)) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
bundle->putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
service::loader::startApp("ImageViewer", false, bundle);
|
||||
} else if (isSupportedTextFile(filename)) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
} else {
|
||||
// Remove forward slash, because we need a relative path
|
||||
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath + 1);
|
||||
}
|
||||
service::loader::startApp("TextViewer", false, bundle);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "opening files of this type is not supported");
|
||||
}
|
||||
|
||||
free(filepath);
|
||||
}
|
||||
|
||||
static void onFilePressed(lv_event_t* event) {
|
||||
auto files_data = optData();
|
||||
if (files_data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
auto* dir_entry = static_cast<dirent*>(lv_event_get_user_data(event));
|
||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry->d_name, dir_entry->d_type);
|
||||
|
||||
switch (dir_entry->d_type) {
|
||||
case TT_DT_DIR:
|
||||
case TT_DT_CHR:
|
||||
data_set_entries_for_child_path(files_data, dir_entry->d_name);
|
||||
updateViews(files_data);
|
||||
break;
|
||||
case TT_DT_LNK:
|
||||
TT_LOG_W(TAG, "opening links is not supported");
|
||||
break;
|
||||
case TT_DT_REG:
|
||||
viewFile(files_data->current_path, dir_entry->d_name);
|
||||
break;
|
||||
default:
|
||||
// Assume it's a file
|
||||
// TODO: Find a better way to identify a file
|
||||
viewFile(files_data->current_path, dir_entry->d_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void createFileWidget(lv_obj_t* parent, struct dirent* dir_entry) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
const char* symbol;
|
||||
if (dir_entry->d_type == TT_DT_DIR || dir_entry->d_type == TT_DT_CHR) {
|
||||
symbol = LV_SYMBOL_DIRECTORY;
|
||||
} else if (isSupportedImageFile(dir_entry->d_name)) {
|
||||
symbol = LV_SYMBOL_IMAGE;
|
||||
} else if (dir_entry->d_type == TT_DT_LNK) {
|
||||
symbol = LV_SYMBOL_LOOP;
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry->d_name);
|
||||
lv_obj_add_event_cb(button, &onFilePressed, LV_EVENT_CLICKED, (void*)dir_entry);
|
||||
}
|
||||
|
||||
static void updateViews(std::shared_ptr<Data> data) {
|
||||
lv_obj_clean(data->list);
|
||||
for (int i = 0; i < data->dir_entries_count; ++i) {
|
||||
TT_LOG_D(TAG, "Entry: %s %d", data->dir_entries[i]->d_name, data->dir_entries[i]->d_type);
|
||||
createFileWidget(data->list, data->dir_entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Views
|
||||
|
||||
// region Lifecycle
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
|
||||
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressed, nullptr);
|
||||
|
||||
data->list = lv_list_create(parent);
|
||||
lv_obj_set_width(data->list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(data->list, 1);
|
||||
|
||||
updateViews(data);
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onShow(parent);
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto* test = new uint32_t;
|
||||
delete test;
|
||||
auto data = std::make_shared<Data>();
|
||||
// PC platform is bound to current work directory because of the LVGL file system mapping
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
|
||||
data_set_entries_for_path(data, cwd);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to get current work directory files");
|
||||
data_set_entries_for_path(data, "/");
|
||||
}
|
||||
} else {
|
||||
data_set_entries_for_path(data, "/");
|
||||
}
|
||||
|
||||
app.setData(data);
|
||||
auto files = std::make_shared<Files>();
|
||||
app.setData(files);
|
||||
}
|
||||
|
||||
// endregion Lifecycle
|
||||
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onResult(result, bundle);
|
||||
}
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Files",
|
||||
@@ -257,6 +34,7 @@ extern const AppManifest manifest = {
|
||||
.type = TypeHidden,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "View.h"
|
||||
#include "State.h"
|
||||
#include "app/AppManifest.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <dirent.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
class Files {
|
||||
std::unique_ptr<View> view;
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
public:
|
||||
Files() {
|
||||
state = std::make_shared<State>();
|
||||
view = std::make_unique<View>(state);
|
||||
}
|
||||
|
||||
void onShow(lv_obj_t* parent) {
|
||||
view->init(parent);
|
||||
}
|
||||
|
||||
void onResult(Result result, const Bundle& bundle) {
|
||||
view->onResult(result, bundle);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} // namespace
|
||||
@@ -1,96 +0,0 @@
|
||||
#include <cstring>
|
||||
#include "FilesData.h"
|
||||
#include "FileUtils.h"
|
||||
#include "StringUtils.h"
|
||||
#include "Tactility.h"
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
#define TAG "files_app"
|
||||
|
||||
static bool get_child_path(char* base_path, const char* child_path, char* out_path, size_t max_chars) {
|
||||
size_t current_path_length = strlen(base_path);
|
||||
size_t added_path_length = strlen(child_path);
|
||||
size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/`
|
||||
|
||||
if (total_path_length >= max_chars) {
|
||||
TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH);
|
||||
return false;
|
||||
} else {
|
||||
// Postfix with "/" when the current path isn't "/"
|
||||
if (current_path_length != 1) {
|
||||
sprintf(out_path, "%s/%s", base_path, child_path);
|
||||
} else {
|
||||
sprintf(out_path, "/%s", child_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void data_set_entries(std::shared_ptr<Data> data, struct dirent** entries, int count) {
|
||||
if (data->dir_entries != nullptr) {
|
||||
data->freeEntries();
|
||||
}
|
||||
|
||||
data->dir_entries = entries;
|
||||
data->dir_entries_count = count;
|
||||
}
|
||||
|
||||
bool data_set_entries_for_path(std::shared_ptr<Data> data, const char* path) {
|
||||
TT_LOG_I(TAG, "Changing path: %s -> %s", data->current_path, path);
|
||||
|
||||
/**
|
||||
* ESP32 does not have a root directory, so we have to create it manually.
|
||||
* We'll add the NVS Flash partitions and the binding for the sdcard.
|
||||
*/
|
||||
#if TT_SCREENSHOT_MODE
|
||||
bool show_custom_root = true;
|
||||
#else
|
||||
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp && strcmp(path, "/") == 0);
|
||||
#endif
|
||||
if (show_custom_root) {
|
||||
int dir_entries_count = 3;
|
||||
auto** dir_entries = (dirent**)malloc(sizeof(struct dirent*) * 3);
|
||||
|
||||
dir_entries[0] = (dirent*)malloc(sizeof(struct dirent));
|
||||
dir_entries[0]->d_type = TT_DT_DIR;
|
||||
strcpy(dir_entries[0]->d_name, "assets");
|
||||
|
||||
dir_entries[1] = (dirent*)malloc(sizeof(struct dirent));
|
||||
dir_entries[1]->d_type = TT_DT_DIR;
|
||||
strcpy(dir_entries[1]->d_name, "config");
|
||||
|
||||
dir_entries[2] = (dirent*)malloc(sizeof(struct dirent));
|
||||
dir_entries[2]->d_type = TT_DT_DIR;
|
||||
strcpy(dir_entries[2]->d_name, "sdcard");
|
||||
|
||||
data_set_entries(data, dir_entries, dir_entries_count);
|
||||
strcpy(data->current_path, path);
|
||||
return true;
|
||||
} else {
|
||||
struct dirent** entries = nullptr;
|
||||
int count = tt::app::files::scandir(path, &entries, &dirent_filter_dot_entries, &dirent_sort_alpha_and_type);
|
||||
if (count >= 0) {
|
||||
TT_LOG_I(TAG, "%s has %u entries", path, count);
|
||||
data_set_entries(data, entries, count);
|
||||
strcpy(data->current_path, path);
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to fetch entries for %s", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool data_set_entries_for_child_path(std::shared_ptr<Data> data, const char* child_path) {
|
||||
char new_absolute_path[MAX_PATH_LENGTH + 1];
|
||||
if (get_child_path(data->current_path, child_path, new_absolute_path, MAX_PATH_LENGTH)) {
|
||||
TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path);
|
||||
return data_set_entries_for_path(data, new_absolute_path);
|
||||
} else {
|
||||
TT_LOG_I(TAG, "Failed to get child path for %s/%s", data->current_path, child_path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include <dirent.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
#define MAX_PATH_LENGTH 256
|
||||
|
||||
struct Data {
|
||||
char current_path[MAX_PATH_LENGTH] = { 0 };
|
||||
struct dirent** dir_entries = nullptr;
|
||||
int dir_entries_count = 0;
|
||||
lv_obj_t* list = nullptr;
|
||||
|
||||
void freeEntries() {
|
||||
for (int i = 0; i < dir_entries_count; ++i) {
|
||||
free(dir_entries[i]);
|
||||
}
|
||||
free(dir_entries);
|
||||
dir_entries = nullptr;
|
||||
dir_entries_count = 0;
|
||||
}
|
||||
|
||||
~Data() {
|
||||
freeEntries();
|
||||
}
|
||||
};
|
||||
|
||||
void data_free(std::shared_ptr<Data> data);
|
||||
void data_free_entries(std::shared_ptr<Data> data);
|
||||
bool data_set_entries_for_child_path(std::shared_ptr<Data> data, const char* child_path);
|
||||
bool data_set_entries_for_path(std::shared_ptr<Data> data, const char* path);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "State.h"
|
||||
#include "kernel/Kernel.h"
|
||||
#include "Log.h"
|
||||
#include "FileUtils.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#define TAG "files_app"
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
State::State() {
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
|
||||
setEntriesForPath(cwd);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to get current work directory files");
|
||||
setEntriesForPath("/");
|
||||
}
|
||||
} else {
|
||||
setEntriesForPath("/");
|
||||
}
|
||||
}
|
||||
|
||||
std::string State::getSelectedChildPath() const {
|
||||
return getChildPath(current_path, selected_child_entry);
|
||||
}
|
||||
|
||||
bool State::setEntriesForPath(const std::string& path) {
|
||||
auto scoped_lock = mutex.scoped();
|
||||
if (!scoped_lock->lock(100)) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath");
|
||||
return false;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
|
||||
|
||||
/**
|
||||
* ESP32 does not have a root directory, so we have to create it manually.
|
||||
* We'll add the NVS Flash partitions and the binding for the sdcard.
|
||||
*/
|
||||
#if TT_SCREENSHOT_MODE
|
||||
bool show_custom_root = true;
|
||||
#else
|
||||
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
|
||||
#endif
|
||||
if (show_custom_root) {
|
||||
TT_LOG_I(TAG, "Setting custom root");
|
||||
dir_entries.clear();
|
||||
dir_entries.push_back({
|
||||
.d_ino = 0,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_name = "assets"
|
||||
});
|
||||
dir_entries.push_back({
|
||||
.d_ino = 1,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_name = "config"
|
||||
});
|
||||
dir_entries.push_back({
|
||||
.d_ino = 2,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_name = "sdcard"
|
||||
});
|
||||
|
||||
current_path = path;
|
||||
selected_child_entry = "";
|
||||
action = ActionNone;
|
||||
return true;
|
||||
} else {
|
||||
dir_entries.clear();
|
||||
int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type);
|
||||
if (count >= 0) {
|
||||
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
|
||||
current_path = path;
|
||||
selected_child_entry = "";
|
||||
action = ActionNone;
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool State::setEntriesForChildPath(const std::string& child_path) {
|
||||
auto path = getChildPath(current_path, child_path);
|
||||
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
||||
return setEntriesForPath(path);
|
||||
}
|
||||
|
||||
bool State::getDirent(uint32_t index, dirent& dirent) {
|
||||
auto scoped_mutex = mutex.scoped();
|
||||
if (!scoped_mutex->lock(50 / portTICK_PERIOD_MS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index < dir_entries.size()) {
|
||||
dirent = dir_entries[index];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <dirent.h>
|
||||
#include "Mutex.h"
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
class State {
|
||||
|
||||
public:
|
||||
|
||||
enum PendingAction {
|
||||
ActionNone,
|
||||
ActionDelete,
|
||||
ActionRename
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex = Mutex(Mutex::TypeRecursive);
|
||||
std::vector<dirent> dir_entries;
|
||||
std::string current_path;
|
||||
std::string selected_child_entry;
|
||||
PendingAction action = ActionNone;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
State();
|
||||
|
||||
void freeEntries() {
|
||||
dir_entries.clear();
|
||||
}
|
||||
|
||||
~State() {
|
||||
freeEntries();
|
||||
}
|
||||
|
||||
bool setEntriesForChildPath(const std::string& child_path);
|
||||
bool setEntriesForPath(const std::string& path);
|
||||
|
||||
const std::vector<dirent>& lockEntries() const {
|
||||
mutex.lock(TtWaitForever);
|
||||
return dir_entries;
|
||||
}
|
||||
|
||||
void unlockEntries() {
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
bool getDirent(uint32_t index, dirent& dirent);
|
||||
|
||||
void setSelectedChildEntry(const std::string& newFile) {
|
||||
selected_child_entry = newFile;
|
||||
action = ActionNone;
|
||||
}
|
||||
|
||||
std::string getSelectedChildEntry() const { return selected_child_entry; }
|
||||
std::string getCurrentPath() const { return current_path; }
|
||||
|
||||
std::string getSelectedChildPath() const;
|
||||
|
||||
PendingAction getPendingAction() const {
|
||||
return action;
|
||||
}
|
||||
|
||||
void setPendingAction(PendingAction newAction) {
|
||||
action = newAction;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
#include <cstring>
|
||||
#include "app/alertdialog/AlertDialog.h"
|
||||
#include "app/imageviewer/ImageViewer.h"
|
||||
#include "app/inputdialog/InputDialog.h"
|
||||
#include "app/textviewer/TextViewer.h"
|
||||
#include "app/ElfApp.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "lvgl/LvglSync.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "FileUtils.h"
|
||||
#include "Tactility.h"
|
||||
#include "View.h"
|
||||
#include "StringUtils.h"
|
||||
#include <filesystem>
|
||||
|
||||
#define TAG "files_app"
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
// region Callbacks
|
||||
|
||||
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
view->onDirEntryListScrollBegin();
|
||||
}
|
||||
|
||||
static void onDirEntryPressedCallback(lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
auto* button = lv_event_get_target_obj(event);
|
||||
auto index = lv_obj_get_index(button);
|
||||
view->onDirEntryPressed(index);
|
||||
}
|
||||
|
||||
static void onDirEntryLongPressedCallback(lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
auto* button = lv_event_get_target_obj(event);
|
||||
auto index = lv_obj_get_index(button);
|
||||
view->onDirEntryLongPressed(index);
|
||||
}
|
||||
|
||||
static void onRenamePressedCallback(lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
view->onRenamePressed();
|
||||
}
|
||||
|
||||
static void onDeletePressedCallback(lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
view->onDeletePressed();
|
||||
}
|
||||
|
||||
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||
auto* view = (View*)lv_event_get_user_data(event);
|
||||
view->onNavigateUpPressed();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
std::string file_path = path + "/" + filename;
|
||||
|
||||
// For PC we need to make the path relative to the current work directory,
|
||||
// because that's how LVGL maps its 'drive letter' to the file system.
|
||||
std::string processed_filepath;
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
|
||||
TT_LOG_E(TAG, "Failed to get current working directory");
|
||||
return;
|
||||
}
|
||||
if (!file_path.starts_with(cwd)) {
|
||||
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
|
||||
return;
|
||||
}
|
||||
processed_filepath = file_path.substr(strlen(cwd));
|
||||
} else {
|
||||
processed_filepath = file_path;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Clicked %s", file_path.c_str());
|
||||
|
||||
if (isSupportedExecutableFile(filename)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
app::startElfApp(processed_filepath);
|
||||
#endif
|
||||
} else if (isSupportedImageFile(filename)) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
bundle->putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
service::loader::startApp("ImageViewer", false, bundle);
|
||||
} else if (isSupportedTextFile(filename)) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
} else {
|
||||
// Remove forward slash, because we need a relative path
|
||||
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath.substr(1));
|
||||
}
|
||||
service::loader::startApp("TextViewer", false, bundle);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "opening files of this type is not supported");
|
||||
}
|
||||
|
||||
onNavigate();
|
||||
}
|
||||
|
||||
void View::onDirEntryPressed(uint32_t index) {
|
||||
dirent dir_entry;
|
||||
if (state->getDirent(index, dir_entry)) {
|
||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
||||
state->setSelectedChildEntry(dir_entry.d_name);
|
||||
switch (dir_entry.d_type) {
|
||||
case TT_DT_DIR:
|
||||
case TT_DT_CHR:
|
||||
state->setEntriesForChildPath(dir_entry.d_name);
|
||||
onNavigate();
|
||||
update();
|
||||
break;
|
||||
case TT_DT_LNK:
|
||||
TT_LOG_W(TAG, "opening links is not supported");
|
||||
break;
|
||||
case TT_DT_REG:
|
||||
viewFile(state->getCurrentPath(), dir_entry.d_name);
|
||||
onNavigate();
|
||||
break;
|
||||
default:
|
||||
// Assume it's a file
|
||||
// TODO: Find a better way to identify a file
|
||||
viewFile(state->getCurrentPath(), dir_entry.d_name);
|
||||
onNavigate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void View::onDirEntryLongPressed(int32_t index) {
|
||||
dirent dir_entry;
|
||||
if (state->getDirent(index, dir_entry)) {
|
||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
||||
state->setSelectedChildEntry(dir_entry.d_name);
|
||||
switch (dir_entry.d_type) {
|
||||
case TT_DT_DIR:
|
||||
case TT_DT_CHR:
|
||||
showActionsForDirectory();
|
||||
break;
|
||||
case TT_DT_LNK:
|
||||
TT_LOG_W(TAG, "opening links is not supported");
|
||||
break;
|
||||
case TT_DT_REG:
|
||||
showActionsForFile();
|
||||
break;
|
||||
default:
|
||||
// Assume it's a file
|
||||
// TODO: Find a better way to identify a file
|
||||
showActionsForFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
const char* symbol;
|
||||
if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) {
|
||||
symbol = LV_SYMBOL_DIRECTORY;
|
||||
} else if (isSupportedImageFile(dir_entry.d_name)) {
|
||||
symbol = LV_SYMBOL_IMAGE;
|
||||
} else if (dir_entry.d_type == TT_DT_LNK) {
|
||||
symbol = LV_SYMBOL_LOOP;
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
|
||||
}
|
||||
|
||||
void View::onNavigateUpPressed() {
|
||||
if (state->getCurrentPath() != "/") {
|
||||
TT_LOG_I(TAG, "Navigating upwards");
|
||||
std::string new_absolute_path;
|
||||
if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) {
|
||||
state->setEntriesForPath(new_absolute_path);
|
||||
}
|
||||
onNavigate();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void View::onRenamePressed() {
|
||||
std::string entry_name = state->getSelectedChildEntry();
|
||||
TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str());
|
||||
state->setPendingAction(State::ActionRename);
|
||||
app::inputdialog::start("Rename", "", entry_name);
|
||||
}
|
||||
|
||||
void View::onDeletePressed() {
|
||||
std::string file_path = state->getSelectedChildPath();
|
||||
TT_LOG_I(TAG, "Pending delete %s", file_path.c_str());
|
||||
state->setPendingAction(State::ActionDelete);
|
||||
std::string message = "Do you want to delete this?\n" + file_path;
|
||||
const std::vector<std::string> choices = { "Yes", "No" };
|
||||
app::alertdialog::start("Are you sure?", message, choices);
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
|
||||
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void View::showActionsForFile() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
|
||||
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
|
||||
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void View::update() {
|
||||
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
|
||||
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_clean(dir_entry_list);
|
||||
auto entries = state->lockEntries();
|
||||
for (auto entry : entries) {
|
||||
TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type);
|
||||
createDirEntryWidget(dir_entry_list, entry);
|
||||
}
|
||||
state->unlockEntries();
|
||||
|
||||
if (state->getCurrentPath() == "/") {
|
||||
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
} else {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl");
|
||||
}
|
||||
}
|
||||
|
||||
void View::init(lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
|
||||
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
dir_entry_list = lv_list_create(wrapper);
|
||||
lv_obj_set_height(dir_entry_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(dir_entry_list, 1);
|
||||
|
||||
lv_obj_add_event_cb(dir_entry_list, dirEntryListScrollBeginCallback, LV_EVENT_SCROLL_BEGIN, this);
|
||||
|
||||
action_list = lv_list_create(wrapper);
|
||||
lv_obj_set_height(action_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(action_list, 1);
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void View::onDirEntryListScrollBegin() {
|
||||
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
|
||||
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
void View::onNavigate() {
|
||||
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
|
||||
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
void View::onResult(Result result, const Bundle& bundle) {
|
||||
if (result != ResultOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string filepath = state->getSelectedChildPath();
|
||||
TT_LOG_I(TAG, "Result for %s", filepath.c_str());
|
||||
|
||||
switch (state->getPendingAction()) {
|
||||
case State::ActionDelete: {
|
||||
if (alertdialog::getResultIndex(bundle) == 0) {
|
||||
int delete_count = (int)remove(filepath.c_str());
|
||||
if (delete_count > 0) {
|
||||
TT_LOG_I(TAG, "Deleted %d items", delete_count);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str());
|
||||
}
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State::ActionRename: {
|
||||
auto new_name = app::inputdialog::getResult(bundle);
|
||||
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
|
||||
std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
|
||||
if (rename(filepath.c_str(), rename_to.c_str())) {
|
||||
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
|
||||
}
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "State.h"
|
||||
#include "app/AppManifest.h"
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
class View {
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
lv_obj_t* dir_entry_list = nullptr;
|
||||
lv_obj_t* action_list = nullptr;
|
||||
lv_obj_t* navigate_up_button = nullptr;
|
||||
|
||||
void showActionsForDirectory();
|
||||
void showActionsForFile();
|
||||
|
||||
void viewFile(const std::string&path, const std::string&filename);
|
||||
void createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry);
|
||||
void onNavigate();
|
||||
|
||||
public:
|
||||
|
||||
explicit View(const std::shared_ptr<State>& state) : state(state) {}
|
||||
|
||||
void init(lv_obj_t* parent);
|
||||
void update();
|
||||
|
||||
void onNavigateUpPressed();
|
||||
void onDirEntryPressed(uint32_t index);
|
||||
void onDirEntryLongPressed(int32_t index);
|
||||
void onRenamePressed();
|
||||
void onDeletePressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onResult(Result result, const Bundle& bundle);
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user