Просмотр исходного кода

[FL-2491] File browser GUI module (#1237)

* File browser module and test app
* nfc: Add support for saved files in subdirectories
* nfc: Use helper function to get shadow path when loading data
* File browser dialog integration pt.1
* File browser dialog integration pt.2
* Gui,Dialogs: drop file select
* Correct use of dynamic string_t(string_ptr)

Co-authored-by: Yukai Li <yukaili.geek@gmail.com>
Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
Nikolay Minaylov 3 лет назад
Родитель
Сommit
79920a3522
82 измененных файлов с 2025 добавлено и 1007 удалено
  1. 9 0
      applications/applications.c
  2. 6 0
      applications/applications.mk
  3. 9 28
      applications/bad_usb/bad_usb_app.c
  4. 1 2
      applications/bad_usb/bad_usb_app_i.h
  5. 8 6
      applications/bad_usb/scenes/bad_usb_scene_file_select.c
  6. 5 4
      applications/bad_usb/scenes/bad_usb_scene_work.c
  7. 5 3
      applications/bad_usb/views/bad_usb_view.c
  8. 1 1
      applications/bad_usb/views/bad_usb_view.h
  9. 99 0
      applications/debug_tools/file_browser_test/file_browser_app.c
  10. 32 0
      applications/debug_tools/file_browser_test/file_browser_app_i.h
  11. 30 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene.c
  12. 29 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene.h
  13. 43 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene_browser.c
  14. 3 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene_config.h
  15. 36 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene_result.c
  16. 44 0
      applications/debug_tools/file_browser_test/scenes/file_browser_scene_start.c
  17. 4 3
      applications/dialogs/dialogs.c
  18. 14 11
      applications/dialogs/dialogs.h
  19. 17 12
      applications/dialogs/dialogs_api.c
  20. 9 7
      applications/dialogs/dialogs_message.h
  21. 59 0
      applications/dialogs/dialogs_module_file_browser.c
  22. 1 1
      applications/dialogs/dialogs_module_file_browser.h
  23. 0 59
      applications/dialogs/dialogs_module_file_select.c
  24. 534 0
      applications/gui/modules/file_browser.c
  25. 39 0
      applications/gui/modules/file_browser.h
  26. 408 0
      applications/gui/modules/file_browser_worker.c
  27. 55 0
      applications/gui/modules/file_browser_worker.h
  28. 0 475
      applications/gui/modules/file_select.c
  29. 0 31
      applications/gui/modules/file_select.h
  30. 3 2
      applications/gui/modules/validators.c
  31. 33 58
      applications/ibutton/ibutton.c
  32. 1 2
      applications/ibutton/ibutton_i.h
  33. 2 1
      applications/ibutton/scenes/ibutton_scene_add_type.c
  34. 8 1
      applications/ibutton/scenes/ibutton_scene_delete_confirm.c
  35. 11 3
      applications/ibutton/scenes/ibutton_scene_emulate.c
  36. 8 1
      applications/ibutton/scenes/ibutton_scene_info.c
  37. 1 1
      applications/ibutton/scenes/ibutton_scene_read.c
  38. 19 5
      applications/ibutton/scenes/ibutton_scene_save_name.c
  39. 1 0
      applications/ibutton/scenes/ibutton_scene_start.c
  40. 12 3
      applications/ibutton/scenes/ibutton_scene_write.c
  41. 9 8
      applications/infrared/infrared_app.cpp
  42. 2 0
      applications/infrared/infrared_app.h
  43. 86 44
      applications/infrared/infrared_app_remote_manager.cpp
  44. 12 15
      applications/infrared/infrared_app_remote_manager.h
  45. 12 1
      applications/infrared/scene/infrared_app_scene_edit_rename.cpp
  46. 8 11
      applications/infrared/scene/infrared_app_scene_remote_list.cpp
  47. 2 0
      applications/infrared/scene/infrared_app_scene_start.cpp
  48. 27 37
      applications/lfrfid/lfrfid_app.cpp
  49. 5 2
      applications/lfrfid/lfrfid_app.h
  50. 11 1
      applications/lfrfid/scene/lfrfid_app_scene_save_name.cpp
  51. 11 9
      applications/music_player/music_player.c
  52. 78 38
      applications/nfc/nfc_device.c
  53. 3 4
      applications/nfc/nfc_device.h
  54. 1 1
      applications/nfc/scenes/nfc_scene_delete.c
  55. 16 3
      applications/nfc/scenes/nfc_scene_save_name.c
  56. 0 3
      applications/nfc/scenes/nfc_scene_save_success.c
  57. 1 1
      applications/nfc/scenes/nfc_scene_saved_menu.c
  58. 2 0
      applications/nfc/scenes/nfc_scene_set_type.c
  59. 1 1
      applications/subghz/scenes/subghz_scene_delete.c
  60. 2 2
      applications/subghz/scenes/subghz_scene_delete_raw.c
  61. 1 1
      applications/subghz/scenes/subghz_scene_more_raw.c
  62. 4 4
      applications/subghz/scenes/subghz_scene_read_raw.c
  63. 28 24
      applications/subghz/scenes/subghz_scene_save_name.c
  64. 3 3
      applications/subghz/scenes/subghz_scene_set_type.c
  65. 1 1
      applications/subghz/scenes/subghz_scene_transmitter.c
  66. 11 5
      applications/subghz/subghz.c
  67. 1 1
      applications/subghz/subghz_cli.c
  68. 2 2
      applications/subghz/subghz_history.c
  69. 29 23
      applications/subghz/subghz_i.c
  70. 5 5
      applications/subghz/subghz_i.h
  71. 3 3
      applications/subghz/views/receiver.c
  72. 8 8
      applications/subghz/views/subghz_read_raw.c
  73. 3 3
      applications/subghz/views/transmitter.c
  74. 12 0
      assets/compiled/assets_icons.c
  75. 3 0
      assets/compiled/assets_icons.h
  76. BIN
      assets/icons/Archive/back_10px.png
  77. BIN
      assets/icons/Archive/loading_10px.png
  78. BIN
      assets/icons/Archive/music_10px.png
  79. 0 9
      lib/one_wire/ibutton/ibutton_key.c
  80. 0 14
      lib/one_wire/ibutton/ibutton_key.h
  81. 14 0
      lib/toolbox/path.c
  82. 9 0
      lib/toolbox/path.h

+ 9 - 0
applications/applications.c

@@ -44,6 +44,7 @@ extern int32_t vibro_test_app(void* p);
 extern int32_t bt_hid_app(void* p);
 extern int32_t battery_test_app(void* p);
 extern int32_t text_box_test_app(void* p);
+extern int32_t file_browser_app(void* p);
 
 // Plugins
 extern int32_t music_player_app(void* p);
@@ -459,6 +460,14 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = {
      .flags = FlipperApplicationFlagDefault},
 #endif
 
+#ifdef APP_FILE_BROWSER_TEST
+    {.app = file_browser_app,
+     .name = "File Browser test",
+     .stack_size = 2048,
+     .icon = &A_BadUsb_14,
+     .flags = FlipperApplicationFlagDefault},
+#endif
+
 #ifdef APP_BATTERY_TEST
     {.app = battery_test_app,
      .name = "Battery Test",

+ 6 - 0
applications/applications.mk

@@ -62,6 +62,7 @@ APP_USB_MOUSE = 1
 APP_BAD_USB = 1
 APP_U2F = 1
 APP_UART_ECHO = 1
+APP_FILE_BROWSER_TEST = 1
 endif
 
 
@@ -207,6 +208,11 @@ CFLAGS		+= -DAPP_KEYPAD_TEST
 SRV_GUI		= 1
 endif
 
+APP_FILE_BROWSER_TEST ?= 0
+ifeq ($(APP_FILE_BROWSER_TEST), 1)
+CFLAGS		+= -DAPP_FILE_BROWSER_TEST
+SRV_GUI = 1
+endif
 
 APP_ACCESSOR ?= 0
 ifeq ($(APP_ACCESSOR), 1)

+ 9 - 28
applications/bad_usb/bad_usb_app.c

@@ -1,4 +1,5 @@
 #include "bad_usb_app_i.h"
+#include "m-string.h"
 #include <furi.h>
 #include <furi_hal.h>
 #include <storage/storage.h>
@@ -22,33 +23,13 @@ static void bad_usb_app_tick_event_callback(void* context) {
     scene_manager_handle_tick_event(app->scene_manager);
 }
 
-static bool bad_usb_check_assets() {
-    Storage* fs_api = furi_record_open("storage");
-
-    File* dir = storage_file_alloc(fs_api);
-    bool ret = false;
-
-    if(storage_dir_open(dir, BAD_USB_APP_PATH_FOLDER)) {
-        ret = true;
-    }
-
-    storage_dir_close(dir);
-    storage_file_free(dir);
-
-    furi_record_close("storage");
-
-    return ret;
-}
-
 BadUsbApp* bad_usb_app_alloc(char* arg) {
     BadUsbApp* app = malloc(sizeof(BadUsbApp));
 
+    string_init(app->file_path);
+
     if(arg != NULL) {
-        string_t filename;
-        string_init(filename);
-        path_extract_filename_no_ext(arg, filename);
-        strncpy(app->file_name, string_get_cstr(filename), BAD_USB_FILE_NAME_LEN);
-        string_clear(filename);
+        string_set_str(app->file_path, arg);
     }
 
     app->gui = furi_record_open("gui");
@@ -83,13 +64,11 @@ BadUsbApp* bad_usb_app_alloc(char* arg) {
         app->error = BadUsbAppErrorCloseRpc;
         scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
     } else {
-        if(*app->file_name != '\0') {
+        if(!string_empty_p(app->file_path)) {
             scene_manager_next_scene(app->scene_manager, BadUsbSceneWork);
-        } else if(bad_usb_check_assets()) {
-            scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect);
         } else {
-            app->error = BadUsbAppErrorNoFiles;
-            scene_manager_next_scene(app->scene_manager, BadUsbSceneError);
+            string_set_str(app->file_path, BAD_USB_APP_PATH_FOLDER);
+            scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect);
         }
     }
 
@@ -117,6 +96,8 @@ void bad_usb_app_free(BadUsbApp* app) {
     furi_record_close("notification");
     furi_record_close("dialogs");
 
+    string_clear(app->file_path);
+
     free(app);
 }
 

+ 1 - 2
applications/bad_usb/bad_usb_app_i.h

@@ -16,7 +16,6 @@
 
 #define BAD_USB_APP_PATH_FOLDER "/any/badusb"
 #define BAD_USB_APP_EXTENSION ".txt"
-#define BAD_USB_FILE_NAME_LEN 40
 
 typedef enum {
     BadUsbAppErrorNoFiles,
@@ -32,7 +31,7 @@ struct BadUsbApp {
     Widget* widget;
 
     BadUsbAppError error;
-    char file_name[BAD_USB_FILE_NAME_LEN + 1];
+    string_t file_path;
     BadUsb* bad_usb_view;
     BadUsbScript* bad_usb_script;
 };

+ 8 - 6
applications/bad_usb/scenes/bad_usb_scene_file_select.c

@@ -5,14 +5,16 @@
 static bool bad_usb_file_select(BadUsbApp* bad_usb) {
     furi_assert(bad_usb);
 
-    // Input events and views are managed by file_select
-    bool res = dialog_file_select_show(
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(
         bad_usb->dialogs,
-        BAD_USB_APP_PATH_FOLDER,
+        bad_usb->file_path,
+        bad_usb->file_path,
         BAD_USB_APP_EXTENSION,
-        bad_usb->file_name,
-        sizeof(bad_usb->file_name),
-        NULL);
+        true,
+        &I_badusb_10px,
+        true);
+
     return res;
 }
 

+ 5 - 4
applications/bad_usb/scenes/bad_usb_scene_work.c

@@ -2,6 +2,8 @@
 #include "../bad_usb_app_i.h"
 #include "../views/bad_usb_view.h"
 #include "furi_hal.h"
+#include "m-string.h"
+#include "toolbox/path.h"
 
 void bad_usb_scene_work_ok_callback(InputType type, void* context) {
     furi_assert(context);
@@ -28,10 +30,9 @@ void bad_usb_scene_work_on_enter(void* context) {
     string_t file_name;
     string_init(file_name);
 
-    bad_usb_set_file_name(app->bad_usb_view, app->file_name);
-    string_printf(
-        file_name, "%s/%s%s", BAD_USB_APP_PATH_FOLDER, app->file_name, BAD_USB_APP_EXTENSION);
-    app->bad_usb_script = bad_usb_script_open(file_name);
+    path_extract_filename(app->file_path, file_name, true);
+    bad_usb_set_file_name(app->bad_usb_view, string_get_cstr(file_name));
+    app->bad_usb_script = bad_usb_script_open(app->file_path);
 
     string_clear(file_name);
 

+ 5 - 3
applications/bad_usb/views/bad_usb_view.c

@@ -2,6 +2,8 @@
 #include "../bad_usb_script.h"
 #include <gui/elements.h>
 
+#define MAX_NAME_LEN 64
+
 struct BadUsb {
     View* view;
     BadUsbOkCallback callback;
@@ -9,7 +11,7 @@ struct BadUsb {
 };
 
 typedef struct {
-    char* file_name;
+    char file_name[MAX_NAME_LEN];
     BadUsbState state;
     uint8_t anim_frame;
 } BadUsbModel;
@@ -149,11 +151,11 @@ void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* c
         });
 }
 
-void bad_usb_set_file_name(BadUsb* bad_usb, char* name) {
+void bad_usb_set_file_name(BadUsb* bad_usb, const char* name) {
     furi_assert(name);
     with_view_model(
         bad_usb->view, (BadUsbModel * model) {
-            model->file_name = name;
+            strncpy(model->file_name, name, MAX_NAME_LEN);
             return true;
         });
 }

+ 1 - 1
applications/bad_usb/views/bad_usb_view.h

@@ -14,6 +14,6 @@ View* bad_usb_get_view(BadUsb* bad_usb);
 
 void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* context);
 
-void bad_usb_set_file_name(BadUsb* bad_usb, char* name);
+void bad_usb_set_file_name(BadUsb* bad_usb, const char* name);
 
 void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st);

+ 99 - 0
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 <furi.h>
+#include <furi_hal.h>
+#include <storage/storage.h>
+#include <lib/toolbox/path.h>
+
+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;
+}

+ 32 - 0
applications/debug_tools/file_browser_test/file_browser_app_i.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include "scenes/file_browser_scene.h"
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/file_browser.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/widget.h>
+
+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;

+ 30 - 0
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,
+};

+ 29 - 0
applications/debug_tools/file_browser_test/scenes/file_browser_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// 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

+ 43 - 0
applications/debug_tools/file_browser_test/scenes/file_browser_scene_browser.c

@@ -0,0 +1,43 @@
+#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) {
+    FileBrowserApp* app = context;
+    furi_assert(app);
+    view_dispatcher_send_custom_event(app->view_dispatcher, SceneManagerEventTypeCustom);
+}
+
+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);
+}

+ 3 - 0
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)

+ 36 - 0
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);
+}

+ 44 - 0
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);
+}

+ 4 - 3
applications/dialogs/dialogs.c

@@ -1,6 +1,7 @@
+#include "dialogs/dialogs_message.h"
 #include "dialogs_i.h"
 #include "dialogs_api_lock.h"
-#include "dialogs_module_file_select.h"
+#include "dialogs_module_file_browser.h"
 #include "dialogs_module_message.h"
 
 static DialogsApp* dialogs_app_alloc() {
@@ -13,9 +14,9 @@ static DialogsApp* dialogs_app_alloc() {
 static void dialogs_app_process_message(DialogsApp* app, DialogsAppMessage* message) {
     UNUSED(app);
     switch(message->command) {
-    case DialogsAppCommandFileOpen:
+    case DialogsAppCommandFileBrowser:
         message->return_data->bool_value =
-            dialogs_app_process_module_file_select(&message->data->file_select);
+            dialogs_app_process_module_file_browser(&message->data->file_browser);
         break;
     case DialogsAppCommandDialog:
         message->return_data->dialog_value =

+ 14 - 11
applications/dialogs/dialogs.h

@@ -1,6 +1,7 @@
 #pragma once
 #include <furi.h>
 #include <gui/canvas.h>
+#include "m-string.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -10,25 +11,27 @@ extern "C" {
 
 typedef struct DialogsApp DialogsApp;
 
-/****************** FILE SELECT ******************/
+/****************** FILE BROWSER ******************/
 
 /**
- * Shows and processes the file selection dialog
+ * Shows and processes the file browser dialog
  * @param context api pointer
- * @param path path to directory
+ * @param result_path selected file path string pointer
+ * @param path preselected file path string pointer
  * @param extension file extension to be offered for selection
- * @param selected_filename buffer where the selected filename will be saved
- * @param selected_filename_size and the size of this buffer
- * @param preselected_filename filename to be preselected
+ * @param skip_assets true - do not show assets folders
+ * @param icon file icon pointer, NULL for default icon
+ * @param hide_ext true - hide extensions for files
  * @return bool whether a file was selected
  */
-bool dialog_file_select_show(
+bool dialog_file_browser_show(
     DialogsApp* context,
-    const char* path,
+    string_ptr result_path,
+    string_ptr path,
     const char* extension,
-    char* result,
-    uint8_t result_size,
-    const char* preselected_filename);
+    bool skip_assets,
+    const Icon* icon,
+    bool hide_ext);
 
 /****************** MESSAGE ******************/
 

+ 17 - 12
applications/dialogs/dialogs_api.c

@@ -1,31 +1,36 @@
+#include "dialogs/dialogs_message.h"
 #include "dialogs_i.h"
 #include "dialogs_api_lock.h"
+#include "m-string.h"
 
-/****************** File select ******************/
+/****************** File browser ******************/
 
-bool dialog_file_select_show(
+bool dialog_file_browser_show(
     DialogsApp* context,
-    const char* path,
+    string_ptr result_path,
+    string_ptr path,
     const char* extension,
-    char* result,
-    uint8_t result_size,
-    const char* preselected_filename) {
+    bool skip_assets,
+    const Icon* icon,
+    bool hide_ext) {
     FuriApiLock lock = API_LOCK_INIT_LOCKED();
     furi_check(lock != NULL);
 
     DialogsAppData data = {
-        .file_select = {
-            .path = path,
+        .file_browser = {
             .extension = extension,
-            .result = result,
-            .result_size = result_size,
-            .preselected_filename = preselected_filename,
+            .result_path = result_path,
+            .file_icon = icon,
+            .hide_ext = hide_ext,
+            .skip_assets = skip_assets,
+            .preselected_filename = path,
+
         }};
 
     DialogsAppReturn return_data;
     DialogsAppMessage message = {
         .lock = lock,
-        .command = DialogsAppCommandFileOpen,
+        .command = DialogsAppCommandFileBrowser,
         .data = &data,
         .return_data = &return_data,
     };

+ 9 - 7
applications/dialogs/dialogs_message.h

@@ -2,25 +2,27 @@
 #include <furi.h>
 #include "dialogs_i.h"
 #include "dialogs_api_lock.h"
+#include "m-string.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 typedef struct {
-    const char* path;
     const char* extension;
-    char* result;
-    uint8_t result_size;
-    const char* preselected_filename;
-} DialogsAppMessageDataFileSelect;
+    bool skip_assets;
+    bool hide_ext;
+    const Icon* file_icon;
+    string_ptr result_path;
+    string_ptr preselected_filename;
+} DialogsAppMessageDataFileBrowser;
 
 typedef struct {
     const DialogMessage* message;
 } DialogsAppMessageDataDialog;
 
 typedef union {
-    DialogsAppMessageDataFileSelect file_select;
+    DialogsAppMessageDataFileBrowser file_browser;
     DialogsAppMessageDataDialog dialog;
 } DialogsAppData;
 
@@ -30,7 +32,7 @@ typedef union {
 } DialogsAppReturn;
 
 typedef enum {
-    DialogsAppCommandFileOpen,
+    DialogsAppCommandFileBrowser,
     DialogsAppCommandDialog,
 } DialogsAppCommand;
 

+ 59 - 0
applications/dialogs/dialogs_module_file_browser.c

@@ -0,0 +1,59 @@
+#include "dialogs_i.h"
+#include "dialogs_api_lock.h"
+#include "gui/modules/file_browser.h"
+
+typedef struct {
+    FuriApiLock lock;
+    bool result;
+} DialogsAppFileBrowserContext;
+
+static void dialogs_app_file_browser_back_callback(void* context) {
+    furi_assert(context);
+    DialogsAppFileBrowserContext* file_browser_context = context;
+    file_browser_context->result = false;
+    API_LOCK_UNLOCK(file_browser_context->lock);
+}
+
+static void dialogs_app_file_browser_callback(void* context) {
+    furi_assert(context);
+    DialogsAppFileBrowserContext* file_browser_context = context;
+    file_browser_context->result = true;
+    API_LOCK_UNLOCK(file_browser_context->lock);
+}
+
+bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrowser* data) {
+    bool ret = false;
+    Gui* gui = furi_record_open("gui");
+
+    DialogsAppFileBrowserContext* file_browser_context =
+        malloc(sizeof(DialogsAppFileBrowserContext));
+    file_browser_context->lock = API_LOCK_INIT_LOCKED();
+
+    ViewHolder* view_holder = view_holder_alloc();
+    view_holder_attach_to_gui(view_holder, gui);
+    view_holder_set_back_callback(
+        view_holder, dialogs_app_file_browser_back_callback, file_browser_context);
+
+    FileBrowser* file_browser = file_browser_alloc(data->result_path);
+    file_browser_set_callback(
+        file_browser, dialogs_app_file_browser_callback, file_browser_context);
+    file_browser_configure(
+        file_browser, data->extension, data->skip_assets, data->file_icon, data->hide_ext);
+    file_browser_start(file_browser, data->preselected_filename);
+
+    view_holder_set_view(view_holder, file_browser_get_view(file_browser));
+    view_holder_start(view_holder);
+    API_LOCK_WAIT_UNTIL_UNLOCK(file_browser_context->lock);
+
+    ret = file_browser_context->result;
+
+    view_holder_stop(view_holder);
+    view_holder_free(view_holder);
+    file_browser_stop(file_browser);
+    file_browser_free(file_browser);
+    API_LOCK_FREE(file_browser_context->lock);
+    free(file_browser_context);
+    furi_record_close("gui");
+
+    return ret;
+}

+ 1 - 1
applications/dialogs/dialogs_module_file_select.h → applications/dialogs/dialogs_module_file_browser.h

@@ -5,7 +5,7 @@
 extern "C" {
 #endif
 
-bool dialogs_app_process_module_file_select(const DialogsAppMessageDataFileSelect* data);
+bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrowser* data);
 
 #ifdef __cplusplus
 }

+ 0 - 59
applications/dialogs/dialogs_module_file_select.c

@@ -1,59 +0,0 @@
-#include "dialogs_i.h"
-#include "dialogs_api_lock.h"
-#include <gui/modules/file_select.h>
-
-typedef struct {
-    FuriApiLock lock;
-    bool result;
-} DialogsAppFileSelectContext;
-
-static void dialogs_app_file_select_back_callback(void* context) {
-    furi_assert(context);
-    DialogsAppFileSelectContext* file_select_context = context;
-    file_select_context->result = false;
-    API_LOCK_UNLOCK(file_select_context->lock);
-}
-
-static void dialogs_app_file_select_callback(bool result, void* context) {
-    furi_assert(context);
-    DialogsAppFileSelectContext* file_select_context = context;
-    file_select_context->result = result;
-    API_LOCK_UNLOCK(file_select_context->lock);
-}
-
-bool dialogs_app_process_module_file_select(const DialogsAppMessageDataFileSelect* data) {
-    bool ret = false;
-    Gui* gui = furi_record_open("gui");
-
-    DialogsAppFileSelectContext* file_select_context = malloc(sizeof(DialogsAppFileSelectContext));
-    file_select_context->lock = API_LOCK_INIT_LOCKED();
-
-    ViewHolder* view_holder = view_holder_alloc();
-    view_holder_attach_to_gui(view_holder, gui);
-    view_holder_set_back_callback(
-        view_holder, dialogs_app_file_select_back_callback, file_select_context);
-
-    FileSelect* file_select = file_select_alloc();
-    file_select_set_callback(file_select, dialogs_app_file_select_callback, file_select_context);
-    file_select_set_filter(file_select, data->path, data->extension);
-    file_select_set_result_buffer(file_select, data->result, data->result_size);
-    file_select_init(file_select);
-    if(data->preselected_filename != NULL) {
-        file_select_set_selected_file(file_select, data->preselected_filename);
-    }
-
-    view_holder_set_view(view_holder, file_select_get_view(file_select));
-    view_holder_start(view_holder);
-    API_LOCK_WAIT_UNTIL_UNLOCK(file_select_context->lock);
-
-    ret = file_select_context->result;
-
-    view_holder_stop(view_holder);
-    view_holder_free(view_holder);
-    file_select_free(file_select);
-    API_LOCK_FREE(file_select_context->lock);
-    free(file_select_context);
-    furi_record_close("gui");
-
-    return ret;
-}

+ 534 - 0
applications/gui/modules/file_browser.c

@@ -0,0 +1,534 @@
+#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 <m-array.h>
+#include <gui/elements.h>
+#include <furi.h>
+#include "toolbox/path.h"
+
+#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;
+    const char* ext_filter;
+    bool skip_assets;
+
+    FileBrowserCallback callback;
+    void* context;
+
+    string_ptr 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_ptr 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,
+    const 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) {
+            items_array_reset(model->items);
+            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;
+            path_extract_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);
+                    }
+                }
+            }
+            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;
+}

+ 39 - 0
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 <gui/view.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct FileBrowser FileBrowser;
+typedef void (*FileBrowserCallback)(void* context);
+
+FileBrowser* file_browser_alloc(string_ptr result_path);
+
+void file_browser_free(FileBrowser* browser);
+
+View* file_browser_get_view(FileBrowser* browser);
+
+void file_browser_configure(
+    FileBrowser* browser,
+    const 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

+ 408 - 0
applications/gui/modules/file_browser_worker.c

@@ -0,0 +1,408 @@
+#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 <m-array.h>
+#include <stdbool.h>
+#include <storage/storage.h>
+#include <furi.h>
+#include <stddef.h>
+#include "toolbox/path.h"
+
+#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) {
+                    // There are too many files in folder and counting them will take some time - send callback to app
+                    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)) {
+        path_extract_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;
+}
+
+BrowserWorker* file_browser_worker_alloc(string_t path, const 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);
+}

+ 55 - 0
applications/gui/modules/file_browser_worker.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include "m-string.h"
+#include <gui/view.h>
+#include <stdint.h>
+
+#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);
+
+BrowserWorker* file_browser_worker_alloc(string_t path, const 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

+ 0 - 475
applications/gui/modules/file_select.c

@@ -1,475 +0,0 @@
-#include "file_select.h"
-#include <gui/elements.h>
-#include <m-string.h>
-#include <storage/storage.h>
-
-#define FILENAME_COUNT 4
-
-struct FileSelect {
-    // public
-    View* view;
-    Storage* fs_api;
-    const char* path;
-    const char* extension;
-
-    bool init_completed;
-
-    FileSelectCallback callback;
-    void* context;
-
-    char* buffer;
-    uint8_t buffer_size;
-};
-
-typedef struct {
-    string_t filename[FILENAME_COUNT];
-    uint8_t position;
-
-    uint16_t first_file_index;
-    uint16_t file_count;
-
-} FileSelectModel;
-
-bool file_select_fill_strings(FileSelect* file_select);
-bool file_select_fill_count(FileSelect* file_select);
-static bool file_select_init_inner(FileSelect* file_select);
-
-static void file_select_draw_callback(Canvas* canvas, void* _model) {
-    FileSelectModel* model = _model;
-
-    string_t string_buff;
-    const uint8_t item_height = 16;
-    const uint8_t item_width = 123;
-    const uint8_t text_max_width = 115;
-
-    canvas_clear(canvas);
-    canvas_set_font(canvas, FontSecondary);
-
-    if(model->file_count) {
-        for(uint8_t i = 0; i < MIN(FILENAME_COUNT, model->file_count); i++) {
-            if(i == model->position) {
-                canvas_set_color(canvas, ColorBlack);
-                canvas_draw_box(canvas, 0, (i * item_height) + 1, item_width, item_height - 2);
-
-                canvas_set_color(canvas, ColorWhite);
-                canvas_draw_dot(canvas, 0, (i * item_height) + 1);
-                canvas_draw_dot(canvas, 0, (i * item_height) + item_height - 2);
-                canvas_draw_dot(canvas, item_width - 1, (i * item_height) + 1);
-                canvas_draw_dot(canvas, item_width - 1, (i * item_height) + item_height - 2);
-            } else {
-                canvas_set_color(canvas, ColorBlack);
-            }
-
-            string_init_set(string_buff, model->filename[i]);
-            elements_string_fit_width(canvas, string_buff, text_max_width);
-            canvas_draw_str(
-                canvas, 6, (i * item_height) + item_height - 4, string_get_cstr(string_buff));
-
-            string_clear(string_buff);
-        }
-    } else {
-        canvas_draw_str(canvas, 6, item_height, "Empty folder");
-    }
-    elements_scrollbar(canvas, model->first_file_index + model->position, model->file_count);
-}
-
-static bool file_select_input_callback(InputEvent* event, void* context) {
-    FileSelect* file_select = (FileSelect*)context;
-    bool consumed = false;
-
-    if((event->type == InputTypeShort) | (event->type == InputTypeRepeat)) {
-        if(!file_select->init_completed) {
-            if(!file_select_init_inner(file_select)) {
-                file_select->callback(false, file_select->context);
-            }
-        } else if(event->key == InputKeyUp) {
-            with_view_model(
-                file_select->view, (FileSelectModel * model) {
-                    if(model->position == 0) {
-                        if(model->first_file_index == 0) {
-                            // wrap
-                            int16_t max_first_file_index = model->file_count - FILENAME_COUNT;
-                            model->position = MIN(FILENAME_COUNT - 1, model->file_count - 1);
-                            model->first_file_index =
-                                max_first_file_index < 0 ? 0 : max_first_file_index;
-                        } else {
-                            model->first_file_index--;
-                        }
-                    } else if(model->position == 1) {
-                        if(model->first_file_index == 0) {
-                            model->position--;
-                        } else {
-                            model->first_file_index--;
-                        }
-                    } else {
-                        model->position--;
-                    }
-                    return true;
-                });
-            consumed = true;
-            if(!file_select_fill_strings(file_select)) {
-                file_select->callback(false, file_select->context);
-            }
-        } else if(event->key == InputKeyDown) {
-            with_view_model(
-                file_select->view, (FileSelectModel * model) {
-                    uint16_t max_first_file_index = model->file_count > FILENAME_COUNT ?
-                                                        model->file_count - FILENAME_COUNT :
-                                                        0;
-
-                    if(model->position >= MIN(FILENAME_COUNT - 1, model->file_count - 1)) {
-                        if(model->first_file_index >= max_first_file_index) {
-                            // wrap
-                            model->position = 0;
-                            model->first_file_index = 0;
-                        } else {
-                            model->first_file_index++;
-                        }
-                    } else if(model->position >= (FILENAME_COUNT - 2)) {
-                        if(model->first_file_index >= max_first_file_index) {
-                            model->position++;
-                        } else {
-                            model->first_file_index++;
-                        }
-                    } else {
-                        model->position++;
-                    }
-                    return true;
-                });
-            consumed = true;
-            if(!file_select_fill_strings(file_select)) {
-                file_select->callback(false, file_select->context);
-            }
-        } else if(event->key == InputKeyOk) {
-            if(file_select->callback != NULL) {
-                size_t files = 0;
-                if(file_select->buffer) {
-                    with_view_model(
-                        file_select->view, (FileSelectModel * model) {
-                            files = model->file_count;
-                            strlcpy(
-                                file_select->buffer,
-                                string_get_cstr(model->filename[model->position]),
-                                file_select->buffer_size);
-
-                            return false;
-                        });
-                };
-                if(files > 0) {
-                    file_select->callback(true, file_select->context);
-                }
-            }
-            consumed = true;
-        }
-    }
-
-    return consumed;
-}
-
-static bool file_select_init_inner(FileSelect* file_select) {
-    bool result = false;
-    if(file_select->path && file_select->extension && file_select->fs_api) {
-        if(file_select_fill_count(file_select)) {
-            if(file_select_fill_strings(file_select)) {
-                file_select->init_completed = true;
-                result = true;
-            }
-        }
-    }
-
-    return result;
-}
-
-FileSelect* file_select_alloc() {
-    FileSelect* file_select = malloc(sizeof(FileSelect));
-    file_select->view = view_alloc();
-    file_select->fs_api = furi_record_open("storage");
-
-    view_set_context(file_select->view, file_select);
-    view_allocate_model(file_select->view, ViewModelTypeLockFree, sizeof(FileSelectModel));
-    view_set_draw_callback(file_select->view, file_select_draw_callback);
-    view_set_input_callback(file_select->view, file_select_input_callback);
-
-    with_view_model(
-        file_select->view, (FileSelectModel * model) {
-            for(uint8_t i = 0; i < FILENAME_COUNT; i++) {
-                string_init(model->filename[i]);
-            }
-
-            model->first_file_index = 0;
-            model->file_count = 0;
-            return false;
-        });
-
-    return file_select;
-}
-
-void file_select_free(FileSelect* file_select) {
-    furi_assert(file_select);
-    with_view_model(
-        file_select->view, (FileSelectModel * model) {
-            for(uint8_t i = 0; i < FILENAME_COUNT; i++) {
-                string_clear(model->filename[i]);
-            }
-            return false;
-        });
-    view_free(file_select->view);
-    free(file_select);
-    furi_record_close("storage");
-}
-
-View* file_select_get_view(FileSelect* file_select) {
-    furi_assert(file_select);
-    return file_select->view;
-}
-
-void file_select_set_callback(FileSelect* file_select, FileSelectCallback callback, void* context) {
-    file_select->context = context;
-    file_select->callback = callback;
-}
-
-void file_select_set_filter(FileSelect* file_select, const char* path, const char* extension) {
-    furi_assert(file_select);
-    file_select->path = path;
-    file_select->extension = extension;
-}
-
-void file_select_set_result_buffer(FileSelect* file_select, char* buffer, uint8_t buffer_size) {
-    file_select->buffer = buffer;
-    file_select->buffer_size = buffer_size;
-
-    if(file_select->buffer) {
-        strlcpy(file_select->buffer, "", file_select->buffer_size);
-    }
-}
-
-bool file_select_init(FileSelect* file_select) {
-    if(!file_select_init_inner(file_select)) {
-        file_select->callback(false, file_select->context);
-        return false;
-    } else {
-        return true;
-    }
-}
-
-static bool filter_file(FileSelect* file_select, FileInfo* file_info, char* name) {
-    bool result = false;
-
-    if(!(file_info->flags & FSF_DIRECTORY)) {
-        if(strcmp(file_select->extension, "*") == 0) {
-            result = true;
-        } else if(strstr(name, file_select->extension) != NULL) {
-            result = true;
-        }
-    }
-
-    return result;
-}
-
-bool file_select_fill_strings(FileSelect* file_select) {
-    furi_assert(file_select);
-    furi_assert(file_select->fs_api);
-    furi_assert(file_select->path);
-    furi_assert(file_select->extension);
-
-    FileInfo file_info;
-    File* directory = storage_file_alloc(file_select->fs_api);
-
-    uint8_t string_counter = 0;
-    uint16_t file_counter = 0;
-    const uint8_t name_length = 100;
-    char* name = malloc(name_length);
-    uint16_t first_file_index = 0;
-
-    with_view_model(
-        file_select->view, (FileSelectModel * model) {
-            first_file_index = model->first_file_index;
-            return false;
-        });
-
-    if(!storage_dir_open(directory, file_select->path)) {
-        storage_dir_close(directory);
-        storage_file_free(directory);
-        free(name);
-        return true;
-    }
-
-    while(1) {
-        if(!storage_dir_read(directory, &file_info, name, name_length)) {
-            break;
-        }
-
-        if(storage_file_get_error(directory) == FSE_OK) {
-            if(filter_file(file_select, &file_info, name)) {
-                if(file_counter >= first_file_index) {
-                    with_view_model(
-                        file_select->view, (FileSelectModel * model) {
-                            string_set_str(model->filename[string_counter], name);
-
-                            if(strcmp(file_select->extension, "*") != 0) {
-                                string_replace_all_str(
-                                    model->filename[string_counter], file_select->extension, "");
-                            }
-
-                            return true;
-                        });
-                    string_counter++;
-
-                    if(string_counter >= FILENAME_COUNT) {
-                        break;
-                    }
-                }
-                file_counter++;
-            }
-        } else {
-            storage_dir_close(directory);
-            storage_file_free(directory);
-            free(name);
-            return false;
-        }
-    }
-
-    storage_dir_close(directory);
-    storage_file_free(directory);
-    free(name);
-    return true;
-}
-
-bool file_select_fill_count(FileSelect* file_select) {
-    furi_assert(file_select);
-    furi_assert(file_select->fs_api);
-    furi_assert(file_select->path);
-    furi_assert(file_select->extension);
-
-    FileInfo file_info;
-    File* directory = storage_file_alloc(file_select->fs_api);
-
-    uint16_t file_counter = 0;
-    const uint8_t name_length = 100;
-    char* name = malloc(name_length);
-
-    if(!storage_dir_open(directory, file_select->path)) {
-        storage_dir_close(directory);
-        storage_file_free(directory);
-        free(name);
-        return true;
-    }
-
-    while(1) {
-        if(!storage_dir_read(directory, &file_info, name, name_length)) {
-            break;
-        }
-
-        if(storage_file_get_error(directory) == FSE_OK) {
-            if(filter_file(file_select, &file_info, name)) {
-                file_counter++;
-            }
-        } else {
-            storage_dir_close(directory);
-            storage_file_free(directory);
-            free(name);
-            return false;
-        }
-    }
-
-    with_view_model(
-        file_select->view, (FileSelectModel * model) {
-            model->file_count = file_counter;
-            return false;
-        });
-
-    storage_dir_close(directory);
-    storage_file_free(directory);
-    free(name);
-    return true;
-}
-
-void file_select_set_selected_file_internal(FileSelect* file_select, const char* filename) {
-    furi_assert(file_select);
-    furi_assert(filename);
-    furi_assert(file_select->fs_api);
-    furi_assert(file_select->path);
-    furi_assert(file_select->extension);
-
-    if(strlen(filename) == 0) return;
-
-    FileInfo file_info;
-    File* directory = storage_file_alloc(file_select->fs_api);
-
-    const uint8_t name_length = 100;
-    char* name = malloc(name_length);
-    uint16_t file_position = 0;
-    bool file_found = false;
-
-    string_t filename_str;
-    string_init_set_str(filename_str, filename);
-    if(strcmp(file_select->extension, "*") != 0) {
-        string_cat_str(filename_str, file_select->extension);
-    }
-
-    if(!storage_dir_open(directory, file_select->path)) {
-        string_clear(filename_str);
-        storage_dir_close(directory);
-        storage_file_free(directory);
-        free(name);
-        return;
-    }
-
-    while(1) {
-        if(!storage_dir_read(directory, &file_info, name, name_length)) {
-            break;
-        }
-
-        if(storage_file_get_error(directory) == FSE_OK) {
-            if(filter_file(file_select, &file_info, name)) {
-                if(strcmp(string_get_cstr(filename_str), name) == 0) {
-                    file_found = true;
-                    break;
-                }
-
-                file_position++;
-            }
-        } else {
-            string_clear(filename_str);
-            storage_dir_close(directory);
-            storage_file_free(directory);
-            free(name);
-            return;
-        }
-    }
-
-    if(file_found) {
-        with_view_model(
-            file_select->view, (FileSelectModel * model) {
-                uint16_t max_first_file_index =
-                    model->file_count > FILENAME_COUNT ? model->file_count - FILENAME_COUNT : 0;
-
-                model->first_file_index = file_position;
-
-                if(model->first_file_index > 0) {
-                    model->first_file_index -= 1;
-                }
-
-                if(model->first_file_index >= max_first_file_index) {
-                    model->first_file_index = max_first_file_index;
-                }
-
-                model->position = file_position - model->first_file_index;
-
-                return true;
-            });
-    }
-
-    string_clear(filename_str);
-    storage_dir_close(directory);
-    storage_file_free(directory);
-    free(name);
-}
-
-void file_select_set_selected_file(FileSelect* file_select, const char* filename) {
-    file_select_set_selected_file_internal(file_select, filename);
-
-    if(!file_select_fill_strings(file_select)) {
-        file_select->callback(false, file_select->context);
-    }
-}

+ 0 - 31
applications/gui/modules/file_select.h

@@ -1,31 +0,0 @@
-/**
- * @file file_select.h
- * GUI: FileSelect view module API
- */
-
-#pragma once
-
-#include <gui/view.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct FileSelect FileSelect;
-
-typedef void (*FileSelectCallback)(bool result, void* context);
-
-FileSelect* file_select_alloc();
-
-void file_select_free(FileSelect* file_select);
-View* file_select_get_view(FileSelect* file_select);
-
-void file_select_set_callback(FileSelect* file_select, FileSelectCallback callback, void* context);
-void file_select_set_filter(FileSelect* file_select, const char* path, const char* extension);
-void file_select_set_result_buffer(FileSelect* file_select, char* buffer, uint8_t buffer_size);
-bool file_select_init(FileSelect* file_select);
-void file_select_set_selected_file(FileSelect* file_select, const char* filename);
-
-#ifdef __cplusplus
-}
-#endif

+ 3 - 2
applications/gui/modules/validators.c

@@ -3,7 +3,7 @@
 #include "applications/storage/storage.h"
 
 struct ValidatorIsFile {
-    const char* app_path_folder;
+    char* app_path_folder;
     const char* app_extension;
     char* current_name;
 };
@@ -40,7 +40,7 @@ ValidatorIsFile* validator_is_file_alloc_init(
     const char* current_name) {
     ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile));
 
-    instance->app_path_folder = app_path_folder;
+    instance->app_path_folder = strdup(app_path_folder);
     instance->app_extension = app_extension;
     instance->current_name = strdup(current_name);
 
@@ -49,6 +49,7 @@ ValidatorIsFile* validator_is_file_alloc_init(
 
 void validator_is_file_free(ValidatorIsFile* instance) {
     furi_assert(instance);
+    free(instance->app_path_folder);
     free(instance->current_name);
     free(instance);
 }

+ 33 - 58
applications/ibutton/ibutton.c

@@ -1,7 +1,8 @@
 #include "ibutton.h"
+#include "assets_icons.h"
 #include "ibutton_i.h"
 #include "ibutton/scenes/ibutton_scene.h"
-
+#include "m-string.h"
 #include <toolbox/path.h>
 #include <flipper_format/flipper_format.h>
 
@@ -85,6 +86,8 @@ void ibutton_tick_event_callback(void* context) {
 iButton* ibutton_alloc() {
     iButton* ibutton = malloc(sizeof(iButton));
 
+    string_init(ibutton->file_path);
+
     ibutton->scene_manager = scene_manager_alloc(&ibutton_scene_handlers, ibutton);
 
     ibutton->view_dispatcher = view_dispatcher_alloc();
@@ -176,49 +179,28 @@ void ibutton_free(iButton* ibutton) {
     ibutton_worker_free(ibutton->key_worker);
     ibutton_key_free(ibutton->key);
 
+    string_clear(ibutton->file_path);
+
     free(ibutton);
 }
 
 bool ibutton_file_select(iButton* ibutton) {
-    bool success = dialog_file_select_show(
+    bool success = dialog_file_browser_show(
         ibutton->dialogs,
-        IBUTTON_APP_FOLDER,
+        ibutton->file_path,
+        ibutton->file_path,
         IBUTTON_APP_EXTENSION,
-        ibutton->file_name,
-        IBUTTON_FILE_NAME_SIZE,
-        ibutton_key_get_name_p(ibutton->key));
+        true,
+        &I_ibutt_10px,
+        true);
 
     if(success) {
-        string_t key_str;
-        string_init_printf(
-            key_str, "%s/%s%s", IBUTTON_APP_FOLDER, ibutton->file_name, IBUTTON_APP_EXTENSION);
-        success = ibutton_load_key_data(ibutton, key_str);
-
-        if(success) {
-            ibutton_key_set_name(ibutton->key, ibutton->file_name);
-        }
-
-        string_clear(key_str);
+        success = ibutton_load_key_data(ibutton, ibutton->file_path);
     }
 
     return success;
 }
 
-bool ibutton_load_key(iButton* ibutton, const char* key_name) {
-    string_t key_path;
-    string_init_set_str(key_path, key_name);
-
-    const bool success = ibutton_load_key_data(ibutton, key_path);
-
-    if(success) {
-        path_extract_filename_no_ext(key_name, key_path);
-        ibutton_key_set_name(ibutton->key, string_get_cstr(key_path));
-    }
-
-    string_clear(key_path);
-    return success;
-}
-
 bool ibutton_save_key(iButton* ibutton, const char* key_name) {
     // Create ibutton directory if necessary
     ibutton_make_app_folder(ibutton);
@@ -226,27 +208,22 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
     FlipperFormat* file = flipper_format_file_alloc(ibutton->storage);
     iButtonKey* key = ibutton->key;
 
-    string_t key_file_name;
     bool result = false;
-    string_init(key_file_name);
 
     do {
         // First remove key if it was saved (we rename the key)
-        if(!ibutton_delete_key(ibutton)) break;
-
-        // Save the key
-        ibutton_key_set_name(key, key_name);
+        ibutton_delete_key(ibutton);
 
         // Set full file name, for new key
-        string_printf(
-            key_file_name,
-            "%s/%s%s",
-            IBUTTON_APP_FOLDER,
-            ibutton_key_get_name_p(key),
-            IBUTTON_APP_EXTENSION);
+        if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+            size_t filename_start = string_search_rchar(ibutton->file_path, '/');
+            string_left(ibutton->file_path, filename_start);
+        }
+
+        string_cat_printf(ibutton->file_path, "/%s%s", key_name, IBUTTON_APP_EXTENSION);
 
         // Open file for write
-        if(!flipper_format_file_open_always(file, string_get_cstr(key_file_name))) break;
+        if(!flipper_format_file_open_always(file, string_get_cstr(ibutton->file_path))) break;
 
         // Write header
         if(!flipper_format_write_header_cstr(file, IBUTTON_APP_FILE_TYPE, 1)) break;
@@ -271,8 +248,6 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
 
     flipper_format_free(file);
 
-    string_clear(key_file_name);
-
     if(!result) {
         dialog_message_show_storage_error(ibutton->dialogs, "Cannot save\nkey file");
     }
@@ -281,17 +256,8 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) {
 }
 
 bool ibutton_delete_key(iButton* ibutton) {
-    string_t file_name;
     bool result = false;
-
-    string_init_printf(
-        file_name,
-        "%s/%s%s",
-        IBUTTON_APP_FOLDER,
-        ibutton_key_get_name_p(ibutton->key),
-        IBUTTON_APP_EXTENSION);
-    result = storage_simply_remove(ibutton->storage, string_get_cstr(file_name));
-    string_clear(file_name);
+    result = storage_simply_remove(ibutton->storage, string_get_cstr(ibutton->file_path));
 
     return result;
 }
@@ -335,8 +301,17 @@ int32_t ibutton_app(void* p) {
 
     ibutton_make_app_folder(ibutton);
 
-    if(p && ibutton_load_key(ibutton, (const char*)p)) {
-        // TODO: Display an error if the key from p could not be loaded
+    bool key_loaded = false;
+
+    if(p) {
+        string_set_str(ibutton->file_path, (const char*)p);
+        if(ibutton_load_key_data(ibutton, ibutton->file_path)) {
+            key_loaded = true;
+            // TODO: Display an error if the key from p could not be loaded
+        }
+    }
+
+    if(key_loaded) {
         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate);
     } else {
         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneStart);

+ 1 - 2
applications/ibutton/ibutton_i.h

@@ -41,7 +41,7 @@ struct iButton {
     iButtonWorker* key_worker;
     iButtonKey* key;
 
-    char file_name[IBUTTON_FILE_NAME_SIZE];
+    string_t file_path;
     char text_store[IBUTTON_TEXT_STORE_SIZE + 1];
 
     Submenu* submenu;
@@ -74,7 +74,6 @@ typedef enum {
 } iButtonNotificationMessage;
 
 bool ibutton_file_select(iButton* ibutton);
-bool ibutton_load_key(iButton* ibutton, const char* key_name);
 bool ibutton_save_key(iButton* ibutton, const char* key_name);
 bool ibutton_delete_key(iButton* ibutton);
 void ibutton_text_store_set(iButton* ibutton, const char* text, ...);

+ 2 - 1
applications/ibutton/scenes/ibutton_scene_add_type.c

@@ -1,4 +1,5 @@
 #include "../ibutton_i.h"
+#include "m-string.h"
 
 enum SubmenuIndex {
     SubmenuIndexCyfral,
@@ -44,7 +45,7 @@ bool ibutton_scene_add_type_on_event(void* context, SceneManagerEvent event) {
             furi_crash("Unknown key type");
         }
 
-        ibutton_key_set_name(key, "");
+        string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
         ibutton_key_clear_data(key);
         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddValue);
     }

+ 8 - 1
applications/ibutton/scenes/ibutton_scene_delete_confirm.c

@@ -1,4 +1,5 @@
 #include "../ibutton_i.h"
+#include <toolbox/path.h>
 
 static void ibutton_scene_delete_confirm_widget_callback(
     GuiButtonType result,
@@ -16,7 +17,11 @@ void ibutton_scene_delete_confirm_on_enter(void* context) {
     iButtonKey* key = ibutton->key;
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", ibutton_key_get_name_p(key));
+    string_t key_name;
+    string_init(key_name);
+    path_extract_filename(ibutton->file_path, key_name, true);
+
+    ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", string_get_cstr(key_name));
     widget_add_text_box_element(
         widget, 0, 0, 128, 27, AlignCenter, AlignCenter, ibutton->text_store, false);
     widget_add_button_element(
@@ -62,6 +67,8 @@ void ibutton_scene_delete_confirm_on_enter(void* context) {
         widget, 64, 33, AlignCenter, AlignBottom, FontSecondary, ibutton->text_store);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
+
+    string_clear(key_name);
 }
 
 bool ibutton_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) {

+ 11 - 3
applications/ibutton/scenes/ibutton_scene_emulate.c

@@ -1,5 +1,6 @@
 #include "../ibutton_i.h"
 #include <dolphin/dolphin.h>
+#include <toolbox/path.h>
 
 static void ibutton_scene_emulate_callback(void* context, bool emulated) {
     iButton* ibutton = context;
@@ -15,14 +16,19 @@ void ibutton_scene_emulate_on_enter(void* context) {
     iButtonKey* key = ibutton->key;
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
-    const char* key_name = ibutton_key_get_name_p(key);
+
+    string_t key_name;
+    string_init(key_name);
+    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+        path_extract_filename(ibutton->file_path, key_name, true);
+    }
 
     uint8_t line_count = 2;
     DOLPHIN_DEED(DolphinDeedIbuttonEmulate);
 
     // check that stored key has name
-    if(strcmp(key_name, "") != 0) {
-        ibutton_text_store_set(ibutton, "emulating\n%s", key_name);
+    if(!string_empty_p(key_name)) {
+        ibutton_text_store_set(ibutton, "emulating\n%s", string_get_cstr(key_name));
         line_count = 2;
     } else {
         // if not, show key data
@@ -77,6 +83,8 @@ void ibutton_scene_emulate_on_enter(void* context) {
     ibutton_worker_emulate_set_callback(
         ibutton->key_worker, ibutton_scene_emulate_callback, ibutton);
     ibutton_worker_emulate_start(ibutton->key_worker, key);
+
+    string_clear(key_name);
 }
 
 bool ibutton_scene_emulate_on_event(void* context, SceneManagerEvent event) {

+ 8 - 1
applications/ibutton/scenes/ibutton_scene_info.c

@@ -1,4 +1,5 @@
 #include "../ibutton_i.h"
+#include <toolbox/path.h>
 
 void ibutton_scene_info_on_enter(void* context) {
     iButton* ibutton = context;
@@ -7,7 +8,11 @@ void ibutton_scene_info_on_enter(void* context) {
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
 
-    ibutton_text_store_set(ibutton, "%s", ibutton_key_get_name_p(key));
+    string_t key_name;
+    string_init(key_name);
+    path_extract_filename(ibutton->file_path, key_name, true);
+
+    ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name));
     widget_add_text_box_element(
         widget, 0, 0, 128, 28, AlignCenter, AlignCenter, ibutton->text_store, false);
 
@@ -46,6 +51,8 @@ void ibutton_scene_info_on_enter(void* context) {
         widget, 64, 35, AlignCenter, AlignBottom, FontPrimary, ibutton->text_store);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget);
+
+    string_clear(key_name);
 }
 
 bool ibutton_scene_info_on_event(void* context, SceneManagerEvent event) {

+ 1 - 1
applications/ibutton/scenes/ibutton_scene_read.c

@@ -18,7 +18,7 @@ void ibutton_scene_read_on_enter(void* context) {
     popup_set_icon(popup, 0, 5, &I_DolphinWait_61x59);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup);
-    ibutton_key_set_name(key, "");
+    string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
 
     ibutton_worker_read_set_callback(worker, ibutton_scene_read_callback, ibutton);
     ibutton_worker_read_start(worker, key);

+ 19 - 5
applications/ibutton/scenes/ibutton_scene_save_name.c

@@ -1,5 +1,7 @@
 #include "../ibutton_i.h"
+#include "m-string.h"
 #include <lib/toolbox/random_name.h>
+#include <toolbox/path.h>
 
 static void ibutton_scene_save_name_text_input_callback(void* context) {
     iButton* ibutton = context;
@@ -10,13 +12,17 @@ void ibutton_scene_save_name_on_enter(void* context) {
     iButton* ibutton = context;
     TextInput* text_input = ibutton->text_input;
 
-    const char* key_name = ibutton_key_get_name_p(ibutton->key);
-    const bool key_name_is_empty = !strcmp(key_name, "");
+    string_t key_name;
+    string_init(key_name);
+    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+        path_extract_filename(ibutton->file_path, key_name, true);
+    }
 
+    const bool key_name_is_empty = string_empty_p(key_name);
     if(key_name_is_empty) {
         set_random_name(ibutton->text_store, IBUTTON_TEXT_STORE_SIZE);
     } else {
-        ibutton_text_store_set(ibutton, "%s", key_name);
+        ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name));
     }
 
     text_input_set_header_text(text_input, "Name the key");
@@ -28,11 +34,19 @@ void ibutton_scene_save_name_on_enter(void* context) {
         IBUTTON_KEY_NAME_SIZE,
         key_name_is_empty);
 
-    ValidatorIsFile* validator_is_file =
-        validator_is_file_alloc_init(IBUTTON_APP_FOLDER, IBUTTON_APP_EXTENSION, key_name);
+    string_t folder_path;
+    string_init(folder_path);
+
+    path_extract_dirname(string_get_cstr(ibutton->file_path), folder_path);
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        string_get_cstr(folder_path), IBUTTON_APP_EXTENSION, string_get_cstr(key_name));
     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewTextInput);
+
+    string_clear(key_name);
+    string_clear(folder_path);
 }
 
 bool ibutton_scene_save_name_on_event(void* context, SceneManagerEvent event) {

+ 1 - 0
applications/ibutton/scenes/ibutton_scene_start.c

@@ -36,6 +36,7 @@ bool ibutton_scene_start_on_event(void* context, SceneManagerEvent event) {
         if(event.event == SubmenuIndexRead) {
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneRead);
         } else if(event.event == SubmenuIndexSaved) {
+            string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER);
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSelectKey);
         } else if(event.event == SubmenuIndexAdd) {
             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddType);

+ 12 - 3
applications/ibutton/scenes/ibutton_scene_write.c

@@ -1,4 +1,6 @@
 #include "../ibutton_i.h"
+#include "m-string.h"
+#include "toolbox/path.h"
 
 typedef enum {
     iButtonSceneWriteStateDefault,
@@ -17,13 +19,18 @@ void ibutton_scene_write_on_enter(void* context) {
     iButtonWorker* worker = ibutton->key_worker;
 
     const uint8_t* key_data = ibutton_key_get_data_p(key);
-    const char* key_name = ibutton_key_get_name_p(key);
+
+    string_t key_name;
+    string_init(key_name);
+    if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) {
+        path_extract_filename(ibutton->file_path, key_name, true);
+    }
 
     uint8_t line_count = 2;
 
     // check that stored key has name
-    if(strcmp(key_name, "") != 0) {
-        ibutton_text_store_set(ibutton, "writing\n%s", key_name);
+    if(!string_empty_p(key_name)) {
+        ibutton_text_store_set(ibutton, "writing\n%s", string_get_cstr(key_name));
         line_count = 2;
     } else {
         // if not, show key data
@@ -79,6 +86,8 @@ void ibutton_scene_write_on_enter(void* context) {
 
     ibutton_worker_write_set_callback(worker, ibutton_scene_write_callback, ibutton);
     ibutton_worker_write_start(worker, key);
+
+    string_clear(key_name);
 }
 
 bool ibutton_scene_write_on_event(void* context, SceneManagerEvent event) {

+ 9 - 8
applications/infrared/infrared_app.cpp

@@ -1,4 +1,5 @@
 #include "infrared_app.h"
+#include "m-string.h"
 #include <infrared_worker.h>
 #include <furi.h>
 #include <gui/gui.h>
@@ -12,20 +13,18 @@ int32_t InfraredApp::run(void* args) {
     bool exit = false;
 
     if(args) {
-        std::string path = static_cast<const char*>(args);
-        std::string remote_name(path, path.find_last_of('/') + 1, path.size());
-        auto last_dot = remote_name.find_last_of('.');
-        if(last_dot != std::string::npos) {
-            remote_name.erase(last_dot);
-            path.erase(path.find_last_of('/'));
-            bool result = remote_manager.load(path, remote_name);
+        string_t path;
+        string_init_set_str(path, (char*)args);
+        if(string_end_with_str_p(path, InfraredApp::infrared_extension)) {
+            bool result = remote_manager.load(path);
             if(result) {
                 current_scene = InfraredApp::Scene::Remote;
             } else {
-                printf("Failed to load remote \'%s\'\r\n", remote_name.c_str());
+                printf("Failed to load remote \'%s\'\r\n", string_get_cstr(path));
                 return -1;
             }
         }
+        string_clear(path);
     }
 
     scenes[current_scene]->on_enter(this);
@@ -51,6 +50,7 @@ int32_t InfraredApp::run(void* args) {
 
 InfraredApp::InfraredApp() {
     furi_check(InfraredAppRemoteManager::max_button_name_length < get_text_store_size());
+    string_init_set_str(file_path, InfraredApp::infrared_directory);
     notification = static_cast<NotificationApp*>(furi_record_open("notification"));
     dialogs = static_cast<DialogsApp*>(furi_record_open("dialogs"));
     infrared_worker = infrared_worker_alloc();
@@ -60,6 +60,7 @@ InfraredApp::~InfraredApp() {
     infrared_worker_free(infrared_worker);
     furi_record_close("notification");
     furi_record_close("dialogs");
+    string_clear(file_path);
     for(auto& [key, scene] : scenes) delete scene;
 }
 

+ 2 - 0
applications/infrared/infrared_app.h

@@ -251,6 +251,8 @@ public:
     /** Main class destructor, deinitializes all critical objects */
     ~InfraredApp();
 
+    string_t file_path;
+
     /** Path to Infrared directory */
     static constexpr const char* infrared_directory = "/any/infrared";
     /** Infrared files extension (remote files and universal databases) */

+ 86 - 44
applications/infrared/infrared_app_remote_manager.cpp

@@ -1,3 +1,5 @@
+#include "m-string.h"
+#include "storage/filesystem_api_defines.h"
 #include <flipper_format/flipper_format.h>
 #include "infrared_app_remote_manager.h"
 #include "infrared/helpers/infrared_parser.h"
@@ -11,44 +13,58 @@
 #include <gui/modules/button_menu.h>
 #include <storage/storage.h>
 #include "infrared_app.h"
+#include <toolbox/path.h>
 
 static const char* default_remote_name = "remote";
 
-std::string InfraredAppRemoteManager::make_full_name(
-    const std::string& path,
-    const std::string& remote_name) const {
-    return std::string("") + path + "/" + remote_name + InfraredApp::infrared_extension;
-}
-
-std::string InfraredAppRemoteManager::find_vacant_remote_name(const std::string& name) {
-    std::string result_name;
+void InfraredAppRemoteManager::find_vacant_remote_name(string_t name, string_t path) {
     Storage* storage = static_cast<Storage*>(furi_record_open("storage"));
 
-    FS_Error error = storage_common_stat(
-        storage, make_full_name(InfraredApp::infrared_directory, name).c_str(), NULL);
+    string_t base_path;
+    string_init_set(base_path, path);
+
+    if(string_end_with_str_p(base_path, InfraredApp::infrared_extension)) {
+        size_t filename_start = string_search_rchar(base_path, '/');
+        string_left(base_path, filename_start);
+    }
+
+    string_printf(
+        base_path,
+        "%s/%s%s",
+        string_get_cstr(path),
+        string_get_cstr(name),
+        InfraredApp::infrared_extension);
+
+    FS_Error error = storage_common_stat(storage, string_get_cstr(base_path), NULL);
 
-    if(error == FSE_NOT_EXIST) {
-        result_name = name;
-    } else if(error != FSE_OK) {
-        result_name = std::string();
-    } else {
+    if(error == FSE_OK) {
         /* if suggested name is occupied, try another one (name2, name3, etc) */
+        size_t dot = string_search_rchar(base_path, '.');
+        string_left(base_path, dot);
+
+        string_t path_temp;
+        string_init(path_temp);
+
         uint32_t i = 1;
-        std::string new_name;
         do {
-            new_name = make_full_name(InfraredApp::infrared_directory, name + std::to_string(++i));
-            error = storage_common_stat(storage, new_name.c_str(), NULL);
+            string_printf(
+                path_temp,
+                "%s%u%s",
+                string_get_cstr(base_path),
+                ++i,
+                InfraredApp::infrared_extension);
+            error = storage_common_stat(storage, string_get_cstr(path_temp), NULL);
         } while(error == FSE_OK);
 
+        string_clear(path_temp);
+
         if(error == FSE_NOT_EXIST) {
-            result_name = name + std::to_string(i);
-        } else {
-            result_name = std::string();
+            string_cat_printf(name, "%u", i);
         }
     }
 
+    string_clear(base_path);
     furi_record_close("storage");
-    return result_name;
 }
 
 bool InfraredAppRemoteManager::add_button(const char* button_name, const InfraredAppSignal& signal) {
@@ -61,12 +77,23 @@ bool InfraredAppRemoteManager::add_remote_with_button(
     const InfraredAppSignal& signal) {
     furi_check(button_name != nullptr);
 
-    auto new_name = find_vacant_remote_name(default_remote_name);
-    if(new_name.empty()) {
-        return false;
-    }
+    string_t new_name;
+    string_init_set_str(new_name, default_remote_name);
+
+    string_t new_path;
+    string_init_set_str(new_path, InfraredApp::infrared_directory);
+
+    find_vacant_remote_name(new_name, new_path);
+
+    string_cat_printf(
+        new_path, "/%s%s", string_get_cstr(new_name), InfraredApp::infrared_extension);
+
+    remote = std::make_unique<InfraredAppRemote>(new_path);
+    remote->name = std::string(string_get_cstr(new_name));
+
+    string_clear(new_path);
+    string_clear(new_name);
 
-    remote = std::make_unique<InfraredAppRemote>(InfraredApp::infrared_directory, new_name);
     return add_button(button_name, signal);
 }
 
@@ -93,8 +120,7 @@ const InfraredAppSignal& InfraredAppRemoteManager::get_button_data(size_t index)
 bool InfraredAppRemoteManager::delete_remote() {
     Storage* storage = static_cast<Storage*>(furi_record_open("storage"));
 
-    FS_Error error =
-        storage_common_remove(storage, make_full_name(remote->path, remote->name).c_str());
+    FS_Error error = storage_common_remove(storage, string_get_cstr(remote->path));
     reset_remote();
 
     furi_record_close("storage");
@@ -128,22 +154,33 @@ std::string InfraredAppRemoteManager::get_remote_name() {
 bool InfraredAppRemoteManager::rename_remote(const char* str) {
     furi_check(str != nullptr);
     furi_check(remote.get() != nullptr);
+    furi_check(!string_empty_p(remote->path));
 
     if(!remote->name.compare(str)) {
         return true;
     }
 
-    auto new_name = find_vacant_remote_name(str);
-    if(new_name.empty()) {
-        return false;
+    string_t new_name;
+    string_init_set_str(new_name, str);
+    find_vacant_remote_name(new_name, remote->path);
+
+    string_t new_path;
+    string_init_set(new_path, remote->path);
+    if(string_end_with_str_p(new_path, InfraredApp::infrared_extension)) {
+        size_t filename_start = string_search_rchar(new_path, '/');
+        string_left(new_path, filename_start);
     }
+    string_cat_printf(
+        new_path, "/%s%s", string_get_cstr(new_name), InfraredApp::infrared_extension);
 
     Storage* storage = static_cast<Storage*>(furi_record_open("storage"));
 
-    std::string old_filename = make_full_name(remote->path, remote->name);
-    std::string new_filename = make_full_name(remote->path, new_name);
-    FS_Error error = storage_common_rename(storage, old_filename.c_str(), new_filename.c_str());
-    remote->name = new_name;
+    FS_Error error =
+        storage_common_rename(storage, string_get_cstr(remote->path), string_get_cstr(new_path));
+    remote->name = std::string(string_get_cstr(new_name));
+
+    string_clear(new_name);
+    string_clear(new_path);
 
     furi_record_close("storage");
     return (error == FSE_OK || error == FSE_EXIST);
@@ -171,10 +208,8 @@ bool InfraredAppRemoteManager::store(void) {
 
     FlipperFormat* ff = flipper_format_file_alloc(storage);
 
-    FURI_LOG_I(
-        "RemoteManager", "store file: \'%s\'", make_full_name(remote->path, remote->name).c_str());
-    result =
-        flipper_format_file_open_always(ff, make_full_name(remote->path, remote->name).c_str());
+    FURI_LOG_I("RemoteManager", "store file: \'%s\'", string_get_cstr(remote->path));
+    result = flipper_format_file_open_always(ff, string_get_cstr(remote->path));
     if(result) {
         result = flipper_format_write_header_cstr(ff, "IR signals file", 1);
     }
@@ -192,13 +227,13 @@ bool InfraredAppRemoteManager::store(void) {
     return result;
 }
 
-bool InfraredAppRemoteManager::load(const std::string& path, const std::string& remote_name) {
+bool InfraredAppRemoteManager::load(string_t path) {
     bool result = false;
     Storage* storage = static_cast<Storage*>(furi_record_open("storage"));
     FlipperFormat* ff = flipper_format_file_alloc(storage);
 
-    FURI_LOG_I("RemoteManager", "load file: \'%s\'", make_full_name(path, remote_name).c_str());
-    result = flipper_format_file_open_existing(ff, make_full_name(path, remote_name).c_str());
+    FURI_LOG_I("RemoteManager", "load file: \'%s\'", string_get_cstr(path));
+    result = flipper_format_file_open_existing(ff, string_get_cstr(path));
     if(result) {
         string_t header;
         string_init(header);
@@ -210,7 +245,14 @@ bool InfraredAppRemoteManager::load(const std::string& path, const std::string&
         string_clear(header);
     }
     if(result) {
-        remote = std::make_unique<InfraredAppRemote>(path, remote_name);
+        string_t new_name;
+        string_init(new_name);
+
+        remote = std::make_unique<InfraredAppRemote>(path);
+        path_extract_filename(path, new_name, true);
+        remote->name = std::string(string_get_cstr(new_name));
+
+        string_clear(new_name);
         InfraredAppSignal signal;
         std::string signal_name;
         while(infrared_parser_read_signal(ff, signal, signal_name)) {

+ 12 - 15
applications/infrared/infrared_app_remote_manager.h

@@ -8,6 +8,7 @@
 
 #include "infrared_app_signal.h"
 
+#include "m-string.h"
 #include <infrared_worker.h>
 #include <infrared.h>
 
@@ -60,17 +61,19 @@ class InfraredAppRemote {
     /** Name of remote */
     std::string name;
     /** Path to remote file */
-    std::string path;
+    string_t path;
 
 public:
     /** Initialize new remote
      * 
      * @param path - remote file path
-     * @param name - new remote name
      */
-    InfraredAppRemote(const std::string& path, const std::string& name)
-        : name(name)
-        , path(path) {
+    InfraredAppRemote(string_t file_path) {
+        string_init_set(path, file_path);
+    }
+
+    ~InfraredAppRemote() {
+        string_clear(path);
     }
 };
 
@@ -78,12 +81,6 @@ public:
 class InfraredAppRemoteManager {
     /** Remote instance. There can be 1 remote loaded at a time. */
     std::unique_ptr<InfraredAppRemote> remote;
-    /** Make full name from remote name
-     *
-     * @param remote_name name of remote
-     * @retval full name of remote on disk
-     */
-    std::string make_full_name(const std::string& path, const std::string& remote_name) const;
 
 public:
     /** Restriction to button name length. Buttons larger are ignored. */
@@ -125,9 +122,9 @@ public:
      * incremented digit(2,3,4,etc) added to name and check repeated.
      *
      * @param name - suggested remote name
-     * @retval garanteed free remote name, prefixed with suggested
+     * @param path - remote file path
      */
-    std::string find_vacant_remote_name(const std::string& name);
+    void find_vacant_remote_name(string_t name, string_t path);
 
     /** Get button list
      *
@@ -185,8 +182,8 @@ public:
 
     /** Load data from disk into current remote
      *
-     * @param name - name of remote to load
+     * @param path - path to remote file
      * @retval true if success, false otherwise
      */
-    bool load(const std::string& path, const std::string& name);
+    bool load(string_t path);
 };

+ 12 - 1
applications/infrared/scene/infrared_app_scene_edit_rename.cpp

@@ -1,4 +1,6 @@
 #include "../infrared_app.h"
+#include "m-string.h"
+#include "toolbox/path.h"
 
 void InfraredAppSceneEditRename::on_enter(InfraredApp* app) {
     InfraredAppViewManager* view_manager = app->get_view_manager();
@@ -21,9 +23,18 @@ void InfraredAppSceneEditRename::on_enter(InfraredApp* app) {
         enter_name_length = InfraredAppRemoteManager::max_remote_name_length;
         text_input_set_header_text(text_input, "Name the remote");
 
+        string_t folder_path;
+        string_init(folder_path);
+
+        if(string_end_with_str_p(app->file_path, InfraredApp::infrared_extension)) {
+            path_extract_dirname(string_get_cstr(app->file_path), folder_path);
+        }
+
         ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
-            app->infrared_directory, app->infrared_extension, remote_name.c_str());
+            string_get_cstr(folder_path), app->infrared_extension, remote_name.c_str());
         text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+        string_clear(folder_path);
     }
 
     text_input_set_result_callback(

+ 8 - 11
applications/infrared/scene/infrared_app_scene_remote_list.cpp

@@ -1,4 +1,5 @@
 #include "../infrared_app.h"
+#include "assets_icons.h"
 #include "infrared/infrared_app_event.h"
 #include <text_store.h>
 
@@ -8,11 +9,6 @@ void InfraredAppSceneRemoteList::on_enter(InfraredApp* app) {
     bool result = false;
     bool file_select_result;
     auto remote_manager = app->get_remote_manager();
-    auto last_selected_remote = remote_manager->get_remote_name();
-    const char* last_selected_remote_name =
-        last_selected_remote.size() ? last_selected_remote.c_str() : nullptr;
-    auto filename_ts =
-        std::make_unique<TextStore>(InfraredAppRemoteManager::max_remote_name_length);
     DialogsApp* dialogs = app->get_dialogs();
 
     InfraredAppViewManager* view_manager = app->get_view_manager();
@@ -20,16 +16,17 @@ void InfraredAppSceneRemoteList::on_enter(InfraredApp* app) {
     button_menu_reset(button_menu);
     view_manager->switch_to(InfraredAppViewManager::ViewId::ButtonMenu);
 
-    file_select_result = dialog_file_select_show(
+    file_select_result = dialog_file_browser_show(
         dialogs,
-        InfraredApp::infrared_directory,
+        app->file_path,
+        app->file_path,
         InfraredApp::infrared_extension,
-        filename_ts->text,
-        filename_ts->text_size,
-        last_selected_remote_name);
+        true,
+        &I_ir_10px,
+        true);
 
     if(file_select_result) {
-        if(remote_manager->load(InfraredApp::infrared_directory, std::string(filename_ts->text))) {
+        if(remote_manager->load(app->file_path)) {
             app->switch_to_next_scene(InfraredApp::Scene::Remote);
             result = true;
         }

+ 2 - 0
applications/infrared/scene/infrared_app_scene_start.cpp

@@ -26,6 +26,8 @@ void InfraredAppSceneStart::on_enter(InfraredApp* app) {
         submenu, "Learn New Remote", SubmenuIndexLearnNewRemote, submenu_callback, app);
     submenu_add_item(submenu, "Saved Remotes", SubmenuIndexSavedRemotes, submenu_callback, app);
     submenu_set_selected_item(submenu, submenu_item_selected);
+
+    string_set_str(app->file_path, InfraredApp::infrared_directory);
     submenu_item_selected = 0;
 
     view_manager->switch_to(InfraredAppViewManager::ViewId::Submenu);

+ 27 - 37
applications/lfrfid/lfrfid_app.cpp

@@ -1,4 +1,7 @@
 #include "lfrfid_app.h"
+#include "assets_icons.h"
+#include "furi/common_defines.h"
+#include "m-string.h"
 #include "scene/lfrfid_app_scene_start.h"
 #include "scene/lfrfid_app_scene_read.h"
 #include "scene/lfrfid_app_scene_read_success.h"
@@ -31,9 +34,11 @@ LfRfidApp::LfRfidApp()
     , storage{"storage"}
     , dialogs{"dialogs"}
     , text_store(40) {
+    string_init_set_str(file_path, app_folder);
 }
 
 LfRfidApp::~LfRfidApp() {
+    string_clear(file_path);
 }
 
 void LfRfidApp::run(void* _args) {
@@ -42,7 +47,8 @@ void LfRfidApp::run(void* _args) {
     make_app_folder();
 
     if(strlen(args)) {
-        load_key_data(args, &worker.key);
+        string_set_str(file_path, args);
+        load_key_data(file_path, &worker.key);
         scene_controller.add_scene(SceneType::Emulate, new LfRfidAppSceneEmulate());
         scene_controller.process(100, SceneType::Emulate);
     } else {
@@ -69,65 +75,49 @@ void LfRfidApp::run(void* _args) {
 }
 
 bool LfRfidApp::save_key(RfidKey* key) {
-    string_t file_name;
     bool result = false;
 
     make_app_folder();
 
-    string_init_printf(file_name, "%s/%s%s", app_folder, key->get_name(), app_extension);
-    result = save_key_data(string_get_cstr(file_name), key);
-    string_clear(file_name);
+    if(string_end_with_str_p(file_path, app_extension)) {
+        size_t filename_start = string_search_rchar(file_path, '/');
+        string_left(file_path, filename_start);
+    }
+
+    string_cat_printf(file_path, "/%s%s", key->get_name(), app_extension);
 
+    result = save_key_data(file_path, key);
     return result;
 }
 
 bool LfRfidApp::load_key_from_file_select(bool need_restore) {
-    TextStore* filename_ts = new TextStore(64);
-    bool result = false;
-
-    if(need_restore) {
-        result = dialog_file_select_show(
-            dialogs,
-            app_folder,
-            app_extension,
-            filename_ts->text,
-            filename_ts->text_size,
-            worker.key.get_name());
-    } else {
-        result = dialog_file_select_show(
-            dialogs, app_folder, app_extension, filename_ts->text, filename_ts->text_size, NULL);
+    if(!need_restore) {
+        string_set_str(file_path, app_folder);
     }
 
+    bool result = dialog_file_browser_show(
+        dialogs, file_path, file_path, app_extension, true, &I_125_10px, true);
+
     if(result) {
-        string_t key_str;
-        string_init_printf(key_str, "%s/%s%s", app_folder, filename_ts->text, app_extension);
-        result = load_key_data(string_get_cstr(key_str), &worker.key);
-        string_clear(key_str);
+        result = load_key_data(file_path, &worker.key);
     }
 
-    delete filename_ts;
     return result;
 }
 
 bool LfRfidApp::delete_key(RfidKey* key) {
-    string_t file_name;
-    bool result = false;
-
-    string_init_printf(file_name, "%s/%s%s", app_folder, key->get_name(), app_extension);
-    result = storage_simply_remove(storage, string_get_cstr(file_name));
-    string_clear(file_name);
-
-    return result;
+    UNUSED(key);
+    return storage_simply_remove(storage, string_get_cstr(file_path));
 }
 
-bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
+bool LfRfidApp::load_key_data(string_t path, RfidKey* key) {
     FlipperFormat* file = flipper_format_file_alloc(storage);
     bool result = false;
     string_t str_result;
     string_init(str_result);
 
     do {
-        if(!flipper_format_file_open_existing(file, path)) break;
+        if(!flipper_format_file_open_existing(file, string_get_cstr(path))) break;
 
         // header
         uint32_t version;
@@ -149,7 +139,7 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
             break;
         loaded_key.set_data(key_data, loaded_key.get_type_data_count());
 
-        path_extract_filename_no_ext(path, str_result);
+        path_extract_filename(path, str_result, true);
         loaded_key.set_name(string_get_cstr(str_result));
 
         *key = loaded_key;
@@ -166,12 +156,12 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
     return result;
 }
 
-bool LfRfidApp::save_key_data(const char* path, RfidKey* key) {
+bool LfRfidApp::save_key_data(string_t path, RfidKey* key) {
     FlipperFormat* file = flipper_format_file_alloc(storage);
     bool result = false;
 
     do {
-        if(!flipper_format_file_open_always(file, path)) break;
+        if(!flipper_format_file_open_always(file, string_get_cstr(path))) break;
         if(!flipper_format_write_header_cstr(file, app_filetype, 1)) break;
         if(!flipper_format_write_comment_cstr(file, "Key type can be EM4100, H10301 or I40134"))
             break;

+ 5 - 2
applications/lfrfid/lfrfid_app.h

@@ -1,4 +1,5 @@
 #pragma once
+#include "m-string.h"
 #include <furi.h>
 #include <furi_hal.h>
 
@@ -76,6 +77,8 @@ public:
 
     TextStore text_store;
 
+    string_t file_path;
+
     void run(void* args);
 
     static const char* app_folder;
@@ -86,8 +89,8 @@ public:
     bool load_key_from_file_select(bool need_restore);
     bool delete_key(RfidKey* key);
 
-    bool load_key_data(const char* path, RfidKey* key);
-    bool save_key_data(const char* path, RfidKey* key);
+    bool load_key_data(string_t path, RfidKey* key);
+    bool save_key_data(string_t path, RfidKey* key);
 
     void make_app_folder();
 };

+ 11 - 1
applications/lfrfid/scene/lfrfid_app_scene_save_name.cpp

@@ -1,11 +1,14 @@
 #include "lfrfid_app_scene_save_name.h"
+#include "m-string.h"
 #include <lib/toolbox/random_name.h>
+#include <lib/toolbox/path.h>
 
 void LfRfidAppSceneSaveName::on_enter(LfRfidApp* app, bool /* need_restore */) {
     const char* key_name = app->worker.key.get_name();
 
     bool key_name_empty = !strcmp(key_name, "");
     if(key_name_empty) {
+        string_set_str(app->file_path, app->app_folder);
         set_random_name(app->text_store.text, app->text_store.text_size);
     } else {
         app->text_store.set("%s", key_name);
@@ -21,10 +24,17 @@ void LfRfidAppSceneSaveName::on_enter(LfRfidApp* app, bool /* need_restore */) {
         app->worker.key.get_name_length(),
         key_name_empty);
 
+    string_t folder_path;
+    string_init(folder_path);
+
+    path_extract_dirname(string_get_cstr(app->file_path), folder_path);
+
     ValidatorIsFile* validator_is_file =
-        validator_is_file_alloc_init(app->app_folder, app->app_extension, key_name);
+        validator_is_file_alloc_init(string_get_cstr(folder_path), app->app_extension, key_name);
     text_input->set_validator(validator_is_file_callback, validator_is_file);
 
+    string_clear(folder_path);
+
     app->view_controller.switch_to<TextInputVM>();
 }
 

+ 11 - 9
applications/music_player/music_player.c

@@ -1,3 +1,5 @@
+#include "assets_icons.h"
+#include "m-string.h"
 #include <furi.h>
 #include <furi_hal.h>
 
@@ -298,23 +300,23 @@ int32_t music_player_app(void* p) {
         if(p) {
             string_cat_str(file_path, p);
         } else {
-            char file_name[256] = {0};
+            string_set_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER);
+
             DialogsApp* dialogs = furi_record_open("dialogs");
-            bool res = dialog_file_select_show(
+            bool res = dialog_file_browser_show(
                 dialogs,
-                MUSIC_PLAYER_APP_PATH_FOLDER,
+                file_path,
+                file_path,
                 MUSIC_PLAYER_APP_EXTENSION,
-                file_name,
-                255,
-                NULL);
+                true,
+                &I_music_10px,
+                false);
+
             furi_record_close("dialogs");
             if(!res) {
                 FURI_LOG_E(TAG, "No file selected");
                 break;
             }
-            string_cat_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER);
-            string_cat_str(file_path, "/");
-            string_cat_str(file_path, file_name);
         }
 
         if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) {

+ 78 - 38
applications/nfc/nfc_device.c

@@ -1,4 +1,6 @@
 #include "nfc_device.h"
+#include "assets_icons.h"
+#include "m-string.h"
 #include "nfc_types.h"
 
 #include <toolbox/path.h>
@@ -14,6 +16,7 @@ NfcDevice* nfc_device_alloc() {
     NfcDevice* nfc_dev = malloc(sizeof(NfcDevice));
     nfc_dev->storage = furi_record_open("storage");
     nfc_dev->dialogs = furi_record_open("dialogs");
+    string_init(nfc_dev->load_path);
     return nfc_dev;
 }
 
@@ -22,6 +25,7 @@ void nfc_device_free(NfcDevice* nfc_dev) {
     nfc_device_clear(nfc_dev);
     furi_record_close("storage");
     furi_record_close("dialogs");
+    string_clear(nfc_dev->load_path);
     free(nfc_dev);
 }
 
@@ -730,11 +734,24 @@ void nfc_device_set_name(NfcDevice* dev, const char* name) {
     strlcpy(dev->dev_name, name, NFC_DEV_NAME_MAX_LEN);
 }
 
+static void nfc_device_get_path_without_ext(string_t orig_path, string_t shadow_path) {
+    // TODO: this won't work if there is ".nfc" anywhere in the path other than
+    // at the end
+    size_t ext_start = string_search_str(orig_path, NFC_APP_EXTENSION);
+    string_set_n(shadow_path, orig_path, 0, ext_start);
+}
+
+static void nfc_device_get_shadow_path(string_t orig_path, string_t shadow_path) {
+    nfc_device_get_path_without_ext(orig_path, shadow_path);
+    string_cat_printf(shadow_path, "%s", NFC_APP_SHADOW_EXTENSION);
+}
+
 static bool nfc_device_save_file(
     NfcDevice* dev,
     const char* dev_name,
     const char* folder,
-    const char* extension) {
+    const char* extension,
+    bool use_load_path) {
     furi_assert(dev);
 
     bool saved = false;
@@ -744,10 +761,19 @@ static bool nfc_device_save_file(
     string_init(temp_str);
 
     do {
-        // Create nfc directory if necessary
-        if(!storage_simply_mkdir(dev->storage, NFC_APP_FOLDER)) break;
-        // First remove nfc device file if it was saved
-        string_printf(temp_str, "%s/%s%s", folder, dev_name, extension);
+        if(use_load_path && !string_empty_p(dev->load_path)) {
+            // Get directory name
+            path_extract_dirname(string_get_cstr(dev->load_path), temp_str);
+            // Create nfc directory if necessary
+            if(!storage_simply_mkdir(dev->storage, string_get_cstr(temp_str))) break;
+            // Make path to file to save
+            string_cat_printf(temp_str, "/%s%s", dev_name, extension);
+        } else {
+            // Create nfc directory if necessary
+            if(!storage_simply_mkdir(dev->storage, NFC_APP_FOLDER)) break;
+            // First remove nfc device file if it was saved
+            string_printf(temp_str, "%s/%s%s", folder, dev_name, extension);
+        }
         // Open file
         if(!flipper_format_file_open_always(file, string_get_cstr(temp_str))) break;
         // Write header
@@ -786,12 +812,12 @@ static bool nfc_device_save_file(
 }
 
 bool nfc_device_save(NfcDevice* dev, const char* dev_name) {
-    return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_EXTENSION);
+    return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_EXTENSION, true);
 }
 
 bool nfc_device_save_shadow(NfcDevice* dev, const char* dev_name) {
     dev->shadow_file_exist = true;
-    return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_SHADOW_EXTENSION);
+    return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_SHADOW_EXTENSION, true);
 }
 
 static bool nfc_device_load_data(NfcDevice* dev, string_t path) {
@@ -805,9 +831,7 @@ static bool nfc_device_load_data(NfcDevice* dev, string_t path) {
 
     do {
         // Check existance of shadow file
-        size_t ext_start = string_search_str(path, NFC_APP_EXTENSION);
-        string_set_n(temp_str, path, 0, ext_start);
-        string_cat_printf(temp_str, "%s", NFC_APP_SHADOW_EXTENSION);
+        nfc_device_get_shadow_path(path, temp_str);
         dev->shadow_file_exist =
             storage_common_stat(dev->storage, string_get_cstr(temp_str), NULL) == FSE_OK;
         // Open shadow file if it exists. If not - open original
@@ -864,15 +888,16 @@ bool nfc_device_load(NfcDevice* dev, const char* file_path) {
     furi_assert(file_path);
 
     // Load device data
-    string_t path;
-    string_init_set_str(path, file_path);
-    bool dev_load = nfc_device_load_data(dev, path);
+    string_set_str(dev->load_path, file_path);
+    bool dev_load = nfc_device_load_data(dev, dev->load_path);
     if(dev_load) {
         // Set device name
-        path_extract_filename_no_ext(file_path, path);
-        nfc_device_set_name(dev, string_get_cstr(path));
+        string_t filename;
+        string_init(filename);
+        path_extract_filename_no_ext(file_path, filename);
+        nfc_device_set_name(dev, string_get_cstr(filename));
+        string_clear(filename);
     }
-    string_clear(path);
 
     return dev_load;
 }
@@ -880,23 +905,19 @@ bool nfc_device_load(NfcDevice* dev, const char* file_path) {
 bool nfc_file_select(NfcDevice* dev) {
     furi_assert(dev);
 
-    // Input events and views are managed by file_select
-    bool res = dialog_file_select_show(
-        dev->dialogs,
-        NFC_APP_FOLDER,
-        NFC_APP_EXTENSION,
-        dev->file_name,
-        sizeof(dev->file_name),
-        dev->dev_name);
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(
+        dev->dialogs, dev->load_path, dev->load_path, NFC_APP_EXTENSION, true, &I_Nfc_10px, true);
     if(res) {
-        string_t dev_str;
-        // Get key file path
-        string_init_printf(dev_str, "%s/%s%s", NFC_APP_FOLDER, dev->file_name, NFC_APP_EXTENSION);
-        res = nfc_device_load_data(dev, dev_str);
+        string_t filename;
+        string_init(filename);
+        path_extract_filename(dev->load_path, filename, true);
+        strncpy(dev->dev_name, string_get_cstr(filename), NFC_DEV_NAME_MAX_LEN);
+        res = nfc_device_load_data(dev, dev->load_path);
         if(res) {
-            nfc_device_set_name(dev, dev->file_name);
+            nfc_device_set_name(dev, dev->dev_name);
         }
-        string_clear(dev_str);
+        string_clear(filename);
     }
 
     return res;
@@ -914,9 +935,10 @@ void nfc_device_clear(NfcDevice* dev) {
     nfc_device_data_clear(&dev->dev_data);
     memset(&dev->dev_data, 0, sizeof(dev->dev_data));
     dev->format = NfcDeviceSaveFormatUid;
+    string_set_str(dev->load_path, NFC_APP_FOLDER);
 }
 
-bool nfc_device_delete(NfcDevice* dev) {
+bool nfc_device_delete(NfcDevice* dev, bool use_load_path) {
     furi_assert(dev);
 
     bool deleted = false;
@@ -925,12 +947,20 @@ bool nfc_device_delete(NfcDevice* dev) {
 
     do {
         // Delete original file
-        string_init_printf(file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION);
+        if(use_load_path && !string_empty_p(dev->load_path)) {
+            string_set(file_path, dev->load_path);
+        } else {
+            string_printf(file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION);
+        }
         if(!storage_simply_remove(dev->storage, string_get_cstr(file_path))) break;
         // Delete shadow file if it exists
         if(dev->shadow_file_exist) {
-            string_printf(
-                file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION);
+            if(use_load_path && !string_empty_p(dev->load_path)) {
+                nfc_device_get_shadow_path(dev->load_path, file_path);
+            } else {
+                string_printf(
+                    file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION);
+            }
             if(!storage_simply_remove(dev->storage, string_get_cstr(file_path))) break;
         }
         deleted = true;
@@ -944,19 +974,29 @@ bool nfc_device_delete(NfcDevice* dev) {
     return deleted;
 }
 
-bool nfc_device_restore(NfcDevice* dev) {
+bool nfc_device_restore(NfcDevice* dev, bool use_load_path) {
     furi_assert(dev);
     furi_assert(dev->shadow_file_exist);
 
     bool restored = false;
     string_t path;
 
+    string_init(path);
+
     do {
-        string_init_printf(
-            path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION);
+        if(use_load_path && !string_empty_p(dev->load_path)) {
+            nfc_device_get_shadow_path(dev->load_path, path);
+        } else {
+            string_printf(
+                path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION);
+        }
         if(!storage_simply_remove(dev->storage, string_get_cstr(path))) break;
         dev->shadow_file_exist = false;
-        string_printf(path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION);
+        if(use_load_path && !string_empty_p(dev->load_path)) {
+            string_set(path, dev->load_path);
+        } else {
+            string_printf(path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION);
+        }
         if(!nfc_device_load_data(dev, path)) break;
         restored = true;
     } while(0);

+ 3 - 4
applications/nfc/nfc_device.h

@@ -12,7 +12,6 @@
 #include <lib/nfc_protocols/mifare_desfire.h>
 
 #define NFC_DEV_NAME_MAX_LEN 22
-#define NFC_FILE_NAME_MAX_LEN 120
 #define NFC_READER_DATA_MAX_SIZE 64
 
 #define NFC_APP_FOLDER "/any/nfc"
@@ -57,7 +56,7 @@ typedef struct {
     DialogsApp* dialogs;
     NfcDeviceData dev_data;
     char dev_name[NFC_DEV_NAME_MAX_LEN + 1];
-    char file_name[NFC_FILE_NAME_MAX_LEN];
+    string_t load_path;
     NfcDeviceSaveFormat format;
     bool shadow_file_exist;
 } NfcDevice;
@@ -80,6 +79,6 @@ void nfc_device_data_clear(NfcDeviceData* dev);
 
 void nfc_device_clear(NfcDevice* dev);
 
-bool nfc_device_delete(NfcDevice* dev);
+bool nfc_device_delete(NfcDevice* dev, bool use_load_path);
 
-bool nfc_device_restore(NfcDevice* dev);
+bool nfc_device_restore(NfcDevice* dev, bool use_load_path);

+ 1 - 1
applications/nfc/scenes/nfc_scene_delete.c

@@ -73,7 +73,7 @@ bool nfc_scene_delete_on_event(void* context, SceneManagerEvent event) {
         if(event.event == GuiButtonTypeLeft) {
             return scene_manager_previous_scene(nfc->scene_manager);
         } else if(event.event == GuiButtonTypeRight) {
-            if(nfc_device_delete(nfc->dev)) {
+            if(nfc_device_delete(nfc->dev, true)) {
                 scene_manager_next_scene(nfc->scene_manager, NfcSceneDeleteSuccess);
             } else {
                 scene_manager_search_and_switch_to_previous_scene(

+ 16 - 3
applications/nfc/scenes/nfc_scene_save_name.c

@@ -1,6 +1,8 @@
 #include "../nfc_i.h"
+#include "m-string.h"
 #include <lib/toolbox/random_name.h>
 #include <gui/modules/validators.h>
+#include <toolbox/path.h>
 
 void nfc_scene_save_name_text_input_callback(void* context) {
     Nfc* nfc = context;
@@ -29,11 +31,22 @@ void nfc_scene_save_name_on_enter(void* context) {
         NFC_DEV_NAME_MAX_LEN,
         dev_name_empty);
 
-    ValidatorIsFile* validator_is_file =
-        validator_is_file_alloc_init(NFC_APP_FOLDER, NFC_APP_EXTENSION, nfc->dev->dev_name);
+    string_t folder_path;
+    string_init(folder_path);
+
+    if(string_end_with_str_p(nfc->dev->load_path, NFC_APP_EXTENSION)) {
+        path_extract_dirname(string_get_cstr(nfc->dev->load_path), folder_path);
+    } else {
+        string_set_str(folder_path, NFC_APP_FOLDER);
+    }
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        string_get_cstr(folder_path), NFC_APP_EXTENSION, nfc->dev->dev_name);
     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewTextInput);
+
+    string_clear(folder_path);
 }
 
 bool nfc_scene_save_name_on_event(void* context, SceneManagerEvent event) {
@@ -43,7 +56,7 @@ bool nfc_scene_save_name_on_event(void* context, SceneManagerEvent event) {
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == NfcCustomEventTextInputDone) {
             if(strcmp(nfc->dev->dev_name, "")) {
-                nfc_device_delete(nfc->dev);
+                nfc_device_delete(nfc->dev, true);
             }
             if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetUid)) {
                 nfc->dev->dev_data.nfc_data = nfc->dev_edit_data;

+ 0 - 3
applications/nfc/scenes/nfc_scene_save_success.c

@@ -30,9 +30,6 @@ bool nfc_scene_save_success_on_event(void* context, SceneManagerEvent event) {
             if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneCardMenu)) {
                 consumed = scene_manager_search_and_switch_to_previous_scene(
                     nfc->scene_manager, NfcSceneCardMenu);
-            } else if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) {
-                consumed = scene_manager_search_and_switch_to_another_scene(
-                    nfc->scene_manager, NfcSceneFileSelect);
             } else if(scene_manager_has_previous_scene(
                           nfc->scene_manager, NfcSceneMifareDesfireMenu)) {
                 consumed = scene_manager_search_and_switch_to_previous_scene(

+ 1 - 1
applications/nfc/scenes/nfc_scene_saved_menu.c

@@ -78,7 +78,7 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
             scene_manager_next_scene(nfc->scene_manager, NfcSceneDeviceInfo);
             consumed = true;
         } else if(event.event == SubmenuIndexRestoreOriginal) {
-            if(!nfc_device_restore(nfc->dev)) {
+            if(!nfc_device_restore(nfc->dev, true)) {
                 scene_manager_search_and_switch_to_previous_scene(
                     nfc->scene_manager, NfcSceneStart);
             } else {

+ 2 - 0
applications/nfc/scenes/nfc_scene_set_type.c

@@ -1,4 +1,5 @@
 #include "../nfc_i.h"
+#include "m-string.h"
 
 enum SubmenuIndex {
     SubmenuIndexNFCA4,
@@ -16,6 +17,7 @@ void nfc_scene_set_type_on_enter(void* context) {
     Submenu* submenu = nfc->submenu;
     // Clear device name
     nfc_device_set_name(nfc->dev, "");
+    string_set_str(nfc->dev->load_path, "");
     submenu_add_item(
         submenu, "NFC-A 7-bytes UID", SubmenuIndexNFCA7, nfc_scene_set_type_submenu_callback, nfc);
     submenu_add_item(

+ 1 - 1
applications/subghz/scenes/subghz_scene_delete.c

@@ -49,7 +49,7 @@ bool subghz_scene_delete_on_event(void* context, SceneManagerEvent event) {
     SubGhz* subghz = context;
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == SubGhzCustomEventSceneDelete) {
-            strncpy(subghz->file_path_tmp, subghz->file_path, SUBGHZ_MAX_LEN_NAME);
+            string_set(subghz->file_path_tmp, subghz->file_path);
             if(subghz_delete_file(subghz)) {
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteSuccess);
             } else {

+ 2 - 2
applications/subghz/scenes/subghz_scene_delete_raw.c

@@ -24,7 +24,7 @@ void subghz_scene_delete_raw_on_enter(void* context) {
     char delete_str[SUBGHZ_MAX_LEN_NAME + 16];
     string_t file_name;
     string_init(file_name);
-    path_extract_filename_no_ext(subghz->file_path, file_name);
+    path_extract_filename(subghz->file_path, file_name, true);
     snprintf(delete_str, sizeof(delete_str), "\e#Delete %s?\e#", string_get_cstr(file_name));
     string_clear(file_name);
 
@@ -61,7 +61,7 @@ bool subghz_scene_delete_raw_on_event(void* context, SceneManagerEvent event) {
     SubGhz* subghz = context;
     if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == SubGhzCustomEventSceneDeleteRAW) {
-            strncpy(subghz->file_path_tmp, subghz->file_path, SUBGHZ_MAX_LEN_NAME);
+            string_set(subghz->file_path_tmp, subghz->file_path);
             if(subghz_delete_file(subghz)) {
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteSuccess);
             } else {

+ 1 - 1
applications/subghz/scenes/subghz_scene_more_raw.c

@@ -45,7 +45,7 @@ bool subghz_scene_more_raw_on_event(void* context, SceneManagerEvent event) {
             scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteRAW);
             return true;
         } else if(event.event == SubmenuIndexEdit) {
-            memset(subghz->file_path_tmp, 0, sizeof(subghz->file_path_tmp));
+            string_reset(subghz->file_path_tmp);
             scene_manager_set_scene_state(
                 subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexEdit);
             scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);

+ 4 - 4
applications/subghz/scenes/subghz_scene_read_raw.c

@@ -23,7 +23,7 @@ bool subghz_scene_read_raw_update_filename(SubGhz* subghz) {
             break;
         }
 
-        strncpy(subghz->file_path, string_get_cstr(temp_str), SUBGHZ_MAX_LEN_NAME);
+        string_set(subghz->file_path, temp_str);
 
         ret = true;
     } while(false);
@@ -73,13 +73,13 @@ void subghz_scene_read_raw_on_enter(void* context) {
         subghz_read_raw_set_status(subghz->subghz_read_raw, SubGhzReadRAWStatusIDLE, "");
         break;
     case SubGhzRxKeyStateRAWLoad:
-        path_extract_filename_no_ext(subghz->file_path, file_name);
+        path_extract_filename(subghz->file_path, file_name, true);
         subghz_read_raw_set_status(
             subghz->subghz_read_raw, SubGhzReadRAWStatusLoadKeyTX, string_get_cstr(file_name));
         subghz->txrx->rx_key_state = SubGhzRxKeyStateIDLE;
         break;
     case SubGhzRxKeyStateRAWSave:
-        path_extract_filename_no_ext(subghz->file_path, file_name);
+        path_extract_filename(subghz->file_path, file_name, true);
         subghz_read_raw_set_status(
             subghz->subghz_read_raw, SubGhzReadRAWStatusSaveKey, string_get_cstr(file_name));
         subghz->txrx->rx_key_state = SubGhzRxKeyStateIDLE;
@@ -273,7 +273,7 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) {
                     subghz->state_notifications = SubGhzNotificationStateRx;
                     subghz->txrx->rx_key_state = SubGhzRxKeyStateAddKey;
                 } else {
-                    string_set(subghz->error_str, "Function requires\nan SD card.");
+                    string_set_str(subghz->error_str, "Function requires\nan SD card.");
                     scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowError);
                 }
             }

+ 28 - 24
applications/subghz/scenes/subghz_scene_save_name.c

@@ -1,4 +1,6 @@
 #include "../subghz_i.h"
+#include "m-string.h"
+#include "subghz/types.h"
 #include <lib/toolbox/random_name.h>
 #include "../helpers/subghz_custom_event.h"
 #include <lib/subghz/protocols/raw.h>
@@ -20,45 +22,49 @@ void subghz_scene_save_name_on_enter(void* context) {
     bool dev_name_empty = false;
 
     string_t file_name;
+    string_t dir_name;
     string_init(file_name);
+    string_init(dir_name);
 
-    if(!strcmp(subghz->file_path, "")) {
+    if(!subghz_path_is_file(subghz->file_path)) {
         char file_name_buf[SUBGHZ_MAX_LEN_NAME] = {0};
         set_random_name(file_name_buf, SUBGHZ_MAX_LEN_NAME);
-        string_set(file_name, file_name_buf);
-        strncpy(subghz->file_dir, SUBGHZ_APP_FOLDER, SUBGHZ_MAX_LEN_NAME);
+        string_set_str(file_name, file_name_buf);
+        string_set_str(subghz->file_path, SUBGHZ_APP_FOLDER);
         //highlighting the entire filename by default
         dev_name_empty = true;
     } else {
-        strncpy(subghz->file_path_tmp, subghz->file_path, SUBGHZ_MAX_LEN_NAME);
-        path_extract_dirname(subghz->file_path, file_name);
-        strncpy(subghz->file_dir, string_get_cstr(file_name), SUBGHZ_MAX_LEN_NAME);
-        path_extract_filename_no_ext(subghz->file_path, file_name);
+        string_set(subghz->file_path_tmp, subghz->file_path);
+        path_extract_dirname(string_get_cstr(subghz->file_path), dir_name);
+        path_extract_filename(subghz->file_path, file_name, true);
         if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
            SubGhzCustomEventManagerNoSet) {
             subghz_get_next_name_file(subghz, SUBGHZ_MAX_LEN_NAME);
-            path_extract_filename_no_ext(subghz->file_path, file_name);
+            path_extract_filename(subghz->file_path, file_name, true);
             if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) ==
                SubGhzCustomEventManagerSetRAW) {
                 dev_name_empty = true;
             }
         }
+        string_set(subghz->file_path, dir_name);
     }
-    strncpy(subghz->file_path, string_get_cstr(file_name), SUBGHZ_MAX_LEN_NAME);
+
+    strncpy(subghz->file_name_tmp, string_get_cstr(file_name), SUBGHZ_MAX_LEN_NAME);
     text_input_set_header_text(text_input, "Name signal");
     text_input_set_result_callback(
         text_input,
         subghz_scene_save_name_text_input_callback,
         subghz,
-        subghz->file_path,
+        subghz->file_name_tmp,
         MAX_TEXT_INPUT_LEN, // buffer size
         dev_name_empty);
 
-    ValidatorIsFile* validator_is_file =
-        validator_is_file_alloc_init(subghz->file_dir, SUBGHZ_APP_EXTENSION, NULL);
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        string_get_cstr(subghz->file_path), SUBGHZ_APP_EXTENSION, NULL);
     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
 
     string_clear(file_name);
+    string_clear(dir_name);
 
     view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdTextInput);
 }
@@ -66,18 +72,15 @@ void subghz_scene_save_name_on_enter(void* context) {
 bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
     SubGhz* subghz = context;
     if(event.type == SceneManagerEventTypeBack) {
-        strncpy(subghz->file_path, subghz->file_path_tmp, SUBGHZ_MAX_LEN_NAME);
+        string_set(subghz->file_path, subghz->file_path_tmp);
         scene_manager_previous_scene(subghz->scene_manager);
         return true;
     } else if(event.type == SceneManagerEventTypeCustom) {
         if(event.event == SubGhzCustomEventSceneSaveName) {
-            if(strcmp(subghz->file_path, "")) {
-                string_t temp_str;
-                string_init_printf(
-                    temp_str, "%s/%s%s", subghz->file_dir, subghz->file_path, SUBGHZ_APP_EXTENSION);
-                strncpy(subghz->file_path, string_get_cstr(temp_str), SUBGHZ_MAX_LEN_NAME);
-                string_clear(temp_str);
-                if(strcmp(subghz->file_path_tmp, "")) {
+            if(strcmp(subghz->file_name_tmp, "")) {
+                string_cat_printf(
+                    subghz->file_path, "/%s%s", subghz->file_name_tmp, SUBGHZ_APP_EXTENSION);
+                if(subghz_path_is_file(subghz->file_path_tmp)) {
                     if(!subghz_rename_file(subghz)) {
                         return false;
                     }
@@ -85,7 +88,7 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
                     if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType) !=
                        SubGhzCustomEventManagerNoSet) {
                         subghz_save_protocol_to_file(
-                            subghz, subghz->txrx->fff_data, subghz->file_path);
+                            subghz, subghz->txrx->fff_data, string_get_cstr(subghz->file_path));
                         scene_manager_set_scene_state(
                             subghz->scene_manager,
                             SubGhzSceneSetType,
@@ -95,13 +98,14 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
                             subghz,
                             subghz_history_get_raw_data(
                                 subghz->txrx->history, subghz->txrx->idx_menu_chosen),
-                            subghz->file_path);
+                            string_get_cstr(subghz->file_path));
                     }
                 }
 
                 if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=
                    SubGhzCustomEventManagerNoSet) {
-                    subghz_protocol_raw_gen_fff_data(subghz->txrx->fff_data, subghz->file_path);
+                    subghz_protocol_raw_gen_fff_data(
+                        subghz->txrx->fff_data, string_get_cstr(subghz->file_path));
                     scene_manager_set_scene_state(
                         subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet);
                 } else {
@@ -111,7 +115,7 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveSuccess);
                 return true;
             } else {
-                string_set(subghz->error_str, "No name file");
+                string_set_str(subghz->error_str, "No name file");
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);
                 return true;
             }

+ 3 - 3
applications/subghz/scenes/subghz_scene_set_type.c

@@ -39,7 +39,7 @@ bool subghz_scene_set_type_submenu_gen_data_protocol(
         subghz_receiver_search_decoder_base_by_name(subghz->txrx->receiver, protocol_name);
 
     if(subghz->txrx->decoder_result == NULL) {
-        string_set(subghz->error_str, "Protocol not found");
+        string_set_str(subghz->error_str, "Protocol not found");
         scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);
         return false;
     }
@@ -282,7 +282,7 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
             }
             subghz_transmitter_free(subghz->txrx->transmitter);
             if(!generated_protocol) {
-                string_set(
+                string_set_str(
                     subghz->error_str, "Function requires\nan SD card with\nfresh databases.");
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowError);
             }
@@ -306,7 +306,7 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
             }
             subghz_transmitter_free(subghz->txrx->transmitter);
             if(!generated_protocol) {
-                string_set(
+                string_set_str(
                     subghz->error_str, "Function requires\nan SD card with\nfresh databases.");
                 scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowError);
             }

+ 1 - 1
applications/subghz/scenes/subghz_scene_transmitter.c

@@ -94,7 +94,7 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) {
                 subghz->scene_manager, SubGhzSceneStart);
             return true;
         } else if(event.event == SubGhzCustomEventViewTransmitterError) {
-            string_set(subghz->error_str, "Protocol not found");
+            string_set_str(subghz->error_str, "Protocol not found");
             scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowErrorSub);
         }
     } else if(event.type == SceneManagerEventTypeTick) {

+ 11 - 5
applications/subghz/subghz.c

@@ -1,5 +1,7 @@
 /* Abandon hope, all ye who enter here. */
 
+#include "m-string.h"
+#include "subghz/types.h"
 #include "subghz_i.h"
 #include <lib/toolbox/path.h>
 
@@ -24,6 +26,9 @@ void subghz_tick_event_callback(void* context) {
 SubGhz* subghz_alloc() {
     SubGhz* subghz = malloc(sizeof(SubGhz));
 
+    string_init(subghz->file_path);
+    string_init(subghz->file_path_tmp);
+
     // GUI
     subghz->gui = furi_record_open("gui");
 
@@ -241,9 +246,9 @@ void subghz_free(SubGhz* subghz) {
     furi_record_close("notification");
     subghz->notifications = NULL;
 
-    // About birds
-    furi_assert(subghz->file_path[SUBGHZ_MAX_LEN_NAME] == 0);
-    furi_assert(subghz->file_path_tmp[SUBGHZ_MAX_LEN_NAME] == 0);
+    // Path strings
+    string_clear(subghz->file_path);
+    string_clear(subghz->file_path_tmp);
 
     // The rest
     free(subghz);
@@ -260,7 +265,7 @@ int32_t subghz_app(void* p) {
     // Check argument and run corresponding scene
     if(p) {
         if(subghz_key_load(subghz, p)) {
-            strncpy(subghz->file_path, p, SUBGHZ_MAX_LEN_NAME);
+            string_set_str(subghz->file_path, p);
 
             if((!strcmp(subghz->txrx->decoder_result->protocol->name, "RAW"))) {
                 //Load Raw TX
@@ -276,12 +281,13 @@ int32_t subghz_app(void* p) {
             view_dispatcher_stop(subghz->view_dispatcher);
         }
     } else {
+        string_set_str(subghz->file_path, SUBGHZ_APP_FOLDER);
         if(load_database) {
             scene_manager_next_scene(subghz->scene_manager, SubGhzSceneStart);
         } else {
             scene_manager_set_scene_state(
                 subghz->scene_manager, SubGhzSceneShowError, SubGhzCustomEventManagerSet);
-            string_set(
+            string_set_str(
                 subghz->error_str,
                 "No SD card or\ndatabase found.\nSome app function\nmay be reduced.");
             scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowError);

+ 1 - 1
applications/subghz/subghz_cli.c

@@ -303,7 +303,7 @@ void subghz_cli_command_decode_raw(Cli* cli, string_t args, void* context) {
     UNUSED(context);
     string_t file_name;
     string_init(file_name);
-    string_set(file_name, "/any/subghz/test.sub");
+    string_set_str(file_name, "/any/subghz/test.sub");
 
     Storage* storage = furi_record_open("storage");
     FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);

+ 2 - 2
applications/subghz/subghz_history.c

@@ -169,14 +169,14 @@ bool subghz_history_add_to_history(
             break;
         }
         if(!strcmp(string_get_cstr(instance->tmp_string), "KeeLoq")) {
-            string_set(instance->tmp_string, "KL ");
+            string_set_str(instance->tmp_string, "KL ");
             if(!flipper_format_read_string(item->flipper_string, "Manufacture", text)) {
                 FURI_LOG_E(TAG, "Missing Protocol");
                 break;
             }
             string_cat(instance->tmp_string, text);
         } else if(!strcmp(string_get_cstr(instance->tmp_string), "Star Line")) {
-            string_set(instance->tmp_string, "SL ");
+            string_set_str(instance->tmp_string, "SL ");
             if(!flipper_format_read_string(item->flipper_string, "Manufacture", text)) {
                 FURI_LOG_E(TAG, "Missing Protocol");
                 break;

+ 29 - 23
applications/subghz/subghz_i.c

@@ -1,5 +1,8 @@
 #include "subghz_i.h"
 
+#include "assets_icons.h"
+#include "m-string.h"
+#include "subghz/types.h"
 #include <math.h>
 #include <furi.h>
 #include <furi_hal.h>
@@ -45,11 +48,11 @@ void subghz_get_frequency_modulation(SubGhz* subghz, string_t frequency, string_
     if(modulation != NULL) {
         if(subghz->txrx->preset == FuriHalSubGhzPresetOok650Async ||
            subghz->txrx->preset == FuriHalSubGhzPresetOok270Async) {
-            string_set(modulation, "AM");
+            string_set_str(modulation, "AM");
         } else if(
             subghz->txrx->preset == FuriHalSubGhzPreset2FSKDev238Async ||
             subghz->txrx->preset == FuriHalSubGhzPreset2FSKDev476Async) {
-            string_set(modulation, "FM");
+            string_set_str(modulation, "FM");
         } else {
             furi_crash("SugGhz: Modulation is incorrect.");
         }
@@ -189,8 +192,9 @@ void subghz_tx_stop(SubGhz* subghz) {
 
     //if protocol dynamic then we save the last upload
     if((subghz->txrx->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) &&
-       (strcmp(subghz->file_path, ""))) {
-        subghz_save_protocol_to_file(subghz, subghz->txrx->fff_data, subghz->file_path);
+       (subghz_path_is_file(subghz->file_path))) {
+        subghz_save_protocol_to_file(
+            subghz, subghz->txrx->fff_data, string_get_cstr(subghz->file_path));
     }
     subghz_idle(subghz);
     notification_message(subghz->notifications, &sequence_reset_red);
@@ -332,10 +336,10 @@ bool subghz_get_next_name_file(SubGhz* subghz, uint8_t max_len) {
 
     bool res = false;
 
-    if(strcmp(subghz->file_path, "")) {
+    if(subghz_path_is_file(subghz->file_path)) {
         //get the name of the next free file
-        path_extract_filename_no_ext(subghz->file_path, file_name);
-        path_extract_dirname(subghz->file_path, file_path);
+        path_extract_filename(subghz->file_path, file_name, true);
+        path_extract_dirname(string_get_cstr(subghz->file_path), file_path);
 
         storage_get_next_filename(
             storage,
@@ -351,7 +355,7 @@ bool subghz_get_next_name_file(SubGhz* subghz, uint8_t max_len) {
             string_get_cstr(file_path),
             string_get_cstr(file_name),
             SUBGHZ_APP_EXTENSION);
-        strncpy(subghz->file_path, string_get_cstr(temp_str), SUBGHZ_MAX_LEN_NAME);
+        string_set(subghz->file_path, temp_str);
         res = true;
     }
 
@@ -411,19 +415,17 @@ bool subghz_load_protocol_from_file(SubGhz* subghz) {
     string_init(file_path);
 
     // Input events and views are managed by file_select
-    bool res = dialog_file_select_show(
+    bool res = dialog_file_browser_show(
         subghz->dialogs,
-        SUBGHZ_APP_FOLDER,
-        SUBGHZ_APP_EXTENSION,
         subghz->file_path,
-        sizeof(subghz->file_path),
-        NULL);
+        subghz->file_path,
+        SUBGHZ_APP_EXTENSION,
+        true,
+        &I_sub1_10px,
+        true);
 
     if(res) {
-        string_printf(
-            file_path, "%s/%s%s", SUBGHZ_APP_FOLDER, subghz->file_path, SUBGHZ_APP_EXTENSION);
-        strncpy(subghz->file_path, string_get_cstr(file_path), SUBGHZ_MAX_LEN_NAME);
-        res = subghz_key_load(subghz, subghz->file_path);
+        res = subghz_key_load(subghz, string_get_cstr(subghz->file_path));
     }
 
     string_clear(file_path);
@@ -437,9 +439,9 @@ bool subghz_rename_file(SubGhz* subghz) {
 
     Storage* storage = furi_record_open("storage");
 
-    if(strcmp(subghz->file_path_tmp, subghz->file_path)) {
-        FS_Error fs_result =
-            storage_common_rename(storage, subghz->file_path_tmp, subghz->file_path);
+    if(string_cmp(subghz->file_path_tmp, subghz->file_path)) {
+        FS_Error fs_result = storage_common_rename(
+            storage, string_get_cstr(subghz->file_path_tmp), string_get_cstr(subghz->file_path));
 
         if(fs_result != FSE_OK) {
             dialog_message_show_storage_error(subghz->dialogs, "Cannot rename\n file/directory");
@@ -455,7 +457,7 @@ bool subghz_delete_file(SubGhz* subghz) {
     furi_assert(subghz);
 
     Storage* storage = furi_record_open("storage");
-    bool result = storage_simply_remove(storage, subghz->file_path_tmp);
+    bool result = storage_simply_remove(storage, string_get_cstr(subghz->file_path_tmp));
     furi_record_close("storage");
 
     subghz_file_name_clear(subghz);
@@ -465,8 +467,12 @@ bool subghz_delete_file(SubGhz* subghz) {
 
 void subghz_file_name_clear(SubGhz* subghz) {
     furi_assert(subghz);
-    memset(subghz->file_path, 0, sizeof(subghz->file_path));
-    memset(subghz->file_path_tmp, 0, sizeof(subghz->file_path_tmp));
+    string_set_str(subghz->file_path, SUBGHZ_APP_FOLDER);
+    string_reset(subghz->file_path_tmp);
+}
+
+bool subghz_path_is_file(string_t path) {
+    return string_end_with_str_p(path, SUBGHZ_APP_EXTENSION);
 }
 
 uint32_t subghz_random_serial(void) {

+ 5 - 5
applications/subghz/subghz_i.h

@@ -35,7 +35,7 @@
 #include <gui/modules/variable_item_list.h>
 #include <lib/toolbox/path.h>
 
-#define SUBGHZ_MAX_LEN_NAME 250
+#define SUBGHZ_MAX_LEN_NAME 64
 
 /** SubGhzNotification state */
 typedef enum {
@@ -119,10 +119,9 @@ struct SubGhz {
     TextInput* text_input;
     Widget* widget;
     DialogsApp* dialogs;
-    char file_path[SUBGHZ_MAX_LEN_NAME + 1];
-    char file_path_tmp[SUBGHZ_MAX_LEN_NAME + 1];
-    //ToDo you can get rid of it, you need to refactor text input to return the path to the folder
-    char file_dir[SUBGHZ_MAX_LEN_NAME + 1];
+    string_t file_path;
+    string_t file_path_tmp;
+    char file_name_tmp[SUBGHZ_MAX_LEN_NAME];
     SubGhzNotificationState state_notifications;
 
     SubGhzViewReceiver* subghz_receiver;
@@ -173,5 +172,6 @@ bool subghz_load_protocol_from_file(SubGhz* subghz);
 bool subghz_rename_file(SubGhz* subghz);
 bool subghz_delete_file(SubGhz* subghz);
 void subghz_file_name_clear(SubGhz* subghz);
+bool subghz_path_is_file(string_t path);
 uint32_t subghz_random_serial(void);
 void subghz_hopper_update(SubGhz* subghz);

+ 3 - 3
applications/subghz/views/receiver.c

@@ -111,9 +111,9 @@ void subghz_view_receiver_add_data_statusbar(
     furi_assert(subghz_receiver);
     with_view_model(
         subghz_receiver->view, (SubGhzViewReceiverModel * model) {
-            string_set(model->frequency_str, frequency_str);
-            string_set(model->preset_str, preset_str);
-            string_set(model->history_stat_str, history_stat_str);
+            string_set_str(model->frequency_str, frequency_str);
+            string_set_str(model->preset_str, preset_str);
+            string_set_str(model->history_stat_str, history_stat_str);
             return true;
         });
 }

+ 8 - 8
applications/subghz/views/subghz_read_raw.c

@@ -46,8 +46,8 @@ void subghz_read_raw_add_data_statusbar(
     furi_assert(instance);
     with_view_model(
         instance->view, (SubGhzReadRAWModel * model) {
-            string_set(model->frequency_str, frequency_str);
-            string_set(model->preset_str, preset_str);
+            string_set_str(model->frequency_str, frequency_str);
+            string_set_str(model->preset_str, preset_str);
             return true;
         });
 }
@@ -372,7 +372,7 @@ bool subghz_read_raw_input(InputEvent* event, void* context) {
                     model->satus = SubGhzReadRAWStatusStart;
                     model->rssi_history_end = false;
                     model->ind_write = 0;
-                    string_set(model->sample_write, "0 spl.");
+                    string_set_str(model->sample_write, "0 spl.");
                     string_reset(model->file_name);
                     instance->callback(SubGhzCustomEventViewReadRAWErase, instance->context);
                 }
@@ -424,7 +424,7 @@ void subghz_read_raw_set_status(
                 model->rssi_history_end = false;
                 model->ind_write = 0;
                 string_reset(model->file_name);
-                string_set(model->sample_write, "0 spl.");
+                string_set_str(model->sample_write, "0 spl.");
                 return true;
             });
         break;
@@ -441,8 +441,8 @@ void subghz_read_raw_set_status(
                 model->satus = SubGhzReadRAWStatusLoadKeyIDLE;
                 model->rssi_history_end = false;
                 model->ind_write = 0;
-                string_set(model->file_name, file_name);
-                string_set(model->sample_write, "RAW");
+                string_set_str(model->file_name, file_name);
+                string_set_str(model->sample_write, "RAW");
                 return true;
             });
         break;
@@ -451,8 +451,8 @@ void subghz_read_raw_set_status(
             instance->view, (SubGhzReadRAWModel * model) {
                 model->satus = SubGhzReadRAWStatusLoadKeyIDLE;
                 if(!model->ind_write) {
-                    string_set(model->file_name, file_name);
-                    string_set(model->sample_write, "RAW");
+                    string_set_str(model->file_name, file_name);
+                    string_set_str(model->sample_write, "RAW");
                 } else {
                     string_reset(model->file_name);
                 }

+ 3 - 3
applications/subghz/views/transmitter.c

@@ -36,9 +36,9 @@ void subghz_view_transmitter_add_data_to_show(
     furi_assert(subghz_transmitter);
     with_view_model(
         subghz_transmitter->view, (SubGhzViewTransmitterModel * model) {
-            string_set(model->key_str, key_str);
-            string_set(model->frequency_str, frequency_str);
-            string_set(model->preset_str, preset_str);
+            string_set_str(model->key_str, key_str);
+            string_set_str(model->frequency_str, frequency_str);
+            string_set_str(model->preset_str, preset_str);
             model->show_button = show_button;
             return true;
         });

+ 12 - 0
assets/compiled/assets_icons.c

@@ -40,6 +40,9 @@ const uint8_t* const _I_125_10px[] = {_I_125_10px_0};
 const uint8_t _I_Nfc_10px_0[] = {0x00,0x80,0x00,0x00,0x01,0x22,0x02,0x43,0x02,0x45,0x02,0x49,0x02,0x31,0x02,0x22,0x02,0x00,0x01,0x80,0x00,};
 const uint8_t* const _I_Nfc_10px[] = {_I_Nfc_10px_0};
 
+const uint8_t _I_back_10px_0[] = {0x00,0x00,0x00,0x10,0x00,0x38,0x00,0x7C,0x00,0xFE,0x00,0x38,0x00,0x38,0x00,0xF8,0x01,0xF8,0x01,0x00,0x00,};
+const uint8_t* const _I_back_10px[] = {_I_back_10px_0};
+
 const uint8_t _I_badusb_10px_0[] = {0x01,0x00,0x11,0x00,0x00,0x0f,0xe2,0x01,0xfc,0x80,0xdd,0x20,0x32,0x48,0x08,0x14,0x40,0x23,0xa8,0x08,0xa0,};
 const uint8_t* const _I_badusb_10px[] = {_I_badusb_10px_0};
 
@@ -55,6 +58,12 @@ const uint8_t* const _I_ibutt_10px[] = {_I_ibutt_10px_0};
 const uint8_t _I_ir_10px_0[] = {0x00,0xFC,0x00,0x02,0x01,0x79,0x02,0x84,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x58,0x00,0x78,0x00,0xFF,0x03,};
 const uint8_t* const _I_ir_10px[] = {_I_ir_10px_0};
 
+const uint8_t _I_loading_10px_0[] = {0x00,0xFE,0x00,0x82,0x00,0xBA,0x00,0x54,0x00,0x28,0x00,0x28,0x00,0x44,0x00,0x92,0x00,0xBA,0x00,0xFE,0x00,};
+const uint8_t* const _I_loading_10px[] = {_I_loading_10px_0};
+
+const uint8_t _I_music_10px_0[] = {0x01,0x00,0x10,0x00,0xf0,0x00,0x46,0x03,0x20,0x80,0x00,0x4e,0x7d,0x00,0x9f,0x80,0x4a,0x3c,0x13,0x20,};
+const uint8_t* const _I_music_10px[] = {_I_music_10px_0};
+
 const uint8_t _I_sub1_10px_0[] = {0x01,0x00,0x12,0x00,0x81,0x40,0x69,0x30,0x2c,0x2c,0x0b,0x6a,0x01,0x28,0x0c,0x0a,0x65,0x01,0x98,0x40,0x00,0x26,};
 const uint8_t* const _I_sub1_10px[] = {_I_sub1_10px_0};
 
@@ -648,11 +657,14 @@ const Icon A_Levelup1_128x64 = {.width=128,.height=64,.frame_count=11,.frame_rat
 const Icon A_Levelup2_128x64 = {.width=128,.height=64,.frame_count=11,.frame_rate=2,.frames=_A_Levelup2_128x64};
 const Icon I_125_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_125_10px};
 const Icon I_Nfc_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_Nfc_10px};
+const Icon I_back_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_back_10px};
 const Icon I_badusb_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_badusb_10px};
 const Icon I_ble_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_ble_10px};
 const Icon I_dir_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_dir_10px};
 const Icon I_ibutt_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_ibutt_10px};
 const Icon I_ir_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_ir_10px};
+const Icon I_loading_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_loading_10px};
+const Icon I_music_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_music_10px};
 const Icon I_sub1_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_sub1_10px};
 const Icon I_u2f_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_u2f_10px};
 const Icon I_unknown_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_unknown_10px};

+ 3 - 0
assets/compiled/assets_icons.h

@@ -7,11 +7,14 @@ extern const Icon A_Levelup1_128x64;
 extern const Icon A_Levelup2_128x64;
 extern const Icon I_125_10px;
 extern const Icon I_Nfc_10px;
+extern const Icon I_back_10px;
 extern const Icon I_badusb_10px;
 extern const Icon I_ble_10px;
 extern const Icon I_dir_10px;
 extern const Icon I_ibutt_10px;
 extern const Icon I_ir_10px;
+extern const Icon I_loading_10px;
+extern const Icon I_music_10px;
 extern const Icon I_sub1_10px;
 extern const Icon I_u2f_10px;
 extern const Icon I_unknown_10px;

BIN
assets/icons/Archive/back_10px.png


BIN
assets/icons/Archive/loading_10px.png


BIN
assets/icons/Archive/music_10px.png


+ 0 - 9
lib/one_wire/ibutton/ibutton_key.c

@@ -4,7 +4,6 @@
 
 struct iButtonKey {
     uint8_t data[IBUTTON_KEY_DATA_SIZE];
-    char name[IBUTTON_KEY_NAME_SIZE];
     iButtonKeyType type;
 };
 
@@ -42,14 +41,6 @@ uint8_t ibutton_key_get_data_size(iButtonKey* key) {
     return ibutton_key_get_size_by_type(key->type);
 }
 
-void ibutton_key_set_name(iButtonKey* key, const char* name) {
-    strlcpy(key->name, name, IBUTTON_KEY_NAME_SIZE);
-}
-
-const char* ibutton_key_get_name_p(iButtonKey* key) {
-    return key->name;
-}
-
 void ibutton_key_set_type(iButtonKey* key, iButtonKeyType key_type) {
     key->type = key_type;
 }

+ 0 - 14
lib/one_wire/ibutton/ibutton_key.h

@@ -68,20 +68,6 @@ const uint8_t* ibutton_key_get_data_p(iButtonKey* key);
  */
 uint8_t ibutton_key_get_data_size(iButtonKey* key);
 
-/**
- * Set key name
- * @param key 
- * @param name 
- */
-void ibutton_key_set_name(iButtonKey* key, const char* name);
-
-/**
- * Get pointer to key name
- * @param key 
- * @return const char* 
- */
-const char* ibutton_key_get_name_p(iButtonKey* key);
-
 /**
  * Set key type
  * @param key 

+ 14 - 0
lib/toolbox/path.c

@@ -19,6 +19,20 @@ void path_extract_filename_no_ext(const char* path, string_t filename) {
     string_mid(filename, start_position, end_position - start_position);
 }
 
+void path_extract_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);
+        }
+    }
+}
+
 static inline void path_cleanup(string_t path) {
     string_strim(path);
     while(string_end_with_str_p(path, "/")) {

+ 9 - 0
lib/toolbox/path.h

@@ -14,6 +14,15 @@ extern "C" {
  */
 void path_extract_filename_no_ext(const char* path, string_t filename);
 
+/**
+ * @brief Extract filename string from path.
+ * 
+ * @param path path string
+ * @param filename output filename string. Must be initialized before.
+ * @param trim_ext true - get filename without extension
+ */
+void path_extract_filename(string_t path, string_t filename, bool trim_ext);
+
 /**
  * @brief Extract last path component
  *