From bdb3aa9cef295a6a5d550d60b25def7ec84ad72a Mon Sep 17 00:00:00 2001 From: rdefeo Date: Sun, 24 Mar 2024 11:40:42 -0400 Subject: [PATCH] added Action settings menu --- .github/workflows/build.yml | 41 ++++++ .gitignore | 6 + actions/action.h | 2 + actions/action_rfid.c | 18 +-- item.h | 2 + quac.c | 47 +++--- quac.h | 16 ++- scenes/.gitignore | 6 + scenes/scene_action_create_group.c | 86 +++++++++++ scenes/scene_action_create_group.h | 8 ++ scenes/scene_action_rename.c | 104 ++++++++++++++ scenes/scene_action_rename.h | 8 ++ scenes/scene_action_settings.c | 223 +++++++++++++++++++++++++++++ scenes/scene_action_settings.h | 8 ++ scenes/scene_items.c | 30 +++- scenes/scene_settings.c | 2 +- scenes/scenes.c | 23 ++- scenes/scenes.h | 22 ++- views/action_menu.c | 48 +++---- 19 files changed, 621 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 scenes/.gitignore create mode 100644 scenes/scene_action_create_group.c create mode 100644 scenes/scene_action_create_group.h create mode 100644 scenes/scene_action_rename.c create mode 100644 scenes/scene_action_rename.h create mode 100644 scenes/scene_action_settings.c create mode 100644 scenes/scene_action_settings.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..143847c4a2e --- /dev/null +++ b/.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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..81a8981f739 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +dist/* +.vscode +.clang-format +.editorconfig +.env +.ufbt diff --git a/actions/action.h b/actions/action.h index 85825de6909..142c7e73cce 100644 --- a/actions/action.h +++ b/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); diff --git a/actions/action_rfid.c b/actions/action_rfid.c index ebca12603eb..797e10a84f5 100644 --- a/actions/action_rfid.c +++ b/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); diff --git a/item.h b/item.h index b5e8ad37634..ef3aa2f792f 100644 --- a/item.h +++ b/item.h @@ -2,6 +2,8 @@ #include +// 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 diff --git a/quac.c b/quac.c index 519d3b28d56..5192d9cdb37 100644 --- a/quac.c +++ b/quac.c @@ -1,21 +1,12 @@ #include -#include -#include -#include -#include -#include - -#include -#include +#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 @@ -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); diff --git a/quac.h b/quac.h index b2de7154927..daab01d59bc 100644 --- a/quac.h +++ b/quac.h @@ -1,9 +1,13 @@ #pragma once +#include #include #include -#include +#include #include +#include +#include +#include #include #include @@ -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 diff --git a/scenes/.gitignore b/scenes/.gitignore new file mode 100644 index 00000000000..81a8981f739 --- /dev/null +++ b/scenes/.gitignore @@ -0,0 +1,6 @@ +dist/* +.vscode +.clang-format +.editorconfig +.env +.ufbt diff --git a/scenes/scene_action_create_group.c b/scenes/scene_action_create_group.c new file mode 100644 index 00000000000..7489652465e --- /dev/null +++ b/scenes/scene_action_create_group.c @@ -0,0 +1,86 @@ +#include + +#include +#include +#include + +#include "quac.h" +#include "scenes.h" +#include "scene_action_create_group.h" +#include "../actions/action.h" + +#include + +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); +} \ No newline at end of file diff --git a/scenes/scene_action_create_group.h b/scenes/scene_action_create_group.h new file mode 100644 index 00000000000..ccaedb1b114 --- /dev/null +++ b/scenes/scene_action_create_group.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +// 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); diff --git a/scenes/scene_action_rename.c b/scenes/scene_action_rename.c new file mode 100644 index 00000000000..8b2fbf46ef5 --- /dev/null +++ b/scenes/scene_action_rename.c @@ -0,0 +1,104 @@ +#include + +#include +#include +#include + +#include "quac.h" +#include "scenes.h" +#include "scene_action_rename.h" +#include "../actions/action.h" + +#include + +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); +} \ No newline at end of file diff --git a/scenes/scene_action_rename.h b/scenes/scene_action_rename.h new file mode 100644 index 00000000000..df6626a2556 --- /dev/null +++ b/scenes/scene_action_rename.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +// 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); diff --git a/scenes/scene_action_settings.c b/scenes/scene_action_settings.c new file mode 100644 index 00000000000..7285a767dd2 --- /dev/null +++ b/scenes/scene_action_settings.c @@ -0,0 +1,223 @@ +#include + +#include +#include +#include +#include + +#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; +} \ No newline at end of file diff --git a/scenes/scene_action_settings.h b/scenes/scene_action_settings.h new file mode 100644 index 00000000000..c054f0b021a --- /dev/null +++ b/scenes/scene_action_settings.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +// 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); diff --git a/scenes/scene_items.c b/scenes/scene_items.c index 4d1b9eb6059..022d9761e95 100644 --- a/scenes/scene_items.c +++ b/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_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; diff --git a/scenes/scene_settings.c b/scenes/scene_settings.c index f24674c0395..44e5a0f1a3b 100644 --- a/scenes/scene_settings.c +++ b/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); diff --git a/scenes/scenes.c b/scenes/scenes.c index 13da3c259e3..2d6c9728dec 100644 --- a/scenes/scenes.c +++ b/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; diff --git a/scenes/scenes.h b/scenes/scenes.h index aba0774666d..d2dfdfd3456 100644 --- a/scenes/scenes.h +++ b/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); diff --git a/views/action_menu.c b/views/action_menu.c index f5011a30347..f6e47bd48be 100644 --- a/views/action_menu.c +++ b/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; }