[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 app_accessor(void* p); | ||||
| int32_t internal_storage_task(void* p); | ||||
| int32_t app_archive(void* p); | ||||
| 
 | ||||
| // On system start hooks declaration
 | ||||
| void nfc_cli_init(); | ||||
| @ -159,6 +160,15 @@ const size_t FLIPPER_SERVICES_COUNT = sizeof(FLIPPER_SERVICES) / sizeof(FlipperA | ||||
| 
 | ||||
| // Main menu APP
 | ||||
| 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 | ||||
|     {.app = subghz_app, .name = "Sub-1 GHz", .stack_size = 1024, .icon = A_Sub1ghz_14}, | ||||
| #endif | ||||
| @ -167,21 +177,18 @@ const FlipperApplication FLIPPER_APPS[] = { | ||||
|     {.app = app_lfrfid, .name = "125 kHz RFID", .stack_size = 1024, .icon = A_125khz_14}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_NFC | ||||
|     {.app = nfc_task, .name = "NFC", .stack_size = 1024, .icon = A_NFC_14}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_IRDA | ||||
|     {.app = irda, .name = "Infrared", .stack_size = 1024, .icon = A_Infrared_14}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_IBUTTON | ||||
|     {.app = app_ibutton, .name = "iButton", .stack_size = 4096, .icon = A_iButton_14}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_GPIO_DEMO | ||||
|     {.app = app_gpio_test, .name = "GPIO", .stack_size = 1024, .icon = A_GPIO_14}, | ||||
| #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); | ||||
| @ -263,6 +270,11 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = { | ||||
| 
 | ||||
| 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 | ||||
| const FlipperApplication FLIPPER_SCENE = | ||||
|     {.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_APPS[]; | ||||
| 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_FLOOPPER_BLOOPPER = 1 | ||||
| APP_IBUTTON = 1 | ||||
| APP_ARCHIVE = 1 | ||||
| 
 | ||||
| # Debug and misc
 | ||||
| APP_GUI_TEST = 1 | ||||
| @ -85,6 +86,12 @@ ifeq ($(APP_UNIT_TESTS), 1) | ||||
| CFLAGS		+= -DAPP_UNIT_TESTS | ||||
| endif | ||||
| 
 | ||||
| APP_ARCHIVE ?= 0 | ||||
| ifeq ($(APP_NFC), 1) | ||||
| CFLAGS		+= -DAPP_ARCHIVE | ||||
| APP_ARCHIVE = 1 | ||||
| endif | ||||
| 
 | ||||
| SRV_EXAMPLE_BLINK ?= 0 | ||||
| ifeq ($(SRV_EXAMPLE_BLINK), 1) | ||||
| 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) { | ||||
|     furi_assert(event); | ||||
|     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(); | ||||
| 
 | ||||
|         // check if pressed sequentially
 | ||||
|         if(press_time - dolphin->lock_lastpress < 200) { | ||||
|             dolphin->lock_lastpress = press_time; | ||||
|             dolphin->lock_count++; | ||||
|         } else if(press_time - dolphin->lock_lastpress > 200) { | ||||
|         if(press_time - dolphin->lock_lastpress > UNLOCK_RST_TIMEOUT) { | ||||
|             dolphin->lock_lastpress = press_time; | ||||
|             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->lock_count = 0; | ||||
| 
 | ||||
|             with_view_model( | ||||
|                 dolphin->view_lockmenu, (DolphinViewLockMenuModel * model) { | ||||
|                     model->locked = false; | ||||
|                     model->door_left_x = -57; // move doors to default pos
 | ||||
|                     model->door_right_x = 115; | ||||
|                     return true; | ||||
|                 }); | ||||
| 
 | ||||
|             with_view_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; | ||||
|                 }); | ||||
| 
 | ||||
| @ -112,9 +122,7 @@ bool dolphin_view_idle_main_input(InputEvent* event, void* context) { | ||||
|         } else if(event->key == InputKeyRight && event->type == InputTypeShort) { | ||||
|             dolphin_switch_to_app(dolphin, &FLIPPER_SCENE); | ||||
|         } else if(event->key == InputKeyDown && event->type == InputTypeShort) { | ||||
| #if 0 | ||||
|             dolphin_switch_to_app(dolphin, &ARCHIVE_APP); | ||||
| #endif | ||||
|             dolphin_switch_to_app(dolphin, &FLIPPER_ARCHIVE); | ||||
|         } else if(event->key == InputKeyDown && event->type == InputTypeLong) { | ||||
|             view_dispatcher_switch_to_view(dolphin->idle_view_dispatcher, DolphinViewStats); | ||||
|         } else if(event->key == InputKeyBack && event->type == InputTypeShort) { | ||||
| @ -123,12 +131,6 @@ bool dolphin_view_idle_main_input(InputEvent* event, void* context) { | ||||
|     } else { | ||||
|         // locked
 | ||||
| 
 | ||||
|         with_view_model( | ||||
|             dolphin->idle_view_main, (DolphinViewMainModel * model) { | ||||
|                 model->hint_timeout = 3; | ||||
|                 return true; | ||||
|             }); | ||||
| 
 | ||||
|         dolphin_lock_handler(event, dolphin); | ||||
|         dolphin_scene_handler_switch_scene(dolphin); | ||||
|     } | ||||
| @ -151,14 +153,19 @@ static void lock_menu_callback(void* context, uint8_t index) { | ||||
|     // lock
 | ||||
|     case 0: | ||||
|         dolphin->locked = true; | ||||
|         DolphinViewLockMenuModel* model = view_get_model(dolphin->view_lockmenu); | ||||
| 
 | ||||
|         model->locked = true; | ||||
|         model->exit_timeout = 20; | ||||
| 
 | ||||
|         view_port_enabled_set(dolphin->lock_viewport, dolphin->locked); | ||||
|         view_commit_model(dolphin->view_lockmenu, true); | ||||
|         with_view_model( | ||||
|             dolphin->view_lockmenu, (DolphinViewLockMenuModel * model) { | ||||
|                 model->locked = true; | ||||
|                 model->exit_timeout = HINT_TIMEOUT_H; | ||||
|                 return true; | ||||
|             }); | ||||
| 
 | ||||
|         with_view_model( | ||||
|             dolphin->idle_view_main, (DolphinViewMainModel * model) { | ||||
|                 model->locked = true; | ||||
|                 return true; | ||||
|             }); | ||||
|         break; | ||||
| 
 | ||||
|     default: | ||||
|  | ||||
| @ -14,6 +14,10 @@ | ||||
| #include <assets_icons.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 { | ||||
|     DolphinEventTypeDeed, | ||||
|     DolphinEventTypeSave, | ||||
|  | ||||
| @ -65,8 +65,13 @@ void dolphin_view_idle_main_draw(Canvas* canvas, void* model) { | ||||
| 
 | ||||
|     if(m->hint_timeout > 0) { | ||||
|         m->hint_timeout--; | ||||
|         canvas_draw_icon_name(canvas, 13, 5, I_LockPopup_100x49); | ||||
|         elements_multiline_text(canvas, 65, 20, "To unlock\npress:"); | ||||
|         if(m->locked) { | ||||
|             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) { | ||||
|         m->exit_timeout--; | ||||
| 
 | ||||
|         m->door_left_x = CLAMP(m->door_left_x + 10, 0, -57); | ||||
|         m->door_right_x = CLAMP(m->door_right_x - 10, 115, 60); | ||||
|         m->door_left_x = CLAMP(m->door_left_x + 5, 0, -57); | ||||
|         m->door_right_x = CLAMP(m->door_right_x - 5, 115, 60); | ||||
| 
 | ||||
|         if(m->door_left_x > -10) { | ||||
|             canvas_set_font(canvas, FontPrimary); | ||||
| @ -90,9 +95,6 @@ void dolphin_view_lockmenu_draw(Canvas* canvas, void* model) { | ||||
|         } | ||||
| 
 | ||||
|     } 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) { | ||||
|             for(uint8_t i = 0; i < 3; ++i) { | ||||
|                 canvas_draw_str_aligned( | ||||
|  | ||||
| @ -6,18 +6,6 @@ | ||||
| #include <input/input.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
 | ||||
| typedef enum { | ||||
|     DolphinViewIdleMain, | ||||
| @ -60,6 +48,7 @@ typedef struct { | ||||
|     Icon* animation; | ||||
|     uint8_t scene_num; | ||||
|     uint8_t hint_timeout; | ||||
|     bool locked; | ||||
| } DolphinViewMainModel; | ||||
| 
 | ||||
| void dolphin_view_idle_main_draw(Canvas* canvas, void* model); | ||||
|  | ||||
| @ -4,22 +4,6 @@ | ||||
| #include <gui/gui_i.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
 | ||||
| #define SCALE 32 | ||||
| // screen
 | ||||
|  | ||||
| @ -66,7 +66,7 @@ void dolphin_scene_update_state(SceneState* state, uint32_t t, uint32_t dt) { | ||||
|         state->player_flipped = false; | ||||
|         if(state->action_timeout == 0) { | ||||
|             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; | ||||
|         } | ||||
|     case INTERACT: | ||||
|  | ||||
| @ -37,17 +37,14 @@ static void scene_draw_sleep_emote(SceneState* state, Canvas* canvas) { | ||||
|     furi_assert(state); | ||||
|     furi_assert(canvas); | ||||
| 
 | ||||
|     char dialog_str[] = "zZzZ..."; | ||||
|     char buf[64]; | ||||
|     char dialog_str[] = "zZzZ.."; | ||||
|     // 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->action_timeout % 5 == 0) state->dialog_progress++; | ||||
|             dialog_str[state->dialog_progress] = '\0'; | ||||
|             snprintf(buf, state->dialog_progress, dialog_str); | ||||
|             // bubble vs just text?
 | ||||
|             //elements_multiline_text_framed(canvas, 80, 20, buf);
 | ||||
|             canvas_draw_str(canvas, 80, 20, buf); | ||||
|             if(state->action_timeout % 10 == 0) state->dialog_progress++; | ||||
| 
 | ||||
|             dialog_str[state->dialog_progress + 1] = '\0'; | ||||
|             canvas_draw_str(canvas, 80, 20, dialog_str); | ||||
|         } | ||||
| 
 | ||||
|     } 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) { | ||||
|     furi_assert(state); | ||||
|     furi_assert(canvas); | ||||
| @ -190,8 +206,6 @@ void dolphin_scene_render_state(SceneState* state, Canvas* canvas) { | ||||
|         scene_activate_item_callback(state, canvas); | ||||
|     else if(state->action == SLEEP) | ||||
|         scene_draw_sleep_emote(state, canvas); | ||||
|     /*
 | ||||
|     else if(state->action == IDLE) | ||||
|         draw_idle_emote(state, canvas); | ||||
|     */ | ||||
| } | ||||
| @ -7,6 +7,29 @@ | ||||
| #include <string.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) { | ||||
|     furi_assert(canvas); | ||||
| 
 | ||||
| @ -23,7 +46,7 @@ void elements_scrollbar(Canvas* canvas, uint8_t pos, uint8_t total) { | ||||
|     // Position block
 | ||||
|     if(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" { | ||||
| #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. | ||||
|  * width 3px, height equal to canvas height | ||||
|  | ||||
| @ -28,14 +28,6 @@ typedef struct { | ||||
|     uint8_t first_visible_byte; | ||||
| } 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_y = 31; | ||||
| 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 <furi/common_defines.h> | ||||
| #include <furi/check.h> | ||||
| #include <furi/memmgr.h> | ||||
| #include <furi/pubsub.h> | ||||
|  | ||||
| @ -1,3 +1,33 @@ | ||||
| #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])) | ||||
| #endif | ||||
|  | ||||
| @ -66,13 +66,6 @@ extern "C"{ | ||||
|    *  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 | ||||
| 
 | ||||
|  | ||||
| @ -64,10 +64,6 @@ | ||||
|  */ | ||||
| #define EVAL_ERR_EQ_GOTO(EC, ERR, 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_2           (0x03)                        /*!< Bit mask for two lsb bits              */ | ||||
| #define BITMASK_3           (0x07)                        /*!< Bit mask for three lsb bits            */ | ||||
|  | ||||
 its your bedtime
						its your bedtime