[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>
							
								
								
									
										19
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -71,6 +71,14 @@ jobs: | ||||
|         run: | | ||||
|           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' | ||||
|         uses: ./.github/actions/docker | ||||
|         with: | ||||
| @ -142,6 +150,7 @@ jobs: | ||||
|           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. | ||||
|           edit-mode: replace | ||||
| 
 | ||||
|   compact: | ||||
|     if: ${{ !startsWith(github.ref, 'refs/tags') }} | ||||
|     runs-on: [self-hosted,koteeq] | ||||
| @ -186,6 +195,14 @@ jobs: | ||||
|           echo "WORKFLOW_BRANCH_OR_TAG=${BRANCH_OR_TAG}" >> $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' | ||||
|         uses: ./.github/actions/docker | ||||
|         with: | ||||
| @ -194,4 +211,4 @@ jobs: | ||||
|             for TARGET in ${TARGETS} | ||||
|             do | ||||
|               make TARGET=${TARGET} DEBUG=0 COMPACT=1 | ||||
|             done | ||||
|             done | ||||
|  | ||||
							
								
								
									
										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 "desktop/desktop.h" | ||||
| #include "desktop_i.h" | ||||
| #include "gui/view_composed.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| #include <furi/pubsub.h> | ||||
| #include <furi/record.h> | ||||
| @ -10,7 +11,7 @@ | ||||
| #include "storage/storage.h" | ||||
| #include <stdint.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) { | ||||
|     furi_assert(canvas); | ||||
| @ -32,11 +33,12 @@ bool desktop_back_event_callback(void* context) { | ||||
| Desktop* desktop_alloc() { | ||||
|     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->scene_thread = furi_thread_alloc(); | ||||
|     desktop->view_dispatcher = view_dispatcher_alloc(); | ||||
|     desktop->scene_manager = scene_manager_alloc(&desktop_scene_handlers, desktop); | ||||
|     desktop->animation = desktop_animation_alloc(); | ||||
| 
 | ||||
|     view_dispatcher_enable_queue(desktop->view_dispatcher); | ||||
|     view_dispatcher_attach_to_gui( | ||||
| @ -48,16 +50,34 @@ Desktop* desktop_alloc() { | ||||
|     view_dispatcher_set_navigation_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->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(); | ||||
|     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->first_start_view = desktop_first_start_alloc(); | ||||
|     desktop->hw_mismatch_popup = popup_alloc(); | ||||
|     desktop->code_input = code_input_alloc(); | ||||
| 
 | ||||
|     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( | ||||
|         desktop->view_dispatcher, | ||||
|         DesktopViewLockMenu, | ||||
| @ -67,7 +87,7 @@ Desktop* desktop_alloc() { | ||||
|     view_dispatcher_add_view( | ||||
|         desktop->view_dispatcher, | ||||
|         DesktopViewLocked, | ||||
|         desktop_locked_get_view(desktop->locked_view)); | ||||
|         view_composed_get_view(desktop->locked_view_composed)); | ||||
|     view_dispatcher_add_view( | ||||
|         desktop->view_dispatcher, | ||||
|         DesktopViewFirstStart, | ||||
| @ -91,7 +111,6 @@ Desktop* desktop_alloc() { | ||||
| void desktop_free(Desktop* desktop) { | ||||
|     furi_assert(desktop); | ||||
| 
 | ||||
|     desktop_animation_free(desktop->animation); | ||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewMain); | ||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLockMenu); | ||||
|     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewLocked); | ||||
| @ -103,6 +122,9 @@ void desktop_free(Desktop* desktop) { | ||||
|     view_dispatcher_free(desktop->view_dispatcher); | ||||
|     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_lock_menu_free(desktop->lock_menu); | ||||
|     desktop_locked_free(desktop->locked_view); | ||||
| @ -111,6 +133,8 @@ void desktop_free(Desktop* desktop) { | ||||
|     popup_free(desktop->hw_mismatch_popup); | ||||
|     code_input_free(desktop->code_input); | ||||
| 
 | ||||
|     osSemaphoreDelete(desktop->unload_animation_semaphore); | ||||
| 
 | ||||
|     furi_record_close("gui"); | ||||
|     desktop->gui = NULL; | ||||
| 
 | ||||
| @ -129,29 +153,9 @@ static bool desktop_is_first_start() { | ||||
|     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) { | ||||
|     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); | ||||
|     if(!loaded) { | ||||
|         furi_hal_rtc_reset_flag(FuriHalRtcFlagLock); | ||||
| @ -181,8 +185,6 @@ int32_t desktop_srv(void* p) { | ||||
|     } | ||||
| 
 | ||||
|     view_dispatcher_run(desktop->view_dispatcher); | ||||
|     furi_pubsub_unsubscribe(dolphin_pubsub, dolphin_subscription); | ||||
|     furi_pubsub_unsubscribe(storage_pubsub, storage_subscription); | ||||
|     desktop_free(desktop); | ||||
| 
 | ||||
|     return 0; | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "cmsis_os2.h" | ||||
| #include "desktop.h" | ||||
| 
 | ||||
| #include "animations/animation_manager.h" | ||||
| #include "gui/view_composed.h" | ||||
| #include <furi.h> | ||||
| #include <furi-hal.h> | ||||
| 
 | ||||
| @ -21,7 +24,6 @@ | ||||
| #include "views/desktop_debug.h" | ||||
| 
 | ||||
| #include "scenes/desktop_scene.h" | ||||
| #include "helpers/desktop_animation.h" | ||||
| #include "desktop/desktop_settings/desktop_settings.h" | ||||
| #include <gui/icon.h> | ||||
| 
 | ||||
| @ -46,19 +48,29 @@ struct Desktop { | ||||
|     ViewDispatcher* view_dispatcher; | ||||
|     SceneManager* scene_manager; | ||||
| 
 | ||||
|     DesktopAnimation* animation; | ||||
|     DesktopFirstStartView* first_start_view; | ||||
|     Popup* hw_mismatch_popup; | ||||
|     DesktopMainView* main_view; | ||||
|     DesktopLockMenuView* lock_menu; | ||||
|     DesktopLockedView* locked_view; | ||||
|     DesktopDebugView* debug_view; | ||||
|     CodeInput* code_input; | ||||
| 
 | ||||
|     View* dolphin_view; | ||||
|     DesktopMainView* main_view; | ||||
|     DesktopLockedView* locked_view; | ||||
| 
 | ||||
|     ViewComposed* main_view_composed; | ||||
|     ViewComposed* locked_view_composed; | ||||
| 
 | ||||
|     DesktopSettings settings; | ||||
|     PinCode pincode_buffer; | ||||
| 
 | ||||
|     ViewPort* lock_viewport; | ||||
| 
 | ||||
|     AnimationManager* animation_manager; | ||||
|     osSemaphoreId_t unload_animation_semaphore; | ||||
|     FuriPubSubSubscription* app_start_stop_subscription; | ||||
| 
 | ||||
|     char* text_buffer; | ||||
| }; | ||||
| 
 | ||||
| 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, hw_mismatch, HwMismatch) | ||||
| ADD_SCENE(desktop, pinsetup, PinSetup) | ||||
| ADD_SCENE(desktop, levelup, LevelUp) | ||||
| ADD_SCENE(desktop, fault, Fault) | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| #include <dolphin/dolphin.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; | ||||
|     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: | ||||
|             dolphin_deed(dolphin, DolphinDeedIButtonEmulate); | ||||
|             desktop_debug_get_dolphin_data(desktop->debug_view); | ||||
|             desktop_start_new_idle_animation(desktop->animation); | ||||
|             consumed = true; | ||||
|             break; | ||||
| 
 | ||||
|         case DesktopDebugEventWrongDeed: | ||||
|             dolphin_deed(dolphin, DolphinDeedWrong); | ||||
|             desktop_debug_get_dolphin_data(desktop->debug_view); | ||||
|             desktop_start_new_idle_animation(desktop->animation); | ||||
|             consumed = true; | ||||
|             break; | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| #include "../desktop_i.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; | ||||
|     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) { | ||||
|     Desktop* desktop = (Desktop*)context; | ||||
|     furi_assert(desktop); | ||||
|     furi_assert(!desktop->text_buffer); | ||||
|     Popup* popup = desktop->hw_mismatch_popup; | ||||
|     char buffer[256]; // strange but smaller buffer not making it
 | ||||
|     desktop->text_buffer = furi_alloc(256); | ||||
|     snprintf( | ||||
|         buffer, | ||||
|         sizeof(buffer), | ||||
|         desktop->text_buffer, | ||||
|         256, | ||||
|         "HW target: %d\nFW target: %d", | ||||
|         furi_hal_version_get_hw_target(), | ||||
|         version_get_target(NULL)); | ||||
|     popup_set_context(popup, desktop); | ||||
|     popup_set_header( | ||||
|         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); | ||||
|     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) { | ||||
|     Desktop* desktop = (Desktop*)context; | ||||
|     furi_assert(desktop); | ||||
|     furi_assert(desktop->text_buffer); | ||||
|     Popup* popup = desktop->hw_mismatch_popup; | ||||
|     popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); | ||||
|     popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); | ||||
|     popup_set_callback(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 <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; | ||||
|     view_dispatcher_send_custom_event(desktop->view_dispatcher, event); | ||||
| } | ||||
|  | ||||
| @ -1,34 +1,28 @@ | ||||
| #include "../desktop_i.h" | ||||
| #include "../views/desktop_locked.h" | ||||
| #include "desktop/helpers/desktop_animation.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; | ||||
|     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); | ||||
|     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) { | ||||
|     Desktop* desktop = (Desktop*)context; | ||||
|     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_reset_door_pos(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); | ||||
| 
 | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|     size_t length = desktop->pincode_buffer.length; | ||||
| @ -81,15 +75,10 @@ bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) { | ||||
|         case DesktopLockedEventInputReset: | ||||
|             desktop->pincode_buffer.length = 0; | ||||
|             break; | ||||
|         case DesktopMainEventUpdateAnimation: { | ||||
|             bool status_bar_background_black = false; | ||||
|             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); | ||||
|         case DesktopLockedEventCheckAnimation: | ||||
|             animation_manager_check_blocking_process(desktop->animation_manager); | ||||
|             consumed = true; | ||||
|             break; | ||||
|         } | ||||
|         default: | ||||
|             if(desktop_scene_locked_check_pin(desktop, event.event)) { | ||||
|                 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) { | ||||
|     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); | ||||
|     osTimerStop(desktop->locked_view->timer); | ||||
| } | ||||
|  | ||||
| @ -2,14 +2,51 @@ | ||||
| #include "../views/desktop_main.h" | ||||
| #include "applications.h" | ||||
| #include "assets_icons.h" | ||||
| #include "cmsis_os2.h" | ||||
| #include "desktop/desktop.h" | ||||
| #include "desktop/views/desktop_events.h" | ||||
| #include "dolphin/dolphin.h" | ||||
| #include "furi/pubsub.h" | ||||
| #include "furi/record.h" | ||||
| #include "furi/thread.h" | ||||
| #include "storage/storage-glue.h" | ||||
| #include <loader/loader.h> | ||||
| #include <m-list.h> | ||||
| #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) { | ||||
|     furi_assert(desktop); | ||||
|     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_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; | ||||
|     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) { | ||||
|     Desktop* desktop = (Desktop*)context; | ||||
|     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); | ||||
|     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_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); | ||||
| } | ||||
| 
 | ||||
| @ -84,48 +122,50 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | ||||
| 
 | ||||
|         case DesktopMainEventOpenArchive: | ||||
| #ifdef APP_ARCHIVE | ||||
|             animation_manager_unload_and_stall_animation(desktop->animation_manager); | ||||
|             desktop_switch_to_app(desktop, &FLIPPER_ARCHIVE); | ||||
|             animation_manager_load_and_continue_animation(desktop->animation_manager); | ||||
| #endif | ||||
|             consumed = true; | ||||
|             break; | ||||
| 
 | ||||
|         case DesktopMainEventOpenFavorite: | ||||
|             LOAD_DESKTOP_SETTINGS(&desktop->settings); | ||||
|             animation_manager_unload_and_stall_animation(desktop->animation_manager); | ||||
|             if(desktop->settings.favorite < FLIPPER_APPS_COUNT) { | ||||
|                 desktop_switch_to_app(desktop, &FLIPPER_APPS[desktop->settings.favorite]); | ||||
|             } else { | ||||
|                 FURI_LOG_E("DesktopSrv", "Can't find favorite application"); | ||||
|             } | ||||
|             animation_manager_load_and_continue_animation(desktop->animation_manager); | ||||
|             consumed = true; | ||||
|             break; | ||||
| 
 | ||||
|         case DesktopMainEventUpdateAnimation: { | ||||
|             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); | ||||
|         case DesktopMainEventCheckAnimation: | ||||
|             animation_manager_check_blocking_process(desktop->animation_manager); | ||||
|             consumed = true; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         case DesktopMainEventRightShort: { | ||||
|             DesktopAnimationState state = desktop_animation_handle_right(desktop->animation); | ||||
|             if(state == DesktopAnimationStateLevelUpIsPending) { | ||||
|                 scene_manager_next_scene(desktop->scene_manager, DesktopSceneLevelUp); | ||||
|             } | ||||
|         case DesktopMainEventNewIdleAnimation: | ||||
|             animation_manager_new_idle_process(desktop->animation_manager); | ||||
|             consumed = true; | ||||
|             break; | ||||
|         case DesktopMainEventInteractAnimation: | ||||
|             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; | ||||
|         } | ||||
| 
 | ||||
|         default: | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(event.event != DesktopMainEventUpdateAnimation) { | ||||
|             desktop_animation_activate(desktop->animation); | ||||
|         } | ||||
|     } else if(event.type != SceneManagerEventTypeTick) { | ||||
|         desktop_animation_activate(desktop->animation); | ||||
|     } | ||||
| 
 | ||||
|     return consumed; | ||||
| @ -134,7 +174,18 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | ||||
| void desktop_scene_main_on_exit(void* 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); | ||||
|     desktop_main_reset_hint(desktop->main_view); | ||||
| } | ||||
|  | ||||
| @ -7,17 +7,11 @@ | ||||
| #include <furi.h> | ||||
| #include <storage/storage.h> | ||||
| #include <time.h> | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopDebugEventDeed, | ||||
|     DesktopDebugEventWrongDeed, | ||||
|     DesktopDebugEventSaveState, | ||||
|     DesktopDebugEventExit, | ||||
| } DesktopDebugEvent; | ||||
| #include "desktop_events.h" | ||||
| 
 | ||||
| typedef struct DesktopDebugView DesktopDebugView; | ||||
| 
 | ||||
| typedef void (*DesktopDebugViewCallback)(DesktopDebugEvent event, void* context); | ||||
| typedef void (*DesktopDebugViewCallback)(DesktopEvent event, void* context); | ||||
| 
 | ||||
| // Debug info
 | ||||
| typedef enum { | ||||
| @ -51,4 +45,4 @@ DesktopDebugView* desktop_debug_alloc(); | ||||
| void desktop_debug_free(DesktopDebugView* debug_view); | ||||
| 
 | ||||
| void desktop_debug_get_dolphin_data(DesktopDebugView* debug_view); | ||||
| void desktop_debug_reset_screen_idx(DesktopDebugView* debug_view); | ||||
| void desktop_debug_reset_screen_idx(DesktopDebugView* debug_view); | ||||
|  | ||||
							
								
								
									
										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/elements.h> | ||||
| #include <furi.h> | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopFirstStartCompleted, | ||||
|     DesktopFirstStartPoweroff, | ||||
| } DesktopFirstStartEvent; | ||||
| #include "desktop_events.h" | ||||
| 
 | ||||
| typedef struct DesktopFirstStartView DesktopFirstStartView; | ||||
| 
 | ||||
| typedef void (*DesktopFirstStartViewCallback)(DesktopFirstStartEvent event, void* context); | ||||
| typedef void (*DesktopFirstStartViewCallback)(DesktopEvent event, void* context); | ||||
| 
 | ||||
| DesktopFirstStartView* desktop_first_start_alloc(); | ||||
| 
 | ||||
|  | ||||
| @ -5,18 +5,13 @@ | ||||
| #include <gui/canvas.h> | ||||
| #include <gui/elements.h> | ||||
| #include <furi.h> | ||||
| #include "desktop_events.h" | ||||
| 
 | ||||
| #define HINT_TIMEOUT 2 | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopLockMenuEventLock, | ||||
|     DesktopLockMenuEventPinLock, | ||||
|     DesktopLockMenuEventExit, | ||||
| } DesktopLockMenuEvent; | ||||
| 
 | ||||
| typedef struct DesktopLockMenuView DesktopLockMenuView; | ||||
| 
 | ||||
| typedef void (*DesktopLockMenuViewCallback)(DesktopLockMenuEvent event, void* context); | ||||
| typedef void (*DesktopLockMenuViewCallback)(DesktopEvent event, void* context); | ||||
| 
 | ||||
| struct DesktopLockMenuView { | ||||
|     View* view; | ||||
|  | ||||
| @ -17,21 +17,6 @@ void locked_view_timer_callback(void* 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) { | ||||
|     with_view_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) { | ||||
|     DesktopLockedViewModel* m = model; | ||||
|     uint32_t now = osKernelGetTickCount(); | ||||
|     canvas_clear(canvas); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
| 
 | ||||
|     if(!m->animation_seq_end) { | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| #include <gui/canvas.h> | ||||
| #include <gui/elements.h> | ||||
| #include <furi.h> | ||||
| #include "desktop_events.h" | ||||
| 
 | ||||
| #define UNLOCK_RST_TIMEOUT 300 | ||||
| #define UNLOCK_CNT 2 // 3 actually
 | ||||
| @ -14,12 +15,6 @@ | ||||
| #define DOOR_R_POS 115 | ||||
| #define DOOR_R_POS_MIN 60 | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopLockedEventUnlock = 10U, | ||||
|     DesktopLockedEventUpdate = 11U, | ||||
|     DesktopLockedEventInputReset = 12U, | ||||
| } DesktopLockedEvent; | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopLockedWithPin, | ||||
|     DesktopLockedNoPin, | ||||
| @ -27,7 +22,7 @@ typedef enum { | ||||
| 
 | ||||
| typedef struct DesktopLockedView DesktopLockedView; | ||||
| 
 | ||||
| typedef void (*DesktopLockedViewCallback)(DesktopLockedEvent event, void* context); | ||||
| typedef void (*DesktopLockedViewCallback)(DesktopEvent event, void* context); | ||||
| 
 | ||||
| struct DesktopLockedView { | ||||
|     View* view; | ||||
| @ -57,10 +52,6 @@ void desktop_locked_set_callback( | ||||
|     DesktopLockedViewCallback callback, | ||||
|     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_reset_counter(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/view.h" | ||||
| #include "gui/view_composed.h" | ||||
| #include "input/input.h" | ||||
| #include <furi.h> | ||||
| #include "../desktop_i.h" | ||||
| #include "desktop_main.h" | ||||
| //#include "../animations/views/bubble_animation_view.h"
 | ||||
| 
 | ||||
| void desktop_main_set_callback( | ||||
|     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) { | ||||
|     canvas_clear(canvas); | ||||
|     DesktopMainViewModel* m = model; | ||||
|     uint32_t now = osKernelGetTickCount(); | ||||
| 
 | ||||
| @ -78,6 +82,7 @@ bool desktop_main_input(InputEvent* event, void* context) { | ||||
|     furi_assert(context); | ||||
| 
 | ||||
|     DesktopMainView* main_view = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event->key == InputKeyOk && event->type == InputTypeShort) { | ||||
|         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); | ||||
|     } else if(event->key == InputKeyRight && event->type == InputTypeShort) { | ||||
|         main_view->callback(DesktopMainEventRightShort, main_view->context); | ||||
|     } else if(event->key == InputKeyBack && event->type == InputTypeShort) { | ||||
|         consumed = true; | ||||
|     } | ||||
| 
 | ||||
|     desktop_main_reset_hint(main_view); | ||||
| 
 | ||||
|     return true; | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void desktop_main_enter(void* context) { | ||||
| @ -119,6 +126,7 @@ void desktop_main_exit(void* context) { | ||||
| 
 | ||||
| DesktopMainView* desktop_main_alloc() { | ||||
|     DesktopMainView* main_view = furi_alloc(sizeof(DesktopMainView)); | ||||
| 
 | ||||
|     main_view->view = view_alloc(); | ||||
|     view_allocate_model(main_view->view, ViewModelTypeLocking, sizeof(DesktopMainViewModel)); | ||||
|     view_set_context(main_view->view, main_view); | ||||
|  | ||||
| @ -1,26 +1,16 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "gui/view_composed.h" | ||||
| #include <gui/gui_i.h> | ||||
| #include <gui/view.h> | ||||
| #include <gui/canvas.h> | ||||
| #include <gui/elements.h> | ||||
| #include <furi.h> | ||||
| 
 | ||||
| typedef enum { | ||||
|     DesktopMainEventOpenLockMenu, | ||||
|     DesktopMainEventOpenArchive, | ||||
|     DesktopMainEventOpenFavorite, | ||||
|     DesktopMainEventOpenMenu, | ||||
|     DesktopMainEventOpenDebug, | ||||
|     DesktopMainEventUnlocked, | ||||
|     DesktopMainEventRightShort, | ||||
|     DesktopMainEventUpdateAnimation, | ||||
|     DesktopMainEventUpdateOneShotAnimation, | ||||
| } DesktopMainEvent; | ||||
| #include "desktop_events.h" | ||||
| 
 | ||||
| typedef struct DesktopMainView DesktopMainView; | ||||
| 
 | ||||
| typedef void (*DesktopMainViewCallback)(DesktopMainEvent event, void* context); | ||||
| typedef void (*DesktopMainViewCallback)(DesktopEvent event, void* context); | ||||
| 
 | ||||
| struct DesktopMainView { | ||||
|     View* view; | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "furi/pubsub.h" | ||||
| #include "gui/view.h" | ||||
| #include "helpers/dolphin_deed.h" | ||||
| #include <stdbool.h> | ||||
| 
 | ||||
|  | ||||
| @ -11,6 +11,9 @@ typedef enum { | ||||
|     DolphinEventTypeDeed, | ||||
|     DolphinEventTypeStats, | ||||
|     DolphinEventTypeFlush, | ||||
|     DolphinEventTypeAnimationStartNewIdle, | ||||
|     DolphinEventTypeAnimationCheckBlocking, | ||||
|     DolphinEventTypeAnimationInteract, | ||||
| } DolphinEventType; | ||||
| 
 | ||||
| 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); | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
|     furi_assert(canvas); | ||||
|     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); | ||||
| 
 | ||||
| /** 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
 | ||||
|  * | ||||
|  * @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) { | ||||
|     furi_assert(view_dispatcher); | ||||
|     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( | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| #include <furi/pubsub.h> | ||||
| #include "loader/loader.h" | ||||
| #include "loader_i.h" | ||||
| 
 | ||||
| @ -21,6 +22,7 @@ static void loader_menu_callback(void* _ctx, uint32_t index) { | ||||
|         return; | ||||
|     } | ||||
|     furi_hal_power_insomnia_enter(); | ||||
| 
 | ||||
|     loader_instance->current_app = flipper_app; | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|     Loader* instance = context; | ||||
|     LoaderEvent event; | ||||
| 
 | ||||
|     if(thread_state == FuriThreadStateRunning) { | ||||
|         instance->free_heap_size = memmgr_get_free_heap(); | ||||
|         event.type = LoaderEventTypeApplicationStarted; | ||||
|         furi_pubsub_publish(loader_instance->pubsub, &event); | ||||
|     } else if(thread_state == FuriThreadStateStopped) { | ||||
|         /*
 | ||||
|          * 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_hal_power_insomnia_exit(); | ||||
|         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); | ||||
| 
 | ||||
|     instance->pubsub = furi_pubsub_alloc(); | ||||
|     instance->mutex = osMutexNew(NULL); | ||||
| 
 | ||||
| #ifdef SRV_CLI | ||||
| @ -334,6 +342,8 @@ static void loader_free(Loader* instance) { | ||||
| 
 | ||||
|     osMutexDelete(instance->mutex); | ||||
| 
 | ||||
|     furi_pubsub_free(instance->pubsub); | ||||
| 
 | ||||
|     string_clear(instance->args); | ||||
| 
 | ||||
|     furi_thread_free(instance->thread); | ||||
| @ -471,3 +481,7 @@ int32_t loader_srv(void* p) { | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| FuriPubSub* loader_get_pubsub() { | ||||
|     return loader_instance->pubsub; | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <furi/pubsub.h> | ||||
| #include <stdbool.h> | ||||
| 
 | ||||
| typedef struct Loader Loader; | ||||
| @ -11,6 +12,15 @@ typedef enum { | ||||
|     LoaderStatusErrorInternal, | ||||
| } LoaderStatus; | ||||
| 
 | ||||
| typedef enum { | ||||
|     LoaderEventTypeApplicationStarted, | ||||
|     LoaderEventTypeApplicationStopped | ||||
| } LoaderEventType; | ||||
| 
 | ||||
| typedef struct { | ||||
|     LoaderEventType type; | ||||
| } LoaderEvent; | ||||
| 
 | ||||
| /** Start application
 | ||||
|  * @param name - application name | ||||
|  * @param args - application arguments | ||||
| @ -34,3 +44,6 @@ void loader_show_menu(); | ||||
| 
 | ||||
| /** Show primary loader */ | ||||
| void loader_update_menu(); | ||||
| 
 | ||||
| /** Show primary loader */ | ||||
| FuriPubSub* loader_get_pubsub(); | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| 
 | ||||
| #include <furi.h> | ||||
| #include <furi-hal.h> | ||||
| #include <furi/pubsub.h> | ||||
| #include <cli/cli.h> | ||||
| #include <lib/toolbox/args.h> | ||||
| 
 | ||||
| @ -30,6 +31,8 @@ struct Loader { | ||||
|     size_t free_heap_size; | ||||
|     osMutexId_t mutex; | ||||
|     volatile uint8_t lock_semaphore; | ||||
| 
 | ||||
|     FuriPubSub* pubsub; | ||||
| }; | ||||
| 
 | ||||
| typedef enum { | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
| #include <pb_decode.h> | ||||
| #include <pb_encode.h> | ||||
| 
 | ||||
| #include <status.pb.h> | ||||
| #include <storage.pb.h> | ||||
| #include <flipper.pb.h> | ||||
| #include <portmacro.h> | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| #include "flipper.pb.h" | ||||
| #include "furi/record.h" | ||||
| #include "status.pb.h" | ||||
| #include "rpc_i.h" | ||||
| #include <furi.h> | ||||
| #include <loader/loader.h> | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| #include "flipper.pb.h" | ||||
| #include "rpc_i.h" | ||||
| #include "status.pb.h" | ||||
| 
 | ||||
| #include <furi-hal.h> | ||||
| #include <power/power_service/power.h> | ||||
|  | ||||
| @ -189,4 +189,4 @@ typedef struct { | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| @ -2,20 +2,35 @@ PROJECT_ROOT		= $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..) | ||||
| 
 | ||||
| include				$(PROJECT_ROOT)/assets/assets.mk | ||||
| 
 | ||||
| all: icons protobuf | ||||
| .PHONY: all | ||||
| all: icons protobuf dolphin | ||||
| 
 | ||||
| $(ASSETS): $(ASSETS_SOURCES) $(ASSETS_COMPILLER) | ||||
| 	@echo "\tASSETS\t\t" $@ | ||||
| 	@$(ASSETS_COMPILLER) icons "$(ASSETS_SOURCE_DIR)" "$(ASSETS_COMPILED_DIR)" | ||||
| 
 | ||||
| .PHONY: icons | ||||
| icons: $(ASSETS) | ||||
| 
 | ||||
| $(PROTOBUF) &: $(PROTOBUF_SOURCES) $(PROTOBUF_COMPILER) | ||||
| 	@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_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: | ||||
| 	@echo "\tCLEAN\t" | ||||
| 	@$(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_`. | ||||
| 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 | ||||
| 
 | ||||
| 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				+= $(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_COMPILER		:= $(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py | ||||
| PROTOBUF_COMPILED_DIR	:= $(ASSETS_COMPILED_DIR) | ||||
| 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				:= $(addprefix $(PROTOBUF_COMPILED_DIR)/,$(PROTOBUF_FILENAMES)) | ||||
| PROTOBUF_CFLAGS			+= -DPB_ENABLE_MALLOC | ||||
|  | ||||
| @ -1,145 +1,118 @@ | ||||
| #pragma once | ||||
| #include <gui/icon.h> | ||||
| 
 | ||||
| extern const Icon I_Certification2_119x30; | ||||
| extern const Icon I_Certification1_103x23; | ||||
| extern const Icon A_BadBattery_128x51; | ||||
| extern const Icon A_BoxActive_128x51; | ||||
| extern const Icon A_Box_128x51; | ||||
| extern const Icon A_CardBad_128x51; | ||||
| extern const Icon A_CardNoDBUrl_128x51; | ||||
| extern const Icon A_CardNoDB_128x51; | ||||
| extern const Icon A_CardOk_128x51; | ||||
| extern const Icon A_CryActive_128x51; | ||||
| extern const Icon A_Cry_128x51; | ||||
| extern const Icon A_KnifeActive_128x51; | ||||
| extern const Icon A_Knife_128x51; | ||||
| extern const Icon A_LaptopActive_128x52; | ||||
| extern const Icon A_Laptop_128x52; | ||||
| extern const Icon A_LeavingActive_128x51; | ||||
| extern const Icon A_Leaving_128x51; | ||||
| extern const Icon A_Level1FurippaActive_128x51; | ||||
| extern const Icon A_Level1Furippa_128x51; | ||||
| extern const Icon A_Level1ReadActive_128x51; | ||||
| extern const Icon A_Level1Read_128x51; | ||||
| extern const Icon A_Level1ToysActive_128x51; | ||||
| extern const Icon A_Level1Toys_128x51; | ||||
| extern const Icon A_Level2FurippaActive_128x51; | ||||
| extern const Icon A_Level2Furippa_128x51; | ||||
| extern const Icon A_Level2HackActive_128x51; | ||||
| extern const Icon A_Level2Hack_128x51; | ||||
| extern const Icon A_Level2SolderingActive_128x51; | ||||
| extern const Icon A_Level2Soldering_128x51; | ||||
| extern const Icon A_Level3FurippaActive_128x51; | ||||
| extern const Icon A_Level3Furippa_128x51; | ||||
| 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_Certification2_119x30; | ||||
| extern const Icon I_card_bad1; | ||||
| extern const Icon I_card_bad2; | ||||
| extern const Icon I_card_ok1; | ||||
| extern const Icon I_card_ok2; | ||||
| extern const Icon I_card_ok3; | ||||
| extern const Icon I_card_ok4; | ||||
| extern const Icon I_no_databases1; | ||||
| extern const Icon I_no_databases2; | ||||
| extern const Icon I_no_databases3; | ||||
| extern const Icon I_no_databases4; | ||||
| extern const Icon I_no_sd1; | ||||
| extern const Icon I_no_sd2; | ||||
| extern const Icon I_no_sd3; | ||||
| extern const Icon I_no_sd4; | ||||
| extern const Icon I_no_sd5; | ||||
| extern const Icon I_no_sd6; | ||||
| extern const Icon I_tv1; | ||||
| extern const Icon I_tv2; | ||||
| extern const Icon I_tv3; | ||||
| extern const Icon I_tv4; | ||||
| extern const Icon I_tv5; | ||||
| extern const Icon I_tv6; | ||||
| extern const Icon I_tv7; | ||||
| extern const Icon I_tv8; | ||||
| extern const Icon I_url1; | ||||
| extern const Icon I_url2; | ||||
| extern const Icon I_url3; | ||||
| extern const Icon I_url4; | ||||
| 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_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_BLE_Pairing_128x64; | ||||
| extern const Icon I_Volup_8x6; | ||||
| extern const Icon I_Circles_47x47; | ||||
| extern const Icon I_Ble_connected_38x34; | ||||
| 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_Pressed_Button_13x13; | ||||
| extern const Icon I_Space_65x18; | ||||
| extern const Icon I_Voldwn_6x6; | ||||
| extern const Icon I_Ble_connected_38x34; | ||||
| extern const Icon I_Button_18x18; | ||||
| extern const Icon I_EviSmile2_18x21; | ||||
| extern const Icon I_Volup_8x6; | ||||
| extern const Icon I_Clock_18x18; | ||||
| extern const Icon I_Error_18x18; | ||||
| 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_EviWaiting2_18x21; | ||||
| extern const Icon I_Percent_10x14; | ||||
| extern const Icon I_Smile_18x18; | ||||
| extern const Icon I_Error_18x18; | ||||
| extern const Icon I_Clock_18x18; | ||||
| extern const Icon I_ButtonRightSmall_3x5; | ||||
| extern const Icon I_ButtonLeftSmall_3x5; | ||||
| extern const Icon I_UsbTree_48x22; | ||||
| extern const Icon I_ButtonCenter_7x7; | ||||
| extern const Icon I_ButtonDown_7x4; | ||||
| extern const Icon I_ButtonRight_4x7; | ||||
| extern const Icon I_DFU_128x50; | ||||
| extern const Icon I_ButtonUp_7x4; | ||||
| extern const Icon I_Warning_30x23; | ||||
| extern const Icon I_ButtonLeftSmall_3x5; | ||||
| extern const Icon I_ButtonLeft_4x7; | ||||
| extern const Icon I_DolphinFirstStart7_61x51; | ||||
| extern const Icon I_DolphinOkay_41x43; | ||||
| extern const Icon I_DolphinFirstStart5_54x49; | ||||
| extern const Icon I_Flipper_young_80x60; | ||||
| extern const Icon I_DolphinFirstStart2_59x51; | ||||
| extern const Icon I_DolphinFirstStart8_56x51; | ||||
| extern const Icon I_DolphinFirstStart3_57x48; | ||||
| extern const Icon I_ButtonRightSmall_3x5; | ||||
| extern const Icon I_ButtonRight_4x7; | ||||
| extern const Icon I_ButtonUp_7x4; | ||||
| extern const Icon I_DFU_128x50; | ||||
| extern const Icon I_Warning_30x23; | ||||
| 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_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_ArrowUpEmpty_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_DoorRight_70x55; | ||||
| extern const Icon I_LockPopup_100x49; | ||||
| extern const Icon I_PassportBottom_128x17; | ||||
| 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_KeySaveSelected_24x11; | ||||
| extern const Icon I_KeySave_24x11; | ||||
| extern const Icon I_DolphinReadingSuccess_59x63; | ||||
| 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_KeyBackspace_16x9; | ||||
| extern const Icon I_KeySaveSelected_24x11; | ||||
| extern const Icon I_KeySave_24x11; | ||||
| extern const Icon A_125khz_14; | ||||
| extern const Icon A_BadUsb_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_U2F_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_passport_happy3_46x49; | ||||
| extern const Icon I_Medium_chip_22x21; | ||||
| 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_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_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_Health_16x16; | ||||
| extern const Icon I_Voltage_16x16; | ||||
| extern const Icon I_passport_happy3_46x49; | ||||
| 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_FaceNormal_29x14; | ||||
| extern const Icon I_FaceCharging_29x14; | ||||
| extern const Icon I_Battery_16x16; | ||||
| extern const Icon I_FaceCharging_29x14; | ||||
| extern const Icon I_FaceConfused_29x14; | ||||
| extern const Icon I_Temperature_16x16; | ||||
| 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_RFIDDolphinReceive_97x61; | ||||
| extern const Icon I_RFIDDolphinSend_97x61; | ||||
| extern const Icon I_RFIDDolphinSuccess_108x57; | ||||
| extern const Icon I_SDError_43x35; | ||||
| extern const Icon I_SDQuestion_35x43; | ||||
| 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_Charging_lightning_mask_9x10; | ||||
| extern const Icon I_PlaceholderL_11x13; | ||||
| 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_Quest_7x8; | ||||
| extern const Icon I_Lock_7x8; | ||||
| extern const Icon I_MHz_25x11; | ||||
| extern const Icon I_Quest_7x8; | ||||
| extern const Icon I_Scanning_123x52; | ||||
| 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_iButtonKey_49x44; | ||||
| extern const Icon I_iButtonDolphinVerySuccess_108x52; | ||||
| extern const Icon I_DolphinWait_61x59; | ||||
| 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 | 
 Albert Kharisov
						Albert Kharisov