Files
Momentum-Firmware/applications/main/archive/scenes/archive_scene_browser.c
T
Alexander Bays 0e3e1b352b Desktop / MNTM Settings: Directories and generic files support for Keybinds / Main Menu (#331)
* feat(Desktop): Directories support for keybinds

- Adds *RIGHT* button select in the file browser dialogs and changing the `Open File` action to `Open File/Directory` in `Settings > Desktop > Keybinds Setup`. This adds the ability to open to any directory in the Archive app, in addition to the default behavior of opening a file in it's default app.

* line order mixup

* Main Menu: Allow adding JS files (or any file)

- Normal files and directories are now able to be added to then main menu and are run in their appropriate apps.
- e.g. .txt files shown in text viewer, .js files are run in the JS Runner app, and folders are navigated to by the Archive app. All similar to the desktop keybinds functionality.
- Icons are also assigned appropriately based on the extensions, though more could probably be added to the `loader_menu_get_ext_icon` function.
- Also replaced some of the long arduous is_dir checks and just used the `storage_dir_exists` function since its already there and does the same.

* should be checking `ext` for NULL

* Move select_right at end of structs for binary compatibility

apps may blindly reach into these structs so need to keep the basics in same structure

for DialogsFileBrowserOptions this is even in public api and after compilation this would be incompatible with other firmwares even without reaching into private structs

* Select menu item / folder for directories too

* Move api below too

* Keep ofw order here too

* Refactor starting archive into desktop, less FuriString passing around

* Dont leave main menu when launching archive

* Simplify/fix a few things

* Handle folders in run_with_default_app()

* Update App -> Item naming in MNTM settings

* Fix build

* Explain pressing right

* Update changelog

---------

Co-authored-by: WillyJL <me@willyjl.dev>
2025-07-22 01:51:33 +00:00

516 lines
20 KiB
C

#include "../archive_i.h"
#include "../helpers/archive_files.h"
#include "../helpers/archive_favorites.h"
#include "../helpers/archive_browser.h"
#include "../helpers/archive_helpers_ext.h"
#include "../views/archive_browser_view.h"
#include "archive/scenes/archive_scene.h"
#include <desktop/desktop_i.h>
#define TAG "ArchiveSceneBrowser"
#define SCENE_STATE_DEFAULT (0)
#define SCENE_STATE_NEED_REFRESH (1)
const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
switch(file_type) {
case ArchiveFileTypeIButton:
return "iButton";
case ArchiveFileTypeNFC:
return "NFC";
case ArchiveFileTypeSubGhz:
return "Sub-GHz";
case ArchiveFileTypeLFRFID:
return "125 kHz RFID";
case ArchiveFileTypeInfrared:
return "Infrared";
case ArchiveFileTypeSubghzPlaylist:
return EXT_PATH("apps/Sub-Ghz/subghz_playlist.fap");
case ArchiveFileTypeSubghzRemote:
return EXT_PATH("apps/Sub-Ghz/subghz_remote.fap");
case ArchiveFileTypeInfraredRemote:
return EXT_PATH("apps/Infrared/ir_remote.fap");
case ArchiveFileTypeBadUsb:
return "Bad KB";
case ArchiveFileTypeWAV:
return EXT_PATH("apps/Media/wav_player.fap");
case ArchiveFileTypeMag:
return EXT_PATH("apps/GPIO/magspoof.fap");
case ArchiveFileTypeCrossRemote:
return EXT_PATH("apps/Infrared/xremote.fap");
case ArchiveFileTypeU2f:
return "U2F";
case ArchiveFileTypeUpdateManifest:
return "UpdaterApp";
case ArchiveFileTypeDiskImage:
return EXT_PATH("apps/USB/mass_storage.fap");
case ArchiveFileTypeJS:
#ifdef JS_RUNNER_FAP
return EXT_PATH("apps/assets/js_app.fap");
#else
return "JS Runner";
#endif
default:
return NULL;
}
}
static void archive_loader_callback(const void* message, void* context) {
furi_assert(message);
furi_assert(context);
const LoaderEvent* event = message;
ArchiveApp* archive = (ArchiveApp*)context;
if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
view_dispatcher_send_custom_event(
archive->view_dispatcher, ArchiveBrowserEventListRefresh);
}
}
static void archive_show_file(Loader* loader, const char* path) {
File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
bool text = true;
if(storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
uint8_t buf[1000];
size_t read = storage_file_read(file, buf, sizeof(buf));
for(size_t i = 0; i < read; i++) {
const char c = buf[i];
if((c < ' ' || c > '~') && c != '\r' && c != '\n') {
text = false;
break;
}
}
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(text) {
loader_start_detached_with_gui_error(loader, EXT_PATH("apps/Tools/text_viewer.fap"), path);
} else {
loader_start_detached_with_gui_error(loader, EXT_PATH("apps/Tools/hex_viewer.fap"), path);
}
}
static void archive_mount_disk_image(ArchiveBrowserView* browser, ArchiveFile_t* selected) {
Storage* storage = furi_record_open(RECORD_STORAGE);
File* disk_image = NULL;
do {
if(browser->disk_image) {
// Deinit and recycle File object
if(storage_virtual_quit(storage) != FSE_OK) break;
storage_file_close(browser->disk_image);
disk_image = browser->disk_image;
browser->disk_image = NULL;
} else {
disk_image = storage_file_alloc(storage);
}
if(!storage_file_open(
disk_image,
furi_string_get_cstr(selected->path),
FSAM_READ | FSAM_WRITE,
FSOM_OPEN_EXISTING))
break;
FS_Error init = storage_virtual_init(storage, disk_image);
if(init == FSE_ALREADY_OPEN) {
if(storage_virtual_quit(storage) == FSE_OK) {
init = storage_virtual_init(storage, disk_image);
}
}
if(init != FSE_OK) break;
if(storage_virtual_mount(storage) != FSE_OK) {
storage_virtual_quit(storage);
break;
}
browser->disk_image = disk_image;
while(archive_get_tab(browser) != ArchiveTabDiskImage) {
archive_switch_tab(browser, TAB_LEFT);
}
} while(0);
if(disk_image && !browser->disk_image) storage_file_free(disk_image);
furi_record_close(RECORD_STORAGE);
}
static void
archive_run_in_app(ArchiveBrowserView* browser, ArchiveFile_t* selected, bool favorites) {
Loader* loader = furi_record_open(RECORD_LOADER);
const char* app_name = archive_get_flipper_app_name(selected->type);
if(selected->type == ArchiveFileTypeSetting) {
FuriString* app_name = furi_string_alloc_set(selected->path);
furi_string_right(app_name, furi_string_search_char(app_name, '/', 1) + 1);
size_t slash = furi_string_search_char(app_name, '/', 1);
if(slash != FURI_STRING_FAILURE) {
furi_string_left(app_name, slash);
FuriString* app_args =
furi_string_alloc_set_str(furi_string_get_cstr(app_name) + slash + 1);
loader_start_with_gui_error(
loader, furi_string_get_cstr(app_name), furi_string_get_cstr(app_args));
furi_string_free(app_args);
} else {
loader_start_with_gui_error(loader, furi_string_get_cstr(app_name), NULL);
}
furi_string_free(app_name);
} else if(selected->type == ArchiveFileTypeSearch) {
while(archive_get_tab(browser) != ArchiveTabSearch) {
archive_switch_tab(browser, TAB_LEFT);
}
browser->callback(ArchiveBrowserEventSearch, browser->context);
} else if(app_name) {
if(selected->is_app) {
char* param = strrchr(furi_string_get_cstr(selected->path), '/');
if(param != NULL) {
param++;
}
loader_start_with_gui_error(loader, app_name, param);
} else {
const char* str = furi_string_get_cstr(selected->path);
if(favorites &&
(selected->type == ArchiveFileTypeIButton ||
selected->type == ArchiveFileTypeLFRFID || selected->type == ArchiveFileTypeNFC ||
selected->type == ArchiveFileTypeSubGhz)) {
char arg[strlen(str) + 4];
snprintf(arg, sizeof(arg), "fav%s", str);
loader_start_with_gui_error(loader, app_name, arg);
} else {
loader_start_detached_with_gui_error(loader, app_name, str);
}
}
} else if(selected->type == ArchiveFileTypeApplication) {
loader_start_detached_with_gui_error(loader, furi_string_get_cstr(selected->path), NULL);
} else if(selected->type == ArchiveFileTypeFolder) {
// Folders are handled by archive, so we should only get here with run_with_default_app() outside archive
furi_check(browser == NULL, "What you doin?");
Desktop* desktop = furi_record_open(RECORD_DESKTOP);
desktop_launch_archive(desktop, furi_string_get_cstr(selected->path));
furi_record_close(RECORD_DESKTOP);
} else {
archive_show_file(loader, furi_string_get_cstr(selected->path));
}
furi_record_close(RECORD_LOADER);
}
// Hijack existing archive code for default app choosing without needing archive running
void run_with_default_app(const char* path) {
// Kostily
FileInfo info;
Storage* storage = furi_record_open(RECORD_STORAGE);
bool is_dir = storage_common_stat(storage, path, &info) == FSE_OK &&
info.flags & FSF_DIRECTORY;
furi_record_close(RECORD_STORAGE);
// Velosipedy
ArchiveFile_t item;
ArchiveFile_t_init(&item);
furi_string_set(item.path, path);
archive_set_file_type(&item, path, is_dir, false);
// Bydlo kod go brrr
archive_run_in_app(NULL, &item, false);
ArchiveFile_t_clear(&item);
}
void archive_scene_browser_callback(ArchiveBrowserEvent event, void* context) {
ArchiveApp* archive = (ArchiveApp*)context;
view_dispatcher_send_custom_event(archive->view_dispatcher, event);
}
void archive_scene_browser_on_enter(void* context) {
ArchiveApp* archive = (ArchiveApp*)context;
ArchiveBrowserView* browser = archive->browser;
browser->is_root = true;
archive_browser_set_callback(browser, archive_scene_browser_callback, archive);
if(archive_get_tab(browser) == ArchiveTabFavorites && archive_favorites_count(browser) < 1) {
archive_switch_tab(browser, TAB_LEFT);
}
archive_update_focus(browser, archive->text_store);
view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewBrowser);
archive->loader_stop_subscription = furi_pubsub_subscribe(
loader_get_pubsub(archive->loader), archive_loader_callback, archive);
uint32_t state = scene_manager_get_scene_state(archive->scene_manager, ArchiveAppSceneBrowser);
if(state == SCENE_STATE_NEED_REFRESH) {
view_dispatcher_send_custom_event(
archive->view_dispatcher, ArchiveBrowserEventListRefresh);
}
scene_manager_set_scene_state(
archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_DEFAULT);
}
bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) {
ArchiveApp* archive = (ArchiveApp*)context;
ArchiveBrowserView* browser = archive->browser;
ArchiveFile_t* selected = archive_get_current_file(browser);
bool favorites = archive_get_tab(browser) == ArchiveTabFavorites;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
switch(event.event) {
case ArchiveBrowserEventFileMenuOpen:
archive_show_file_menu(browser, true, false);
consumed = true;
break;
case ArchiveBrowserEventManageMenuOpen:
archive_show_file_menu(browser, true, true);
consumed = true;
break;
case ArchiveBrowserEventFileMenuClose:
archive_show_file_menu(browser, false, false);
consumed = true;
break;
case ArchiveBrowserEventFileMenuRun:
if(archive_is_known_app(selected->type)) {
archive_run_in_app(browser, selected, favorites);
}
archive_show_file_menu(browser, false, false);
consumed = true;
break;
case ArchiveBrowserEventFileMenuFavorite: {
const char* name = archive_get_name(browser);
if(favorites) {
archive_favorites_delete("%s", name);
archive_file_array_rm_selected(browser);
} else if(archive_is_known_app(selected->type)) {
if(archive_is_favorite("%s", name)) {
archive_favorites_delete("%s", name);
} else {
archive_file_append(ARCHIVE_FAV_PATH, "%s\n", name);
}
}
archive_show_file_menu(browser, false, false);
consumed = true;
} break;
case ArchiveBrowserEventFileMenuInfo:
archive_show_file_menu(browser, false, false);
scene_manager_set_scene_state(
archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneInfo);
consumed = true;
break;
case ArchiveBrowserEventFileMenuShow:
if(selected->type == ArchiveFileTypeDiskImage &&
archive_get_tab(browser) != ArchiveTabDiskImage) {
archive_mount_disk_image(browser, selected);
} else {
archive_show_file(
furi_record_open(RECORD_LOADER), furi_string_get_cstr(selected->path));
furi_record_close(RECORD_LOADER);
}
archive_show_file_menu(browser, false, false);
consumed = true;
break;
case ArchiveBrowserEventFileMenuPaste:
archive_show_file_menu(browser, false, false);
if(!favorites) {
FuriString* path_src = NULL;
FuriString* path_dst = NULL;
bool copy;
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;
free(model->clipboard);
model->clipboard = NULL;
}
},
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);
}
}
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) {
with_view_model(
browser->view,
ArchiveBrowserViewModel * model,
{
if(model->clipboard == NULL) {
model->clipboard = strdup(furi_string_get_cstr(selected->path));
model->clipboard_copy = true;
}
},
false);
}
consumed = true;
break;
case ArchiveBrowserEventFileMenuNewDir:
archive_show_file_menu(browser, false, false);
if(!favorites) {
scene_manager_set_scene_state(
archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneNewDir);
}
consumed = true;
break;
case ArchiveBrowserEventFileMenuRename:
archive_show_file_menu(browser, false, false);
scene_manager_set_scene_state(
archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneRename);
consumed = true;
break;
case ArchiveBrowserEventFileMenuDelete:
archive_show_file_menu(browser, false, false);
scene_manager_set_scene_state(
archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH);
scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneDelete);
consumed = true;
break;
case ArchiveBrowserEventEnterDir:
if(favorites) {
archive_switch_tab(browser, TAB_LEFT);
}
archive_enter_dir(browser, selected->path);
consumed = true;
break;
case ArchiveBrowserEventSearch: {
bool open =
!scene_manager_get_scene_state(archive->scene_manager, ArchiveAppSceneSearch);
scene_manager_set_scene_state(archive->scene_manager, ArchiveAppSceneSearch, false);
if(archive->search_thread) {
furi_thread_join(archive->search_thread);
furi_thread_free(archive->search_thread);
archive->search_thread = NULL;
}
if(open) scene_manager_next_scene(archive->scene_manager, ArchiveAppSceneSearch);
consumed = true;
break;
}
case ArchiveBrowserEventFavMoveUp:
archive_file_array_swap(browser, 1);
consumed = true;
break;
case ArchiveBrowserEventFavMoveDown:
archive_file_array_swap(browser, -1);
consumed = true;
break;
case ArchiveBrowserEventEnterFavMove:
archive_show_file_menu(browser, false, false);
furi_string_set(archive->fav_move_str, selected->path);
archive_favorites_move_mode(archive->browser, true);
consumed = true;
break;
case ArchiveBrowserEventExitFavMove:
archive_update_focus(browser, furi_string_get_cstr(archive->fav_move_str));
archive_favorites_move_mode(archive->browser, false);
consumed = true;
break;
case ArchiveBrowserEventSaveFavMove:
archive_favorites_move_mode(archive->browser, false);
archive_favorites_save(archive->browser);
consumed = true;
break;
case ArchiveBrowserEventLoadPrevItems:
archive_file_array_load(archive->browser, -1);
consumed = true;
break;
case ArchiveBrowserEventLoadNextItems:
archive_file_array_load(archive->browser, 1);
consumed = true;
break;
case ArchiveBrowserEventListRefresh:
if(!favorites) {
archive_refresh_dir(browser);
} else {
archive_favorites_read(browser);
}
consumed = true;
break;
case ArchiveBrowserEventExit:
if(!archive_is_home(browser)) {
archive_leave_dir(browser);
} else {
if(archive->loader_stop_subscription) {
furi_pubsub_unsubscribe(
loader_get_pubsub(archive->loader), archive->loader_stop_subscription);
archive->loader_stop_subscription = NULL;
}
view_dispatcher_stop(archive->view_dispatcher);
}
consumed = true;
break;
default:
break;
}
}
return consumed;
}
void archive_scene_browser_on_exit(void* context) {
ArchiveApp* archive = (ArchiveApp*)context;
if(archive->loader_stop_subscription) {
furi_pubsub_unsubscribe(
loader_get_pubsub(archive->loader), archive->loader_stop_subscription);
archive->loader_stop_subscription = NULL;
}
}