diff --git a/applications/main/momentum_app/momentum_app.c b/applications/main/momentum_app/momentum_app.c index 9b929ab70..50874f716 100644 --- a/applications/main/momentum_app/momentum_app.c +++ b/applications/main/momentum_app/momentum_app.c @@ -313,6 +313,10 @@ MomentumApp* momentum_app_alloc() { view_dispatcher_add_view( app->view_dispatcher, MomentumAppViewDialogEx, dialog_ex_get_view(app->dialog_ex)); + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MomentumAppViewWidget, widget_get_view(app->widget)); + // Settings init app->asset_pack_index = 0; @@ -441,6 +445,8 @@ void momentum_app_free(MomentumApp* app) { popup_free(app->popup); view_dispatcher_remove_view(app->view_dispatcher, MomentumAppViewDialogEx); dialog_ex_free(app->dialog_ex); + view_dispatcher_remove_view(app->view_dispatcher, MomentumAppViewWidget); + widget_free(app->widget); // View Dispatcher and Scene Manager view_dispatcher_free(app->view_dispatcher); diff --git a/applications/main/momentum_app/momentum_app.h b/applications/main/momentum_app/momentum_app.h index 776f21b67..f43e07ade 100644 --- a/applications/main/momentum_app/momentum_app.h +++ b/applications/main/momentum_app/momentum_app.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -63,7 +64,7 @@ typedef struct { NumberInput* number_input; Popup* popup; DialogEx* dialog_ex; - + Widget* widget; CharList_t asset_pack_names; uint8_t asset_pack_index; CharList_t mainmenu_app_labels; @@ -107,6 +108,7 @@ typedef enum { MomentumAppViewNumberInput, MomentumAppViewPopup, MomentumAppViewDialogEx, + MomentumAppViewWidget, } MomentumAppView; bool momentum_app_apply(MomentumApp* app); diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_config.h b/applications/main/momentum_app/scenes/momentum_app_scene_config.h index c9eb6e996..804b09d1c 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_config.h +++ b/applications/main/momentum_app/scenes/momentum_app_scene_config.h @@ -2,6 +2,8 @@ ADD_SCENE(momentum_app, start, Start) ADD_SCENE(momentum_app, interface, Interface) ADD_SCENE(momentum_app, interface_graphics, InterfaceGraphics) ADD_SCENE(momentum_app, interface_graphics_pack, InterfaceGraphicsPack) +ADD_SCENE(momentum_app, interface_graphics_pack_warning, InterfaceGraphicsPackWarning) +ADD_SCENE(momentum_app, interface_graphics_pack_warning_info, InterfaceGraphicsPackWarningInfo) ADD_SCENE(momentum_app, interface_mainmenu, InterfaceMainmenu) ADD_SCENE(momentum_app, interface_mainmenu_add, InterfaceMainmenuAdd) ADD_SCENE(momentum_app, interface_mainmenu_add_main, InterfaceMainmenuAddMain) diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics.c b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics.c index b3b781ec3..2bac8710e 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics.c +++ b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics.c @@ -2,11 +2,29 @@ enum VarItemListIndex { VarItemListIndexAssetPack, + VarItemListIndexPackWarning, VarItemListIndexAnimSpeed, VarItemListIndexCycleAnims, VarItemListIndexUnlockAnims, }; +static bool check_asset_pack_folders(const char* pack_name) { + if(pack_name == NULL) return false; + Storage* storage = furi_record_open(RECORD_STORAGE); + FuriString* path = furi_string_alloc(); + bool result = false; + + furi_string_printf(path, "%s/%s/Icons", ASSET_PACKS_PATH, pack_name); + if(storage_dir_exists(storage, furi_string_get_cstr(path))) result = true; + if(!result) { + furi_string_printf(path, "%s/%s/Fonts", ASSET_PACKS_PATH, pack_name); + if(storage_dir_exists(storage, furi_string_get_cstr(path))) result = true; + } + + furi_string_free(path); + return result; +} + void momentum_app_scene_interface_graphics_var_item_list_callback(void* context, uint32_t index) { MomentumApp* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); @@ -24,6 +42,12 @@ static void momentum_app_scene_interface_graphics_asset_pack_changed(VariableIte app->asset_pack_index = index; app->save_settings = true; app->apply_pack = true; + + // Workaround to force scene reload to rebuild the list with/without pack warning and + // return to 0 index ("Asset Pack"). + scene_manager_previous_scene(app->scene_manager); + scene_manager_next_scene(app->scene_manager, MomentumAppSceneInterfaceGraphics); + scene_manager_set_scene_state(app->scene_manager, MomentumAppSceneInterfaceGraphics, 0); } const char* const anim_speed_names[] = { @@ -118,6 +142,10 @@ void momentum_app_scene_interface_graphics_on_enter(void* context) { VariableItem* item; uint8_t value_index; + const char* selected_asset_pack = + app->asset_pack_index == 0 ? + "Default" : + *CharList_get(app->asset_pack_names, app->asset_pack_index - 1); item = variable_item_list_add( var_item_list, "Asset Pack", @@ -125,11 +153,14 @@ void momentum_app_scene_interface_graphics_on_enter(void* context) { momentum_app_scene_interface_graphics_asset_pack_changed, app); variable_item_set_current_value_index(item, app->asset_pack_index); - variable_item_set_current_value_text( - item, - app->asset_pack_index == 0 ? - "Default" : - *CharList_get(app->asset_pack_names, app->asset_pack_index - 1)); + variable_item_set_current_value_text(item, selected_asset_pack); + + if(app->asset_pack_index > 0) { + if(check_asset_pack_folders(selected_asset_pack)) { + item = variable_item_list_add(var_item_list, "Size Warning", 0, NULL, app); + variable_item_set_current_value_text(item, ">"); + } + } item = variable_item_list_add( var_item_list, @@ -183,6 +214,11 @@ bool momentum_app_scene_interface_graphics_on_event(void* context, SceneManagerE switch(event.event) { case VarItemListIndexAssetPack: scene_manager_next_scene(app->scene_manager, MomentumAppSceneInterfaceGraphicsPack); + break; + case VarItemListIndexPackWarning: + scene_manager_next_scene( + app->scene_manager, MomentumAppSceneInterfaceGraphicsPackWarning); + break; default: break; } diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning.c b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning.c new file mode 100644 index 000000000..dd0a4c177 --- /dev/null +++ b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning.c @@ -0,0 +1,60 @@ +#include "../momentum_app.h" + +void momentum_app_scene_interface_graphics_pack_warning_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + MomentumApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +void momentum_app_scene_interface_graphics_pack_warning_on_enter(void* context) { + MomentumApp* app = context; + + widget_add_button_element( + app->widget, + GuiButtonTypeLeft, + "Back", + momentum_app_scene_interface_graphics_pack_warning_widget_callback, + app); + widget_add_button_element( + app->widget, + GuiButtonTypeRight, + "Info", + momentum_app_scene_interface_graphics_pack_warning_widget_callback, + app); + + widget_add_text_box_element( + app->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, "\e#Size Warning\e#", false); + + const char* body = + "Your selected pack contains Fonts & Icons, which remain loaded and use up memory"; + widget_add_string_multiline_element( + app->widget, 0, 18, AlignLeft, AlignTop, FontSecondary, body); + + view_dispatcher_switch_to_view(app->view_dispatcher, MomentumAppViewWidget); +} + +bool momentum_app_scene_interface_graphics_pack_warning_on_event( + void* context, + SceneManagerEvent event) { + MomentumApp* app = context; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + return scene_manager_previous_scene(app->scene_manager); + } else if(event.event == GuiButtonTypeRight) { + scene_manager_next_scene( + app->scene_manager, MomentumAppSceneInterfaceGraphicsPackWarningInfo); + return true; + } + } + return false; +} + +void momentum_app_scene_interface_graphics_pack_warning_on_exit(void* context) { + MomentumApp* app = context; + widget_reset(app->widget); +} diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning_info.c b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning_info.c new file mode 100644 index 000000000..6aec95459 --- /dev/null +++ b/applications/main/momentum_app/scenes/momentum_app_scene_interface_graphics_pack_warning_info.c @@ -0,0 +1,107 @@ +#include "../momentum_app.h" + +void momentum_app_scene_interface_graphics_pack_warning_info_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + MomentumApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +void momentum_app_scene_interface_graphics_pack_warning_info_on_enter(void* context) { + MomentumApp* app = context; + + widget_add_button_element( + app->widget, + GuiButtonTypeLeft, + "Exit", + momentum_app_scene_interface_graphics_pack_warning_info_widget_callback, + app); + + const char* dirs[] = {"Fonts", "Icons"}; + const char* font_exts[] = {".u8f"}; + const char* icon_exts[] = {".bm", ".bmx"}; + const char** exts[] = {font_exts, icon_exts}; + const size_t ext_counts[] = {COUNT_OF(font_exts), COUNT_OF(icon_exts)}; + const char* selected_pack = + app->asset_pack_index == 0 ? + "Default" : + *CharList_get(app->asset_pack_names, app->asset_pack_index - 1); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FuriString* path = furi_string_alloc(); + FuriString** paths = NULL; + size_t num_paths = 0; + + for(size_t i = 0; i < COUNT_OF(dirs); i++) { + FuriString** files = NULL; + size_t num_files = 0; + + furi_string_printf(path, "%s/%s/%s", ASSET_PACKS_PATH, selected_pack, dirs[i]); + if(storage_dir_exists(storage, furi_string_get_cstr(path))) { + if(storage_list_dir( + storage, + furi_string_get_cstr(path), + &files, + &num_files, + true, + exts[i], + ext_counts[i])) { + paths = realloc(paths, (num_paths + num_files) * sizeof(FuriString*)); + memcpy(&paths[num_paths], files, num_files * sizeof(FuriString*)); + num_paths += num_files; + free(files); + } + } + } + + size_t f_count = 0; + size_t i_count = 0; + for(size_t i = 0; i < num_paths; i++) { + if(furi_string_start_with_str(paths[i], ASSET_PACKS_PATH)) { + if(furi_string_search_str(paths[i], "/Fonts/") != FURI_STRING_FAILURE) { + f_count++; + } else if(furi_string_search_str(paths[i], "/Icons/") != FURI_STRING_FAILURE) { + i_count++; + } + } + } + + FuriString* title = furi_string_alloc(); + if(f_count > 0 && i_count > 0) { + furi_string_printf(title, "%zu Fonts, %zu Icons", f_count, i_count); + } else if(f_count > 0) { + furi_string_printf(title, "%zu Fonts", f_count); + } else if(i_count > 0) { + furi_string_printf(title, "%zu Icons", i_count); + } + + widget_add_string_element( + app->widget, 35, 57, AlignLeft, AlignCenter, FontPrimary, furi_string_get_cstr(title)); + furi_string_free(title); + + widget_add_file_list_element(app->widget, 0, 12, 4, paths, num_paths, 0, 52, true); + + view_dispatcher_switch_to_view(app->view_dispatcher, MomentumAppViewWidget); +} + +bool momentum_app_scene_interface_graphics_pack_warning_info_on_event( + void* context, + SceneManagerEvent event) { + MomentumApp* app = context; + + if((event.type == SceneManagerEventTypeCustom && event.event == GuiButtonTypeLeft) || + event.type == SceneManagerEventTypeBack) { + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MomentumAppSceneInterfaceGraphics); + return true; + } + return false; +} + +void momentum_app_scene_interface_graphics_pack_warning_info_on_exit(void* context) { + MomentumApp* app = context; + widget_reset(app->widget); +} diff --git a/applications/services/gui/modules/widget.c b/applications/services/gui/modules/widget.c index 8efb9601d..5996f6ba8 100644 --- a/applications/services/gui/modules/widget.c +++ b/applications/services/gui/modules/widget.c @@ -5,11 +5,6 @@ ARRAY_DEF(ElementArray, WidgetElement*, M_PTR_OPLIST); // NOLINT -struct Widget { - View* view; - void* context; -}; - typedef struct { ElementArray_t element; } GuiWidgetModel; @@ -119,6 +114,23 @@ static void widget_add_element(Widget* widget, WidgetElement* element) { true); } +WidgetElement* widget_add_file_list_element( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size) { + furi_assert(widget); + WidgetElement* file_list_element = widget_element_file_list_create( + widget, x, y, lines, files, count, scrollbar_y, scrollbar_height, show_size); + widget_add_element(widget, file_list_element); + return file_list_element; +} + WidgetElement* widget_add_string_multiline_element( Widget* widget, uint8_t x, diff --git a/applications/services/gui/modules/widget.h b/applications/services/gui/modules/widget.h index 51ce2785f..c912d20f7 100644 --- a/applications/services/gui/modules/widget.h +++ b/applications/services/gui/modules/widget.h @@ -41,6 +41,29 @@ void widget_reset(Widget* widget); */ View* widget_get_view(Widget* widget); +/** Add File List Element + * + * @param widget Widget instance + * @param x x coordinate + * @param y y coordinate + * @param lines Number of lines visible + * @param files Array of FuriString pointers + * @param count Number of files + * @param scrollbar_y Y coordinate of the scrollbar + * @param scrollbar_height Height of the scrollbar + * @param show_size Show file size + */ +WidgetElement* widget_add_file_list_element( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size); + /** Add Multi String Element * * @param widget Widget instance diff --git a/applications/services/gui/modules/widget_elements/widget_element.h b/applications/services/gui/modules/widget_elements/widget_element.h index 473fabd04..15faa4a46 100644 --- a/applications/services/gui/modules/widget_elements/widget_element.h +++ b/applications/services/gui/modules/widget_elements/widget_element.h @@ -5,6 +5,7 @@ #pragma once +#include "input/input.h" #ifdef __cplusplus extern "C" { #endif diff --git a/applications/services/gui/modules/widget_elements/widget_element_file_list.c b/applications/services/gui/modules/widget_elements/widget_element_file_list.c new file mode 100644 index 000000000..f51547fcd --- /dev/null +++ b/applications/services/gui/modules/widget_elements/widget_element_file_list.c @@ -0,0 +1,225 @@ +#include "assets_icons.h" +#include "path.h" +#include "widget_element_i.h" +#include +#include +#include "archive/archive_i.h" +#include "assets_icons.h" + +#define SCROLL_INTERVAL (333) +#define SCROLL_DELAY (2) + +const char* units_short[] = {"B", "K", "M", "G", "T"}; + +typedef struct { + FuriString* path; + const Icon* icon; + char size_num[8]; + char size_unit[2]; +} FileListItem; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t lines; + FileListItem* files; + size_t count; + size_t offset; + uint8_t scrollbar_y; + bool show_size; + uint8_t scrollbar_height; + size_t scroll_counter; + FuriTimer* scroll_timer; +} FileListModel; + +static void format_file_size( + uint64_t size, + char* num_buf, + size_t num_size, + char* unit_buf, + size_t unit_size) { + double formatted_size = size; + uint8_t unit = 0; + + while(formatted_size >= 1024 && unit < COUNT_OF(units_short) - 1) { + formatted_size /= 1024; + unit++; + } + + if(unit == 0) { + snprintf(num_buf, num_size, "%d", (int)formatted_size); + } else { + snprintf(num_buf, num_size, "%.1f", (double)formatted_size); + } + snprintf(unit_buf, unit_size, "%s", units_short[unit]); +} + +static void widget_element_file_list_draw(Canvas* canvas, WidgetElement* element) { + furi_assert(canvas); + furi_assert(element); + FileListModel* model = element->model; + size_t items_visible = MIN(model->lines, model->count); + + canvas_set_font(canvas, FontSecondary); + for(size_t i = 0; i < items_visible; i++) { + size_t idx = model->offset + i; + if(idx < model->count) { + canvas_draw_icon( + canvas, model->x + 2, model->y + (i * FRAME_HEIGHT) - 9, model->files[idx].icon); + + size_t inner_x = 123; + if(model->show_size && model->files[idx].size_num[0] != '\0') { + canvas_set_font(canvas, FontPrimary); + uint16_t num_width = canvas_string_width(canvas, model->files[idx].size_num); + canvas_set_font(canvas, FontSecondary); + uint16_t unit_width = canvas_string_width(canvas, model->files[idx].size_unit); + uint16_t total_width = num_width + unit_width; + inner_x = model->x + (128 - model->x) - total_width - 5; + inner_x--; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str( + canvas, inner_x, model->y + (i * FRAME_HEIGHT), model->files[idx].size_num); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str( + canvas, + inner_x + num_width + 1, + model->y + (i * FRAME_HEIGHT), + model->files[idx].size_unit); + } + + size_t scroll_counter = model->scroll_counter; + scroll_counter = + i == 0 ? (model->count > model->lines && + (scroll_counter < SCROLL_DELAY ? 0 : scroll_counter - SCROLL_DELAY)) : + 0; + + elements_scrollable_text_line( + canvas, + model->x + 15, + model->y + (i * FRAME_HEIGHT), + inner_x - 19, + model->files[idx].path, + scroll_counter, + i != 0 || model->count <= model->lines); + } + } + + if(model->count > model->lines) { + elements_scrollbar_pos( + canvas, + 128, + model->scrollbar_y, + model->scrollbar_height, + model->offset, + model->count - (model->lines - 1)); + } +} + +static bool widget_element_file_list_input(InputEvent* event, WidgetElement* element) { + furi_assert(element); + FileListModel* model = element->model; + bool consumed = false; + + if(model->count > model->lines && + (event->type == InputTypeShort || event->type == InputTypeRepeat)) { + if(event->key == InputKeyUp) { + model->offset = (model->offset > 0) ? model->offset - 1 : model->count - model->lines; + model->scroll_counter = 0; + consumed = true; + } else if(event->key == InputKeyDown) { + model->offset = ((model->offset + model->lines) < model->count) ? model->offset + 1 : + 0; + model->scroll_counter = 0; + consumed = true; + } + } + + return consumed; +} + +static void widget_element_file_list_timer_callback(void* context) { + WidgetElement* element = context; + FileListModel* file_model = element->model; + file_model->scroll_counter++; + with_view_model(element->parent->view, void* _model, { UNUSED(_model); }, true); +} + +static void widget_element_file_list_free(WidgetElement* element) { + furi_assert(element); + FileListModel* model = element->model; + furi_timer_stop(model->scroll_timer); + furi_timer_free(model->scroll_timer); + for(size_t i = 0; i < model->count; i++) { + furi_string_free(model->files[i].path); + } + free(model->files); + free(model); + free(element); +} + +WidgetElement* widget_element_file_list_create( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size) { + // Allocate and init model + FileListModel* model = malloc(sizeof(FileListModel)); + model->x = x; + model->y = y; + model->lines = lines; + model->count = count; + model->scrollbar_y = scrollbar_y; + model->scrollbar_height = scrollbar_height; + model->show_size = show_size; + model->offset = 0; + model->scroll_counter = 0; + model->files = malloc(sizeof(FileListItem) * count); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FileInfo info; + for(size_t i = 0; i < count; i++) { + model->files[i].path = furi_string_alloc(); + path_extract_filename(files[i], model->files[i].path, false); + model->files[i].size_num[0] = '\0'; + model->files[i].size_unit[0] = '\0'; + + if(storage_dir_exists(storage, furi_string_get_cstr(files[i]))) { + model->files[i].icon = &I_dir_10px; + } else { + const char* ext = strrchr(furi_string_get_cstr(model->files[i].path), '.'); + model->files[i].icon = (ext && strcasecmp(ext, ".js") == 0) ? &I_js_script_10px : + &I_unknown_10px; + } + + if(show_size) { + storage_common_stat(storage, furi_string_get_cstr(files[i]), &info); + format_file_size( + info.size, + model->files[i].size_num, + sizeof(model->files[i].size_num), + model->files[i].size_unit, + sizeof(model->files[i].size_unit)); + } + } + furi_record_close(RECORD_STORAGE); + + // Allocate and init Element + WidgetElement* element = malloc(sizeof(WidgetElement)); + element->draw = widget_element_file_list_draw; + element->input = widget_element_file_list_input; + element->free = widget_element_file_list_free; + element->parent = widget; + element->model = model; + + model->scroll_timer = + furi_timer_alloc(widget_element_file_list_timer_callback, FuriTimerTypePeriodic, element); + furi_timer_start(model->scroll_timer, SCROLL_INTERVAL); + + return element; +} diff --git a/applications/services/gui/modules/widget_elements/widget_element_i.h b/applications/services/gui/modules/widget_elements/widget_element_i.h index 9a873fef7..f6ff6579b 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_i.h +++ b/applications/services/gui/modules/widget_elements/widget_element_i.h @@ -5,7 +5,7 @@ #pragma once -#include "../widget.h" +#include "../widget_i.h" #include "widget_element.h" #include #include @@ -34,6 +34,18 @@ struct WidgetElement { Widget* parent; }; +/** Create file list element */ +WidgetElement* widget_element_file_list_create( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size); + /** Create multi string element */ WidgetElement* widget_element_string_multiline_create( uint8_t x, diff --git a/applications/services/gui/modules/widget_i.h b/applications/services/gui/modules/widget_i.h new file mode 100644 index 000000000..d4f0a57eb --- /dev/null +++ b/applications/services/gui/modules/widget_i.h @@ -0,0 +1,10 @@ +#pragma once + +#include "widget.h" +#include +#include + +struct Widget { + View* view; + void* context; +}; diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index 6d707fa9d..4bb064b76 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -274,6 +274,27 @@ bool storage_dir_rewind(File* file); */ bool storage_dir_exists(Storage* storage, const char* path); +/** + * @brief List the contents of a directory. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path of the directory in question. + * @param files pointer to an array of FuriString pointers to contain the file names. + * @param num_files pointer to the number of files in the directory. + * @param ignore_dirs if true, only files will be returned, not directories. + * @param include_ext pointer to an array of zero-terminated strings of extensions to include. + * @param ext_count the number of extensions in the include_ext array. + * @return true if the directory was successfully listed, false otherwise. + */ +bool storage_list_dir( + Storage* storage, + const char* path, + FuriString*** files, + size_t* num_files, + bool ignore_dirs, + const char** include_ext, + size_t ext_count); + /******************* Common Functions *******************/ /** diff --git a/applications/services/storage/storage_external_api.c b/applications/services/storage/storage_external_api.c index 95119561c..7cfb70ad0 100644 --- a/applications/services/storage/storage_external_api.c +++ b/applications/services/storage/storage_external_api.c @@ -440,6 +440,79 @@ bool storage_dir_exists(Storage* storage, const char* path) { return exist; } + +bool storage_list_dir( + Storage* storage, + const char* path, + FuriString*** files, + size_t* num_files, + bool ignore_dirs, + const char** include_ext, + size_t ext_count) { + furi_check(storage); + furi_check(path); + furi_check(files); + furi_check(num_files); + + FileInfo fileinfo; + bool result = false; + char* name = malloc(MAX_NAME_LENGTH); + File* dir = storage_file_alloc(storage); + + do { + if(!storage_dir_open(dir, path)) break; + + while(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { + if(strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + + if(file_info_is_dir(&fileinfo)) { + if(!ignore_dirs) { + *files = realloc(*files, (*num_files + 1) * sizeof(FuriString*)); + (*files)[*num_files] = furi_string_alloc_printf("%s/%s", path, name); + (*num_files)++; + } + FuriString* subpath = furi_string_alloc_printf("%s/%s", path, name); + storage_list_dir( + storage, + furi_string_get_cstr(subpath), + files, + num_files, + ignore_dirs, + include_ext, + ext_count); + furi_string_free(subpath); + } else { + bool add_file = (include_ext == NULL || ext_count == 0); + + if(!add_file) { + const char* ext = strrchr(name, '.'); + if(ext != NULL) { + for(size_t i = 0; i < ext_count; i++) { + if(strcasecmp(ext, include_ext[i]) == 0) { + add_file = true; + break; + } + } + } + } + + if(add_file) { + *files = realloc(*files, (*num_files + 1) * sizeof(FuriString*)); + (*files)[*num_files] = furi_string_alloc_printf("%s/%s", path, name); + (*num_files)++; + } + } + } + result = true; + } while(false); + + storage_dir_close(dir); + storage_file_free(dir); + free(name); + + return result; +} + /****************** COMMON ******************/ FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp) { diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index f5e79d7ee..8576602f0 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3333,6 +3333,7 @@ Function,+,storage_get_next_filename,void,"Storage*, const char*, const char*, c Function,+,storage_get_pubsub,FuriPubSub*,Storage* Function,+,storage_int_backup,FS_Error,"Storage*, const char*" Function,+,storage_int_restore,FS_Error,"Storage*, const char*, StorageNameConverter" +Function,+,storage_list_dir,_Bool,"Storage*, const char*, FuriString***, size_t*, _Bool, const char**, size_t" Function,+,storage_sd_format,FS_Error,Storage* Function,+,storage_sd_info,FS_Error,"Storage*, SDInfo*" Function,+,storage_sd_mount,FS_Error,Storage* @@ -3836,6 +3837,7 @@ Function,-,vsscanf,int,"const char*, const char*, __gnuc_va_list" Function,-,wcstombs,size_t,"char*, const wchar_t*, size_t" Function,-,wctomb,int,"char*, wchar_t" Function,+,widget_add_button_element,WidgetElement*,"Widget*, GuiButtonType, const char*, ButtonCallback, void*" +Function,+,widget_add_file_list_element,WidgetElement*,"Widget*, uint8_t, uint8_t, uint8_t, FuriString**, size_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_frame_element,WidgetElement*,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t" Function,+,widget_add_icon_element,WidgetElement*,"Widget*, uint8_t, uint8_t, const Icon*" Function,+,widget_add_string_element,WidgetElement*,"Widget*, uint8_t, uint8_t, Align, Align, Font, const char*"