Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Archive: Allow selecting multiple files #327

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions applications/main/archive/helpers/archive_browser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -680,3 +718,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;
}
5 changes: 5 additions & 0 deletions applications/main/archive/helpers/archive_browser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -108,3 +110,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);
4 changes: 4 additions & 0 deletions applications/main/archive/helpers/archive_files.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
197 changes: 141 additions & 56 deletions applications/main/archive/scenes/archive_scene_browser.c
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,69 @@ 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);
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) {
Expand All @@ -289,75 +352,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) {
Expand All @@ -366,8 +437,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);
Expand Down
Loading