feat(Archive): Adds multiple file selection

Allows selecting both files and folders at the same time to perform
actions on. Selected files are either moved to clipboard for the
`Cut`/`Copy` and `Paste` actions, or deleted with `Delete`. `Rename`
action still only works on single file, but that file can be in the
multi selection.
This commit is contained in:
Alexander Bays
2024-12-20 12:19:25 -06:00
parent ef05ee44c9
commit 04e6eb251d
11 changed files with 411 additions and 100 deletions
@@ -85,12 +85,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;
@@ -138,6 +154,28 @@ static void archive_file_browser_set_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);
@@ -680,3 +718,18 @@ 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;
}
}
@@ -80,6 +80,8 @@ inline bool archive_is_known_app(ArchiveFileTypeEnum type) {
return type < ArchiveFileTypeUnknown;
}
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);
@@ -108,3 +110,5 @@ 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);
@@ -40,6 +40,7 @@ typedef struct {
FuriString* custom_name;
bool fav;
bool is_app;
bool selected;
} ArchiveFile_t;
static void ArchiveFile_t_init(ArchiveFile_t* obj) {
@@ -49,6 +50,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) {
@@ -63,6 +65,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) {
@@ -77,6 +80,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) {
@@ -274,6 +274,82 @@ 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) {
size_t write_idx = 0;
for(size_t i = 0; i < model->selected_count; i++) {
if(!furi_string_start_with(
model->selected_files[i], current->path)) {
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;
}
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);
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) {
@@ -289,75 +365,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) {
@@ -366,8 +450,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);
@@ -15,41 +15,48 @@ 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 > 0) {
snprintf(
delete_str,
sizeof(delete_str),
"\e#Delete %d files?\e#",
model->selected_count);
} 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 +64,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) {
@@ -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);
@@ -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",
@@ -119,6 +125,10 @@ static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) {
menu_array_push_raw(model->context_menu),
"Info",
ArchiveBrowserEventFileMenuInfo);
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),
@@ -197,6 +207,7 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* 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;
@@ -208,7 +219,7 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
uint8_t* custom_icon_data = NULL;
if(!model->list_loading && archive_is_item_in_array(model, idx)) {
ArchiveFile_t* file = files_array_get(
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 ||
@@ -251,15 +262,55 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) {
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,
((scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset),
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);
}
@@ -287,6 +338,7 @@ 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);
@@ -295,12 +347,20 @@ static void archive_render_status_bar(Canvas* canvas, ArchiveBrowserViewModel* m
canvas_draw_line(canvas, 1, 11, 49, 11); // shadow bottom
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);
@@ -509,11 +569,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);
@@ -3,6 +3,7 @@
#include "../helpers/archive_files.h"
#include "../helpers/archive_favorites.h"
#include "archive/archive.h"
#include <gui/gui_i.h>
#include <gui/view.h>
#include <gui/canvas.h>
@@ -44,6 +45,7 @@ typedef enum {
ArchiveBrowserEventFileMenuRun,
ArchiveBrowserEventFileMenuFavorite,
ArchiveBrowserEventFileMenuInfo,
ArchiveBrowserEventFileMenuSelectMode,
ArchiveBrowserEventFileMenuShow,
ArchiveBrowserEventFileMenuPaste,
ArchiveBrowserEventFileMenuCut,
@@ -52,6 +54,8 @@ typedef enum {
ArchiveBrowserEventFileMenuRename,
ArchiveBrowserEventFileMenuDelete,
ArchiveBrowserEventFileMenuClose,
ArchiveBrowserEventFileSelect,
ArchiveBrowserEventFileDeselect,
ArchiveBrowserEventEnterDir,
@@ -103,7 +107,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;
@@ -7,6 +7,12 @@ enum VarItemListIndex {
VarItemListIndexFavoriteTimeout,
};
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);
@@ -38,6 +44,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);
@@ -80,6 +95,16 @@ void momentum_app_scene_interface_filebrowser_on_enter(void* context) {
variable_item_set_current_value_index(item, momentum_settings.show_internal_tab);
variable_item_set_current_value_text(item, momentum_settings.show_internal_tab ? "ON" : "OFF");
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",