mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-20 07:25:06 +00:00
Implemented LaunchId and FileSelection, updated Notes (#281)
- Implemented `LaunchId` to keep track of the apps that are started - Implemented `FileSelection` app to select existing and/or new files. - Moved some re-usable file functionality to `tt::file::` - Renamed `Files` app to `FileBrowser` - Updated `Notes` app to use new `FileSelection` functionality, and cleaned it up a bit. - General code cleanliness improvements
This commit is contained in:
parent
74eb830870
commit
2691dbb014
@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <Tactility/hal/Configuration.h>
|
||||||
|
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
#include <sdkconfig.h>
|
#include <sdkconfig.h>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Main.h"
|
#include "Main.h"
|
||||||
#include <Tactility/hal/Configuration.h>
|
|
||||||
|
|
||||||
namespace simulator {
|
namespace simulator {
|
||||||
/** Set the function pointer of the real app_main() */
|
/** Set the function pointer of the real app_main() */
|
||||||
|
|||||||
@ -16,9 +16,9 @@ namespace tt::app {
|
|||||||
class AppContext;
|
class AppContext;
|
||||||
enum class Result;
|
enum class Result;
|
||||||
|
|
||||||
class App {
|
typedef unsigned int LaunchId;
|
||||||
|
|
||||||
private:
|
class App {
|
||||||
|
|
||||||
Mutex mutex;
|
Mutex mutex;
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ public:
|
|||||||
virtual void onDestroy(AppContext& appContext) {}
|
virtual void onDestroy(AppContext& appContext) {}
|
||||||
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
|
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
|
||||||
virtual void onHide(AppContext& appContext) {}
|
virtual void onHide(AppContext& appContext) {}
|
||||||
virtual void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
|
virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
|
||||||
|
|
||||||
Mutex& getMutex() { return mutex; }
|
Mutex& getMutex() { return mutex; }
|
||||||
|
|
||||||
@ -83,15 +83,15 @@ std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
|
|||||||
* @param[in] id application name or id
|
* @param[in] id application name or id
|
||||||
* @param[in] parameters optional parameters to pass onto the application
|
* @param[in] parameters optional parameters to pass onto the application
|
||||||
*/
|
*/
|
||||||
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||||
|
|
||||||
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
|
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
|
||||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
|
std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
|
||||||
|
|
||||||
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
||||||
std::shared_ptr<app::App> _Nullable getCurrentApp();
|
std::shared_ptr<App> _Nullable getCurrentApp();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ typedef void (*OnCreate)(void* appContext, void* _Nullable data);
|
|||||||
typedef void (*OnDestroy)(void* appContext, void* _Nullable data);
|
typedef void (*OnDestroy)(void* appContext, void* _Nullable data);
|
||||||
typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent);
|
typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent);
|
||||||
typedef void (*OnHide)(void* appContext, void* _Nullable data);
|
typedef void (*OnHide)(void* appContext, void* _Nullable data);
|
||||||
typedef void (*OnResult)(void* appContext, void* _Nullable data, Result result, Bundle* resultData);
|
typedef void (*OnResult)(void* appContext, void* _Nullable data, LaunchId launchId, Result result, Bundle* resultData);
|
||||||
|
|
||||||
void setElfAppManifest(
|
void setElfAppManifest(
|
||||||
const char* name,
|
const char* name,
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "app/files/View.h"
|
|
||||||
#include "app/files/State.h"
|
|
||||||
#include "app/AppManifest.h"
|
|
||||||
|
|
||||||
#include <lvgl.h>
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace tt::app::files {
|
|
||||||
|
|
||||||
void start();
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a file selection dialog that allows the user to select an existing file.
|
||||||
|
* This app returns the absolute file path as a result.
|
||||||
|
*/
|
||||||
|
LaunchId startForExistingFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a file selection dialog that allows the user to select a new or existing file.
|
||||||
|
* This app returns the absolute file path as a result.
|
||||||
|
*/
|
||||||
|
LaunchId startForExistingOrNewFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bundle the result bundle of an app
|
||||||
|
* @return the path from the bundle, or empty string if none is present
|
||||||
|
*/
|
||||||
|
std::string getResultPath(const Bundle& bundle);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@ -53,7 +53,7 @@ std::shared_ptr<SdCardDevice> _Nullable find(const std::string& path);
|
|||||||
* Always calls the function, but doesn't lock if the path is not an SD card path.
|
* Always calls the function, but doesn't lock if the path is not an SD card path.
|
||||||
*/
|
*/
|
||||||
template<typename ReturnType>
|
template<typename ReturnType>
|
||||||
inline ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
|
ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
|
||||||
auto sdcard = find(path);
|
auto sdcard = find(path);
|
||||||
if (sdcard != nullptr) {
|
if (sdcard != nullptr) {
|
||||||
auto scoped_lockable = sdcard->getLock().asScopedLock();
|
auto scoped_lockable = sdcard->getLock().asScopedLock();
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
#include <Tactility/Bundle.h>
|
#include <Tactility/Bundle.h>
|
||||||
#include <Tactility/PubSub.h>
|
#include <Tactility/PubSub.h>
|
||||||
#include <Tactility/service/ServiceManifest.h>
|
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ struct LoaderEvent {
|
|||||||
* @param[in] id application name or id
|
* @param[in] id application name or id
|
||||||
* @param[in] parameters optional parameters to pass onto the application
|
* @param[in] parameters optional parameters to pass onto the application
|
||||||
*/
|
*/
|
||||||
void startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||||
|
|
||||||
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
||||||
void stopApp();
|
void stopApp();
|
||||||
|
|||||||
@ -25,22 +25,21 @@ enum class State {
|
|||||||
*/
|
*/
|
||||||
class AppInstance : public AppContext {
|
class AppInstance : public AppContext {
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Mutex mutex = Mutex(Mutex::Type::Normal);
|
Mutex mutex = Mutex(Mutex::Type::Normal);
|
||||||
const std::shared_ptr<AppManifest> manifest;
|
const std::shared_ptr<AppManifest> manifest;
|
||||||
State state = State::Initial;
|
State state = State::Initial;
|
||||||
|
LaunchId launchId;
|
||||||
Flags flags = { .showStatusbar = true };
|
Flags flags = { .showStatusbar = true };
|
||||||
/** @brief Optional parameters to start the app with
|
/** @brief Optional parameters to start the app with
|
||||||
* When these are stored in the app struct, the struct takes ownership.
|
* When these are stored in the app struct, the struct takes ownership.
|
||||||
* Do not mutate after app creation.
|
* Do not mutate after app creation.
|
||||||
*/
|
*/
|
||||||
std::shared_ptr<const tt::Bundle> _Nullable parameters;
|
std::shared_ptr<const Bundle> _Nullable parameters;
|
||||||
|
|
||||||
std::shared_ptr<App> app;
|
std::shared_ptr<App> app;
|
||||||
|
|
||||||
static std::shared_ptr<app::App> createApp(
|
static std::shared_ptr<App> createApp(
|
||||||
const std::shared_ptr<app::AppManifest>& manifest
|
const std::shared_ptr<AppManifest>& manifest
|
||||||
) {
|
) {
|
||||||
if (manifest->location.isInternal()) {
|
if (manifest->location.isInternal()) {
|
||||||
assert(manifest->createApp != nullptr);
|
assert(manifest->createApp != nullptr);
|
||||||
@ -50,7 +49,7 @@ private:
|
|||||||
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
|
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
|
||||||
}
|
}
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
return app::createElfApp(manifest);
|
return createElfApp(manifest);
|
||||||
#else
|
#else
|
||||||
tt_crash("not supported");
|
tt_crash("not supported");
|
||||||
#endif
|
#endif
|
||||||
@ -61,18 +60,23 @@ private:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest) :
|
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId) :
|
||||||
manifest(manifest),
|
manifest(manifest),
|
||||||
|
launchId(launchId),
|
||||||
app(createApp(manifest))
|
app(createApp(manifest))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
AppInstance(const std::shared_ptr<AppManifest>& manifest, std::shared_ptr<const Bundle> parameters) :
|
AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId, std::shared_ptr<const Bundle> parameters) :
|
||||||
manifest(manifest),
|
manifest(manifest),
|
||||||
|
launchId(launchId),
|
||||||
parameters(std::move(parameters)),
|
parameters(std::move(parameters)),
|
||||||
app(createApp(manifest)) {}
|
app(createApp(manifest))
|
||||||
|
{}
|
||||||
|
|
||||||
~AppInstance() override = default;
|
~AppInstance() override = default;
|
||||||
|
|
||||||
|
LaunchId getLaunchId() const { return launchId; }
|
||||||
|
|
||||||
void setState(State state);
|
void setState(State state);
|
||||||
State getState() const;
|
State getState() const;
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
class State {
|
class State {
|
||||||
|
|
||||||
11
Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h
Normal file
11
Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
|
bool isSupportedExecutableFile(const std::string& filename);
|
||||||
|
bool isSupportedImageFile(const std::string& filename);
|
||||||
|
bool isSupportedTextFile(const std::string& filename);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@ -7,7 +7,7 @@
|
|||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
class View {
|
class View {
|
||||||
std::shared_ptr<State> state;
|
std::shared_ptr<State> state;
|
||||||
@ -1,66 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <string>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace tt::app::files {
|
|
||||||
|
|
||||||
/** File types for `dirent`'s `d_type`. */
|
|
||||||
enum {
|
|
||||||
TT_DT_UNKNOWN = 0,
|
|
||||||
#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type
|
|
||||||
TT_DT_FIFO = 1,
|
|
||||||
#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO
|
|
||||||
TT_DT_CHR = 2,
|
|
||||||
#define TT_DT_CHR TT_DT_CHR // Character device
|
|
||||||
TT_DT_DIR = 4,
|
|
||||||
#define TT_DT_DIR TT_DT_DIR // Directory
|
|
||||||
TT_DT_BLK = 6,
|
|
||||||
#define TT_DT_BLK TT_DT_BLK // Block device
|
|
||||||
TT_DT_REG = 8,
|
|
||||||
#define TT_DT_REG TT_DT_REG // Regular file
|
|
||||||
TT_DT_LNK = 10,
|
|
||||||
#define TT_DT_LNK TT_DT_LNK // Symbolic link
|
|
||||||
TT_DT_SOCK = 12,
|
|
||||||
#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket
|
|
||||||
TT_DT_WHT = 14
|
|
||||||
#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 bool (*ScandirSort)(const struct dirent&, const struct dirent&);
|
|
||||||
|
|
||||||
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
|
|
||||||
|
|
||||||
int dirent_filter_dot_entries(const struct dirent* entry);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A scandir()-like implementation that works on ESP32.
|
|
||||||
* It does not return "." and ".." items but otherwise functions the same.
|
|
||||||
* It returns an allocated output array with allocated dirent instances.
|
|
||||||
* The caller is responsible for free-ing the memory of these.
|
|
||||||
*
|
|
||||||
* @param[in] path path the scan for files and directories
|
|
||||||
* @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 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
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Tactility/Bundle.h>
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
Existing = 0,
|
||||||
|
ExistingOrNew = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
Mode getMode(const Bundle& bundle);
|
||||||
|
|
||||||
|
}
|
||||||
59
Tactility/Private/Tactility/app/fileselection/State.h
Normal file
59
Tactility/Private/Tactility/app/fileselection/State.h
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Tactility/Mutex.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
class State {
|
||||||
|
|
||||||
|
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||||
|
std::vector<dirent> dir_entries;
|
||||||
|
std::string current_path;
|
||||||
|
std::string selected_child_entry;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
State();
|
||||||
|
|
||||||
|
void freeEntries() {
|
||||||
|
dir_entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
~State() {
|
||||||
|
freeEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setEntriesForChildPath(const std::string& child_path);
|
||||||
|
bool setEntriesForPath(const std::string& path);
|
||||||
|
|
||||||
|
template <std::invocable<const std::vector<dirent> &> Func>
|
||||||
|
void withEntries(Func&& onEntries) const {
|
||||||
|
mutex.withLock([&]() {
|
||||||
|
std::invoke(std::forward<Func>(onEntries), dir_entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getDirent(uint32_t index, dirent& dirent);
|
||||||
|
|
||||||
|
void setSelectedChildEntry(const std::string& newFile) {
|
||||||
|
selected_child_entry = newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getSelectedChildEntry() const { return selected_child_entry; }
|
||||||
|
std::string getCurrentPath() const { return current_path; }
|
||||||
|
std::string getCurrentPathWithTrailingSlash() const {
|
||||||
|
if (current_path.length() > 1) {
|
||||||
|
return current_path + "/";
|
||||||
|
} else {
|
||||||
|
return current_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getSelectedChildPath() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
44
Tactility/Private/Tactility/app/fileselection/View.h
Normal file
44
Tactility/Private/Tactility/app/fileselection/View.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./State.h"
|
||||||
|
#include "./FileSelectionPrivate.h"
|
||||||
|
|
||||||
|
#include "Tactility/app/AppManifest.h"
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
class View {
|
||||||
|
std::shared_ptr<State> state;
|
||||||
|
|
||||||
|
lv_obj_t* dir_entry_list = nullptr;
|
||||||
|
lv_obj_t* navigate_up_button = nullptr;
|
||||||
|
lv_obj_t* path_textarea = nullptr;
|
||||||
|
lv_obj_t* select_button = nullptr;
|
||||||
|
std::function<void(std::string path)> on_file_selected;
|
||||||
|
|
||||||
|
void onTapFile(const std::string&path, const std::string&filename);
|
||||||
|
static void onSelectButtonPressed(lv_event_t* event);
|
||||||
|
static void onPathTextChanged(lv_event_t* event);
|
||||||
|
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit View(const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
|
||||||
|
state(state),
|
||||||
|
on_file_selected(std::move(onFileSelected))
|
||||||
|
{}
|
||||||
|
|
||||||
|
void init(lv_obj_t* parent, Mode mode);
|
||||||
|
void update();
|
||||||
|
|
||||||
|
void onNavigateUpPressed();
|
||||||
|
void onDirEntryPressed(uint32_t index);
|
||||||
|
void onFileSelected(const std::string& path) const {
|
||||||
|
on_file_selected(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -37,7 +37,8 @@ namespace app {
|
|||||||
namespace chat { extern const AppManifest manifest; }
|
namespace chat { extern const AppManifest manifest; }
|
||||||
namespace boot { extern const AppManifest manifest; }
|
namespace boot { extern const AppManifest manifest; }
|
||||||
namespace display { extern const AppManifest manifest; }
|
namespace display { extern const AppManifest manifest; }
|
||||||
namespace files { extern const AppManifest manifest; }
|
namespace filebrowser { extern const AppManifest manifest; }
|
||||||
|
namespace fileselection { extern const AppManifest manifest; }
|
||||||
namespace gpio { extern const AppManifest manifest; }
|
namespace gpio { extern const AppManifest manifest; }
|
||||||
namespace gpssettings { extern const AppManifest manifest; }
|
namespace gpssettings { extern const AppManifest manifest; }
|
||||||
namespace i2cscanner { extern const AppManifest manifest; }
|
namespace i2cscanner { extern const AppManifest manifest; }
|
||||||
@ -78,7 +79,8 @@ static void registerSystemApps() {
|
|||||||
addApp(app::applist::manifest);
|
addApp(app::applist::manifest);
|
||||||
addApp(app::calculator::manifest);
|
addApp(app::calculator::manifest);
|
||||||
addApp(app::display::manifest);
|
addApp(app::display::manifest);
|
||||||
addApp(app::files::manifest);
|
addApp(app::filebrowser::manifest);
|
||||||
|
addApp(app::fileselection::manifest);
|
||||||
addApp(app::gpio::manifest);
|
addApp(app::gpio::manifest);
|
||||||
addApp(app::gpssettings::manifest);
|
addApp(app::gpssettings::manifest);
|
||||||
addApp(app::i2cscanner::manifest);
|
addApp(app::i2cscanner::manifest);
|
||||||
|
|||||||
@ -4,19 +4,19 @@
|
|||||||
|
|
||||||
namespace tt::app {
|
namespace tt::app {
|
||||||
|
|
||||||
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
|
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
|
||||||
service::loader::startApp(id, std::move(parameters));
|
return service::loader::startApp(id, std::move(parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
service::loader::stopApp();
|
service::loader::stopApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
|
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
|
||||||
return service::loader::getCurrentAppContext();
|
return service::loader::getCurrentAppContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<app::App> _Nullable getCurrentApp() {
|
std::shared_ptr<App> _Nullable getCurrentApp() {
|
||||||
return service::loader::getCurrentApp();
|
return service::loader::getCurrentApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,8 +36,6 @@ static ElfManifest elfManifest;
|
|||||||
|
|
||||||
class ElfApp : public App {
|
class ElfApp : public App {
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
const std::string filePath;
|
const std::string filePath;
|
||||||
std::unique_ptr<uint8_t[]> elfFileData;
|
std::unique_ptr<uint8_t[]> elfFileData;
|
||||||
esp_elf_t elf;
|
esp_elf_t elf;
|
||||||
@ -143,9 +141,9 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
|
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultBundle) override {
|
||||||
if (manifest != nullptr && manifest->onResult != nullptr) {
|
if (manifest != nullptr && manifest->onResult != nullptr) {
|
||||||
manifest->onResult(&appContext, data, result, resultBundle.get());
|
manifest->onResult(&appContext, data, launchId, result, resultBundle.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#include "Tactility/app/files/View.h"
|
#include "Tactility/app/filebrowser/View.h"
|
||||||
#include "Tactility/app/files/State.h"
|
#include "Tactility/app/filebrowser/State.h"
|
||||||
#include "Tactility/app/AppContext.h"
|
#include "Tactility/app/AppContext.h"
|
||||||
|
|
||||||
#include <Tactility/Assets.h>
|
#include <Tactility/Assets.h>
|
||||||
@ -7,18 +7,18 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
#define TAG "files_app"
|
#define TAG "filebrowser_app"
|
||||||
|
|
||||||
extern const AppManifest manifest;
|
extern const AppManifest manifest;
|
||||||
|
|
||||||
class FilesApp : public App {
|
class FileBrowser : public App {
|
||||||
std::unique_ptr<View> view;
|
std::unique_ptr<View> view;
|
||||||
std::shared_ptr<State> state;
|
std::shared_ptr<State> state;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
FilesApp() {
|
FileBrowser() {
|
||||||
state = std::make_shared<State>();
|
state = std::make_shared<State>();
|
||||||
view = std::make_unique<View>(state);
|
view = std::make_unique<View>(state);
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ public:
|
|||||||
view->init(parent);
|
view->init(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
|
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||||
view->onResult(result, std::move(bundle));
|
view->onResult(result, std::move(bundle));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -37,7 +37,7 @@ extern const AppManifest manifest = {
|
|||||||
.name = "Files",
|
.name = "Files",
|
||||||
.icon = TT_ASSETS_APP_ICON_FILES,
|
.icon = TT_ASSETS_APP_ICON_FILES,
|
||||||
.type = Type::Hidden,
|
.type = Type::Hidden,
|
||||||
.createApp = create<FilesApp>
|
.createApp = create<FileBrowser>
|
||||||
};
|
};
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#include "Tactility/app/files/State.h"
|
#include "Tactility/app/filebrowser/State.h"
|
||||||
#include "Tactility/app/files/FileUtils.h"
|
|
||||||
|
|
||||||
|
#include <Tactility/file/File.h>
|
||||||
#include "Tactility/hal/sdcard/SdCardDevice.h"
|
#include "Tactility/hal/sdcard/SdCardDevice.h"
|
||||||
#include <Tactility/Log.h>
|
#include <Tactility/Log.h>
|
||||||
#include <Tactility/Partitions.h>
|
#include <Tactility/Partitions.h>
|
||||||
@ -11,9 +11,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
|
||||||
#define TAG "files_app"
|
#define TAG "filebrowser_app"
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
State::State() {
|
State::State() {
|
||||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||||
@ -30,7 +30,7 @@ State::State() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string State::getSelectedChildPath() const {
|
std::string State::getSelectedChildPath() const {
|
||||||
return getChildPath(current_path, selected_child_entry);
|
return file::getChildPath(current_path, selected_child_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool State::setEntriesForPath(const std::string& path) {
|
bool State::setEntriesForPath(const std::string& path) {
|
||||||
@ -52,12 +52,12 @@ bool State::setEntriesForPath(const std::string& path) {
|
|||||||
dir_entries.clear();
|
dir_entries.clear();
|
||||||
dir_entries.push_back(dirent{
|
dir_entries.push_back(dirent{
|
||||||
.d_ino = 0,
|
.d_ino = 0,
|
||||||
.d_type = TT_DT_DIR,
|
.d_type = file::TT_DT_DIR,
|
||||||
.d_name = SYSTEM_PARTITION_NAME
|
.d_name = SYSTEM_PARTITION_NAME
|
||||||
});
|
});
|
||||||
dir_entries.push_back(dirent{
|
dir_entries.push_back(dirent{
|
||||||
.d_ino = 1,
|
.d_ino = 1,
|
||||||
.d_type = TT_DT_DIR,
|
.d_type = file::TT_DT_DIR,
|
||||||
.d_name = DATA_PARTITION_NAME
|
.d_name = DATA_PARTITION_NAME
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ bool State::setEntriesForPath(const std::string& path) {
|
|||||||
auto mount_name = sdcard->getMountPath().substr(1);
|
auto mount_name = sdcard->getMountPath().substr(1);
|
||||||
auto dir_entry = dirent {
|
auto dir_entry = dirent {
|
||||||
.d_ino = 2,
|
.d_ino = 2,
|
||||||
.d_type = TT_DT_DIR,
|
.d_type = file::TT_DT_DIR,
|
||||||
.d_name = { 0 }
|
.d_name = { 0 }
|
||||||
};
|
};
|
||||||
assert(mount_name.length() < sizeof(dirent::d_name));
|
assert(mount_name.length() < sizeof(dirent::d_name));
|
||||||
@ -83,7 +83,7 @@ bool State::setEntriesForPath(const std::string& path) {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
dir_entries.clear();
|
dir_entries.clear();
|
||||||
int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type);
|
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
|
||||||
if (count >= 0) {
|
if (count >= 0) {
|
||||||
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
|
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
|
||||||
current_path = path;
|
current_path = path;
|
||||||
@ -97,8 +97,8 @@ bool State::setEntriesForPath(const std::string& path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool State::setEntriesForChildPath(const std::string& child_path) {
|
bool State::setEntriesForChildPath(const std::string& childPath) {
|
||||||
auto path = getChildPath(current_path, child_path);
|
auto path = file::getChildPath(current_path, childPath);
|
||||||
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
||||||
return setEntriesForPath(path);
|
return setEntriesForPath(path);
|
||||||
}
|
}
|
||||||
34
Tactility/Source/app/filebrowser/SupportedFiles.cpp
Normal file
34
Tactility/Source/app/filebrowser/SupportedFiles.cpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#include <Tactility/StringUtils.h>
|
||||||
|
#include <Tactility/TactilityCore.h>
|
||||||
|
|
||||||
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
|
#define TAG "filebrowser_app"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSupportedImageFile(const std::string& filename) {
|
||||||
|
// Currently only the PNG library is built into Tactility
|
||||||
|
return string::lowercase(filename).ends_with(".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSupportedTextFile(const std::string& filename) {
|
||||||
|
std::string filename_lower = string::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");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tt::app::filebrowser
|
||||||
@ -1,5 +1,5 @@
|
|||||||
#include "Tactility/app/files/FileUtils.h"
|
#include "Tactility/app/filebrowser/View.h"
|
||||||
#include "Tactility/app/files/View.h"
|
#include "Tactility/app/filebrowser/SupportedFiles.h"
|
||||||
|
|
||||||
#include "Tactility/app/alertdialog/AlertDialog.h"
|
#include "Tactility/app/alertdialog/AlertDialog.h"
|
||||||
#include "Tactility/app/imageviewer/ImageViewer.h"
|
#include "Tactility/app/imageviewer/ImageViewer.h"
|
||||||
@ -10,6 +10,7 @@
|
|||||||
#include "Tactility/lvgl/LvglSync.h"
|
#include "Tactility/lvgl/LvglSync.h"
|
||||||
|
|
||||||
#include <Tactility/Tactility.h>
|
#include <Tactility/Tactility.h>
|
||||||
|
#include "Tactility/file/File.h"
|
||||||
#include <Tactility/StringUtils.h>
|
#include <Tactility/StringUtils.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -19,43 +20,43 @@
|
|||||||
#include "Tactility/service/loader/Loader.h"
|
#include "Tactility/service/loader/Loader.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define TAG "files_app"
|
#define TAG "filebrowser_app"
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::filebrowser {
|
||||||
|
|
||||||
// region Callbacks
|
// region Callbacks
|
||||||
|
|
||||||
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
|
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
view->onDirEntryListScrollBegin();
|
view->onDirEntryListScrollBegin();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onDirEntryPressedCallback(lv_event_t* event) {
|
static void onDirEntryPressedCallback(lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
auto* button = lv_event_get_target_obj(event);
|
auto* button = lv_event_get_target_obj(event);
|
||||||
auto index = lv_obj_get_index(button);
|
auto index = lv_obj_get_index(button);
|
||||||
view->onDirEntryPressed(index);
|
view->onDirEntryPressed(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onDirEntryLongPressedCallback(lv_event_t* event) {
|
static void onDirEntryLongPressedCallback(lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
auto* button = lv_event_get_target_obj(event);
|
auto* button = lv_event_get_target_obj(event);
|
||||||
auto index = lv_obj_get_index(button);
|
auto index = lv_obj_get_index(button);
|
||||||
view->onDirEntryLongPressed(index);
|
view->onDirEntryLongPressed(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onRenamePressedCallback(lv_event_t* event) {
|
static void onRenamePressedCallback(lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
view->onRenamePressed();
|
view->onRenamePressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onDeletePressedCallback(lv_event_t* event) {
|
static void onDeletePressedCallback(lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
view->onDeletePressed();
|
view->onDeletePressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||||
auto* view = (View*)lv_event_get_user_data(event);
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
view->onNavigateUpPressed();
|
view->onNavigateUpPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,18 +87,18 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
|||||||
|
|
||||||
if (isSupportedExecutableFile(filename)) {
|
if (isSupportedExecutableFile(filename)) {
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
app::registerElfApp(processed_filepath);
|
registerElfApp(processed_filepath);
|
||||||
auto app_id = app::getElfAppId(processed_filepath);
|
auto app_id = getElfAppId(processed_filepath);
|
||||||
service::loader::startApp(app_id);
|
service::loader::startApp(app_id);
|
||||||
#endif
|
#endif
|
||||||
} else if (isSupportedImageFile(filename)) {
|
} else if (isSupportedImageFile(filename)) {
|
||||||
app::imageviewer::start(processed_filepath);
|
imageviewer::start(processed_filepath);
|
||||||
} else if (isSupportedTextFile(filename)) {
|
} else if (isSupportedTextFile(filename)) {
|
||||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||||
app::textviewer::start(processed_filepath);
|
textviewer::start(processed_filepath);
|
||||||
} else {
|
} else {
|
||||||
// Remove forward slash, because we need a relative path
|
// Remove forward slash, because we need a relative path
|
||||||
app::textviewer::start(processed_filepath.substr(1));
|
textviewer::start(processed_filepath.substr(1));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TT_LOG_W(TAG, "opening files of this type is not supported");
|
TT_LOG_W(TAG, "opening files of this type is not supported");
|
||||||
@ -111,6 +112,7 @@ void View::onDirEntryPressed(uint32_t index) {
|
|||||||
if (state->getDirent(index, dir_entry)) {
|
if (state->getDirent(index, dir_entry)) {
|
||||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
||||||
state->setSelectedChildEntry(dir_entry.d_name);
|
state->setSelectedChildEntry(dir_entry.d_name);
|
||||||
|
using namespace tt::file;
|
||||||
switch (dir_entry.d_type) {
|
switch (dir_entry.d_type) {
|
||||||
case TT_DT_DIR:
|
case TT_DT_DIR:
|
||||||
case TT_DT_CHR:
|
case TT_DT_CHR:
|
||||||
@ -140,6 +142,7 @@ void View::onDirEntryLongPressed(int32_t index) {
|
|||||||
if (state->getDirent(index, dir_entry)) {
|
if (state->getDirent(index, dir_entry)) {
|
||||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
|
||||||
state->setSelectedChildEntry(dir_entry.d_name);
|
state->setSelectedChildEntry(dir_entry.d_name);
|
||||||
|
using namespace file;
|
||||||
switch (dir_entry.d_type) {
|
switch (dir_entry.d_type) {
|
||||||
case TT_DT_DIR:
|
case TT_DT_DIR:
|
||||||
case TT_DT_CHR:
|
case TT_DT_CHR:
|
||||||
@ -161,15 +164,14 @@ void View::onDirEntryLongPressed(int32_t index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) {
|
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||||
tt_check(parent);
|
tt_check(list);
|
||||||
auto* list = (lv_obj_t*)parent;
|
|
||||||
const char* symbol;
|
const char* symbol;
|
||||||
if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) {
|
if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) {
|
||||||
symbol = LV_SYMBOL_DIRECTORY;
|
symbol = LV_SYMBOL_DIRECTORY;
|
||||||
} else if (isSupportedImageFile(dir_entry.d_name)) {
|
} else if (isSupportedImageFile(dir_entry.d_name)) {
|
||||||
symbol = LV_SYMBOL_IMAGE;
|
symbol = LV_SYMBOL_IMAGE;
|
||||||
} else if (dir_entry.d_type == TT_DT_LNK) {
|
} else if (dir_entry.d_type == file::TT_DT_LNK) {
|
||||||
symbol = LV_SYMBOL_LOOP;
|
symbol = LV_SYMBOL_LOOP;
|
||||||
} else {
|
} else {
|
||||||
symbol = LV_SYMBOL_FILE;
|
symbol = LV_SYMBOL_FILE;
|
||||||
@ -195,7 +197,7 @@ void View::onRenamePressed() {
|
|||||||
std::string entry_name = state->getSelectedChildEntry();
|
std::string entry_name = state->getSelectedChildEntry();
|
||||||
TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str());
|
TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str());
|
||||||
state->setPendingAction(State::ActionRename);
|
state->setPendingAction(State::ActionRename);
|
||||||
app::inputdialog::start("Rename", "", entry_name);
|
inputdialog::start("Rename", "", entry_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void View::onDeletePressed() {
|
void View::onDeletePressed() {
|
||||||
@ -204,7 +206,7 @@ void View::onDeletePressed() {
|
|||||||
state->setPendingAction(State::ActionDelete);
|
state->setPendingAction(State::ActionDelete);
|
||||||
std::string message = "Do you want to delete this?\n" + file_path;
|
std::string message = "Do you want to delete this?\n" + file_path;
|
||||||
const std::vector<std::string> choices = { "Yes", "No" };
|
const std::vector<std::string> choices = { "Yes", "No" };
|
||||||
app::alertdialog::start("Are you sure?", message, choices);
|
alertdialog::start("Are you sure?", message, choices);
|
||||||
}
|
}
|
||||||
|
|
||||||
void View::showActionsForDirectory() {
|
void View::showActionsForDirectory() {
|
||||||
@ -301,7 +303,7 @@ void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
|
|||||||
switch (state->getPendingAction()) {
|
switch (state->getPendingAction()) {
|
||||||
case State::ActionDelete: {
|
case State::ActionDelete: {
|
||||||
if (alertdialog::getResultIndex(*bundle) == 0) {
|
if (alertdialog::getResultIndex(*bundle) == 0) {
|
||||||
int delete_count = (int)remove(filepath.c_str());
|
int delete_count = remove(filepath.c_str());
|
||||||
if (delete_count > 0) {
|
if (delete_count > 0) {
|
||||||
TT_LOG_I(TAG, "Deleted %d items", delete_count);
|
TT_LOG_I(TAG, "Deleted %d items", delete_count);
|
||||||
} else {
|
} else {
|
||||||
@ -313,9 +315,9 @@ void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::ActionRename: {
|
case State::ActionRename: {
|
||||||
auto new_name = app::inputdialog::getResult(*bundle);
|
auto new_name = inputdialog::getResult(*bundle);
|
||||||
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
|
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
|
||||||
std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
|
std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name);
|
||||||
if (rename(filepath.c_str(), rename_to.c_str())) {
|
if (rename(filepath.c_str(), rename_to.c_str())) {
|
||||||
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
|
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
|
||||||
} else {
|
} else {
|
||||||
@ -1,91 +0,0 @@
|
|||||||
#include "Tactility/app/files/FileUtils.h"
|
|
||||||
|
|
||||||
#include <Tactility/StringUtils.h>
|
|
||||||
#include <Tactility/TactilityCore.h>
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace tt::app::files {
|
|
||||||
|
|
||||||
#define TAG "file_utils"
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) < 0;
|
|
||||||
} else {
|
|
||||||
return left_is_dir > right_is_dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int scandir(
|
|
||||||
const std::string& path,
|
|
||||||
std::vector<dirent>& outList,
|
|
||||||
ScandirFilter _Nullable filterMethod,
|
|
||||||
ScandirSort _Nullable sortMethod
|
|
||||||
) {
|
|
||||||
TT_LOG_I(TAG, "scandir start");
|
|
||||||
DIR* dir = opendir(path.c_str());
|
|
||||||
if (dir == nullptr) {
|
|
||||||
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct dirent* current_entry;
|
|
||||||
while ((current_entry = readdir(dir)) != nullptr) {
|
|
||||||
if (filterMethod(current_entry) == 0) {
|
|
||||||
outList.push_back(*current_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closedir(dir);
|
|
||||||
|
|
||||||
if (sortMethod != nullptr) {
|
|
||||||
sort(outList.begin(), outList.end(), sortMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
TT_LOG_I(TAG, "scandir finish");
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSupportedImageFile(const std::string& filename) {
|
|
||||||
// Currently only the PNG library is built into Tactility
|
|
||||||
return string::lowercase(filename).ends_with(".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSupportedTextFile(const std::string& filename) {
|
|
||||||
std::string filename_lower = string::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");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace tt::app::files
|
|
||||||
78
Tactility/Source/app/fileselection/FileSelection.cpp
Normal file
78
Tactility/Source/app/fileselection/FileSelection.cpp
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#include "Tactility/app/fileselection/FileSelectionPrivate.h"
|
||||||
|
#include "Tactility/app/fileselection/View.h"
|
||||||
|
#include "Tactility/app/fileselection/State.h"
|
||||||
|
#include "Tactility/app/AppContext.h"
|
||||||
|
|
||||||
|
#include <Tactility/Assets.h>
|
||||||
|
#include <Tactility/service/loader/Loader.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
#define TAG "fileselection_app"
|
||||||
|
|
||||||
|
extern const AppManifest manifest;
|
||||||
|
|
||||||
|
std::string getResultPath(const Bundle& bundle) {
|
||||||
|
std::string result;
|
||||||
|
if (bundle.optString("path", result)) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode getMode(const Bundle& bundle) {
|
||||||
|
int32_t mode = static_cast<int32_t>(Mode::ExistingOrNew);
|
||||||
|
bundle.optInt32("mode", mode);
|
||||||
|
return static_cast<Mode>(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMode(Bundle& bundle, Mode mode) {
|
||||||
|
auto mode_int = static_cast<int32_t>(mode);
|
||||||
|
bundle.putInt32("mode", mode_int);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSelection : public App {
|
||||||
|
std::unique_ptr<View> view;
|
||||||
|
std::shared_ptr<State> state;
|
||||||
|
|
||||||
|
public:
|
||||||
|
FileSelection() {
|
||||||
|
state = std::make_shared<State>();
|
||||||
|
view = std::make_unique<View>(state, [this](const std::string& path) {
|
||||||
|
auto bundle = std::make_unique<Bundle>();
|
||||||
|
bundle->putString("path", path);
|
||||||
|
setResult(Result::Ok, std::move(bundle));
|
||||||
|
service::loader::stopApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onShow(AppContext& appContext, lv_obj_t* parent) override {
|
||||||
|
auto mode = getMode(*appContext.getParameters());
|
||||||
|
view->init(parent, mode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const AppManifest manifest = {
|
||||||
|
.id = "FileSelection",
|
||||||
|
.name = "File Selection",
|
||||||
|
.icon = TT_ASSETS_APP_ICON_FILES,
|
||||||
|
.type = Type::Hidden,
|
||||||
|
.createApp = create<FileSelection>
|
||||||
|
};
|
||||||
|
|
||||||
|
LaunchId startForExistingFile() {
|
||||||
|
auto bundle = std::make_shared<Bundle>();
|
||||||
|
setMode(*bundle, Mode::Existing);
|
||||||
|
return service::loader::startApp(manifest.id, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchId startForExistingOrNewFile() {
|
||||||
|
auto bundle = std::make_shared<Bundle>();
|
||||||
|
setMode(*bundle, Mode::ExistingOrNew);
|
||||||
|
return service::loader::startApp(manifest.id, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
118
Tactility/Source/app/fileselection/State.cpp
Normal file
118
Tactility/Source/app/fileselection/State.cpp
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#include "Tactility/app/fileselection/State.h"
|
||||||
|
|
||||||
|
#include <Tactility/file/File.h>
|
||||||
|
#include "Tactility/hal/sdcard/SdCardDevice.h"
|
||||||
|
#include <Tactility/Log.h>
|
||||||
|
#include <Tactility/Partitions.h>
|
||||||
|
#include <Tactility/kernel/Kernel.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
#define TAG "fileselection_app"
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
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 file::getChildPath(current_path, selected_child_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool State::setEntriesForPath(const std::string& path) {
|
||||||
|
auto lock = mutex.asScopedLock();
|
||||||
|
if (!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.
|
||||||
|
*/
|
||||||
|
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
|
||||||
|
if (show_custom_root) {
|
||||||
|
TT_LOG_I(TAG, "Setting custom root");
|
||||||
|
dir_entries.clear();
|
||||||
|
dir_entries.push_back(dirent{
|
||||||
|
.d_ino = 0,
|
||||||
|
.d_type = file::TT_DT_DIR,
|
||||||
|
.d_name = SYSTEM_PARTITION_NAME
|
||||||
|
});
|
||||||
|
dir_entries.push_back(dirent{
|
||||||
|
.d_ino = 1,
|
||||||
|
.d_type = file::TT_DT_DIR,
|
||||||
|
.d_name = DATA_PARTITION_NAME
|
||||||
|
});
|
||||||
|
|
||||||
|
auto sdcards = tt::hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||||
|
for (auto& sdcard : sdcards) {
|
||||||
|
auto state = sdcard->getState();
|
||||||
|
if (state == hal::sdcard::SdCardDevice::State::Mounted) {
|
||||||
|
auto mount_name = sdcard->getMountPath().substr(1);
|
||||||
|
auto dir_entry = dirent {
|
||||||
|
.d_ino = 2,
|
||||||
|
.d_type = file::TT_DT_DIR,
|
||||||
|
.d_name = { 0 }
|
||||||
|
};
|
||||||
|
assert(mount_name.length() < sizeof(dirent::d_name));
|
||||||
|
strcpy(dir_entry.d_name, mount_name.c_str());
|
||||||
|
dir_entries.push_back(dir_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_path = path;
|
||||||
|
selected_child_entry = "";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
dir_entries.clear();
|
||||||
|
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
|
||||||
|
if (count >= 0) {
|
||||||
|
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
|
||||||
|
current_path = path;
|
||||||
|
selected_child_entry = "";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool State::setEntriesForChildPath(const std::string& childPath) {
|
||||||
|
auto path = file::getChildPath(current_path, childPath);
|
||||||
|
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 lock = mutex.asScopedLock();
|
||||||
|
if (!lock.lock(50 / portTICK_PERIOD_MS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < dir_entries.size()) {
|
||||||
|
dirent = dir_entries[index];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
215
Tactility/Source/app/fileselection/View.cpp
Normal file
215
Tactility/Source/app/fileselection/View.cpp
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
#include "Tactility/app/fileselection/View.h"
|
||||||
|
|
||||||
|
#include "Tactility/app/alertdialog/AlertDialog.h"
|
||||||
|
#include "Tactility/lvgl/Toolbar.h"
|
||||||
|
#include "Tactility/lvgl/LvglSync.h"
|
||||||
|
|
||||||
|
#include <Tactility/Tactility.h>
|
||||||
|
#include "Tactility/file/File.h"
|
||||||
|
#include <Tactility/StringUtils.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <Tactility/service/gui/Gui.h>
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
#include "Tactility/service/loader/Loader.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TAG "fileselection_app"
|
||||||
|
|
||||||
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
|
// region Callbacks
|
||||||
|
|
||||||
|
static void onDirEntryPressedCallback(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<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 onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||||
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
|
view->onNavigateUpPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
void View::onTapFile(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", processed_filepath.c_str());
|
||||||
|
|
||||||
|
lv_textarea_set_text(path_textarea, processed_filepath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
using namespace tt::file;
|
||||||
|
switch (dir_entry.d_type) {
|
||||||
|
case TT_DT_DIR:
|
||||||
|
case TT_DT_CHR:
|
||||||
|
state->setEntriesForChildPath(dir_entry.d_name);
|
||||||
|
lv_textarea_set_text(path_textarea, state->getCurrentPathWithTrailingSlash().c_str());
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case TT_DT_LNK:
|
||||||
|
TT_LOG_W(TAG, "opening links is not supported");
|
||||||
|
break;
|
||||||
|
case TT_DT_REG:
|
||||||
|
onTapFile(state->getCurrentPath(), dir_entry.d_name);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Assume it's a file
|
||||||
|
// TODO: Find a better way to identify a file
|
||||||
|
onTapFile(state->getCurrentPath(), dir_entry.d_name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void View::onSelectButtonPressed(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
|
const char* path = lv_textarea_get_text(view->path_textarea);
|
||||||
|
if (path == nullptr || strlen(path) == 0) {
|
||||||
|
TT_LOG_W(TAG, "Select pressed, but not path found in textarea");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view->onFileSelected(std::string(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isSelectableFilePath(const char* path) {
|
||||||
|
if (path == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto len = strlen(path);
|
||||||
|
if (len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[len - 1] != '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
void View::onPathTextChanged(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||||
|
const char* path = lv_textarea_get_text(view->path_textarea);
|
||||||
|
if (isSelectableFilePath(path)) {
|
||||||
|
lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||||
|
tt_check(list);
|
||||||
|
const char* symbol;
|
||||||
|
if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) {
|
||||||
|
symbol = LV_SYMBOL_DIRECTORY;
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (new_absolute_path.length() > 1) {
|
||||||
|
lv_textarea_set_text(path_textarea, (new_absolute_path + "/").c_str());
|
||||||
|
} else {
|
||||||
|
lv_textarea_set_text(path_textarea, new_absolute_path.c_str());
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void View::update() {
|
||||||
|
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
|
||||||
|
if (scoped_lockable.lock(lvgl::defaultLockTime)) {
|
||||||
|
lv_obj_clean(dir_entry_list);
|
||||||
|
|
||||||
|
state->withEntries([this](const std::vector<dirent>& entries) {
|
||||||
|
for (auto entry : entries) {
|
||||||
|
TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type);
|
||||||
|
createDirEntryWidget(dir_entry_list, entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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, Mode mode) {
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
auto* toolbar = lvgl::toolbar_create(parent, "Select File");
|
||||||
|
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||||
|
|
||||||
|
auto* 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);
|
||||||
|
|
||||||
|
auto* bottom_wrapper = lv_obj_create(parent);
|
||||||
|
lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_border_width(bottom_wrapper, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(bottom_wrapper, 0, 0);
|
||||||
|
|
||||||
|
path_textarea = lv_textarea_create(bottom_wrapper);
|
||||||
|
lv_textarea_set_one_line(path_textarea, true);
|
||||||
|
lv_obj_set_flex_grow(path_textarea, 1);
|
||||||
|
service::gui::keyboardAddTextArea(path_textarea);
|
||||||
|
lv_obj_add_event_cb(path_textarea, onPathTextChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
|
||||||
|
select_button = lv_button_create(bottom_wrapper);
|
||||||
|
auto* select_button_label = lv_label_create(select_button);
|
||||||
|
lv_label_set_text(select_button_label, "Select");
|
||||||
|
lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||||
|
lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -91,10 +91,9 @@ public:
|
|||||||
updateLogEntries();
|
updateLogEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
|
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||||
if (result == Result::Ok && bundle != nullptr) {
|
if (result == Result::Ok && bundle != nullptr) {
|
||||||
auto resultIndex = selectiondialog::getResultIndex(*bundle);
|
switch (selectiondialog::getResultIndex(*bundle)) {
|
||||||
switch (resultIndex) {
|
|
||||||
case 0:
|
case 0:
|
||||||
filterLevel = LogLevel::Verbose;
|
filterLevel = LogLevel::Verbose;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -5,35 +5,30 @@
|
|||||||
#include <Tactility/Assets.h>
|
#include <Tactility/Assets.h>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
#include <dirent.h>
|
#include <Tactility/app/fileselection/FileSelection.h>
|
||||||
#include <fstream>
|
#include <Tactility/hal/sdcard/SdCardDevice.h>
|
||||||
|
#include <Tactility/lvgl/LvglSync.h>
|
||||||
|
|
||||||
namespace tt::app::notes {
|
namespace tt::app::notes {
|
||||||
|
|
||||||
constexpr const char* TAG = "Notes";
|
constexpr const char* TAG = "Notes";
|
||||||
|
|
||||||
class NotesApp : public App {
|
class NotesApp : public App {
|
||||||
|
|
||||||
AppContext* appContext = nullptr;
|
AppContext* appContext = nullptr;
|
||||||
|
|
||||||
lv_obj_t* uiCurrentFileName;
|
lv_obj_t* uiCurrentFileName;
|
||||||
lv_obj_t* uiDropDownMenu;
|
lv_obj_t* uiDropDownMenu;
|
||||||
lv_obj_t* uiFileList;
|
|
||||||
lv_obj_t* uiFileListCloseBtn;
|
|
||||||
lv_obj_t* uiNoteText;
|
lv_obj_t* uiNoteText;
|
||||||
lv_obj_t* uiSaveDialog;
|
|
||||||
lv_obj_t* uiSaveDialogFileName;
|
|
||||||
lv_obj_t* uiSaveDialogSaveBtn;
|
|
||||||
lv_obj_t* uiSaveDialogCancelBtn;
|
|
||||||
lv_obj_t* uiMessageBox;
|
lv_obj_t* uiMessageBox;
|
||||||
lv_obj_t* uiMessageBoxButtonOk;
|
lv_obj_t* uiMessageBoxButtonOk;
|
||||||
lv_obj_t* uiMessageBoxButtonNo;
|
lv_obj_t* uiMessageBoxButtonNo;
|
||||||
|
|
||||||
char menuItem[32];
|
|
||||||
uint8_t menuIdx = 0;
|
|
||||||
std::string fileContents;
|
|
||||||
std::string fileName;
|
|
||||||
std::string newFileName;
|
|
||||||
std::string filePath;
|
std::string filePath;
|
||||||
|
std::string saveBuffer;
|
||||||
|
|
||||||
|
LaunchId loadFileLaunchId = 0;
|
||||||
|
LaunchId saveFileLaunchId = 0;
|
||||||
|
|
||||||
#pragma region Main_Events_Functions
|
#pragma region Main_Events_Functions
|
||||||
|
|
||||||
@ -42,81 +37,56 @@ class NotesApp : public App {
|
|||||||
lv_obj_t* obj = lv_event_get_target_obj(e);
|
lv_obj_t* obj = lv_event_get_target_obj(e);
|
||||||
|
|
||||||
if (code == LV_EVENT_CLICKED) {
|
if (code == LV_EVENT_CLICKED) {
|
||||||
if (obj == uiFileListCloseBtn) {
|
if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) {
|
||||||
lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
lv_obj_del(uiFileList);
|
|
||||||
} else if (obj == uiSaveDialogSaveBtn) {
|
|
||||||
newFileName = lv_textarea_get_text(uiSaveDialogFileName);
|
|
||||||
if (newFileName.length() == 0) {
|
|
||||||
uiMessageBoxShow(menuItem, "Filename is empty.", false);
|
|
||||||
} else {
|
|
||||||
std::string noteText = lv_textarea_get_text(uiNoteText);
|
|
||||||
filePath = appContext->getPaths()->getDataPath(newFileName);
|
|
||||||
|
|
||||||
if (writeFile(filePath, noteText)) {
|
|
||||||
uiMessageBoxShow(menuItem, "File created successfully!", false);
|
|
||||||
lv_label_set_text(uiCurrentFileName, newFileName.c_str());
|
|
||||||
} else {
|
|
||||||
uiMessageBoxShow(menuItem, "Something went wrong!\nFile creation failed.", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lv_obj_del(uiSaveDialog);
|
|
||||||
|
|
||||||
} else if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) {
|
|
||||||
lv_obj_del(uiMessageBox);
|
lv_obj_del(uiMessageBox);
|
||||||
} else if (obj == uiSaveDialogCancelBtn) {
|
|
||||||
lv_obj_del(uiSaveDialog);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||||
if (obj == uiDropDownMenu) {
|
if (obj == uiDropDownMenu) {
|
||||||
lv_dropdown_get_selected_str(obj, menuItem, sizeof(menuItem));
|
switch (lv_dropdown_get_selected(obj)) {
|
||||||
menuIdx = lv_dropdown_get_selected(obj);
|
case 0: // New
|
||||||
std::string newContents = lv_textarea_get_text(uiNoteText);
|
resetFileContent();
|
||||||
if (menuIdx == 1) { //Save
|
break;
|
||||||
//Normal Save?
|
case 1: // Save
|
||||||
|
if (!filePath.empty()) {
|
||||||
|
lvgl::getSyncLock()->lock();
|
||||||
|
saveBuffer = lv_textarea_get_text(uiNoteText);
|
||||||
|
lvgl::getSyncLock()->unlock();
|
||||||
|
saveFile(filePath);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: // Save as...
|
||||||
|
lvgl::getSyncLock()->lock();
|
||||||
|
saveBuffer = lv_textarea_get_text(uiNoteText);
|
||||||
|
lvgl::getSyncLock()->unlock();
|
||||||
|
saveFileLaunchId = fileselection::startForExistingOrNewFile();
|
||||||
|
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
|
||||||
|
break;
|
||||||
|
case 3: // Load
|
||||||
|
loadFileLaunchId = fileselection::startForExistingFile();
|
||||||
|
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (menuIdx == 2) { //Save As...
|
|
||||||
uiSaveFileDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Not working...more investigation needed.
|
|
||||||
//If note contents has changed in currently open file, save it.
|
|
||||||
|
|
||||||
//bool newToSave = newContents != fileContents && newContents.length() != 0;
|
|
||||||
//if (newToSave) {
|
|
||||||
//uiMessageBoxShow(menuItem, "Do you want to save it?", true);
|
|
||||||
//} else {
|
|
||||||
menuAction();
|
|
||||||
//}
|
|
||||||
} else {
|
} else {
|
||||||
lv_obj_t* cont = lv_event_get_current_target_obj(e);
|
auto* cont = lv_event_get_current_target_obj(e);
|
||||||
if (obj == cont) return;
|
if (obj == cont) return;
|
||||||
if (lv_obj_get_child(cont, 1)) {
|
if (lv_obj_get_child(cont, 1)) {
|
||||||
uiSaveFileDialog();
|
saveFileLaunchId = fileselection::startForExistingOrNewFile();
|
||||||
|
TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId);
|
||||||
} else { //Reset
|
} else { //Reset
|
||||||
lv_textarea_set_text(uiNoteText, "");
|
resetFileContent();
|
||||||
fileName = "";
|
|
||||||
lv_label_set_text(uiCurrentFileName, "Untitled");
|
|
||||||
}
|
}
|
||||||
lv_obj_delete(uiMessageBox);
|
lv_obj_delete(uiMessageBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuAction() {
|
void resetFileContent() {
|
||||||
switch (menuIdx) {
|
lv_textarea_set_text(uiNoteText, "");
|
||||||
case 0: //Reset
|
filePath = "";
|
||||||
lv_textarea_set_text(uiNoteText, "");
|
saveBuffer = "";
|
||||||
fileName = "";
|
lv_label_set_text(uiCurrentFileName, "Untitled");
|
||||||
lv_label_set_text(uiCurrentFileName, "Untitled");
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
uiOpenFileDialog();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) {
|
void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) {
|
||||||
@ -166,166 +136,49 @@ class NotesApp : public App {
|
|||||||
lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk);
|
lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk);
|
||||||
lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0);
|
lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||||
lv_label_set_text(buttonLabelOk, "Ok");
|
lv_label_set_text(buttonLabelOk, "Ok");
|
||||||
lv_obj_add_event_cb(uiMessageBoxButtonOk, [](lv_event_t* e) {
|
lv_obj_add_event_cb(uiMessageBoxButtonOk,
|
||||||
|
[](lv_event_t* e) {
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
||||||
self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this);
|
self->appNotesEventCb(e);
|
||||||
|
},
|
||||||
|
LV_EVENT_CLICKED,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath.empty()) {
|
||||||
|
openFile(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma endregion Main_Events_Functions
|
|
||||||
|
|
||||||
#pragma region Save_Events_Functions
|
|
||||||
|
|
||||||
void uiSaveFileDialog() {
|
|
||||||
uiSaveDialog = lv_obj_create(lv_scr_act());
|
|
||||||
if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens
|
|
||||||
lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 80);
|
|
||||||
} else { //large screens
|
|
||||||
lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 230);
|
|
||||||
}
|
|
||||||
lv_obj_align(uiSaveDialog, LV_ALIGN_TOP_MID, 0, 0);
|
|
||||||
lv_obj_remove_flag(uiSaveDialog, LV_OBJ_FLAG_SCROLLABLE);
|
|
||||||
|
|
||||||
lv_obj_t* uiSaveDialogTitle = lv_label_create(uiSaveDialog);
|
|
||||||
lv_label_set_text(uiSaveDialogTitle, menuItem);
|
|
||||||
lv_obj_set_size(uiSaveDialogTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30);
|
|
||||||
lv_obj_align(uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 0);
|
|
||||||
|
|
||||||
uiSaveDialogFileName = lv_textarea_create(uiSaveDialog);
|
|
||||||
lv_obj_set_size(uiSaveDialogFileName, lv_display_get_horizontal_resolution(nullptr) - 30, 40);
|
|
||||||
lv_obj_align_to(uiSaveDialogFileName, uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 30);
|
|
||||||
lv_textarea_set_placeholder_text(uiSaveDialogFileName, "Enter file name...");
|
|
||||||
lv_textarea_set_one_line(uiSaveDialogFileName, true);
|
|
||||||
lv_obj_add_state(uiSaveDialogFileName, LV_STATE_FOCUSED);
|
|
||||||
|
|
||||||
//Both hardware and software keyboard not auto attaching here for some reason unless the textarea is touched to focus it...
|
|
||||||
tt::lvgl::keyboard_add_textarea(uiSaveDialogFileName);
|
|
||||||
|
|
||||||
if (fileName != "" || fileName != "Untitled") {
|
|
||||||
lv_textarea_set_text(uiSaveDialogFileName, fileName.c_str());
|
|
||||||
} else {
|
|
||||||
lv_textarea_set_placeholder_text(uiSaveDialogFileName, "filename?");
|
|
||||||
}
|
|
||||||
|
|
||||||
uiSaveDialogSaveBtn = lv_btn_create(uiSaveDialog);
|
|
||||||
lv_obj_add_event_cb(uiSaveDialogSaveBtn, [](lv_event_t* e) {
|
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
|
||||||
self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this);
|
|
||||||
lv_obj_align(uiSaveDialogSaveBtn, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
|
||||||
lv_obj_t* btnLabel = lv_label_create(uiSaveDialogSaveBtn);
|
|
||||||
lv_label_set_text(btnLabel, "Save");
|
|
||||||
lv_obj_center(btnLabel);
|
|
||||||
|
|
||||||
uiSaveDialogCancelBtn = lv_btn_create(uiSaveDialog);
|
|
||||||
lv_obj_add_event_cb(uiSaveDialogCancelBtn, [](lv_event_t* e) {
|
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
|
||||||
self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this);
|
|
||||||
lv_obj_align(uiSaveDialogCancelBtn, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
|
||||||
lv_obj_t* btnLabel2 = lv_label_create(uiSaveDialogCancelBtn);
|
|
||||||
lv_label_set_text(btnLabel2, "Cancel");
|
|
||||||
lv_obj_center(btnLabel2);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool writeFile(std::string path, std::string message) {
|
|
||||||
std::ofstream fileStream(path);
|
|
||||||
|
|
||||||
if (!fileStream.is_open()) {
|
|
||||||
TT_LOG_E(TAG, "Failed to write file");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (fileStream.is_open()) {
|
|
||||||
fileStream << message;
|
|
||||||
TT_LOG_I(TAG, "File written successfully");
|
|
||||||
fileStream.close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma endregion Save_Events_Functions
|
|
||||||
|
|
||||||
#pragma region Open_Events_Functions
|
#pragma region Open_Events_Functions
|
||||||
|
|
||||||
void openFileEventCb(lv_event_t* e) {
|
void openFile(const std::string& path) {
|
||||||
lv_event_code_t code = lv_event_get_code(e);
|
// We might be reading from the SD card, which could share a SPI bus with other devices (display)
|
||||||
lv_obj_t* obj = lv_event_get_target_obj(e);
|
hal::sdcard::withSdCardLock<void>(path, [this, path]() {
|
||||||
|
auto data = file::readString(path);
|
||||||
if (code == LV_EVENT_CLICKED) {
|
if (data != nullptr) {
|
||||||
std::string selectedFile = lv_list_get_btn_text(uiFileList, obj);
|
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||||
fileName = selectedFile.substr(0, selectedFile.find(" ("));
|
lock.lock();
|
||||||
std::string filePath = appContext->getPaths()->getDataPath(fileName);
|
lv_textarea_set_text(uiNoteText, reinterpret_cast<const char*>(data.get()));
|
||||||
fileContents = readFile(filePath.c_str());
|
lv_label_set_text(uiCurrentFileName, path.c_str());
|
||||||
lv_textarea_set_text(uiNoteText, fileContents.c_str());
|
filePath = path;
|
||||||
lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN);
|
TT_LOG_I(TAG, "Loaded from %s", path.c_str());
|
||||||
lv_obj_del(uiFileList);
|
}
|
||||||
|
});
|
||||||
lv_label_set_text(uiCurrentFileName, fileName.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiOpenFileDialog() {
|
bool saveFile(const std::string& path) {
|
||||||
uiFileList = lv_list_create(lv_scr_act());
|
// We might be writing to SD card, which could share a SPI bus with other devices (display)
|
||||||
lv_obj_set_size(uiFileList, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr));
|
return hal::sdcard::withSdCardLock<bool>(path, [this, path]() {
|
||||||
lv_obj_align(uiFileList, LV_ALIGN_TOP_MID, 0, 0);
|
if (file::writeString(path, saveBuffer.c_str())) {
|
||||||
lv_list_add_text(uiFileList, "Notes");
|
TT_LOG_I(TAG, "Saved to %s", path.c_str());
|
||||||
|
filePath = path;
|
||||||
uiFileListCloseBtn = lv_btn_create(uiFileList);
|
return true;
|
||||||
lv_obj_set_size(uiFileListCloseBtn, 36, 36);
|
} else {
|
||||||
lv_obj_add_flag(uiFileListCloseBtn, LV_OBJ_FLAG_FLOATING);
|
return false;
|
||||||
lv_obj_align(uiFileListCloseBtn, LV_ALIGN_TOP_RIGHT, 10, 4);
|
}
|
||||||
lv_obj_add_event_cb(uiFileListCloseBtn, [](lv_event_t* e) {
|
});
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
|
||||||
self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this);
|
|
||||||
|
|
||||||
lv_obj_t* uiFileListCloseLabel = lv_label_create(uiFileListCloseBtn);
|
|
||||||
lv_label_set_text(uiFileListCloseLabel, LV_SYMBOL_CLOSE);
|
|
||||||
lv_obj_center(uiFileListCloseLabel);
|
|
||||||
|
|
||||||
lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
|
|
||||||
//TODO: Move this to SD Card?
|
|
||||||
std::vector<std::string> noteFileList;
|
|
||||||
const std::string& path = appContext->getPaths()->getDataDirectory();
|
|
||||||
DIR* dir = opendir(path.c_str());
|
|
||||||
if (dir == nullptr) {
|
|
||||||
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct dirent* current_entry;
|
|
||||||
while ((current_entry = readdir(dir)) != nullptr) {
|
|
||||||
noteFileList.push_back(current_entry->d_name);
|
|
||||||
}
|
|
||||||
closedir(dir);
|
|
||||||
|
|
||||||
if (noteFileList.size() == 0) return;
|
|
||||||
|
|
||||||
for (std::vector<std::string>::iterator item = noteFileList.begin(); item != noteFileList.end(); ++item) {
|
|
||||||
lv_obj_t* btn = lv_list_add_btn(uiFileList, LV_SYMBOL_FILE, (*item).c_str());
|
|
||||||
lv_obj_add_event_cb(btn, [](lv_event_t* e) {
|
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
|
||||||
self->openFileEventCb(e); }, LV_EVENT_CLICKED, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
lv_obj_move_foreground(uiFileListCloseBtn);
|
|
||||||
lv_obj_remove_flag(uiFileList, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string readFile(std::string path) {
|
|
||||||
std::ifstream fileStream(path);
|
|
||||||
|
|
||||||
if (!fileStream.is_open()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string temp = "";
|
|
||||||
std::string file_contents;
|
|
||||||
while (std::getline(fileStream, temp)) {
|
|
||||||
file_contents += temp;
|
|
||||||
file_contents.push_back('\n');
|
|
||||||
}
|
|
||||||
fileStream.close();
|
|
||||||
return file_contents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma endregion Open_Events_Functions
|
#pragma endregion Open_Events_Functions
|
||||||
@ -347,9 +200,14 @@ class NotesApp : public App {
|
|||||||
lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
|
lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
|
||||||
lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN);
|
lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN);
|
||||||
lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0);
|
lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
lv_obj_add_event_cb(uiDropDownMenu, [](lv_event_t* e) {
|
lv_obj_add_event_cb(uiDropDownMenu,
|
||||||
|
[](lv_event_t* e) {
|
||||||
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
auto *self = static_cast<NotesApp *>(lv_event_get_user_data(e));
|
||||||
self->appNotesEventCb(e); }, LV_EVENT_VALUE_CHANGED, this);
|
self->appNotesEventCb(e);
|
||||||
|
},
|
||||||
|
LV_EVENT_VALUE_CHANGED,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
@ -387,11 +245,31 @@ class NotesApp : public App {
|
|||||||
lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
|
||||||
//TODO: Move this to SD Card?
|
//TODO: Move this to SD Card?
|
||||||
if (!tt::file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) {
|
if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) {
|
||||||
TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str());
|
TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
tt::lvgl::keyboard_add_textarea(uiNoteText);
|
lvgl::keyboard_add_textarea(uiNoteText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultData) override {
|
||||||
|
TT_LOG_I(TAG, "Result for launch id %d", launchId);
|
||||||
|
if (launchId == loadFileLaunchId) {
|
||||||
|
loadFileLaunchId = 0;
|
||||||
|
if (result == Result::Ok && resultData != nullptr) {
|
||||||
|
auto path = fileselection::getResultPath(*resultData);
|
||||||
|
openFile(path);
|
||||||
|
}
|
||||||
|
} else if (launchId == saveFileLaunchId) {
|
||||||
|
saveFileLaunchId = 0;
|
||||||
|
if (result == Result::Ok && resultData != nullptr) {
|
||||||
|
auto path = fileselection::getResultPath(*resultData);
|
||||||
|
// Must re-open file, because UI was cleared after opening other app
|
||||||
|
if (saveFile(path)) {
|
||||||
|
openFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -401,4 +279,5 @@ extern const AppManifest manifest = {
|
|||||||
.icon = TT_ASSETS_APP_ICON_NOTES,
|
.icon = TT_ASSETS_APP_ICON_NOTES,
|
||||||
.createApp = create<NotesApp>
|
.createApp = create<NotesApp>
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace tt::app::notes
|
} // namespace tt::app::notes
|
||||||
@ -88,7 +88,7 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
|
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||||
if (result == Result::Ok && bundle != nullptr) {
|
if (result == Result::Ok && bundle != nullptr) {
|
||||||
auto name = timezone::getResultName(*bundle);
|
auto name = timezone::getResultName(*bundle);
|
||||||
auto code = timezone::getResultCode(*bundle);
|
auto code = timezone::getResultCode(*bundle);
|
||||||
|
|||||||
@ -112,7 +112,7 @@ class WifiApSettings : public App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
|
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
|
||||||
if (result == Result::Ok && bundle != nullptr) {
|
if (result == Result::Ok && bundle != nullptr) {
|
||||||
auto index = alertdialog::getResultIndex(*bundle);
|
auto index = alertdialog::getResultIndex(*bundle);
|
||||||
if (index == 0) { // Yes
|
if (index == 0) { // Yes
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
#include <Tactility/lvgl/LabelUtils.h>
|
#include <Tactility/lvgl/LabelUtils.h>
|
||||||
#include <Tactility/file/File.h>
|
#include <Tactility/file/File.h>
|
||||||
|
#include <Tactility/hal/sdcard/SdCardDevice.h>
|
||||||
|
|
||||||
namespace tt::lvgl {
|
namespace tt::lvgl {
|
||||||
|
|
||||||
#define TAG "tt_lv_label"
|
#define TAG "tt_lv_label"
|
||||||
|
|
||||||
bool label_set_text_file(lv_obj_t* label, const char* filepath) {
|
bool label_set_text_file(lv_obj_t* label, const char* filepath) {
|
||||||
auto text = file::readString(filepath);
|
auto text = hal::sdcard::withSdCardLock<std::unique_ptr<uint8_t[]>>(std::string(filepath), [filepath]() {
|
||||||
|
return file::readString(filepath);
|
||||||
|
});
|
||||||
|
|
||||||
if (text != nullptr) {
|
if (text != nullptr) {
|
||||||
lv_label_set_text(label, (const char*)text.get());
|
lv_label_set_text(label, reinterpret_cast<const char*>(text.get()));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -47,17 +47,17 @@ static const char* appStateToString(app::State state) {
|
|||||||
|
|
||||||
class LoaderService final : public Service {
|
class LoaderService final : public Service {
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
|
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
|
||||||
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||||
std::stack<std::shared_ptr<app::AppInstance>> appStack;
|
std::stack<std::shared_ptr<app::AppInstance>> appStack;
|
||||||
|
app::LaunchId nextLaunchId = 0;
|
||||||
|
|
||||||
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
|
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
|
||||||
* This includes full LVGL redraw via Gui::redraw()
|
* This includes full LVGL redraw via Gui::redraw()
|
||||||
*/
|
*/
|
||||||
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
|
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
|
||||||
|
|
||||||
void onStartAppMessage(const std::string& id, std::shared_ptr<const Bundle> parameters);
|
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
|
||||||
void onStopAppMessage(const std::string& id);
|
void onStopAppMessage(const std::string& id);
|
||||||
|
|
||||||
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
|
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
|
||||||
@ -75,7 +75,7 @@ public:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
|
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
|
||||||
void stopApp();
|
void stopApp();
|
||||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
|
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
|
||||||
|
|
||||||
@ -86,8 +86,7 @@ std::shared_ptr<LoaderService> _Nullable optScreenshotService() {
|
|||||||
return service::findServiceById<LoaderService>(manifest.id);
|
return service::findServiceById<LoaderService>(manifest.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<const Bundle> parameters) {
|
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
|
||||||
|
|
||||||
TT_LOG_I(TAG, "Start by id %s", id.c_str());
|
TT_LOG_I(TAG, "Start by id %s", id.c_str());
|
||||||
|
|
||||||
auto app_manifest = app::findAppById(id);
|
auto app_manifest = app::findAppById(id);
|
||||||
@ -103,7 +102,7 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<con
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
|
auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
|
||||||
auto new_app = std::make_shared<app::AppInstance>(app_manifest, parameters);
|
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
|
||||||
|
|
||||||
new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot);
|
new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot);
|
||||||
|
|
||||||
@ -123,7 +122,6 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr<con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LoaderService::onStopAppMessage(const std::string& id) {
|
void LoaderService::onStopAppMessage(const std::string& id) {
|
||||||
|
|
||||||
auto lock = mutex.asScopedLock();
|
auto lock = mutex.asScopedLock();
|
||||||
if (!lock.lock(LOADER_TIMEOUT)) {
|
if (!lock.lock(LOADER_TIMEOUT)) {
|
||||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||||
@ -157,6 +155,8 @@ void LoaderService::onStopAppMessage(const std::string& id) {
|
|||||||
result_set = true;
|
result_set = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto app_to_stop_launch_id = app_to_stop->getLaunchId();
|
||||||
|
|
||||||
transitionAppToState(app_to_stop, app::State::Hiding);
|
transitionAppToState(app_to_stop, app::State::Hiding);
|
||||||
transitionAppToState(app_to_stop, app::State::Stopped);
|
transitionAppToState(app_to_stop, app::State::Stopped);
|
||||||
|
|
||||||
@ -196,12 +196,14 @@ void LoaderService::onStopAppMessage(const std::string& id) {
|
|||||||
if (result_bundle != nullptr) {
|
if (result_bundle != nullptr) {
|
||||||
instance_to_resume->getApp()->onResult(
|
instance_to_resume->getApp()->onResult(
|
||||||
*instance_to_resume,
|
*instance_to_resume,
|
||||||
|
app_to_stop_launch_id,
|
||||||
result,
|
result,
|
||||||
std::move(result_bundle)
|
std::move(result_bundle)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
instance_to_resume->getApp()->onResult(
|
instance_to_resume->getApp()->onResult(
|
||||||
*instance_to_resume,
|
*instance_to_resume,
|
||||||
|
app_to_stop_launch_id,
|
||||||
result,
|
result,
|
||||||
nullptr
|
nullptr
|
||||||
);
|
);
|
||||||
@ -210,6 +212,7 @@ void LoaderService::onStopAppMessage(const std::string& id) {
|
|||||||
const Bundle empty_bundle;
|
const Bundle empty_bundle;
|
||||||
instance_to_resume->getApp()->onResult(
|
instance_to_resume->getApp()->onResult(
|
||||||
*instance_to_resume,
|
*instance_to_resume,
|
||||||
|
app_to_stop_launch_id,
|
||||||
app::Result::Cancelled,
|
app::Result::Cancelled,
|
||||||
nullptr
|
nullptr
|
||||||
);
|
);
|
||||||
@ -255,10 +258,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
|
|||||||
app->setState(state);
|
app->setState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
|
app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
|
||||||
dispatcherThread->dispatch([this, id, parameters]() {
|
auto launch_id = nextLaunchId++;
|
||||||
onStartAppMessage(id, parameters);
|
dispatcherThread->dispatch([this, id, launch_id, parameters]() {
|
||||||
|
onStartAppMessage(id, launch_id, parameters);
|
||||||
});
|
});
|
||||||
|
return launch_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoaderService::stopApp() {
|
void LoaderService::stopApp() {
|
||||||
@ -278,11 +283,11 @@ std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext()
|
|||||||
|
|
||||||
// region Public API
|
// region Public API
|
||||||
|
|
||||||
void startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
|
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
|
||||||
TT_LOG_I(TAG, "Start app %s", id.c_str());
|
TT_LOG_I(TAG, "Start app %s", id.c_str());
|
||||||
auto service = optScreenshotService();
|
auto service = optScreenshotService();
|
||||||
assert(service);
|
assert(service);
|
||||||
service->startApp(id, std::move(parameters));
|
return service->startApp(id, std::move(parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopApp() {
|
void stopApp() {
|
||||||
|
|||||||
@ -16,6 +16,8 @@ typedef enum {
|
|||||||
|
|
||||||
typedef void* AppHandle;
|
typedef void* AppHandle;
|
||||||
|
|
||||||
|
typedef unsigned int LaunchId;
|
||||||
|
|
||||||
/** Important: These function types must map to t::app types exactly */
|
/** Important: These function types must map to t::app types exactly */
|
||||||
typedef void* (*AppCreateData)();
|
typedef void* (*AppCreateData)();
|
||||||
typedef void (*AppDestroyData)(void* data);
|
typedef void (*AppDestroyData)(void* data);
|
||||||
@ -23,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data);
|
|||||||
typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data);
|
typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data);
|
||||||
typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent);
|
typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent);
|
||||||
typedef void (*AppOnHide)(AppHandle app, void* _Nullable data);
|
typedef void (*AppOnHide)(AppHandle app, void* _Nullable data);
|
||||||
typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, Result result, BundleHandle resultData);
|
typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, LaunchId launchId, Result result, BundleHandle resultData);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/** The application's human-readable name */
|
/** The application's human-readable name */
|
||||||
|
|||||||
@ -12,16 +12,16 @@ void tt_app_register(
|
|||||||
) {
|
) {
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr));
|
assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr));
|
||||||
tt::app::setElfAppManifest(
|
setElfAppManifest(
|
||||||
manifest->name,
|
manifest->name,
|
||||||
manifest->icon,
|
manifest->icon,
|
||||||
(tt::app::CreateData)manifest->createData,
|
manifest->createData,
|
||||||
(tt::app::DestroyData)manifest->destroyData,
|
manifest->destroyData,
|
||||||
(tt::app::OnCreate)manifest->onCreate,
|
manifest->onCreate,
|
||||||
(tt::app::OnDestroy)manifest->onDestroy,
|
manifest->onDestroy,
|
||||||
(tt::app::OnShow)manifest->onShow,
|
manifest->onShow,
|
||||||
(tt::app::OnHide)manifest->onHide,
|
manifest->onHide,
|
||||||
(tt::app::OnResult)manifest->onResult
|
reinterpret_cast<tt::app::OnResult>(manifest->onResult)
|
||||||
);
|
);
|
||||||
#else
|
#else
|
||||||
tt_crash("TactilityC is not intended for PC/Simulator");
|
tt_crash("TactilityC is not intended for PC/Simulator");
|
||||||
|
|||||||
@ -3,10 +3,34 @@
|
|||||||
#include "Tactility/TactilityCore.h"
|
#include "Tactility/TactilityCore.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <dirent.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace tt::file {
|
namespace tt::file {
|
||||||
|
|
||||||
|
/** File types for `dirent`'s `d_type`. */
|
||||||
|
enum {
|
||||||
|
TT_DT_UNKNOWN = 0,
|
||||||
|
#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type
|
||||||
|
TT_DT_FIFO = 1,
|
||||||
|
#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO
|
||||||
|
TT_DT_CHR = 2,
|
||||||
|
#define TT_DT_CHR TT_DT_CHR // Character device
|
||||||
|
TT_DT_DIR = 4,
|
||||||
|
#define TT_DT_DIR TT_DT_DIR // Directory
|
||||||
|
TT_DT_BLK = 6,
|
||||||
|
#define TT_DT_BLK TT_DT_BLK // Block device
|
||||||
|
TT_DT_REG = 8,
|
||||||
|
#define TT_DT_REG TT_DT_REG // Regular file
|
||||||
|
TT_DT_LNK = 10,
|
||||||
|
#define TT_DT_LNK TT_DT_LNK // Symbolic link
|
||||||
|
TT_DT_SOCK = 12,
|
||||||
|
#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket
|
||||||
|
TT_DT_WHT = 14
|
||||||
|
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes
|
||||||
|
};
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
constexpr char SEPARATOR = '\\';
|
constexpr char SEPARATOR = '\\';
|
||||||
#else
|
#else
|
||||||
@ -36,6 +60,13 @@ std::unique_ptr<uint8_t[]> readBinary(const std::string& filepath, size_t& outSi
|
|||||||
*/
|
*/
|
||||||
std::unique_ptr<uint8_t[]> readString(const std::string& filepath);
|
std::unique_ptr<uint8_t[]> readString(const std::string& filepath);
|
||||||
|
|
||||||
|
/** Write text to a file
|
||||||
|
* @param[in] path file path to write to
|
||||||
|
* @param[in] content file content to write
|
||||||
|
* @return true when operation is successful
|
||||||
|
*/
|
||||||
|
bool writeString(const std::string& filepath, const std::string& content);
|
||||||
|
|
||||||
/** Ensure a directory path exists.
|
/** Ensure a directory path exists.
|
||||||
* @param[in] path the directory path to find, or to create recursively
|
* @param[in] path the directory path to find, or to create recursively
|
||||||
* @param[in] mode the mode to use when creating directories
|
* @param[in] mode the mode to use when creating directories
|
||||||
@ -43,4 +74,41 @@ std::unique_ptr<uint8_t[]> readString(const std::string& filepath);
|
|||||||
*/
|
*/
|
||||||
bool findOrCreateDirectory(std::string path, mode_t mode);
|
bool findOrCreateDirectory(std::string path, mode_t mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenate a child path with a parent path, ensuring proper slash inbetween
|
||||||
|
* @param basePath an absolute path with or without trailing "/"
|
||||||
|
* @param childPath the name of the child path (e.g. subfolder or file)
|
||||||
|
* @return the concatenated path
|
||||||
|
*/
|
||||||
|
std::string getChildPath(const std::string& basePath, const std::string& childPath);
|
||||||
|
|
||||||
|
typedef int (*ScandirFilter)(const dirent*);
|
||||||
|
|
||||||
|
typedef bool (*ScandirSort)(const dirent&, const dirent&);
|
||||||
|
|
||||||
|
/** Used for sorting by alphanumeric value and file type */
|
||||||
|
bool direntSortAlphaAndType(const dirent& left, const dirent& right);
|
||||||
|
|
||||||
|
/** A filter for filtering out "." and ".." */
|
||||||
|
int direntFilterDotEntries(const dirent* entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scandir()-like implementation that works on ESP32.
|
||||||
|
* It does not return "." and ".." items but otherwise functions the same.
|
||||||
|
* It returns an allocated output array with allocated dirent instances.
|
||||||
|
* The caller is responsible for free-ing the memory of these.
|
||||||
|
*
|
||||||
|
* @param[in] path path the scan for files and directories
|
||||||
|
* @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 std::string& path,
|
||||||
|
std::vector<dirent>& outList,
|
||||||
|
ScandirFilter _Nullable filter,
|
||||||
|
ScandirSort _Nullable sort
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,66 @@
|
|||||||
#include "Tactility/file/File.h"
|
#include "Tactility/file/File.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
namespace tt::file {
|
namespace tt::file {
|
||||||
|
|
||||||
#define TAG "file"
|
#define TAG "file"
|
||||||
|
|
||||||
|
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 direntFilterDotEntries(const dirent* entry) {
|
||||||
|
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool direntSortAlphaAndType(const dirent& left, const 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) < 0;
|
||||||
|
} else {
|
||||||
|
return left_is_dir > right_is_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int scandir(
|
||||||
|
const std::string& path,
|
||||||
|
std::vector<dirent>& outList,
|
||||||
|
ScandirFilter _Nullable filterMethod,
|
||||||
|
ScandirSort _Nullable sortMethod
|
||||||
|
) {
|
||||||
|
TT_LOG_I(TAG, "scandir start");
|
||||||
|
DIR* dir = opendir(path.c_str());
|
||||||
|
if (dir == nullptr) {
|
||||||
|
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirent* current_entry;
|
||||||
|
while ((current_entry = readdir(dir)) != nullptr) {
|
||||||
|
if (filterMethod(current_entry) == 0) {
|
||||||
|
outList.push_back(*current_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
|
||||||
|
if (sortMethod != nullptr) {
|
||||||
|
std::ranges::sort(outList, sortMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
TT_LOG_I(TAG, "scandir finish");
|
||||||
|
return outList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
long getSize(FILE* file) {
|
long getSize(FILE* file) {
|
||||||
long original_offset = ftell(file);
|
long original_offset = ftell(file);
|
||||||
|
|
||||||
@ -80,12 +137,25 @@ std::unique_ptr<uint8_t[]> readBinary(const std::string& filepath, size_t& outSi
|
|||||||
std::unique_ptr<uint8_t[]> readString(const std::string& filepath) {
|
std::unique_ptr<uint8_t[]> readString(const std::string& filepath) {
|
||||||
size_t size = 0;
|
size_t size = 0;
|
||||||
auto data = readBinaryInternal(filepath, size, 1);
|
auto data = readBinaryInternal(filepath, size, 1);
|
||||||
if (data != nullptr) {
|
if (data == nullptr) {
|
||||||
data.get()[size] = 0; // Append null terminator
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.get()[size] = 0; // Append null terminator
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeString(const std::string& filepath, const std::string& content) {
|
||||||
|
std::ofstream fileStream(filepath);
|
||||||
|
|
||||||
|
if (!fileStream.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream << content;
|
||||||
|
fileStream.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) {
|
static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user