mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-06-21 20:42:15 -07:00
Merge remote-tracking branch '956MB:feat/archive-select-multiple' into 956/prs #327
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "assets_icons.h"
|
||||
#include "path.h"
|
||||
#include "widget_element_i.h"
|
||||
#include <gui/elements.h>
|
||||
#include <core/common_defines.h>
|
||||
#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;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../widget.h"
|
||||
#include "../widget_i.h"
|
||||
#include "widget_element.h"
|
||||
#include <furi.h>
|
||||
#include <gui/view.h>
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "widget.h"
|
||||
#include <furi.h>
|
||||
#include <m-array.h>
|
||||
|
||||
struct Widget {
|
||||
View* view;
|
||||
void* context;
|
||||
};
|
||||
@@ -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)},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user