diff --git a/applications/main/archive/helpers/archive_browser.c b/applications/main/archive/helpers/archive_browser.c index ef39ca802..ec2630152 100644 --- a/applications/main/archive/helpers/archive_browser.c +++ b/applications/main/archive/helpers/archive_browser.c @@ -86,12 +86,28 @@ static void break; } } + furi_string_free(selected); } if(model->item_idx < 0) { model->item_idx = 0; } } + + // Files lose their selected stateafter re entering, so we need to restore them + if(model->select_mode && model->selected_count > 0) { + for(size_t i = 0; i < files_array_size(model->files); i++) { + ArchiveFile_t* file = files_array_get(model->files, i); + file->selected = false; + for(size_t j = 0; j < model->selected_count; j++) { + if(furi_string_cmp(model->selected_files[j], file->path) == 0) { + file->selected = true; + break; + } + } + } + } + if(archive_is_file_list_load_required(model)) { model->list_loading = true; load_again = true; @@ -141,6 +157,28 @@ void archive_file_browser_set_path( browser->override_home_path = override_home_path; } +bool archive_is_parent_or_identical(const char* path_a, const char* path_b) { + size_t len_a = strlen(path_a); + return ( + strncmp(path_b, path_a, len_a) == 0 && (path_b[len_a] == '/' || path_b[len_a] == '\0')); +} + +bool archive_is_nested_path(const char* dst_path, char** clipboard_paths, size_t clipboard_count) { + if(!dst_path || !clipboard_paths) { + return false; + } + + for(size_t i = 0; i < clipboard_count; i++) { + if(!clipboard_paths[i]) continue; + const char* src_path = clipboard_paths[i]; + if(archive_is_parent_or_identical(src_path, dst_path) && strcmp(src_path, dst_path) != 0) { + return true; + } + } + + return false; +} + bool archive_is_item_in_array(ArchiveBrowserViewModel* model, uint32_t idx) { size_t array_size = files_array_size(model->files); @@ -695,3 +733,33 @@ void archive_refresh_dir(ArchiveBrowserView* browser) { file_browser_worker_folder_refresh_sel(browser->worker, furi_string_get_cstr(str)); furi_string_free(str); } + +void archive_clear_selection(ArchiveBrowserViewModel* model) { + model->select_mode = false; + for(size_t i = 0; i < model->selected_count; i++) { + furi_string_free(model->selected_files[i]); + } + free(model->selected_files); + model->selected_files = NULL; + model->selected_count = 0; + + for(size_t i = 0; i < files_array_size(model->files); i++) { + ArchiveFile_t* file = files_array_get(model->files, i); + file->selected = false; + } +} + +void archive_deselect_children(ArchiveBrowserViewModel* model, const char* parent) { + size_t write_idx = 0; + for(size_t i = 0; i < model->selected_count; i++) { + if(!furi_string_start_with(model->selected_files[i], parent)) { + if(write_idx != i) { + model->selected_files[write_idx] = model->selected_files[i]; + } + write_idx++; + } else { + furi_string_free(model->selected_files[i]); + } + } + model->selected_count = write_idx; +} diff --git a/applications/main/archive/helpers/archive_browser.h b/applications/main/archive/helpers/archive_browser.h index 2378c53b2..ba3510ca1 100644 --- a/applications/main/archive/helpers/archive_browser.h +++ b/applications/main/archive/helpers/archive_browser.h @@ -90,6 +90,8 @@ void archive_file_browser_set_path( bool skip_assets, bool hide_dot_files, const char* override_home_path); +bool archive_is_parent_or_identical(const char* path_a, const char* path_b); +bool archive_is_nested_path(const char* dst_path, char** clipboard_paths, size_t clipboard_count); bool archive_is_item_in_array(ArchiveBrowserViewModel* model, uint32_t idx); bool archive_is_file_list_load_required(ArchiveBrowserViewModel* model); void archive_update_offset(ArchiveBrowserView* browser); @@ -119,3 +121,6 @@ void archive_switch_tab(ArchiveBrowserView* browser, InputKey key); void archive_enter_dir(ArchiveBrowserView* browser, FuriString* name); void archive_leave_dir(ArchiveBrowserView* browser); void archive_refresh_dir(ArchiveBrowserView* browser); + +void archive_clear_selection(ArchiveBrowserViewModel* model); +void archive_deselect_children(ArchiveBrowserViewModel* model, const char* parent); diff --git a/applications/main/archive/helpers/archive_files.h b/applications/main/archive/helpers/archive_files.h index a33313284..65e3beb13 100644 --- a/applications/main/archive/helpers/archive_files.h +++ b/applications/main/archive/helpers/archive_files.h @@ -43,6 +43,7 @@ typedef struct { FuriString* custom_name; bool fav; bool is_app; + bool selected; } ArchiveFile_t; static void ArchiveFile_t_init(ArchiveFile_t* obj) { @@ -52,6 +53,7 @@ static void ArchiveFile_t_init(ArchiveFile_t* obj) { obj->custom_name = furi_string_alloc(); obj->fav = false; obj->is_app = false; + obj->selected = false; } static void ArchiveFile_t_init_set(ArchiveFile_t* obj, const ArchiveFile_t* src) { @@ -66,6 +68,7 @@ static void ArchiveFile_t_init_set(ArchiveFile_t* obj, const ArchiveFile_t* src) obj->custom_name = furi_string_alloc_set(src->custom_name); obj->fav = src->fav; obj->is_app = src->is_app; + obj->selected = false; } static void ArchiveFile_t_set(ArchiveFile_t* obj, const ArchiveFile_t* src) { @@ -80,6 +83,7 @@ static void ArchiveFile_t_set(ArchiveFile_t* obj, const ArchiveFile_t* src) { furi_string_set(obj->custom_name, src->custom_name); obj->fav = src->fav; obj->is_app = src->is_app; + obj->selected = false; } static void ArchiveFile_t_clear(ArchiveFile_t* obj) { diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index 6042c8f32..d7a8c3c40 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -301,6 +301,73 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneInfo); consumed = true; break; + case ArchiveBrowserEventFileMenuSelectMode: + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + if(!model->select_mode) { + model->select_mode = true; + if(model->selected_files == NULL) { + model->selected_files = malloc(sizeof(FuriString*) * 50); + model->selected_count = 0; + } + + ArchiveFile_t* current = archive_get_current_file(browser); + model->selected_files[model->selected_count] = + furi_string_alloc_set(current->path); + model->selected_count++; + current->selected = true; + } else { + archive_clear_selection(model); + } + }, + true); + archive_show_file_menu(browser, false, false); + break; + case ArchiveBrowserEventFileSelect: + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + ArchiveFile_t* current = archive_get_current_file(browser); + if(!current->selected) { + // If current file type is a folder, deselect all files that start with the same path to not have a conflict. + if(current->type == ArchiveFileTypeFolder) { + archive_deselect_children(model, furi_string_get_cstr(current->path)); + } + model->selected_files[model->selected_count] = + furi_string_alloc_set(current->path); + model->selected_count++; + current->selected = true; + } + }, + true); + break; + case ArchiveBrowserEventFileDeselect: + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + ArchiveFile_t* current = archive_get_current_file(browser); + if(!current->selected && current->type == ArchiveFileTypeFolder) { + archive_deselect_children(model, furi_string_get_cstr(current->path)); + } else { + for(size_t i = 0; i < model->selected_count; i++) { + if(furi_string_cmp(model->selected_files[i], current->path) == 0) { + furi_string_free(model->selected_files[i]); + for(size_t j = i; j < model->selected_count - 1; j++) { + model->selected_files[j] = model->selected_files[j + 1]; + } + model->selected_count--; + current->selected = false; + break; + } + } + } + }, + true); + break; case ArchiveBrowserEventFileMenuShow: if(selected->type == ArchiveFileTypeDiskImage && archive_get_tab(browser) != ArchiveTabDiskImage) { @@ -316,75 +383,83 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) { case ArchiveBrowserEventFileMenuPaste: archive_show_file_menu(browser, false, false); if(!favorites) { - FuriString* path_src = NULL; - FuriString* path_dst = NULL; - bool copy; + bool show_nested_error = false; with_view_model( browser->view, ArchiveBrowserViewModel * model, { if(model->clipboard != NULL) { - path_src = furi_string_alloc_set(model->clipboard); - path_dst = furi_string_alloc(); - FuriString* base = furi_string_alloc(); - path_extract_basename(model->clipboard, base); - path_concat( - furi_string_get_cstr(browser->path), - furi_string_get_cstr(base), - path_dst); - furi_string_free(base); - copy = model->clipboard_copy; + for(size_t i = 0; i < model->clipboard_count; i++) { + FuriString* path_src = furi_string_alloc_set(model->clipboard[i]); + FuriString* path_dst = furi_string_alloc(); + FuriString* base = furi_string_alloc(); + path_extract_basename(model->clipboard[i], base); + path_concat( + furi_string_get_cstr(browser->path), + furi_string_get_cstr(base), + path_dst); + + if(archive_is_nested_path( + furi_string_get_cstr(path_dst), + model->clipboard, + model->clipboard_count)) { + show_nested_error = true; + furi_string_free(path_src); + furi_string_free(path_dst); + furi_string_free(base); + break; + } + + view_dispatcher_switch_to_view( + archive->view_dispatcher, ArchiveViewStack); + archive_show_loading_popup(archive, true); + FS_Error error = archive_copy_rename_file_or_dir( + archive->browser, + furi_string_get_cstr(path_src), + path_dst, + model->clipboard_copy, + true); + archive_show_loading_popup(archive, false); + + if(error != FSE_OK) { + FuriString* dialog_msg = furi_string_alloc(); + furi_string_cat_printf( + dialog_msg, + "Cannot %s:\n%s", + model->clipboard_copy ? "copy" : "move", + storage_error_get_desc(error)); + dialog_message_show_storage_error( + archive->dialogs, furi_string_get_cstr(dialog_msg)); + furi_string_free(dialog_msg); + } + + furi_string_free(path_src); + furi_string_free(path_dst); + furi_string_free(base); + } + + for(size_t i = 0; i < model->clipboard_count; i++) { + free(model->clipboard[i]); + } free(model->clipboard); model->clipboard = NULL; + model->clipboard_count = 0; } }, false); - if(path_src && path_dst) { - view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewStack); - archive_show_loading_popup(archive, true); - FS_Error error = archive_copy_rename_file_or_dir( - archive->browser, furi_string_get_cstr(path_src), path_dst, copy, true); - archive_show_loading_popup(archive, false); - if(error != FSE_OK) { - FuriString* dialog_msg; - dialog_msg = furi_string_alloc(); - furi_string_cat_printf( - dialog_msg, - "Cannot %s:\n%s", - copy ? "copy" : "move", - storage_error_get_desc(error)); - dialog_message_show_storage_error( - archive->dialogs, furi_string_get_cstr(dialog_msg)); - furi_string_free(dialog_msg); - } else { - ArchiveFile_t* current = archive_get_current_file(archive->browser); - if(current != NULL) furi_string_set(current->path, path_dst); - view_dispatcher_send_custom_event( - archive->view_dispatcher, ArchiveBrowserEventListRefresh); - } - furi_string_free(path_src); - furi_string_free(path_dst); - view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewBrowser); + + if(show_nested_error) { + dialog_message_show_storage_error( + archive->dialogs, "Cannot paste into\nchild folder"); } + + view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewBrowser); + view_dispatcher_send_custom_event( + archive->view_dispatcher, ArchiveBrowserEventListRefresh); } consumed = true; break; case ArchiveBrowserEventFileMenuCut: - archive_show_file_menu(browser, false, false); - if(!favorites) { - with_view_model( - browser->view, - ArchiveBrowserViewModel * model, - { - if(model->clipboard == NULL) { - model->clipboard = strdup(furi_string_get_cstr(selected->path)); - model->clipboard_copy = false; - } - }, - false); - } - consumed = true; - break; case ArchiveBrowserEventFileMenuCopy: archive_show_file_menu(browser, false, false); if(!favorites) { @@ -393,8 +468,22 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) { ArchiveBrowserViewModel * model, { if(model->clipboard == NULL) { - model->clipboard = strdup(furi_string_get_cstr(selected->path)); - model->clipboard_copy = true; + if(model->select_mode) { + model->clipboard = + malloc(sizeof(FuriString*) * model->selected_count); + model->clipboard_count = model->selected_count; + for(size_t i = 0; i < model->selected_count; i++) { + model->clipboard[i] = + strdup(furi_string_get_cstr(model->selected_files[i])); + } + archive_clear_selection(model); + } else { + model->clipboard = malloc(sizeof(FuriString*)); + model->clipboard[0] = strdup(furi_string_get_cstr(selected->path)); + model->clipboard_count = 1; + } + model->clipboard_copy = + (event.event == ArchiveBrowserEventFileMenuCopy); } }, false); diff --git a/applications/main/archive/scenes/archive_scene_delete.c b/applications/main/archive/scenes/archive_scene_delete.c index 217e4a490..dd0c37011 100644 --- a/applications/main/archive/scenes/archive_scene_delete.c +++ b/applications/main/archive/scenes/archive_scene_delete.c @@ -15,41 +15,58 @@ void archive_scene_delete_widget_callback(GuiButtonType result, InputType type, void archive_scene_delete_on_enter(void* context) { furi_assert(context); ArchiveApp* app = (ArchiveApp*)context; + ArchiveBrowserView* browser = app->browser; widget_add_button_element( app->widget, GuiButtonTypeLeft, "Cancel", archive_scene_delete_widget_callback, app); widget_add_button_element( app->widget, GuiButtonTypeRight, "Delete", archive_scene_delete_widget_callback, app); - FuriString* filename; - filename = furi_string_alloc(); - - ArchiveFile_t* current = archive_get_current_file(app->browser); - - FuriString* filename_no_ext = furi_string_alloc(); - path_extract_filename(current->path, filename_no_ext, true); - strlcpy(app->text_store, furi_string_get_cstr(filename_no_ext), MAX_NAME_LEN); - furi_string_free(filename_no_ext); - - path_extract_filename(current->path, filename, false); - char delete_str[64]; - snprintf(delete_str, sizeof(delete_str), "\e#Delete %s?\e#", furi_string_get_cstr(filename)); + + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + if(model->select_mode && model->selected_count > 1) { + snprintf( + delete_str, + sizeof(delete_str), + "\e#Delete %d files?\e#", + model->selected_count); + widget_add_file_list_element( + app->widget, + 0, + 23, + 3, + model->selected_files, + model->selected_count, + 14, + FRAME_HEIGHT * 3, + false); + } else { + ArchiveFile_t* current = archive_get_current_file(browser); + FuriString* filename = furi_string_alloc(); + path_extract_filename(current->path, filename, false); + snprintf( + delete_str, + sizeof(delete_str), + "\e#Delete %s?\e#", + furi_string_get_cstr(filename)); + furi_string_free(filename); + } + }, + false); + widget_add_text_box_element( app->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, delete_str, false); - - furi_string_free(filename); - view_dispatcher_switch_to_view(app->view_dispatcher, ArchiveViewWidget); } bool archive_scene_delete_on_event(void* context, SceneManagerEvent event) { furi_assert(context); ArchiveApp* app = (ArchiveApp*)context; - ArchiveBrowserView* browser = app->browser; - ArchiveFile_t* selected = archive_get_current_file(browser); - const char* name = archive_get_name(browser); if(event.type == SceneManagerEventTypeCustom) { if(event.event == GuiButtonTypeRight) { @@ -57,11 +74,28 @@ bool archive_scene_delete_on_event(void* context, SceneManagerEvent event) { view_dispatcher_switch_to_view(app->view_dispatcher, ArchiveViewStack); archive_show_loading_popup(app, true); - if(selected->is_app) { - archive_app_delete_file(browser, name); - } else { - archive_delete_file(browser, "%s", name); - } + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + if(model->select_mode && model->selected_count > 0) { + for(size_t i = 0; i < model->selected_count; i++) { + archive_delete_file( + browser, "%s", furi_string_get_cstr(model->selected_files[i])); + } + archive_clear_selection(model); + } else { + ArchiveFile_t* selected = archive_get_current_file(browser); + const char* name = archive_get_name(browser); + if(selected->is_app) { + archive_app_delete_file(browser, name); + } else { + archive_delete_file(browser, "%s", name); + } + } + }, + false); + archive_show_loading_popup(app, false); return scene_manager_previous_scene(app->scene_manager); } else if(event.event == GuiButtonTypeLeft) { diff --git a/applications/main/archive/scenes/archive_scene_rename.c b/applications/main/archive/scenes/archive_scene_rename.c index 053bc74f5..5d0f0c6b5 100644 --- a/applications/main/archive/scenes/archive_scene_rename.c +++ b/applications/main/archive/scenes/archive_scene_rename.c @@ -69,10 +69,7 @@ bool archive_scene_rename_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SCENE_RENAME_CUSTOM_EVENT) { const char* path_src = archive_get_name(archive->browser); - - FuriString* path_dst; - - path_dst = furi_string_alloc(); + FuriString* path_dst = furi_string_alloc(); path_extract_dirname(path_src, path_dst); furi_string_cat_printf( @@ -89,8 +86,7 @@ bool archive_scene_rename_on_event(void* context, SceneManagerEvent event) { archive_show_loading_popup(archive, false); if(error != FSE_OK) { - FuriString* dialog_msg; - dialog_msg = furi_string_alloc(); + FuriString* dialog_msg = furi_string_alloc(); furi_string_cat_printf( dialog_msg, "Cannot rename:\n%s", storage_error_get_desc(error)); dialog_message_show_storage_error( @@ -98,7 +94,21 @@ bool archive_scene_rename_on_event(void* context, SceneManagerEvent event) { furi_string_free(dialog_msg); } else { ArchiveFile_t* current = archive_get_current_file(archive->browser); - if(current != NULL) furi_string_set(current->path, path_dst); + if(current->selected) { + with_view_model( + archive->browser->view, + ArchiveBrowserViewModel * model, + { + for(size_t i = 0; i < model->selected_count; i++) { + if(furi_string_equal(model->selected_files[i], current->path)) { + furi_string_set(model->selected_files[i], path_dst); + break; + } + } + }, + false); + } + furi_string_set(current->path, path_dst); } furi_string_free(path_dst); diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index 98462d86a..ce1e84d5c 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -8,6 +8,12 @@ #define SCROLL_INTERVAL (333) #define SCROLL_DELAY (2) +static const char* const selection_indicator_styles[] = { + "+", + "*", + "-", +}; + static const char* ArchiveTabNames[] = { [ArchiveTabFavorites] = "Favorites", [ArchiveTabIButton] = "iButton", @@ -36,10 +42,7 @@ static const Icon* ArchiveItemIcons[] = { [ArchiveFileTypeBadUsb] = &I_badusb_10px, [ArchiveFileTypeWAV] = &I_music_10px, [ArchiveFileTypeMag] = &I_mag_card_10px, - [ArchiveFileTypeCrossRemote] = &I_xremote_10px, - [ArchiveFileTypePicopass] = &I_125_10px, [ArchiveFileTypeU2f] = &I_u2f_10px, - [ArchiveFileTypeSetting] = &I_settings_10px, [ArchiveFileTypeApplication] = &I_Apps_10px, [ArchiveFileTypeJS] = &I_js_script_10px, [ArchiveFileTypeSearch] = &I_search_10px, @@ -61,58 +64,6 @@ void archive_browser_set_callback( browser->context = context; } -static void archive_update_formatted_path(ArchiveBrowserViewModel* model) { - ArchiveBrowserView* browser = model->archive->browser; - if(!browser->path_changed) { - return; - } - - if(momentum_settings.browser_path_mode == BrowserPathOff || archive_is_home(browser)) { - furi_string_set(browser->formatted_path, ArchiveTabNames[model->tab_idx]); - } else { - const char* path = furi_string_get_cstr(browser->path); - switch(momentum_settings.browser_path_mode) { - case BrowserPathFull: - furi_string_set(browser->formatted_path, browser->path); - break; - - case BrowserPathBrief: { - furi_string_reset(browser->formatted_path); - FuriString* token = furi_string_alloc(); - FuriString* remaining = furi_string_alloc_set(path); - - while(furi_string_size(remaining) > 0) { - size_t slash_pos = furi_string_search_char(remaining, '/'); - if(slash_pos == FURI_STRING_FAILURE) { - furi_string_cat_printf( - browser->formatted_path, "/%s", furi_string_get_cstr(remaining)); - break; - } - furi_string_set_n(token, remaining, 0, slash_pos); - if(furi_string_size(token) > 0) { - furi_string_cat_printf( - browser->formatted_path, "/%c", furi_string_get_char(token, 0)); - } - furi_string_right(remaining, slash_pos + 1); - } - - furi_string_free(token); - furi_string_free(remaining); - break; - } - - case BrowserPathCurrent: - path_extract_basename(path, browser->formatted_path); - break; - - default: - break; - } - } - - browser->path_changed = false; -} - static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) { if(menu_array_size(model->context_menu) == 0) { // Need init context menu @@ -174,6 +125,12 @@ static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) { menu_array_push_raw(model->context_menu), "Info", ArchiveBrowserEventFileMenuInfo); + if(!favorites) { + archive_menu_add_item( + menu_array_push_raw(model->context_menu), + model->select_mode ? "Deselect" : "Select", + ArchiveBrowserEventFileMenuSelectMode); + } if(selected->type != ArchiveFileTypeFolder) { archive_menu_add_item( menu_array_push_raw(model->context_menu), @@ -195,9 +152,9 @@ static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) { const uint8_t calc_height = menu_height - ((MENU_ITEMS - size_menu - 1) * line_height); canvas_set_color(canvas, ColorWhite); - canvas_draw_rbox(canvas, 72, 2, 56, calc_height + 4, 3); + canvas_draw_box(canvas, 72, 2, 56, calc_height + 4); canvas_set_color(canvas, ColorBlack); - canvas_draw_rframe(canvas, 71, 2, 57, calc_height + 4, 3); + elements_slightly_rounded_frame(canvas, 71, 2, 57, calc_height + 4); canvas_draw_str_aligned( canvas, 100, 11, AlignCenter, AlignBottom, model->menu_manage ? "Manage:" : "Actions:"); @@ -247,93 +204,116 @@ static void archive_draw_loading(Canvas* canvas, ArchiveBrowserViewModel* model) canvas_draw_icon(canvas, x, y, &A_Loading_24); } -static void draw_list_item( - Canvas* canvas, - ArchiveBrowserViewModel* model, - bool scrollbar, - uint32_t i, - int32_t idx) { - size_t array_size = files_array_size(model->files); - - FuriString* str_buf; - str_buf = furi_string_alloc(); - uint8_t x_offset = (model->move_fav && model->item_idx == idx) ? MOVE_OFFSET : 0; - - ArchiveFileTypeEnum file_type = ArchiveFileTypeLoading; - uint8_t* custom_icon_data = NULL; - - if(!model->list_loading && archive_is_item_in_array(model, idx)) { - ArchiveFile_t* file = files_array_get( - model->files, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0)); - file_type = file->type; - bool ext = model->tab_idx == ArchiveTabBrowser || model->tab_idx == ArchiveTabInternal || - model->tab_idx == ArchiveTabDiskImage || model->tab_idx == ArchiveTabSearch; - if(file_type == ArchiveFileTypeApplication) { - if(file->custom_icon_data) { - custom_icon_data = file->custom_icon_data; - furi_string_set(str_buf, file->custom_name); - } else { - file_type = ArchiveFileTypeUnknown; - path_extract_filename(file->path, str_buf, !ext); - } - } else { - path_extract_filename(file->path, str_buf, !ext); - } - } else { - furi_string_set(str_buf, "---"); - } - - size_t scroll_counter = model->scroll_counter; - - if(!model->list_loading && model->item_idx == idx) { - archive_draw_frame(canvas, i, scrollbar, model->move_fav); - if(scroll_counter < SCROLL_DELAY) { - scroll_counter = 0; - } else { - scroll_counter -= SCROLL_DELAY; - } - } else { - canvas_set_color(canvas, ColorBlack); - scroll_counter = 0; - } - - if(custom_icon_data) { - canvas_draw_bitmap(canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, 11, 10, custom_icon_data); - } else { - canvas_draw_icon(canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]); - } - - elements_scrollable_text_line( - canvas, - 15 + x_offset, - 24 + i * FRAME_HEIGHT, - ((scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset), - str_buf, - scroll_counter, - (model->item_idx != idx)); - - furi_string_free(str_buf); -} - static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) { furi_assert(model); + size_t array_size = files_array_size(model->files); bool scrollbar = model->item_cnt > 4; + ArchiveFile_t* file = NULL; for(uint32_t i = 0; i < MIN(model->item_cnt, MENU_ITEMS); ++i) { + FuriString* str_buf; + str_buf = furi_string_alloc(); int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u); - if(model->item_idx == idx) continue; - draw_list_item(canvas, model, scrollbar, i, idx); - } + uint8_t x_offset = (model->move_fav && model->item_idx == idx) ? MOVE_OFFSET : 0; - if(momentum_settings.popup_overlay && model->menu) { - canvas_draw_overlay(canvas); - } + ArchiveFileTypeEnum file_type = ArchiveFileTypeLoading; + uint8_t* custom_icon_data = NULL; - for(uint32_t i = 0; i < MIN(model->item_cnt, MENU_ITEMS); ++i) { - int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u); - if(model->item_idx != idx) continue; - draw_list_item(canvas, model, scrollbar, i, idx); + if(!model->list_loading && archive_is_item_in_array(model, idx)) { + file = files_array_get( + model->files, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0)); + file_type = file->type; + bool ext = model->tab_idx == ArchiveTabBrowser || + model->tab_idx == ArchiveTabInternal || + model->tab_idx == ArchiveTabDiskImage || model->tab_idx == ArchiveTabSearch; + if(file_type == ArchiveFileTypeApplication) { + if(file->custom_icon_data) { + custom_icon_data = file->custom_icon_data; + furi_string_set(str_buf, file->custom_name); + } else { + file_type = ArchiveFileTypeUnknown; + path_extract_filename(file->path, str_buf, !ext); + } + } else { + path_extract_filename(file->path, str_buf, !ext); + } + } else { + furi_string_set(str_buf, "---"); + } + + size_t scroll_counter = model->scroll_counter; + + if(!model->list_loading && model->item_idx == idx) { + archive_draw_frame(canvas, i, scrollbar, model->move_fav); + if(scroll_counter < SCROLL_DELAY) { + scroll_counter = 0; + } else { + scroll_counter -= SCROLL_DELAY; + } + } else { + canvas_set_color(canvas, ColorBlack); + scroll_counter = 0; + } + + if(custom_icon_data) { + canvas_draw_bitmap( + canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, 11, 10, custom_icon_data); + } else { + canvas_draw_icon( + canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]); + } + + uint32_t text_width = scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX; + if(model->select_mode && file && file->selected) { + text_width -= 16; + } + + elements_scrollable_text_line( + canvas, + 15 + x_offset, + 24 + i * FRAME_HEIGHT, + text_width - x_offset, + str_buf, + scroll_counter, + (model->item_idx != idx)); + + if(!model->list_loading && model->select_mode && archive_is_item_in_array(model, idx)) { + uint32_t selected_in_dir = 0; + if(file->type == ArchiveFileTypeFolder) { + size_t path_len = strlen(furi_string_get_cstr(file->path)); + for(uint32_t j = 0; j < model->selected_count; j++) { + const char* selected_path = furi_string_get_cstr(model->selected_files[j]); + if(archive_is_parent_or_identical( + furi_string_get_cstr(file->path), selected_path)) { + if(strlen(selected_path) != path_len) { + selected_in_dir++; + } + } + } + } + + if(selected_in_dir > 0 || file->selected) { + FuriString* indicator = furi_string_alloc(); + if(selected_in_dir > 0 && !file->selected) { + furi_string_printf(indicator, "[%lu]", selected_in_dir); + } else { + furi_string_printf( + indicator, + "[%s]", + selection_indicator_styles[momentum_settings.selection_indicator_style]); + } + + const char* indicator_str = furi_string_get_cstr(indicator); + uint8_t indicator_width = canvas_string_width(canvas, indicator_str); + uint8_t x_pos = (scrollbar ? 122 : 127) - indicator_width - 2; + + canvas_draw_str(canvas, x_pos, 24 + i * FRAME_HEIGHT, indicator_str); + furi_string_free(indicator); + } + } + + furi_string_free(str_buf); } if(scrollbar) { @@ -348,15 +328,10 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) { static void archive_render_status_bar(Canvas* canvas, ArchiveBrowserViewModel* model) { furi_assert(model); - const char* tab_name = NULL; - if(model->tab_idx == ArchiveTabSearch) { - if(scene_manager_get_scene_state(model->archive->scene_manager, ArchiveAppSceneSearch)) { - tab_name = "Searching"; - } else { - tab_name = ArchiveTabNames[model->tab_idx]; - } - } else { - archive_update_formatted_path(model); + const char* tab_name = ArchiveTabNames[model->tab_idx]; + if(model->tab_idx == ArchiveTabSearch && + scene_manager_get_scene_state(model->archive->scene_manager, ArchiveAppSceneSearch)) { + tab_name = "Searching"; } bool clip = model->clipboard != NULL; @@ -365,32 +340,29 @@ static void archive_render_status_bar(Canvas* canvas, ArchiveBrowserViewModel* m canvas_set_color(canvas, ColorWhite); canvas_draw_box(canvas, 0, 0, 50, 13); if(clip) canvas_draw_box(canvas, 69, 0, 24, 13); + if(model->select_mode) canvas_draw_box(canvas, 69, 0, 30, 13); canvas_draw_box(canvas, 107, 0, 20, 13); canvas_set_color(canvas, ColorBlack); canvas_draw_rframe(canvas, 0, 0, 51, 13, 1); // frame canvas_draw_line(canvas, 49, 1, 49, 11); // shadow right canvas_draw_line(canvas, 1, 11, 49, 11); // shadow bottom - if(tab_name) { - canvas_draw_str_aligned(canvas, 25, 9, AlignCenter, AlignBottom, tab_name); - } else { - elements_scrollable_text_line_centered( - canvas, - 25, - 9, - 45, - model->archive->browser->formatted_path, - model->menu ? 0 : model->scroll_counter, - false, - true); - } + canvas_draw_str_aligned(canvas, 25, 9, AlignCenter, AlignBottom, tab_name); - if(clip) { - canvas_draw_rframe(canvas, 69, 0, 25, 13, 1); - canvas_draw_line(canvas, 92, 1, 92, 11); - canvas_draw_line(canvas, 70, 11, 92, 11); + if(clip || model->select_mode) { + const uint8_t box_w = clip ? 25 : 31; + const uint8_t box_shadow_x = clip ? 92 : 98; + + canvas_draw_rframe(canvas, 69, 0, box_w, 13, 1); + canvas_draw_line(canvas, box_shadow_x, 1, box_shadow_x, 11); + canvas_draw_line(canvas, 70, 11, box_shadow_x, 11); canvas_draw_str_aligned( - canvas, 81, 9, AlignCenter, AlignBottom, model->clipboard_copy ? "Copy" : "Cut"); + canvas, + clip ? 81 : 84, + 9, + AlignCenter, + AlignBottom, + model->select_mode ? "Select" : (model->clipboard_copy ? "Copy" : "Cut")); } canvas_draw_rframe(canvas, 107, 0, 21, 13, 1); @@ -599,11 +571,26 @@ static bool archive_view_input(InputEvent* event, void* context) { archive_update_offset(browser); } else if(event->type == InputTypeShort) { if(event->key == InputKeyLeft || event->key == InputKeyRight) { - if(move_fav_mode) { - return true; // Return without doing anything - } else { - archive_switch_tab(browser, event->key); - } + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { + if(model->select_mode) { + if(event->key == InputKeyLeft) { + browser->callback( + ArchiveBrowserEventFileDeselect, browser->context); + } else if(event->key == InputKeyRight) { + browser->callback(ArchiveBrowserEventFileSelect, browser->context); + } + } else { + if(move_fav_mode) { + return true; // Return without doing anything + } else { + archive_switch_tab(browser, event->key); + } + } + }, + false); } else if(event->key == InputKeyOk) { if(move_fav_mode) { browser->callback(ArchiveBrowserEventSaveFavMove, browser->context); @@ -683,8 +670,6 @@ ArchiveBrowserView* browser_alloc(void) { browser->scroll_timer = furi_timer_alloc(browser_scroll_timer, FuriTimerTypePeriodic, browser); browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT)); - browser->formatted_path = furi_string_alloc(); - browser->path_changed = true; with_view_model( browser->view, @@ -718,7 +703,6 @@ void browser_free(ArchiveBrowserView* browser) { false); furi_string_free(browser->path); - furi_string_free(browser->formatted_path); view_free(browser->view); free(browser); diff --git a/applications/main/archive/views/archive_browser_view.h b/applications/main/archive/views/archive_browser_view.h index 098e2d22c..b605fa570 100644 --- a/applications/main/archive/views/archive_browser_view.h +++ b/applications/main/archive/views/archive_browser_view.h @@ -45,6 +45,7 @@ typedef enum { ArchiveBrowserEventFileMenuRun, ArchiveBrowserEventFileMenuFavorite, ArchiveBrowserEventFileMenuInfo, + ArchiveBrowserEventFileMenuSelectMode, ArchiveBrowserEventFileMenuShow, ArchiveBrowserEventFileMenuPaste, ArchiveBrowserEventFileMenuCut, @@ -53,6 +54,8 @@ typedef enum { ArchiveBrowserEventFileMenuRename, ArchiveBrowserEventFileMenuDelete, ArchiveBrowserEventFileMenuClose, + ArchiveBrowserEventFileSelect, + ArchiveBrowserEventFileDeselect, ArchiveBrowserEventEnterDir, @@ -107,7 +110,11 @@ typedef struct { bool menu; bool menu_manage; bool menu_can_switch; - char* clipboard; + bool select_mode; + FuriString** selected_files; + size_t selected_count; + char** clipboard; + size_t clipboard_count; bool clipboard_copy; menu_array_t context_menu; diff --git a/applications/main/momentum_app/scenes/momentum_app_scene_interface_filebrowser.c b/applications/main/momentum_app/scenes/momentum_app_scene_interface_filebrowser.c index 7032b2f9c..da1433dcf 100644 --- a/applications/main/momentum_app/scenes/momentum_app_scene_interface_filebrowser.c +++ b/applications/main/momentum_app/scenes/momentum_app_scene_interface_filebrowser.c @@ -14,6 +14,12 @@ const char* const browser_path_names[BrowserPathModeCount] = { "Full", }; +const char* const selection_indicator_styles[SelectionIndicatorStyleCount] = { + "+", + "*", + "-", +}; + void momentum_app_scene_interface_filebrowser_var_item_list_callback(void* context, uint32_t index) { MomentumApp* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); @@ -54,6 +60,15 @@ static void app->save_settings = true; } +static void momentum_app_scene_interface_filebrowser_selection_indicator_style_changed( + VariableItem* item) { + MomentumApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, selection_indicator_styles[index]); + momentum_settings.selection_indicator_style = index; + app->save_settings = true; +} + static void momentum_app_scene_interface_filebrowser_favorite_timeout_changed(VariableItem* item) { MomentumApp* app = variable_item_get_context(item); uint32_t value = variable_item_get_current_value_index(item); @@ -106,6 +121,16 @@ void momentum_app_scene_interface_filebrowser_on_enter(void* context) { variable_item_set_current_value_text( item, browser_path_names[momentum_settings.browser_path_mode]); + item = variable_item_list_add( + var_item_list, + "Selection Indicator", + SelectionIndicatorStyleCount, + momentum_app_scene_interface_filebrowser_selection_indicator_style_changed, + app); + variable_item_set_current_value_index(item, momentum_settings.selection_indicator_style); + variable_item_set_current_value_text( + item, selection_indicator_styles[momentum_settings.selection_indicator_style]); + item = variable_item_list_add( var_item_list, "Favorite Timeout", diff --git a/applications/services/gui/modules/widget.c b/applications/services/gui/modules/widget.c index 5282d596b..ace1c157b 100644 --- a/applications/services/gui/modules/widget.c +++ b/applications/services/gui/modules/widget.c @@ -5,11 +5,6 @@ ARRAY_DEF(ElementArray, WidgetElement*, M_PTR_OPLIST); // NOLINT -struct Widget { - View* view; - void* context; -}; - typedef struct { ElementArray_t element; } GuiWidgetModel; @@ -119,6 +114,23 @@ static void widget_add_element(Widget* widget, WidgetElement* element) { true); } +WidgetElement* widget_add_file_list_element( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size) { + furi_assert(widget); + WidgetElement* file_list_element = widget_element_file_list_create( + widget, x, y, lines, files, count, scrollbar_y, scrollbar_height, show_size); + widget_add_element(widget, file_list_element); + return file_list_element; +} + void widget_add_string_multiline_element( Widget* widget, uint8_t x, diff --git a/applications/services/gui/modules/widget.h b/applications/services/gui/modules/widget.h index a087db44d..d865ceadc 100644 --- a/applications/services/gui/modules/widget.h +++ b/applications/services/gui/modules/widget.h @@ -41,6 +41,29 @@ void widget_reset(Widget* widget); */ View* widget_get_view(Widget* widget); +/** Add File List Element + * + * @param widget Widget instance + * @param x x coordinate + * @param y y coordinate + * @param lines Number of lines visible + * @param files Array of FuriString pointers + * @param count Number of files + * @param scrollbar_y Y coordinate of the scrollbar + * @param scrollbar_height Height of the scrollbar + * @param show_size Show file size + */ +WidgetElement* widget_add_file_list_element( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size); + /** Add Multi String Element * * @param widget Widget instance diff --git a/applications/services/gui/modules/widget_elements/widget_element_file_list.c b/applications/services/gui/modules/widget_elements/widget_element_file_list.c new file mode 100644 index 000000000..f51547fcd --- /dev/null +++ b/applications/services/gui/modules/widget_elements/widget_element_file_list.c @@ -0,0 +1,225 @@ +#include "assets_icons.h" +#include "path.h" +#include "widget_element_i.h" +#include +#include +#include "archive/archive_i.h" +#include "assets_icons.h" + +#define SCROLL_INTERVAL (333) +#define SCROLL_DELAY (2) + +const char* units_short[] = {"B", "K", "M", "G", "T"}; + +typedef struct { + FuriString* path; + const Icon* icon; + char size_num[8]; + char size_unit[2]; +} FileListItem; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t lines; + FileListItem* files; + size_t count; + size_t offset; + uint8_t scrollbar_y; + bool show_size; + uint8_t scrollbar_height; + size_t scroll_counter; + FuriTimer* scroll_timer; +} FileListModel; + +static void format_file_size( + uint64_t size, + char* num_buf, + size_t num_size, + char* unit_buf, + size_t unit_size) { + double formatted_size = size; + uint8_t unit = 0; + + while(formatted_size >= 1024 && unit < COUNT_OF(units_short) - 1) { + formatted_size /= 1024; + unit++; + } + + if(unit == 0) { + snprintf(num_buf, num_size, "%d", (int)formatted_size); + } else { + snprintf(num_buf, num_size, "%.1f", (double)formatted_size); + } + snprintf(unit_buf, unit_size, "%s", units_short[unit]); +} + +static void widget_element_file_list_draw(Canvas* canvas, WidgetElement* element) { + furi_assert(canvas); + furi_assert(element); + FileListModel* model = element->model; + size_t items_visible = MIN(model->lines, model->count); + + canvas_set_font(canvas, FontSecondary); + for(size_t i = 0; i < items_visible; i++) { + size_t idx = model->offset + i; + if(idx < model->count) { + canvas_draw_icon( + canvas, model->x + 2, model->y + (i * FRAME_HEIGHT) - 9, model->files[idx].icon); + + size_t inner_x = 123; + if(model->show_size && model->files[idx].size_num[0] != '\0') { + canvas_set_font(canvas, FontPrimary); + uint16_t num_width = canvas_string_width(canvas, model->files[idx].size_num); + canvas_set_font(canvas, FontSecondary); + uint16_t unit_width = canvas_string_width(canvas, model->files[idx].size_unit); + uint16_t total_width = num_width + unit_width; + inner_x = model->x + (128 - model->x) - total_width - 5; + inner_x--; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str( + canvas, inner_x, model->y + (i * FRAME_HEIGHT), model->files[idx].size_num); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str( + canvas, + inner_x + num_width + 1, + model->y + (i * FRAME_HEIGHT), + model->files[idx].size_unit); + } + + size_t scroll_counter = model->scroll_counter; + scroll_counter = + i == 0 ? (model->count > model->lines && + (scroll_counter < SCROLL_DELAY ? 0 : scroll_counter - SCROLL_DELAY)) : + 0; + + elements_scrollable_text_line( + canvas, + model->x + 15, + model->y + (i * FRAME_HEIGHT), + inner_x - 19, + model->files[idx].path, + scroll_counter, + i != 0 || model->count <= model->lines); + } + } + + if(model->count > model->lines) { + elements_scrollbar_pos( + canvas, + 128, + model->scrollbar_y, + model->scrollbar_height, + model->offset, + model->count - (model->lines - 1)); + } +} + +static bool widget_element_file_list_input(InputEvent* event, WidgetElement* element) { + furi_assert(element); + FileListModel* model = element->model; + bool consumed = false; + + if(model->count > model->lines && + (event->type == InputTypeShort || event->type == InputTypeRepeat)) { + if(event->key == InputKeyUp) { + model->offset = (model->offset > 0) ? model->offset - 1 : model->count - model->lines; + model->scroll_counter = 0; + consumed = true; + } else if(event->key == InputKeyDown) { + model->offset = ((model->offset + model->lines) < model->count) ? model->offset + 1 : + 0; + model->scroll_counter = 0; + consumed = true; + } + } + + return consumed; +} + +static void widget_element_file_list_timer_callback(void* context) { + WidgetElement* element = context; + FileListModel* file_model = element->model; + file_model->scroll_counter++; + with_view_model(element->parent->view, void* _model, { UNUSED(_model); }, true); +} + +static void widget_element_file_list_free(WidgetElement* element) { + furi_assert(element); + FileListModel* model = element->model; + furi_timer_stop(model->scroll_timer); + furi_timer_free(model->scroll_timer); + for(size_t i = 0; i < model->count; i++) { + furi_string_free(model->files[i].path); + } + free(model->files); + free(model); + free(element); +} + +WidgetElement* widget_element_file_list_create( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size) { + // Allocate and init model + FileListModel* model = malloc(sizeof(FileListModel)); + model->x = x; + model->y = y; + model->lines = lines; + model->count = count; + model->scrollbar_y = scrollbar_y; + model->scrollbar_height = scrollbar_height; + model->show_size = show_size; + model->offset = 0; + model->scroll_counter = 0; + model->files = malloc(sizeof(FileListItem) * count); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FileInfo info; + for(size_t i = 0; i < count; i++) { + model->files[i].path = furi_string_alloc(); + path_extract_filename(files[i], model->files[i].path, false); + model->files[i].size_num[0] = '\0'; + model->files[i].size_unit[0] = '\0'; + + if(storage_dir_exists(storage, furi_string_get_cstr(files[i]))) { + model->files[i].icon = &I_dir_10px; + } else { + const char* ext = strrchr(furi_string_get_cstr(model->files[i].path), '.'); + model->files[i].icon = (ext && strcasecmp(ext, ".js") == 0) ? &I_js_script_10px : + &I_unknown_10px; + } + + if(show_size) { + storage_common_stat(storage, furi_string_get_cstr(files[i]), &info); + format_file_size( + info.size, + model->files[i].size_num, + sizeof(model->files[i].size_num), + model->files[i].size_unit, + sizeof(model->files[i].size_unit)); + } + } + furi_record_close(RECORD_STORAGE); + + // Allocate and init Element + WidgetElement* element = malloc(sizeof(WidgetElement)); + element->draw = widget_element_file_list_draw; + element->input = widget_element_file_list_input; + element->free = widget_element_file_list_free; + element->parent = widget; + element->model = model; + + model->scroll_timer = + furi_timer_alloc(widget_element_file_list_timer_callback, FuriTimerTypePeriodic, element); + furi_timer_start(model->scroll_timer, SCROLL_INTERVAL); + + return element; +} diff --git a/applications/services/gui/modules/widget_elements/widget_element_i.h b/applications/services/gui/modules/widget_elements/widget_element_i.h index 1a7c653d8..b3735deae 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_i.h +++ b/applications/services/gui/modules/widget_elements/widget_element_i.h @@ -5,7 +5,7 @@ #pragma once -#include "../widget.h" +#include "../widget_i.h" #include "widget_element.h" #include #include @@ -34,6 +34,18 @@ struct WidgetElement { Widget* parent; }; +/** Create file list element */ +WidgetElement* widget_element_file_list_create( + Widget* widget, + uint8_t x, + uint8_t y, + uint8_t lines, + FuriString** files, + size_t count, + uint8_t scrollbar_y, + uint8_t scrollbar_height, + bool show_size); + /** Create multi string element */ WidgetElement* widget_element_string_multiline_create( uint8_t x, diff --git a/applications/services/gui/modules/widget_i.h b/applications/services/gui/modules/widget_i.h new file mode 100644 index 000000000..d4f0a57eb --- /dev/null +++ b/applications/services/gui/modules/widget_i.h @@ -0,0 +1,10 @@ +#pragma once + +#include "widget.h" +#include +#include + +struct Widget { + View* view; + void* context; +}; diff --git a/lib/momentum/settings.c b/lib/momentum/settings.c index c2030aac1..2d25559f5 100644 --- a/lib/momentum/settings.c +++ b/lib/momentum/settings.c @@ -32,6 +32,7 @@ MomentumSettings momentum_settings = { .show_hidden_files = false, // OFF .show_internal_tab = false, // OFF .browser_path_mode = BrowserPathOff, // OFF + .selection_indicator_style = SelectionIndicatorStylePlus, // + .favorite_timeout = 0, // OFF .scroll_marquee = false, // OFF .dark_mode = false, // OFF @@ -106,6 +107,7 @@ static const struct { {setting_bool(show_hidden_files)}, {setting_bool(show_internal_tab)}, {setting_enum(browser_path_mode, BrowserPathModeCount)}, + {setting_enum(selection_indicator_style, SelectionIndicatorStyleCount)}, {setting_uint(favorite_timeout, 0, 60)}, {setting_bool(scroll_marquee)}, {setting_bool(dark_mode)}, diff --git a/lib/momentum/settings.h b/lib/momentum/settings.h index 87a259d09..a6f09b398 100644 --- a/lib/momentum/settings.h +++ b/lib/momentum/settings.h @@ -63,6 +63,13 @@ typedef enum { BrowserPathModeCount, } BrowserPathMode; +typedef enum { + SelectionIndicatorStylePlus, + SelectionIndicatorStyleStar, + SelectionIndicatorStyleDash, + SelectionIndicatorStyleCount, +} SelectionIndicatorStyle; + typedef struct { char asset_pack[ASSET_PACKS_NAME_LEN]; uint32_t anim_speed; @@ -89,6 +96,7 @@ typedef struct { bool show_hidden_files; bool show_internal_tab; BrowserPathMode browser_path_mode; + SelectionIndicatorStyle selection_indicator_style; uint32_t favorite_timeout; bool scroll_marquee; bool dark_mode; diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 0179b6ad1..24be3b54c 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -4023,6 +4023,7 @@ Function,-,wcstombs,size_t,"char*, const wchar_t*, size_t" Function,-,wctomb,int,"char*, wchar_t" Function,+,widget_add_button_element,void,"Widget*, GuiButtonType, const char*, ButtonCallback, void*" Function,+,widget_add_circle_element,void,"Widget*, uint8_t, uint8_t, uint8_t, _Bool" +Function,+,widget_add_file_list_element,WidgetElement*,"Widget*, uint8_t, uint8_t, uint8_t, FuriString**, size_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_icon_element,void,"Widget*, uint8_t, uint8_t, const Icon*" Function,+,widget_add_line_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t" Function,+,widget_add_rect_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, _Bool"