Przeglądaj źródła

added Action settings menu

rdefeo 1 rok temu
rodzic
commit
bdb3aa9cef

+ 41 - 0
.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "branches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 2 - 0
actions/action.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#define EMPTY_ACTION_INDEX -1
+
 struct Item;
 
 void action_tx(void* context, Item* item, FuriString* error);

+ 9 - 9
actions/action_rfid.c

@@ -12,12 +12,15 @@
 #include "action_i.h"
 #include "quac.h"
 
+#define RFID_FILE_TYPE "Flipper RFID key"
+#define RFID_FILE_VERSION 1
+
 // lifted from flipperzero-firmware/applications/main/lfrfid/lfrfid_cli.c
 void action_rfid_tx(void* context, const FuriString* action_path, FuriString* error) {
     UNUSED(error);
 
     App* app = context;
-    const FuriString* file_name = action_path;
+    const char* file_name = furi_string_get_cstr(action_path);
 
     FlipperFormat* fff_data_file = flipper_format_file_alloc(app->storage);
     FuriString* temp_str;
@@ -32,22 +35,20 @@ void action_rfid_tx(void* context, const FuriString* action_path, FuriString* er
     // FURI_LOG_I(TAG, "Max dict data size is %d", data_size);
     bool successful_read = false;
     do {
-        if(!flipper_format_file_open_existing(fff_data_file, furi_string_get_cstr(file_name))) {
-            ACTION_SET_ERROR("RFID: Error opening %s", furi_string_get_cstr(file_name));
+        if(!flipper_format_file_open_existing(fff_data_file, file_name)) {
+            ACTION_SET_ERROR("RFID: Error opening %s", file_name);
             break;
         }
         if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) {
             ACTION_SET_ERROR("RFID: Missing or incorrect header");
             break;
         }
-        // FURI_LOG_I(TAG, "Read file headers");
-        // TODO: add better header checks here...
-        if(!strcmp(furi_string_get_cstr(temp_str), "Flipper RFID key")) {
+        if(!strcmp(furi_string_get_cstr(temp_str), RFID_FILE_TYPE) &&
+           temp_data32 == RFID_FILE_VERSION) {
         } else {
             ACTION_SET_ERROR("RFID: Type or version mismatch");
             break;
         }
-
         // read and check the protocol field
         if(!flipper_format_read_string(fff_data_file, "Key type", temp_str)) {
             ACTION_SET_ERROR("RFID: Error reading protocol");
@@ -90,8 +91,7 @@ void action_rfid_tx(void* context, const FuriString* action_path, FuriString* er
         lfrfid_worker_emulate_start(worker, protocol);
 
         int16_t time_ms = app->settings.rfid_duration;
-        FURI_LOG_I(
-            TAG, "RFID: Emulating RFID (%s) for %d ms", furi_string_get_cstr(file_name), time_ms);
+        FURI_LOG_I(TAG, "RFID: Emulating RFID (%s) for %d ms", file_name, time_ms);
         int16_t interval_ms = 100;
         while(time_ms > 0) {
             furi_delay_ms(interval_ms);

+ 2 - 0
item.h

@@ -2,6 +2,8 @@
 
 #include <m-array.h>
 
+// Max length of a filename, final path element only
+#define MAX_NAME_LEN 64
 #define MAX_EXT_LEN 6
 
 /** Defines an individual item action or item group. Each object contains

+ 27 - 20
quac.c

@@ -1,21 +1,12 @@
 #include <furi.h>
 
-#include <gui/gui.h>
-#include <gui/view_dispatcher.h>
-#include <gui/scene_manager.h>
-#include <gui/modules/dialog_ex.h>
-#include <gui/modules/variable_item_list.h>
-
-#include <storage/storage.h>
-#include <notification/notification_messages.h>
+#include "quac.h"
+#include "quac_settings.h"
 
 #include "item.h"
 #include "scenes/scenes.h"
 #include "scenes/scene_items.h"
 
-#include "quac.h"
-#include "quac_settings.h"
-
 /* generated by fbt from .png files in images folder */
 #include <quac_icons.h>
 
@@ -32,15 +23,21 @@ App* app_alloc() {
     // Main interface
     app->action_menu = action_menu_alloc();
     view_dispatcher_add_view(
-        app->view_dispatcher, Q_ActionMenu, action_menu_get_view(app->action_menu));
+        app->view_dispatcher, QView_ActionMenu, action_menu_get_view(app->action_menu));
 
     // App settings
     app->vil_settings = variable_item_list_alloc();
     view_dispatcher_add_view(
-        app->view_dispatcher, Q_Settings, variable_item_list_get_view(app->vil_settings));
+        app->view_dispatcher, QView_Settings, variable_item_list_get_view(app->vil_settings));
 
-    app->dialog = dialog_ex_alloc();
-    view_dispatcher_add_view(app->view_dispatcher, Q_Dialog, dialog_ex_get_view(app->dialog));
+    // Misc interfaces
+    app->sub_menu = submenu_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, QView_ActionSettings, submenu_get_view(app->sub_menu));
+
+    app->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, QView_ActionTextInput, text_input_get_view(app->text_input));
 
     // Storage
     app->storage = furi_record_open(RECORD_STORAGE);
@@ -48,10 +45,14 @@ App* app_alloc() {
     // Notifications - for LED light access
     app->notifications = furi_record_open(RECORD_NOTIFICATION);
 
+    app->dialog = furi_record_open(RECORD_DIALOGS);
+
     // data member initialize
     app->depth = 0;
     app->selected_item = -1;
 
+    app->temp_str = furi_string_alloc();
+
     return app;
 }
 
@@ -60,19 +61,24 @@ void app_free(App* app) {
 
     item_items_view_free(app->items_view);
 
-    view_dispatcher_remove_view(app->view_dispatcher, Q_ActionMenu);
-    view_dispatcher_remove_view(app->view_dispatcher, Q_Settings);
-    view_dispatcher_remove_view(app->view_dispatcher, Q_Dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, QView_ActionMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, QView_Settings);
+    view_dispatcher_remove_view(app->view_dispatcher, QView_ActionSettings);
+    view_dispatcher_remove_view(app->view_dispatcher, QView_ActionTextInput);
 
     action_menu_free(app->action_menu);
     variable_item_list_free(app->vil_settings);
-    dialog_ex_free(app->dialog);
+    submenu_free(app->sub_menu);
+    text_input_free(app->text_input);
 
     scene_manager_free(app->scene_manager);
     view_dispatcher_free(app->view_dispatcher);
 
+    furi_string_free(app->temp_str);
+
     furi_record_close(RECORD_STORAGE);
     furi_record_close(RECORD_NOTIFICATION);
+    furi_record_close(RECORD_DIALOGS);
 
     free(app);
 }
@@ -83,6 +89,7 @@ int32_t quac_app(void* p) {
     FURI_LOG_I(TAG, "QUAC! QUAC!");
 
     size_t free_start = memmgr_get_free_heap();
+    furi_assert(0);
 
     App* app = app_alloc();
     quac_load_settings(app);
@@ -92,7 +99,7 @@ int32_t quac_app(void* p) {
 
     Gui* gui = furi_record_open(RECORD_GUI);
     view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
-    scene_manager_next_scene(app->scene_manager, Q_Scene_Items);
+    scene_manager_next_scene(app->scene_manager, QScene_Items);
     view_dispatcher_run(app->view_dispatcher);
 
     furi_record_close(RECORD_GUI);

+ 14 - 2
quac.h

@@ -1,9 +1,13 @@
 #pragma once
 
+#include <gui/gui.h>
 #include <gui/scene_manager.h>
 #include <gui/view_dispatcher.h>
-#include <gui/modules/dialog_ex.h>
+#include <gui/scene_manager.h>
 #include <gui/modules/variable_item_list.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_input.h>
+#include <dialogs/dialogs.h>
 
 #include <storage/storage.h>
 #include <notification/notification_messages.h>
@@ -12,6 +16,9 @@
 
 #include "item.h"
 
+// #pragma GCC push_options
+// #pragma GCC optimize("O0")
+
 #define QUAC_NAME "Quac!"
 #define TAG "Quac" // log statement id
 
@@ -28,7 +35,9 @@ typedef struct App {
 
     ActionMenu* action_menu;
     VariableItemList* vil_settings;
-    DialogEx* dialog;
+    DialogsApp* dialog;
+    Submenu* sub_menu;
+    TextInput* text_input;
 
     Storage* storage;
     NotificationApp* notifications;
@@ -37,6 +46,9 @@ typedef struct App {
     int depth;
     int selected_item;
 
+    FuriString* temp_str; // used for renames/etc
+    char temp_cstr[MAX_NAME_LEN]; // used for renames/etc
+
     struct {
         QuacAppLayout layout; // Defaults to Portrait
         bool show_icons; // Defaults to True

+ 6 - 0
scenes/.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 86 - 0
scenes/scene_action_create_group.c

@@ -0,0 +1,86 @@
+#include <furi.h>
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/text_input.h>
+
+#include "quac.h"
+#include "scenes.h"
+#include "scene_action_create_group.h"
+#include "../actions/action.h"
+
+#include <lib/toolbox/path.h>
+
+enum {
+    SceneActionCreateGroupEvent,
+};
+
+void scene_action_create_group_callback(void* context) {
+    App* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, SceneActionCreateGroupEvent);
+}
+
+void scene_action_create_group_on_enter(void* context) {
+    App* app = context;
+    TextInput* text = app->text_input;
+
+    text_input_set_header_text(text, "Enter new group name:");
+
+    app->temp_cstr[0] = 0;
+    text_input_set_result_callback(
+        text, scene_action_create_group_callback, app, app->temp_cstr, MAX_NAME_LEN, false);
+
+    // TextInputValidatorCallback
+    // text_input_set_validator(text, validator_callback, context)
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, QView_ActionTextInput);
+}
+
+bool scene_action_create_group_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SceneActionCreateGroupEvent) {
+            // FURI_LOG_I(TAG, "Attempting to create group %s", app->temp_cstr);
+            if(!strcmp(app->temp_cstr, "")) {
+                return false;
+            }
+            Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+            FuriString* current_path = furi_string_alloc();
+            path_extract_dirname(furi_string_get_cstr(item->path), current_path);
+
+            FuriString* new_group_path = furi_string_alloc();
+            furi_string_printf(
+                new_group_path, "%s/%s", furi_string_get_cstr(current_path), app->temp_cstr);
+            // FURI_LOG_I(TAG, "Full new path: %s", furi_string_get_cstr(new_group_path));
+
+            FS_Error fs_result =
+                storage_common_mkdir(app->storage, furi_string_get_cstr(new_group_path));
+            if(fs_result == FSE_OK) {
+                ItemsView* new_items = item_get_items_view_from_path(app, current_path);
+                item_items_view_free(app->items_view);
+                app->items_view = new_items;
+            } else {
+                FURI_LOG_E(
+                    TAG, "Create Group failed! %s", filesystem_api_error_get_desc(fs_result));
+                FuriString* error_msg = furi_string_alloc_printf(
+                    "Create Group failed!\nError: %s", filesystem_api_error_get_desc(fs_result));
+                dialog_message_show_storage_error(app->dialog, furi_string_get_cstr(error_msg));
+                furi_string_free(error_msg);
+            }
+
+            furi_string_free(current_path);
+            furi_string_free(new_group_path);
+
+            scene_manager_search_and_switch_to_previous_scene(app->scene_manager, QScene_Items);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void scene_action_create_group_on_exit(void* context) {
+    App* app = context;
+    text_input_reset(app->text_input);
+}

+ 8 - 0
scenes/scene_action_create_group.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// For each scene, implement handler callbacks
+void scene_action_create_group_on_enter(void* context);
+bool scene_action_create_group_on_event(void* context, SceneManagerEvent event);
+void scene_action_create_group_on_exit(void* context);

+ 104 - 0
scenes/scene_action_rename.c

@@ -0,0 +1,104 @@
+#include <furi.h>
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/text_input.h>
+
+#include "quac.h"
+#include "scenes.h"
+#include "scene_action_rename.h"
+#include "../actions/action.h"
+
+#include <lib/toolbox/path.h>
+
+enum {
+    SceneActionRenameEvent,
+};
+
+void scene_action_rename_callback(void* context) {
+    App* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, SceneActionRenameEvent);
+}
+
+void scene_action_rename_on_enter(void* context) {
+    App* app = context;
+    TextInput* text = app->text_input;
+
+    Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+
+    text_input_set_header_text(text, "Enter new name:");
+
+    FuriString* file_name = furi_string_alloc();
+    path_extract_filename_no_ext(furi_string_get_cstr(item->path), file_name);
+    strncpy(app->temp_cstr, furi_string_get_cstr(file_name), MAX_NAME_LEN);
+
+    text_input_set_result_callback(
+        text, scene_action_rename_callback, app, app->temp_cstr, MAX_NAME_LEN, false);
+
+    furi_string_free(file_name);
+    view_dispatcher_switch_to_view(app->view_dispatcher, QView_ActionTextInput);
+}
+
+bool scene_action_rename_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SceneActionRenameEvent) {
+            // FURI_LOG_I(TAG, "Attempting rename to %s", app->temp_cstr);
+            if(!strcmp(app->temp_cstr, "")) {
+                return false;
+            }
+            Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+            const char* old_path = furi_string_get_cstr(item->path);
+
+            FuriString* file_name = furi_string_alloc();
+            path_extract_filename(item->path, file_name, true);
+            // FURI_LOG_I(TAG, "Original name is %s", furi_string_get_cstr(file_name));
+            if(!furi_string_cmp_str(file_name, app->temp_cstr)) {
+                // FURI_LOG_W(TAG, "Rename: File names are the same!");
+                furi_string_free(file_name);
+                return false;
+            }
+
+            // build the new name full path, with extension
+            FuriString* dir_name = furi_string_alloc();
+            path_extract_dirname(old_path, dir_name);
+            FuriString* new_path = furi_string_alloc_printf(
+                "%s/%s%s", furi_string_get_cstr(dir_name), app->temp_cstr, item->ext);
+
+            // FURI_LOG_I(TAG, "Rename: %s to %s", old_path, furi_string_get_cstr(new_path));
+            FS_Error fs_result =
+                storage_common_rename(app->storage, old_path, furi_string_get_cstr(new_path));
+            if(fs_result == FSE_OK) {
+                ItemsView* new_items = item_get_items_view_from_path(app, dir_name);
+                item_items_view_free(app->items_view);
+                app->items_view = new_items;
+                // furi_string_swap(item->path, new_path);
+                // furi_string_set_str(item->name, app->temp_cstr);
+                // item_prettify_name(item->name);
+            } else {
+                FURI_LOG_E(
+                    TAG, "Rename file failed! %s", filesystem_api_error_get_desc(fs_result));
+                FuriString* error_msg = furi_string_alloc_printf(
+                    "Rename failed!\nError: %s", filesystem_api_error_get_desc(fs_result));
+                dialog_message_show_storage_error(app->dialog, furi_string_get_cstr(error_msg));
+                furi_string_free(error_msg);
+            }
+
+            scene_manager_search_and_switch_to_previous_scene(app->scene_manager, QScene_Items);
+
+            furi_string_free(dir_name);
+            furi_string_free(file_name);
+            furi_string_free(new_path);
+
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void scene_action_rename_on_exit(void* context) {
+    App* app = context;
+    text_input_reset(app->text_input);
+}

+ 8 - 0
scenes/scene_action_rename.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// For each scene, implement handler callbacks
+void scene_action_rename_on_enter(void* context);
+bool scene_action_rename_on_event(void* context, SceneManagerEvent event);
+void scene_action_rename_on_exit(void* context);

+ 223 - 0
scenes/scene_action_settings.c

@@ -0,0 +1,223 @@
+#include <furi.h>
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <lib/toolbox/path.h>
+
+#include "quac.h"
+#include "scenes.h"
+#include "scene_action_settings.h"
+#include "../actions/action.h"
+#include "quac_icons.h"
+
+// Define different settings per Action
+typedef enum {
+    ActionSettingsRename, // Rename file or folder
+    ActionSettingsDelete, // Delete file or folder on SDcard
+    ActionSettingsImport, // Copy a remote file into "current" folder
+    ActionSettingsCreateGroup, // Create new empty folder in "current" folder
+    ActionSettingsCreatePlaylist, // Turn this folder into a playlist
+    ActionSettingsAddToPlaylist, // Append a remote file to this playlist
+} ActionSettingsIndex;
+
+// Delete the file of the currently selected item
+// Update items_view list before returning so that UI is updated and correct
+bool scene_action_settings_delete(App* app) {
+    bool success = false;
+    Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+
+    DialogMessage* dialog = dialog_message_alloc();
+    dialog_message_set_header(dialog, "Delete?", 64, 0, AlignCenter, AlignTop);
+    FuriString* text = furi_string_alloc();
+    furi_string_printf(text, "%s\n\n%s", furi_string_get_cstr(item->name), "Are you sure?");
+    dialog_message_set_text(dialog, furi_string_get_cstr(text), 64, 18, AlignCenter, AlignTop);
+    dialog_message_set_buttons(dialog, "Cancel", NULL, "OK");
+    DialogMessageButton button = dialog_message_show(app->dialog, dialog);
+
+    if(button == DialogMessageButtonRight) {
+        FuriString* current_path = furi_string_alloc();
+        path_extract_dirname(furi_string_get_cstr(item->path), current_path);
+
+        FS_Error fs_result = storage_common_remove(app->storage, furi_string_get_cstr(item->path));
+        if(fs_result == FSE_OK) {
+            success = true;
+            FURI_LOG_I(TAG, "Deleted file: %s", furi_string_get_cstr(item->path));
+            // ItemsView* new_items = item_get_items_view_from_path(app, current_path);
+            // item_items_view_free(app->items_view);
+            // app->items_view = new_items;
+        } else {
+            FURI_LOG_E(
+                TAG, "Error deleting file! Error=%s", filesystem_api_error_get_desc(fs_result));
+            FuriString* error_msg = furi_string_alloc();
+            furi_string_printf(
+                error_msg, "Delete failed!\nError: %s", filesystem_api_error_get_desc(fs_result));
+            dialog_message_show_storage_error(app->dialog, furi_string_get_cstr(error_msg));
+            furi_string_free(error_msg);
+        }
+
+        furi_string_free(current_path);
+    } else {
+        // FURI_LOG_I(TAG, "Used cancelled Delete");
+    }
+
+    furi_string_free(text);
+    dialog_message_free(dialog);
+    return success;
+}
+
+static bool scene_action_settings_import_file_browser_callback(
+    FuriString* path,
+    void* context,
+    uint8_t** icon,
+    FuriString* item_name) {
+    UNUSED(context);
+    UNUSED(item_name);
+    char ext[MAX_EXT_LEN];
+    path_extract_extension(path, ext, MAX_EXT_LEN);
+    if(!strcmp(ext, ".sub")) {
+        memcpy(*icon, icon_get_data(&I_SubGHz_10px), 32); // TODO: find the right size!
+    } else if(!strcmp(ext, ".rfid")) {
+        memcpy(*icon, icon_get_data(&I_RFID_10px), 32);
+    } else if(!strcmp(ext, ".ir")) {
+        memcpy(*icon, icon_get_data(&I_IR_10px), 32);
+    } else if(!strcmp(ext, ".qpl")) {
+        memcpy(*icon, icon_get_data(&I_Playlist_10px), 32);
+    }
+    return true;
+}
+
+// Import a file from elsewhere on the SD card
+// Update items_view list before returning so that UI is updated and correct
+bool scene_action_settings_import(App* app) {
+    bool success = false;
+    FuriString* current_path = furi_string_alloc();
+    if(app->selected_item != EMPTY_ACTION_INDEX) {
+        Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+        path_extract_dirname(furi_string_get_cstr(item->path), current_path);
+    } else {
+        furi_string_set(current_path, app->items_view->path);
+    }
+
+    // Setup our file browser options
+    DialogsFileBrowserOptions fb_options;
+    dialog_file_browser_set_basic_options(&fb_options, "", NULL);
+    fb_options.base_path = furi_string_get_cstr(current_path);
+    fb_options.skip_assets = true;
+    furi_string_set_str(app->temp_str, fb_options.base_path);
+    fb_options.item_loader_callback = scene_action_settings_import_file_browser_callback;
+    fb_options.item_loader_context = app;
+
+    if(dialog_file_browser_show(app->dialog, app->temp_str, app->temp_str, &fb_options)) {
+        // FURI_LOG_I(TAG, "Selected file is %s", furi_string_get_cstr(app->temp_str));
+        FuriString* file_name = furi_string_alloc();
+        path_extract_filename(app->temp_str, file_name, false);
+        // FURI_LOG_I(TAG, "Importing file %s", furi_string_get_cstr(file_name));
+        FuriString* full_path;
+        full_path = furi_string_alloc_printf(
+            "%s/%s", furi_string_get_cstr(current_path), furi_string_get_cstr(file_name));
+        // FURI_LOG_I(TAG, "New path is %s", furi_string_get_cstr(full_path));
+
+        FS_Error fs_result = storage_common_copy(
+            app->storage, furi_string_get_cstr(app->temp_str), furi_string_get_cstr(full_path));
+        if(fs_result == FSE_OK) {
+            success = true;
+            // FURI_LOG_I(TAG, "File copied / updating items view list");
+            // ItemsView* new_items = item_get_items_view_from_path(app, current_path);
+            // item_items_view_free(app->items_view);
+            // app->items_view = new_items;
+        } else {
+            FURI_LOG_E(TAG, "File copy failed! %s", filesystem_api_error_get_desc(fs_result));
+            FuriString* error_msg = furi_string_alloc_printf(
+                "File copy failed!\nError: %s", filesystem_api_error_get_desc(fs_result));
+            dialog_message_show_storage_error(app->dialog, furi_string_get_cstr(error_msg));
+            furi_string_free(error_msg);
+        }
+        furi_string_free(file_name);
+        furi_string_free(full_path);
+    } else {
+        // FURI_LOG_I(TAG, "User cancelled");
+    }
+
+    furi_string_free(current_path);
+    return success;
+}
+
+// Prompt user for the name of the new Group
+// Update items_view list before returning so that UI is updated and correct
+bool scene_action_settings_create_group(App* app) {
+    UNUSED(app);
+    return false;
+}
+
+void scene_action_settings_callback(void* context, uint32_t index) {
+    App* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void scene_action_settings_on_enter(void* context) {
+    App* app = context;
+
+    Submenu* menu = app->sub_menu;
+    submenu_reset(menu);
+
+    if(app->selected_item >= 0) {
+        Item* item = ItemArray_get(app->items_view->items, app->selected_item);
+        submenu_set_header(menu, furi_string_get_cstr(item->name));
+
+        submenu_add_item(
+            menu, "Rename", ActionSettingsRename, scene_action_settings_callback, app);
+        submenu_add_item(
+            menu, "Delete", ActionSettingsDelete, scene_action_settings_callback, app);
+    } else {
+        submenu_set_header(menu, furi_string_get_cstr(app->items_view->name));
+    }
+
+    submenu_add_item(
+        menu, "Import Here", ActionSettingsImport, scene_action_settings_callback, app);
+    submenu_add_item(
+        menu, "Create Group", ActionSettingsCreateGroup, scene_action_settings_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, QView_ActionSettings);
+}
+
+bool scene_action_settings_on_event(void* context, SceneManagerEvent event) {
+    App* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case ActionSettingsRename:
+            consumed = true;
+            scene_manager_next_scene(app->scene_manager, QScene_ActionRename);
+            break;
+        case ActionSettingsDelete:
+            consumed = true;
+            if(scene_action_settings_delete(app)) {
+                scene_manager_previous_scene(app->scene_manager);
+            }
+            break;
+        case ActionSettingsImport:
+            consumed = true;
+            if(scene_action_settings_import(app)) {
+                scene_manager_previous_scene(app->scene_manager);
+            }
+            break;
+        case ActionSettingsCreateGroup:
+            consumed = true;
+            scene_manager_next_scene(app->scene_manager, QScene_ActionCreateGroup);
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void scene_action_settings_on_exit(void* context) {
+    App* app = context;
+    submenu_reset(app->sub_menu);
+
+    // Rebuild our list on exit, to pick up any renames
+    ItemsView* new_items = item_get_items_view_from_path(app, app->items_view->path);
+    item_items_view_free(app->items_view);
+    app->items_view = new_items;
+}

+ 8 - 0
scenes/scene_action_settings.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// For each scene, implement handler callbacks
+void scene_action_settings_on_enter(void* context);
+bool scene_action_settings_on_event(void* context, SceneManagerEvent event);
+void scene_action_settings_on_exit(void* context);

+ 23 - 7
scenes/scene_items.c

@@ -26,9 +26,14 @@ static const ActionMenuItemType ItemToMenuItem[] = {
 void scene_items_item_callback(void* context, int32_t index, InputType type) {
     App* app = context;
 
-    if(type == InputTypeShort || type == InputTypeRelease) {
+    // FURI_LOG_I(TAG, "scene_items callback, type == %s", input_get_type_name(type));
+
+    if(type == InputTypeShort) {
         app->selected_item = index;
         view_dispatcher_send_custom_event(app->view_dispatcher, Event_ButtonPressed);
+    } else if(type == InputTypeLong) {
+        app->selected_item = index;
+        view_dispatcher_send_custom_event(app->view_dispatcher, Event_ButtonPressedLong);
     } else {
         // do nothing
     }
@@ -65,7 +70,14 @@ void scene_items_on_enter(void* context) {
         }
     } else {
         FURI_LOG_W(TAG, "No items for: %s", furi_string_get_cstr(items_view->path));
-        // TODO: Display Error popup? Empty folder?
+        // Add a bogus item - this lets the user still access the Action menu to import, etc
+        action_menu_add_item(
+            menu,
+            "<Empty>",
+            EMPTY_ACTION_INDEX,
+            scene_items_item_callback,
+            ActionMenuItemTypeGroup,
+            app);
     }
 
     // Always add the "Settings" item at the end of our list - but only at top level!
@@ -79,7 +91,7 @@ void scene_items_on_enter(void* context) {
             app);
     }
 
-    view_dispatcher_switch_to_view(app->view_dispatcher, Q_ActionMenu);
+    view_dispatcher_switch_to_view(app->view_dispatcher, QView_ActionMenu);
 }
 bool scene_items_on_event(void* context, SceneManagerEvent event) {
     App* app = context;
@@ -87,9 +99,8 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
 
     switch(event.type) {
     case SceneManagerEventTypeCustom:
-        if(event.event == Event_ButtonPressed) {
+        if(event.event == Event_ButtonPressed && app->selected_item != EMPTY_ACTION_INDEX) {
             consumed = true;
-            // furi_delay_ms(100);
             // FURI_LOG_I(TAG, "button pressed is %d", app->selected_item);
             if(app->selected_item < (int)ItemArray_size(app->items_view->items)) {
                 Item* item = ItemArray_get(app->items_view->items, app->selected_item);
@@ -98,7 +109,7 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
                     ItemsView* new_items = item_get_items_view_from_path(app, item->path);
                     item_items_view_free(app->items_view);
                     app->items_view = new_items;
-                    scene_manager_next_scene(app->scene_manager, Q_Scene_Items);
+                    scene_manager_next_scene(app->scene_manager, QScene_Items);
                 } else {
                     FURI_LOG_I(
                         TAG, "Initiating item action: %s", furi_string_get_cstr(item->name));
@@ -129,7 +140,12 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) {
             } else {
                 // FURI_LOG_I(TAG, "Selected Settings!");
                 // TODO: Do we need to free this current items_view??
-                scene_manager_next_scene(app->scene_manager, Q_Scene_Settings);
+                scene_manager_next_scene(app->scene_manager, QScene_Settings);
+            }
+        } else if(event.event == Event_ButtonPressedLong) {
+            if(app->selected_item < (int)ItemArray_size(app->items_view->items)) {
+                consumed = true;
+                scene_manager_next_scene(app->scene_manager, QScene_ActionSettings);
             }
         }
         break;

+ 1 - 1
scenes/scene_settings.c

@@ -109,7 +109,7 @@ void scene_settings_on_enter(void* context) {
     // TODO: Set Enter callback here - why?? All settings have custom callbacks
     // variable_item_list_set_enter_callback(vil, my_cb, app);
 
-    view_dispatcher_switch_to_view(app->view_dispatcher, Q_Settings);
+    view_dispatcher_switch_to_view(app->view_dispatcher, QView_Settings);
 }
 bool scene_settings_on_event(void* context, SceneManagerEvent event) {
     UNUSED(context);

+ 20 - 3
scenes/scenes.c

@@ -4,22 +4,39 @@
 #include "scenes.h"
 #include "scene_items.h"
 #include "scene_settings.h"
+#include "scene_action_settings.h"
+#include "scene_action_rename.h"
+#include "scene_action_create_group.h"
 
 // define handler callbacks - order must match appScenes enum!
 void (*const app_on_enter_handlers[])(void* context) = {
     scene_items_on_enter,
-    scene_settings_on_enter};
+    scene_settings_on_enter,
+    scene_action_settings_on_enter,
+    scene_action_rename_on_enter,
+    scene_action_create_group_on_enter,
+};
 bool (*const app_on_event_handlers[])(void* context, SceneManagerEvent event) = {
     scene_items_on_event,
     scene_settings_on_event,
+    scene_action_settings_on_event,
+    scene_action_rename_on_event,
+    scene_action_create_group_on_event,
+
+};
+void (*const app_on_exit_handlers[])(void* context) = {
+    scene_items_on_exit,
+    scene_settings_on_exit,
+    scene_action_settings_on_exit,
+    scene_action_rename_on_exit,
+    scene_action_create_group_on_exit,
 };
-void (*const app_on_exit_handlers[])(void* context) = {scene_items_on_exit, scene_settings_on_exit};
 
 const SceneManagerHandlers app_scene_handlers = {
     .on_enter_handlers = app_on_enter_handlers,
     .on_event_handlers = app_on_event_handlers,
     .on_exit_handlers = app_on_exit_handlers,
-    .scene_num = Q_Scene_count};
+    .scene_num = QScene_count};
 
 bool app_scene_custom_callback(void* context, uint32_t custom_event_id) {
     App* app = context;

+ 17 - 5
scenes/scenes.h

@@ -1,14 +1,26 @@
 #pragma once
 
-typedef enum { Q_Scene_Items, Q_Scene_Settings, Q_Scene_count } appScenes;
+typedef enum {
+    QScene_Items,
+    QScene_Settings,
+    QScene_ActionSettings,
+    QScene_ActionRename,
+    QScene_ActionCreateGroup,
+    QScene_count
+} appScenes;
 
 typedef enum {
-    Q_ActionMenu, // new UI,
-    Q_Settings, // Variable Item List for settings
-    Q_Dialog, // TODO: shows errors
+    QView_ActionMenu, // new UI,
+    QView_Settings, // Variable Item List for settings
+    QView_ActionSettings, // [SubMenu] Action: Rename, Delete, Import (copies from elsewhere)
+    QView_ActionTextInput, // Action: Rename, Create Group
 } appView;
 
-typedef enum { Event_DeviceSelected, Event_ButtonPressed } AppCustomEvents;
+typedef enum {
+    Event_DeviceSelected,
+    Event_ButtonPressed,
+    Event_ButtonPressedLong
+} AppCustomEvents;
 
 extern void (*const app_on_enter_handlers[])(void*);
 extern bool (*const app_on_event_handlers[])(void*, SceneManagerEvent);

+ 16 - 32
views/action_menu.c

@@ -42,7 +42,6 @@ ARRAY_DEF(ActionMenuItemArray, ActionMenuItem, M_POD_OPLIST);
 
 struct ActionMenu {
     View* view;
-    bool freeze_input;
 };
 
 typedef struct {
@@ -263,6 +262,7 @@ static void action_menu_process_down(ActionMenu* action_menu) {
         true);
 }
 
+// Used for both the Short and Long presses of OK
 static void action_menu_process_ok(ActionMenu* action_menu, InputType type) {
     furi_assert(action_menu);
 
@@ -275,16 +275,12 @@ static void action_menu_process_ok(ActionMenu* action_menu, InputType type) {
         {
             if(model->position < (ActionMenuItemArray_size(model->items))) {
                 item = ActionMenuItemArray_get(model->items, model->position);
+                if(item->callback) {
+                    item->callback(item->callback_context, item->index, type);
+                }
             }
         },
         false);
-
-    // Landscape: Press, Short, Release
-
-    if(item) {
-        if(type == InputTypeRelease && item->callback)
-            item->callback(item->callback_context, item->index, type);
-    }
 }
 
 static bool action_menu_view_input_callback(InputEvent* event, void* context) {
@@ -293,22 +289,12 @@ static bool action_menu_view_input_callback(InputEvent* event, void* context) {
     ActionMenu* action_menu = context;
     bool consumed = false;
 
-    // Item selection
-    if(event->key == InputKeyOk) {
-        if((event->type == InputTypeRelease) || (event->type == InputTypePress)) {
-            consumed = true;
-            action_menu->freeze_input = (event->type == InputTypePress);
-            action_menu_process_ok(action_menu, event->type);
-        } else if(event->type == InputTypeShort) {
+    if(event->type == InputTypeShort) {
+        switch(event->key) {
+        case InputKeyOk:
             consumed = true;
             action_menu_process_ok(action_menu, event->type);
-        }
-    }
-
-    if(!action_menu->freeze_input &&
-       ((event->type == InputTypeRepeat) || (event->type == InputTypeShort))) {
-        // FURI_LOG_I("AM", "Directional key: %d", event->key);
-        switch(event->key) {
+            break;
         case InputKeyUp:
             consumed = true;
             action_menu_process_up(action_menu);
@@ -317,18 +303,17 @@ static bool action_menu_view_input_callback(InputEvent* event, void* context) {
             consumed = true;
             action_menu_process_down(action_menu);
             break;
-        case InputKeyRight:
-            FURI_LOG_W("AM", "InputKeyRight ignored");
-            // consumed = true;
-            // action_menu_process_right(action_menu);
-            break;
         case InputKeyLeft:
-            FURI_LOG_W("AM", "InputKeyLeft ignored");
-            // consumed = true;
-            // action_menu_process_left(action_menu);
             break;
-        default:
+        case InputKeyRight:
             break;
+        default:
+            FURI_LOG_E("AM", "Unknown key!");
+        }
+    } else if(event->type == InputTypeLong) {
+        if(event->key == InputKeyRight) {
+            consumed = true;
+            action_menu_process_ok(action_menu, event->type);
         }
     }
 
@@ -454,7 +439,6 @@ ActionMenu* action_menu_alloc(void) {
         },
         true);
 
-    action_menu->freeze_input = false;
     return action_menu;
 }