[FL-2150] Dolphin animation refactoring (#938)
* Dolphin Animation Refactoring, part 1 * Remove animations from desktop * Remove excess, first start * Split animation_manager with callbacks * allocate view inside animation_view * Work on ViewComposed * Draw white rectangles under bubble corners * Fix bubbles sequence * RPC: remove obsolete include "status.pb.h" * Add animations manifest decoding * Flipper file: add strict mode * FFF: Animation structures parsing * Assembling structure of animation * Lot of view fixes: Add multi-line bubbles Add support for passive bubbles (frame_order values starts from passive now) Add hard-coded delay (active_shift) for active state enabling Fix active state handling Fix leaks Fix parsing uncorrect bubble_animation meta file Fix bubble rules of showing * Animation load/unload & view freeze/unfreeze * Blocking & system animations, fixes: View correct activation Refactoring + blocking animation Freeze first passive/active frames Many insert/eject SD tests fixes Add system animations Add Loader events app started/finished Add system no_sd animation * Assets: dolphin packer. Scripts: minor refactoring. * Desktop: update logging tags. Scripts: add metadata to dolphin bundling process, extra sorting for fs traversing. Make: phony assets rules. * Github: rebuild assets on build * Docker: add missing dependencies for assets compilation * Docker: fix run command syntax * ReadMe: update naming rules with link to source * Assets: recompile icons * Loader: add loader event * Desktop, Gui, Furi Core: const shenanigans macros Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
							
								
								
									
										17
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -71,6 +71,14 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           tar czpf artifacts/flipper-z-any-scripts-${{steps.names.outputs.suffix}}.tgz scripts |           tar czpf artifacts/flipper-z-any-scripts-${{steps.names.outputs.suffix}}.tgz scripts | ||||||
| 
 | 
 | ||||||
|  |       - name: 'Rebuild Assets' | ||||||
|  |         uses: ./.github/actions/docker | ||||||
|  |         with: | ||||||
|  |           run: | | ||||||
|  |             set -e | ||||||
|  |             make -C assets clean | ||||||
|  |             make -C assets | ||||||
|  | 
 | ||||||
|       - name: 'Build the firmware in docker' |       - name: 'Build the firmware in docker' | ||||||
|         uses: ./.github/actions/docker |         uses: ./.github/actions/docker | ||||||
|         with: |         with: | ||||||
| @ -142,6 +150,7 @@ jobs: | |||||||
|           body: | |           body: | | ||||||
|             [Click here](https://update.flipperzero.one/?url=https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.artifacts-path}}/flipper-z-${{steps.names.outputs.default-target}}-full-${{steps.names.outputs.suffix}}.dfu&channel=${{steps.names.outputs.artifacts-path}}&version=${{steps.names.outputs.short-hash}}&target=${{steps.names.outputs.default-target}}) to flash the `${{steps.names.outputs.short-hash}}` version of this branch via WebUSB. |             [Click here](https://update.flipperzero.one/?url=https://update.flipperzero.one/builds/firmware/${{steps.names.outputs.artifacts-path}}/flipper-z-${{steps.names.outputs.default-target}}-full-${{steps.names.outputs.suffix}}.dfu&channel=${{steps.names.outputs.artifacts-path}}&version=${{steps.names.outputs.short-hash}}&target=${{steps.names.outputs.default-target}}) to flash the `${{steps.names.outputs.short-hash}}` version of this branch via WebUSB. | ||||||
|           edit-mode: replace |           edit-mode: replace | ||||||
|  | 
 | ||||||
|   compact: |   compact: | ||||||
|     if: ${{ !startsWith(github.ref, 'refs/tags') }} |     if: ${{ !startsWith(github.ref, 'refs/tags') }} | ||||||
|     runs-on: [self-hosted,koteeq] |     runs-on: [self-hosted,koteeq] | ||||||
| @ -186,6 +195,14 @@ jobs: | |||||||
|           echo "WORKFLOW_BRANCH_OR_TAG=${BRANCH_OR_TAG}" >> $GITHUB_ENV |           echo "WORKFLOW_BRANCH_OR_TAG=${BRANCH_OR_TAG}" >> $GITHUB_ENV | ||||||
|           echo "DIST_SUFFIX=${SUFFIX}" >> $GITHUB_ENV |           echo "DIST_SUFFIX=${SUFFIX}" >> $GITHUB_ENV | ||||||
| 
 | 
 | ||||||
|  |       - name: 'Rebuild Assets' | ||||||
|  |         uses: ./.github/actions/docker | ||||||
|  |         with: | ||||||
|  |           run: | | ||||||
|  |             set -e | ||||||
|  |             make -C assets clean | ||||||
|  |             make -C assets | ||||||
|  | 
 | ||||||
|       - name: 'Build the firmware in docker' |       - name: 'Build the firmware in docker' | ||||||
|         uses: ./.github/actions/docker |         uses: ./.github/actions/docker | ||||||
|         with: |         with: | ||||||
|  | |||||||
							
								
								
									
										436
									
								
								applications/desktop/animations/animation_manager.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,436 @@ | |||||||
|  | #include "animation_manager.h" | ||||||
|  | #include "furi-hal-delay.h" | ||||||
|  | #include "portmacro.h" | ||||||
|  | #include "views/bubble_animation_view.h" | ||||||
|  | #include "animation_storage.h" | ||||||
|  | 
 | ||||||
|  | #include <cmsis_os2.h> | ||||||
|  | #include <dolphin/dolphin.h> | ||||||
|  | #include <furi/check.h> | ||||||
|  | #include <furi/pubsub.h> | ||||||
|  | #include <furi/record.h> | ||||||
|  | #include <m-string.h> | ||||||
|  | #include <power/power_service/power.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <storage/storage.h> | ||||||
|  | #include <dolphin/dolphin_i.h> | ||||||
|  | #include <storage/filesystem-api-defines.h> | ||||||
|  | 
 | ||||||
|  | #define TAG "AnimationManager" | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     AnimationManagerStateIdle, | ||||||
|  |     AnimationManagerStateBlocked, | ||||||
|  |     AnimationManagerStateFreezedIdle, | ||||||
|  |     AnimationManagerStateFreezedBlocked, | ||||||
|  | } AnimationManagerState; | ||||||
|  | 
 | ||||||
|  | struct AnimationManager { | ||||||
|  |     bool sd_show_url; | ||||||
|  |     bool sd_shown_no_db; | ||||||
|  |     bool sd_shown_sd_ok; | ||||||
|  |     AnimationManagerState state; | ||||||
|  |     FuriPubSubSubscription* pubsub_subscription_storage; | ||||||
|  |     FuriPubSubSubscription* pubsub_subscription_dolphin; | ||||||
|  |     BubbleAnimationView* animation_view; | ||||||
|  |     osTimerId_t idle_animation_timer; | ||||||
|  |     StorageAnimation* current_animation; | ||||||
|  |     AnimationManagerInteractCallback interact_callback; | ||||||
|  |     AnimationManagerSetNewIdleAnimationCallback new_idle_callback; | ||||||
|  |     AnimationManagerSetNewIdleAnimationCallback check_blocking_callback; | ||||||
|  |     void* context; | ||||||
|  |     string_t freezed_animation_name; | ||||||
|  |     int32_t freezed_animation_time_left; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static StorageAnimation* | ||||||
|  |     animation_manager_select_idle_animation(AnimationManager* animation_manager); | ||||||
|  | static void animation_manager_replace_current_animation( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     StorageAnimation* storage_animation); | ||||||
|  | static void animation_manager_start_new_idle(AnimationManager* animation_manager); | ||||||
|  | static bool animation_manager_check_blocking(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | void animation_manager_set_context(AnimationManager* animation_manager, void* context) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     animation_manager->context = context; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_set_new_idle_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerSetNewIdleAnimationCallback callback) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     animation_manager->new_idle_callback = callback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_set_check_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerCheckBlockingCallback callback) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     animation_manager->check_blocking_callback = callback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_set_interact_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerInteractCallback callback) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     animation_manager->interact_callback = callback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_manager_check_blocking_callback(const void* message, void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     AnimationManager* animation_manager = context; | ||||||
|  |     if(animation_manager->check_blocking_callback) { | ||||||
|  |         animation_manager->check_blocking_callback(animation_manager->context); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_manager_timer_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     AnimationManager* animation_manager = context; | ||||||
|  |     if(animation_manager->new_idle_callback) { | ||||||
|  |         animation_manager->new_idle_callback(animation_manager->context); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_manager_interact_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     AnimationManager* animation_manager = context; | ||||||
|  |     if(animation_manager->interact_callback) { | ||||||
|  |         animation_manager->interact_callback(animation_manager->context); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* reaction to animation_manager->interact_callback() */ | ||||||
|  | void animation_manager_check_blocking_process(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     if(animation_manager->state == AnimationManagerStateIdle) { | ||||||
|  |         animation_manager_check_blocking(animation_manager); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* reaction to animation_manager->new_idle_callback() */ | ||||||
|  | void animation_manager_new_idle_process(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     if(animation_manager->state == AnimationManagerStateIdle) { | ||||||
|  |         animation_manager_start_new_idle(animation_manager); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* reaction to animation_manager->check_blocking_callback() */ | ||||||
|  | void animation_manager_interact_process(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     if(animation_manager->state == AnimationManagerStateBlocked) { | ||||||
|  |         /* check if new blocking animation has to be displayed */ | ||||||
|  |         bool blocked = animation_manager_check_blocking(animation_manager); | ||||||
|  |         if(!blocked) { | ||||||
|  |             animation_manager_start_new_idle(animation_manager); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_manager_start_new_idle(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     StorageAnimation* new_animation = animation_manager_select_idle_animation(animation_manager); | ||||||
|  |     animation_manager_replace_current_animation(animation_manager, new_animation); | ||||||
|  |     const BubbleAnimation* bubble_animation = | ||||||
|  |         animation_storage_get_bubble_animation(animation_manager->current_animation); | ||||||
|  |     animation_manager->state = AnimationManagerStateIdle; | ||||||
|  |     osTimerStart(animation_manager->idle_animation_timer, bubble_animation->duration * 1000); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool animation_manager_check_blocking(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     StorageAnimation* blocking_animation = NULL; | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     FS_Error sd_status = storage_sd_status(storage); | ||||||
|  | 
 | ||||||
|  |     if(sd_status == FSE_INTERNAL) { | ||||||
|  |         blocking_animation = animation_storage_find_animation(BAD_SD_ANIMATION_NAME); | ||||||
|  |     } else if(sd_status == FSE_NOT_READY) { | ||||||
|  |         animation_manager->sd_shown_sd_ok = false; | ||||||
|  |         animation_manager->sd_shown_no_db = false; | ||||||
|  |     } else if(sd_status == FSE_OK) { | ||||||
|  |         if(!animation_manager->sd_shown_sd_ok) { | ||||||
|  |             blocking_animation = animation_storage_find_animation(SD_OK_ANIMATION_NAME); | ||||||
|  |             animation_manager->sd_shown_sd_ok = true; | ||||||
|  |         } else if(!animation_manager->sd_shown_no_db) { | ||||||
|  |             bool db_exists = storage_common_stat(storage, "/ext/Manifest", NULL) == FSE_OK; | ||||||
|  |             if(!db_exists) { | ||||||
|  |                 blocking_animation = animation_storage_find_animation(NO_DB_ANIMATION_NAME); | ||||||
|  |                 animation_manager->sd_shown_no_db = true; | ||||||
|  |                 animation_manager->sd_show_url = true; | ||||||
|  |             } | ||||||
|  |         } else if(animation_manager->sd_show_url) { | ||||||
|  |             blocking_animation = animation_storage_find_animation(URL_ANIMATION_NAME); | ||||||
|  |             animation_manager->sd_show_url = false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Dolphin* dolphin = furi_record_open("dolphin"); | ||||||
|  |     DolphinStats stats = dolphin_stats(dolphin); | ||||||
|  |     furi_record_close("dolphin"); | ||||||
|  |     if(!blocking_animation && stats.level_up_is_pending) { | ||||||
|  |         blocking_animation = animation_storage_find_animation(LEVELUP_ANIMATION_NAME); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(blocking_animation) { | ||||||
|  |         osTimerStop(animation_manager->idle_animation_timer); | ||||||
|  |         animation_manager_replace_current_animation(animation_manager, blocking_animation); | ||||||
|  |         /* no starting timer because its blocking animation */ | ||||||
|  |         animation_manager->state = AnimationManagerStateBlocked; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_record_close("storage"); | ||||||
|  | 
 | ||||||
|  |     return !!blocking_animation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_manager_replace_current_animation( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     StorageAnimation* storage_animation) { | ||||||
|  |     furi_assert(storage_animation); | ||||||
|  |     StorageAnimation* previous_animation = animation_manager->current_animation; | ||||||
|  | 
 | ||||||
|  |     const BubbleAnimation* animation = animation_storage_get_bubble_animation(storage_animation); | ||||||
|  |     bubble_animation_view_set_animation(animation_manager->animation_view, animation); | ||||||
|  |     const char* new_name = string_get_cstr(animation_storage_get_meta(storage_animation)->name); | ||||||
|  |     FURI_LOG_I(TAG, "Select \'%s\' animation", new_name); | ||||||
|  |     animation_manager->current_animation = storage_animation; | ||||||
|  | 
 | ||||||
|  |     if(previous_animation) { | ||||||
|  |         animation_storage_free_storage_animation(&previous_animation); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | AnimationManager* animation_manager_alloc(void) { | ||||||
|  |     animation_storage_initialize_internal_animations(); | ||||||
|  |     AnimationManager* animation_manager = furi_alloc(sizeof(AnimationManager)); | ||||||
|  |     animation_manager->animation_view = bubble_animation_view_alloc(); | ||||||
|  |     string_init(animation_manager->freezed_animation_name); | ||||||
|  | 
 | ||||||
|  |     animation_manager->idle_animation_timer = | ||||||
|  |         osTimerNew(animation_manager_timer_callback, osTimerOnce, animation_manager, NULL); | ||||||
|  |     bubble_animation_view_set_interact_callback( | ||||||
|  |         animation_manager->animation_view, animation_manager_interact_callback, animation_manager); | ||||||
|  | 
 | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     animation_manager->pubsub_subscription_storage = furi_pubsub_subscribe( | ||||||
|  |         storage_get_pubsub(storage), animation_manager_check_blocking_callback, animation_manager); | ||||||
|  |     furi_record_close("storage"); | ||||||
|  | 
 | ||||||
|  |     Dolphin* dolphin = furi_record_open("dolphin"); | ||||||
|  |     animation_manager->pubsub_subscription_dolphin = furi_pubsub_subscribe( | ||||||
|  |         dolphin_get_pubsub(dolphin), animation_manager_check_blocking_callback, animation_manager); | ||||||
|  |     furi_record_close("dolphin"); | ||||||
|  | 
 | ||||||
|  |     animation_manager->sd_shown_sd_ok = true; | ||||||
|  |     if(!animation_manager_check_blocking(animation_manager)) { | ||||||
|  |         animation_manager_start_new_idle(animation_manager); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return animation_manager; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_free(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     Dolphin* dolphin = furi_record_open("dolphin"); | ||||||
|  |     furi_pubsub_unsubscribe( | ||||||
|  |         dolphin_get_pubsub(dolphin), animation_manager->pubsub_subscription_dolphin); | ||||||
|  |     furi_record_close("dolphin"); | ||||||
|  | 
 | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     furi_pubsub_unsubscribe( | ||||||
|  |         storage_get_pubsub(storage), animation_manager->pubsub_subscription_storage); | ||||||
|  |     furi_record_close("storage"); | ||||||
|  | 
 | ||||||
|  |     string_clear(animation_manager->freezed_animation_name); | ||||||
|  |     bubble_animation_view_free(animation_manager->animation_view); | ||||||
|  |     osTimerDelete(animation_manager->idle_animation_timer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | View* animation_manager_get_animation_view(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  | 
 | ||||||
|  |     return bubble_animation_get_view(animation_manager->animation_view); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static StorageAnimation* | ||||||
|  |     animation_manager_select_idle_animation(AnimationManager* animation_manager) { | ||||||
|  |     StorageAnimationList_t animation_list; | ||||||
|  |     StorageAnimationList_init(animation_list); | ||||||
|  |     animation_storage_fill_animation_list(&animation_list); | ||||||
|  | 
 | ||||||
|  |     Power* power = furi_record_open("power"); | ||||||
|  |     PowerInfo info; | ||||||
|  |     power_get_info(power, &info); | ||||||
|  |     bool battery_is_well = power_is_battery_well(&info); | ||||||
|  |     furi_record_close("power"); | ||||||
|  | 
 | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     FS_Error sd_status = storage_sd_status(storage); | ||||||
|  |     furi_record_close("storage"); | ||||||
|  | 
 | ||||||
|  |     Dolphin* dolphin = furi_record_open("dolphin"); | ||||||
|  |     DolphinStats stats = dolphin_stats(dolphin); | ||||||
|  |     uint32_t whole_weight = 0; | ||||||
|  | 
 | ||||||
|  |     StorageAnimationList_it_t it; | ||||||
|  |     for(StorageAnimationList_it(it, animation_list); !StorageAnimationList_end_p(it);) { | ||||||
|  |         StorageAnimation* storage_animation = *StorageAnimationList_ref(it); | ||||||
|  |         const StorageAnimationMeta* meta = animation_storage_get_meta(storage_animation); | ||||||
|  |         bool skip_animation = false; | ||||||
|  |         if(battery_is_well && !string_cmp_str(meta->name, BAD_BATTERY_ANIMATION_NAME)) { | ||||||
|  |             skip_animation = true; | ||||||
|  |         } else if((sd_status != FSE_NOT_READY) && !string_cmp_str(meta->name, NO_SD_ANIMATION_NAME)) { | ||||||
|  |             skip_animation = true; | ||||||
|  |         } else if((stats.butthurt < meta->min_butthurt) || (stats.butthurt > meta->max_butthurt)) { | ||||||
|  |             skip_animation = true; | ||||||
|  |         } else if((stats.level < meta->min_level) || (stats.level > meta->max_level)) { | ||||||
|  |             skip_animation = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(skip_animation) { | ||||||
|  |             animation_storage_free_storage_animation(&storage_animation); | ||||||
|  |             /* remove and increase iterator */ | ||||||
|  |             StorageAnimationList_remove(animation_list, it); | ||||||
|  |         } else { | ||||||
|  |             whole_weight += meta->weight; | ||||||
|  |             StorageAnimationList_next(it); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     uint32_t lucky_number = random() % whole_weight; | ||||||
|  |     uint32_t weight = 0; | ||||||
|  | 
 | ||||||
|  |     StorageAnimation* selected = NULL; | ||||||
|  |     for | ||||||
|  |         M_EACH(item, animation_list, StorageAnimationList_t) { | ||||||
|  |             if(lucky_number < weight) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             weight += animation_storage_get_meta(*item)->weight; | ||||||
|  |             selected = *item; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     for | ||||||
|  |         M_EACH(item, animation_list, StorageAnimationList_t) { | ||||||
|  |             if(*item != selected) { | ||||||
|  |                 animation_storage_free_storage_animation(item); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     StorageAnimationList_clear(animation_list); | ||||||
|  |     furi_record_close("dolphin"); | ||||||
|  | 
 | ||||||
|  |     /* cache animation, if failed - choose reliable animation */ | ||||||
|  |     if(!animation_storage_get_bubble_animation(selected)) { | ||||||
|  |         const char* name = string_get_cstr(animation_storage_get_meta(selected)->name); | ||||||
|  |         FURI_LOG_E(TAG, "Can't upload animation described in manifest: \'%s\'", name); | ||||||
|  |         animation_storage_free_storage_animation(&selected); | ||||||
|  |         selected = animation_storage_find_animation(HARDCODED_ANIMATION_NAME); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_assert(selected); | ||||||
|  |     return selected; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_unload_and_stall_animation(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     furi_assert(animation_manager->current_animation); | ||||||
|  |     furi_assert(!string_size(animation_manager->freezed_animation_name)); | ||||||
|  |     furi_assert( | ||||||
|  |         (animation_manager->state == AnimationManagerStateIdle) || | ||||||
|  |         (animation_manager->state == AnimationManagerStateBlocked)); | ||||||
|  | 
 | ||||||
|  |     if(animation_manager->state == AnimationManagerStateBlocked) { | ||||||
|  |         animation_manager->state = AnimationManagerStateFreezedBlocked; | ||||||
|  |     } else if(animation_manager->state == AnimationManagerStateIdle) { | ||||||
|  |         animation_manager->state = AnimationManagerStateFreezedIdle; | ||||||
|  | 
 | ||||||
|  |         animation_manager->freezed_animation_time_left = | ||||||
|  |             xTimerGetExpiryTime(animation_manager->idle_animation_timer) - xTaskGetTickCount(); | ||||||
|  |         if(animation_manager->freezed_animation_time_left < 0) { | ||||||
|  |             animation_manager->freezed_animation_time_left = 0; | ||||||
|  |         } | ||||||
|  |         osTimerStop(animation_manager->idle_animation_timer); | ||||||
|  |     } else { | ||||||
|  |         furi_assert(0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     StorageAnimationMeta* meta = animation_storage_get_meta(animation_manager->current_animation); | ||||||
|  |     /* copy str, not move, because it can be internal animation */ | ||||||
|  |     string_set(animation_manager->freezed_animation_name, meta->name); | ||||||
|  | 
 | ||||||
|  |     bubble_animation_freeze(animation_manager->animation_view); | ||||||
|  |     animation_storage_free_storage_animation(&animation_manager->current_animation); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_manager_load_and_continue_animation(AnimationManager* animation_manager) { | ||||||
|  |     furi_assert(animation_manager); | ||||||
|  |     furi_assert(!animation_manager->current_animation); | ||||||
|  |     furi_assert(string_size(animation_manager->freezed_animation_name)); | ||||||
|  |     furi_assert( | ||||||
|  |         (animation_manager->state == AnimationManagerStateFreezedIdle) || | ||||||
|  |         (animation_manager->state == AnimationManagerStateFreezedBlocked)); | ||||||
|  | 
 | ||||||
|  |     if(animation_manager->state == AnimationManagerStateFreezedBlocked) { | ||||||
|  |         StorageAnimation* restore_animation = animation_storage_find_animation( | ||||||
|  |             string_get_cstr(animation_manager->freezed_animation_name)); | ||||||
|  |         /* all blocked animations must be in flipper -> we can
 | ||||||
|  |          * always find blocking animation */ | ||||||
|  |         furi_assert(restore_animation); | ||||||
|  |         animation_manager_replace_current_animation(animation_manager, restore_animation); | ||||||
|  |         animation_manager->state = AnimationManagerStateBlocked; | ||||||
|  |     } else if(animation_manager->state == AnimationManagerStateFreezedIdle) { | ||||||
|  |         /* check if we missed some system notifications, and set current_animation */ | ||||||
|  |         bool blocked = animation_manager_check_blocking(animation_manager); | ||||||
|  |         if(!blocked) { | ||||||
|  |             /* if no blocking - try restore last one idle */ | ||||||
|  |             StorageAnimation* restore_animation = animation_storage_find_animation( | ||||||
|  |                 string_get_cstr(animation_manager->freezed_animation_name)); | ||||||
|  |             if(restore_animation) { | ||||||
|  |                 animation_manager_replace_current_animation(animation_manager, restore_animation); | ||||||
|  |                 animation_manager->state = AnimationManagerStateIdle; | ||||||
|  | 
 | ||||||
|  |                 if(animation_manager->freezed_animation_time_left) { | ||||||
|  |                     osTimerStart( | ||||||
|  |                         animation_manager->idle_animation_timer, | ||||||
|  |                         animation_manager->freezed_animation_time_left); | ||||||
|  |                 } else { | ||||||
|  |                     const BubbleAnimation* animation = animation_storage_get_bubble_animation( | ||||||
|  |                         animation_manager->current_animation); | ||||||
|  |                     osTimerStart( | ||||||
|  |                         animation_manager->idle_animation_timer, animation->duration * 1000); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 FURI_LOG_E( | ||||||
|  |                     TAG, | ||||||
|  |                     "Failed to restore \'%s\'", | ||||||
|  |                     string_get_cstr(animation_manager->freezed_animation_name)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         /* Unknown state is an error. But not in release version.*/ | ||||||
|  |         furi_assert(0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* if can't restore previous animation - select new */ | ||||||
|  |     if(!animation_manager->current_animation) { | ||||||
|  |         animation_manager_start_new_idle(animation_manager); | ||||||
|  |     } | ||||||
|  |     FURI_LOG_D( | ||||||
|  |         TAG, | ||||||
|  |         "Load & Continue with \'%s\'", | ||||||
|  |         string_get_cstr(animation_storage_get_meta(animation_manager->current_animation)->name)); | ||||||
|  | 
 | ||||||
|  |     bubble_animation_unfreeze(animation_manager->animation_view); | ||||||
|  |     string_reset(animation_manager->freezed_animation_name); | ||||||
|  |     furi_assert(animation_manager->current_animation); | ||||||
|  | } | ||||||
							
								
								
									
										151
									
								
								applications/desktop/animations/animation_manager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,151 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include "dolphin/dolphin.h" | ||||||
|  | #include <gui/view.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | 
 | ||||||
|  | typedef struct AnimationManager AnimationManager; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     uint8_t x; | ||||||
|  |     uint8_t y; | ||||||
|  |     const char* str; | ||||||
|  |     Align horizontal; | ||||||
|  |     Align vertical; | ||||||
|  | } Bubble; | ||||||
|  | 
 | ||||||
|  | typedef struct FrameBubble { | ||||||
|  |     Bubble bubble; | ||||||
|  |     uint8_t starts_at_frame; | ||||||
|  |     uint8_t ends_at_frame; | ||||||
|  |     struct FrameBubble* next_bubble; | ||||||
|  | } FrameBubble; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     FrameBubble** frame_bubbles; | ||||||
|  |     uint8_t frame_bubbles_count; | ||||||
|  |     const Icon** icons; | ||||||
|  |     uint8_t passive_frames; | ||||||
|  |     uint8_t active_frames; | ||||||
|  |     uint8_t active_cycles; | ||||||
|  |     uint8_t frame_rate; | ||||||
|  |     uint16_t duration; | ||||||
|  |     uint16_t active_cooldown; | ||||||
|  | } BubbleAnimation; | ||||||
|  | 
 | ||||||
|  | typedef void (*AnimationManagerSetNewIdleAnimationCallback)(void* context); | ||||||
|  | typedef void (*AnimationManagerCheckBlockingCallback)(void* context); | ||||||
|  | typedef void (*AnimationManagerInteractCallback)(void*); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Allocate Animation Manager | ||||||
|  |  * | ||||||
|  |  * @return animation manager instance | ||||||
|  |  */ | ||||||
|  | AnimationManager* animation_manager_alloc(void); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Free Animation Manager | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_free(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Get View of Animation Manager | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  * @return      view | ||||||
|  |  */ | ||||||
|  | View* animation_manager_get_animation_view(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set context for all callbacks for Animation Manager | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  * @context             context | ||||||
|  |  */ | ||||||
|  | void animation_manager_set_context(AnimationManager* animation_manager, void* context); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set callback for Animation Manager for defered calls | ||||||
|  |  * for animation_manager_new_idle_process(). | ||||||
|  |  * Animation Manager doesn't have it's own thread, so main thread gives | ||||||
|  |  * callbacks to A.M. to call when it should perform some inner manipulations. | ||||||
|  |  * This callback is called from other threads and should notify main thread | ||||||
|  |  * when to call animation_manager_new_idle_process(). | ||||||
|  |  * So scheme is this: | ||||||
|  |  * A.M. sets callbacks, | ||||||
|  |  * callbacks notifies main thread | ||||||
|  |  * main thread in its own context calls appropriate *_process() function. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  * @callback            callback | ||||||
|  |  */ | ||||||
|  | void animation_manager_set_new_idle_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerSetNewIdleAnimationCallback callback); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Function to call in main thread as a response to | ||||||
|  |  * set_new_idle_callback's call. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_new_idle_process(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set callback for Animation Manager for defered calls | ||||||
|  |  * for animation_manager_check_blocking_process(). | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  * @callback            callback | ||||||
|  |  */ | ||||||
|  | void animation_manager_set_check_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerCheckBlockingCallback callback); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Function to call in main thread as a response to | ||||||
|  |  * set_new_idle_callback's call. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_check_blocking_process(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set callback for Animation Manager for defered calls | ||||||
|  |  * for animation_manager_interact_process(). | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  * @callback            callback | ||||||
|  |  */ | ||||||
|  | void animation_manager_set_interact_callback( | ||||||
|  |     AnimationManager* animation_manager, | ||||||
|  |     AnimationManagerInteractCallback callback); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Function to call in main thread as a response to | ||||||
|  |  * set_new_idle_callback's call. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_interact_process(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Unload and Stall animation actions. Draw callback in view | ||||||
|  |  * paints first frame of current animation until | ||||||
|  |  * animation_manager_load_and_continue_animation() is called. | ||||||
|  |  * Can't be called multiple times. Every Stall has to be finished | ||||||
|  |  * with Continue. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_unload_and_stall_animation(AnimationManager* animation_manager); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Load and Contunue execution of animation manager. | ||||||
|  |  * | ||||||
|  |  * @animation_manager   instance | ||||||
|  |  */ | ||||||
|  | void animation_manager_load_and_continue_animation(AnimationManager* animation_manager); | ||||||
							
								
								
									
										482
									
								
								applications/desktop/animations/animation_storage.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,482 @@ | |||||||
|  | #include "animation_manager.h" | ||||||
|  | #include "file-worker.h" | ||||||
|  | #include "flipper_file.h" | ||||||
|  | #include "furi/common_defines.h" | ||||||
|  | #include "furi/memmgr.h" | ||||||
|  | #include "furi/record.h" | ||||||
|  | #include "animation_storage.h" | ||||||
|  | #include "gui/canvas.h" | ||||||
|  | #include "m-string.h" | ||||||
|  | #include "pb.h" | ||||||
|  | #include "pb_decode.h" | ||||||
|  | #include "storage/filesystem-api-defines.h" | ||||||
|  | #include "storage/storage.h" | ||||||
|  | #include "animation_storage_i.h" | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <gui/icon_i.h> | ||||||
|  | 
 | ||||||
|  | #define ANIMATION_META_FILE "meta.txt" | ||||||
|  | #define ANIMATION_DIR "/ext/dolphin/animations" | ||||||
|  | #define ANIMATION_MANIFEST_FILE ANIMATION_DIR "/manifest.txt" | ||||||
|  | #define TAG "AnimationStorage" | ||||||
|  | #define DEBUG_PB 0 | ||||||
|  | 
 | ||||||
|  | static void animation_storage_free_bubbles(BubbleAnimation* animation); | ||||||
|  | static void animation_storage_free_frames(BubbleAnimation* animation); | ||||||
|  | static void animation_storage_free_animation(BubbleAnimation** storage_animation); | ||||||
|  | static BubbleAnimation* animation_storage_load_animation(const char* name); | ||||||
|  | 
 | ||||||
|  | void animation_storage_fill_animation_list(StorageAnimationList_t* animation_list) { | ||||||
|  |     furi_assert(sizeof(StorageAnimationList_t) == sizeof(void*)); | ||||||
|  |     furi_assert(!StorageAnimationList_size(*animation_list)); | ||||||
|  | 
 | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     FlipperFile* file = flipper_file_alloc(storage); | ||||||
|  |     /* Forbid skipping fields */ | ||||||
|  |     flipper_file_set_strict_mode(file, true); | ||||||
|  |     string_t header; | ||||||
|  |     string_init(header); | ||||||
|  | 
 | ||||||
|  |     do { | ||||||
|  |         uint32_t u32value; | ||||||
|  |         StorageAnimation* storage_animation = NULL; | ||||||
|  | 
 | ||||||
|  |         if(FSE_OK != storage_sd_status(storage)) break; | ||||||
|  |         if(!flipper_file_open_existing(file, ANIMATION_MANIFEST_FILE)) break; | ||||||
|  |         if(!flipper_file_read_header(file, header, &u32value)) break; | ||||||
|  |         if(string_cmp_str(header, "Flipper Animation Manifest")) break; | ||||||
|  |         do { | ||||||
|  |             storage_animation = furi_alloc(sizeof(StorageAnimation)); | ||||||
|  |             storage_animation->external = true; | ||||||
|  |             storage_animation->animation = NULL; | ||||||
|  | 
 | ||||||
|  |             if(!flipper_file_read_string(file, "Name", storage_animation->meta.name)) break; | ||||||
|  |             if(!flipper_file_read_uint32(file, "Min butthurt", &u32value, 1)) break; | ||||||
|  |             storage_animation->meta.min_butthurt = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(file, "Max butthurt", &u32value, 1)) break; | ||||||
|  |             storage_animation->meta.max_butthurt = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(file, "Min level", &u32value, 1)) break; | ||||||
|  |             storage_animation->meta.min_level = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(file, "Max level", &u32value, 1)) break; | ||||||
|  |             storage_animation->meta.max_level = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(file, "Weight", &u32value, 1)) break; | ||||||
|  |             storage_animation->meta.weight = u32value; | ||||||
|  | 
 | ||||||
|  |             StorageAnimationList_push_back(*animation_list, storage_animation); | ||||||
|  |         } while(1); | ||||||
|  | 
 | ||||||
|  |         animation_storage_free_storage_animation(&storage_animation); | ||||||
|  |     } while(0); | ||||||
|  | 
 | ||||||
|  |     string_clear(header); | ||||||
|  |     flipper_file_close(file); | ||||||
|  |     flipper_file_free(file); | ||||||
|  | 
 | ||||||
|  |     // add hard-coded animations
 | ||||||
|  |     for(int i = 0; i < COUNT_OF(StorageAnimationInternal); ++i) { | ||||||
|  |         StorageAnimationList_push_back(*animation_list, &StorageAnimationInternal[i]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_record_close("storage"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | StorageAnimation* animation_storage_find_animation(const char* name) { | ||||||
|  |     furi_assert(name); | ||||||
|  |     furi_assert(strlen(name)); | ||||||
|  |     StorageAnimation* storage_animation = NULL; | ||||||
|  | 
 | ||||||
|  |     /* look through internal animations */ | ||||||
|  |     for(int i = 0; i < COUNT_OF(StorageAnimationInternal); ++i) { | ||||||
|  |         if(!string_cmp_str(StorageAnimationInternal[i].meta.name, name)) { | ||||||
|  |             storage_animation = &StorageAnimationInternal[i]; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* look through external animations */ | ||||||
|  |     if(!storage_animation) { | ||||||
|  |         BubbleAnimation* animation = animation_storage_load_animation(name); | ||||||
|  | 
 | ||||||
|  |         if(animation != NULL) { | ||||||
|  |             storage_animation = furi_alloc(sizeof(StorageAnimation)); | ||||||
|  |             storage_animation->animation = animation; | ||||||
|  |             storage_animation->external = true; | ||||||
|  |             /* meta data takes part in random animation selection, so it
 | ||||||
|  |              * doesn't need here as we exactly know which animation we need, | ||||||
|  |              * that's why we can ignore reading manifest.txt file | ||||||
|  |              * filling meta data by zeroes */ | ||||||
|  |             storage_animation->meta.min_butthurt = 0; | ||||||
|  |             storage_animation->meta.max_butthurt = 0; | ||||||
|  |             storage_animation->meta.min_level = 0; | ||||||
|  |             storage_animation->meta.max_level = 0; | ||||||
|  |             storage_animation->meta.weight = 0; | ||||||
|  |             string_init_set_str(storage_animation->meta.name, name); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return storage_animation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | StorageAnimationMeta* animation_storage_get_meta(StorageAnimation* storage_animation) { | ||||||
|  |     furi_assert(storage_animation); | ||||||
|  |     return &storage_animation->meta; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const BubbleAnimation* | ||||||
|  |     animation_storage_get_bubble_animation(StorageAnimation* storage_animation) { | ||||||
|  |     furi_assert(storage_animation); | ||||||
|  |     animation_storage_cache_animation(storage_animation); | ||||||
|  |     return storage_animation->animation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_storage_cache_animation(StorageAnimation* storage_animation) { | ||||||
|  |     furi_assert(storage_animation); | ||||||
|  | 
 | ||||||
|  |     if(storage_animation->external) { | ||||||
|  |         if(!storage_animation->animation) { | ||||||
|  |             storage_animation->animation = | ||||||
|  |                 animation_storage_load_animation(string_get_cstr(storage_animation->meta.name)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_storage_free_animation(BubbleAnimation** animation) { | ||||||
|  |     furi_assert(animation); | ||||||
|  | 
 | ||||||
|  |     if(*animation) { | ||||||
|  |         animation_storage_free_bubbles(*animation); | ||||||
|  |         animation_storage_free_frames(*animation); | ||||||
|  |         free(*animation); | ||||||
|  |         *animation = NULL; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void animation_storage_free_storage_animation(StorageAnimation** storage_animation) { | ||||||
|  |     furi_assert(storage_animation); | ||||||
|  |     furi_assert(*storage_animation); | ||||||
|  | 
 | ||||||
|  |     if((*storage_animation)->external) { | ||||||
|  |         animation_storage_free_animation((BubbleAnimation**)&(*storage_animation)->animation); | ||||||
|  | 
 | ||||||
|  |         string_clear((*storage_animation)->meta.name); | ||||||
|  |         free(*storage_animation); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     *storage_animation = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool animation_storage_cast_align(string_t align_str, Align* align) { | ||||||
|  |     if(!string_cmp_str(align_str, "Bottom")) { | ||||||
|  |         *align = AlignBottom; | ||||||
|  |     } else if(!string_cmp_str(align_str, "Top")) { | ||||||
|  |         *align = AlignTop; | ||||||
|  |     } else if(!string_cmp_str(align_str, "Left")) { | ||||||
|  |         *align = AlignLeft; | ||||||
|  |     } else if(!string_cmp_str(align_str, "Right")) { | ||||||
|  |         *align = AlignRight; | ||||||
|  |     } else if(!string_cmp_str(align_str, "Center")) { | ||||||
|  |         *align = AlignCenter; | ||||||
|  |     } else { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_storage_free_frames(BubbleAnimation* animation) { | ||||||
|  |     furi_assert(animation); | ||||||
|  |     furi_assert(animation->icons); | ||||||
|  | 
 | ||||||
|  |     const Icon** icons = animation->icons; | ||||||
|  |     uint16_t frames = animation->active_frames + animation->passive_frames; | ||||||
|  |     furi_assert(frames > 0); | ||||||
|  | 
 | ||||||
|  |     for(int i = 0; i < frames; ++i) { | ||||||
|  |         if(!icons[i]) continue; | ||||||
|  | 
 | ||||||
|  |         const Icon* icon = icons[i]; | ||||||
|  |         free((void*)icon->frames[0]); | ||||||
|  |         free(icon->frames); | ||||||
|  |         free((void*)icon); | ||||||
|  |         for(int j = i; j < frames; ++j) { | ||||||
|  |             if(icons[j] == icon) { | ||||||
|  |                 icons[j] = NULL; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     free(animation->icons); | ||||||
|  |     animation->icons = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static Icon* animation_storage_alloc_icon(size_t frame_size) { | ||||||
|  |     Icon* icon = furi_alloc(sizeof(Icon)); | ||||||
|  |     icon->frames = furi_alloc(sizeof(const uint8_t*)); | ||||||
|  |     icon->frames[0] = furi_alloc(frame_size); | ||||||
|  |     return icon; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_storage_free_icon(Icon* icon) { | ||||||
|  |     free((void*)icon->frames[0]); | ||||||
|  |     free(icon->frames); | ||||||
|  |     free(icon); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool animation_storage_load_frames( | ||||||
|  |     Storage* storage, | ||||||
|  |     const char* name, | ||||||
|  |     BubbleAnimation* animation, | ||||||
|  |     uint32_t* frame_order, | ||||||
|  |     uint32_t width, | ||||||
|  |     uint32_t height) { | ||||||
|  |     furi_assert(!animation->icons); | ||||||
|  |     uint16_t frame_order_size = animation->passive_frames + animation->active_frames; | ||||||
|  | 
 | ||||||
|  |     bool frames_ok = false; | ||||||
|  |     animation->icons = furi_alloc(sizeof(const Icon*) * frame_order_size); | ||||||
|  |     File* file = storage_file_alloc(storage); | ||||||
|  |     FileInfo file_info; | ||||||
|  |     string_t filename; | ||||||
|  |     string_init(filename); | ||||||
|  |     size_t max_filesize = ROUND_UP_TO(width, 8) * height + 1; | ||||||
|  | 
 | ||||||
|  |     for(int i = 0; i < frame_order_size; ++i) { | ||||||
|  |         if(animation->icons[i]) continue; | ||||||
|  | 
 | ||||||
|  |         frames_ok = false; | ||||||
|  |         string_printf(filename, ANIMATION_DIR "/%s/frame_%d.bm", name, frame_order[i]); | ||||||
|  | 
 | ||||||
|  |         if(storage_common_stat(storage, string_get_cstr(filename), &file_info) != FSE_OK) break; | ||||||
|  |         if(file_info.size > max_filesize) { | ||||||
|  |             FURI_LOG_E( | ||||||
|  |                 TAG, | ||||||
|  |                 "Filesize %d, max: %d (width %d, height %d)", | ||||||
|  |                 file_info.size, | ||||||
|  |                 max_filesize, | ||||||
|  |                 width, | ||||||
|  |                 height); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         if(!storage_file_open(file, string_get_cstr(filename), FSAM_READ, FSOM_OPEN_EXISTING)) { | ||||||
|  |             FURI_LOG_E(TAG, "Can't open file \'%s\'", string_get_cstr(filename)); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Icon* icon = animation_storage_alloc_icon(file_info.size); | ||||||
|  |         if(storage_file_read(file, (void*)icon->frames[0], file_info.size) != file_info.size) { | ||||||
|  |             FURI_LOG_E(TAG, "Read failed: \'%s\'", string_get_cstr(filename)); | ||||||
|  |             animation_storage_free_icon(icon); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         storage_file_close(file); | ||||||
|  |         FURI_CONST_ASSIGN(icon->frame_count, 1); | ||||||
|  |         FURI_CONST_ASSIGN(icon->frame_rate, 0); | ||||||
|  |         FURI_CONST_ASSIGN(icon->height, height); | ||||||
|  |         FURI_CONST_ASSIGN(icon->width, width); | ||||||
|  | 
 | ||||||
|  |         /* Claim 1 allocation for 1 files blob and several links to it */ | ||||||
|  |         for(int j = i; j < frame_order_size; ++j) { | ||||||
|  |             if(frame_order[i] == frame_order[j]) { | ||||||
|  |                 animation->icons[j] = icon; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         frames_ok = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(!frames_ok) { | ||||||
|  |         FURI_LOG_E( | ||||||
|  |             TAG, | ||||||
|  |             "Load \'%s\' failed, %dx%d, size: %d", | ||||||
|  |             string_get_cstr(filename), | ||||||
|  |             width, | ||||||
|  |             height, | ||||||
|  |             file_info.size); | ||||||
|  |         animation_storage_free_frames(animation); | ||||||
|  |         animation->icons = NULL; | ||||||
|  |     } else { | ||||||
|  |         for(int i = 0; i < frame_order_size; ++i) { | ||||||
|  |             furi_check(animation->icons[i]); | ||||||
|  |             furi_check(animation->icons[i]->frames[0]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     storage_file_free(file); | ||||||
|  |     string_clear(filename); | ||||||
|  | 
 | ||||||
|  |     return frames_ok; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool animation_storage_load_bubbles(BubbleAnimation* animation, FlipperFile* ff) { | ||||||
|  |     uint32_t u32value; | ||||||
|  |     string_t str; | ||||||
|  |     string_init(str); | ||||||
|  |     bool success = false; | ||||||
|  |     furi_assert(!animation->frame_bubbles); | ||||||
|  | 
 | ||||||
|  |     do { | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Bubble slots", &u32value, 1)) break; | ||||||
|  |         if(u32value > 20) break; | ||||||
|  |         animation->frame_bubbles_count = u32value; | ||||||
|  |         if(animation->frame_bubbles_count == 0) { | ||||||
|  |             animation->frame_bubbles = NULL; | ||||||
|  |             success = true; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         animation->frame_bubbles = | ||||||
|  |             furi_alloc(sizeof(FrameBubble*) * animation->frame_bubbles_count); | ||||||
|  | 
 | ||||||
|  |         uint32_t current_slot = 0; | ||||||
|  |         for(int i = 0; i < animation->frame_bubbles_count; ++i) { | ||||||
|  |             animation->frame_bubbles[i] = furi_alloc(sizeof(FrameBubble)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         FrameBubble* bubble = animation->frame_bubbles[0]; | ||||||
|  |         int8_t index = -1; | ||||||
|  |         for(;;) { | ||||||
|  |             if(!flipper_file_read_uint32(ff, "Slot", ¤t_slot, 1)) break; | ||||||
|  |             if((current_slot != 0) && (index == -1)) break; | ||||||
|  | 
 | ||||||
|  |             if(current_slot == index) { | ||||||
|  |                 bubble->next_bubble = furi_alloc(sizeof(FrameBubble)); | ||||||
|  |                 bubble = bubble->next_bubble; | ||||||
|  |             } else if(current_slot == index + 1) { | ||||||
|  |                 ++index; | ||||||
|  |                 bubble = animation->frame_bubbles[index]; | ||||||
|  |             } else { | ||||||
|  |                 /* slots have to start from 0, be ascending sorted, and
 | ||||||
|  |                  * have exact number of slots as specified in "Bubble slots" */ | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             if(index >= animation->frame_bubbles_count) break; | ||||||
|  | 
 | ||||||
|  |             if(!flipper_file_read_uint32(ff, "X", &u32value, 1)) break; | ||||||
|  |             bubble->bubble.x = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(ff, "Y", &u32value, 1)) break; | ||||||
|  |             bubble->bubble.y = u32value; | ||||||
|  | 
 | ||||||
|  |             if(!flipper_file_read_string(ff, "Text", str)) break; | ||||||
|  |             if(string_size(str) > 100) break; | ||||||
|  | 
 | ||||||
|  |             string_replace_all_str(str, "\\n", "\n"); | ||||||
|  | 
 | ||||||
|  |             bubble->bubble.str = furi_alloc(string_size(str) + 1); | ||||||
|  |             strcpy((char*)bubble->bubble.str, string_get_cstr(str)); | ||||||
|  | 
 | ||||||
|  |             if(!flipper_file_read_string(ff, "AlignH", str)) break; | ||||||
|  |             if(!animation_storage_cast_align(str, &bubble->bubble.horizontal)) break; | ||||||
|  |             if(!flipper_file_read_string(ff, "AlignV", str)) break; | ||||||
|  |             if(!animation_storage_cast_align(str, &bubble->bubble.vertical)) break; | ||||||
|  | 
 | ||||||
|  |             if(!flipper_file_read_uint32(ff, "StartFrame", &u32value, 1)) break; | ||||||
|  |             bubble->starts_at_frame = u32value; | ||||||
|  |             if(!flipper_file_read_uint32(ff, "EndFrame", &u32value, 1)) break; | ||||||
|  |             bubble->ends_at_frame = u32value; | ||||||
|  |         } | ||||||
|  |         success = (index + 1) == animation->frame_bubbles_count; | ||||||
|  |     } while(0); | ||||||
|  | 
 | ||||||
|  |     if(!success) { | ||||||
|  |         if(animation->frame_bubbles) { | ||||||
|  |             FURI_LOG_E(TAG, "Failed to load animation bubbles"); | ||||||
|  |             animation_storage_free_bubbles(animation); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     string_clear(str); | ||||||
|  |     return success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static BubbleAnimation* animation_storage_load_animation(const char* name) { | ||||||
|  |     furi_assert(name); | ||||||
|  |     BubbleAnimation* animation = furi_alloc(sizeof(BubbleAnimation)); | ||||||
|  | 
 | ||||||
|  |     uint32_t height = 0; | ||||||
|  |     uint32_t width = 0; | ||||||
|  |     uint32_t* u32array = NULL; | ||||||
|  |     Storage* storage = furi_record_open("storage"); | ||||||
|  |     FlipperFile* ff = flipper_file_alloc(storage); | ||||||
|  |     /* Forbid skipping fields */ | ||||||
|  |     flipper_file_set_strict_mode(ff, true); | ||||||
|  |     string_t str; | ||||||
|  |     string_init(str); | ||||||
|  |     animation->frame_bubbles = NULL; | ||||||
|  | 
 | ||||||
|  |     bool success = false; | ||||||
|  |     do { | ||||||
|  |         uint32_t u32value; | ||||||
|  | 
 | ||||||
|  |         if(FSE_OK != storage_sd_status(storage)) break; | ||||||
|  | 
 | ||||||
|  |         string_printf(str, ANIMATION_DIR "/%s/" ANIMATION_META_FILE, name); | ||||||
|  |         if(!flipper_file_open_existing(ff, string_get_cstr(str))) break; | ||||||
|  |         if(!flipper_file_read_header(ff, str, &u32value)) break; | ||||||
|  |         if(string_cmp_str(str, "Flipper Animation")) break; | ||||||
|  | 
 | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Width", &width, 1)) break; | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Height", &height, 1)) break; | ||||||
|  | 
 | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Passive frames", &u32value, 1)) break; | ||||||
|  |         animation->passive_frames = u32value; | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Active frames", &u32value, 1)) break; | ||||||
|  |         animation->active_frames = u32value; | ||||||
|  | 
 | ||||||
|  |         uint8_t frames = animation->passive_frames + animation->active_frames; | ||||||
|  |         u32array = furi_alloc(sizeof(uint32_t) * frames); | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Frames order", u32array, frames)) break; | ||||||
|  | 
 | ||||||
|  |         /* passive and active frames must be loaded up to this point */ | ||||||
|  |         if(!animation_storage_load_frames(storage, name, animation, u32array, width, height)) | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Active cycles", &u32value, 1)) break; | ||||||
|  |         animation->active_cycles = u32value; | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Frame rate", &u32value, 1)) break; | ||||||
|  |         animation->frame_rate = u32value; | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Duration", &u32value, 1)) break; | ||||||
|  |         animation->duration = u32value; | ||||||
|  |         if(!flipper_file_read_uint32(ff, "Active cooldown", &u32value, 1)) break; | ||||||
|  |         animation->active_cooldown = u32value; | ||||||
|  | 
 | ||||||
|  |         if(!animation_storage_load_bubbles(animation, ff)) break; | ||||||
|  |         success = true; | ||||||
|  |     } while(0); | ||||||
|  | 
 | ||||||
|  |     string_clear(str); | ||||||
|  |     flipper_file_close(ff); | ||||||
|  |     flipper_file_free(ff); | ||||||
|  |     if(u32array) { | ||||||
|  |         free(u32array); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(!success) { | ||||||
|  |         free(animation); | ||||||
|  |         animation = NULL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return animation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void animation_storage_free_bubbles(BubbleAnimation* animation) { | ||||||
|  |     if(!animation->frame_bubbles) return; | ||||||
|  | 
 | ||||||
|  |     for(int i = 0; i < animation->frame_bubbles_count;) { | ||||||
|  |         FrameBubble** bubble = &animation->frame_bubbles[i]; | ||||||
|  | 
 | ||||||
|  |         if((*bubble) == NULL) break; | ||||||
|  | 
 | ||||||
|  |         while((*bubble)->next_bubble != NULL) { | ||||||
|  |             bubble = &(*bubble)->next_bubble; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if((*bubble)->bubble.str) { | ||||||
|  |             free((void*)(*bubble)->bubble.str); | ||||||
|  |         } | ||||||
|  |         if((*bubble) == animation->frame_bubbles[i]) { | ||||||
|  |             ++i; | ||||||
|  |         } | ||||||
|  |         free(*bubble); | ||||||
|  |         *bubble = NULL; | ||||||
|  |     } | ||||||
|  |     free(animation->frame_bubbles); | ||||||
|  |     animation->frame_bubbles = NULL; | ||||||
|  | } | ||||||
							
								
								
									
										98
									
								
								applications/desktop/animations/animation_storage.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,98 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <m-list.h> | ||||||
|  | #include "views/bubble_animation_view.h" | ||||||
|  | #include <m-string.h> | ||||||
|  | 
 | ||||||
|  | #define HARDCODED_ANIMATION_NAME "tv" | ||||||
|  | #define NO_SD_ANIMATION_NAME "no_sd" | ||||||
|  | #define BAD_BATTERY_ANIMATION_NAME "bad_battery" | ||||||
|  | #define NO_DB_ANIMATION_NAME "no_db" | ||||||
|  | #define BAD_SD_ANIMATION_NAME "bad_sd" | ||||||
|  | #define SD_OK_ANIMATION_NAME "sd_ok" | ||||||
|  | #define URL_ANIMATION_NAME "url" | ||||||
|  | #define LEVELUP_ANIMATION_NAME "level" | ||||||
|  | 
 | ||||||
|  | /** Main structure to handle animation data.
 | ||||||
|  |  * Contains all, including animation playing data (BubbleAnimation), | ||||||
|  |  * data for random animation selection (StorageAnimationMeta) and | ||||||
|  |  * flag of location internal/external */ | ||||||
|  | typedef struct StorageAnimation StorageAnimation; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     string_t name; | ||||||
|  |     uint8_t min_butthurt; | ||||||
|  |     uint8_t max_butthurt; | ||||||
|  |     uint8_t min_level; | ||||||
|  |     uint8_t max_level; | ||||||
|  |     uint8_t weight; | ||||||
|  | } StorageAnimationMeta; | ||||||
|  | 
 | ||||||
|  | /** Container to return available animations list */ | ||||||
|  | LIST_DEF(StorageAnimationList, StorageAnimation*, M_PTR_OPLIST) | ||||||
|  | #define M_OPL_StorageAnimationList_t() LIST_OPLIST(StorageAnimationList) | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Fill list of available animations. | ||||||
|  |  * List will contain all idle animations on inner flash | ||||||
|  |  * and all available on SD-card, mentioned in manifest.txt. | ||||||
|  |  * Performs caching of animation. If fail - falls back to | ||||||
|  |  * inner animation. | ||||||
|  |  * List has to be initialized. | ||||||
|  |  * | ||||||
|  |  * @list        list to fill with animations data | ||||||
|  |  */ | ||||||
|  | void animation_storage_fill_animation_list(StorageAnimationList_t* list); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Get bubble animation of storage animation. | ||||||
|  |  * Bubble Animation is a structure which describes animation | ||||||
|  |  * independent of it's place of storage and meta data. | ||||||
|  |  * It contain all what is need to be played. | ||||||
|  |  * If storage_animation is not cached - caches it. | ||||||
|  |  * | ||||||
|  |  * @storage_animation       animation from which extract bubble animation | ||||||
|  |  * @return                  bubble_animation, NULL if failed to cache data. | ||||||
|  |  */ | ||||||
|  | const BubbleAnimation* animation_storage_get_bubble_animation(StorageAnimation* storage_animation); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Performs caching animation data (Bubble Animation) | ||||||
|  |  * if this is not done yet. | ||||||
|  |  * | ||||||
|  |  * @storage_animation       animation to cache | ||||||
|  |  */ | ||||||
|  | void animation_storage_cache_animation(StorageAnimation* storage_animation); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Find animation by name. | ||||||
|  |  * Search through the inner flash, and SD-card if has. | ||||||
|  |  * | ||||||
|  |  * @name        name of animation | ||||||
|  |  * @return      found animation. NULL if nothing found. | ||||||
|  |  */ | ||||||
|  | StorageAnimation* animation_storage_find_animation(const char* name); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Get meta information of storage animation. | ||||||
|  |  * This information allows to randomly select animation. | ||||||
|  |  * Also it contains name. Never returns NULL. | ||||||
|  |  * | ||||||
|  |  * @storage_animation       item of whom we have to extract meta. | ||||||
|  |  * @return                  meta itself | ||||||
|  |  */ | ||||||
|  | StorageAnimationMeta* animation_storage_get_meta(StorageAnimation* storage_animation); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Free storage_animation, which previously acquired | ||||||
|  |  * by Animation Storage. | ||||||
|  |  * | ||||||
|  |  * @storage_animation   item to free. NULL-ed after all. | ||||||
|  |  */ | ||||||
|  | void animation_storage_free_storage_animation(StorageAnimation** storage_animation); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Has to be called at least 1 time to initialize runtime structures | ||||||
|  |  * of animations in inner flash. | ||||||
|  |  */ | ||||||
|  | void animation_storage_initialize_internal_animations(void); | ||||||
							
								
								
									
										195
									
								
								applications/desktop/animations/animation_storage_i.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,195 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "animation_storage.h" | ||||||
|  | #include "assets_icons.h" | ||||||
|  | #include "animation_manager.h" | ||||||
|  | #include "gui/canvas.h" | ||||||
|  | 
 | ||||||
|  | struct StorageAnimation { | ||||||
|  |     const BubbleAnimation* animation; | ||||||
|  |     bool external; | ||||||
|  |     StorageAnimationMeta meta; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Hard-coded, always available idle animation
 | ||||||
|  | FrameBubble tv_bubble1 = { | ||||||
|  |     .bubble = | ||||||
|  |         {.x = 1, | ||||||
|  |          .y = 23, | ||||||
|  |          .str = "Take the red pill", | ||||||
|  |          .horizontal = AlignRight, | ||||||
|  |          .vertical = AlignBottom}, | ||||||
|  |     .starts_at_frame = 7, | ||||||
|  |     .ends_at_frame = 9, | ||||||
|  |     .next_bubble = NULL, | ||||||
|  | }; | ||||||
|  | FrameBubble tv_bubble2 = { | ||||||
|  |     .bubble = | ||||||
|  |         {.x = 1, | ||||||
|  |          .y = 23, | ||||||
|  |          .str = "I can joke better", | ||||||
|  |          .horizontal = AlignRight, | ||||||
|  |          .vertical = AlignBottom}, | ||||||
|  |     .starts_at_frame = 7, | ||||||
|  |     .ends_at_frame = 9, | ||||||
|  |     .next_bubble = NULL, | ||||||
|  | }; | ||||||
|  | FrameBubble* tv_bubbles[] = {&tv_bubble1, &tv_bubble2}; | ||||||
|  | const Icon* tv_icons[] = { | ||||||
|  |     &I_tv1, | ||||||
|  |     &I_tv2, | ||||||
|  |     &I_tv3, | ||||||
|  |     &I_tv4, | ||||||
|  |     &I_tv5, | ||||||
|  |     &I_tv6, | ||||||
|  |     &I_tv7, | ||||||
|  |     &I_tv8, | ||||||
|  | }; | ||||||
|  | const BubbleAnimation tv_bubble_animation = { | ||||||
|  |     .icons = tv_icons, | ||||||
|  |     .frame_bubbles = tv_bubbles, | ||||||
|  |     .frame_bubbles_count = COUNT_OF(tv_bubbles), | ||||||
|  |     .passive_frames = 6, | ||||||
|  |     .active_frames = 2, | ||||||
|  |     .active_cycles = 2, | ||||||
|  |     .frame_rate = 2, | ||||||
|  |     .duration = 3600, | ||||||
|  |     .active_cooldown = 5, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // System animation - no SD card
 | ||||||
|  | const Icon* no_sd_icons[] = { | ||||||
|  |     &I_no_sd1, | ||||||
|  |     &I_no_sd2, | ||||||
|  |     &I_no_sd1, | ||||||
|  |     &I_no_sd2, | ||||||
|  |     &I_no_sd1, | ||||||
|  |     &I_no_sd3, | ||||||
|  |     &I_no_sd4, | ||||||
|  |     &I_no_sd5, | ||||||
|  |     &I_no_sd4, | ||||||
|  |     &I_no_sd6, | ||||||
|  | }; | ||||||
|  | FrameBubble no_sd_bubble = { | ||||||
|  |     .bubble = | ||||||
|  |         {.x = 40, | ||||||
|  |          .y = 18, | ||||||
|  |          .str = "Need an\nSD card", | ||||||
|  |          .horizontal = AlignRight, | ||||||
|  |          .vertical = AlignBottom}, | ||||||
|  |     .starts_at_frame = 0, | ||||||
|  |     .ends_at_frame = 9, | ||||||
|  |     .next_bubble = NULL, | ||||||
|  | }; | ||||||
|  | FrameBubble* no_sd_bubbles[] = {&no_sd_bubble}; | ||||||
|  | const BubbleAnimation no_sd_bubble_animation = { | ||||||
|  |     .icons = no_sd_icons, | ||||||
|  |     .frame_bubbles = no_sd_bubbles, | ||||||
|  |     .frame_bubbles_count = COUNT_OF(no_sd_bubbles), | ||||||
|  |     .passive_frames = 10, | ||||||
|  |     .active_frames = 0, | ||||||
|  |     .frame_rate = 2, | ||||||
|  |     .duration = 3600, | ||||||
|  |     .active_cooldown = 0, | ||||||
|  |     .active_cycles = 0, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // BLOCKING ANIMATION - no_db, bad_sd, sd_ok, url
 | ||||||
|  | const Icon* no_db_icons[] = { | ||||||
|  |     &I_no_databases1, | ||||||
|  |     &I_no_databases2, | ||||||
|  |     &I_no_databases3, | ||||||
|  |     &I_no_databases4, | ||||||
|  | }; | ||||||
|  | const BubbleAnimation no_db_bubble_animation = { | ||||||
|  |     .icons = no_db_icons, | ||||||
|  |     .passive_frames = COUNT_OF(no_db_icons), | ||||||
|  |     .frame_rate = 2, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Icon* bad_sd_icons[] = { | ||||||
|  |     &I_card_bad1, | ||||||
|  |     &I_card_bad2, | ||||||
|  | }; | ||||||
|  | const BubbleAnimation bad_sd_bubble_animation = { | ||||||
|  |     .icons = bad_sd_icons, | ||||||
|  |     .passive_frames = COUNT_OF(bad_sd_icons), | ||||||
|  |     .frame_rate = 2, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Icon* url_icons[] = { | ||||||
|  |     &I_url1, | ||||||
|  |     &I_url2, | ||||||
|  |     &I_url3, | ||||||
|  |     &I_url4, | ||||||
|  | }; | ||||||
|  | const BubbleAnimation url_bubble_animation = { | ||||||
|  |     .icons = url_icons, | ||||||
|  |     .passive_frames = COUNT_OF(url_icons), | ||||||
|  |     .frame_rate = 2, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Icon* sd_ok_icons[] = { | ||||||
|  |     &I_card_ok1, | ||||||
|  |     &I_card_ok2, | ||||||
|  |     &I_card_ok3, | ||||||
|  |     &I_card_ok4, | ||||||
|  | }; | ||||||
|  | const BubbleAnimation sd_ok_bubble_animation = { | ||||||
|  |     .icons = sd_ok_icons, | ||||||
|  |     .passive_frames = COUNT_OF(sd_ok_icons), | ||||||
|  |     .frame_rate = 2, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static StorageAnimation StorageAnimationInternal[] = { | ||||||
|  |     {.animation = &tv_bubble_animation, | ||||||
|  |      .external = false, | ||||||
|  |      .meta = | ||||||
|  |          { | ||||||
|  |              .min_butthurt = 0, | ||||||
|  |              .max_butthurt = 11, | ||||||
|  |              .min_level = 1, | ||||||
|  |              .max_level = 3, | ||||||
|  |              .weight = 3, | ||||||
|  |          }}, | ||||||
|  |     {.animation = &no_sd_bubble_animation, | ||||||
|  |      .external = false, | ||||||
|  |      .meta = | ||||||
|  |          { | ||||||
|  |              .min_butthurt = 0, | ||||||
|  |              .max_butthurt = 14, | ||||||
|  |              .min_level = 1, | ||||||
|  |              .max_level = 3, | ||||||
|  |              .weight = 6, | ||||||
|  |          }}, | ||||||
|  |     { | ||||||
|  |         .animation = &no_db_bubble_animation, | ||||||
|  |         .external = false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         .animation = &bad_sd_bubble_animation, | ||||||
|  |         .external = false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         .animation = &sd_ok_bubble_animation, | ||||||
|  |         .external = false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         .animation = &url_bubble_animation, | ||||||
|  |         .external = false, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | void animation_storage_initialize_internal_animations(void) { | ||||||
|  |     /* not in constructor - no memory pool yet */ | ||||||
|  |     /* called in 1 thread - no need in double check */ | ||||||
|  |     static bool initialized = false; | ||||||
|  |     if(!initialized) { | ||||||
|  |         initialized = true; | ||||||
|  |         string_init_set_str(StorageAnimationInternal[0].meta.name, HARDCODED_ANIMATION_NAME); | ||||||
|  |         string_init_set_str(StorageAnimationInternal[1].meta.name, NO_SD_ANIMATION_NAME); | ||||||
|  |         string_init_set_str(StorageAnimationInternal[2].meta.name, NO_DB_ANIMATION_NAME); | ||||||
|  |         string_init_set_str(StorageAnimationInternal[3].meta.name, BAD_SD_ANIMATION_NAME); | ||||||
|  |         string_init_set_str(StorageAnimationInternal[4].meta.name, SD_OK_ANIMATION_NAME); | ||||||
|  |         string_init_set_str(StorageAnimationInternal[5].meta.name, URL_ANIMATION_NAME); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										410
									
								
								applications/desktop/animations/views/bubble_animation_view.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,410 @@ | |||||||
|  | 
 | ||||||
|  | #include "cmsis_os2.h" | ||||||
|  | #include "../animation_manager.h" | ||||||
|  | #include "../animation_storage.h" | ||||||
|  | #include "furi-hal-delay.h" | ||||||
|  | #include "furi-hal-resources.h" | ||||||
|  | #include "furi/check.h" | ||||||
|  | #include "furi/memmgr.h" | ||||||
|  | #include "gui/canvas.h" | ||||||
|  | #include "gui/elements.h" | ||||||
|  | #include "gui/view.h" | ||||||
|  | #include "input/input.h" | ||||||
|  | #include <furi.h> | ||||||
|  | #include "portmacro.h" | ||||||
|  | #include <gui/icon.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | #include <FreeRTOS.h> | ||||||
|  | #include <timers.h> | ||||||
|  | #include "bubble_animation_view.h" | ||||||
|  | #include <gui/icon_i.h> | ||||||
|  | 
 | ||||||
|  | #define ACTIVE_SHIFT 2 | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     const BubbleAnimation* current; | ||||||
|  |     const FrameBubble* current_bubble; | ||||||
|  |     uint8_t current_frame; | ||||||
|  |     uint8_t active_cycle; | ||||||
|  |     uint8_t active_bubbles; | ||||||
|  |     uint8_t passive_bubbles; | ||||||
|  |     uint8_t active_shift; | ||||||
|  |     TickType_t active_ended_at; | ||||||
|  |     Icon* freeze_frame; | ||||||
|  | } BubbleAnimationViewModel; | ||||||
|  | 
 | ||||||
|  | struct BubbleAnimationView { | ||||||
|  |     View* view; | ||||||
|  |     osTimerId_t timer; | ||||||
|  |     BubbleAnimationInteractCallback interact_callback; | ||||||
|  |     void* interact_callback_context; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_activate(BubbleAnimationView* view, bool force); | ||||||
|  | static void bubble_animation_activate_right_now(BubbleAnimationView* view); | ||||||
|  | 
 | ||||||
|  | static uint8_t bubble_animation_get_icon_index(BubbleAnimationViewModel* model) { | ||||||
|  |     furi_assert(model); | ||||||
|  |     uint8_t icon_index = 0; | ||||||
|  |     const BubbleAnimation* animation = model->current; | ||||||
|  | 
 | ||||||
|  |     if(model->current_frame < animation->passive_frames) { | ||||||
|  |         icon_index = model->current_frame; | ||||||
|  |     } else { | ||||||
|  |         icon_index = | ||||||
|  |             (model->current_frame - animation->passive_frames) % animation->active_frames + | ||||||
|  |             animation->passive_frames; | ||||||
|  |     } | ||||||
|  |     furi_assert(icon_index < (animation->passive_frames + animation->active_frames)); | ||||||
|  | 
 | ||||||
|  |     return icon_index; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_draw_callback(Canvas* canvas, void* model_) { | ||||||
|  |     furi_assert(model_); | ||||||
|  |     furi_assert(canvas); | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = model_; | ||||||
|  |     const BubbleAnimation* animation = model->current; | ||||||
|  | 
 | ||||||
|  |     if(model->freeze_frame) { | ||||||
|  |         uint8_t y_offset = canvas_height(canvas) - icon_get_height(model->freeze_frame); | ||||||
|  |         canvas_draw_icon(canvas, 0, y_offset, model->freeze_frame); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(!animation) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_assert(model->current_frame < 255); | ||||||
|  | 
 | ||||||
|  |     const Icon* icon = animation->icons[bubble_animation_get_icon_index(model)]; | ||||||
|  |     furi_assert(icon); | ||||||
|  |     uint8_t y_offset = canvas_height(canvas) - icon_get_height(icon); | ||||||
|  |     canvas_draw_icon(canvas, 0, y_offset, icon); | ||||||
|  | 
 | ||||||
|  |     const FrameBubble* bubble = model->current_bubble; | ||||||
|  |     if(bubble) { | ||||||
|  |         if((model->current_frame >= bubble->starts_at_frame) && | ||||||
|  |            (model->current_frame <= bubble->ends_at_frame)) { | ||||||
|  |             const Bubble* b = &bubble->bubble; | ||||||
|  |             elements_bubble_str(canvas, b->x, b->y, b->str, b->horizontal, b->vertical); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static FrameBubble* bubble_animation_pick_bubble(BubbleAnimationViewModel* model, bool active) { | ||||||
|  |     FrameBubble* bubble = NULL; | ||||||
|  | 
 | ||||||
|  |     if((model->active_bubbles == 0) && (model->passive_bubbles == 0)) { | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     uint8_t index = random() % (active ? model->active_bubbles : model->passive_bubbles); | ||||||
|  |     const BubbleAnimation* animation = model->current; | ||||||
|  | 
 | ||||||
|  |     for(int i = 0; i < animation->frame_bubbles_count; ++i) { | ||||||
|  |         if((animation->frame_bubbles[i]->starts_at_frame < animation->passive_frames) ^ active) { | ||||||
|  |             if(!index) { | ||||||
|  |                 bubble = animation->frame_bubbles[i]; | ||||||
|  |             } | ||||||
|  |             --index; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return bubble; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool bubble_animation_input_callback(InputEvent* event, void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     furi_assert(event); | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationView* animation_view = context; | ||||||
|  |     bool consumed = false; | ||||||
|  | 
 | ||||||
|  |     if(event->type == InputTypePress) { | ||||||
|  |         bubble_animation_activate(animation_view, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(event->key == InputKeyRight) { | ||||||
|  |         /* Right button reserved for animation activation, so consume */ | ||||||
|  |         consumed = true; | ||||||
|  |         if(event->type == InputTypeShort) { | ||||||
|  |             if(animation_view->interact_callback) { | ||||||
|  |                 animation_view->interact_callback(animation_view->interact_callback_context); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else if(event->key == InputKeyBack) { | ||||||
|  |         /* Prevent back button to fall down to common handler - leaving
 | ||||||
|  |          * application, so consume */ | ||||||
|  |         consumed = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return consumed; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_activate(BubbleAnimationView* view, bool force) { | ||||||
|  |     furi_assert(view); | ||||||
|  |     bool activate = true; | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     if(!model->current) { | ||||||
|  |         activate = false; | ||||||
|  |     } else if(model->freeze_frame) { | ||||||
|  |         activate = false; | ||||||
|  |     } else if(model->current->active_frames == 0) { | ||||||
|  |         activate = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(!force) { | ||||||
|  |         if((model->active_ended_at + model->current->active_cooldown * 1000) > | ||||||
|  |            xTaskGetTickCount()) { | ||||||
|  |             activate = false; | ||||||
|  |         } else if(model->active_shift) { | ||||||
|  |             activate = false; | ||||||
|  |         } else if(model->current_frame >= model->current->passive_frames) { | ||||||
|  |             activate = false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     view_commit_model(view->view, false); | ||||||
|  | 
 | ||||||
|  |     if(!activate && !force) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(ACTIVE_SHIFT > 0) { | ||||||
|  |         BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |         model->active_shift = ACTIVE_SHIFT; | ||||||
|  |         view_commit_model(view->view, false); | ||||||
|  |     } else { | ||||||
|  |         bubble_animation_activate_right_now(view); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_activate_right_now(BubbleAnimationView* view) { | ||||||
|  |     furi_assert(view); | ||||||
|  | 
 | ||||||
|  |     uint8_t frame_rate = 0; | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     if(model->current && (model->current->active_frames > 0) && (!model->freeze_frame)) { | ||||||
|  |         model->current_frame = model->current->passive_frames; | ||||||
|  |         model->current_bubble = bubble_animation_pick_bubble(model, true); | ||||||
|  |         frame_rate = model->current->frame_rate; | ||||||
|  |     } | ||||||
|  |     view_commit_model(view->view, true); | ||||||
|  | 
 | ||||||
|  |     if(frame_rate) { | ||||||
|  |         osTimerStart(view->timer, 1000 / frame_rate); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_next_frame(BubbleAnimationViewModel* model) { | ||||||
|  |     furi_assert(model); | ||||||
|  | 
 | ||||||
|  |     if(!model->current) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(model->current_frame < model->current->passive_frames) { | ||||||
|  |         model->current_frame = (model->current_frame + 1) % model->current->passive_frames; | ||||||
|  |     } else { | ||||||
|  |         ++model->current_frame; | ||||||
|  |         model->active_cycle += | ||||||
|  |             !((model->current_frame - model->current->passive_frames) % | ||||||
|  |               model->current->active_frames); | ||||||
|  |         if(model->active_cycle >= model->current->active_cycles) { | ||||||
|  |             // switch to passive
 | ||||||
|  |             model->active_cycle = 0; | ||||||
|  |             model->current_frame = 0; | ||||||
|  |             model->current_bubble = bubble_animation_pick_bubble(model, false); | ||||||
|  |             model->active_ended_at = xTaskGetTickCount(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(model->current_bubble) { | ||||||
|  |             if(model->current_frame > model->current_bubble->ends_at_frame) { | ||||||
|  |                 model->current_bubble = model->current_bubble->next_bubble; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_timer_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     BubbleAnimationView* view = context; | ||||||
|  |     bool activate = false; | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  | 
 | ||||||
|  |     if(model->active_shift > 0) { | ||||||
|  |         activate = (--model->active_shift == 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(!model->freeze_frame && !activate) { | ||||||
|  |         bubble_animation_next_frame(model); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(view->view, !activate); | ||||||
|  | 
 | ||||||
|  |     if(activate) { | ||||||
|  |         bubble_animation_activate_right_now(view); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static Icon* bubble_animation_clone_frame(const Icon* icon_orig) { | ||||||
|  |     furi_assert(icon_orig); | ||||||
|  |     furi_assert(icon_orig->frames); | ||||||
|  |     furi_assert(icon_orig->frames[0]); | ||||||
|  | 
 | ||||||
|  |     Icon* icon_clone = furi_alloc(sizeof(Icon)); | ||||||
|  |     memcpy(icon_clone, icon_orig, sizeof(Icon)); | ||||||
|  | 
 | ||||||
|  |     icon_clone->frames = furi_alloc(sizeof(uint8_t*)); | ||||||
|  |     /* icon bitmap can be either compressed or not. It is compressed if
 | ||||||
|  |      * compressed size is less than original, so max size for bitmap is | ||||||
|  |      * uncompressed (width * height) + 1 byte (in uncompressed case) | ||||||
|  |      * for compressed header | ||||||
|  |      */ | ||||||
|  |     size_t max_bitmap_size = ROUND_UP_TO(icon_orig->width, 8) * icon_orig->height + 1; | ||||||
|  |     icon_clone->frames[0] = furi_alloc(max_bitmap_size); | ||||||
|  |     memcpy((void*)icon_clone->frames[0], icon_orig->frames[0], max_bitmap_size); | ||||||
|  | 
 | ||||||
|  |     return icon_clone; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_release_frame(Icon** icon) { | ||||||
|  |     furi_assert(icon); | ||||||
|  |     furi_assert(*icon); | ||||||
|  | 
 | ||||||
|  |     free((void*)(*icon)->frames[0]); | ||||||
|  |     free((*icon)->frames); | ||||||
|  |     free(*icon); | ||||||
|  |     *icon = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_enter(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     BubbleAnimationView* view = context; | ||||||
|  |     bubble_animation_activate(view, false); | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     uint8_t frame_rate = model->current->frame_rate; | ||||||
|  |     view_commit_model(view->view, false); | ||||||
|  | 
 | ||||||
|  |     if(frame_rate) { | ||||||
|  |         osTimerStart(view->timer, 1000 / frame_rate); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void bubble_animation_exit(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     BubbleAnimationView* view = context; | ||||||
|  |     osTimerStop(view->timer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | BubbleAnimationView* bubble_animation_view_alloc(void) { | ||||||
|  |     BubbleAnimationView* view = furi_alloc(sizeof(BubbleAnimationView)); | ||||||
|  |     view->view = view_alloc(); | ||||||
|  |     view->interact_callback = NULL; | ||||||
|  |     view->timer = osTimerNew(bubble_animation_timer_callback, osTimerPeriodic, view, NULL); | ||||||
|  | 
 | ||||||
|  |     view_allocate_model(view->view, ViewModelTypeLocking, sizeof(BubbleAnimationViewModel)); | ||||||
|  |     view_set_context(view->view, view); | ||||||
|  |     view_set_draw_callback(view->view, bubble_animation_draw_callback); | ||||||
|  |     view_set_input_callback(view->view, bubble_animation_input_callback); | ||||||
|  |     view_set_enter_callback(view->view, bubble_animation_enter); | ||||||
|  |     view_set_exit_callback(view->view, bubble_animation_exit); | ||||||
|  | 
 | ||||||
|  |     return view; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void bubble_animation_view_free(BubbleAnimationView* view) { | ||||||
|  |     furi_assert(view); | ||||||
|  | 
 | ||||||
|  |     view_set_draw_callback(view->view, NULL); | ||||||
|  |     view_set_input_callback(view->view, NULL); | ||||||
|  |     view_set_context(view->view, NULL); | ||||||
|  | 
 | ||||||
|  |     view_free(view->view); | ||||||
|  |     view->view = NULL; | ||||||
|  |     free(view); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void bubble_animation_view_set_interact_callback( | ||||||
|  |     BubbleAnimationView* view, | ||||||
|  |     BubbleAnimationInteractCallback callback, | ||||||
|  |     void* context) { | ||||||
|  |     furi_assert(view); | ||||||
|  | 
 | ||||||
|  |     view->interact_callback_context = context; | ||||||
|  |     view->interact_callback = callback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void bubble_animation_view_set_animation( | ||||||
|  |     BubbleAnimationView* view, | ||||||
|  |     const BubbleAnimation* new_animation) { | ||||||
|  |     furi_assert(view); | ||||||
|  |     furi_assert(new_animation); | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     furi_assert(model); | ||||||
|  |     model->current = new_animation; | ||||||
|  | 
 | ||||||
|  |     model->active_ended_at = xTaskGetTickCount() - (model->current->active_cooldown * 1000); | ||||||
|  |     model->active_bubbles = 0; | ||||||
|  |     model->passive_bubbles = 0; | ||||||
|  |     for(int i = 0; i < new_animation->frame_bubbles_count; ++i) { | ||||||
|  |         if(new_animation->frame_bubbles[i]->starts_at_frame < new_animation->passive_frames) { | ||||||
|  |             ++model->passive_bubbles; | ||||||
|  |         } else { | ||||||
|  |             ++model->active_bubbles; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* select bubble sequence */ | ||||||
|  |     model->current_bubble = bubble_animation_pick_bubble(model, false); | ||||||
|  |     model->current_frame = 0; | ||||||
|  |     model->active_cycle = 0; | ||||||
|  |     view_commit_model(view->view, true); | ||||||
|  | 
 | ||||||
|  |     osTimerStart(view->timer, 1000 / new_animation->frame_rate); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void bubble_animation_freeze(BubbleAnimationView* view) { | ||||||
|  |     furi_assert(view); | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     furi_assert(model->current); | ||||||
|  |     furi_assert(!model->freeze_frame); | ||||||
|  |     /* always freeze first passive frame, because
 | ||||||
|  |      * animation is always activated at unfreezing and played | ||||||
|  |      * passive frame first, and 2 frames after - active | ||||||
|  |      */ | ||||||
|  |     uint8_t icon_index = 0; | ||||||
|  |     model->freeze_frame = bubble_animation_clone_frame(model->current->icons[icon_index]); | ||||||
|  |     model->current = NULL; | ||||||
|  |     view_commit_model(view->view, false); | ||||||
|  |     osTimerStop(view->timer); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void bubble_animation_unfreeze(BubbleAnimationView* view) { | ||||||
|  |     furi_assert(view); | ||||||
|  |     uint8_t frame_rate; | ||||||
|  | 
 | ||||||
|  |     BubbleAnimationViewModel* model = view_get_model(view->view); | ||||||
|  |     furi_assert(model->freeze_frame); | ||||||
|  |     bubble_animation_release_frame(&model->freeze_frame); | ||||||
|  |     furi_assert(model->current); | ||||||
|  |     furi_assert(model->current->icons); | ||||||
|  |     frame_rate = model->current->frame_rate; | ||||||
|  |     view_commit_model(view->view, true); | ||||||
|  | 
 | ||||||
|  |     osTimerStart(view->timer, 1000 / frame_rate); | ||||||
|  |     bubble_animation_activate(view, false); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | View* bubble_animation_get_view(BubbleAnimationView* view) { | ||||||
|  |     furi_assert(view); | ||||||
|  | 
 | ||||||
|  |     return view->view; | ||||||
|  | } | ||||||
| @ -0,0 +1,89 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <gui/view.h> | ||||||
|  | #include "../animation_manager.h" | ||||||
|  | 
 | ||||||
|  | /** Bubble Animation instance */ | ||||||
|  | typedef struct BubbleAnimationView BubbleAnimationView; | ||||||
|  | 
 | ||||||
|  | /** Callback type to be called when interact button pressed */ | ||||||
|  | typedef void (*BubbleAnimationInteractCallback)(void*); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Allocate bubble animation view. | ||||||
|  |  * This is animation with bubbles, and 2 phases: | ||||||
|  |  * active and passive. | ||||||
|  |  * | ||||||
|  |  * @return instance of new bubble animation | ||||||
|  |  */ | ||||||
|  | BubbleAnimationView* bubble_animation_view_alloc(void); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Free bubble animation view. | ||||||
|  |  * | ||||||
|  |  * @view        bubble animation view instance | ||||||
|  |  */ | ||||||
|  | void bubble_animation_view_free(BubbleAnimationView* view); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set callback for interact action for animation. | ||||||
|  |  * Currently this is right button. | ||||||
|  |  * | ||||||
|  |  * @view        bubble animation view instance | ||||||
|  |  * @callback    callback to call when button pressed | ||||||
|  |  * @context     context | ||||||
|  |  */ | ||||||
|  | void bubble_animation_view_set_interact_callback( | ||||||
|  |     BubbleAnimationView* view, | ||||||
|  |     BubbleAnimationInteractCallback callback, | ||||||
|  |     void* context); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Set new animation. | ||||||
|  |  * BubbleAnimation doesn't posses Bubble Animation object | ||||||
|  |  * so it doesn't handle any memory manipulation on Bubble Animations. | ||||||
|  |  * | ||||||
|  |  * @view                    bubble animation view instance | ||||||
|  |  * @new_bubble_animation    new animation to set | ||||||
|  |  */ | ||||||
|  | void bubble_animation_view_set_animation( | ||||||
|  |     BubbleAnimationView* view, | ||||||
|  |     const BubbleAnimation* new_bubble_animation); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Get view of bubble animation. | ||||||
|  |  * | ||||||
|  |  * @view        bubble animation view instance | ||||||
|  |  * @return      view | ||||||
|  |  */ | ||||||
|  | View* bubble_animation_get_view(BubbleAnimationView* view); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Freeze current playing animation. Saves a frame to be shown | ||||||
|  |  * during next unfreeze called. | ||||||
|  |  * bubble_animation_freeze() stops any reference to 'current' animation | ||||||
|  |  * so it can be freed. Therefore lock unfreeze should be preceeded with | ||||||
|  |  * new animation set. | ||||||
|  |  * | ||||||
|  |  * Freeze/Unfreeze usage example: | ||||||
|  |  * | ||||||
|  |  *  animation_view_alloc() | ||||||
|  |  *  set_animation() | ||||||
|  |  *  ... | ||||||
|  |  *  freeze_animation() | ||||||
|  |  *   // release animation
 | ||||||
|  |  *  ... | ||||||
|  |  *   // allocate animation
 | ||||||
|  |  *  set_animation() | ||||||
|  |  *  unfreeze() | ||||||
|  |  * | ||||||
|  |  * @view        bubble animation view instance | ||||||
|  |  */ | ||||||
|  | void bubble_animation_freeze(BubbleAnimationView* view); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Starts bubble animation after freezing. | ||||||
|  |  * | ||||||
|  |  * @view        bubble animation view instance | ||||||
|  |  */ | ||||||
|  | void bubble_animation_unfreeze(BubbleAnimationView* view); | ||||||
| @ -2,6 +2,7 @@ | |||||||
| #include "cmsis_os2.h" | #include "cmsis_os2.h" | ||||||
| #include "desktop/desktop.h" | #include "desktop/desktop.h" | ||||||
| #include "desktop_i.h" | #include "desktop_i.h" | ||||||
|  | #include "gui/view_composed.h" | ||||||
| #include <dolphin/dolphin.h> | #include <dolphin/dolphin.h> | ||||||
| #include <furi/pubsub.h> | #include <furi/pubsub.h> | ||||||
| #include <furi/record.h> | #include <furi/record.h> | ||||||
| @ -10,7 +11,7 @@ | |||||||
| #include "storage/storage.h" | #include "storage/storage.h" | ||||||
| #include <stdint.h> | #include <stdint.h> | ||||||
| #include <power/power_service/power.h> | #include <power/power_service/power.h> | ||||||
| #include "helpers/desktop_animation.h" | #include "animations/animation_manager.h" | ||||||
| 
 | 
 | ||||||
| static void desktop_lock_icon_callback(Canvas* canvas, void* context) { | static void desktop_lock_icon_callback(Canvas* canvas, void* context) { | ||||||
|     furi_assert(canvas); |     furi_assert(canvas); | ||||||
| @ -32,11 +33,12 @@ bool desktop_back_event_callback(void* context) { | |||||||
| Desktop* desktop_alloc() { | Desktop* desktop_alloc() { | ||||||
|     Desktop* desktop = furi_alloc(sizeof(Desktop)); |     Desktop* desktop = furi_alloc(sizeof(Desktop)); | ||||||
| 
 | 
 | ||||||
|  |     desktop->unload_animation_semaphore = osSemaphoreNew(1, 0, NULL); | ||||||
|  |     desktop->animation_manager = animation_manager_alloc(); | ||||||
|     desktop->gui = furi_record_open("gui"); |     desktop->gui = furi_record_open("gui"); | ||||||
|     desktop->scene_thread = furi_thread_alloc(); |     desktop->scene_thread = furi_thread_alloc(); | ||||||
|     desktop->view_dispatcher = view_dispatcher_alloc(); |     desktop->view_dispatcher = view_dispatcher_alloc(); | ||||||
|     desktop->scene_manager = scene_manager_alloc(&desktop_scene_handlers, desktop); |     desktop->scene_manager = scene_manager_alloc(&desktop_scene_handlers, desktop); | ||||||
|     desktop->animation = desktop_animation_alloc(); |  | ||||||
| 
 | 
 | ||||||
|     view_dispatcher_enable_queue(desktop->view_dispatcher); |     view_dispatcher_enable_queue(desktop->view_dispatcher); | ||||||
|     view_dispatcher_attach_to_gui( |     view_dispatcher_attach_to_gui( | ||||||
| @ -48,16 +50,34 @@ Desktop* desktop_alloc() { | |||||||
|     view_dispatcher_set_navigation_event_callback( |     view_dispatcher_set_navigation_event_callback( | ||||||
|         desktop->view_dispatcher, desktop_back_event_callback); |         desktop->view_dispatcher, desktop_back_event_callback); | ||||||
| 
 | 
 | ||||||
|  |     desktop->dolphin_view = animation_manager_get_animation_view(desktop->animation_manager); | ||||||
|  | 
 | ||||||
|  |     desktop->main_view_composed = view_composed_alloc(); | ||||||
|     desktop->main_view = desktop_main_alloc(); |     desktop->main_view = desktop_main_alloc(); | ||||||
|     desktop->lock_menu = desktop_lock_menu_alloc(); |     view_composed_tie_views( | ||||||
|  |         desktop->main_view_composed, | ||||||
|  |         desktop->dolphin_view, | ||||||
|  |         desktop_main_get_view(desktop->main_view)); | ||||||
|  |     view_composed_top_enable(desktop->main_view_composed, true); | ||||||
|  | 
 | ||||||
|  |     desktop->locked_view_composed = view_composed_alloc(); | ||||||
|     desktop->locked_view = desktop_locked_alloc(); |     desktop->locked_view = desktop_locked_alloc(); | ||||||
|  |     view_composed_tie_views( | ||||||
|  |         desktop->locked_view_composed, | ||||||
|  |         desktop->dolphin_view, | ||||||
|  |         desktop_locked_get_view(desktop->locked_view)); | ||||||
|  |     view_composed_top_enable(desktop->locked_view_composed, true); | ||||||
|  | 
 | ||||||
|  |     desktop->lock_menu = desktop_lock_menu_alloc(); | ||||||
|     desktop->debug_view = desktop_debug_alloc(); |     desktop->debug_view = desktop_debug_alloc(); | ||||||
|     desktop->first_start_view = desktop_first_start_alloc(); |     desktop->first_start_view = desktop_first_start_alloc(); | ||||||
|     desktop->hw_mismatch_popup = popup_alloc(); |     desktop->hw_mismatch_popup = popup_alloc(); | ||||||
|     desktop->code_input = code_input_alloc(); |     desktop->code_input = code_input_alloc(); | ||||||
| 
 | 
 | ||||||
|     view_dispatcher_add_view( |     view_dispatcher_add_view( | ||||||
|         desktop->view_dispatcher, DesktopViewMain, desktop_main_get_view(desktop->main_view)); |         desktop->view_dispatcher, | ||||||
|  |         DesktopViewMain, | ||||||
|  |         view_composed_get_view(desktop->main_view_composed)); | ||||||
|     view_dispatcher_add_view( |     view_dispatcher_add_view( | ||||||
|         desktop->view_dispatcher, |         desktop->view_dispatcher, | ||||||
|         DesktopViewLockMenu, |         DesktopViewLockMenu, | ||||||
| @ -67,7 +87,7 @@ Desktop* desktop_alloc() { | |||||||
|     view_dispatcher_add_view( |     view_dispatcher_add_view( | ||||||
|         desktop->view_dispatcher, |         desktop->view_dispatcher, | ||||||
|         DesktopViewLocked, |         DesktopViewLocked, | ||||||
|         desktop_locked_get_view(desktop->locked_view)); |         view_composed_get_view(desktop->locked_view_composed)); | ||||||
|     view_dispatcher_add_view( |     view_dispatcher_add_view( | ||||||
|         desktop->view_dispatcher, |         desktop->view_dispatcher, | ||||||
|         DesktopViewFirstStart, |         DesktopViewFirstStart, | ||||||
| @ -91,7 +111,6 @@ Desktop* desktop_alloc() { | |||||||
| void desktop_free(Desktop* desktop) { | void desktop_free(Desktop* desktop) { | ||||||
|     furi_assert(desktop); |     furi_assert(desktop); | ||||||
| 
 | 
 | ||||||
|     desktop_animation_free(desktop->animation); |  | ||||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewMain); |     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewMain); | ||||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLockMenu); |     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLockMenu); | ||||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLocked); |     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLocked); | ||||||
| @ -103,6 +122,9 @@ void desktop_free(Desktop* desktop) { | |||||||
|     view_dispatcher_free(desktop->view_dispatcher); |     view_dispatcher_free(desktop->view_dispatcher); | ||||||
|     scene_manager_free(desktop->scene_manager); |     scene_manager_free(desktop->scene_manager); | ||||||
| 
 | 
 | ||||||
|  |     animation_manager_free(desktop->animation_manager); | ||||||
|  |     view_composed_free(desktop->main_view_composed); | ||||||
|  |     view_composed_free(desktop->locked_view_composed); | ||||||
|     desktop_main_free(desktop->main_view); |     desktop_main_free(desktop->main_view); | ||||||
|     desktop_lock_menu_free(desktop->lock_menu); |     desktop_lock_menu_free(desktop->lock_menu); | ||||||
|     desktop_locked_free(desktop->locked_view); |     desktop_locked_free(desktop->locked_view); | ||||||
| @ -111,6 +133,8 @@ void desktop_free(Desktop* desktop) { | |||||||
|     popup_free(desktop->hw_mismatch_popup); |     popup_free(desktop->hw_mismatch_popup); | ||||||
|     code_input_free(desktop->code_input); |     code_input_free(desktop->code_input); | ||||||
| 
 | 
 | ||||||
|  |     osSemaphoreDelete(desktop->unload_animation_semaphore); | ||||||
|  | 
 | ||||||
|     furi_record_close("gui"); |     furi_record_close("gui"); | ||||||
|     desktop->gui = NULL; |     desktop->gui = NULL; | ||||||
| 
 | 
 | ||||||
| @ -129,29 +153,9 @@ static bool desktop_is_first_start() { | |||||||
|     return exists; |     return exists; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void desktop_dolphin_state_changed_callback(const void* message, void* context) { |  | ||||||
|     Desktop* desktop = context; |  | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventUpdateAnimation); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void desktop_storage_state_changed_callback(const void* message, void* context) { |  | ||||||
|     Desktop* desktop = context; |  | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventUpdateAnimation); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| int32_t desktop_srv(void* p) { | int32_t desktop_srv(void* p) { | ||||||
|     Desktop* desktop = desktop_alloc(); |     Desktop* desktop = desktop_alloc(); | ||||||
| 
 | 
 | ||||||
|     Dolphin* dolphin = furi_record_open("dolphin"); |  | ||||||
|     FuriPubSub* dolphin_pubsub = dolphin_get_pubsub(dolphin); |  | ||||||
|     FuriPubSubSubscription* dolphin_subscription = |  | ||||||
|         furi_pubsub_subscribe(dolphin_pubsub, desktop_dolphin_state_changed_callback, desktop); |  | ||||||
| 
 |  | ||||||
|     Storage* storage = furi_record_open("storage"); |  | ||||||
|     FuriPubSub* storage_pubsub = storage_get_pubsub(storage); |  | ||||||
|     FuriPubSubSubscription* storage_subscription = |  | ||||||
|         furi_pubsub_subscribe(storage_pubsub, desktop_storage_state_changed_callback, desktop); |  | ||||||
| 
 |  | ||||||
|     bool loaded = LOAD_DESKTOP_SETTINGS(&desktop->settings); |     bool loaded = LOAD_DESKTOP_SETTINGS(&desktop->settings); | ||||||
|     if(!loaded) { |     if(!loaded) { | ||||||
|         furi_hal_rtc_reset_flag(FuriHalRtcFlagLock); |         furi_hal_rtc_reset_flag(FuriHalRtcFlagLock); | ||||||
| @ -181,8 +185,6 @@ int32_t desktop_srv(void* p) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     view_dispatcher_run(desktop->view_dispatcher); |     view_dispatcher_run(desktop->view_dispatcher); | ||||||
|     furi_pubsub_unsubscribe(dolphin_pubsub, dolphin_subscription); |  | ||||||
|     furi_pubsub_unsubscribe(storage_pubsub, storage_subscription); |  | ||||||
|     desktop_free(desktop); |     desktop_free(desktop); | ||||||
| 
 | 
 | ||||||
|     return 0; |     return 0; | ||||||
|  | |||||||
| @ -1,7 +1,10 @@ | |||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include "cmsis_os2.h" | ||||||
| #include "desktop.h" | #include "desktop.h" | ||||||
| 
 | 
 | ||||||
|  | #include "animations/animation_manager.h" | ||||||
|  | #include "gui/view_composed.h" | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| #include <furi-hal.h> | #include <furi-hal.h> | ||||||
| 
 | 
 | ||||||
| @ -21,7 +24,6 @@ | |||||||
| #include "views/desktop_debug.h" | #include "views/desktop_debug.h" | ||||||
| 
 | 
 | ||||||
| #include "scenes/desktop_scene.h" | #include "scenes/desktop_scene.h" | ||||||
| #include "helpers/desktop_animation.h" |  | ||||||
| #include "desktop/desktop_settings/desktop_settings.h" | #include "desktop/desktop_settings/desktop_settings.h" | ||||||
| #include <gui/icon.h> | #include <gui/icon.h> | ||||||
| 
 | 
 | ||||||
| @ -46,19 +48,29 @@ struct Desktop { | |||||||
|     ViewDispatcher* view_dispatcher; |     ViewDispatcher* view_dispatcher; | ||||||
|     SceneManager* scene_manager; |     SceneManager* scene_manager; | ||||||
| 
 | 
 | ||||||
|     DesktopAnimation* animation; |  | ||||||
|     DesktopFirstStartView* first_start_view; |     DesktopFirstStartView* first_start_view; | ||||||
|     Popup* hw_mismatch_popup; |     Popup* hw_mismatch_popup; | ||||||
|     DesktopMainView* main_view; |  | ||||||
|     DesktopLockMenuView* lock_menu; |     DesktopLockMenuView* lock_menu; | ||||||
|     DesktopLockedView* locked_view; |  | ||||||
|     DesktopDebugView* debug_view; |     DesktopDebugView* debug_view; | ||||||
|     CodeInput* code_input; |     CodeInput* code_input; | ||||||
| 
 | 
 | ||||||
|  |     View* dolphin_view; | ||||||
|  |     DesktopMainView* main_view; | ||||||
|  |     DesktopLockedView* locked_view; | ||||||
|  | 
 | ||||||
|  |     ViewComposed* main_view_composed; | ||||||
|  |     ViewComposed* locked_view_composed; | ||||||
|  | 
 | ||||||
|     DesktopSettings settings; |     DesktopSettings settings; | ||||||
|     PinCode pincode_buffer; |     PinCode pincode_buffer; | ||||||
| 
 | 
 | ||||||
|     ViewPort* lock_viewport; |     ViewPort* lock_viewport; | ||||||
|  | 
 | ||||||
|  |     AnimationManager* animation_manager; | ||||||
|  |     osSemaphoreId_t unload_animation_semaphore; | ||||||
|  |     FuriPubSubSubscription* app_start_stop_subscription; | ||||||
|  | 
 | ||||||
|  |     char* text_buffer; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Desktop* desktop_alloc(); | Desktop* desktop_alloc(); | ||||||
|  | |||||||
| @ -1,382 +0,0 @@ | |||||||
| #include "desktop/helpers/desktop_animation.h" |  | ||||||
| #include "assets_icons.h" |  | ||||||
| #include "desktop_animation_i.h" |  | ||||||
| #include "cmsis_os2.h" |  | ||||||
| #include "furi/common_defines.h" |  | ||||||
| #include "furi/record.h" |  | ||||||
| #include "storage/filesystem-api-defines.h" |  | ||||||
| #include <power/power_service/power.h> |  | ||||||
| #include <m-list.h> |  | ||||||
| #include <storage/storage.h> |  | ||||||
| #include <desktop/desktop.h> |  | ||||||
| #include <dolphin/dolphin.h> |  | ||||||
| 
 |  | ||||||
| #define KEEP_ONLY_CALM_BASIC_ANIMATIONS 1 |  | ||||||
| 
 |  | ||||||
| LIST_DEF(AnimationList, const PairedAnimation*, M_PTR_OPLIST) |  | ||||||
| #define M_OPL_AnimationList_t() LIST_OPLIST(AnimationList) |  | ||||||
| 
 |  | ||||||
| #define PUSH_BACK_ANIMATIONS(listname, animations, butthurt)                          \ |  | ||||||
|     for(int i = 0; i < COUNT_OF(animations); ++i) {                                   \ |  | ||||||
|         if(!(animations)[i].basic->butthurt_level_mask ||                             \ |  | ||||||
|            ((animations)[i].basic->butthurt_level_mask & BUTTHURT_LEVEL(butthurt))) { \ |  | ||||||
|             AnimationList_push_back(animation_list, &(animations)[i]);                \ |  | ||||||
|         }                                                                             \ |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| #define IS_BLOCKING_ANIMATION(x) \ |  | ||||||
|     (((x) != DesktopAnimationStateBasic) && ((x) != DesktopAnimationStateActive)) |  | ||||||
| #define IS_ONESHOT_ANIMATION(x) ((x) == DesktopAnimationStateLevelUpIsPending) |  | ||||||
| 
 |  | ||||||
| static void desktop_animation_timer_callback(void* context); |  | ||||||
| 
 |  | ||||||
| struct DesktopAnimation { |  | ||||||
|     bool sd_shown_error_db; |  | ||||||
|     bool sd_shown_error_card_bad; |  | ||||||
|     osTimerId_t timer; |  | ||||||
|     const PairedAnimation* current; |  | ||||||
|     const Icon* current_blocking_icon; |  | ||||||
|     const Icon** current_one_shot_icons; |  | ||||||
|     uint8_t one_shot_animation_counter; |  | ||||||
|     uint8_t one_shot_animation_size; |  | ||||||
|     DesktopAnimationState state; |  | ||||||
|     TickType_t basic_started_at; |  | ||||||
|     TickType_t active_finished_at; |  | ||||||
|     AnimationChangedCallback animation_changed_callback; |  | ||||||
|     void* animation_changed_callback_context; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| DesktopAnimation* desktop_animation_alloc(void) { |  | ||||||
|     DesktopAnimation* animation = furi_alloc(sizeof(DesktopAnimation)); |  | ||||||
| 
 |  | ||||||
|     animation->timer = osTimerNew( |  | ||||||
|         desktop_animation_timer_callback, osTimerPeriodic /* osTimerOnce */, animation, NULL); |  | ||||||
|     animation->active_finished_at = (TickType_t)(-30); |  | ||||||
|     animation->basic_started_at = 0; |  | ||||||
|     animation->animation_changed_callback = NULL; |  | ||||||
|     animation->animation_changed_callback_context = NULL; |  | ||||||
|     desktop_start_new_idle_animation(animation); |  | ||||||
| 
 |  | ||||||
|     return animation; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_animation_free(DesktopAnimation* animation) { |  | ||||||
|     furi_assert(animation); |  | ||||||
| 
 |  | ||||||
|     osTimerDelete(animation->timer); |  | ||||||
|     free(animation); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_animation_set_animation_changed_callback( |  | ||||||
|     DesktopAnimation* animation, |  | ||||||
|     AnimationChangedCallback callback, |  | ||||||
|     void* context) { |  | ||||||
|     furi_assert(animation); |  | ||||||
| 
 |  | ||||||
|     animation->animation_changed_callback = callback; |  | ||||||
|     animation->animation_changed_callback_context = context; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_start_new_idle_animation(DesktopAnimation* animation) { |  | ||||||
|     Dolphin* dolphin = furi_record_open("dolphin"); |  | ||||||
|     DolphinStats stats = dolphin_stats(dolphin); |  | ||||||
|     furi_record_close("dolphin"); |  | ||||||
| 
 |  | ||||||
|     furi_check((stats.level >= 1) && (stats.level <= 3)); |  | ||||||
| 
 |  | ||||||
|     AnimationList_t animation_list; |  | ||||||
|     AnimationList_init(animation_list); |  | ||||||
| 
 |  | ||||||
| #if KEEP_ONLY_CALM_BASIC_ANIMATIONS |  | ||||||
|     PUSH_BACK_ANIMATIONS(animation_list, calm_animation, 0); |  | ||||||
| #else |  | ||||||
|     PUSH_BACK_ANIMATIONS(animation_list, calm_animation, stats.butthurt); |  | ||||||
|     PUSH_BACK_ANIMATIONS(animation_list, mad_animation, stats.butthurt); |  | ||||||
|     switch(stats.level) { |  | ||||||
|     case 1: |  | ||||||
|         PUSH_BACK_ANIMATIONS(animation_list, level_1_animation, stats.butthurt); |  | ||||||
|         break; |  | ||||||
|     case 2: |  | ||||||
|         PUSH_BACK_ANIMATIONS(animation_list, level_2_animation, stats.butthurt); |  | ||||||
|         break; |  | ||||||
|     case 3: |  | ||||||
|         PUSH_BACK_ANIMATIONS(animation_list, level_3_animation, stats.butthurt); |  | ||||||
|         break; |  | ||||||
|     default: |  | ||||||
|         furi_crash("Dolphin level is out of bounds"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Power* power = furi_record_open("power"); |  | ||||||
|     PowerInfo info; |  | ||||||
|     power_get_info(power, &info); |  | ||||||
| 
 |  | ||||||
|     if(!power_is_battery_well(&info)) { |  | ||||||
|         PUSH_BACK_ANIMATIONS(animation_list, check_battery_animation, stats.butthurt); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Storage* storage = furi_record_open("storage"); |  | ||||||
|     FS_Error sd_status = storage_sd_status(storage); |  | ||||||
|     animation->current = NULL; |  | ||||||
| 
 |  | ||||||
|     if(sd_status == FSE_NOT_READY) { |  | ||||||
|         PUSH_BACK_ANIMATIONS(animation_list, no_sd_animation, stats.butthurt); |  | ||||||
|         animation->sd_shown_error_card_bad = false; |  | ||||||
|         animation->sd_shown_error_db = false; |  | ||||||
|     } |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
|     uint32_t whole_weight = 0; |  | ||||||
|     for |  | ||||||
|         M_EACH(item, animation_list, AnimationList_t) { |  | ||||||
|             whole_weight += (*item)->basic->weight; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     uint32_t lucky_number = random() % whole_weight; |  | ||||||
|     uint32_t weight = 0; |  | ||||||
| 
 |  | ||||||
|     const PairedAnimation* selected = NULL; |  | ||||||
|     for |  | ||||||
|         M_EACH(item, animation_list, AnimationList_t) { |  | ||||||
|             if(lucky_number < weight) { |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             weight += (*item)->basic->weight; |  | ||||||
|             selected = *item; |  | ||||||
|         } |  | ||||||
|     animation->basic_started_at = osKernelGetTickCount(); |  | ||||||
|     animation->current = selected; |  | ||||||
|     osTimerStart(animation->timer, animation->current->basic->duration * 1000); |  | ||||||
|     animation->state = DesktopAnimationStateBasic; |  | ||||||
|     furi_assert(selected); |  | ||||||
|     AnimationList_clear(animation_list); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void desktop_animation_timer_callback(void* context) { |  | ||||||
|     furi_assert(context); |  | ||||||
|     DesktopAnimation* animation = context; |  | ||||||
|     TickType_t now_ms = osKernelGetTickCount(); |  | ||||||
|     AnimationList_t animation_list; |  | ||||||
|     AnimationList_init(animation_list); |  | ||||||
|     bool new_basic_animation = false; |  | ||||||
| 
 |  | ||||||
|     if(animation->state == DesktopAnimationStateActive) { |  | ||||||
|         animation->state = DesktopAnimationStateBasic; |  | ||||||
|         TickType_t basic_lasts_ms = now_ms - animation->basic_started_at; |  | ||||||
|         animation->active_finished_at = now_ms; |  | ||||||
|         TickType_t basic_duration_ms = animation->current->basic->duration * 1000; |  | ||||||
|         if(basic_lasts_ms > basic_duration_ms) { |  | ||||||
|             // if active animation finished, and basic duration came to an end
 |  | ||||||
|             // select new idle animation
 |  | ||||||
|             new_basic_animation = true; |  | ||||||
|         } else { |  | ||||||
|             // if active animation finished, but basic duration is not finished
 |  | ||||||
|             // play current animation for the rest of time
 |  | ||||||
|             furi_assert(basic_duration_ms != basic_lasts_ms); |  | ||||||
|             osTimerStart(animation->timer, basic_duration_ms - basic_lasts_ms); |  | ||||||
|         } |  | ||||||
|     } else if(animation->state == DesktopAnimationStateBasic) { |  | ||||||
|         // if basic animation finished
 |  | ||||||
|         // select new idle animation
 |  | ||||||
|         new_basic_animation = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(new_basic_animation) { |  | ||||||
|         animation->basic_started_at = now_ms; |  | ||||||
|         desktop_start_new_idle_animation(animation); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // for oneshot generate events every time
 |  | ||||||
|     if(animation->animation_changed_callback) { |  | ||||||
|         animation->animation_changed_callback(animation->animation_changed_callback_context); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_animation_activate(DesktopAnimation* animation) { |  | ||||||
|     furi_assert(animation); |  | ||||||
| 
 |  | ||||||
|     if(animation->state != DesktopAnimationStateBasic) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(animation->state == DesktopAnimationStateActive) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(!animation->current->active) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     TickType_t now = osKernelGetTickCount(); |  | ||||||
|     TickType_t time_since_last_active = now - animation->active_finished_at; |  | ||||||
| 
 |  | ||||||
|     if(time_since_last_active > (animation->current->basic->active_cooldown * 1000)) { |  | ||||||
|         animation->state = DesktopAnimationStateActive; |  | ||||||
|         furi_assert(animation->current->active->duration > 0); |  | ||||||
|         osTimerStart(animation->timer, animation->current->active->duration * 1000); |  | ||||||
|         if(animation->animation_changed_callback) { |  | ||||||
|             animation->animation_changed_callback(animation->animation_changed_callback_context); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static const Icon* desktop_animation_get_current_idle_animation( |  | ||||||
|     DesktopAnimation* animation, |  | ||||||
|     bool* status_bar_background_black) { |  | ||||||
|     const ActiveAnimation* active = animation->current->active; |  | ||||||
|     const BasicAnimation* basic = animation->current->basic; |  | ||||||
|     if(animation->state == DesktopAnimationStateActive && active->icon) { |  | ||||||
|         *status_bar_background_black = active->black_status_bar; |  | ||||||
|         return active->icon; |  | ||||||
|     } else { |  | ||||||
|         *status_bar_background_black = basic->black_status_bar; |  | ||||||
|         return basic->icon; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Every time somebody starts 'desktop_animation_get_animation()'
 |  | ||||||
| // 1) check if there is a new level
 |  | ||||||
| // 2) check if there is SD card corruption
 |  | ||||||
| // 3) check if the SD card is empty
 |  | ||||||
| // 4) if all false - get idle animation
 |  | ||||||
| 
 |  | ||||||
| const Icon* desktop_animation_get_animation( |  | ||||||
|     DesktopAnimation* animation, |  | ||||||
|     bool* status_bar_background_black) { |  | ||||||
|     Dolphin* dolphin = furi_record_open("dolphin"); |  | ||||||
|     Storage* storage = furi_record_open("storage"); |  | ||||||
|     const Icon* icon = NULL; |  | ||||||
|     furi_assert(animation); |  | ||||||
|     FS_Error sd_status = storage_sd_status(storage); |  | ||||||
| 
 |  | ||||||
|     if(IS_BLOCKING_ANIMATION(animation->state)) { |  | ||||||
|         // don't give new animation till blocked animation
 |  | ||||||
|         // is reseted
 |  | ||||||
|         icon = animation->current_blocking_icon; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(!icon) { |  | ||||||
|         if(sd_status == FSE_INTERNAL) { |  | ||||||
|             osTimerStop(animation->timer); |  | ||||||
|             icon = &A_CardBad_128x51; |  | ||||||
|             animation->current_blocking_icon = icon; |  | ||||||
|             animation->state = DesktopAnimationStateSDCorrupted; |  | ||||||
|             animation->sd_shown_error_card_bad = true; |  | ||||||
|             animation->sd_shown_error_db = false; |  | ||||||
|         } else if(sd_status == FSE_NOT_READY) { |  | ||||||
|             animation->sd_shown_error_card_bad = false; |  | ||||||
|             animation->sd_shown_error_db = false; |  | ||||||
|         } else if(sd_status == FSE_OK) { |  | ||||||
|             bool db_exists = storage_common_stat(storage, "/ext/manifest.txt", NULL) == FSE_OK; |  | ||||||
|             if(db_exists && !animation->sd_shown_error_db) { |  | ||||||
|                 osTimerStop(animation->timer); |  | ||||||
|                 icon = &A_CardNoDB_128x51; |  | ||||||
|                 animation->current_blocking_icon = icon; |  | ||||||
|                 animation->state = DesktopAnimationStateSDEmpty; |  | ||||||
|                 animation->sd_shown_error_db = true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     DolphinStats stats = dolphin_stats(dolphin); |  | ||||||
|     if(!icon && stats.level_up_is_pending) { |  | ||||||
|         osTimerStop(animation->timer); |  | ||||||
|         icon = &A_LevelUpPending_128x51; |  | ||||||
|         animation->current_blocking_icon = icon; |  | ||||||
|         animation->state = DesktopAnimationStateLevelUpIsPending; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(!icon) { |  | ||||||
|         icon = |  | ||||||
|             desktop_animation_get_current_idle_animation(animation, status_bar_background_black); |  | ||||||
|     } else { |  | ||||||
|         status_bar_background_black = false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     furi_record_close("storage"); |  | ||||||
|     furi_record_close("dolphin"); |  | ||||||
| 
 |  | ||||||
|     return icon; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| DesktopAnimationState desktop_animation_handle_right(DesktopAnimation* animation) { |  | ||||||
|     furi_assert(animation); |  | ||||||
| 
 |  | ||||||
|     bool reset_animation = false; |  | ||||||
|     bool update_animation = false; |  | ||||||
| 
 |  | ||||||
|     switch(animation->state) { |  | ||||||
|     case DesktopAnimationStateActive: |  | ||||||
|     case DesktopAnimationStateBasic: |  | ||||||
|         /* nothing */ |  | ||||||
|         break; |  | ||||||
|     case DesktopAnimationStateLevelUpIsPending: |  | ||||||
|         /* do nothing, main scene should change itself */ |  | ||||||
|         break; |  | ||||||
|     case DesktopAnimationStateSDCorrupted: |  | ||||||
|         reset_animation = true; |  | ||||||
|         break; |  | ||||||
|     case DesktopAnimationStateSDEmpty: |  | ||||||
|         animation->state = DesktopAnimationStateSDEmptyURL; |  | ||||||
|         animation->current_blocking_icon = &A_CardNoDBUrl_128x51; |  | ||||||
|         update_animation = true; |  | ||||||
|         break; |  | ||||||
|     case DesktopAnimationStateSDEmptyURL: |  | ||||||
|         reset_animation = true; |  | ||||||
|         break; |  | ||||||
|     default: |  | ||||||
|         furi_crash("Unhandled desktop animation state"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(reset_animation) { |  | ||||||
|         desktop_start_new_idle_animation(animation); |  | ||||||
|         update_animation = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(update_animation) { |  | ||||||
|         if(animation->animation_changed_callback) { |  | ||||||
|             animation->animation_changed_callback(animation->animation_changed_callback_context); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return animation->state; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #define LEVELUP_FRAME_RATE (0.2) |  | ||||||
| 
 |  | ||||||
| void desktop_animation_start_oneshot_levelup(DesktopAnimation* animation) { |  | ||||||
|     animation->one_shot_animation_counter = 0; |  | ||||||
|     animation->state = DesktopAnimationStateLevelUpIsPending; |  | ||||||
| 
 |  | ||||||
|     Dolphin* dolphin = furi_record_open("dolphin"); |  | ||||||
|     DolphinStats stats = dolphin_stats(dolphin); |  | ||||||
|     furi_record_close("dolphin"); |  | ||||||
|     furi_assert(stats.level_up_is_pending); |  | ||||||
|     if(stats.level == 1) { |  | ||||||
|         animation->current_one_shot_icons = animation_level2up; |  | ||||||
|         animation->one_shot_animation_size = COUNT_OF(animation_level2up); |  | ||||||
|     } else if(stats.level == 2) { |  | ||||||
|         animation->current_one_shot_icons = animation_level3up; |  | ||||||
|         animation->one_shot_animation_size = COUNT_OF(animation_level3up); |  | ||||||
|     } else { |  | ||||||
|         furi_crash("Dolphin level is out of bounds"); |  | ||||||
|     } |  | ||||||
|     osTimerStart(animation->timer, LEVELUP_FRAME_RATE * 1000); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const Icon* desktop_animation_get_oneshot_frame(DesktopAnimation* animation) { |  | ||||||
|     furi_assert(IS_ONESHOT_ANIMATION(animation->state)); |  | ||||||
|     furi_assert(animation->one_shot_animation_size > 0); |  | ||||||
|     const Icon* icon = NULL; |  | ||||||
| 
 |  | ||||||
|     if(animation->one_shot_animation_counter < animation->one_shot_animation_size) { |  | ||||||
|         icon = animation->current_one_shot_icons[animation->one_shot_animation_counter]; |  | ||||||
|         ++animation->one_shot_animation_counter; |  | ||||||
|     } else { |  | ||||||
|         animation->state = DesktopAnimationStateBasic; |  | ||||||
|         animation->one_shot_animation_size = 0; |  | ||||||
|         osTimerStop(animation->timer); |  | ||||||
|         icon = NULL; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return icon; |  | ||||||
| } |  | ||||||
| @ -1,59 +0,0 @@ | |||||||
| #pragma once |  | ||||||
| 
 |  | ||||||
| #include <stdbool.h> |  | ||||||
| #include <stdint.h> |  | ||||||
| #include <gui/icon.h> |  | ||||||
| 
 |  | ||||||
| typedef struct DesktopAnimation DesktopAnimation; |  | ||||||
| 
 |  | ||||||
| typedef struct ActiveAnimation ActiveAnimation; |  | ||||||
| typedef struct BasicAnimation BasicAnimation; |  | ||||||
| 
 |  | ||||||
| typedef enum { |  | ||||||
|     DesktopAnimationStateBasic, |  | ||||||
|     DesktopAnimationStateActive, |  | ||||||
|     DesktopAnimationStateLevelUpIsPending, |  | ||||||
|     DesktopAnimationStateSDEmpty, |  | ||||||
|     DesktopAnimationStateSDEmptyURL, |  | ||||||
|     DesktopAnimationStateSDCorrupted, |  | ||||||
| } DesktopAnimationState; |  | ||||||
| 
 |  | ||||||
| struct BasicAnimation { |  | ||||||
|     const Icon* icon; |  | ||||||
|     uint16_t duration; // sec
 |  | ||||||
|     uint16_t active_cooldown; |  | ||||||
|     uint8_t weight; |  | ||||||
|     bool black_status_bar; |  | ||||||
|     uint16_t butthurt_level_mask; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| struct ActiveAnimation { |  | ||||||
|     const Icon* icon; |  | ||||||
|     bool black_status_bar; |  | ||||||
|     uint16_t duration; // sec
 |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| typedef struct { |  | ||||||
|     const BasicAnimation* basic; |  | ||||||
|     const ActiveAnimation* active; |  | ||||||
| } PairedAnimation; |  | ||||||
| 
 |  | ||||||
| typedef void (*AnimationChangedCallback)(void*); |  | ||||||
| 
 |  | ||||||
| DesktopAnimation* desktop_animation_alloc(void); |  | ||||||
| void desktop_animation_free(DesktopAnimation*); |  | ||||||
| void desktop_animation_activate(DesktopAnimation* instance); |  | ||||||
| void desktop_animation_set_animation_changed_callback( |  | ||||||
|     DesktopAnimation* instance, |  | ||||||
|     AnimationChangedCallback callback, |  | ||||||
|     void* context); |  | ||||||
| 
 |  | ||||||
| DesktopAnimationState desktop_animation_handle_right(DesktopAnimation* animation); |  | ||||||
| 
 |  | ||||||
| void desktop_animation_start_oneshot_levelup(DesktopAnimation* animation); |  | ||||||
| 
 |  | ||||||
| const Icon* |  | ||||||
|     desktop_animation_get_animation(DesktopAnimation* animation, bool* status_bar_background_black); |  | ||||||
| const Icon* desktop_animation_get_oneshot_frame(DesktopAnimation* animation); |  | ||||||
| 
 |  | ||||||
| void desktop_start_new_idle_animation(DesktopAnimation* animation); |  | ||||||
| @ -1,332 +0,0 @@ | |||||||
| #include <assets_icons.h> |  | ||||||
| #include <stddef.h> |  | ||||||
| #include <stdint.h> |  | ||||||
| #include <gui/icon.h> |  | ||||||
| #include "desktop_animation.h" |  | ||||||
| 
 |  | ||||||
| // Calm/Mad Basic Idle Animations
 |  | ||||||
| 
 |  | ||||||
| #define COMMON_BASIC_DURATION (2 * 60 * 60) |  | ||||||
| #define COMMON_ACTIVE_CYCLES 1 |  | ||||||
| #define COMMON_ACTIVE_COOLDOWN 15 |  | ||||||
| #define COMMON_WEIGHT 3 |  | ||||||
| 
 |  | ||||||
| #define BUTTHURT_LEVEL(x) (1UL << (x)) |  | ||||||
| #define BUTTHURT_LEVEL_0 0 |  | ||||||
| 
 |  | ||||||
| // frames * cycles / frame_rate
 |  | ||||||
| #define COMMON_ACTIVE_DURATION(x) ((x)*COMMON_ACTIVE_CYCLES / 2) |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_TV = { |  | ||||||
|     .icon = &A_Tv_128x52, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_TV_active = { |  | ||||||
|     .icon = &A_TvActive_128x52, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(6), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_sleep = { |  | ||||||
|     .icon = &A_Sleep_128x52, |  | ||||||
|     .black_status_bar = true, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8) | |  | ||||||
|                            BUTTHURT_LEVEL(9) | BUTTHURT_LEVEL(10)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_sleep_active = { |  | ||||||
|     .icon = &A_SleepActive_128x52, |  | ||||||
|     .black_status_bar = true, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(5), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_leaving = { |  | ||||||
|     .icon = &A_Leaving_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(13) | BUTTHURT_LEVEL(14), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_leaving_active = { |  | ||||||
|     .icon = &A_LeavingActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_laptop = { |  | ||||||
|     .icon = &A_Laptop_128x52, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_laptop_active = { |  | ||||||
|     .icon = &A_LaptopActive_128x52, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(8), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_knife = { |  | ||||||
|     .icon = &A_Knife_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(5) | BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | |  | ||||||
|                            BUTTHURT_LEVEL(8) | BUTTHURT_LEVEL(9) | BUTTHURT_LEVEL(10) | |  | ||||||
|                            BUTTHURT_LEVEL(11) | BUTTHURT_LEVEL(12) | BUTTHURT_LEVEL(13)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_knife_active = { |  | ||||||
|     .icon = &A_KnifeActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_cry = { |  | ||||||
|     .icon = &A_Cry_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8) | |  | ||||||
|                            BUTTHURT_LEVEL(9) | BUTTHURT_LEVEL(10) | BUTTHURT_LEVEL(11) | |  | ||||||
|                            BUTTHURT_LEVEL(12) | BUTTHURT_LEVEL(13)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_cry_active = { |  | ||||||
|     .icon = &A_CryActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(3), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_box = { |  | ||||||
|     .icon = &A_Box_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8) | BUTTHURT_LEVEL(9) | |  | ||||||
|                            BUTTHURT_LEVEL(10) | BUTTHURT_LEVEL(11) | BUTTHURT_LEVEL(12) | |  | ||||||
|                            BUTTHURT_LEVEL(13)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_box_active = { |  | ||||||
|     .icon = &A_BoxActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_waves = { |  | ||||||
|     .icon = &A_Waves_128x52, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_waves_active = { |  | ||||||
|     .icon = &A_WavesActive_128x52, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(7), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // Level Idle Animations
 |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level1furippa = { |  | ||||||
|     .icon = &A_Level1Furippa_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level1furippa_active = { |  | ||||||
|     .icon = &A_Level1FurippaActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(6), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level1read = { |  | ||||||
|     .icon = &A_Level1Read_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level1read_active = { |  | ||||||
|     .icon = &A_Level1ReadActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level1toys = { |  | ||||||
|     .icon = &A_Level1Toys_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level1toys_active = { |  | ||||||
|     .icon = &A_Level1ToysActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level2furippa = { |  | ||||||
|     .icon = &A_Level2Furippa_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level2furippa_active = { |  | ||||||
|     .icon = &A_Level2FurippaActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(6), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level2soldering = { |  | ||||||
|     .icon = &A_Level2Soldering_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8) | |  | ||||||
|                            BUTTHURT_LEVEL(9)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level2soldering_active = { |  | ||||||
|     .icon = &A_Level2SolderingActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level2hack = { |  | ||||||
|     .icon = &A_Level2Hack_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level2hack_active = { |  | ||||||
|     .icon = &A_Level2HackActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level3furippa = { |  | ||||||
|     .icon = &A_Level3Furippa_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level3furippa_active = { |  | ||||||
|     .icon = &A_Level3FurippaActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(6), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level3hijack = { |  | ||||||
|     .icon = &A_Level3Hijack_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8) | |  | ||||||
|                            BUTTHURT_LEVEL(9)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level3hijack_active = { |  | ||||||
|     .icon = &A_Level3HijackActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_level3lab = { |  | ||||||
|     .icon = &A_Level3Lab_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = COMMON_WEIGHT, |  | ||||||
|     .active_cooldown = COMMON_ACTIVE_COOLDOWN, |  | ||||||
|     .butthurt_level_mask = BUTTHURT_LEVEL(0) | BUTTHURT_LEVEL(1) | BUTTHURT_LEVEL(2) | |  | ||||||
|                            BUTTHURT_LEVEL(3) | BUTTHURT_LEVEL(4) | BUTTHURT_LEVEL(5) | |  | ||||||
|                            BUTTHURT_LEVEL(6) | BUTTHURT_LEVEL(7) | BUTTHURT_LEVEL(8)}; |  | ||||||
| 
 |  | ||||||
| static const ActiveAnimation animation_level3lab_active = { |  | ||||||
|     .icon = &A_Level3LabActive_128x51, |  | ||||||
|     .duration = COMMON_ACTIVE_DURATION(2), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // System Idle Animations
 |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_bad_battery = { |  | ||||||
|     .icon = &A_BadBattery_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = 7, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const BasicAnimation animation_no_sd_card = { |  | ||||||
|     .icon = &A_NoSdCard_128x51, |  | ||||||
|     .duration = COMMON_BASIC_DURATION, |  | ||||||
|     .weight = 7, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const Icon* animation_level2up[] = { |  | ||||||
|     &I_LevelUp2_01, |  | ||||||
|     &I_LevelUp2_02, |  | ||||||
|     &I_LevelUp2_03, |  | ||||||
|     &I_LevelUp2_04, |  | ||||||
|     &I_LevelUp2_05, |  | ||||||
|     &I_LevelUp2_06, |  | ||||||
|     &I_LevelUp2_07}; |  | ||||||
| 
 |  | ||||||
| const Icon* animation_level3up[] = { |  | ||||||
|     &I_LevelUp3_01, |  | ||||||
|     &I_LevelUp3_02, |  | ||||||
|     &I_LevelUp3_03, |  | ||||||
|     &I_LevelUp3_04, |  | ||||||
|     &I_LevelUp3_05, |  | ||||||
|     &I_LevelUp3_06, |  | ||||||
|     &I_LevelUp3_07}; |  | ||||||
| 
 |  | ||||||
| // Blocking Idle Animations & One shot Animations represented as naked Icon
 |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation calm_animation[] = { |  | ||||||
|     {.basic = &animation_TV, .active = &animation_TV_active}, |  | ||||||
|     {.basic = &animation_waves, .active = &animation_waves_active}, |  | ||||||
|     {.basic = &animation_sleep, .active = &animation_sleep_active}, |  | ||||||
|     {.basic = &animation_laptop, .active = &animation_laptop_active}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation mad_animation[] = { |  | ||||||
|     {.basic = &animation_cry, .active = &animation_cry_active}, |  | ||||||
|     {.basic = &animation_knife, .active = &animation_knife_active}, |  | ||||||
|     {.basic = &animation_box, .active = &animation_box_active}, |  | ||||||
|     {.basic = &animation_leaving, .active = &animation_leaving_active}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation level_1_animation[] = { |  | ||||||
|     {.basic = &animation_level1furippa, .active = &animation_level1furippa_active}, |  | ||||||
|     {.basic = &animation_level1read, .active = &animation_level1read_active}, |  | ||||||
|     {.basic = &animation_level1toys, .active = &animation_level1toys_active}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation level_2_animation[] = { |  | ||||||
|     {.basic = &animation_level2furippa, .active = &animation_level2furippa_active}, |  | ||||||
|     {.basic = &animation_level2soldering, .active = &animation_level2soldering_active}, |  | ||||||
|     {.basic = &animation_level2hack, .active = &animation_level2hack_active}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation level_3_animation[] = { |  | ||||||
|     {.basic = &animation_level3furippa, .active = &animation_level3furippa_active}, |  | ||||||
|     {.basic = &animation_level3hijack, .active = &animation_level3hijack_active}, |  | ||||||
|     {.basic = &animation_level3lab, .active = &animation_level3lab_active}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation no_sd_animation[] = { |  | ||||||
|     {.basic = &animation_no_sd_card, .active = NULL}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static const PairedAnimation check_battery_animation[] = { |  | ||||||
|     {.basic = &animation_bad_battery, .active = NULL}, |  | ||||||
| }; |  | ||||||
| @ -5,5 +5,4 @@ ADD_SCENE(desktop, debug, Debug) | |||||||
| ADD_SCENE(desktop, first_start, FirstStart) | ADD_SCENE(desktop, first_start, FirstStart) | ||||||
| ADD_SCENE(desktop, hw_mismatch, HwMismatch) | ADD_SCENE(desktop, hw_mismatch, HwMismatch) | ||||||
| ADD_SCENE(desktop, pinsetup, PinSetup) | ADD_SCENE(desktop, pinsetup, PinSetup) | ||||||
| ADD_SCENE(desktop, levelup, LevelUp) |  | ||||||
| ADD_SCENE(desktop, fault, Fault) | ADD_SCENE(desktop, fault, Fault) | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| #include <dolphin/dolphin.h> | #include <dolphin/dolphin.h> | ||||||
| #include <dolphin/helpers/dolphin_deed.h> | #include <dolphin/helpers/dolphin_deed.h> | ||||||
| 
 | 
 | ||||||
| void desktop_scene_debug_callback(DesktopDebugEvent event, void* context) { | void desktop_scene_debug_callback(DesktopEvent event, void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||||
| } | } | ||||||
| @ -33,14 +33,12 @@ bool desktop_scene_debug_on_event(void* context, SceneManagerEvent event) { | |||||||
|         case DesktopDebugEventDeed: |         case DesktopDebugEventDeed: | ||||||
|             dolphin_deed(dolphin, DolphinDeedIButtonEmulate); |             dolphin_deed(dolphin, DolphinDeedIButtonEmulate); | ||||||
|             desktop_debug_get_dolphin_data(desktop->debug_view); |             desktop_debug_get_dolphin_data(desktop->debug_view); | ||||||
|             desktop_start_new_idle_animation(desktop->animation); |  | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|         case DesktopDebugEventWrongDeed: |         case DesktopDebugEventWrongDeed: | ||||||
|             dolphin_deed(dolphin, DolphinDeedWrong); |             dolphin_deed(dolphin, DolphinDeedWrong); | ||||||
|             desktop_debug_get_dolphin_data(desktop->debug_view); |             desktop_debug_get_dolphin_data(desktop->debug_view); | ||||||
|             desktop_start_new_idle_animation(desktop->animation); |  | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| #include "../desktop_i.h" | #include "../desktop_i.h" | ||||||
| #include "../views/desktop_first_start.h" | #include "../views/desktop_first_start.h" | ||||||
|  | #include "../views/desktop_events.h" | ||||||
| 
 | 
 | ||||||
| void desktop_scene_first_start_callback(DesktopFirstStartEvent event, void* context) { | void desktop_scene_first_start_callback(DesktopEvent event, void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||||
| } | } | ||||||
|  | |||||||
| @ -10,18 +10,21 @@ void desktop_scene_hw_mismatch_callback(void* context) { | |||||||
| 
 | 
 | ||||||
| void desktop_scene_hw_mismatch_on_enter(void* context) { | void desktop_scene_hw_mismatch_on_enter(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|  |     furi_assert(desktop); | ||||||
|  |     furi_assert(!desktop->text_buffer); | ||||||
|     Popup* popup = desktop->hw_mismatch_popup; |     Popup* popup = desktop->hw_mismatch_popup; | ||||||
|     char buffer[256]; // strange but smaller buffer not making it
 |     desktop->text_buffer = furi_alloc(256); | ||||||
|     snprintf( |     snprintf( | ||||||
|         buffer, |         desktop->text_buffer, | ||||||
|         sizeof(buffer), |         256, | ||||||
|         "HW target: %d\nFW target: %d", |         "HW target: %d\nFW target: %d", | ||||||
|         furi_hal_version_get_hw_target(), |         furi_hal_version_get_hw_target(), | ||||||
|         version_get_target(NULL)); |         version_get_target(NULL)); | ||||||
|     popup_set_context(popup, desktop); |     popup_set_context(popup, desktop); | ||||||
|     popup_set_header( |     popup_set_header( | ||||||
|         popup, "!!!! HW Mismatch !!!!", 60, 14 + STATUS_BAR_Y_SHIFT, AlignCenter, AlignCenter); |         popup, "!!!! HW Mismatch !!!!", 60, 14 + STATUS_BAR_Y_SHIFT, AlignCenter, AlignCenter); | ||||||
|     popup_set_text(popup, buffer, 60, 37 + STATUS_BAR_Y_SHIFT, AlignCenter, AlignCenter); |     popup_set_text( | ||||||
|  |         popup, desktop->text_buffer, 60, 37 + STATUS_BAR_Y_SHIFT, AlignCenter, AlignCenter); | ||||||
|     popup_set_callback(popup, desktop_scene_hw_mismatch_callback); |     popup_set_callback(popup, desktop_scene_hw_mismatch_callback); | ||||||
|     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewHwMismatch); |     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewHwMismatch); | ||||||
| } | } | ||||||
| @ -46,9 +49,13 @@ bool desktop_scene_hw_mismatch_on_event(void* context, SceneManagerEvent event) | |||||||
| 
 | 
 | ||||||
| void desktop_scene_hw_mismatch_on_exit(void* context) { | void desktop_scene_hw_mismatch_on_exit(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|  |     furi_assert(desktop); | ||||||
|  |     furi_assert(desktop->text_buffer); | ||||||
|     Popup* popup = desktop->hw_mismatch_popup; |     Popup* popup = desktop->hw_mismatch_popup; | ||||||
|     popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); |     popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); | ||||||
|     popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); |     popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); | ||||||
|     popup_set_callback(popup, NULL); |     popup_set_callback(popup, NULL); | ||||||
|     popup_set_context(popup, NULL); |     popup_set_context(popup, NULL); | ||||||
|  |     free(desktop->text_buffer); | ||||||
|  |     desktop->text_buffer = NULL; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,79 +0,0 @@ | |||||||
| #include "../desktop_i.h" |  | ||||||
| #include "../views/desktop_main.h" |  | ||||||
| #include "applications.h" |  | ||||||
| #include "assets_icons.h" |  | ||||||
| #include "desktop/desktop.h" |  | ||||||
| #include "desktop/helpers/desktop_animation.h" |  | ||||||
| #include "dolphin/dolphin.h" |  | ||||||
| #include "furi/pubsub.h" |  | ||||||
| #include "furi/record.h" |  | ||||||
| #include "storage/storage-glue.h" |  | ||||||
| #include <loader/loader.h> |  | ||||||
| #include <m-list.h> |  | ||||||
| 
 |  | ||||||
| #define LEVELUP_SCENE_PLAYING 0 |  | ||||||
| #define LEVELUP_SCENE_STOPPED 1 |  | ||||||
| 
 |  | ||||||
| static void desktop_scene_levelup_callback(DesktopMainEvent event, void* context) { |  | ||||||
|     Desktop* desktop = (Desktop*)context; |  | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void desktop_scene_levelup_animation_changed_callback(void* context) { |  | ||||||
|     furi_assert(context); |  | ||||||
|     Desktop* desktop = context; |  | ||||||
|     view_dispatcher_send_custom_event( |  | ||||||
|         desktop->view_dispatcher, DesktopMainEventUpdateOneShotAnimation); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_scene_levelup_on_enter(void* context) { |  | ||||||
|     Desktop* desktop = (Desktop*)context; |  | ||||||
|     DesktopMainView* main_view = desktop->main_view; |  | ||||||
| 
 |  | ||||||
|     desktop_main_set_callback(main_view, desktop_scene_levelup_callback, desktop); |  | ||||||
|     desktop_animation_set_animation_changed_callback( |  | ||||||
|         desktop->animation, desktop_scene_levelup_animation_changed_callback, desktop); |  | ||||||
| 
 |  | ||||||
|     desktop_animation_start_oneshot_levelup(desktop->animation); |  | ||||||
|     const Icon* icon = desktop_animation_get_oneshot_frame(desktop->animation); |  | ||||||
|     desktop_main_switch_dolphin_icon(desktop->main_view, icon); |  | ||||||
|     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewMain); |  | ||||||
|     scene_manager_set_scene_state( |  | ||||||
|         desktop->scene_manager, DesktopSceneLevelUp, LEVELUP_SCENE_PLAYING); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| bool desktop_scene_levelup_on_event(void* context, SceneManagerEvent event) { |  | ||||||
|     Desktop* desktop = (Desktop*)context; |  | ||||||
|     bool consumed = false; |  | ||||||
|     DesktopMainEvent main_event = event.event; |  | ||||||
| 
 |  | ||||||
|     if(event.type == SceneManagerEventTypeCustom) { |  | ||||||
|         if(main_event == DesktopMainEventUpdateOneShotAnimation) { |  | ||||||
|             const Icon* icon = desktop_animation_get_oneshot_frame(desktop->animation); |  | ||||||
|             if(icon) { |  | ||||||
|                 desktop_main_switch_dolphin_icon(desktop->main_view, icon); |  | ||||||
|             } else { |  | ||||||
|                 scene_manager_set_scene_state( |  | ||||||
|                     desktop->scene_manager, DesktopSceneLevelUp, LEVELUP_SCENE_STOPPED); |  | ||||||
|             } |  | ||||||
|             consumed = true; |  | ||||||
|         } else { |  | ||||||
|             if(scene_manager_get_scene_state(desktop->scene_manager, DesktopSceneLevelUp) == |  | ||||||
|                LEVELUP_SCENE_STOPPED) { |  | ||||||
|                 scene_manager_previous_scene(desktop->scene_manager); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return consumed; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_scene_levelup_on_exit(void* context) { |  | ||||||
|     Desktop* desktop = (Desktop*)context; |  | ||||||
| 
 |  | ||||||
|     Dolphin* dolphin = furi_record_open("dolphin"); |  | ||||||
|     dolphin_upgrade_level(dolphin); |  | ||||||
|     furi_record_close("dolphin"); |  | ||||||
|     desktop_animation_set_animation_changed_callback(desktop->animation, NULL, NULL); |  | ||||||
|     desktop_start_new_idle_animation(desktop->animation); |  | ||||||
| } |  | ||||||
| @ -3,7 +3,7 @@ | |||||||
| #include <toolbox/saved_struct.h> | #include <toolbox/saved_struct.h> | ||||||
| #include <stdbool.h> | #include <stdbool.h> | ||||||
| 
 | 
 | ||||||
| void desktop_scene_lock_menu_callback(DesktopLockMenuEvent event, void* context) { | void desktop_scene_lock_menu_callback(DesktopEvent event, void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,34 +1,28 @@ | |||||||
| #include "../desktop_i.h" | #include "../desktop_i.h" | ||||||
| #include "../views/desktop_locked.h" | #include "../views/desktop_locked.h" | ||||||
| #include "desktop/helpers/desktop_animation.h" |  | ||||||
| #include "desktop/views/desktop_main.h" | #include "desktop/views/desktop_main.h" | ||||||
| 
 | 
 | ||||||
| void desktop_scene_locked_callback(DesktopLockedEvent event, void* context) { | void desktop_scene_locked_callback(DesktopEvent event, void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void desktop_scene_locked_animation_changed_callback(void* context) { | static void desktop_scene_locked_new_idle_animation_callback(void* context) { | ||||||
|     furi_assert(context); |     furi_assert(context); | ||||||
|     Desktop* desktop = context; |     Desktop* desktop = context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventUpdateAnimation); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopLockedEventCheckAnimation); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void desktop_scene_locked_on_enter(void* context) { | void desktop_scene_locked_on_enter(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     DesktopLockedView* locked_view = desktop->locked_view; |     DesktopLockedView* locked_view = desktop->locked_view; | ||||||
| 
 | 
 | ||||||
|  |     animation_manager_set_new_idle_callback( | ||||||
|  |         desktop->animation_manager, desktop_scene_locked_new_idle_animation_callback); | ||||||
|     desktop_locked_set_callback(locked_view, desktop_scene_locked_callback, desktop); |     desktop_locked_set_callback(locked_view, desktop_scene_locked_callback, desktop); | ||||||
|     desktop_locked_reset_door_pos(locked_view); |     desktop_locked_reset_door_pos(locked_view); | ||||||
|     desktop_locked_update_hint_timeout(locked_view); |     desktop_locked_update_hint_timeout(locked_view); | ||||||
| 
 | 
 | ||||||
|     desktop_animation_set_animation_changed_callback( |  | ||||||
|         desktop->animation, desktop_scene_locked_animation_changed_callback, desktop); |  | ||||||
|     bool status_bar_background_black = false; |  | ||||||
|     const Icon* icon = |  | ||||||
|         desktop_animation_get_animation(desktop->animation, &status_bar_background_black); |  | ||||||
|     desktop_locked_set_dolphin_animation(locked_view, icon, status_bar_background_black); |  | ||||||
| 
 |  | ||||||
|     uint32_t state = scene_manager_get_scene_state(desktop->scene_manager, DesktopViewLocked); |     uint32_t state = scene_manager_get_scene_state(desktop->scene_manager, DesktopViewLocked); | ||||||
| 
 | 
 | ||||||
|     desktop_locked_with_pin(desktop->locked_view, state == DesktopLockedWithPin); |     desktop_locked_with_pin(desktop->locked_view, state == DesktopLockedWithPin); | ||||||
| @ -39,7 +33,7 @@ void desktop_scene_locked_on_enter(void* context) { | |||||||
|     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewLocked); |     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewLocked); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static bool desktop_scene_locked_check_pin(Desktop* desktop, DesktopMainEvent event) { | static bool desktop_scene_locked_check_pin(Desktop* desktop, DesktopEvent event) { | ||||||
|     bool match = false; |     bool match = false; | ||||||
| 
 | 
 | ||||||
|     size_t length = desktop->pincode_buffer.length; |     size_t length = desktop->pincode_buffer.length; | ||||||
| @ -81,15 +75,10 @@ bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) { | |||||||
|         case DesktopLockedEventInputReset: |         case DesktopLockedEventInputReset: | ||||||
|             desktop->pincode_buffer.length = 0; |             desktop->pincode_buffer.length = 0; | ||||||
|             break; |             break; | ||||||
|         case DesktopMainEventUpdateAnimation: { |         case DesktopLockedEventCheckAnimation: | ||||||
|             bool status_bar_background_black = false; |             animation_manager_check_blocking_process(desktop->animation_manager); | ||||||
|             const Icon* icon = |  | ||||||
|                 desktop_animation_get_animation(desktop->animation, &status_bar_background_black); |  | ||||||
|             desktop_locked_set_dolphin_animation( |  | ||||||
|                 desktop->locked_view, icon, status_bar_background_black); |  | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
|         } |  | ||||||
|         default: |         default: | ||||||
|             if(desktop_scene_locked_check_pin(desktop, event.event)) { |             if(desktop_scene_locked_check_pin(desktop, event.event)) { | ||||||
|                 scene_manager_set_scene_state( |                 scene_manager_set_scene_state( | ||||||
| @ -106,7 +95,7 @@ bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) { | |||||||
| 
 | 
 | ||||||
| void desktop_scene_locked_on_exit(void* context) { | void desktop_scene_locked_on_exit(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     desktop_animation_set_animation_changed_callback(desktop->animation, NULL, NULL); |     animation_manager_set_new_idle_callback(desktop->animation_manager, NULL); | ||||||
|     desktop_locked_reset_counter(desktop->locked_view); |     desktop_locked_reset_counter(desktop->locked_view); | ||||||
|     osTimerStop(desktop->locked_view->timer); |     osTimerStop(desktop->locked_view->timer); | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,14 +2,51 @@ | |||||||
| #include "../views/desktop_main.h" | #include "../views/desktop_main.h" | ||||||
| #include "applications.h" | #include "applications.h" | ||||||
| #include "assets_icons.h" | #include "assets_icons.h" | ||||||
|  | #include "cmsis_os2.h" | ||||||
|  | #include "desktop/desktop.h" | ||||||
|  | #include "desktop/views/desktop_events.h" | ||||||
| #include "dolphin/dolphin.h" | #include "dolphin/dolphin.h" | ||||||
| #include "furi/pubsub.h" | #include "furi/pubsub.h" | ||||||
| #include "furi/record.h" | #include "furi/record.h" | ||||||
|  | #include "furi/thread.h" | ||||||
| #include "storage/storage-glue.h" | #include "storage/storage-glue.h" | ||||||
| #include <loader/loader.h> | #include <loader/loader.h> | ||||||
| #include <m-list.h> | #include <m-list.h> | ||||||
| #define MAIN_VIEW_DEFAULT (0UL) | #define MAIN_VIEW_DEFAULT (0UL) | ||||||
| 
 | 
 | ||||||
|  | static void desktop_scene_main_app_started_callback(const void* message, void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     Desktop* desktop = context; | ||||||
|  |     const LoaderEvent* event = message; | ||||||
|  | 
 | ||||||
|  |     if(event->type == LoaderEventTypeApplicationStarted) { | ||||||
|  |         view_dispatcher_send_custom_event( | ||||||
|  |             desktop->view_dispatcher, DesktopMainEventBeforeAppStarted); | ||||||
|  |         osSemaphoreAcquire(desktop->unload_animation_semaphore, osWaitForever); | ||||||
|  |     } else if(event->type == LoaderEventTypeApplicationStopped) { | ||||||
|  |         view_dispatcher_send_custom_event( | ||||||
|  |             desktop->view_dispatcher, DesktopMainEventAfterAppFinished); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void desktop_scene_main_new_idle_animation_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     Desktop* desktop = context; | ||||||
|  |     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventNewIdleAnimation); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void desktop_scene_main_check_animation_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     Desktop* desktop = context; | ||||||
|  |     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventCheckAnimation); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void desktop_scene_main_interact_animation_callback(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  |     Desktop* desktop = context; | ||||||
|  |     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventInteractAnimation); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* flipper_app) { | static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* flipper_app) { | ||||||
|     furi_assert(desktop); |     furi_assert(desktop); | ||||||
|     furi_assert(flipper_app); |     furi_assert(flipper_app); | ||||||
| @ -26,23 +63,31 @@ static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* fl | |||||||
|     furi_thread_set_callback(desktop->scene_thread, flipper_app->app); |     furi_thread_set_callback(desktop->scene_thread, flipper_app->app); | ||||||
| 
 | 
 | ||||||
|     furi_thread_start(desktop->scene_thread); |     furi_thread_start(desktop->scene_thread); | ||||||
|  | 
 | ||||||
|  |     furi_thread_join(desktop->scene_thread); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void desktop_scene_main_callback(DesktopMainEvent event, void* context) { | void desktop_scene_main_callback(DesktopEvent event, void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); |     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void desktop_scene_main_animation_changed_callback(void* context) { |  | ||||||
|     furi_assert(context); |  | ||||||
|     Desktop* desktop = context; |  | ||||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventUpdateAnimation); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_scene_main_on_enter(void* context) { | void desktop_scene_main_on_enter(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
|     DesktopMainView* main_view = desktop->main_view; |     DesktopMainView* main_view = desktop->main_view; | ||||||
| 
 | 
 | ||||||
|  |     animation_manager_set_context(desktop->animation_manager, desktop); | ||||||
|  |     animation_manager_set_new_idle_callback( | ||||||
|  |         desktop->animation_manager, desktop_scene_main_new_idle_animation_callback); | ||||||
|  |     animation_manager_set_check_callback( | ||||||
|  |         desktop->animation_manager, desktop_scene_main_check_animation_callback); | ||||||
|  |     animation_manager_set_interact_callback( | ||||||
|  |         desktop->animation_manager, desktop_scene_main_interact_animation_callback); | ||||||
|  | 
 | ||||||
|  |     furi_assert(osSemaphoreGetCount(desktop->unload_animation_semaphore) == 0); | ||||||
|  |     desktop->app_start_stop_subscription = furi_pubsub_subscribe( | ||||||
|  |         loader_get_pubsub(), desktop_scene_main_app_started_callback, desktop); | ||||||
|  | 
 | ||||||
|     desktop_main_set_callback(main_view, desktop_scene_main_callback, desktop); |     desktop_main_set_callback(main_view, desktop_scene_main_callback, desktop); | ||||||
|     view_port_enabled_set(desktop->lock_viewport, false); |     view_port_enabled_set(desktop->lock_viewport, false); | ||||||
| 
 | 
 | ||||||
| @ -51,13 +96,6 @@ void desktop_scene_main_on_enter(void* context) { | |||||||
|         desktop_main_unlocked(desktop->main_view); |         desktop_main_unlocked(desktop->main_view); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     desktop_animation_activate(desktop->animation); |  | ||||||
|     desktop_animation_set_animation_changed_callback( |  | ||||||
|         desktop->animation, desktop_scene_main_animation_changed_callback, desktop); |  | ||||||
|     bool status_bar_background_black = false; |  | ||||||
|     const Icon* icon = |  | ||||||
|         desktop_animation_get_animation(desktop->animation, &status_bar_background_black); |  | ||||||
|     desktop_main_switch_dolphin_animation(desktop->main_view, icon, status_bar_background_black); |  | ||||||
|     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewMain); |     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewMain); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -84,48 +122,50 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | |||||||
| 
 | 
 | ||||||
|         case DesktopMainEventOpenArchive: |         case DesktopMainEventOpenArchive: | ||||||
| #ifdef APP_ARCHIVE | #ifdef APP_ARCHIVE | ||||||
|  |             animation_manager_unload_and_stall_animation(desktop->animation_manager); | ||||||
|             desktop_switch_to_app(desktop, &FLIPPER_ARCHIVE); |             desktop_switch_to_app(desktop, &FLIPPER_ARCHIVE); | ||||||
|  |             animation_manager_load_and_continue_animation(desktop->animation_manager); | ||||||
| #endif | #endif | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|         case DesktopMainEventOpenFavorite: |         case DesktopMainEventOpenFavorite: | ||||||
|             LOAD_DESKTOP_SETTINGS(&desktop->settings); |             LOAD_DESKTOP_SETTINGS(&desktop->settings); | ||||||
|  |             animation_manager_unload_and_stall_animation(desktop->animation_manager); | ||||||
|             if(desktop->settings.favorite < FLIPPER_APPS_COUNT) { |             if(desktop->settings.favorite < FLIPPER_APPS_COUNT) { | ||||||
|                 desktop_switch_to_app(desktop, &FLIPPER_APPS[desktop->settings.favorite]); |                 desktop_switch_to_app(desktop, &FLIPPER_APPS[desktop->settings.favorite]); | ||||||
|             } else { |             } else { | ||||||
|                 FURI_LOG_E("DesktopSrv", "Can't find favorite application"); |                 FURI_LOG_E("DesktopSrv", "Can't find favorite application"); | ||||||
|             } |             } | ||||||
|  |             animation_manager_load_and_continue_animation(desktop->animation_manager); | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|         case DesktopMainEventUpdateAnimation: { |         case DesktopMainEventCheckAnimation: | ||||||
|             bool status_bar_background_black = false; |             animation_manager_check_blocking_process(desktop->animation_manager); | ||||||
|             const Icon* icon = |  | ||||||
|                 desktop_animation_get_animation(desktop->animation, &status_bar_background_black); |  | ||||||
|             desktop_main_switch_dolphin_animation( |  | ||||||
|                 desktop->main_view, icon, status_bar_background_black); |  | ||||||
|             consumed = true; |             consumed = true; | ||||||
|             break; |             break; | ||||||
|         } |         case DesktopMainEventNewIdleAnimation: | ||||||
| 
 |             animation_manager_new_idle_process(desktop->animation_manager); | ||||||
|         case DesktopMainEventRightShort: { |             consumed = true; | ||||||
|             DesktopAnimationState state = desktop_animation_handle_right(desktop->animation); |             break; | ||||||
|             if(state == DesktopAnimationStateLevelUpIsPending) { |         case DesktopMainEventInteractAnimation: | ||||||
|                 scene_manager_next_scene(desktop->scene_manager, DesktopSceneLevelUp); |             animation_manager_interact_process(desktop->animation_manager); | ||||||
|             } |             consumed = true; | ||||||
|  |             break; | ||||||
|  |         case DesktopMainEventBeforeAppStarted: | ||||||
|  |             animation_manager_unload_and_stall_animation(desktop->animation_manager); | ||||||
|  |             osSemaphoreRelease(desktop->unload_animation_semaphore); | ||||||
|  |             consumed = true; | ||||||
|  |             break; | ||||||
|  |         case DesktopMainEventAfterAppFinished: | ||||||
|  |             animation_manager_load_and_continue_animation(desktop->animation_manager); | ||||||
|  |             consumed = true; | ||||||
|             break; |             break; | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         default: |         default: | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         if(event.event != DesktopMainEventUpdateAnimation) { |  | ||||||
|             desktop_animation_activate(desktop->animation); |  | ||||||
|         } |  | ||||||
|     } else if(event.type != SceneManagerEventTypeTick) { |  | ||||||
|         desktop_animation_activate(desktop->animation); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return consumed; |     return consumed; | ||||||
| @ -134,7 +174,18 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | |||||||
| void desktop_scene_main_on_exit(void* context) { | void desktop_scene_main_on_exit(void* context) { | ||||||
|     Desktop* desktop = (Desktop*)context; |     Desktop* desktop = (Desktop*)context; | ||||||
| 
 | 
 | ||||||
|     desktop_animation_set_animation_changed_callback(desktop->animation, NULL, NULL); |     /**
 | ||||||
|  |      * We're allowed to leave this scene only when any other app & loader | ||||||
|  |      * is finished, that's why we can be sure there is no task waiting | ||||||
|  |      * for start/stop semaphore | ||||||
|  |      */ | ||||||
|  |     furi_pubsub_unsubscribe(loader_get_pubsub(), desktop->app_start_stop_subscription); | ||||||
|  |     furi_assert(osSemaphoreGetCount(desktop->unload_animation_semaphore) == 0); | ||||||
|  | 
 | ||||||
|  |     animation_manager_set_new_idle_callback(desktop->animation_manager, NULL); | ||||||
|  |     animation_manager_set_check_callback(desktop->animation_manager, NULL); | ||||||
|  |     animation_manager_set_interact_callback(desktop->animation_manager, NULL); | ||||||
|  |     animation_manager_set_context(desktop->animation_manager, desktop); | ||||||
|     scene_manager_set_scene_state(desktop->scene_manager, DesktopSceneMain, MAIN_VIEW_DEFAULT); |     scene_manager_set_scene_state(desktop->scene_manager, DesktopSceneMain, MAIN_VIEW_DEFAULT); | ||||||
|     desktop_main_reset_hint(desktop->main_view); |     desktop_main_reset_hint(desktop->main_view); | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,17 +7,11 @@ | |||||||
| #include <furi.h> | #include <furi.h> | ||||||
| #include <storage/storage.h> | #include <storage/storage.h> | ||||||
| #include <time.h> | #include <time.h> | ||||||
| 
 | #include "desktop_events.h" | ||||||
| typedef enum { |  | ||||||
|     DesktopDebugEventDeed, |  | ||||||
|     DesktopDebugEventWrongDeed, |  | ||||||
|     DesktopDebugEventSaveState, |  | ||||||
|     DesktopDebugEventExit, |  | ||||||
| } DesktopDebugEvent; |  | ||||||
| 
 | 
 | ||||||
| typedef struct DesktopDebugView DesktopDebugView; | typedef struct DesktopDebugView DesktopDebugView; | ||||||
| 
 | 
 | ||||||
| typedef void (*DesktopDebugViewCallback)(DesktopDebugEvent event, void* context); | typedef void (*DesktopDebugViewCallback)(DesktopEvent event, void* context); | ||||||
| 
 | 
 | ||||||
| // Debug info
 | // Debug info
 | ||||||
| typedef enum { | typedef enum { | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								applications/desktop/views/desktop_events.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,30 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     DesktopMainEventOpenLockMenu, | ||||||
|  |     DesktopMainEventOpenArchive, | ||||||
|  |     DesktopMainEventOpenFavorite, | ||||||
|  |     DesktopMainEventOpenMenu, | ||||||
|  |     DesktopMainEventOpenDebug, | ||||||
|  |     DesktopMainEventUnlocked, | ||||||
|  |     DesktopMainEventRightShort, | ||||||
|  |     DesktopMainEventCheckAnimation, | ||||||
|  |     DesktopMainEventNewIdleAnimation, | ||||||
|  |     DesktopMainEventInteractAnimation, | ||||||
|  |     DesktopMainEventBeforeAppStarted, | ||||||
|  |     DesktopMainEventAfterAppFinished, | ||||||
|  |     DesktopLockedEventUnlock, | ||||||
|  |     DesktopLockedEventUpdate, | ||||||
|  |     DesktopLockedEventInputReset, | ||||||
|  |     DesktopLockedEventCheckAnimation, | ||||||
|  |     DesktopLockedEventMax, | ||||||
|  |     DesktopDebugEventDeed, | ||||||
|  |     DesktopDebugEventWrongDeed, | ||||||
|  |     DesktopDebugEventSaveState, | ||||||
|  |     DesktopDebugEventExit, | ||||||
|  |     DesktopFirstStartCompleted, | ||||||
|  |     DesktopFirstStartPoweroff, | ||||||
|  |     DesktopLockMenuEventLock, | ||||||
|  |     DesktopLockMenuEventPinLock, | ||||||
|  |     DesktopLockMenuEventExit, | ||||||
|  | } DesktopEvent; | ||||||
| @ -5,15 +5,11 @@ | |||||||
| #include <gui/canvas.h> | #include <gui/canvas.h> | ||||||
| #include <gui/elements.h> | #include <gui/elements.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| 
 | #include "desktop_events.h" | ||||||
| typedef enum { |  | ||||||
|     DesktopFirstStartCompleted, |  | ||||||
|     DesktopFirstStartPoweroff, |  | ||||||
| } DesktopFirstStartEvent; |  | ||||||
| 
 | 
 | ||||||
| typedef struct DesktopFirstStartView DesktopFirstStartView; | typedef struct DesktopFirstStartView DesktopFirstStartView; | ||||||
| 
 | 
 | ||||||
| typedef void (*DesktopFirstStartViewCallback)(DesktopFirstStartEvent event, void* context); | typedef void (*DesktopFirstStartViewCallback)(DesktopEvent event, void* context); | ||||||
| 
 | 
 | ||||||
| DesktopFirstStartView* desktop_first_start_alloc(); | DesktopFirstStartView* desktop_first_start_alloc(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,18 +5,13 @@ | |||||||
| #include <gui/canvas.h> | #include <gui/canvas.h> | ||||||
| #include <gui/elements.h> | #include <gui/elements.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
|  | #include "desktop_events.h" | ||||||
| 
 | 
 | ||||||
| #define HINT_TIMEOUT 2 | #define HINT_TIMEOUT 2 | ||||||
| 
 | 
 | ||||||
| typedef enum { |  | ||||||
|     DesktopLockMenuEventLock, |  | ||||||
|     DesktopLockMenuEventPinLock, |  | ||||||
|     DesktopLockMenuEventExit, |  | ||||||
| } DesktopLockMenuEvent; |  | ||||||
| 
 |  | ||||||
| typedef struct DesktopLockMenuView DesktopLockMenuView; | typedef struct DesktopLockMenuView DesktopLockMenuView; | ||||||
| 
 | 
 | ||||||
| typedef void (*DesktopLockMenuViewCallback)(DesktopLockMenuEvent event, void* context); | typedef void (*DesktopLockMenuViewCallback)(DesktopEvent event, void* context); | ||||||
| 
 | 
 | ||||||
| struct DesktopLockMenuView { | struct DesktopLockMenuView { | ||||||
|     View* view; |     View* view; | ||||||
|  | |||||||
| @ -17,21 +17,6 @@ void locked_view_timer_callback(void* context) { | |||||||
|     locked_view->callback(DesktopLockedEventUpdate, locked_view->context); |     locked_view->callback(DesktopLockedEventUpdate, locked_view->context); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void desktop_locked_set_dolphin_animation( |  | ||||||
|     DesktopLockedView* locked_view, |  | ||||||
|     const Icon* icon, |  | ||||||
|     bool status_bar_background_black) { |  | ||||||
|     with_view_model( |  | ||||||
|         locked_view->view, (DesktopLockedViewModel * model) { |  | ||||||
|             if(model->animation) icon_animation_free(model->animation); |  | ||||||
|             model->animation = icon_animation_alloc(icon); |  | ||||||
|             view_tie_icon_animation(locked_view->view, model->animation); |  | ||||||
|             icon_animation_start(model->animation); |  | ||||||
|             model->status_bar_background_black = status_bar_background_black; |  | ||||||
|             return true; |  | ||||||
|         }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void desktop_locked_update_hint_timeout(DesktopLockedView* locked_view) { | void desktop_locked_update_hint_timeout(DesktopLockedView* locked_view) { | ||||||
|     with_view_model( |     with_view_model( | ||||||
|         locked_view->view, (DesktopLockedViewModel * model) { |         locked_view->view, (DesktopLockedViewModel * model) { | ||||||
| @ -95,7 +80,6 @@ void desktop_locked_with_pin(DesktopLockedView* locked_view, bool locked) { | |||||||
| void desktop_locked_render(Canvas* canvas, void* model) { | void desktop_locked_render(Canvas* canvas, void* model) { | ||||||
|     DesktopLockedViewModel* m = model; |     DesktopLockedViewModel* m = model; | ||||||
|     uint32_t now = osKernelGetTickCount(); |     uint32_t now = osKernelGetTickCount(); | ||||||
|     canvas_clear(canvas); |  | ||||||
|     canvas_set_color(canvas, ColorBlack); |     canvas_set_color(canvas, ColorBlack); | ||||||
| 
 | 
 | ||||||
|     if(!m->animation_seq_end) { |     if(!m->animation_seq_end) { | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ | |||||||
| #include <gui/canvas.h> | #include <gui/canvas.h> | ||||||
| #include <gui/elements.h> | #include <gui/elements.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
|  | #include "desktop_events.h" | ||||||
| 
 | 
 | ||||||
| #define UNLOCK_RST_TIMEOUT 300 | #define UNLOCK_RST_TIMEOUT 300 | ||||||
| #define UNLOCK_CNT 2 // 3 actually
 | #define UNLOCK_CNT 2 // 3 actually
 | ||||||
| @ -14,12 +15,6 @@ | |||||||
| #define DOOR_R_POS 115 | #define DOOR_R_POS 115 | ||||||
| #define DOOR_R_POS_MIN 60 | #define DOOR_R_POS_MIN 60 | ||||||
| 
 | 
 | ||||||
| typedef enum { |  | ||||||
|     DesktopLockedEventUnlock = 10U, |  | ||||||
|     DesktopLockedEventUpdate = 11U, |  | ||||||
|     DesktopLockedEventInputReset = 12U, |  | ||||||
| } DesktopLockedEvent; |  | ||||||
| 
 |  | ||||||
| typedef enum { | typedef enum { | ||||||
|     DesktopLockedWithPin, |     DesktopLockedWithPin, | ||||||
|     DesktopLockedNoPin, |     DesktopLockedNoPin, | ||||||
| @ -27,7 +22,7 @@ typedef enum { | |||||||
| 
 | 
 | ||||||
| typedef struct DesktopLockedView DesktopLockedView; | typedef struct DesktopLockedView DesktopLockedView; | ||||||
| 
 | 
 | ||||||
| typedef void (*DesktopLockedViewCallback)(DesktopLockedEvent event, void* context); | typedef void (*DesktopLockedViewCallback)(DesktopEvent event, void* context); | ||||||
| 
 | 
 | ||||||
| struct DesktopLockedView { | struct DesktopLockedView { | ||||||
|     View* view; |     View* view; | ||||||
| @ -57,10 +52,6 @@ void desktop_locked_set_callback( | |||||||
|     DesktopLockedViewCallback callback, |     DesktopLockedViewCallback callback, | ||||||
|     void* context); |     void* context); | ||||||
| 
 | 
 | ||||||
| void desktop_locked_set_dolphin_animation( |  | ||||||
|     DesktopLockedView* locked_view, |  | ||||||
|     const Icon* icon, |  | ||||||
|     bool status_bar_background_black); |  | ||||||
| void desktop_locked_update_hint_timeout(DesktopLockedView* locked_view); | void desktop_locked_update_hint_timeout(DesktopLockedView* locked_view); | ||||||
| void desktop_locked_reset_counter(DesktopLockedView* locked_view); | void desktop_locked_reset_counter(DesktopLockedView* locked_view); | ||||||
| void desktop_locked_reset_door_pos(DesktopLockedView* locked_view); | void desktop_locked_reset_door_pos(DesktopLockedView* locked_view); | ||||||
|  | |||||||
| @ -1,8 +1,13 @@ | |||||||
|  | #include "dolphin/dolphin.h" | ||||||
|  | #include "furi/record.h" | ||||||
| #include "gui/canvas.h" | #include "gui/canvas.h" | ||||||
|  | #include "gui/view.h" | ||||||
|  | #include "gui/view_composed.h" | ||||||
| #include "input/input.h" | #include "input/input.h" | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| #include "../desktop_i.h" | #include "../desktop_i.h" | ||||||
| #include "desktop_main.h" | #include "desktop_main.h" | ||||||
|  | //#include "../animations/views/bubble_animation_view.h"
 | ||||||
| 
 | 
 | ||||||
| void desktop_main_set_callback( | void desktop_main_set_callback( | ||||||
|     DesktopMainView* main_view, |     DesktopMainView* main_view, | ||||||
| @ -49,7 +54,6 @@ void desktop_main_switch_dolphin_icon(DesktopMainView* main_view, const Icon* ic | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void desktop_main_render(Canvas* canvas, void* model) { | void desktop_main_render(Canvas* canvas, void* model) { | ||||||
|     canvas_clear(canvas); |  | ||||||
|     DesktopMainViewModel* m = model; |     DesktopMainViewModel* m = model; | ||||||
|     uint32_t now = osKernelGetTickCount(); |     uint32_t now = osKernelGetTickCount(); | ||||||
| 
 | 
 | ||||||
| @ -78,6 +82,7 @@ bool desktop_main_input(InputEvent* event, void* context) { | |||||||
|     furi_assert(context); |     furi_assert(context); | ||||||
| 
 | 
 | ||||||
|     DesktopMainView* main_view = context; |     DesktopMainView* main_view = context; | ||||||
|  |     bool consumed = false; | ||||||
| 
 | 
 | ||||||
|     if(event->key == InputKeyOk && event->type == InputTypeShort) { |     if(event->key == InputKeyOk && event->type == InputTypeShort) { | ||||||
|         main_view->callback(DesktopMainEventOpenMenu, main_view->context); |         main_view->callback(DesktopMainEventOpenMenu, main_view->context); | ||||||
| @ -91,11 +96,13 @@ bool desktop_main_input(InputEvent* event, void* context) { | |||||||
|         main_view->callback(DesktopMainEventOpenFavorite, main_view->context); |         main_view->callback(DesktopMainEventOpenFavorite, main_view->context); | ||||||
|     } else if(event->key == InputKeyRight && event->type == InputTypeShort) { |     } else if(event->key == InputKeyRight && event->type == InputTypeShort) { | ||||||
|         main_view->callback(DesktopMainEventRightShort, main_view->context); |         main_view->callback(DesktopMainEventRightShort, main_view->context); | ||||||
|  |     } else if(event->key == InputKeyBack && event->type == InputTypeShort) { | ||||||
|  |         consumed = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     desktop_main_reset_hint(main_view); |     desktop_main_reset_hint(main_view); | ||||||
| 
 | 
 | ||||||
|     return true; |     return consumed; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void desktop_main_enter(void* context) { | void desktop_main_enter(void* context) { | ||||||
| @ -119,6 +126,7 @@ void desktop_main_exit(void* context) { | |||||||
| 
 | 
 | ||||||
| DesktopMainView* desktop_main_alloc() { | DesktopMainView* desktop_main_alloc() { | ||||||
|     DesktopMainView* main_view = furi_alloc(sizeof(DesktopMainView)); |     DesktopMainView* main_view = furi_alloc(sizeof(DesktopMainView)); | ||||||
|  | 
 | ||||||
|     main_view->view = view_alloc(); |     main_view->view = view_alloc(); | ||||||
|     view_allocate_model(main_view->view, ViewModelTypeLocking, sizeof(DesktopMainViewModel)); |     view_allocate_model(main_view->view, ViewModelTypeLocking, sizeof(DesktopMainViewModel)); | ||||||
|     view_set_context(main_view->view, main_view); |     view_set_context(main_view->view, main_view); | ||||||
|  | |||||||
| @ -1,26 +1,16 @@ | |||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include "gui/view_composed.h" | ||||||
| #include <gui/gui_i.h> | #include <gui/gui_i.h> | ||||||
| #include <gui/view.h> | #include <gui/view.h> | ||||||
| #include <gui/canvas.h> | #include <gui/canvas.h> | ||||||
| #include <gui/elements.h> | #include <gui/elements.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| 
 | #include "desktop_events.h" | ||||||
| typedef enum { |  | ||||||
|     DesktopMainEventOpenLockMenu, |  | ||||||
|     DesktopMainEventOpenArchive, |  | ||||||
|     DesktopMainEventOpenFavorite, |  | ||||||
|     DesktopMainEventOpenMenu, |  | ||||||
|     DesktopMainEventOpenDebug, |  | ||||||
|     DesktopMainEventUnlocked, |  | ||||||
|     DesktopMainEventRightShort, |  | ||||||
|     DesktopMainEventUpdateAnimation, |  | ||||||
|     DesktopMainEventUpdateOneShotAnimation, |  | ||||||
| } DesktopMainEvent; |  | ||||||
| 
 | 
 | ||||||
| typedef struct DesktopMainView DesktopMainView; | typedef struct DesktopMainView DesktopMainView; | ||||||
| 
 | 
 | ||||||
| typedef void (*DesktopMainViewCallback)(DesktopMainEvent event, void* context); | typedef void (*DesktopMainViewCallback)(DesktopEvent event, void* context); | ||||||
| 
 | 
 | ||||||
| struct DesktopMainView { | struct DesktopMainView { | ||||||
|     View* view; |     View* view; | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
| #include "furi/pubsub.h" | #include "furi/pubsub.h" | ||||||
|  | #include "gui/view.h" | ||||||
| #include "helpers/dolphin_deed.h" | #include "helpers/dolphin_deed.h" | ||||||
| #include <stdbool.h> | #include <stdbool.h> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -11,6 +11,9 @@ typedef enum { | |||||||
|     DolphinEventTypeDeed, |     DolphinEventTypeDeed, | ||||||
|     DolphinEventTypeStats, |     DolphinEventTypeStats, | ||||||
|     DolphinEventTypeFlush, |     DolphinEventTypeFlush, | ||||||
|  |     DolphinEventTypeAnimationStartNewIdle, | ||||||
|  |     DolphinEventTypeAnimationCheckBlocking, | ||||||
|  |     DolphinEventTypeAnimationInteract, | ||||||
| } DolphinEventType; | } DolphinEventType; | ||||||
| 
 | 
 | ||||||
| typedef struct { | typedef struct { | ||||||
|  | |||||||
| @ -348,6 +348,140 @@ void elements_bubble(Canvas* canvas, uint8_t x, uint8_t y, uint8_t width, uint8_ | |||||||
|     canvas_set_color(canvas, ColorBlack); |     canvas_set_color(canvas, ColorBlack); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void elements_bubble_str( | ||||||
|  |     Canvas* canvas, | ||||||
|  |     uint8_t x, | ||||||
|  |     uint8_t y, | ||||||
|  |     const char* text, | ||||||
|  |     Align horizontal, | ||||||
|  |     Align vertical) { | ||||||
|  |     furi_assert(canvas); | ||||||
|  |     furi_assert(text); | ||||||
|  | 
 | ||||||
|  |     uint8_t font_y = canvas_current_font_height(canvas); | ||||||
|  |     uint16_t str_width = canvas_string_width(canvas, text); | ||||||
|  |     // count \n's
 | ||||||
|  |     uint8_t lines = 1; | ||||||
|  |     const char* t = text; | ||||||
|  |     while(*t != '\0') { | ||||||
|  |         if(*t == '\n') { | ||||||
|  |             lines++; | ||||||
|  |             uint16_t temp_width = canvas_string_width(canvas, t + 1); | ||||||
|  |             str_width = temp_width > str_width ? temp_width : str_width; | ||||||
|  |         } | ||||||
|  |         t++; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     uint8_t frame_x = x; | ||||||
|  |     uint8_t frame_y = y; | ||||||
|  |     uint8_t frame_width = str_width + 8; | ||||||
|  |     uint8_t frame_height = font_y * lines + 4; | ||||||
|  | 
 | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_box(canvas, frame_x + 1, frame_y + 1, frame_width - 2, frame_height - 2); | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     canvas_draw_rframe(canvas, frame_x, frame_y, frame_width, frame_height, 1); | ||||||
|  |     elements_multiline_text(canvas, x + 4, y - 1 + font_y, text); | ||||||
|  | 
 | ||||||
|  |     uint8_t x1 = 0; | ||||||
|  |     uint8_t x2 = 0; | ||||||
|  |     uint8_t x3 = 0; | ||||||
|  |     uint8_t y1 = 0; | ||||||
|  |     uint8_t y2 = 0; | ||||||
|  |     uint8_t y3 = 0; | ||||||
|  |     if((horizontal == AlignLeft) && (vertical == AlignTop)) { | ||||||
|  |         x1 = frame_x; | ||||||
|  |         y1 = frame_y; | ||||||
|  |         x2 = frame_x - 4; | ||||||
|  |         y2 = frame_y; | ||||||
|  |         x3 = frame_x; | ||||||
|  |         y3 = frame_y + 4; | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 + 2, y2 + 1, 2, 2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignLeft) && (vertical == AlignCenter)) { | ||||||
|  |         x1 = frame_x; | ||||||
|  |         y1 = frame_y + (frame_height - 1) / 2 - 4; | ||||||
|  |         x2 = frame_x - 4; | ||||||
|  |         y2 = frame_y + (frame_height - 1) / 2; | ||||||
|  |         x3 = frame_x; | ||||||
|  |         y3 = frame_y + (frame_height - 1) / 2 + 4; | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 + 2, y2 - 2, 2, 5); | ||||||
|  |         canvas_draw_dot(canvas, x2 + 1, y2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignLeft) && (vertical == AlignBottom)) { | ||||||
|  |         x1 = frame_x; | ||||||
|  |         y1 = frame_y + (frame_height - 1) - 4; | ||||||
|  |         x2 = frame_x - 4; | ||||||
|  |         y2 = frame_y + (frame_height - 1); | ||||||
|  |         x3 = frame_x; | ||||||
|  |         y3 = frame_y + (frame_height - 1); | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 + 2, y2 - 2, 2, 2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignRight) && (vertical == AlignTop)) { | ||||||
|  |         x1 = frame_x + (frame_width - 1); | ||||||
|  |         y1 = frame_y; | ||||||
|  |         x2 = frame_x + (frame_width - 1) + 4; | ||||||
|  |         y2 = frame_y; | ||||||
|  |         x3 = frame_x + (frame_width - 1); | ||||||
|  |         y3 = frame_y + 4; | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 - 3, y2 + 1, 2, 2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignRight) && (vertical == AlignCenter)) { | ||||||
|  |         x1 = frame_x + (frame_width - 1); | ||||||
|  |         y1 = frame_y + (frame_height - 1) / 2 - 4; | ||||||
|  |         x2 = frame_x + (frame_width - 1) + 4; | ||||||
|  |         y2 = frame_y + (frame_height - 1) / 2; | ||||||
|  |         x3 = frame_x + (frame_width - 1); | ||||||
|  |         y3 = frame_y + (frame_height - 1) / 2 + 4; | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 - 3, y2 - 2, 2, 5); | ||||||
|  |         canvas_draw_dot(canvas, x2 - 1, y2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignRight) && (vertical == AlignBottom)) { | ||||||
|  |         x1 = frame_x + (frame_width - 1); | ||||||
|  |         y1 = frame_y + (frame_height - 1) - 4; | ||||||
|  |         x2 = frame_x + (frame_width - 1) + 4; | ||||||
|  |         y2 = frame_y + (frame_height - 1); | ||||||
|  |         x3 = frame_x + (frame_width - 1); | ||||||
|  |         y3 = frame_y + (frame_height - 1); | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 - 3, y2 - 2, 2, 2); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignCenter) && (vertical == AlignTop)) { | ||||||
|  |         x1 = frame_x + (frame_width - 1) / 2 - 4; | ||||||
|  |         y1 = frame_y; | ||||||
|  |         x2 = frame_x + (frame_width - 1) / 2; | ||||||
|  |         y2 = frame_y - 4; | ||||||
|  |         x3 = frame_x + (frame_width - 1) / 2 + 4; | ||||||
|  |         y3 = frame_y; | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 - 2, y2 + 2, 5, 2); | ||||||
|  |         canvas_draw_dot(canvas, x2, y2 + 1); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } else if((horizontal == AlignCenter) && (vertical == AlignBottom)) { | ||||||
|  |         x1 = frame_x + (frame_width - 1) / 2 - 4; | ||||||
|  |         y1 = frame_y + (frame_height - 1); | ||||||
|  |         x2 = frame_x + (frame_width - 1) / 2; | ||||||
|  |         y2 = frame_y + (frame_height - 1) + 4; | ||||||
|  |         x3 = frame_x + (frame_width - 1) / 2 + 4; | ||||||
|  |         y3 = frame_y + (frame_height - 1); | ||||||
|  |         canvas_set_color(canvas, ColorWhite); | ||||||
|  |         canvas_draw_box(canvas, x2 - 2, y2 - 3, 5, 2); | ||||||
|  |         canvas_draw_dot(canvas, x2, y2 - 1); | ||||||
|  |         canvas_set_color(canvas, ColorBlack); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     canvas_set_color(canvas, ColorWhite); | ||||||
|  |     canvas_draw_line(canvas, x3, y3, x1, y1); | ||||||
|  |     canvas_set_color(canvas, ColorBlack); | ||||||
|  |     canvas_draw_line(canvas, x1, y1, x2, y2); | ||||||
|  |     canvas_draw_line(canvas, x2, y2, x3, y3); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void elements_string_fit_width(Canvas* canvas, string_t string, uint8_t width) { | void elements_string_fit_width(Canvas* canvas, string_t string, uint8_t width) { | ||||||
|     furi_assert(canvas); |     furi_assert(canvas); | ||||||
|     furi_assert(string); |     furi_assert(string); | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								applications/gui/elements.h
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						| @ -160,6 +160,24 @@ void elements_slightly_rounded_box( | |||||||
|  */ |  */ | ||||||
| void elements_bubble(Canvas* canvas, uint8_t x, uint8_t y, uint8_t width, uint8_t height); | void elements_bubble(Canvas* canvas, uint8_t x, uint8_t y, uint8_t width, uint8_t height); | ||||||
| 
 | 
 | ||||||
|  | /** Draw bubble frame for text with corner
 | ||||||
|  |  * | ||||||
|  |  * @param   canvas      Canvas instance | ||||||
|  |  * @param   x           left x coordinates | ||||||
|  |  * @param   y           top y coordinate | ||||||
|  |  * @param   width       bubble width | ||||||
|  |  * @param   height      bubble height | ||||||
|  |  * @param   horizontal  horizontal aligning | ||||||
|  |  * @param   vertical    aligning | ||||||
|  |  */ | ||||||
|  | void elements_bubble_str( | ||||||
|  |     Canvas* canvas, | ||||||
|  |     uint8_t x, | ||||||
|  |     uint8_t y, | ||||||
|  |     const char* text, | ||||||
|  |     Align horizontal, | ||||||
|  |     Align vertical); | ||||||
|  | 
 | ||||||
| /** Trim string buffer to fit width in pixels
 | /** Trim string buffer to fit width in pixels
 | ||||||
|  * |  * | ||||||
|  * @param   canvas  Canvas instance |  * @param   canvas  Canvas instance | ||||||
|  | |||||||
							
								
								
									
										166
									
								
								applications/gui/view_composed.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,166 @@ | |||||||
|  | #include "gui/view.h" | ||||||
|  | #include "furi/memmgr.h" | ||||||
|  | #include "view_composed.h" | ||||||
|  | #include "view_i.h" | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     View* bottom; | ||||||
|  |     View* top; | ||||||
|  |     bool top_enabled; | ||||||
|  | } ViewComposedModel; | ||||||
|  | 
 | ||||||
|  | struct ViewComposed { | ||||||
|  |     View* view; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static void view_composed_draw(Canvas* canvas, void* model); | ||||||
|  | static bool view_composed_input(InputEvent* event, void* context); | ||||||
|  | 
 | ||||||
|  | static void view_composed_update_callback(View* view_top_or_bottom, void* context) { | ||||||
|  |     furi_assert(view_top_or_bottom); | ||||||
|  |     furi_assert(context); | ||||||
|  | 
 | ||||||
|  |     View* view_composed_view = context; | ||||||
|  |     view_composed_view->update_callback( | ||||||
|  |         view_composed_view, view_composed_view->update_callback_context); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void view_composed_enter(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  | 
 | ||||||
|  |     ViewComposed* view_composed = context; | ||||||
|  |     ViewComposedModel* model = view_get_model(view_composed->view); | ||||||
|  | 
 | ||||||
|  |     /* if more than 1 composed views hold same view it has to reassign update_callback_context */ | ||||||
|  |     if(model->bottom) { | ||||||
|  |         view_set_update_callback_context(model->bottom, view_composed->view); | ||||||
|  |         if(model->bottom->enter_callback) { | ||||||
|  |             model->bottom->enter_callback(model->bottom->context); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if(model->top) { | ||||||
|  |         view_set_update_callback_context(model->top, view_composed->view); | ||||||
|  |         if(model->top->enter_callback) { | ||||||
|  |             model->top->enter_callback(model->top->context); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(view_composed->view, false); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void view_composed_exit(void* context) { | ||||||
|  |     furi_assert(context); | ||||||
|  | 
 | ||||||
|  |     ViewComposed* view_composed = context; | ||||||
|  |     ViewComposedModel* model = view_get_model(view_composed->view); | ||||||
|  | 
 | ||||||
|  |     if(model->bottom) { | ||||||
|  |         if(model->bottom->exit_callback) { | ||||||
|  |             model->bottom->exit_callback(model->bottom->context); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if(model->top) { | ||||||
|  |         if(model->top->exit_callback) { | ||||||
|  |             model->top->exit_callback(model->top->context); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(view_composed->view, false); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ViewComposed* view_composed_alloc(void) { | ||||||
|  |     ViewComposed* view_composed = furi_alloc(sizeof(ViewComposed)); | ||||||
|  |     view_composed->view = view_alloc(); | ||||||
|  | 
 | ||||||
|  |     view_allocate_model(view_composed->view, ViewModelTypeLocking, sizeof(ViewComposedModel)); | ||||||
|  |     view_set_draw_callback(view_composed->view, view_composed_draw); | ||||||
|  |     view_set_input_callback(view_composed->view, view_composed_input); | ||||||
|  |     view_set_context(view_composed->view, view_composed); | ||||||
|  |     view_set_enter_callback(view_composed->view, view_composed_enter); | ||||||
|  |     view_set_exit_callback(view_composed->view, view_composed_exit); | ||||||
|  |     return view_composed; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void view_composed_free(ViewComposed* view_composed) { | ||||||
|  |     furi_assert(view_composed); | ||||||
|  | 
 | ||||||
|  |     ViewComposedModel* view_composed_model = view_get_model(view_composed->view); | ||||||
|  |     view_set_update_callback(view_composed_model->bottom, NULL); | ||||||
|  |     view_set_update_callback_context(view_composed_model->bottom, NULL); | ||||||
|  |     view_set_update_callback(view_composed_model->top, NULL); | ||||||
|  |     view_set_update_callback_context(view_composed_model->top, NULL); | ||||||
|  |     view_commit_model(view_composed->view, true); | ||||||
|  | 
 | ||||||
|  |     view_free(view_composed->view); | ||||||
|  |     free(view_composed); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void view_composed_draw(Canvas* canvas, void* model) { | ||||||
|  |     furi_assert(model); | ||||||
|  | 
 | ||||||
|  |     ViewComposedModel* view_composed_model = model; | ||||||
|  | 
 | ||||||
|  |     view_draw(view_composed_model->bottom, canvas); | ||||||
|  |     if(view_composed_model->top_enabled && view_composed_model->top) { | ||||||
|  |         view_draw(view_composed_model->top, canvas); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool view_composed_input(InputEvent* event, void* context) { | ||||||
|  |     furi_assert(event); | ||||||
|  |     furi_assert(context); | ||||||
|  | 
 | ||||||
|  |     ViewComposed* view_composed = context; | ||||||
|  |     ViewComposedModel* view_composed_model = view_get_model(view_composed->view); | ||||||
|  |     bool consumed = false; | ||||||
|  | 
 | ||||||
|  |     if(view_composed_model->top_enabled && view_composed_model->top) { | ||||||
|  |         consumed = view_input(view_composed_model->top, event); | ||||||
|  |     } | ||||||
|  |     if(!consumed) { | ||||||
|  |         consumed = view_input(view_composed_model->bottom, event); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_commit_model(view_composed->view, false); | ||||||
|  | 
 | ||||||
|  |     return consumed; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void view_composed_top_enable(ViewComposed* view_composed, bool enable) { | ||||||
|  |     furi_assert(view_composed); | ||||||
|  | 
 | ||||||
|  |     ViewComposedModel* view_composed_model = view_get_model(view_composed->view); | ||||||
|  |     bool update = (view_composed_model->top_enabled != enable); | ||||||
|  |     view_composed_model->top_enabled = enable; | ||||||
|  |     view_commit_model(view_composed->view, update); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void view_composed_tie_views(ViewComposed* view_composed, View* view_bottom, View* view_top) { | ||||||
|  |     furi_assert(view_composed); | ||||||
|  |     furi_assert(view_bottom); | ||||||
|  | 
 | ||||||
|  |     ViewComposedModel* view_composed_model = view_get_model(view_composed->view); | ||||||
|  | 
 | ||||||
|  |     if(view_composed_model->bottom) { | ||||||
|  |         view_set_update_callback(view_composed_model->bottom, NULL); | ||||||
|  |         view_set_update_callback_context(view_composed_model->bottom, NULL); | ||||||
|  |     } | ||||||
|  |     if(view_composed_model->top) { | ||||||
|  |         view_set_update_callback(view_composed_model->top, NULL); | ||||||
|  |         view_set_update_callback_context(view_composed_model->top, NULL); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     view_composed_model->bottom = view_bottom; | ||||||
|  |     view_set_update_callback(view_bottom, view_composed_update_callback); | ||||||
|  |     view_set_update_callback_context(view_bottom, view_composed->view); | ||||||
|  |     view_composed_model->top = view_top; | ||||||
|  |     view_set_update_callback(view_top, view_composed_update_callback); | ||||||
|  |     view_set_update_callback_context(view_top, view_composed->view); | ||||||
|  | 
 | ||||||
|  |     view_commit_model(view_composed->view, true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | View* view_composed_get_view(ViewComposed* view_composed) { | ||||||
|  |     furi_assert(view_composed); | ||||||
|  |     return view_composed->view; | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								applications/gui/view_composed.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <stdbool.h> | ||||||
|  | #include "view.h" | ||||||
|  | 
 | ||||||
|  | typedef struct ViewComposed ViewComposed; | ||||||
|  | 
 | ||||||
|  | ViewComposed* view_composed_alloc(void); | ||||||
|  | void view_composed_free(ViewComposed* view_composed); | ||||||
|  | void view_composed_top_enable(ViewComposed* view_composed, bool enable); | ||||||
|  | void view_composed_tie_views(ViewComposed* view_composed, View* view_bottom, View* view_top); | ||||||
|  | View* view_composed_get_view(ViewComposed* view_composed); | ||||||
| @ -188,7 +188,7 @@ void view_dispatcher_send_to_front(ViewDispatcher* view_dispatcher) { | |||||||
| void view_dispatcher_send_to_back(ViewDispatcher* view_dispatcher) { | void view_dispatcher_send_to_back(ViewDispatcher* view_dispatcher) { | ||||||
|     furi_assert(view_dispatcher); |     furi_assert(view_dispatcher); | ||||||
|     furi_assert(view_dispatcher->gui); |     furi_assert(view_dispatcher->gui); | ||||||
|     gui_view_port_send_to_front(view_dispatcher->gui, view_dispatcher->view_port); |     gui_view_port_send_to_back(view_dispatcher->gui, view_dispatcher->view_port); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void view_dispatcher_attach_to_gui( | void view_dispatcher_attach_to_gui( | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | #include <furi/pubsub.h> | ||||||
| #include "loader/loader.h" | #include "loader/loader.h" | ||||||
| #include "loader_i.h" | #include "loader_i.h" | ||||||
| 
 | 
 | ||||||
| @ -21,6 +22,7 @@ static void loader_menu_callback(void* _ctx, uint32_t index) { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     furi_hal_power_insomnia_enter(); |     furi_hal_power_insomnia_enter(); | ||||||
|  | 
 | ||||||
|     loader_instance->current_app = flipper_app; |     loader_instance->current_app = flipper_app; | ||||||
| 
 | 
 | ||||||
|     FURI_LOG_I(TAG, "Starting: %s", loader_instance->current_app->name); |     FURI_LOG_I(TAG, "Starting: %s", loader_instance->current_app->name); | ||||||
| @ -228,9 +230,12 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con | |||||||
|     furi_assert(context); |     furi_assert(context); | ||||||
| 
 | 
 | ||||||
|     Loader* instance = context; |     Loader* instance = context; | ||||||
|  |     LoaderEvent event; | ||||||
| 
 | 
 | ||||||
|     if(thread_state == FuriThreadStateRunning) { |     if(thread_state == FuriThreadStateRunning) { | ||||||
|         instance->free_heap_size = memmgr_get_free_heap(); |         instance->free_heap_size = memmgr_get_free_heap(); | ||||||
|  |         event.type = LoaderEventTypeApplicationStarted; | ||||||
|  |         furi_pubsub_publish(loader_instance->pubsub, &event); | ||||||
|     } else if(thread_state == FuriThreadStateStopped) { |     } else if(thread_state == FuriThreadStateStopped) { | ||||||
|         /*
 |         /*
 | ||||||
|          * Current Leak Sanitizer assumes that memory is allocated and freed |          * Current Leak Sanitizer assumes that memory is allocated and freed | ||||||
| @ -251,6 +256,8 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con | |||||||
|             furi_thread_get_heap_size(instance->thread)); |             furi_thread_get_heap_size(instance->thread)); | ||||||
|         furi_hal_power_insomnia_exit(); |         furi_hal_power_insomnia_exit(); | ||||||
|         loader_unlock(instance); |         loader_unlock(instance); | ||||||
|  |         event.type = LoaderEventTypeApplicationStopped; | ||||||
|  |         furi_pubsub_publish(loader_instance->pubsub, &event); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -275,6 +282,7 @@ static Loader* loader_alloc() { | |||||||
| 
 | 
 | ||||||
|     string_init(instance->args); |     string_init(instance->args); | ||||||
| 
 | 
 | ||||||
|  |     instance->pubsub = furi_pubsub_alloc(); | ||||||
|     instance->mutex = osMutexNew(NULL); |     instance->mutex = osMutexNew(NULL); | ||||||
| 
 | 
 | ||||||
| #ifdef SRV_CLI | #ifdef SRV_CLI | ||||||
| @ -334,6 +342,8 @@ static void loader_free(Loader* instance) { | |||||||
| 
 | 
 | ||||||
|     osMutexDelete(instance->mutex); |     osMutexDelete(instance->mutex); | ||||||
| 
 | 
 | ||||||
|  |     furi_pubsub_free(instance->pubsub); | ||||||
|  | 
 | ||||||
|     string_clear(instance->args); |     string_clear(instance->args); | ||||||
| 
 | 
 | ||||||
|     furi_thread_free(instance->thread); |     furi_thread_free(instance->thread); | ||||||
| @ -471,3 +481,7 @@ int32_t loader_srv(void* p) { | |||||||
| 
 | 
 | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | FuriPubSub* loader_get_pubsub() { | ||||||
|  |     return loader_instance->pubsub; | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include <furi/pubsub.h> | ||||||
| #include <stdbool.h> | #include <stdbool.h> | ||||||
| 
 | 
 | ||||||
| typedef struct Loader Loader; | typedef struct Loader Loader; | ||||||
| @ -11,6 +12,15 @@ typedef enum { | |||||||
|     LoaderStatusErrorInternal, |     LoaderStatusErrorInternal, | ||||||
| } LoaderStatus; | } LoaderStatus; | ||||||
| 
 | 
 | ||||||
|  | typedef enum { | ||||||
|  |     LoaderEventTypeApplicationStarted, | ||||||
|  |     LoaderEventTypeApplicationStopped | ||||||
|  | } LoaderEventType; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     LoaderEventType type; | ||||||
|  | } LoaderEvent; | ||||||
|  | 
 | ||||||
| /** Start application
 | /** Start application
 | ||||||
|  * @param name - application name |  * @param name - application name | ||||||
|  * @param args - application arguments |  * @param args - application arguments | ||||||
| @ -34,3 +44,6 @@ void loader_show_menu(); | |||||||
| 
 | 
 | ||||||
| /** Show primary loader */ | /** Show primary loader */ | ||||||
| void loader_update_menu(); | void loader_update_menu(); | ||||||
|  | 
 | ||||||
|  | /** Show primary loader */ | ||||||
|  | FuriPubSub* loader_get_pubsub(); | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| #include <furi-hal.h> | #include <furi-hal.h> | ||||||
|  | #include <furi/pubsub.h> | ||||||
| #include <cli/cli.h> | #include <cli/cli.h> | ||||||
| #include <lib/toolbox/args.h> | #include <lib/toolbox/args.h> | ||||||
| 
 | 
 | ||||||
| @ -30,6 +31,8 @@ struct Loader { | |||||||
|     size_t free_heap_size; |     size_t free_heap_size; | ||||||
|     osMutexId_t mutex; |     osMutexId_t mutex; | ||||||
|     volatile uint8_t lock_semaphore; |     volatile uint8_t lock_semaphore; | ||||||
|  | 
 | ||||||
|  |     FuriPubSub* pubsub; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| typedef enum { | typedef enum { | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ | |||||||
| #include <pb_decode.h> | #include <pb_decode.h> | ||||||
| #include <pb_encode.h> | #include <pb_encode.h> | ||||||
| 
 | 
 | ||||||
| #include <status.pb.h> |  | ||||||
| #include <storage.pb.h> | #include <storage.pb.h> | ||||||
| #include <flipper.pb.h> | #include <flipper.pb.h> | ||||||
| #include <portmacro.h> | #include <portmacro.h> | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| #include "flipper.pb.h" | #include "flipper.pb.h" | ||||||
| #include "furi/record.h" | #include "furi/record.h" | ||||||
| #include "status.pb.h" |  | ||||||
| #include "rpc_i.h" | #include "rpc_i.h" | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| #include <loader/loader.h> | #include <loader/loader.h> | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| #include "flipper.pb.h" | #include "flipper.pb.h" | ||||||
| #include "rpc_i.h" | #include "rpc_i.h" | ||||||
| #include "status.pb.h" |  | ||||||
| 
 | 
 | ||||||
| #include <furi-hal.h> | #include <furi-hal.h> | ||||||
| #include <power/power_service/power.h> | #include <power/power_service/power.h> | ||||||
|  | |||||||
| @ -2,20 +2,35 @@ PROJECT_ROOT		= $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..) | |||||||
| 
 | 
 | ||||||
| include				$(PROJECT_ROOT)/assets/assets.mk | include				$(PROJECT_ROOT)/assets/assets.mk | ||||||
| 
 | 
 | ||||||
| all: icons protobuf | .PHONY: all | ||||||
|  | all: icons protobuf dolphin | ||||||
| 
 | 
 | ||||||
| $(ASSETS): $(ASSETS_SOURCES) $(ASSETS_COMPILLER) | $(ASSETS): $(ASSETS_SOURCES) $(ASSETS_COMPILLER) | ||||||
| 	@echo "\tASSETS\t\t" $@ | 	@echo "\tASSETS\t\t" $@ | ||||||
| 	@$(ASSETS_COMPILLER) icons "$(ASSETS_SOURCE_DIR)" "$(ASSETS_COMPILED_DIR)" | 	@$(ASSETS_COMPILLER) icons "$(ASSETS_SOURCE_DIR)" "$(ASSETS_COMPILED_DIR)" | ||||||
| 
 | 
 | ||||||
|  | .PHONY: icons | ||||||
| icons: $(ASSETS) | icons: $(ASSETS) | ||||||
| 
 | 
 | ||||||
| $(PROTOBUF) &: $(PROTOBUF_SOURCES) $(PROTOBUF_COMPILER) | $(PROTOBUF) &: $(PROTOBUF_SOURCES) $(PROTOBUF_COMPILER) | ||||||
| 	@echo "\tPROTOBUF\t" $(PROTOBUF_FILENAMES) | 	@echo "\tPROTOBUF\t" $(PROTOBUF_FILENAMES) | ||||||
| 	@$(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py -q -I$(PROTOBUF_SOURCE_DIR) -D$(PROTOBUF_COMPILED_DIR) $(PROTOBUF_SOURCES) | 	@$(PROTOBUF_COMPILER) -q -I$(PROTOBUF_SOURCE_DIR) -D$(PROTOBUF_COMPILED_DIR) $(PROTOBUF_SOURCES) | ||||||
| 
 | 
 | ||||||
|  | .PHONY: protobuf | ||||||
| protobuf: $(PROTOBUF) | protobuf: $(PROTOBUF) | ||||||
| 
 | 
 | ||||||
|  | $(PROTOBUF_FILE_ANIMATIONS): $(PROTOBUF_SOURCES_FILE_ANIMATIONS) $(PROTOBUF_COMPILER) | ||||||
|  | 	@echo "\tFILE ANIMATIONS\t" $(PROTOBUF_FILE_ANIMATIONS_FILENAMES) | ||||||
|  | 	@$(PROTOBUF_COMPILER) -q -I$(PROTOBUF_FILE_ANIMATIONS_SOURCE_DIR) -D$(PROTOBUF_FILE_ANIMATIONS_COMPILED_DIR) $(PROTOBUF_FILE_ANIMATIONS_SOURCES) | ||||||
|  | 
 | ||||||
|  | $(DOLPHIN_OUTPUT_DIR): $(DOLPHIN_SOURCE_DIR) | ||||||
|  | 	@echo "\tDOLPHIN" | ||||||
|  | 	@$(ASSETS_COMPILLER) dolphin "$(DOLPHIN_SOURCE_DIR)" "$(DOLPHIN_OUTPUT_DIR)" | ||||||
|  | 
 | ||||||
|  | .PHONY: dolphin | ||||||
|  | dolphin: $(DOLPHIN_OUTPUT_DIR) | ||||||
|  | 
 | ||||||
| clean: | clean: | ||||||
| 	@echo "\tCLEAN\t" | 	@echo "\tCLEAN\t" | ||||||
| 	@$(RM) $(ASSETS_COMPILED_DIR)/* | 	@$(RM) $(ASSETS_COMPILED_DIR)/* | ||||||
|  | 	@$(RM) -rf $(DOLPHIN_OUTPUT_DIR) | ||||||
|  | |||||||
| @ -23,6 +23,11 @@ make all | |||||||
| Image names will be automatically prefixed with `I_`, animation names with `A_`. | Image names will be automatically prefixed with `I_`, animation names with `A_`. | ||||||
| Icons and Animations will be gathered into `icon.h` and `icon.c`. | Icons and Animations will be gathered into `icon.h` and `icon.c`. | ||||||
| 
 | 
 | ||||||
|  | ## Dolphin and Games assets | ||||||
|  | 
 | ||||||
|  | Rules are same as for Images and Animations plus assets are grouped by level and level prepends `NAME`. | ||||||
|  | Good starting point: https://docs.unrealengine.com/4.27/en-US/ProductionPipelines/AssetNaming/ | ||||||
|  | 
 | ||||||
| # Important notes | # Important notes | ||||||
| 
 | 
 | ||||||
| Don't include assets that you are not using, compiler is not going to strip unused assets. | Don't include assets that you are not using, compiler is not going to strip unused assets. | ||||||
|  | |||||||
| @ -6,11 +6,13 @@ ASSETS_SOURCE_DIR	:= $(ASSETS_DIR)/icons | |||||||
| ASSETS_SOURCES		+= $(shell find $(ASSETS_SOURCE_DIR) -type f -iname '*.png' -or -iname 'frame_rate') | ASSETS_SOURCES		+= $(shell find $(ASSETS_SOURCE_DIR) -type f -iname '*.png' -or -iname 'frame_rate') | ||||||
| ASSETS				+= $(ASSETS_COMPILED_DIR)/assets_icons.c | ASSETS				+= $(ASSETS_COMPILED_DIR)/assets_icons.c | ||||||
| 
 | 
 | ||||||
|  | DOLPHIN_SOURCE_DIR	:= $(ASSETS_DIR)/dolphin | ||||||
|  | DOLPHIN_OUTPUT_DIR	:= $(ASSETS_DIR)/resources/dolphin | ||||||
|  | 
 | ||||||
| PROTOBUF_SOURCE_DIR		:= $(ASSETS_DIR)/protobuf | PROTOBUF_SOURCE_DIR		:= $(ASSETS_DIR)/protobuf | ||||||
| PROTOBUF_COMPILER		:= $(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py | PROTOBUF_COMPILER		:= $(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py | ||||||
| PROTOBUF_COMPILED_DIR	:= $(ASSETS_COMPILED_DIR) | PROTOBUF_COMPILED_DIR	:= $(ASSETS_COMPILED_DIR) | ||||||
| PROTOBUF_SOURCES		:= $(shell find $(PROTOBUF_SOURCE_DIR) -type f -iname '*.proto') | PROTOBUF_SOURCES		:= $(shell find $(PROTOBUF_SOURCE_DIR) -type f -iname '*.proto') | ||||||
| #PROTOBUF_FILENAMES		:= $(notdir $(PROTOBUF))
 |  | ||||||
| PROTOBUF_FILENAMES		:= $(notdir $(addsuffix .pb.c,$(basename $(PROTOBUF_SOURCES)))) | PROTOBUF_FILENAMES		:= $(notdir $(addsuffix .pb.c,$(basename $(PROTOBUF_SOURCES)))) | ||||||
| PROTOBUF				:= $(addprefix $(PROTOBUF_COMPILED_DIR)/,$(PROTOBUF_FILENAMES)) | PROTOBUF				:= $(addprefix $(PROTOBUF_COMPILED_DIR)/,$(PROTOBUF_FILENAMES)) | ||||||
| PROTOBUF_CFLAGS			+= -DPB_ENABLE_MALLOC | PROTOBUF_CFLAGS			+= -DPB_ENABLE_MALLOC | ||||||
|  | |||||||
| @ -1,145 +1,118 @@ | |||||||
| #pragma once | #pragma once | ||||||
| #include <gui/icon.h> | #include <gui/icon.h> | ||||||
| 
 | 
 | ||||||
| extern const Icon I_Certification2_119x30; |  | ||||||
| extern const Icon I_Certification1_103x23; | extern const Icon I_Certification1_103x23; | ||||||
| extern const Icon A_BadBattery_128x51; | extern const Icon I_Certification2_119x30; | ||||||
| extern const Icon A_BoxActive_128x51; | extern const Icon I_card_bad1; | ||||||
| extern const Icon A_Box_128x51; | extern const Icon I_card_bad2; | ||||||
| extern const Icon A_CardBad_128x51; | extern const Icon I_card_ok1; | ||||||
| extern const Icon A_CardNoDBUrl_128x51; | extern const Icon I_card_ok2; | ||||||
| extern const Icon A_CardNoDB_128x51; | extern const Icon I_card_ok3; | ||||||
| extern const Icon A_CardOk_128x51; | extern const Icon I_card_ok4; | ||||||
| extern const Icon A_CryActive_128x51; | extern const Icon I_no_databases1; | ||||||
| extern const Icon A_Cry_128x51; | extern const Icon I_no_databases2; | ||||||
| extern const Icon A_KnifeActive_128x51; | extern const Icon I_no_databases3; | ||||||
| extern const Icon A_Knife_128x51; | extern const Icon I_no_databases4; | ||||||
| extern const Icon A_LaptopActive_128x52; | extern const Icon I_no_sd1; | ||||||
| extern const Icon A_Laptop_128x52; | extern const Icon I_no_sd2; | ||||||
| extern const Icon A_LeavingActive_128x51; | extern const Icon I_no_sd3; | ||||||
| extern const Icon A_Leaving_128x51; | extern const Icon I_no_sd4; | ||||||
| extern const Icon A_Level1FurippaActive_128x51; | extern const Icon I_no_sd5; | ||||||
| extern const Icon A_Level1Furippa_128x51; | extern const Icon I_no_sd6; | ||||||
| extern const Icon A_Level1ReadActive_128x51; | extern const Icon I_tv1; | ||||||
| extern const Icon A_Level1Read_128x51; | extern const Icon I_tv2; | ||||||
| extern const Icon A_Level1ToysActive_128x51; | extern const Icon I_tv3; | ||||||
| extern const Icon A_Level1Toys_128x51; | extern const Icon I_tv4; | ||||||
| extern const Icon A_Level2FurippaActive_128x51; | extern const Icon I_tv5; | ||||||
| extern const Icon A_Level2Furippa_128x51; | extern const Icon I_tv6; | ||||||
| extern const Icon A_Level2HackActive_128x51; | extern const Icon I_tv7; | ||||||
| extern const Icon A_Level2Hack_128x51; | extern const Icon I_tv8; | ||||||
| extern const Icon A_Level2SolderingActive_128x51; | extern const Icon I_url1; | ||||||
| extern const Icon A_Level2Soldering_128x51; | extern const Icon I_url2; | ||||||
| extern const Icon A_Level3FurippaActive_128x51; | extern const Icon I_url3; | ||||||
| extern const Icon A_Level3Furippa_128x51; | extern const Icon I_url4; | ||||||
| extern const Icon A_Level3HijackActive_128x51; |  | ||||||
| extern const Icon A_Level3Hijack_128x51; |  | ||||||
| extern const Icon A_Level3LabActive_128x51; |  | ||||||
| extern const Icon A_Level3Lab_128x51; |  | ||||||
| extern const Icon I_LevelUp2_03; |  | ||||||
| extern const Icon I_LevelUp2_02; |  | ||||||
| extern const Icon I_LevelUp2_05; |  | ||||||
| extern const Icon I_LevelUp2_04; |  | ||||||
| extern const Icon I_LevelUp2_01; |  | ||||||
| extern const Icon I_LevelUp2_06; |  | ||||||
| extern const Icon I_LevelUp2_07; |  | ||||||
| extern const Icon I_LevelUp3_05; |  | ||||||
| extern const Icon I_LevelUp3_06; |  | ||||||
| extern const Icon I_LevelUp3_02; |  | ||||||
| extern const Icon I_LevelUp3_07; |  | ||||||
| extern const Icon I_LevelUp3_04; |  | ||||||
| extern const Icon I_LevelUp3_03; |  | ||||||
| extern const Icon I_LevelUp3_01; |  | ||||||
| extern const Icon A_LevelUpPending_128x51; |  | ||||||
| extern const Icon A_NoSdCard_128x51; |  | ||||||
| extern const Icon A_SleepActive_128x52; |  | ||||||
| extern const Icon A_Sleep_128x52; |  | ||||||
| extern const Icon A_TvActive_128x52; |  | ||||||
| extern const Icon A_Tv_128x52; |  | ||||||
| extern const Icon A_WavesActive_128x52; |  | ||||||
| extern const Icon A_Waves_128x52; |  | ||||||
| extern const Icon I_ble_10px; |  | ||||||
| extern const Icon I_ibutt_10px; |  | ||||||
| extern const Icon I_125_10px; | extern const Icon I_125_10px; | ||||||
| extern const Icon I_sub1_10px; |  | ||||||
| extern const Icon I_dir_10px; |  | ||||||
| extern const Icon I_ir_10px; |  | ||||||
| extern const Icon I_Nfc_10px; | extern const Icon I_Nfc_10px; | ||||||
|  | extern const Icon I_ble_10px; | ||||||
|  | extern const Icon I_dir_10px; | ||||||
|  | extern const Icon I_ibutt_10px; | ||||||
|  | extern const Icon I_ir_10px; | ||||||
|  | extern const Icon I_sub1_10px; | ||||||
| extern const Icon I_unknown_10px; | extern const Icon I_unknown_10px; | ||||||
| extern const Icon I_BLE_Pairing_128x64; | extern const Icon I_BLE_Pairing_128x64; | ||||||
| extern const Icon I_Volup_8x6; | extern const Icon I_Ble_connected_38x34; | ||||||
| extern const Icon I_Circles_47x47; |  | ||||||
| extern const Icon I_Ble_disconnected_24x34; | extern const Icon I_Ble_disconnected_24x34; | ||||||
| extern const Icon I_Space_65x18; | extern const Icon I_Button_18x18; | ||||||
|  | extern const Icon I_Circles_47x47; | ||||||
| extern const Icon I_Ok_btn_9x9; | extern const Icon I_Ok_btn_9x9; | ||||||
| extern const Icon I_Pressed_Button_13x13; | extern const Icon I_Pressed_Button_13x13; | ||||||
|  | extern const Icon I_Space_65x18; | ||||||
| extern const Icon I_Voldwn_6x6; | extern const Icon I_Voldwn_6x6; | ||||||
| extern const Icon I_Ble_connected_38x34; | extern const Icon I_Volup_8x6; | ||||||
| extern const Icon I_Button_18x18; | extern const Icon I_Clock_18x18; | ||||||
| extern const Icon I_EviSmile2_18x21; | extern const Icon I_Error_18x18; | ||||||
| extern const Icon I_EviSmile1_18x21; | extern const Icon I_EviSmile1_18x21; | ||||||
| extern const Icon I_UsbTree_48x22; | extern const Icon I_EviSmile2_18x21; | ||||||
| extern const Icon I_EviWaiting1_18x21; | extern const Icon I_EviWaiting1_18x21; | ||||||
| extern const Icon I_EviWaiting2_18x21; | extern const Icon I_EviWaiting2_18x21; | ||||||
| extern const Icon I_Percent_10x14; | extern const Icon I_Percent_10x14; | ||||||
| extern const Icon I_Smile_18x18; | extern const Icon I_Smile_18x18; | ||||||
| extern const Icon I_Error_18x18; | extern const Icon I_UsbTree_48x22; | ||||||
| extern const Icon I_Clock_18x18; |  | ||||||
| extern const Icon I_ButtonRightSmall_3x5; |  | ||||||
| extern const Icon I_ButtonLeftSmall_3x5; |  | ||||||
| extern const Icon I_ButtonCenter_7x7; | extern const Icon I_ButtonCenter_7x7; | ||||||
| extern const Icon I_ButtonDown_7x4; | extern const Icon I_ButtonDown_7x4; | ||||||
| extern const Icon I_ButtonRight_4x7; | extern const Icon I_ButtonLeftSmall_3x5; | ||||||
| extern const Icon I_DFU_128x50; |  | ||||||
| extern const Icon I_ButtonUp_7x4; |  | ||||||
| extern const Icon I_Warning_30x23; |  | ||||||
| extern const Icon I_ButtonLeft_4x7; | extern const Icon I_ButtonLeft_4x7; | ||||||
| extern const Icon I_DolphinFirstStart7_61x51; | extern const Icon I_ButtonRightSmall_3x5; | ||||||
| extern const Icon I_DolphinOkay_41x43; | extern const Icon I_ButtonRight_4x7; | ||||||
| extern const Icon I_DolphinFirstStart5_54x49; | extern const Icon I_ButtonUp_7x4; | ||||||
| extern const Icon I_Flipper_young_80x60; | extern const Icon I_DFU_128x50; | ||||||
| extern const Icon I_DolphinFirstStart2_59x51; | extern const Icon I_Warning_30x23; | ||||||
| extern const Icon I_DolphinFirstStart8_56x51; |  | ||||||
| extern const Icon I_DolphinFirstStart3_57x48; |  | ||||||
| extern const Icon I_DolphinFirstStart0_70x53; | extern const Icon I_DolphinFirstStart0_70x53; | ||||||
| extern const Icon I_DolphinFirstStart4_67x53; |  | ||||||
| extern const Icon I_DolphinFirstStart6_58x54; |  | ||||||
| extern const Icon I_DolphinFirstStart1_59x53; | extern const Icon I_DolphinFirstStart1_59x53; | ||||||
|  | extern const Icon I_DolphinFirstStart2_59x51; | ||||||
|  | extern const Icon I_DolphinFirstStart3_57x48; | ||||||
|  | extern const Icon I_DolphinFirstStart4_67x53; | ||||||
|  | extern const Icon I_DolphinFirstStart5_54x49; | ||||||
|  | extern const Icon I_DolphinFirstStart6_58x54; | ||||||
|  | extern const Icon I_DolphinFirstStart7_61x51; | ||||||
|  | extern const Icon I_DolphinFirstStart8_56x51; | ||||||
|  | extern const Icon I_DolphinOkay_41x43; | ||||||
|  | extern const Icon I_Flipper_young_80x60; | ||||||
|  | extern const Icon I_ArrowDownEmpty_14x15; | ||||||
| extern const Icon I_ArrowDownFilled_14x15; | extern const Icon I_ArrowDownFilled_14x15; | ||||||
| extern const Icon I_ArrowUpEmpty_14x15; | extern const Icon I_ArrowUpEmpty_14x15; | ||||||
| extern const Icon I_ArrowUpFilled_14x15; | extern const Icon I_ArrowUpFilled_14x15; | ||||||
| extern const Icon I_ArrowDownEmpty_14x15; | extern const Icon I_DoorLeft_70x55; | ||||||
| extern const Icon I_DoorLocked_10x56; | extern const Icon I_DoorLocked_10x56; | ||||||
|  | extern const Icon I_DoorRight_70x55; | ||||||
|  | extern const Icon I_LockPopup_100x49; | ||||||
| extern const Icon I_PassportBottom_128x17; | extern const Icon I_PassportBottom_128x17; | ||||||
| extern const Icon I_PassportLeft_6x47; | extern const Icon I_PassportLeft_6x47; | ||||||
| extern const Icon I_DoorLeft_70x55; |  | ||||||
| extern const Icon I_LockPopup_100x49; |  | ||||||
| extern const Icon I_DoorRight_70x55; |  | ||||||
| extern const Icon I_IrdaArrowDown_4x8; |  | ||||||
| extern const Icon I_Power_25x27; |  | ||||||
| extern const Icon I_Mute_25x27; |  | ||||||
| extern const Icon I_Down_hvr_25x27; |  | ||||||
| extern const Icon I_Vol_up_25x27; |  | ||||||
| extern const Icon I_IrdaLearnShort_128x31; |  | ||||||
| extern const Icon I_Up_25x27; |  | ||||||
| extern const Icon I_Vol_down_hvr_25x27; |  | ||||||
| extern const Icon I_Vol_down_25x27; |  | ||||||
| extern const Icon I_Vol_up_hvr_25x27; |  | ||||||
| extern const Icon I_Fill_marker_7x7; |  | ||||||
| extern const Icon I_Up_hvr_25x27; |  | ||||||
| extern const Icon I_IrdaArrowUp_4x8; |  | ||||||
| extern const Icon I_Down_25x27; |  | ||||||
| extern const Icon I_DolphinReadingSuccess_59x63; |  | ||||||
| extern const Icon I_IrdaSendShort_128x34; |  | ||||||
| extern const Icon I_IrdaLearn_128x64; |  | ||||||
| extern const Icon I_Mute_hvr_25x27; |  | ||||||
| extern const Icon I_IrdaSend_128x64; |  | ||||||
| extern const Icon I_Power_hvr_25x27; |  | ||||||
| extern const Icon I_Back_15x10; | extern const Icon I_Back_15x10; | ||||||
| extern const Icon I_KeySaveSelected_24x11; | extern const Icon I_DolphinReadingSuccess_59x63; | ||||||
| extern const Icon I_KeySave_24x11; | extern const Icon I_Down_25x27; | ||||||
|  | extern const Icon I_Down_hvr_25x27; | ||||||
|  | extern const Icon I_Fill_marker_7x7; | ||||||
|  | extern const Icon I_IrdaArrowDown_4x8; | ||||||
|  | extern const Icon I_IrdaArrowUp_4x8; | ||||||
|  | extern const Icon I_IrdaLearnShort_128x31; | ||||||
|  | extern const Icon I_IrdaLearn_128x64; | ||||||
|  | extern const Icon I_IrdaSendShort_128x34; | ||||||
|  | extern const Icon I_IrdaSend_128x64; | ||||||
|  | extern const Icon I_Mute_25x27; | ||||||
|  | extern const Icon I_Mute_hvr_25x27; | ||||||
|  | extern const Icon I_Power_25x27; | ||||||
|  | extern const Icon I_Power_hvr_25x27; | ||||||
|  | extern const Icon I_Up_25x27; | ||||||
|  | extern const Icon I_Up_hvr_25x27; | ||||||
|  | extern const Icon I_Vol_down_25x27; | ||||||
|  | extern const Icon I_Vol_down_hvr_25x27; | ||||||
|  | extern const Icon I_Vol_up_25x27; | ||||||
|  | extern const Icon I_Vol_up_hvr_25x27; | ||||||
| extern const Icon I_KeyBackspaceSelected_16x9; | extern const Icon I_KeyBackspaceSelected_16x9; | ||||||
| extern const Icon I_KeyBackspace_16x9; | extern const Icon I_KeyBackspace_16x9; | ||||||
|  | extern const Icon I_KeySaveSelected_24x11; | ||||||
|  | extern const Icon I_KeySave_24x11; | ||||||
| extern const Icon A_125khz_14; | extern const Icon A_125khz_14; | ||||||
| extern const Icon A_BadUsb_14; | extern const Icon A_BadUsb_14; | ||||||
| extern const Icon A_Bluetooth_14; | extern const Icon A_Bluetooth_14; | ||||||
| @ -157,58 +130,58 @@ extern const Icon A_Sub1ghz_14; | |||||||
| extern const Icon A_Tamagotchi_14; | extern const Icon A_Tamagotchi_14; | ||||||
| extern const Icon A_U2F_14; | extern const Icon A_U2F_14; | ||||||
| extern const Icon A_iButton_14; | extern const Icon A_iButton_14; | ||||||
| extern const Icon I_Medium_chip_22x21; |  | ||||||
| extern const Icon I_Detailed_chip_17x13; | extern const Icon I_Detailed_chip_17x13; | ||||||
| extern const Icon I_passport_happy3_46x49; | extern const Icon I_Medium_chip_22x21; | ||||||
| extern const Icon I_passport_bad1_46x49; | extern const Icon I_passport_bad1_46x49; | ||||||
| extern const Icon I_passport_left_6x46; |  | ||||||
| extern const Icon I_passport_bad2_46x49; | extern const Icon I_passport_bad2_46x49; | ||||||
| extern const Icon I_passport_happy1_46x49; |  | ||||||
| extern const Icon I_passport_bottom_128x18; |  | ||||||
| extern const Icon I_passport_okay3_46x49; |  | ||||||
| extern const Icon I_passport_okay2_46x49; |  | ||||||
| extern const Icon I_passport_bad3_46x49; | extern const Icon I_passport_bad3_46x49; | ||||||
| extern const Icon I_passport_okay1_46x49; | extern const Icon I_passport_bottom_128x18; | ||||||
|  | extern const Icon I_passport_happy1_46x49; | ||||||
| extern const Icon I_passport_happy2_46x49; | extern const Icon I_passport_happy2_46x49; | ||||||
| extern const Icon I_Health_16x16; | extern const Icon I_passport_happy3_46x49; | ||||||
| extern const Icon I_Voltage_16x16; | extern const Icon I_passport_left_6x46; | ||||||
|  | extern const Icon I_passport_okay1_46x49; | ||||||
|  | extern const Icon I_passport_okay2_46x49; | ||||||
|  | extern const Icon I_passport_okay3_46x49; | ||||||
| extern const Icon I_BatteryBody_52x28; | extern const Icon I_BatteryBody_52x28; | ||||||
| extern const Icon I_FaceNormal_29x14; |  | ||||||
| extern const Icon I_FaceCharging_29x14; |  | ||||||
| extern const Icon I_Battery_16x16; | extern const Icon I_Battery_16x16; | ||||||
|  | extern const Icon I_FaceCharging_29x14; | ||||||
| extern const Icon I_FaceConfused_29x14; | extern const Icon I_FaceConfused_29x14; | ||||||
| extern const Icon I_Temperature_16x16; |  | ||||||
| extern const Icon I_FaceNopower_29x14; | extern const Icon I_FaceNopower_29x14; | ||||||
| extern const Icon I_RFIDDolphinSuccess_108x57; | extern const Icon I_FaceNormal_29x14; | ||||||
|  | extern const Icon I_Health_16x16; | ||||||
|  | extern const Icon I_Temperature_16x16; | ||||||
|  | extern const Icon I_Voltage_16x16; | ||||||
| extern const Icon I_RFIDBigChip_37x36; | extern const Icon I_RFIDBigChip_37x36; | ||||||
| extern const Icon I_RFIDDolphinReceive_97x61; | extern const Icon I_RFIDDolphinReceive_97x61; | ||||||
| extern const Icon I_RFIDDolphinSend_97x61; | extern const Icon I_RFIDDolphinSend_97x61; | ||||||
|  | extern const Icon I_RFIDDolphinSuccess_108x57; | ||||||
| extern const Icon I_SDError_43x35; | extern const Icon I_SDError_43x35; | ||||||
| extern const Icon I_SDQuestion_35x43; | extern const Icon I_SDQuestion_35x43; | ||||||
| extern const Icon I_Cry_dolph_55x52; | extern const Icon I_Cry_dolph_55x52; | ||||||
| extern const Icon I_Battery_19x8; |  | ||||||
| extern const Icon I_SDcardFail_11x8; |  | ||||||
| extern const Icon I_Bluetooth_5x8; |  | ||||||
| extern const Icon I_PlaceholderR_30x13; |  | ||||||
| extern const Icon I_Battery_26x8; |  | ||||||
| extern const Icon I_Lock_8x8; |  | ||||||
| extern const Icon I_SDcardMounted_11x8; |  | ||||||
| extern const Icon I_Charging_lightning_9x10; |  | ||||||
| extern const Icon I_BadUsb_9x8; |  | ||||||
| extern const Icon I_BT_Pair_9x8; | extern const Icon I_BT_Pair_9x8; | ||||||
| extern const Icon I_Charging_lightning_mask_9x10; |  | ||||||
| extern const Icon I_PlaceholderL_11x13; |  | ||||||
| extern const Icon I_Background_128x11; | extern const Icon I_Background_128x11; | ||||||
|  | extern const Icon I_BadUsb_9x8; | ||||||
|  | extern const Icon I_Battery_19x8; | ||||||
|  | extern const Icon I_Battery_26x8; | ||||||
|  | extern const Icon I_Bluetooth_5x8; | ||||||
|  | extern const Icon I_Charging_lightning_9x10; | ||||||
|  | extern const Icon I_Charging_lightning_mask_9x10; | ||||||
|  | extern const Icon I_Lock_8x8; | ||||||
|  | extern const Icon I_PlaceholderL_11x13; | ||||||
|  | extern const Icon I_PlaceholderR_30x13; | ||||||
|  | extern const Icon I_SDcardFail_11x8; | ||||||
|  | extern const Icon I_SDcardMounted_11x8; | ||||||
| extern const Icon I_USBConnected_15x8; | extern const Icon I_USBConnected_15x8; | ||||||
| extern const Icon I_Quest_7x8; | extern const Icon I_Lock_7x8; | ||||||
| extern const Icon I_MHz_25x11; | extern const Icon I_MHz_25x11; | ||||||
|  | extern const Icon I_Quest_7x8; | ||||||
| extern const Icon I_Scanning_123x52; | extern const Icon I_Scanning_123x52; | ||||||
| extern const Icon I_Unlock_7x8; | extern const Icon I_Unlock_7x8; | ||||||
| extern const Icon I_Lock_7x8; |  | ||||||
| extern const Icon I_DolphinNice_96x59; |  | ||||||
| extern const Icon I_iButtonDolphinSuccess_109x60; |  | ||||||
| extern const Icon I_DolphinExcited_64x63; | extern const Icon I_DolphinExcited_64x63; | ||||||
| extern const Icon I_iButtonKey_49x44; |  | ||||||
| extern const Icon I_iButtonDolphinVerySuccess_108x52; |  | ||||||
| extern const Icon I_DolphinWait_61x59; |  | ||||||
| extern const Icon I_DolphinMafia_115x62; | extern const Icon I_DolphinMafia_115x62; | ||||||
|  | extern const Icon I_DolphinNice_96x59; | ||||||
|  | extern const Icon I_DolphinWait_61x59; | ||||||
|  | extern const Icon I_iButtonDolphinSuccess_109x60; | ||||||
|  | extern const Icon I_iButtonDolphinVerySuccess_108x52; | ||||||
|  | extern const Icon I_iButtonKey_49x44; | ||||||
|  | |||||||
| @ -1,15 +0,0 @@ | |||||||
| /* Automatically generated nanopb constant definitions */ |  | ||||||
| /* Generated by nanopb-0.4.5 */ |  | ||||||
| 
 |  | ||||||
| #include "status.pb.h" |  | ||||||
| #if PB_PROTO_HEADER_VERSION != 40 |  | ||||||
| #error Regenerate this file with the current version of nanopb generator. |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| PB_BIND(PB_Status_PingRequest, PB_Status_PingRequest, AUTO) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| PB_BIND(PB_Status_PingResponse, PB_Status_PingResponse, AUTO) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @ -1,62 +0,0 @@ | |||||||
| /* Automatically generated nanopb header */ |  | ||||||
| /* Generated by nanopb-0.4.5 */ |  | ||||||
| 
 |  | ||||||
| #ifndef PB_PB_STATUS_STATUS_PB_H_INCLUDED |  | ||||||
| #define PB_PB_STATUS_STATUS_PB_H_INCLUDED |  | ||||||
| #include <pb.h> |  | ||||||
| 
 |  | ||||||
| #if PB_PROTO_HEADER_VERSION != 40 |  | ||||||
| #error Regenerate this file with the current version of nanopb generator. |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| /* Struct definitions */ |  | ||||||
| typedef struct _PB_Status_PingRequest {  |  | ||||||
|     pb_bytes_array_t *data;  |  | ||||||
| } PB_Status_PingRequest; |  | ||||||
| 
 |  | ||||||
| typedef struct _PB_Status_PingResponse {  |  | ||||||
|     pb_bytes_array_t *data;  |  | ||||||
| } PB_Status_PingResponse; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| #ifdef __cplusplus |  | ||||||
| extern "C" { |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| /* Initializer values for message structs */ |  | ||||||
| #define PB_Status_PingRequest_init_default       {NULL} |  | ||||||
| #define PB_Status_PingResponse_init_default      {NULL} |  | ||||||
| #define PB_Status_PingRequest_init_zero          {NULL} |  | ||||||
| #define PB_Status_PingResponse_init_zero         {NULL} |  | ||||||
| 
 |  | ||||||
| /* Field tags (for use in manual encoding/decoding) */ |  | ||||||
| #define PB_Status_PingRequest_data_tag           1 |  | ||||||
| #define PB_Status_PingResponse_data_tag          1 |  | ||||||
| 
 |  | ||||||
| /* Struct field encoding specification for nanopb */ |  | ||||||
| #define PB_Status_PingRequest_FIELDLIST(X, a) \ |  | ||||||
| X(a, POINTER,  SINGULAR, BYTES,    data,              1) |  | ||||||
| #define PB_Status_PingRequest_CALLBACK NULL |  | ||||||
| #define PB_Status_PingRequest_DEFAULT NULL |  | ||||||
| 
 |  | ||||||
| #define PB_Status_PingResponse_FIELDLIST(X, a) \ |  | ||||||
| X(a, POINTER,  SINGULAR, BYTES,    data,              1) |  | ||||||
| #define PB_Status_PingResponse_CALLBACK NULL |  | ||||||
| #define PB_Status_PingResponse_DEFAULT NULL |  | ||||||
| 
 |  | ||||||
| extern const pb_msgdesc_t PB_Status_PingRequest_msg; |  | ||||||
| extern const pb_msgdesc_t PB_Status_PingResponse_msg; |  | ||||||
| 
 |  | ||||||
| /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ |  | ||||||
| #define PB_Status_PingRequest_fields &PB_Status_PingRequest_msg |  | ||||||
| #define PB_Status_PingResponse_fields &PB_Status_PingResponse_msg |  | ||||||
| 
 |  | ||||||
| /* Maximum encoded size of messages (where known) */ |  | ||||||
| /* PB_Status_PingRequest_size depends on runtime parameters */ |  | ||||||
| /* PB_Status_PingResponse_size depends on runtime parameters */ |  | ||||||
| 
 |  | ||||||
| #ifdef __cplusplus |  | ||||||
| } /* extern "C" */ |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #endif |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_0.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_4.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_5.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_6.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/laptop/frame_7.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										32
									
								
								assets/dolphin/animations/laptop/meta.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,32 @@ | |||||||
|  | Filetype: Flipper Animation | ||||||
|  | Version: 1 | ||||||
|  | 
 | ||||||
|  | Width: 128 | ||||||
|  | Height: 51 | ||||||
|  | Passive frames: 6 | ||||||
|  | Active frames: 2 | ||||||
|  | Frames order: 0 1 2 3 4 5 6 7 | ||||||
|  | Active cycles: 3 | ||||||
|  | Frame rate: 2 | ||||||
|  | Duration: 3600 | ||||||
|  | Active cooldown: 5 | ||||||
|  | 
 | ||||||
|  | Bubble slots: 1 | ||||||
|  | 
 | ||||||
|  | Slot: 0 | ||||||
|  | X: 60 | ||||||
|  | Y: 23 | ||||||
|  | Text: I have to rest | ||||||
|  | AlignH: Left | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 7 | ||||||
|  | EndFrame: 9 | ||||||
|  | 
 | ||||||
|  | Slot: 0 | ||||||
|  | X: 60 | ||||||
|  | Y: 23 | ||||||
|  | Text: but not today | ||||||
|  | AlignH: Left | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 10 | ||||||
|  | EndFrame: 12 | ||||||
							
								
								
									
										34
									
								
								assets/dolphin/animations/manifest.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,34 @@ | |||||||
|  | Filetype: Flipper Animation Manifest | ||||||
|  | Version: 1 | ||||||
|  | 
 | ||||||
|  | # Animation 1 | ||||||
|  | Name: waves | ||||||
|  | Min butthurt: 0 | ||||||
|  | Max butthurt: 5 | ||||||
|  | Min level: 1 | ||||||
|  | Max level: 3 | ||||||
|  | Weight: 3 | ||||||
|  | 
 | ||||||
|  | # Animation 2 | ||||||
|  | Name: laptop | ||||||
|  | Min butthurt: 0 | ||||||
|  | Max butthurt: 9 | ||||||
|  | Min level: 1 | ||||||
|  | Max level: 3 | ||||||
|  | Weight: 3 | ||||||
|  | 
 | ||||||
|  | # Animation 3 | ||||||
|  | Name: sleep | ||||||
|  | Min butthurt: 0 | ||||||
|  | Max butthurt: 10 | ||||||
|  | Min level: 1 | ||||||
|  | Max level: 3 | ||||||
|  | Weight: 3 | ||||||
|  | 
 | ||||||
|  | # Animation 4 | ||||||
|  | Name: recording | ||||||
|  | Min butthurt: 0 | ||||||
|  | Max butthurt: 8 | ||||||
|  | Min level: 1 | ||||||
|  | Max level: 1 | ||||||
|  | Weight: 3 | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_0.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_10.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_11.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_4.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_5.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_6.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_7.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_8.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/recording/frame_9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										14
									
								
								assets/dolphin/animations/recording/meta.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,14 @@ | |||||||
|  | Filetype: Flipper Animation | ||||||
|  | Version: 1 | ||||||
|  | 
 | ||||||
|  | Width: 128 | ||||||
|  | Height: 51 | ||||||
|  | Passive frames: 6 | ||||||
|  | Active frames: 6 | ||||||
|  | Frames order: 0 1 2 3 4 5 6 7 8 9 10 11 | ||||||
|  | Active cycles: 2 | ||||||
|  | Frame rate: 2 | ||||||
|  | Duration: 3600 | ||||||
|  | Active cooldown: 5 | ||||||
|  | 
 | ||||||
|  | Bubble slots: 0 | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/sleep/frame_0.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/sleep/frame_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/sleep/frame_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/sleep/frame_3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										41
									
								
								assets/dolphin/animations/sleep/meta.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,41 @@ | |||||||
|  | Filetype: Flipper Animation | ||||||
|  | Version: 1 | ||||||
|  | 
 | ||||||
|  | Width: 128 | ||||||
|  | Height: 64 | ||||||
|  | Passive frames: 2 | ||||||
|  | Active frames: 4 | ||||||
|  | Frames order: 0 1 2 3 2 3 | ||||||
|  | Active cycles: 2 | ||||||
|  | Frame rate: 2 | ||||||
|  | Duration: 3600 | ||||||
|  | Active cooldown: 5 | ||||||
|  | 
 | ||||||
|  | Bubble slots: 2 | ||||||
|  | 
 | ||||||
|  | Slot: 0 | ||||||
|  | X: 53 | ||||||
|  | Y: 20 | ||||||
|  | Text: In a lucid dream,\nI could walk... | ||||||
|  | AlignH: Left | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 3 | ||||||
|  | EndFrame: 9 | ||||||
|  | 
 | ||||||
|  | Slot: 1 | ||||||
|  | X: 53 | ||||||
|  | Y: 20 | ||||||
|  | Text: OH MY GOD! | ||||||
|  | AlignH: Left | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 3 | ||||||
|  | EndFrame: 5 | ||||||
|  | 
 | ||||||
|  | Slot: 1 | ||||||
|  | X: 53 | ||||||
|  | Y: 31 | ||||||
|  | Text: Just a dream... | ||||||
|  | AlignH: Left | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 6 | ||||||
|  | EndFrame: 9 | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/waves/frame_0.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/waves/frame_1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/waves/frame_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/dolphin/animations/waves/frame_3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										50
									
								
								assets/dolphin/animations/waves/meta.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,50 @@ | |||||||
|  | Filetype: Flipper Animation | ||||||
|  | Version: 1 | ||||||
|  | 
 | ||||||
|  | Width: 128 | ||||||
|  | Height: 50 | ||||||
|  | Passive frames: 2 | ||||||
|  | Active frames: 4 | ||||||
|  | Frames order: 0 1 2 3 2 3 | ||||||
|  | Active cycles: 2 | ||||||
|  | Frame rate: 2 | ||||||
|  | Duration: 3600 | ||||||
|  | Active cooldown: 5 | ||||||
|  | 
 | ||||||
|  | Bubble slots: 3 | ||||||
|  | 
 | ||||||
|  | Slot: 0 | ||||||
|  | X: 1 | ||||||
|  | Y: 17 | ||||||
|  | Text: I am happy,\nmy friend! | ||||||
|  | AlignH: Right | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 3 | ||||||
|  | EndFrame: 9 | ||||||
|  | 
 | ||||||
|  | Slot: 1 | ||||||
|  | X: 1 | ||||||
|  | Y: 17 | ||||||
|  | Text: So long and\nthanks for\nall the fish! | ||||||
|  | AlignH: Right | ||||||
|  | AlignV: Center | ||||||
|  | StartFrame: 3 | ||||||
|  | EndFrame: 9 | ||||||
|  | 
 | ||||||
|  | Slot: 2 | ||||||
|  | X: 1 | ||||||
|  | Y: 25 | ||||||
|  | Text: I wish I could | ||||||
|  | AlignH: Right | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 3 | ||||||
|  | EndFrame: 5 | ||||||
|  | 
 | ||||||
|  | Slot: 2 | ||||||
|  | X: 1 | ||||||
|  | Y: 25 | ||||||
|  | Text: swim all day | ||||||
|  | AlignH: Right | ||||||
|  | AlignV: Bottom | ||||||
|  | StartFrame: 6 | ||||||
|  | EndFrame: 9 | ||||||
| Before Width: | Height: | Size: 2.4 KiB | 
| Before Width: | Height: | Size: 2.4 KiB | 
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| Before Width: | Height: | Size: 2.3 KiB | 
| Before Width: | Height: | Size: 2.4 KiB | 
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| Before Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 2.1 KiB | 
| Before Width: | Height: | Size: 2.2 KiB | 
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| @ -1 +0,0 @@ | |||||||
| 2 |  | ||||||
| Before Width: | Height: | Size: 2.8 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
 Albert Kharisov
						Albert Kharisov