diff --git a/applications/debug_tools/file_browser_test/file_browser_app.c b/applications/debug_tools/file_browser_test/file_browser_app.c new file mode 100644 index 000000000..a408f5cde --- /dev/null +++ b/applications/debug_tools/file_browser_test/file_browser_app.c @@ -0,0 +1,99 @@ +#include "assets_icons.h" +#include "file_browser_app_i.h" +#include "gui/modules/file_browser.h" +#include "m-string.h" +#include +#include +#include +#include + +static bool file_browser_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + FileBrowserApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool file_browser_app_back_event_callback(void* context) { + furi_assert(context); + FileBrowserApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void file_browser_app_tick_event_callback(void* context) { + furi_assert(context); + FileBrowserApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +FileBrowserApp* file_browser_app_alloc(char* arg) { + UNUSED(arg); + FileBrowserApp* app = malloc(sizeof(FileBrowserApp)); + + app->gui = furi_record_open("gui"); + app->dialogs = furi_record_open("dialogs"); + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + + app->scene_manager = scene_manager_alloc(&file_browser_scene_handlers, app); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, file_browser_app_tick_event_callback, 500); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, file_browser_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, file_browser_app_back_event_callback); + + app->widget = widget_alloc(); + + string_init(app->file_path); + app->file_browser = file_browser_alloc(&(app->file_path)); + file_browser_configure(app->file_browser, "*", true, &I_badusb_10px, true); + + view_dispatcher_add_view( + app->view_dispatcher, FileBrowserAppViewStart, widget_get_view(app->widget)); + view_dispatcher_add_view( + app->view_dispatcher, FileBrowserAppViewResult, widget_get_view(app->widget)); + view_dispatcher_add_view( + app->view_dispatcher, FileBrowserAppViewBrowser, file_browser_get_view(app->file_browser)); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + scene_manager_next_scene(app->scene_manager, FileBrowserSceneStart); + + return app; +} + +void file_browser_app_free(FileBrowserApp* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewStart); + view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewResult); + view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewBrowser); + widget_free(app->widget); + file_browser_free(app->file_browser); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Close records + furi_record_close("gui"); + furi_record_close("notification"); + furi_record_close("dialogs"); + + string_clear(app->file_path); + + free(app); +} + +int32_t file_browser_app(void* p) { + FileBrowserApp* file_browser_app = file_browser_app_alloc((char*)p); + + view_dispatcher_run(file_browser_app->view_dispatcher); + + file_browser_app_free(file_browser_app); + return 0; +} diff --git a/applications/debug_tools/file_browser_test/file_browser_app_i.h b/applications/debug_tools/file_browser_test/file_browser_app_i.h new file mode 100644 index 000000000..6e8412c9b --- /dev/null +++ b/applications/debug_tools/file_browser_test/file_browser_app_i.h @@ -0,0 +1,32 @@ +#pragma once + +#include "scenes/file_browser_scene.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct FileBrowserApp FileBrowserApp; + +struct FileBrowserApp { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + DialogsApp* dialogs; + Widget* widget; + FileBrowser* file_browser; + + string_t file_path; +}; + +typedef enum { + FileBrowserAppViewStart, + FileBrowserAppViewBrowser, + FileBrowserAppViewResult, +} FileBrowserAppView; diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene.c b/applications/debug_tools/file_browser_test/scenes/file_browser_scene.c new file mode 100644 index 000000000..72a6e84d7 --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene.c @@ -0,0 +1,30 @@ +#include "file_browser_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const file_browser_scene_on_enter_handlers[])(void*) = { +#include "file_browser_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const file_browser_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "file_browser_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const file_browser_scene_on_exit_handlers[])(void* context) = { +#include "file_browser_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers file_browser_scene_handlers = { + .on_enter_handlers = file_browser_scene_on_enter_handlers, + .on_event_handlers = file_browser_scene_on_event_handlers, + .on_exit_handlers = file_browser_scene_on_exit_handlers, + .scene_num = FileBrowserSceneNum, +}; diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene.h b/applications/debug_tools/file_browser_test/scenes/file_browser_scene.h new file mode 100644 index 000000000..d690fca9f --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) FileBrowserScene##id, +typedef enum { +#include "file_browser_scene_config.h" + FileBrowserSceneNum, +} FileBrowserScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers file_browser_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "file_browser_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "file_browser_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "file_browser_scene_config.h" +#undef ADD_SCENE diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene_browser.c b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_browser.c new file mode 100644 index 000000000..9c570cec0 --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_browser.c @@ -0,0 +1,45 @@ +#include "../file_browser_app_i.h" +#include "furi/check.h" +#include "furi/log.h" +#include "furi_hal.h" +#include "m-string.h" + +#define DEFAULT_PATH "/" +#define EXTENSION "*" + +bool file_browser_scene_browser_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + FileBrowserApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_next_scene(app->scene_manager, FileBrowserSceneResult); + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + } + return consumed; +} + +static void file_browser_callback(void* context, bool state) { + FileBrowserApp* app = context; + furi_assert(app); + view_dispatcher_send_custom_event(app->view_dispatcher, SceneManagerEventTypeCustom); + + UNUSED(state); +} + +void file_browser_scene_browser_on_enter(void* context) { + FileBrowserApp* app = context; + + file_browser_set_callback(app->file_browser, file_browser_callback, app); + + file_browser_start(app->file_browser, app->file_path); + + view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewBrowser); +} + +void file_browser_scene_browser_on_exit(void* context) { + FileBrowserApp* app = context; + + file_browser_stop(app->file_browser); +} diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene_config.h b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_config.h new file mode 100644 index 000000000..6597df3aa --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_config.h @@ -0,0 +1,3 @@ +ADD_SCENE(file_browser, start, Start) +ADD_SCENE(file_browser, browser, Browser) +ADD_SCENE(file_browser, result, Result) diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene_result.c b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_result.c new file mode 100644 index 000000000..53576cef4 --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_result.c @@ -0,0 +1,36 @@ +#include "../file_browser_app_i.h" +#include "furi_hal.h" +#include "m-string.h" + +void file_browser_scene_result_ok_callback(InputType type, void* context) { + furi_assert(context); + FileBrowserApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, type); +} + +bool file_browser_scene_result_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + //FileBrowserApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + } + return consumed; +} + +void file_browser_scene_result_on_enter(void* context) { + FileBrowserApp* app = context; + + widget_add_string_multiline_element( + app->widget, 64, 10, AlignCenter, AlignTop, FontSecondary, string_get_cstr(app->file_path)); + + view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewResult); +} + +void file_browser_scene_result_on_exit(void* context) { + UNUSED(context); + FileBrowserApp* app = context; + widget_reset(app->widget); +} diff --git a/applications/debug_tools/file_browser_test/scenes/file_browser_scene_start.c b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_start.c new file mode 100644 index 000000000..bb71e83df --- /dev/null +++ b/applications/debug_tools/file_browser_test/scenes/file_browser_scene_start.c @@ -0,0 +1,44 @@ +#include "../file_browser_app_i.h" +#include "furi_hal.h" +#include "gui/modules/widget_elements/widget_element_i.h" + +static void + file_browser_scene_start_ok_callback(GuiButtonType result, InputType type, void* context) { + UNUSED(result); + furi_assert(context); + FileBrowserApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, type); + } +} + +bool file_browser_scene_start_on_event(void* context, SceneManagerEvent event) { + FileBrowserApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + string_set_str(app->file_path, "/any/badusb/demo_windows.txt"); + scene_manager_next_scene(app->scene_manager, FileBrowserSceneBrowser); + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + } + return consumed; +} + +void file_browser_scene_start_on_enter(void* context) { + FileBrowserApp* app = context; + + widget_add_string_multiline_element( + app->widget, 64, 20, AlignCenter, AlignTop, FontSecondary, "Press OK to start"); + + widget_add_button_element( + app->widget, GuiButtonTypeCenter, "Ok", file_browser_scene_start_ok_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewStart); +} + +void file_browser_scene_start_on_exit(void* context) { + UNUSED(context); + FileBrowserApp* app = context; + widget_reset(app->widget); +} diff --git a/applications/gui/modules/file_browser.c b/applications/gui/modules/file_browser.c new file mode 100644 index 000000000..919750962 --- /dev/null +++ b/applications/gui/modules/file_browser.c @@ -0,0 +1,532 @@ +#include "file_browser.h" +#include "assets_icons.h" +#include "cmsis_os2.h" +#include "file_browser_worker.h" +#include "furi/check.h" +#include "furi/common_defines.h" +#include "furi/log.h" +#include "furi_hal_resources.h" +#include "m-string.h" +#include +#include +#include + +#define LIST_ITEMS 5u +#define MAX_LEN_PX 110 +#define FRAME_HEIGHT 12 +#define Y_OFFSET 3 + +#define ITEM_LIST_LEN_MAX 100 + +typedef enum { + BrowserItemTypeLoading, + BrowserItemTypeBack, + BrowserItemTypeFolder, + BrowserItemTypeFile, +} BrowserItemType; + +typedef struct { + string_t path; + BrowserItemType type; +} BrowserItem_t; + +static void BrowserItem_t_init(BrowserItem_t* obj) { + obj->type = BrowserItemTypeLoading; + string_init(obj->path); +} + +static void BrowserItem_t_init_set(BrowserItem_t* obj, const BrowserItem_t* src) { + obj->type = src->type; + string_init_set(obj->path, src->path); +} + +static void BrowserItem_t_set(BrowserItem_t* obj, const BrowserItem_t* src) { + obj->type = src->type; + string_set(obj->path, src->path); +} + +static void BrowserItem_t_clear(BrowserItem_t* obj) { + string_clear(obj->path); +} + +ARRAY_DEF( + items_array, + BrowserItem_t, + (INIT(API_2(BrowserItem_t_init)), + SET(API_6(BrowserItem_t_set)), + INIT_SET(API_6(BrowserItem_t_init_set)), + CLEAR(API_2(BrowserItem_t_clear)))) + +struct FileBrowser { + View* view; + BrowserWorker* worker; + char* ext_filter; + bool skip_assets; + + FileBrowserCallback callback; + void* context; + + string_t* result_path; +}; + +typedef struct { + items_array_t items; + + bool is_root; + bool folder_loading; + bool list_loading; + uint32_t item_cnt; + int32_t item_idx; + int32_t array_offset; + int32_t list_offset; + + const Icon* file_icon; + bool hide_ext; +} FileBrowserModel; + +static const Icon* BrowserItemIcons[] = { + [BrowserItemTypeLoading] = &I_loading_10px, + [BrowserItemTypeBack] = &I_back_10px, + [BrowserItemTypeFolder] = &I_dir_10px, + [BrowserItemTypeFile] = &I_unknown_10px, +}; + +static void file_browser_view_draw_callback(Canvas* canvas, void* _model); +static bool file_browser_view_input_callback(InputEvent* event, void* context); + +static void + browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root); +static void browser_list_load_cb(void* context, uint32_t list_load_offset); +static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last); +static void browser_long_load_cb(void* context); + +FileBrowser* file_browser_alloc(string_t* result_path) { + furi_assert(result_path); + FileBrowser* browser = malloc(sizeof(FileBrowser)); + browser->view = view_alloc(); + view_allocate_model(browser->view, ViewModelTypeLocking, sizeof(FileBrowserModel)); + view_set_context(browser->view, browser); + view_set_draw_callback(browser->view, file_browser_view_draw_callback); + view_set_input_callback(browser->view, file_browser_view_input_callback); + + browser->result_path = result_path; + + with_view_model( + browser->view, (FileBrowserModel * model) { + items_array_init(model->items); + return false; + }); + + return browser; +} + +void file_browser_free(FileBrowser* browser) { + furi_assert(browser); + + with_view_model( + browser->view, (FileBrowserModel * model) { + items_array_clear(model->items); + return false; + }); + + view_free(browser->view); + free(browser); +} + +View* file_browser_get_view(FileBrowser* browser) { + furi_assert(browser); + return browser->view; +} + +void file_browser_configure( + FileBrowser* browser, + char* extension, + bool skip_assets, + const Icon* file_icon, + bool hide_ext) { + furi_assert(browser); + + browser->ext_filter = extension; + browser->skip_assets = skip_assets; + + with_view_model( + browser->view, (FileBrowserModel * model) { + model->file_icon = file_icon; + model->hide_ext = hide_ext; + return false; + }); +} + +void file_browser_start(FileBrowser* browser, string_t path) { + furi_assert(browser); + browser->worker = file_browser_worker_alloc(path, browser->ext_filter, browser->skip_assets); + file_browser_worker_set_callback_context(browser->worker, browser); + file_browser_worker_set_folder_callback(browser->worker, browser_folder_open_cb); + file_browser_worker_set_list_callback(browser->worker, browser_list_load_cb); + file_browser_worker_set_item_callback(browser->worker, browser_list_item_cb); + file_browser_worker_set_long_load_callback(browser->worker, browser_long_load_cb); +} + +void file_browser_stop(FileBrowser* browser) { + furi_assert(browser); + file_browser_worker_free(browser->worker); + with_view_model( + browser->view, (FileBrowserModel * model) { + items_array_reset(model->items); + model->item_cnt = 0; + model->item_idx = 0; + model->array_offset = 0; + model->list_offset = 0; + return false; + }); +} + +void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context) { + browser->context = context; + browser->callback = callback; +} + +static bool browser_is_item_in_array(FileBrowserModel* model, uint32_t idx) { + size_t array_size = items_array_size(model->items); + + if((idx >= (uint32_t)model->array_offset + array_size) || + (idx < (uint32_t)model->array_offset)) { + return false; + } + return true; +} + +static bool browser_is_list_load_required(FileBrowserModel* model) { + size_t array_size = items_array_size(model->items); + uint32_t item_cnt = (model->is_root) ? model->item_cnt : model->item_cnt - 1; + + if((model->list_loading) || (array_size >= item_cnt)) { + return false; + } + + if((model->array_offset > 0) && + (model->item_idx < (model->array_offset + ITEM_LIST_LEN_MAX / 4))) { + return true; + } + + if(((model->array_offset + array_size) < item_cnt) && + (model->item_idx > (int32_t)(model->array_offset + array_size - ITEM_LIST_LEN_MAX / 4))) { + return true; + } + + return false; +} + +static void browser_update_offset(FileBrowser* browser) { + furi_assert(browser); + + with_view_model( + browser->view, (FileBrowserModel * model) { + uint16_t bounds = model->item_cnt > (LIST_ITEMS - 1) ? 2 : model->item_cnt; + + if((model->item_cnt > (LIST_ITEMS - 1)) && + (model->item_idx >= ((int32_t)model->item_cnt - 1))) { + model->list_offset = model->item_idx - (LIST_ITEMS - 1); + } else if(model->list_offset < model->item_idx - bounds) { + model->list_offset = CLAMP( + model->item_idx - (int32_t)(LIST_ITEMS - 2), + (int32_t)model->item_cnt - bounds, + 0); + } else if(model->list_offset > model->item_idx - bounds) { + model->list_offset = + CLAMP(model->item_idx - 1, (int32_t)model->item_cnt - bounds, 0); + } + + return false; + }); +} + +static void + browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root) { + furi_assert(context); + FileBrowser* browser = (FileBrowser*)context; + + int32_t load_offset = 0; + + with_view_model( + browser->view, (FileBrowserModel * model) { + if(is_root) { + model->item_cnt = item_cnt; + model->item_idx = (file_idx > 0) ? file_idx : 0; + load_offset = + CLAMP(model->item_idx - ITEM_LIST_LEN_MAX / 2, (int32_t)model->item_cnt, 0); + } else { + model->item_cnt = item_cnt + 1; + model->item_idx = file_idx + 1; + load_offset = CLAMP( + model->item_idx - ITEM_LIST_LEN_MAX / 2 - 1, (int32_t)model->item_cnt - 1, 0); + } + model->array_offset = 0; + model->list_offset = 0; + model->is_root = is_root; + model->list_loading = true; + model->folder_loading = false; + return true; + }); + browser_update_offset(browser); + + file_browser_worker_load(browser->worker, load_offset, ITEM_LIST_LEN_MAX); +} + +static void browser_list_load_cb(void* context, uint32_t list_load_offset) { + furi_assert(context); + FileBrowser* browser = (FileBrowser*)context; + + BrowserItem_t back_item; + BrowserItem_t_init(&back_item); + back_item.type = BrowserItemTypeBack; + + with_view_model( + browser->view, (FileBrowserModel * model) { + items_array_reset(model->items); + model->array_offset = list_load_offset; + if(!model->is_root) { + if(list_load_offset == 0) { + items_array_push_back(model->items, back_item); + } else { + model->array_offset += 1; + } + } + return false; + }); + + BrowserItem_t_clear(&back_item); +} + +static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last) { + furi_assert(context); + FileBrowser* browser = (FileBrowser*)context; + + BrowserItem_t item; + + if(!is_last) { + BrowserItem_t_init(&item); + string_set(item.path, item_path); + item.type = (is_folder) ? BrowserItemTypeFolder : BrowserItemTypeFile; + + with_view_model( + browser->view, (FileBrowserModel * model) { + items_array_push_back(model->items, item); + return false; + }); + BrowserItem_t_clear(&item); + } else { + with_view_model( + browser->view, (FileBrowserModel * model) { + model->list_loading = false; + return true; + }); + } +} + +static void browser_long_load_cb(void* context) { + furi_assert(context); + FileBrowser* browser = (FileBrowser*)context; + + with_view_model( + browser->view, (FileBrowserModel * model) { + model->folder_loading = true; + return true; + }); +} + +static void browser_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) { + canvas_set_color(canvas, ColorBlack); + canvas_draw_box( + canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT, (scrollbar ? 122 : 127), FRAME_HEIGHT); + + canvas_set_color(canvas, ColorWhite); + canvas_draw_dot(canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT); + canvas_draw_dot(canvas, 1, Y_OFFSET + idx * FRAME_HEIGHT); + canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + 1); + + canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1)); + canvas_draw_dot(canvas, scrollbar ? 121 : 126, Y_OFFSET + idx * FRAME_HEIGHT); + canvas_draw_dot( + canvas, scrollbar ? 121 : 126, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1)); +} + +static void browser_draw_loading(Canvas* canvas, FileBrowserModel* model) { + uint8_t width = 49; + uint8_t height = 47; + uint8_t x = 128 / 2 - width / 2; + uint8_t y = 64 / 2 - height / 2; + + UNUSED(model); + + elements_bold_rounded_frame(canvas, x, y, width, height); + + canvas_set_font(canvas, FontSecondary); + elements_multiline_text(canvas, x + 7, y + 13, "Loading..."); + + canvas_draw_icon(canvas, x + 13, y + 19, &A_Loading_24); +} + +static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) { + uint32_t array_size = items_array_size(model->items); + bool show_scrollbar = model->item_cnt > LIST_ITEMS; + + string_t filename; + string_init(filename); + + for(uint32_t i = 0; i < MIN(model->item_cnt, LIST_ITEMS); i++) { + int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u); + + BrowserItemType item_type = BrowserItemTypeLoading; + + if(browser_is_item_in_array(model, idx)) { + BrowserItem_t* item = items_array_get( + model->items, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0)); + item_type = item->type; + file_browser_worker_get_filename( + item->path, filename, (model->hide_ext) && (item_type == BrowserItemTypeFile)); + } else { + string_set_str(filename, "---"); + } + + if(item_type == BrowserItemTypeBack) { + string_set_str(filename, ". ."); + } + + elements_string_fit_width( + canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX)); + + if(model->item_idx == idx) { + browser_draw_frame(canvas, i, show_scrollbar); + } else { + canvas_set_color(canvas, ColorBlack); + } + + if((item_type == BrowserItemTypeFile) && (model->file_icon)) { + canvas_draw_icon(canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, model->file_icon); + } else if(BrowserItemIcons[item_type] != NULL) { + canvas_draw_icon( + canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]); + } + canvas_draw_str(canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, string_get_cstr(filename)); + } + + if(show_scrollbar) { + elements_scrollbar_pos( + canvas, + 126, + Y_OFFSET, + canvas_height(canvas) - Y_OFFSET, + model->item_idx, + model->item_cnt); + } + + string_clear(filename); +} + +static void file_browser_view_draw_callback(Canvas* canvas, void* _model) { + FileBrowserModel* model = _model; + + if(model->folder_loading) { + browser_draw_loading(canvas, model); + } else { + browser_draw_list(canvas, model); + } +} + +static bool file_browser_view_input_callback(InputEvent* event, void* context) { + FileBrowser* browser = context; + furi_assert(browser); + bool consumed = false; + bool is_loading = false; + + with_view_model( + browser->view, (FileBrowserModel * model) { + is_loading = model->folder_loading; + return false; + }); + + if(is_loading) { + return false; + } else if(event->key == InputKeyUp || event->key == InputKeyDown) { + if(event->type == InputTypeShort || event->type == InputTypeRepeat) { + with_view_model( + browser->view, (FileBrowserModel * model) { + if(event->key == InputKeyUp) { + model->item_idx = + ((model->item_idx - 1) + model->item_cnt) % model->item_cnt; + if(browser_is_list_load_required(model)) { + model->list_loading = true; + int32_t load_offset = CLAMP( + model->item_idx - ITEM_LIST_LEN_MAX / 4 * 3, + (int32_t)model->item_cnt - ITEM_LIST_LEN_MAX, + 0); + file_browser_worker_load( + browser->worker, load_offset, ITEM_LIST_LEN_MAX); + } + } else if(event->key == InputKeyDown) { + model->item_idx = (model->item_idx + 1) % model->item_cnt; + if(browser_is_list_load_required(model)) { + model->list_loading = true; + int32_t load_offset = CLAMP( + model->item_idx - ITEM_LIST_LEN_MAX / 4 * 1, + (int32_t)model->item_cnt - ITEM_LIST_LEN_MAX, + 0); + file_browser_worker_load( + browser->worker, load_offset, ITEM_LIST_LEN_MAX); + } + } + return true; + }); + browser_update_offset(browser); + consumed = true; + } + } else if(event->key == InputKeyOk) { + if(event->type == InputTypeShort) { + BrowserItem_t* selected_item = NULL; + int32_t select_index = 0; + with_view_model( + browser->view, (FileBrowserModel * model) { + if(browser_is_item_in_array(model, model->item_idx)) { + selected_item = + items_array_get(model->items, model->item_idx - model->array_offset); + select_index = model->item_idx; + if((!model->is_root) && (select_index > 0)) { + select_index -= 1; + } + } + return false; + }); + + if(selected_item) { + if(selected_item->type == BrowserItemTypeBack) { + file_browser_worker_folder_exit(browser->worker); + } else if(selected_item->type == BrowserItemTypeFolder) { + file_browser_worker_folder_enter( + browser->worker, selected_item->path, select_index); + } else if(selected_item->type == BrowserItemTypeFile) { + string_set(*(browser->result_path), selected_item->path); + if(browser->callback) { + browser->callback(browser->context, true); + } + } + } + consumed = true; + } + } else if(event->key == InputKeyLeft) { + if(event->type == InputTypeShort) { + bool is_root = false; + with_view_model( + browser->view, (FileBrowserModel * model) { + is_root = model->is_root; + return false; + }); + if(!is_root) { + file_browser_worker_folder_exit(browser->worker); + } + consumed = true; + } + } + + return consumed; +} diff --git a/applications/gui/modules/file_browser.h b/applications/gui/modules/file_browser.h new file mode 100644 index 000000000..b77c6e65c --- /dev/null +++ b/applications/gui/modules/file_browser.h @@ -0,0 +1,39 @@ +/** + * @file file_browser.h + * GUI: FileBrowser view module API + */ + +#pragma once + +#include "m-string.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct FileBrowser FileBrowser; +typedef void (*FileBrowserCallback)(void* context, bool state); + +FileBrowser* file_browser_alloc(string_t* result_path); + +void file_browser_free(FileBrowser* browser); + +View* file_browser_get_view(FileBrowser* browser); + +void file_browser_configure( + FileBrowser* browser, + char* extension, + bool skip_assets, + const Icon* file_icon, + bool hide_ext); + +void file_browser_start(FileBrowser* browser, string_t path); + +void file_browser_stop(FileBrowser* browser); + +void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context); + +#ifdef __cplusplus +} +#endif diff --git a/applications/gui/modules/file_browser_worker.c b/applications/gui/modules/file_browser_worker.c new file mode 100644 index 000000000..13fc97111 --- /dev/null +++ b/applications/gui/modules/file_browser_worker.c @@ -0,0 +1,420 @@ +#include "file_browser_worker.h" +#include "furi/check.h" +#include "furi/common_defines.h" +#include "m-string.h" +#include "storage/filesystem_api_defines.h" +#include +#include +#include +#include +#include + +#define TAG "BrowserWorker" + +#define ASSETS_DIR "assets" +#define BROWSER_ROOT "/any" +#define FILE_NAME_LEN_MAX 256 +#define LONG_LOAD_THRESHOLD 100 + +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtLoad = (1 << 1), + WorkerEvtFolderEnter = (1 << 2), + WorkerEvtFolderExit = (1 << 3), +} WorkerEvtFlags; + +#define WORKER_FLAGS_ALL \ + (WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit) + +ARRAY_DEF(idx_last_array, int32_t) + +struct BrowserWorker { + FuriThread* thread; + + string_t filter_extension; + string_t path_next; + int32_t item_sel_idx; + uint32_t load_offset; + uint32_t load_count; + bool skip_assets; + idx_last_array_t idx_last; + + void* cb_ctx; + BrowserWorkerFolderOpenCallback folder_cb; + BrowserWorkerListLoadCallback list_load_cb; + BrowserWorkerListItemCallback list_item_cb; + BrowserWorkerLongLoadCallback long_load_cb; +}; + +static bool browser_path_is_file(string_t path) { + bool state = false; + FileInfo file_info; + Storage* storage = furi_record_open("storage"); + if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) { + if((file_info.flags & FSF_DIRECTORY) == 0) { + state = true; + } + } + furi_record_close("storage"); + return state; +} + +static bool browser_path_trim(string_t path) { + bool is_root = false; + size_t filename_start = string_search_rchar(path, '/'); + string_left(path, filename_start); + if((string_empty_p(path)) || (filename_start == STRING_FAILURE)) { + string_set_str(path, BROWSER_ROOT); + is_root = true; + } + return is_root; +} + +static bool browser_filter_by_name(BrowserWorker* browser, string_t name, bool is_folder) { + if(is_folder) { + // Skip assets folders (if enabled) + if(browser->skip_assets) { + return ((string_cmp_str(name, ASSETS_DIR) == 0) ? (false) : (true)); + } else { + return true; + } + } else { + // Filter files by extension + if((string_empty_p(browser->filter_extension)) || + (string_cmp_str(browser->filter_extension, "*") == 0)) { + return true; + } + if(string_end_with_string_p(name, browser->filter_extension)) { + return true; + } + } + return false; +} + +static bool browser_folder_check_and_switch(string_t path) { + FileInfo file_info; + Storage* storage = furi_record_open("storage"); + bool is_root = false; + while(1) { + // Check if folder is existing and navigate back if not + if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) { + if(file_info.flags & FSF_DIRECTORY) { + break; + } + } + if(is_root) { + break; + } + is_root = browser_path_trim(path); + } + furi_record_close("storage"); + return is_root; +} + +static bool browser_folder_init( + BrowserWorker* browser, + string_t path, + string_t filename, + uint32_t* item_cnt, + int32_t* file_idx) { + bool state = false; + FileInfo file_info; + uint32_t total_files_cnt = 0; + + Storage* storage = furi_record_open("storage"); + File* directory = storage_file_alloc(storage); + + char name_temp[FILE_NAME_LEN_MAX]; + string_t name_str; + string_init(name_str); + + *item_cnt = 0; + *file_idx = -1; + + if(storage_dir_open(directory, string_get_cstr(path))) { + state = true; + while(1) { + if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { + break; + } + if((storage_file_get_error(directory) == FSE_OK) && (name_temp[0] != '\0')) { + total_files_cnt++; + string_set_str(name_str, name_temp); + if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { + if(!string_empty_p(filename)) { + if(string_cmp(name_str, filename) == 0) { + *file_idx = *item_cnt; + } + } + (*item_cnt)++; + } + if(total_files_cnt == LONG_LOAD_THRESHOLD) { + if(browser->long_load_cb) { + browser->long_load_cb(browser->cb_ctx); + } + } + } + } + } + + string_clear(name_str); + + storage_dir_close(directory); + storage_file_free(directory); + + furi_record_close("storage"); + + return state; +} + +static bool + browser_folder_load(BrowserWorker* browser, string_t path, uint32_t offset, uint32_t count) { + FileInfo file_info; + + Storage* storage = furi_record_open("storage"); + File* directory = storage_file_alloc(storage); + + char name_temp[FILE_NAME_LEN_MAX]; + string_t name_str; + string_init(name_str); + + uint32_t items_cnt = 0; + + do { + if(!storage_dir_open(directory, string_get_cstr(path))) { + break; + } + + items_cnt = 0; + while(items_cnt < offset) { + if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { + break; + } + if(storage_file_get_error(directory) == FSE_OK) { + string_set_str(name_str, name_temp); + if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { + items_cnt++; + } + } else { + break; + } + } + if(items_cnt != offset) { + break; + } + + if(browser->list_load_cb) { + browser->list_load_cb(browser->cb_ctx, offset); + } + + items_cnt = 0; + while(items_cnt < count) { + if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { + break; + } + if(storage_file_get_error(directory) == FSE_OK) { + string_set_str(name_str, name_temp); + if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { + string_printf(name_str, "%s/%s", string_get_cstr(path), name_temp); + if(browser->list_item_cb) { + browser->list_item_cb( + browser->cb_ctx, name_str, (file_info.flags & FSF_DIRECTORY), false); + } + items_cnt++; + } + } else { + break; + } + } + if(browser->list_item_cb) { + browser->list_item_cb(browser->cb_ctx, NULL, false, true); + } + } while(0); + + string_clear(name_str); + + storage_dir_close(directory); + storage_file_free(directory); + + furi_record_close("storage"); + + return (items_cnt == count); +} + +static int32_t browser_worker(void* context) { + BrowserWorker* browser = (BrowserWorker*)context; + furi_assert(browser); + FURI_LOG_D(TAG, "Start"); + + uint32_t items_cnt = 0; + string_t path; + string_init_set_str(path, BROWSER_ROOT); + browser->item_sel_idx = -1; + + // If start path is a path to the file - try finding index of this file in a folder + string_t filename; + string_init(filename); + if(browser_path_is_file(browser->path_next)) { + file_browser_worker_get_filename(browser->path_next, filename, false); + } + + osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderEnter); + + while(1) { + uint32_t flags = osThreadFlagsWait(WORKER_FLAGS_ALL, osFlagsWaitAny, osWaitForever); + furi_assert((flags & osFlagsError) == 0); + + if(flags & WorkerEvtFolderEnter) { + string_set(path, browser->path_next); + bool is_root = browser_folder_check_and_switch(path); + + // Push previous selected item index to history array + idx_last_array_push_back(browser->idx_last, browser->item_sel_idx); + + int32_t file_idx = 0; + browser_folder_init(browser, path, filename, &items_cnt, &file_idx); + FURI_LOG_D( + TAG, + "Enter folder: %s items: %u idx: %d", + string_get_cstr(path), + items_cnt, + file_idx); + if(browser->folder_cb) { + browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root); + } + string_reset(filename); + } + + if(flags & WorkerEvtFolderExit) { + browser_path_trim(path); + bool is_root = browser_folder_check_and_switch(path); + + int32_t file_idx = 0; + browser_folder_init(browser, path, filename, &items_cnt, &file_idx); + if(idx_last_array_size(browser->idx_last) > 0) { + // Pop previous selected item index from history array + idx_last_array_pop_back(&file_idx, browser->idx_last); + } + FURI_LOG_D( + TAG, "Exit to: %s items: %u idx: %d", string_get_cstr(path), items_cnt, file_idx); + if(browser->folder_cb) { + browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root); + } + } + + if(flags & WorkerEvtLoad) { + FURI_LOG_D(TAG, "Load offset: %u cnt: %u", browser->load_offset, browser->load_count); + browser_folder_load(browser, path, browser->load_offset, browser->load_count); + } + + if(flags & WorkerEvtStop) { + break; + } + } + + string_clear(filename); + string_clear(path); + + FURI_LOG_D(TAG, "End"); + return 0; +} + +void file_browser_worker_get_filename(string_t path, string_t name, bool trim_ext) { + size_t filename_start = string_search_rchar(path, '/'); + if(filename_start > 0) { + filename_start++; + string_set_n(name, path, filename_start, string_size(path) - filename_start); + } + if(trim_ext) { + size_t dot = string_search_rchar(name, '.'); + if(dot > 0) { + string_left(name, dot); + } + } +} + +BrowserWorker* file_browser_worker_alloc(string_t path, char* filter_ext, bool skip_assets) { + BrowserWorker* browser = malloc(sizeof(BrowserWorker)); + + idx_last_array_init(browser->idx_last); + + string_init_set_str(browser->filter_extension, filter_ext); + browser->skip_assets = skip_assets; + string_init_set(browser->path_next, path); + + browser->thread = furi_thread_alloc(); + furi_thread_set_name(browser->thread, "BrowserWorker"); + furi_thread_set_stack_size(browser->thread, 2048); + furi_thread_set_context(browser->thread, browser); + furi_thread_set_callback(browser->thread, browser_worker); + furi_thread_start(browser->thread); + + return browser; +} + +void file_browser_worker_free(BrowserWorker* browser) { + furi_assert(browser); + + osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtStop); + furi_thread_join(browser->thread); + furi_thread_free(browser->thread); + + string_clear(browser->filter_extension); + string_clear(browser->path_next); + + idx_last_array_clear(browser->idx_last); + + free(browser); +} + +void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context) { + furi_assert(browser); + browser->cb_ctx = context; +} + +void file_browser_worker_set_folder_callback( + BrowserWorker* browser, + BrowserWorkerFolderOpenCallback cb) { + furi_assert(browser); + browser->folder_cb = cb; +} + +void file_browser_worker_set_list_callback( + BrowserWorker* browser, + BrowserWorkerListLoadCallback cb) { + furi_assert(browser); + browser->list_load_cb = cb; +} + +void file_browser_worker_set_item_callback( + BrowserWorker* browser, + BrowserWorkerListItemCallback cb) { + furi_assert(browser); + browser->list_item_cb = cb; +} + +void file_browser_worker_set_long_load_callback( + BrowserWorker* browser, + BrowserWorkerLongLoadCallback cb) { + furi_assert(browser); + browser->long_load_cb = cb; +} + +void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx) { + furi_assert(browser); + string_set(browser->path_next, path); + browser->item_sel_idx = item_idx; + osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderEnter); +} + +void file_browser_worker_folder_exit(BrowserWorker* browser) { + furi_assert(browser); + osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderExit); +} + +void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count) { + furi_assert(browser); + browser->load_offset = offset; + browser->load_count = count; + osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtLoad); +} diff --git a/applications/gui/modules/file_browser_worker.h b/applications/gui/modules/file_browser_worker.h new file mode 100644 index 000000000..821d5103f --- /dev/null +++ b/applications/gui/modules/file_browser_worker.h @@ -0,0 +1,57 @@ +#pragma once + +#include "m-string.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BrowserWorker BrowserWorker; +typedef void (*BrowserWorkerFolderOpenCallback)( + void* context, + uint32_t item_cnt, + int32_t file_idx, + bool is_root); +typedef void (*BrowserWorkerListLoadCallback)(void* context, uint32_t list_load_offset); +typedef void (*BrowserWorkerListItemCallback)( + void* context, + string_t item_path, + bool is_folder, + bool is_last); +typedef void (*BrowserWorkerLongLoadCallback)(void* context); + +void file_browser_worker_get_filename(string_t path, string_t name, bool trim_ext); + +BrowserWorker* file_browser_worker_alloc(string_t path, char* filter_ext, bool skip_assets); + +void file_browser_worker_free(BrowserWorker* browser); + +void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context); + +void file_browser_worker_set_folder_callback( + BrowserWorker* browser, + BrowserWorkerFolderOpenCallback cb); + +void file_browser_worker_set_list_callback( + BrowserWorker* browser, + BrowserWorkerListLoadCallback cb); + +void file_browser_worker_set_item_callback( + BrowserWorker* browser, + BrowserWorkerListItemCallback cb); + +void file_browser_worker_set_long_load_callback( + BrowserWorker* browser, + BrowserWorkerLongLoadCallback cb); + +void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx); + +void file_browser_worker_folder_exit(BrowserWorker* browser); + +void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count); + +#ifdef __cplusplus +} +#endif