[FL-1181] Archive app (#458)
* fix stack size, file listing works * fix scrollbar, update docs * cut long filenames * Dolphin: overhaul unlocking logic, unlocked message added * furi - added common_defines.h, minor macro cleanup; fix scrollbar type conversion * remove door opening animation * adaptive long file name shortening, item icons, invert selection * archive: browser tab, file types (beta); scenes: fix sleep emote * dont trim unknown extensions * fix string_size usage * array container for file list, fixes * better path handling * archive: renaming, adding to favorites worksl scrollbar fix: limit min bar height to 1px to prevent disappearance on large lists Co-authored-by: あく <alleteam@gmail.com>
| @ -36,6 +36,7 @@ int32_t scene_app(void* p); | |||||||
| int32_t passport(void* p); | int32_t passport(void* p); | ||||||
| int32_t app_accessor(void* p); | int32_t app_accessor(void* p); | ||||||
| int32_t internal_storage_task(void* p); | int32_t internal_storage_task(void* p); | ||||||
|  | int32_t app_archive(void* p); | ||||||
| 
 | 
 | ||||||
| // On system start hooks declaration
 | // On system start hooks declaration
 | ||||||
| void nfc_cli_init(); | void nfc_cli_init(); | ||||||
| @ -159,6 +160,15 @@ const size_t FLIPPER_SERVICES_COUNT = sizeof(FLIPPER_SERVICES) / sizeof(FlipperA | |||||||
| 
 | 
 | ||||||
| // Main menu APP
 | // Main menu APP
 | ||||||
| const FlipperApplication FLIPPER_APPS[] = { | const FlipperApplication FLIPPER_APPS[] = { | ||||||
|  | 
 | ||||||
|  | #ifdef APP_IBUTTON | ||||||
|  |     {.app = app_ibutton, .name = "iButton", .stack_size = 4096, .icon = A_iButton_14}, | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #ifdef APP_NFC | ||||||
|  |     {.app = nfc_task, .name = "NFC", .stack_size = 1024, .icon = A_NFC_14}, | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
| #ifdef APP_SUBGHZ | #ifdef APP_SUBGHZ | ||||||
|     {.app = subghz_app, .name = "Sub-1 GHz", .stack_size = 1024, .icon = A_Sub1ghz_14}, |     {.app = subghz_app, .name = "Sub-1 GHz", .stack_size = 1024, .icon = A_Sub1ghz_14}, | ||||||
| #endif | #endif | ||||||
| @ -167,21 +177,18 @@ const FlipperApplication FLIPPER_APPS[] = { | |||||||
|     {.app = app_lfrfid, .name = "125 kHz RFID", .stack_size = 1024, .icon = A_125khz_14}, |     {.app = app_lfrfid, .name = "125 kHz RFID", .stack_size = 1024, .icon = A_125khz_14}, | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| #ifdef APP_NFC |  | ||||||
|     {.app = nfc_task, .name = "NFC", .stack_size = 1024, .icon = A_NFC_14}, |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifdef APP_IRDA | #ifdef APP_IRDA | ||||||
|     {.app = irda, .name = "Infrared", .stack_size = 1024, .icon = A_Infrared_14}, |     {.app = irda, .name = "Infrared", .stack_size = 1024, .icon = A_Infrared_14}, | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| #ifdef APP_IBUTTON |  | ||||||
|     {.app = app_ibutton, .name = "iButton", .stack_size = 4096, .icon = A_iButton_14}, |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifdef APP_GPIO_DEMO | #ifdef APP_GPIO_DEMO | ||||||
|     {.app = app_gpio_test, .name = "GPIO", .stack_size = 1024, .icon = A_GPIO_14}, |     {.app = app_gpio_test, .name = "GPIO", .stack_size = 1024, .icon = A_GPIO_14}, | ||||||
| #endif | #endif | ||||||
|  | 
 | ||||||
|  | #ifdef APP_ARCHIVE | ||||||
|  |     {.app = app_archive, .name = "Archive", .stack_size = 4096, .icon = A_FileManager_14}, | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const size_t FLIPPER_APPS_COUNT = sizeof(FLIPPER_APPS) / sizeof(FlipperApplication); | const size_t FLIPPER_APPS_COUNT = sizeof(FLIPPER_APPS) / sizeof(FlipperApplication); | ||||||
| @ -263,6 +270,11 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = { | |||||||
| 
 | 
 | ||||||
| const size_t FLIPPER_DEBUG_APPS_COUNT = sizeof(FLIPPER_DEBUG_APPS) / sizeof(FlipperApplication); | const size_t FLIPPER_DEBUG_APPS_COUNT = sizeof(FLIPPER_DEBUG_APPS) / sizeof(FlipperApplication); | ||||||
| 
 | 
 | ||||||
|  | #ifdef APP_ARCHIVE | ||||||
|  | const FlipperApplication FLIPPER_ARCHIVE = | ||||||
|  |     {.app = app_archive, .name = "Archive", .stack_size = 4096, .icon = A_FileManager_14}; | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
| #ifdef SRV_DOLPHIN | #ifdef SRV_DOLPHIN | ||||||
| const FlipperApplication FLIPPER_SCENE = | const FlipperApplication FLIPPER_SCENE = | ||||||
|     {.app = scene_app, .name = "Scenes", .stack_size = 1024, .icon = A_Games_14}; |     {.app = scene_app, .name = "Scenes", .stack_size = 1024, .icon = A_Games_14}; | ||||||
|  | |||||||
| @ -48,3 +48,5 @@ extern const size_t FLIPPER_DEBUG_APPS_COUNT; | |||||||
| extern const FlipperApplication FLIPPER_SCENE; | extern const FlipperApplication FLIPPER_SCENE; | ||||||
| extern const FlipperApplication FLIPPER_SCENE_APPS[]; | extern const FlipperApplication FLIPPER_SCENE_APPS[]; | ||||||
| extern const size_t FLIPPER_SCENE_APPS_COUNT; | extern const size_t FLIPPER_SCENE_APPS_COUNT; | ||||||
|  | 
 | ||||||
|  | extern const FlipperApplication FLIPPER_ARCHIVE; | ||||||
| @ -29,6 +29,7 @@ APP_GPIO_DEMO = 1 | |||||||
| APP_MUSIC_PLAYER = 1 | APP_MUSIC_PLAYER = 1 | ||||||
| APP_FLOOPPER_BLOOPPER = 1 | APP_FLOOPPER_BLOOPPER = 1 | ||||||
| APP_IBUTTON = 1 | APP_IBUTTON = 1 | ||||||
|  | APP_ARCHIVE = 1 | ||||||
| 
 | 
 | ||||||
| # Debug and misc
 | # Debug and misc
 | ||||||
| APP_GUI_TEST = 1 | APP_GUI_TEST = 1 | ||||||
| @ -85,6 +86,12 @@ ifeq ($(APP_UNIT_TESTS), 1) | |||||||
| CFLAGS		+= -DAPP_UNIT_TESTS | CFLAGS		+= -DAPP_UNIT_TESTS | ||||||
| endif | endif | ||||||
| 
 | 
 | ||||||
|  | APP_ARCHIVE ?= 0 | ||||||
|  | ifeq ($(APP_NFC), 1) | ||||||
|  | CFLAGS		+= -DAPP_ARCHIVE | ||||||
|  | APP_ARCHIVE = 1 | ||||||
|  | endif | ||||||
|  | 
 | ||||||
| SRV_EXAMPLE_BLINK ?= 0 | SRV_EXAMPLE_BLINK ?= 0 | ||||||
| ifeq ($(SRV_EXAMPLE_BLINK), 1) | ifeq ($(SRV_EXAMPLE_BLINK), 1) | ||||||
| CFLAGS		+= -DSRV_EXAMPLE_BLINK | CFLAGS		+= -DSRV_EXAMPLE_BLINK | ||||||
|  | |||||||
							
								
								
									
										535
									
								
								applications/archive/archive.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,535 @@ | |||||||
|  | #include "archive_i.h" | ||||||
|  | 
 | ||||||
|  | static bool archive_get_filenames(ArchiveApp* archive); | ||||||
|  | 
 | ||||||
|  | static void update_offset(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     size_t array_size = files_array_size(model->files); | ||||||
|  |     uint16_t bounds = array_size > 3 ? 2 : array_size; | ||||||
|  | 
 | ||||||
|  |     if(model->list_offset < model->idx - bounds) { | ||||||
|  |         model->list_offset = CLAMP(model->list_offset + 1, array_size - (bounds + 2), 0); | ||||||
|  |     } else if(model->list_offset > model->idx - bounds) { | ||||||
|  |         model->list_offset = CLAMP(model->idx - 1, array_size - (bounds), 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_update_last_idx(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  | 
 | ||||||
|  |     archive->browser.last_idx[archive->browser.depth] = | ||||||
|  |         CLAMP(model->idx, files_array_size(model->files) - 1, 0); | ||||||
|  |     model->idx = 0; | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | 
 | ||||||
|  |     model = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_switch_dir(ArchiveApp* archive, const char* path) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(path); | ||||||
|  | 
 | ||||||
|  |     string_set(archive->browser.path, path); | ||||||
|  |     archive_get_filenames(archive); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_switch_tab(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  | 
 | ||||||
|  |     model->tab_idx = archive->browser.tab_id; | ||||||
|  |     model->idx = 0; | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | 
 | ||||||
|  |     model = NULL; | ||||||
|  | 
 | ||||||
|  |     archive->browser.depth = 0; | ||||||
|  |     archive_switch_dir(archive, tab_default_paths[archive->browser.tab_id]); | ||||||
|  | 
 | ||||||
|  |     update_offset(archive); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_leave_dir(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     char* path_ptr = stringi_get_cstr(archive->browser.path); | ||||||
|  |     char* last_char = strrchr(path_ptr, '/'); | ||||||
|  |     if(last_char) path_ptr[last_char - path_ptr] = '\0'; | ||||||
|  | 
 | ||||||
|  |     archive->browser.depth = CLAMP(archive->browser.depth - 1, MAX_DEPTH, 0); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     model->idx = archive->browser.last_idx[archive->browser.depth]; | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | 
 | ||||||
|  |     archive_switch_dir(archive, string_get_cstr(archive->browser.path)); | ||||||
|  | 
 | ||||||
|  |     update_offset(archive); | ||||||
|  | 
 | ||||||
|  |     model = NULL; | ||||||
|  |     path_ptr = NULL; | ||||||
|  |     last_char = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_enter_dir(ArchiveApp* archive, string_t name) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(name); | ||||||
|  | 
 | ||||||
|  |     archive_update_last_idx(archive); | ||||||
|  |     archive->browser.depth = CLAMP(archive->browser.depth + 1, MAX_DEPTH, 0); | ||||||
|  | 
 | ||||||
|  |     string_cat(archive->browser.path, "/"); | ||||||
|  |     string_cat(archive->browser.path, archive->browser.name); | ||||||
|  | 
 | ||||||
|  |     archive_switch_dir(archive, string_get_cstr(archive->browser.path)); | ||||||
|  | 
 | ||||||
|  |     update_offset(archive); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool filter_by_extension(ArchiveApp* archive, FileInfo* file_info, char* name) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(file_info); | ||||||
|  |     furi_assert(name); | ||||||
|  | 
 | ||||||
|  |     bool result = false; | ||||||
|  |     const char* filter_ext_ptr = get_tab_ext(archive->browser.tab_id); | ||||||
|  | 
 | ||||||
|  |     if(strcmp(filter_ext_ptr, "*") == 0) { | ||||||
|  |         result = true; | ||||||
|  |     } else if(strstr(name, filter_ext_ptr) != NULL) { | ||||||
|  |         result = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void set_file_type(ArchiveFile_t* file, FileInfo* file_info) { | ||||||
|  |     furi_assert(file); | ||||||
|  |     furi_assert(file_info); | ||||||
|  | 
 | ||||||
|  |     for(size_t i = 0; i < SIZEOF_ARRAY(known_ext); i++) { | ||||||
|  |         if(string_search_str(file->name, known_ext[i], 0) != STRING_FAILURE) { | ||||||
|  |             file->type = i; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(file_info->flags & FSF_DIRECTORY) { | ||||||
|  |         file->type = ArchiveFileTypeFolder; | ||||||
|  |     } else { | ||||||
|  |         file->type = ArchiveFileTypeUnknown; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool archive_get_filenames(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     FS_Dir_Api* dir_api = &archive->fs_api->dir; | ||||||
|  |     ArchiveFile_t item; | ||||||
|  |     FileInfo file_info; | ||||||
|  |     File directory; | ||||||
|  |     string_t name; | ||||||
|  |     bool result; | ||||||
|  | 
 | ||||||
|  |     string_init_printf(name, "%0*d\n", MAX_NAME_LEN, 0); | ||||||
|  |     result = dir_api->open(&directory, string_get_cstr(archive->browser.path)); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     files_array_clear(model->files); | ||||||
|  | 
 | ||||||
|  |     if(!result) { | ||||||
|  |         dir_api->close(&directory); | ||||||
|  |         string_clear(name); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     while(1) { | ||||||
|  |         char* name_ptr = stringi_get_cstr(name); | ||||||
|  |         result = dir_api->read(&directory, &file_info, name_ptr, MAX_NAME_LEN); | ||||||
|  | 
 | ||||||
|  |         if(directory.error_id == FSE_NOT_EXIST || name_ptr[0] == 0) { | ||||||
|  |             view_commit_model(archive->view_archive_main, true); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(result) { | ||||||
|  |             if(directory.error_id == FSE_OK) { | ||||||
|  |                 if(filter_by_extension(archive, &file_info, name_ptr)) { | ||||||
|  |                     ArchiveFile_t_init(&item); | ||||||
|  |                     string_init_set(item.name, name); | ||||||
|  |                     set_file_type(&item, &file_info); | ||||||
|  | 
 | ||||||
|  |                     files_array_push_back(model->files, item); | ||||||
|  |                     ArchiveFile_t_clear(&item); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             } else { | ||||||
|  |                 dir_api->close(&directory); | ||||||
|  |                 string_clear(name); | ||||||
|  |                 view_commit_model(archive->view_archive_main, true); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  |     model = NULL; | ||||||
|  | 
 | ||||||
|  |     dir_api->close(&directory); | ||||||
|  |     string_clear(name); | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_exit_callback(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     AppEvent event; | ||||||
|  |     event.type = EventTypeExit; | ||||||
|  |     furi_check(osMessageQueuePut(archive->event_queue, &event, 0, osWaitForever) == osOK); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static uint32_t archive_previous_callback(void* context) { | ||||||
|  |     return ArchiveViewMain; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* file menu */ | ||||||
|  | static void archive_add_to_favorites(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     FS_Common_Api* common_api = &archive->fs_api->common; | ||||||
|  | 
 | ||||||
|  |     string_t buffer_src; | ||||||
|  |     string_t buffer_dst; | ||||||
|  | 
 | ||||||
|  |     string_init_set(buffer_src, archive->browser.path); | ||||||
|  |     string_cat(buffer_src, "/"); | ||||||
|  |     string_cat(buffer_src, archive->browser.name); | ||||||
|  | 
 | ||||||
|  |     string_init_set_str(buffer_dst, "/favorites/"); | ||||||
|  |     string_cat(buffer_dst, archive->browser.name); | ||||||
|  | 
 | ||||||
|  |     common_api->rename(string_get_cstr(buffer_src), string_get_cstr(buffer_dst)); | ||||||
|  | 
 | ||||||
|  |     string_clear(buffer_src); | ||||||
|  |     string_clear(buffer_dst); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_text_input_callback(void* context, char* text) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     furi_assert(text); | ||||||
|  | 
 | ||||||
|  |     ArchiveApp* archive = (ArchiveApp*)context; | ||||||
|  |     FS_Common_Api* common_api = &archive->fs_api->common; | ||||||
|  | 
 | ||||||
|  |     string_t buffer_src; | ||||||
|  |     string_t buffer_dst; | ||||||
|  | 
 | ||||||
|  |     string_init_set(buffer_src, archive->browser.path); | ||||||
|  |     string_init_set(buffer_dst, archive->browser.path); | ||||||
|  | 
 | ||||||
|  |     string_cat(buffer_src, "/"); | ||||||
|  |     string_cat(buffer_dst, "/"); | ||||||
|  | 
 | ||||||
|  |     string_cat(buffer_src, archive->browser.name); | ||||||
|  |     string_cat_str(buffer_dst, text); | ||||||
|  | 
 | ||||||
|  |     common_api->rename(string_get_cstr(buffer_src), string_get_cstr(buffer_dst)); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewMain); | ||||||
|  | 
 | ||||||
|  |     string_clear(buffer_src); | ||||||
|  |     string_clear(buffer_dst); | ||||||
|  | 
 | ||||||
|  |     archive_get_filenames(archive); | ||||||
|  | } | ||||||
|  | static void archive_enter_text_input(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     string_set(archive->browser.text_input_buffer, archive->browser.name); | ||||||
|  | 
 | ||||||
|  |     char* text_input_buffer_ptr = stringi_get_cstr(archive->browser.text_input_buffer); | ||||||
|  | 
 | ||||||
|  |     text_input_set_header_text(archive->text_input, "Rename:"); | ||||||
|  | 
 | ||||||
|  |     text_input_set_result_callback( | ||||||
|  |         archive->text_input, | ||||||
|  |         archive_text_input_callback, | ||||||
|  |         archive, | ||||||
|  |         text_input_buffer_ptr, | ||||||
|  |         MAX_NAME_LEN); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewTextInput); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_show_file_menu(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     archive->browser.menu = true; | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     model->menu = true; | ||||||
|  |     model->menu_idx = 0; | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_close_file_menu(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     archive->browser.menu = false; | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     model->menu = false; | ||||||
|  |     model->menu_idx = 0; | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | archive_open_app(ArchiveApp* archive, const FlipperApplication* flipper_app, void* arg) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(flipper_app); | ||||||
|  |     furi_assert(flipper_app->app); | ||||||
|  |     furi_assert(flipper_app->name); | ||||||
|  | 
 | ||||||
|  |     if(arg) { | ||||||
|  |         // pass path to app?
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_thread_set_name(archive->app_thread, flipper_app->name); | ||||||
|  |     furi_thread_set_stack_size(archive->app_thread, flipper_app->stack_size); | ||||||
|  |     furi_thread_set_callback(archive->app_thread, flipper_app->app); | ||||||
|  |     furi_thread_start(archive->app_thread); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_delete_file(ArchiveApp* archive, string_t name) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(name); | ||||||
|  | 
 | ||||||
|  |     FS_Common_Api* common_api = &archive->fs_api->common; | ||||||
|  | 
 | ||||||
|  |     string_t path; | ||||||
|  |     string_init_set(path, archive->browser.path); | ||||||
|  |     string_cat(path, "/"); | ||||||
|  |     string_cat(path, name); | ||||||
|  | 
 | ||||||
|  |     common_api->remove(string_get_cstr(path)); | ||||||
|  |     string_clear(path); | ||||||
|  | 
 | ||||||
|  |     archive_get_filenames(archive); | ||||||
|  | 
 | ||||||
|  |     update_offset(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     model->idx = CLAMP(model->idx, files_array_size(model->files) - 1, 0); | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_file_menu_callback(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     ArchiveFile_t* selected = files_array_get(model->files, model->idx); | ||||||
|  | 
 | ||||||
|  |     switch(model->menu_idx) { | ||||||
|  |     case 0: | ||||||
|  |         if((selected->type != ArchiveFileTypeFolder && selected->type != ArchiveFileTypeUnknown)) { | ||||||
|  |             archive_open_app(archive, &FLIPPER_APPS[selected->type], NULL); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |     case 1: | ||||||
|  | 
 | ||||||
|  |         string_set(archive->browser.name, selected->name); | ||||||
|  |         archive_add_to_favorites(archive); | ||||||
|  |         archive_close_file_menu(archive); | ||||||
|  |         break; | ||||||
|  |     case 2: | ||||||
|  |         // open rename view
 | ||||||
|  |         archive_enter_text_input(archive); | ||||||
|  |         break; | ||||||
|  |     case 3: | ||||||
|  |         // confirmation?
 | ||||||
|  |         archive_delete_file(archive, selected->name); | ||||||
|  |         archive_close_file_menu(archive); | ||||||
|  |         break; | ||||||
|  | 
 | ||||||
|  |     default: | ||||||
|  |         archive_close_file_menu(archive); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     model = NULL; | ||||||
|  |     selected = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void menu_input_handler(ArchiveApp* archive, InputEvent* event) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     if(event->type == InputTypeShort) { | ||||||
|  |         if(event->key == InputKeyUp) { | ||||||
|  |             model->menu_idx = CLAMP(model->menu_idx - 1, MENU_ITEMS - 1, 0); | ||||||
|  |         } else if(event->key == InputKeyDown) { | ||||||
|  |             model->menu_idx = CLAMP(model->menu_idx + 1, MENU_ITEMS - 1, 0); | ||||||
|  |         } else if(event->key == InputKeyOk) { | ||||||
|  |             archive_file_menu_callback(archive); | ||||||
|  |         } else if(event->key == InputKeyBack) { | ||||||
|  |             archive_close_file_menu(archive); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     view_commit_model(archive->view_archive_main, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* main controls */ | ||||||
|  | 
 | ||||||
|  | static bool archive_view_input(InputEvent* event, void* context) { | ||||||
|  |     furi_assert(event); | ||||||
|  |     furi_assert(context); | ||||||
|  | 
 | ||||||
|  |     ArchiveApp* archive = context; | ||||||
|  |     ArchiveViewModel* model = view_get_model(archive->view_archive_main); | ||||||
|  |     bool in_menu = archive->browser.menu; | ||||||
|  | 
 | ||||||
|  |     if(in_menu) { | ||||||
|  |         menu_input_handler(archive, event); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(event->type == InputTypeShort) { | ||||||
|  |         if(event->key == InputKeyLeft) { | ||||||
|  |             archive->browser.tab_id = CLAMP(archive->browser.tab_id - 1, ArchiveTabTotal, 0); | ||||||
|  |             archive_switch_tab(archive); | ||||||
|  |             return true; | ||||||
|  | 
 | ||||||
|  |         } else if(event->key == InputKeyRight) { | ||||||
|  |             archive->browser.tab_id = CLAMP(archive->browser.tab_id + 1, ArchiveTabTotal - 1, 0); | ||||||
|  |             archive_switch_tab(archive); | ||||||
|  |             return true; | ||||||
|  | 
 | ||||||
|  |         } else if(event->key == InputKeyBack) { | ||||||
|  |             if(archive->browser.depth == 0) { | ||||||
|  |                 archive_exit_callback(archive); | ||||||
|  |             } else { | ||||||
|  |                 archive_leave_dir(archive); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     size_t num_elements = files_array_size(model->files) - 1; | ||||||
|  |     if((event->type == InputTypeShort || event->type == InputTypeRepeat)) { | ||||||
|  |         if(event->key == InputKeyUp) { | ||||||
|  |             model->idx = CLAMP(model->idx - 1, num_elements, 0); | ||||||
|  |             update_offset(archive); | ||||||
|  |             return true; | ||||||
|  |         } else if(event->key == InputKeyDown) { | ||||||
|  |             model->idx = CLAMP(model->idx + 1, num_elements, 0); | ||||||
|  |             update_offset(archive); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(event->key == InputKeyOk) { | ||||||
|  |         if(files_array_size(model->files) > 0) { | ||||||
|  |             ArchiveFile_t* selected = files_array_get(model->files, model->idx); | ||||||
|  |             string_set(archive->browser.name, selected->name); | ||||||
|  |             model = NULL; | ||||||
|  | 
 | ||||||
|  |             if(selected->type == ArchiveFileTypeFolder) { | ||||||
|  |                 if(event->type == InputTypeShort) { | ||||||
|  |                     archive_enter_dir(archive, archive->browser.name); | ||||||
|  |                 } else if(event->type == InputTypeLong) { | ||||||
|  |                     archive_show_file_menu(archive); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if(event->type == InputTypeShort) { | ||||||
|  |                     archive_show_file_menu(archive); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void archive_free(ArchiveApp* archive) { | ||||||
|  |     furi_assert(archive); | ||||||
|  | 
 | ||||||
|  |     text_input_free(archive->text_input); | ||||||
|  | 
 | ||||||
|  |     furi_record_close("sdcard"); | ||||||
|  |     archive->fs_api = NULL; | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewMain); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewTextInput); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_free(archive->view_dispatcher); | ||||||
|  | 
 | ||||||
|  |     furi_record_close("gui"); | ||||||
|  |     archive->gui = NULL; | ||||||
|  | 
 | ||||||
|  |     furi_thread_free(archive->app_thread); | ||||||
|  | 
 | ||||||
|  |     osMessageQueueDelete(archive->event_queue); | ||||||
|  | 
 | ||||||
|  |     free(archive); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ArchiveApp* archive_alloc() { | ||||||
|  |     ArchiveApp* archive = furi_alloc(sizeof(ArchiveApp)); | ||||||
|  | 
 | ||||||
|  |     archive->event_queue = osMessageQueueNew(2, sizeof(AppEvent), NULL); | ||||||
|  |     archive->app_thread = furi_thread_alloc(); | ||||||
|  |     archive->gui = furi_record_open("gui"); | ||||||
|  |     archive->view_dispatcher = view_dispatcher_alloc(); | ||||||
|  |     archive->fs_api = furi_record_open("sdcard"); | ||||||
|  |     archive->text_input = text_input_alloc(); | ||||||
|  |     archive->view_archive_main = view_alloc(); | ||||||
|  | 
 | ||||||
|  |     furi_check(archive->event_queue); | ||||||
|  | 
 | ||||||
|  |     view_allocate_model( | ||||||
|  |         archive->view_archive_main, ViewModelTypeLockFree, sizeof(ArchiveViewModel)); | ||||||
|  |     view_set_context(archive->view_archive_main, archive); | ||||||
|  |     view_set_draw_callback(archive->view_archive_main, archive_view_render); | ||||||
|  |     view_set_input_callback(archive->view_archive_main, archive_view_input); | ||||||
|  |     view_set_previous_callback( | ||||||
|  |         text_input_get_view(archive->text_input), archive_previous_callback); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_add_view( | ||||||
|  |         archive->view_dispatcher, ArchiveViewMain, archive->view_archive_main); | ||||||
|  |     view_dispatcher_add_view( | ||||||
|  |         archive->view_dispatcher, ArchiveViewTextInput, text_input_get_view(archive->text_input)); | ||||||
|  |     view_dispatcher_attach_to_gui( | ||||||
|  |         archive->view_dispatcher, archive->gui, ViewDispatcherTypeFullscreen); | ||||||
|  | 
 | ||||||
|  |     view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveTabFavorites); | ||||||
|  | 
 | ||||||
|  |     return archive; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | int32_t app_archive(void* p) { | ||||||
|  |     ArchiveApp* archive = archive_alloc(); | ||||||
|  | 
 | ||||||
|  |     // default tab
 | ||||||
|  |     archive_switch_tab(archive); | ||||||
|  | 
 | ||||||
|  |     AppEvent event; | ||||||
|  |     while(1) { | ||||||
|  |         furi_check(osMessageQueueGet(archive->event_queue, &event, NULL, osWaitForever) == osOK); | ||||||
|  |         if(event.type == EventTypeExit) { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     archive_free(archive); | ||||||
|  |     return 0; | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								applications/archive/archive.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | typedef struct ArchiveApp ArchiveApp; | ||||||
							
								
								
									
										106
									
								
								applications/archive/archive_i.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,106 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "archive.h" | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <furi.h> | ||||||
|  | #include <gui/gui_i.h> | ||||||
|  | #include <gui/view_dispatcher.h> | ||||||
|  | #include <gui/modules/text_input.h> | ||||||
|  | 
 | ||||||
|  | #include <m-string.h> | ||||||
|  | #include <m-array.h> | ||||||
|  | #include <filesystem-api.h> | ||||||
|  | #include "archive_views.h" | ||||||
|  | #include "applications.h" | ||||||
|  | 
 | ||||||
|  | #define MAX_DEPTH 32 | ||||||
|  | #define MAX_NAME_LEN 255 | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     ArchiveViewMain, | ||||||
|  |     ArchiveViewTextInput, | ||||||
|  |     ArchiveViewTotal, | ||||||
|  | } ArchiveViewEnum; | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     ArchiveTabFavorites, | ||||||
|  |     ArchiveTabIButton, | ||||||
|  |     ArchiveTabNFC, | ||||||
|  |     ArchiveTabSubOne, | ||||||
|  |     ArchiveTabLFRFID, | ||||||
|  |     ArchiveTabIrda, | ||||||
|  |     ArchiveTabBrowser, | ||||||
|  |     ArchiveTabTotal, | ||||||
|  | } ArchiveTabEnum; | ||||||
|  | 
 | ||||||
|  | static const char* known_ext[] = { | ||||||
|  |     [ArchiveFileTypeIButton] = ".ibtn", | ||||||
|  |     [ArchiveFileTypeNFC] = ".nfc", | ||||||
|  |     [ArchiveFileTypeSubOne] = ".sub1", | ||||||
|  |     [ArchiveFileTypeLFRFID] = ".rfid", | ||||||
|  |     [ArchiveFileTypeIrda] = ".irda", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static const char* tab_default_paths[] = { | ||||||
|  |     [ArchiveTabFavorites] = "favorites", | ||||||
|  |     [ArchiveTabIButton] = "ibutton", | ||||||
|  |     [ArchiveTabNFC] = "nfc", | ||||||
|  |     [ArchiveTabSubOne] = "subone", | ||||||
|  |     [ArchiveTabLFRFID] = "lfrfid", | ||||||
|  |     [ArchiveTabIrda] = "irda", | ||||||
|  |     [ArchiveTabBrowser] = "/", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static inline const char* get_tab_ext(ArchiveTabEnum tab) { | ||||||
|  |     switch(tab) { | ||||||
|  |     case ArchiveTabIButton: | ||||||
|  |         return known_ext[ArchiveFileTypeIButton]; | ||||||
|  |     case ArchiveTabNFC: | ||||||
|  |         return known_ext[ArchiveFileTypeNFC]; | ||||||
|  |     case ArchiveTabSubOne: | ||||||
|  |         return known_ext[ArchiveFileTypeSubOne]; | ||||||
|  |     case ArchiveTabLFRFID: | ||||||
|  |         return known_ext[ArchiveFileTypeLFRFID]; | ||||||
|  |     case ArchiveTabIrda: | ||||||
|  |         return known_ext[ArchiveFileTypeIrda]; | ||||||
|  |     default: | ||||||
|  |         return "*"; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     EventTypeTick, | ||||||
|  |     EventTypeKey, | ||||||
|  |     EventTypeExit, | ||||||
|  | } EventType; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     union { | ||||||
|  |         InputEvent input; | ||||||
|  |     } value; | ||||||
|  |     EventType type; | ||||||
|  | } AppEvent; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     ArchiveTabEnum tab_id; | ||||||
|  |     string_t name; | ||||||
|  |     string_t path; | ||||||
|  |     string_t text_input_buffer; | ||||||
|  | 
 | ||||||
|  |     uint8_t depth; | ||||||
|  |     uint16_t last_idx[MAX_DEPTH]; | ||||||
|  | 
 | ||||||
|  |     bool menu; | ||||||
|  | } ArchiveBrowser; | ||||||
|  | 
 | ||||||
|  | struct ArchiveApp { | ||||||
|  |     osMessageQueueId_t event_queue; | ||||||
|  |     FuriThread* app_thread; | ||||||
|  |     Gui* gui; | ||||||
|  |     ViewDispatcher* view_dispatcher; | ||||||
|  |     View* view_archive_main; | ||||||
|  |     TextInput* text_input; | ||||||
|  | 
 | ||||||
|  |     FS_Api* fs_api; | ||||||
|  |     ArchiveBrowser browser; | ||||||
|  | }; | ||||||
							
								
								
									
										164
									
								
								applications/archive/archive_views.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,164 @@ | |||||||
|  | #include "archive_views.h" | ||||||
|  | 
 | ||||||
|  | static const char* ArchiveTabNames[] = | ||||||
|  |     {"Favorites", "iButton", "NFC", "SubOne", "Rfid", "Infared", "Browser"}; | ||||||
|  | 
 | ||||||
|  | static const IconName ArchiveItemIcons[] = { | ||||||
|  |     [ArchiveFileTypeIButton] = I_ibutt_10px, | ||||||
|  |     [ArchiveFileTypeNFC] = I_Nfc_10px, | ||||||
|  |     [ArchiveFileTypeSubOne] = I_sub1_10px, | ||||||
|  |     [ArchiveFileTypeLFRFID] = I_125_10px, | ||||||
|  |     [ArchiveFileTypeIrda] = I_ir_10px, | ||||||
|  |     [ArchiveFileTypeFolder] = I_dir_10px, | ||||||
|  |     [ArchiveFileTypeUnknown] = I_unknown_10px, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static inline bool is_known_app(ArchiveFileTypeEnum type) { | ||||||
|  |     return (type != ArchiveFileTypeFolder && type != ArchiveFileTypeUnknown); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void render_item_menu(Canvas* canvas, ArchiveViewModel* model) { | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_box(canvas, 61, 17, 62, 46); | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     elements_slightly_rounded_frame(canvas, 60, 16, 64, 48); | ||||||
|  | 
 | ||||||
|  |     string_t menu[MENU_ITEMS]; | ||||||
|  | 
 | ||||||
|  |     string_init_set_str(menu[0], "Open in app"); | ||||||
|  |     string_init_set_str(menu[1], "Pin"); | ||||||
|  |     string_init_set_str(menu[2], "Rename"); | ||||||
|  |     string_init_set_str(menu[3], "Delete"); | ||||||
|  | 
 | ||||||
|  |     ArchiveFile_t* selected = files_array_get(model->files, model->idx); | ||||||
|  | 
 | ||||||
|  |     if(!is_known_app(selected->type)) { | ||||||
|  |         string_set_str(menu[0], "---"); | ||||||
|  |         string_set_str(menu[1], "---"); | ||||||
|  |     } else if(model->tab_idx == 0) { | ||||||
|  |         string_set_str(menu[1], "Move"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for(size_t i = 0; i < MENU_ITEMS; i++) { | ||||||
|  |         canvas_draw_str(canvas, 72, 27 + i * 11, string_get_cstr(menu[i])); | ||||||
|  |         string_clear(menu[i]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     canvas_draw_icon_name(canvas, 64, 20 + model->menu_idx * 11, I_ButtonRight_4x7); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void trim_file_ext(string_t name) { | ||||||
|  |     size_t str_len = strlen(string_get_cstr(name)); | ||||||
|  |     char* buff_ptr = stringi_get_cstr(name); | ||||||
|  |     char* end = buff_ptr + str_len; | ||||||
|  |     while(end > buff_ptr && *end != '.' && *end != '\\' && *end != '/') { | ||||||
|  |         --end; | ||||||
|  |     } | ||||||
|  |     if((end > buff_ptr && *end == '.') && (*(end - 1) != '\\' && *(end - 1) != '/')) { | ||||||
|  |         *end = '\0'; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void format_filename_buffer(Canvas* canvas, string_t name, ArchiveFileTypeEnum type) { | ||||||
|  |     furi_assert(name); | ||||||
|  | 
 | ||||||
|  |     size_t s_len = strlen(string_get_cstr(name)); | ||||||
|  |     uint16_t len_px = canvas_string_width(canvas, string_get_cstr(name)); | ||||||
|  | 
 | ||||||
|  |     if(is_known_app(type)) trim_file_ext(name); | ||||||
|  | 
 | ||||||
|  |     if(len_px > MAX_LEN_PX) { | ||||||
|  |         string_mid(name, 0, s_len - (size_t)((len_px - MAX_LEN_PX) / ((len_px / s_len) + 2) + 2)); | ||||||
|  |         string_cat(name, "..."); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) { | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     canvas_draw_box(canvas, 0, 15 + idx * FRAME_HEIGHT, scrollbar ? 122 : 127, FRAME_HEIGHT); | ||||||
|  | 
 | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_dot(canvas, 0, 15 + idx * FRAME_HEIGHT); | ||||||
|  |     canvas_draw_dot(canvas, 1, 15 + idx * FRAME_HEIGHT); | ||||||
|  |     canvas_draw_dot(canvas, 0, (15 + idx * FRAME_HEIGHT) + 1); | ||||||
|  | 
 | ||||||
|  |     canvas_draw_dot(canvas, 0, (15 + idx * FRAME_HEIGHT) + 11); | ||||||
|  |     canvas_draw_dot(canvas, scrollbar ? 121 : 126, 15 + idx * FRAME_HEIGHT); | ||||||
|  |     canvas_draw_dot(canvas, scrollbar ? 121 : 126, (15 + idx * FRAME_HEIGHT) + 11); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void draw_list(Canvas* canvas, ArchiveViewModel* model) { | ||||||
|  |     furi_assert(model); | ||||||
|  | 
 | ||||||
|  |     size_t array_size = files_array_size(model->files); | ||||||
|  |     bool scrollbar = array_size > 4; | ||||||
|  | 
 | ||||||
|  |     string_t str_buff; | ||||||
|  |     string_init(str_buff); | ||||||
|  | 
 | ||||||
|  |     for(size_t i = 0; i < MIN(MENU_ITEMS, array_size); ++i) { | ||||||
|  |         size_t idx = CLAMP(i + model->list_offset, array_size, 0); | ||||||
|  |         ArchiveFile_t* file = files_array_get(model->files, CLAMP(idx, array_size - 1, 0)); | ||||||
|  | 
 | ||||||
|  |         string_set(str_buff, file->name); | ||||||
|  |         format_filename_buffer(canvas, str_buff, file->type); | ||||||
|  | 
 | ||||||
|  |         if(model->idx == idx) { | ||||||
|  |             archive_draw_frame(canvas, i, scrollbar); | ||||||
|  |         } else { | ||||||
|  |             canvas_set_color(canvas, ColorBlack); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         canvas_draw_icon_name(canvas, 2, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file->type]); | ||||||
|  |         canvas_draw_str(canvas, 15, 24 + i * FRAME_HEIGHT, stringi_get_cstr(str_buff)); | ||||||
|  |         string_clean(str_buff); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(scrollbar) { | ||||||
|  |         elements_scrollbar_pos(canvas, 126, 16, 48, model->idx, array_size); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(model->menu) { | ||||||
|  |         render_item_menu(canvas, model); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     string_clear(str_buff); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void archive_render_status_bar(Canvas* canvas, ArchiveViewModel* model) { | ||||||
|  |     furi_assert(model); | ||||||
|  | 
 | ||||||
|  |     const char* tab_name = ArchiveTabNames[model->tab_idx]; | ||||||
|  | 
 | ||||||
|  |     canvas_draw_icon_name(canvas, 0, 0, I_Background_128x11); | ||||||
|  | 
 | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_box(canvas, 0, 0, 50, 13); | ||||||
|  |     canvas_draw_box(canvas, 100, 0, 28, 13); | ||||||
|  | 
 | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     elements_frame(canvas, 0, 0, 50, 13); | ||||||
|  |     canvas_draw_str_aligned(canvas, 25, 10, AlignCenter, AlignBottom, tab_name); | ||||||
|  | 
 | ||||||
|  |     elements_frame(canvas, 100, 0, 24, 13); | ||||||
|  | 
 | ||||||
|  |     if(model->tab_idx > 0) { | ||||||
|  |         canvas_draw_icon_name(canvas, 106, 3, I_ButtonLeft_4x7); | ||||||
|  |     } | ||||||
|  |     if(model->tab_idx < SIZEOF_ARRAY(ArchiveTabNames) - 1) { | ||||||
|  |         canvas_draw_icon_name(canvas, 114, 3, I_ButtonRight_4x7); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void archive_view_render(Canvas* canvas, void* model) { | ||||||
|  |     ArchiveViewModel* m = model; | ||||||
|  | 
 | ||||||
|  |     archive_render_status_bar(canvas, model); | ||||||
|  | 
 | ||||||
|  |     if(files_array_size(m->files) > 0) { | ||||||
|  |         draw_list(canvas, m); | ||||||
|  |     } else { | ||||||
|  |         canvas_draw_str_aligned( | ||||||
|  |             canvas, GUI_DISPLAY_WIDTH / 2, 40, AlignCenter, AlignCenter, "Empty"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								applications/archive/archive_views.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,65 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <gui/gui_i.h> | ||||||
|  | #include <gui/canvas.h> | ||||||
|  | #include <gui/elements.h> | ||||||
|  | #include <furi.h> | ||||||
|  | #include <filesystem-api.h> | ||||||
|  | 
 | ||||||
|  | #define MAX_LEN_PX 98 | ||||||
|  | #define FRAME_HEIGHT 12 | ||||||
|  | #define MENU_ITEMS 4 | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     ArchiveFileTypeIButton, | ||||||
|  |     ArchiveFileTypeNFC, | ||||||
|  |     ArchiveFileTypeSubOne, | ||||||
|  |     ArchiveFileTypeLFRFID, | ||||||
|  |     ArchiveFileTypeIrda, | ||||||
|  |     ArchiveFileTypeFolder, | ||||||
|  |     ArchiveFileTypeUnknown, | ||||||
|  |     AppIdTotal, | ||||||
|  | } ArchiveFileTypeEnum; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     string_t name; | ||||||
|  |     ArchiveFileTypeEnum type; | ||||||
|  | } ArchiveFile_t; | ||||||
|  | 
 | ||||||
|  | static void ArchiveFile_t_init(ArchiveFile_t* obj) { | ||||||
|  |     obj->type = ArchiveFileTypeUnknown; | ||||||
|  |     string_init(obj->name); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void ArchiveFile_t_init_set(ArchiveFile_t* obj, const ArchiveFile_t* src) { | ||||||
|  |     obj->type = src->type; | ||||||
|  |     string_init_set(obj->name, src->name); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void ArchiveFile_t_set(ArchiveFile_t* obj, const ArchiveFile_t* src) { | ||||||
|  |     obj->type = src->type; | ||||||
|  |     string_set(obj->name, src->name); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void ArchiveFile_t_clear(ArchiveFile_t* obj) { | ||||||
|  |     string_clear(obj->name); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ARRAY_DEF( | ||||||
|  |     files_array, | ||||||
|  |     ArchiveFile_t, | ||||||
|  |     (INIT(API_2(ArchiveFile_t_init)), | ||||||
|  |      SET(API_6(ArchiveFile_t_set)), | ||||||
|  |      INIT_SET(API_6(ArchiveFile_t_init_set)), | ||||||
|  |      CLEAR(API_2(ArchiveFile_t_clear)))) | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     uint8_t tab_idx; | ||||||
|  |     uint8_t menu_idx; | ||||||
|  |     uint16_t idx; | ||||||
|  |     uint16_t list_offset; | ||||||
|  |     files_array_t files; | ||||||
|  |     bool menu; | ||||||
|  | } ArchiveViewModel; | ||||||
|  | 
 | ||||||
|  | void archive_view_render(Canvas* canvas, void* model); | ||||||
| @ -60,31 +60,41 @@ bool dolphin_view_first_start_input(InputEvent* event, void* context) { | |||||||
| void dolphin_lock_handler(InputEvent* event, Dolphin* dolphin) { | void dolphin_lock_handler(InputEvent* event, Dolphin* dolphin) { | ||||||
|     furi_assert(event); |     furi_assert(event); | ||||||
|     furi_assert(dolphin); |     furi_assert(dolphin); | ||||||
|     if(event->key == InputKeyBack) { | 
 | ||||||
|  |     with_view_model( | ||||||
|  |         dolphin->idle_view_main, (DolphinViewMainModel * model) { | ||||||
|  |             model->hint_timeout = HINT_TIMEOUT_L; | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     if(event->key == InputKeyBack && event->type == InputTypeShort) { | ||||||
|         uint32_t press_time = HAL_GetTick(); |         uint32_t press_time = HAL_GetTick(); | ||||||
| 
 | 
 | ||||||
|         // check if pressed sequentially
 |         // check if pressed sequentially
 | ||||||
|         if(press_time - dolphin->lock_lastpress < 200) { |         if(press_time - dolphin->lock_lastpress > UNLOCK_RST_TIMEOUT) { | ||||||
|             dolphin->lock_lastpress = press_time; |  | ||||||
|             dolphin->lock_count++; |  | ||||||
|         } else if(press_time - dolphin->lock_lastpress > 200) { |  | ||||||
|             dolphin->lock_lastpress = press_time; |             dolphin->lock_lastpress = press_time; | ||||||
|             dolphin->lock_count = 0; |             dolphin->lock_count = 0; | ||||||
|  |         } else if(press_time - dolphin->lock_lastpress < UNLOCK_RST_TIMEOUT) { | ||||||
|  |             dolphin->lock_lastpress = press_time; | ||||||
|  |             dolphin->lock_count++; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(dolphin->lock_count == 3) { |         if(dolphin->lock_count == 2) { | ||||||
|             dolphin->locked = false; |             dolphin->locked = false; | ||||||
|             dolphin->lock_count = 0; |             dolphin->lock_count = 0; | ||||||
| 
 | 
 | ||||||
|             with_view_model( |             with_view_model( | ||||||
|                 dolphin->view_lockmenu, (DolphinViewLockMenuModel * model) { |                 dolphin->view_lockmenu, (DolphinViewLockMenuModel * model) { | ||||||
|                     model->locked = false; |                     model->locked = false; | ||||||
|  |                     model->door_left_x = -57; // move doors to default pos
 | ||||||
|  |                     model->door_right_x = 115; | ||||||
|                     return true; |                     return true; | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             with_view_model( |             with_view_model( | ||||||
|                 dolphin->idle_view_main, (DolphinViewMainModel * model) { |                 dolphin->idle_view_main, (DolphinViewMainModel * model) { | ||||||
|                     model->hint_timeout = 0; |                     model->hint_timeout = HINT_TIMEOUT_L; // "unlocked" hint timeout
 | ||||||
|  |                     model->locked = false; | ||||||
|                     return true; |                     return true; | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
| @ -112,9 +122,7 @@ bool dolphin_view_idle_main_input(InputEvent* event, void* context) { | |||||||
|         } else if(event->key == InputKeyRight && event->type == InputTypeShort) { |         } else if(event->key == InputKeyRight && event->type == InputTypeShort) { | ||||||
|             dolphin_switch_to_app(dolphin, &FLIPPER_SCENE); |             dolphin_switch_to_app(dolphin, &FLIPPER_SCENE); | ||||||
|         } else if(event->key == InputKeyDown && event->type == InputTypeShort) { |         } else if(event->key == InputKeyDown && event->type == InputTypeShort) { | ||||||
| #if 0 |             dolphin_switch_to_app(dolphin, &FLIPPER_ARCHIVE); | ||||||
|             dolphin_switch_to_app(dolphin, &ARCHIVE_APP); |  | ||||||
| #endif |  | ||||||
|         } else if(event->key == InputKeyDown && event->type == InputTypeLong) { |         } else if(event->key == InputKeyDown && event->type == InputTypeLong) { | ||||||
|             view_dispatcher_switch_to_view(dolphin->idle_view_dispatcher, DolphinViewStats); |             view_dispatcher_switch_to_view(dolphin->idle_view_dispatcher, DolphinViewStats); | ||||||
|         } else if(event->key == InputKeyBack && event->type == InputTypeShort) { |         } else if(event->key == InputKeyBack && event->type == InputTypeShort) { | ||||||
| @ -123,12 +131,6 @@ bool dolphin_view_idle_main_input(InputEvent* event, void* context) { | |||||||
|     } else { |     } else { | ||||||
|         // locked
 |         // locked
 | ||||||
| 
 | 
 | ||||||
|         with_view_model( |  | ||||||
|             dolphin->idle_view_main, (DolphinViewMainModel * model) { |  | ||||||
|                 model->hint_timeout = 3; |  | ||||||
|                 return true; |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|         dolphin_lock_handler(event, dolphin); |         dolphin_lock_handler(event, dolphin); | ||||||
|         dolphin_scene_handler_switch_scene(dolphin); |         dolphin_scene_handler_switch_scene(dolphin); | ||||||
|     } |     } | ||||||
| @ -151,14 +153,19 @@ static void lock_menu_callback(void* context, uint8_t index) { | |||||||
|     // lock
 |     // lock
 | ||||||
|     case 0: |     case 0: | ||||||
|         dolphin->locked = true; |         dolphin->locked = true; | ||||||
|         DolphinViewLockMenuModel* model = view_get_model(dolphin->view_lockmenu); |  | ||||||
| 
 | 
 | ||||||
|         model->locked = true; |         with_view_model( | ||||||
|         model->exit_timeout = 20; |             dolphin->view_lockmenu, (DolphinViewLockMenuModel * model) { | ||||||
| 
 |                 model->locked = true; | ||||||
|         view_port_enabled_set(dolphin->lock_viewport, dolphin->locked); |                 model->exit_timeout = HINT_TIMEOUT_H; | ||||||
|         view_commit_model(dolphin->view_lockmenu, true); |                 return true; | ||||||
|  |             }); | ||||||
| 
 | 
 | ||||||
|  |         with_view_model( | ||||||
|  |             dolphin->idle_view_main, (DolphinViewMainModel * model) { | ||||||
|  |                 model->locked = true; | ||||||
|  |                 return true; | ||||||
|  |             }); | ||||||
|         break; |         break; | ||||||
| 
 | 
 | ||||||
|     default: |     default: | ||||||
|  | |||||||
| @ -14,6 +14,10 @@ | |||||||
| #include <assets_icons.h> | #include <assets_icons.h> | ||||||
| #include <stdint.h> | #include <stdint.h> | ||||||
| 
 | 
 | ||||||
|  | #define UNLOCK_RST_TIMEOUT 500 // keypress counter reset timeout (ms)
 | ||||||
|  | #define HINT_TIMEOUT_L 3 // low refresh rate timeout (app ticks)
 | ||||||
|  | #define HINT_TIMEOUT_H 40 // high refresh rate timeout (app ticks)
 | ||||||
|  | 
 | ||||||
| typedef enum { | typedef enum { | ||||||
|     DolphinEventTypeDeed, |     DolphinEventTypeDeed, | ||||||
|     DolphinEventTypeSave, |     DolphinEventTypeSave, | ||||||
|  | |||||||
| @ -65,8 +65,13 @@ void dolphin_view_idle_main_draw(Canvas* canvas, void* model) { | |||||||
| 
 | 
 | ||||||
|     if(m->hint_timeout > 0) { |     if(m->hint_timeout > 0) { | ||||||
|         m->hint_timeout--; |         m->hint_timeout--; | ||||||
|         canvas_draw_icon_name(canvas, 13, 5, I_LockPopup_100x49); |         if(m->locked) { | ||||||
|         elements_multiline_text(canvas, 65, 20, "To unlock\npress:"); |             canvas_draw_icon_name(canvas, 13, 5, I_LockPopup_100x49); | ||||||
|  |             elements_multiline_text(canvas, 65, 20, "To unlock\npress:"); | ||||||
|  |         } else { | ||||||
|  |             canvas_set_font(canvas, FontPrimary); | ||||||
|  |             elements_multiline_text_framed(canvas, 42, 30, "Unlocked"); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -81,8 +86,8 @@ void dolphin_view_lockmenu_draw(Canvas* canvas, void* model) { | |||||||
|     if(m->locked) { |     if(m->locked) { | ||||||
|         m->exit_timeout--; |         m->exit_timeout--; | ||||||
| 
 | 
 | ||||||
|         m->door_left_x = CLAMP(m->door_left_x + 10, 0, -57); |         m->door_left_x = CLAMP(m->door_left_x + 5, 0, -57); | ||||||
|         m->door_right_x = CLAMP(m->door_right_x - 10, 115, 60); |         m->door_right_x = CLAMP(m->door_right_x - 5, 115, 60); | ||||||
| 
 | 
 | ||||||
|         if(m->door_left_x > -10) { |         if(m->door_left_x > -10) { | ||||||
|             canvas_set_font(canvas, FontPrimary); |             canvas_set_font(canvas, FontPrimary); | ||||||
| @ -90,9 +95,6 @@ void dolphin_view_lockmenu_draw(Canvas* canvas, void* model) { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } else { |     } else { | ||||||
|         m->door_left_x = CLAMP(m->door_left_x - 10, 0, -57); |  | ||||||
|         m->door_right_x = CLAMP(m->door_right_x + 10, 115, 60); |  | ||||||
| 
 |  | ||||||
|         if(m->door_left_x == -57) { |         if(m->door_left_x == -57) { | ||||||
|             for(uint8_t i = 0; i < 3; ++i) { |             for(uint8_t i = 0; i < 3; ++i) { | ||||||
|                 canvas_draw_str_aligned( |                 canvas_draw_str_aligned( | ||||||
|  | |||||||
| @ -6,18 +6,6 @@ | |||||||
| #include <input/input.h> | #include <input/input.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| 
 | 
 | ||||||
| #ifndef MAX |  | ||||||
| #define MAX(x, y) (((x) > (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef MIN |  | ||||||
| #define MIN(x, y) (((x) < (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef CLAMP |  | ||||||
| #define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower))) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| // Idle screen
 | // Idle screen
 | ||||||
| typedef enum { | typedef enum { | ||||||
|     DolphinViewIdleMain, |     DolphinViewIdleMain, | ||||||
| @ -60,6 +48,7 @@ typedef struct { | |||||||
|     Icon* animation; |     Icon* animation; | ||||||
|     uint8_t scene_num; |     uint8_t scene_num; | ||||||
|     uint8_t hint_timeout; |     uint8_t hint_timeout; | ||||||
|  |     bool locked; | ||||||
| } DolphinViewMainModel; | } DolphinViewMainModel; | ||||||
| 
 | 
 | ||||||
| void dolphin_view_idle_main_draw(Canvas* canvas, void* model); | void dolphin_view_idle_main_draw(Canvas* canvas, void* model); | ||||||
|  | |||||||
| @ -4,22 +4,6 @@ | |||||||
| #include <gui/gui_i.h> | #include <gui/gui_i.h> | ||||||
| #include <u8g2/u8g2.h> | #include <u8g2/u8g2.h> | ||||||
| 
 | 
 | ||||||
| #ifndef ARRSIZE |  | ||||||
| #define ARRSIZE(arr) (sizeof(arr) / sizeof(arr[0])) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef MAX |  | ||||||
| #define MAX(x, y) (((x) > (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef MIN |  | ||||||
| #define MIN(x, y) (((x) < (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef CLAMP |  | ||||||
| #define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower))) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| // global
 | // global
 | ||||||
| #define SCALE 32 | #define SCALE 32 | ||||||
| // screen
 | // screen
 | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ void dolphin_scene_update_state(SceneState* state, uint32_t t, uint32_t dt) { | |||||||
|         state->player_flipped = false; |         state->player_flipped = false; | ||||||
|         if(state->action_timeout == 0) { |         if(state->action_timeout == 0) { | ||||||
|             scene_proceed_action(state); |             scene_proceed_action(state); | ||||||
|             state->emote_id = roll_new(state->previous_emote, ARRSIZE(emotes_list)); |             state->emote_id = roll_new(state->previous_emote, SIZEOF_ARRAY(emotes_list)); | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|     case INTERACT: |     case INTERACT: | ||||||
|  | |||||||
| @ -37,17 +37,14 @@ static void scene_draw_sleep_emote(SceneState* state, Canvas* canvas) { | |||||||
|     furi_assert(state); |     furi_assert(state); | ||||||
|     furi_assert(canvas); |     furi_assert(canvas); | ||||||
| 
 | 
 | ||||||
|     char dialog_str[] = "zZzZ..."; |     char dialog_str[] = "zZzZ.."; | ||||||
|     char buf[64]; |  | ||||||
|     // 2do - sofa x pos getter
 |     // 2do - sofa x pos getter
 | ||||||
|     if(state->player_global.x == 154 && state->action_timeout % 100 < 30) { |     if(state->player_global.x == 154 && state->action_timeout % 100 < 50) { | ||||||
|         if(state->dialog_progress < strlen(dialog_str)) { |         if(state->dialog_progress < strlen(dialog_str)) { | ||||||
|             if(state->action_timeout % 5 == 0) state->dialog_progress++; |             if(state->action_timeout % 10 == 0) state->dialog_progress++; | ||||||
|             dialog_str[state->dialog_progress] = '\0'; | 
 | ||||||
|             snprintf(buf, state->dialog_progress, dialog_str); |             dialog_str[state->dialog_progress + 1] = '\0'; | ||||||
|             // bubble vs just text?
 |             canvas_draw_str(canvas, 80, 20, dialog_str); | ||||||
|             //elements_multiline_text_framed(canvas, 80, 20, buf);
 |  | ||||||
|             canvas_draw_str(canvas, 80, 20, buf); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } else { |     } else { | ||||||
| @ -83,6 +80,25 @@ static void draw_idle_emote(SceneState* state, Canvas* canvas){ | |||||||
| } | } | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
|  | static void draw_idle_emote(SceneState* state, Canvas* canvas) { | ||||||
|  |     furi_assert(state); | ||||||
|  |     furi_assert(canvas); | ||||||
|  | 
 | ||||||
|  |     char dialog_str[] = "..."; | ||||||
|  | 
 | ||||||
|  |     if(state->action_timeout % 100 < 50) { | ||||||
|  |         if(state->dialog_progress < strlen(dialog_str)) { | ||||||
|  |             if(state->action_timeout % 10 == 0) state->dialog_progress++; | ||||||
|  | 
 | ||||||
|  |             dialog_str[state->dialog_progress + 1] = '\0'; | ||||||
|  |             canvas_draw_str(canvas, 70, 15, dialog_str); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } else { | ||||||
|  |         state->dialog_progress = 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void dolphin_scene_render_dolphin(SceneState* state, Canvas* canvas) { | void dolphin_scene_render_dolphin(SceneState* state, Canvas* canvas) { | ||||||
|     furi_assert(state); |     furi_assert(state); | ||||||
|     furi_assert(canvas); |     furi_assert(canvas); | ||||||
| @ -190,8 +206,6 @@ void dolphin_scene_render_state(SceneState* state, Canvas* canvas) { | |||||||
|         scene_activate_item_callback(state, canvas); |         scene_activate_item_callback(state, canvas); | ||||||
|     else if(state->action == SLEEP) |     else if(state->action == SLEEP) | ||||||
|         scene_draw_sleep_emote(state, canvas); |         scene_draw_sleep_emote(state, canvas); | ||||||
|     /*
 |  | ||||||
|     else if(state->action == IDLE) |     else if(state->action == IDLE) | ||||||
|         draw_idle_emote(state, canvas); |         draw_idle_emote(state, canvas); | ||||||
|     */ |  | ||||||
| } | } | ||||||
| @ -7,6 +7,29 @@ | |||||||
| #include <string.h> | #include <string.h> | ||||||
| #include <stdint.h> | #include <stdint.h> | ||||||
| 
 | 
 | ||||||
|  | void elements_scrollbar_pos( | ||||||
|  |     Canvas* canvas, | ||||||
|  |     uint8_t x, | ||||||
|  |     uint8_t y, | ||||||
|  |     uint8_t height, | ||||||
|  |     uint16_t pos, | ||||||
|  |     uint16_t total) { | ||||||
|  |     furi_assert(canvas); | ||||||
|  |     // prevent overflows
 | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_box(canvas, x - 3, y, 3, height); | ||||||
|  |     // dot line
 | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     for(uint8_t i = y; i < height + y; i += 2) { | ||||||
|  |         canvas_draw_dot(canvas, x - 2, i); | ||||||
|  |     } | ||||||
|  |     // Position block
 | ||||||
|  |     if(total) { | ||||||
|  |         float block_h = ((float)height) / total; | ||||||
|  |         canvas_draw_box(canvas, x - 3, y + (block_h * pos), 3, MAX(block_h, 1)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void elements_scrollbar(Canvas* canvas, uint8_t pos, uint8_t total) { | void elements_scrollbar(Canvas* canvas, uint8_t pos, uint8_t total) { | ||||||
|     furi_assert(canvas); |     furi_assert(canvas); | ||||||
| 
 | 
 | ||||||
| @ -23,7 +46,7 @@ void elements_scrollbar(Canvas* canvas, uint8_t pos, uint8_t total) { | |||||||
|     // Position block
 |     // Position block
 | ||||||
|     if(total) { |     if(total) { | ||||||
|         uint8_t block_h = ((float)height) / total; |         uint8_t block_h = ((float)height) / total; | ||||||
|         canvas_draw_box(canvas, width - 3, block_h * pos, 3, block_h); |         canvas_draw_box(canvas, width - 3, block_h * pos, 3, MAX(block_h, 1)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,6 +7,23 @@ | |||||||
| extern "C" { | extern "C" { | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | /*
 | ||||||
|  |  * Draw scrollbar on canvas at specific position. | ||||||
|  |  * @param x - scrollbar position on X axis | ||||||
|  |  * @param y - scrollbar position on Y axis | ||||||
|  |  * @param height - scrollbar height | ||||||
|  |  * @param pos - current element  | ||||||
|  |  * @param total - total elements | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | void elements_scrollbar_pos( | ||||||
|  |     Canvas* canvas, | ||||||
|  |     uint8_t x, | ||||||
|  |     uint8_t y, | ||||||
|  |     uint8_t height, | ||||||
|  |     uint16_t pos, | ||||||
|  |     uint16_t total); | ||||||
|  | 
 | ||||||
| /*
 | /*
 | ||||||
|  * Draw scrollbar on canvas. |  * Draw scrollbar on canvas. | ||||||
|  * width 3px, height equal to canvas height |  * width 3px, height equal to canvas height | ||||||
|  | |||||||
| @ -28,14 +28,6 @@ typedef struct { | |||||||
|     uint8_t first_visible_byte; |     uint8_t first_visible_byte; | ||||||
| } ByteInputModel; | } ByteInputModel; | ||||||
| 
 | 
 | ||||||
| #ifndef MAX |  | ||||||
| #define MAX(x, y) (((x) > (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef MIN |  | ||||||
| #define MIN(x, y) (((x) < (y)) ? (x) : (y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| static const uint8_t keyboard_origin_x = 7; | static const uint8_t keyboard_origin_x = 7; | ||||||
| static const uint8_t keyboard_origin_y = 31; | static const uint8_t keyboard_origin_y = 31; | ||||||
| static const uint8_t keyboard_row_count = 2; | static const uint8_t keyboard_row_count = 2; | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/125_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 308 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/Nfc_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 304 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/ble_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 301 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/dir_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 311 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/ibutt_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 304 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/ir_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 305 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/sub1_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 299 B | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/Archive/unknown_10px.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 312 B | 
| @ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| #include <cmsis_os2.h> | #include <cmsis_os2.h> | ||||||
| 
 | 
 | ||||||
|  | #include <furi/common_defines.h> | ||||||
| #include <furi/check.h> | #include <furi/check.h> | ||||||
| #include <furi/memmgr.h> | #include <furi/memmgr.h> | ||||||
| #include <furi/pubsub.h> | #include <furi/pubsub.h> | ||||||
|  | |||||||
| @ -1,3 +1,33 @@ | |||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #ifndef MAX | ||||||
|  | 
 | ||||||
|  | #define MAX(a, b)               \ | ||||||
|  |     ({                          \ | ||||||
|  |         __typeof__(a) _a = (a); \ | ||||||
|  |         __typeof__(b) _b = (b); \ | ||||||
|  |         _a > _b ? _a : _b;      \ | ||||||
|  |     }) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #ifndef MIN | ||||||
|  | #define MIN(a, b)               \ | ||||||
|  |     ({                          \ | ||||||
|  |         __typeof__(a) _a = (a); \ | ||||||
|  |         __typeof__(b) _b = (b); \ | ||||||
|  |         _a < _b ? _a : _b;      \ | ||||||
|  |     }) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #ifndef CLAMP | ||||||
|  | #define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower))) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | // need some common semantics for those two
 | ||||||
|  | #ifndef SIZEOF_ARRAY | ||||||
|  | #define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof(arr[0])) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #ifndef COUNT_OF | ||||||
| #define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) | #define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) | ||||||
|  | #endif | ||||||
|  | |||||||
| @ -66,13 +66,6 @@ extern "C"{ | |||||||
|    *  Some useful macro definitions   * |    *  Some useful macro definitions   * | ||||||
|    * -------------------------------- */ |    * -------------------------------- */ | ||||||
| 
 | 
 | ||||||
| #ifndef MAX |  | ||||||
| #define MAX( x, y )          (((x)>(y))?(x):(y)) |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #ifndef MIN |  | ||||||
| #define MIN( x, y )          (((x)<(y))?(x):(y)) |  | ||||||
| #endif |  | ||||||
| 
 | 
 | ||||||
| #define MODINC( a, m )       M_BEGIN  (a)++;  if ((a)>=(m)) (a)=0;  M_END | #define MODINC( a, m )       M_BEGIN  (a)++;  if ((a)>=(m)) (a)=0;  M_END | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -64,10 +64,6 @@ | |||||||
|  */ |  */ | ||||||
| #define EVAL_ERR_EQ_GOTO(EC, ERR, LABEL)                                   \ | #define EVAL_ERR_EQ_GOTO(EC, ERR, LABEL)                                   \ | ||||||
|     if (EC == ERR) goto LABEL; |     if (EC == ERR) goto LABEL; | ||||||
| 
 |  | ||||||
| #define SIZEOF_ARRAY(a)     (sizeof(a) / sizeof(a[0]))    /*!< Compute the size of an array           */ |  | ||||||
| #define MAX(a, b)           (((a) > (b)) ? (a) : (b))    /*!< Return the maximum of the 2 values     */ |  | ||||||
| #define MIN(a, b)           (((a) < (b)) ? (a) : (b))    /*!< Return the minimum of the 2 values     */ |  | ||||||
| #define BITMASK_1           (0x01)                        /*!< Bit mask for lsb bit                   */ | #define BITMASK_1           (0x01)                        /*!< Bit mask for lsb bit                   */ | ||||||
| #define BITMASK_2           (0x03)                        /*!< Bit mask for two lsb bits              */ | #define BITMASK_2           (0x03)                        /*!< Bit mask for two lsb bits              */ | ||||||
| #define BITMASK_3           (0x07)                        /*!< Bit mask for three lsb bits            */ | #define BITMASK_3           (0x07)                        /*!< Bit mask for three lsb bits            */ | ||||||
|  | |||||||
 its your bedtime
						its your bedtime