[FL-2627] Flipper applications: SDK, build and debug system (#1387)
* Added support for running applications from SD card (FAPs - Flipper Application Packages) * Added plugin_dist target for fbt to build FAPs * All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default * Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them * Added debugging support for FAPs with fbt debug & VSCode * Added public firmware API with automated versioning Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
575
applications/services/desktop/animations/animation_manager.c
Normal file
575
applications/services/desktop/animations/animation_manager.c
Normal file
@@ -0,0 +1,575 @@
|
||||
#include <gui/view_stack.h>
|
||||
#include <stdint.h>
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <m-string.h>
|
||||
#include <portmacro.h>
|
||||
#include <dolphin/dolphin.h>
|
||||
#include <power/power_service/power.h>
|
||||
#include <storage/storage.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
#include "views/bubble_animation_view.h"
|
||||
#include "views/one_shot_animation_view.h"
|
||||
#include "animation_storage.h"
|
||||
#include "animation_manager.h"
|
||||
|
||||
#define TAG "AnimationManager"
|
||||
|
||||
#define HARDCODED_ANIMATION_NAME "L1_Tv_128x47"
|
||||
#define NO_SD_ANIMATION_NAME "L1_NoSd_128x49"
|
||||
#define BAD_BATTERY_ANIMATION_NAME "L1_BadBattery_128x47"
|
||||
|
||||
#define NO_DB_ANIMATION_NAME "L0_NoDb_128x51"
|
||||
#define BAD_SD_ANIMATION_NAME "L0_SdBad_128x51"
|
||||
#define SD_OK_ANIMATION_NAME "L0_SdOk_128x51"
|
||||
#define URL_ANIMATION_NAME "L0_Url_128x51"
|
||||
#define NEW_MAIL_ANIMATION_NAME "L0_NewMail_128x51"
|
||||
|
||||
typedef enum {
|
||||
AnimationManagerStateIdle,
|
||||
AnimationManagerStateBlocked,
|
||||
AnimationManagerStateFreezedIdle,
|
||||
AnimationManagerStateFreezedBlocked,
|
||||
} AnimationManagerState;
|
||||
|
||||
struct AnimationManager {
|
||||
bool sd_show_url;
|
||||
bool sd_shown_no_db;
|
||||
bool sd_shown_sd_ok;
|
||||
bool levelup_pending;
|
||||
bool levelup_active;
|
||||
AnimationManagerState state;
|
||||
FuriPubSubSubscription* pubsub_subscription_storage;
|
||||
FuriPubSubSubscription* pubsub_subscription_dolphin;
|
||||
BubbleAnimationView* animation_view;
|
||||
OneShotView* one_shot_view;
|
||||
FuriTimer* 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;
|
||||
ViewStack* view_stack;
|
||||
};
|
||||
|
||||
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);
|
||||
static bool animation_manager_is_valid_idle_animation(
|
||||
const StorageAnimationManifestInfo* info,
|
||||
const DolphinStats* stats);
|
||||
static void animation_manager_switch_to_one_shot_view(AnimationManager* animation_manager);
|
||||
static void animation_manager_switch_to_animation_view(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) {
|
||||
const StorageEvent* storage_event = message;
|
||||
|
||||
switch(storage_event->type) {
|
||||
case StorageEventTypeCardMount:
|
||||
case StorageEventTypeCardUnmount:
|
||||
case StorageEventTypeCardMountError:
|
||||
furi_assert(context);
|
||||
AnimationManager* animation_manager = context;
|
||||
if(animation_manager->check_blocking_callback) {
|
||||
animation_manager->check_blocking_callback(animation_manager->context);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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->check_blocking_callback() */
|
||||
void animation_manager_check_blocking_process(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
|
||||
if(animation_manager->state == AnimationManagerStateIdle) {
|
||||
bool blocked = animation_manager_check_blocking(animation_manager);
|
||||
|
||||
if(!blocked) {
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
DolphinStats stats = dolphin_stats(dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
|
||||
const StorageAnimationManifestInfo* manifest_info =
|
||||
animation_storage_get_meta(animation_manager->current_animation);
|
||||
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
|
||||
|
||||
if(!valid) {
|
||||
animation_manager_start_new_idle(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->interact_callback() */
|
||||
bool animation_manager_interact_process(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
bool consumed = true;
|
||||
|
||||
if(animation_manager->levelup_pending) {
|
||||
animation_manager->levelup_pending = false;
|
||||
animation_manager->levelup_active = true;
|
||||
animation_manager_switch_to_one_shot_view(animation_manager);
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
dolphin_upgrade_level(dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
} else if(animation_manager->levelup_active) {
|
||||
animation_manager->levelup_active = false;
|
||||
animation_manager_start_new_idle(animation_manager);
|
||||
animation_manager_switch_to_animation_view(animation_manager);
|
||||
} else if(animation_manager->state == AnimationManagerStateBlocked) {
|
||||
bool blocked = animation_manager_check_blocking(animation_manager);
|
||||
|
||||
if(!blocked) {
|
||||
animation_manager_start_new_idle(animation_manager);
|
||||
}
|
||||
} else {
|
||||
consumed = false;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
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;
|
||||
furi_timer_start(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(RECORD_STORAGE);
|
||||
FS_Error sd_status = storage_sd_status(storage);
|
||||
|
||||
if(sd_status == FSE_INTERNAL) {
|
||||
blocking_animation = animation_storage_find_animation(BAD_SD_ANIMATION_NAME);
|
||||
furi_assert(blocking_animation);
|
||||
} 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);
|
||||
furi_assert(blocking_animation);
|
||||
animation_manager->sd_shown_sd_ok = true;
|
||||
} else if(!animation_manager->sd_shown_no_db) {
|
||||
if(!storage_file_exists(storage, EXT_PATH("Manifest"))) {
|
||||
blocking_animation = animation_storage_find_animation(NO_DB_ANIMATION_NAME);
|
||||
furi_assert(blocking_animation);
|
||||
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);
|
||||
furi_assert(blocking_animation);
|
||||
animation_manager->sd_show_url = false;
|
||||
}
|
||||
}
|
||||
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
DolphinStats stats = dolphin_stats(dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
if(!blocking_animation && stats.level_up_is_pending) {
|
||||
blocking_animation = animation_storage_find_animation(NEW_MAIL_ANIMATION_NAME);
|
||||
furi_assert(blocking_animation);
|
||||
if(blocking_animation) {
|
||||
animation_manager->levelup_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocking_animation) {
|
||||
furi_timer_stop(animation_manager->idle_animation_timer);
|
||||
animation_manager_replace_current_animation(animation_manager, blocking_animation);
|
||||
/* no timer starting because this is blocking animation */
|
||||
animation_manager->state = AnimationManagerStateBlocked;
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_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 = 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) {
|
||||
AnimationManager* animation_manager = malloc(sizeof(AnimationManager));
|
||||
animation_manager->animation_view = bubble_animation_view_alloc();
|
||||
animation_manager->view_stack = view_stack_alloc();
|
||||
View* animation_view = bubble_animation_get_view(animation_manager->animation_view);
|
||||
view_stack_add_view(animation_manager->view_stack, animation_view);
|
||||
string_init(animation_manager->freezed_animation_name);
|
||||
|
||||
animation_manager->idle_animation_timer =
|
||||
furi_timer_alloc(animation_manager_timer_callback, FuriTimerTypeOnce, animation_manager);
|
||||
bubble_animation_view_set_interact_callback(
|
||||
animation_manager->animation_view, animation_manager_interact_callback, animation_manager);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
animation_manager->pubsub_subscription_storage = furi_pubsub_subscribe(
|
||||
storage_get_pubsub(storage), animation_manager_check_blocking_callback, animation_manager);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
animation_manager->pubsub_subscription_dolphin = furi_pubsub_subscribe(
|
||||
dolphin_get_pubsub(dolphin), animation_manager_check_blocking_callback, animation_manager);
|
||||
furi_record_close(RECORD_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(RECORD_DOLPHIN);
|
||||
furi_pubsub_unsubscribe(
|
||||
dolphin_get_pubsub(dolphin), animation_manager->pubsub_subscription_dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
furi_pubsub_unsubscribe(
|
||||
storage_get_pubsub(storage), animation_manager->pubsub_subscription_storage);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
string_clear(animation_manager->freezed_animation_name);
|
||||
View* animation_view = bubble_animation_get_view(animation_manager->animation_view);
|
||||
view_stack_remove_view(animation_manager->view_stack, animation_view);
|
||||
bubble_animation_view_free(animation_manager->animation_view);
|
||||
furi_timer_free(animation_manager->idle_animation_timer);
|
||||
}
|
||||
|
||||
View* animation_manager_get_animation_view(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
|
||||
return view_stack_get_view(animation_manager->view_stack);
|
||||
}
|
||||
|
||||
static bool animation_manager_is_valid_idle_animation(
|
||||
const StorageAnimationManifestInfo* info,
|
||||
const DolphinStats* stats) {
|
||||
furi_assert(info);
|
||||
furi_assert(info->name);
|
||||
|
||||
bool result = true;
|
||||
|
||||
if(!strcmp(info->name, BAD_BATTERY_ANIMATION_NAME)) {
|
||||
Power* power = furi_record_open(RECORD_POWER);
|
||||
bool battery_is_well = power_is_battery_healthy(power);
|
||||
furi_record_close(RECORD_POWER);
|
||||
|
||||
result = !battery_is_well;
|
||||
}
|
||||
if(!strcmp(info->name, NO_SD_ANIMATION_NAME)) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FS_Error sd_status = storage_sd_status(storage);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
result = (sd_status == FSE_NOT_READY);
|
||||
}
|
||||
if((stats->butthurt < info->min_butthurt) || (stats->butthurt > info->max_butthurt)) {
|
||||
result = false;
|
||||
}
|
||||
if((stats->level < info->min_level) || (stats->level > info->max_level)) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static StorageAnimation*
|
||||
animation_manager_select_idle_animation(AnimationManager* animation_manager) {
|
||||
UNUSED(animation_manager);
|
||||
StorageAnimationList_t animation_list;
|
||||
StorageAnimationList_init(animation_list);
|
||||
animation_storage_fill_animation_list(&animation_list);
|
||||
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
DolphinStats stats = dolphin_stats(dolphin);
|
||||
furi_record_close(RECORD_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 StorageAnimationManifestInfo* manifest_info =
|
||||
animation_storage_get_meta(storage_animation);
|
||||
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
|
||||
|
||||
if(valid) {
|
||||
whole_weight += manifest_info->weight;
|
||||
StorageAnimationList_next(it);
|
||||
} else {
|
||||
animation_storage_free_storage_animation(&storage_animation);
|
||||
/* remove and increase iterator */
|
||||
StorageAnimationList_remove(animation_list, it);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t lucky_number = furi_hal_random_get() % 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);
|
||||
|
||||
/* cache animation, if failed - choose reliable animation */
|
||||
if(!animation_storage_get_bubble_animation(selected)) {
|
||||
const char* name = 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;
|
||||
}
|
||||
|
||||
bool animation_manager_is_animation_loaded(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
return animation_manager->current_animation;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
furi_timer_stop(animation_manager->idle_animation_timer);
|
||||
} else {
|
||||
furi_assert(0);
|
||||
}
|
||||
|
||||
FURI_LOG_I(
|
||||
TAG,
|
||||
"Unload animation \'%s\'",
|
||||
animation_storage_get_meta(animation_manager->current_animation)->name);
|
||||
|
||||
StorageAnimationManifestInfo* meta =
|
||||
animation_storage_get_meta(animation_manager->current_animation);
|
||||
/* copy str, not move, because it can be internal animation */
|
||||
string_set_str(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) {
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
DolphinStats stats = dolphin_stats(dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
const StorageAnimationManifestInfo* manifest_info =
|
||||
animation_storage_get_meta(restore_animation);
|
||||
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
|
||||
if(valid) {
|
||||
animation_manager_replace_current_animation(
|
||||
animation_manager, restore_animation);
|
||||
animation_manager->state = AnimationManagerStateIdle;
|
||||
|
||||
if(animation_manager->freezed_animation_time_left) {
|
||||
furi_timer_start(
|
||||
animation_manager->idle_animation_timer,
|
||||
animation_manager->freezed_animation_time_left);
|
||||
} else {
|
||||
const BubbleAnimation* animation = animation_storage_get_bubble_animation(
|
||||
animation_manager->current_animation);
|
||||
furi_timer_start(
|
||||
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_I(
|
||||
TAG,
|
||||
"Load animation \'%s\'",
|
||||
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);
|
||||
}
|
||||
|
||||
static void animation_manager_switch_to_one_shot_view(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
furi_assert(!animation_manager->one_shot_view);
|
||||
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
|
||||
DolphinStats stats = dolphin_stats(dolphin);
|
||||
furi_record_close(RECORD_DOLPHIN);
|
||||
|
||||
animation_manager->one_shot_view = one_shot_view_alloc();
|
||||
one_shot_view_set_interact_callback(
|
||||
animation_manager->one_shot_view, animation_manager_interact_callback, animation_manager);
|
||||
View* prev_view = bubble_animation_get_view(animation_manager->animation_view);
|
||||
View* next_view = one_shot_view_get_view(animation_manager->one_shot_view);
|
||||
view_stack_remove_view(animation_manager->view_stack, prev_view);
|
||||
view_stack_add_view(animation_manager->view_stack, next_view);
|
||||
if(stats.level == 1) {
|
||||
one_shot_view_start_animation(animation_manager->one_shot_view, &A_Levelup1_128x64);
|
||||
} else if(stats.level == 2) {
|
||||
one_shot_view_start_animation(animation_manager->one_shot_view, &A_Levelup2_128x64);
|
||||
} else {
|
||||
furi_assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void animation_manager_switch_to_animation_view(AnimationManager* animation_manager) {
|
||||
furi_assert(animation_manager);
|
||||
furi_assert(animation_manager->one_shot_view);
|
||||
|
||||
View* prev_view = one_shot_view_get_view(animation_manager->one_shot_view);
|
||||
View* next_view = bubble_animation_get_view(animation_manager->animation_view);
|
||||
view_stack_remove_view(animation_manager->view_stack, prev_view);
|
||||
view_stack_add_view(animation_manager->view_stack, next_view);
|
||||
one_shot_view_free(animation_manager->one_shot_view);
|
||||
animation_manager->one_shot_view = NULL;
|
||||
}
|
159
applications/services/desktop/animations/animation_manager.h
Normal file
159
applications/services/desktop/animations/animation_manager.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
#include <gui/icon_i.h>
|
||||
#include <stdint.h>
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
typedef struct AnimationManager AnimationManager;
|
||||
|
||||
typedef struct {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
const char* text;
|
||||
Align align_h;
|
||||
Align align_v;
|
||||
} Bubble;
|
||||
|
||||
typedef struct FrameBubble {
|
||||
Bubble bubble;
|
||||
uint8_t start_frame;
|
||||
uint8_t end_frame;
|
||||
const struct FrameBubble* next_bubble;
|
||||
} FrameBubble;
|
||||
|
||||
typedef struct {
|
||||
const FrameBubble* const* frame_bubble_sequences;
|
||||
uint8_t frame_bubble_sequences_count;
|
||||
const Icon icon_animation;
|
||||
const uint8_t* frame_order;
|
||||
uint8_t passive_frames;
|
||||
uint8_t active_frames;
|
||||
uint8_t active_cycles;
|
||||
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
|
||||
* @return true if event was consumed
|
||||
*/
|
||||
bool animation_manager_interact_process(AnimationManager* animation_manager);
|
||||
|
||||
/** Check if animation loaded
|
||||
*
|
||||
* @animation_manager instance
|
||||
*/
|
||||
bool animation_manager_is_animation_loaded(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);
|
535
applications/services/desktop/animations/animation_storage.c
Normal file
535
applications/services/desktop/animations/animation_storage.c
Normal file
@@ -0,0 +1,535 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#include <furi.h>
|
||||
#include <core/dangerous_defines.h>
|
||||
#include <storage/storage.h>
|
||||
#include <gui/icon_i.h>
|
||||
#include <m-string.h>
|
||||
|
||||
#include "animation_manager.h"
|
||||
#include "animation_storage.h"
|
||||
#include "animation_storage_i.h"
|
||||
#include <assets_dolphin_internal.h>
|
||||
#include <assets_dolphin_blocking.h>
|
||||
|
||||
#define ANIMATION_META_FILE "meta.txt"
|
||||
#define ANIMATION_DIR EXT_PATH("dolphin")
|
||||
#define ANIMATION_MANIFEST_FILE ANIMATION_DIR "/manifest.txt"
|
||||
#define TAG "AnimationStorage"
|
||||
|
||||
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);
|
||||
|
||||
static bool animation_storage_load_single_manifest_info(
|
||||
StorageAnimationManifestInfo* manifest_info,
|
||||
const char* name) {
|
||||
furi_assert(manifest_info);
|
||||
|
||||
bool result = false;
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
flipper_format_set_strict_mode(file, true);
|
||||
string_t read_string;
|
||||
string_init(read_string);
|
||||
|
||||
do {
|
||||
uint32_t u32value;
|
||||
if(FSE_OK != storage_sd_status(storage)) break;
|
||||
if(!flipper_format_file_open_existing(file, ANIMATION_MANIFEST_FILE)) break;
|
||||
|
||||
if(!flipper_format_read_header(file, read_string, &u32value)) break;
|
||||
if(string_cmp_str(read_string, "Flipper Animation Manifest")) break;
|
||||
|
||||
manifest_info->name = NULL;
|
||||
|
||||
/* skip other animation names */
|
||||
flipper_format_set_strict_mode(file, false);
|
||||
while(flipper_format_read_string(file, "Name", read_string) &&
|
||||
string_cmp_str(read_string, name))
|
||||
;
|
||||
if(string_cmp_str(read_string, name)) break;
|
||||
flipper_format_set_strict_mode(file, true);
|
||||
|
||||
manifest_info->name = malloc(string_size(read_string) + 1);
|
||||
strcpy((char*)manifest_info->name, string_get_cstr(read_string));
|
||||
|
||||
if(!flipper_format_read_uint32(file, "Min butthurt", &u32value, 1)) break;
|
||||
manifest_info->min_butthurt = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Max butthurt", &u32value, 1)) break;
|
||||
manifest_info->max_butthurt = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Min level", &u32value, 1)) break;
|
||||
manifest_info->min_level = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Max level", &u32value, 1)) break;
|
||||
manifest_info->max_level = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Weight", &u32value, 1)) break;
|
||||
manifest_info->weight = u32value;
|
||||
result = true;
|
||||
} while(0);
|
||||
|
||||
if(!result && manifest_info->name) {
|
||||
free((void*)manifest_info->name);
|
||||
}
|
||||
string_clear(read_string);
|
||||
flipper_format_free(file);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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(RECORD_STORAGE);
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
/* Forbid skipping fields */
|
||||
flipper_format_set_strict_mode(file, true);
|
||||
string_t read_string;
|
||||
string_init(read_string);
|
||||
|
||||
do {
|
||||
uint32_t u32value;
|
||||
StorageAnimation* storage_animation = NULL;
|
||||
|
||||
if(FSE_OK != storage_sd_status(storage)) break;
|
||||
if(!flipper_format_file_open_existing(file, ANIMATION_MANIFEST_FILE)) break;
|
||||
if(!flipper_format_read_header(file, read_string, &u32value)) break;
|
||||
if(string_cmp_str(read_string, "Flipper Animation Manifest")) break;
|
||||
do {
|
||||
storage_animation = malloc(sizeof(StorageAnimation));
|
||||
storage_animation->external = true;
|
||||
storage_animation->animation = NULL;
|
||||
storage_animation->manifest_info.name = NULL;
|
||||
|
||||
if(!flipper_format_read_string(file, "Name", read_string)) break;
|
||||
storage_animation->manifest_info.name = malloc(string_size(read_string) + 1);
|
||||
strcpy((char*)storage_animation->manifest_info.name, string_get_cstr(read_string));
|
||||
|
||||
if(!flipper_format_read_uint32(file, "Min butthurt", &u32value, 1)) break;
|
||||
storage_animation->manifest_info.min_butthurt = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Max butthurt", &u32value, 1)) break;
|
||||
storage_animation->manifest_info.max_butthurt = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Min level", &u32value, 1)) break;
|
||||
storage_animation->manifest_info.min_level = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Max level", &u32value, 1)) break;
|
||||
storage_animation->manifest_info.max_level = u32value;
|
||||
if(!flipper_format_read_uint32(file, "Weight", &u32value, 1)) break;
|
||||
storage_animation->manifest_info.weight = u32value;
|
||||
|
||||
StorageAnimationList_push_back(*animation_list, storage_animation);
|
||||
} while(1);
|
||||
|
||||
animation_storage_free_storage_animation(&storage_animation);
|
||||
} while(0);
|
||||
|
||||
string_clear(read_string);
|
||||
flipper_format_free(file);
|
||||
|
||||
// add hard-coded animations
|
||||
for(size_t i = 0; i < dolphin_internal_size; ++i) {
|
||||
StorageAnimationList_push_back(*animation_list, (StorageAnimation*)&dolphin_internal[i]);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
StorageAnimation* animation_storage_find_animation(const char* name) {
|
||||
furi_assert(name);
|
||||
furi_assert(strlen(name));
|
||||
StorageAnimation* storage_animation = NULL;
|
||||
|
||||
for(size_t i = 0; i < dolphin_blocking_size; ++i) {
|
||||
if(!strcmp(dolphin_blocking[i].manifest_info.name, name)) {
|
||||
storage_animation = (StorageAnimation*)&dolphin_blocking[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!storage_animation) {
|
||||
for(size_t i = 0; i < dolphin_internal_size; ++i) {
|
||||
if(!strcmp(dolphin_internal[i].manifest_info.name, name)) {
|
||||
storage_animation = (StorageAnimation*)&dolphin_internal[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* look through external animations */
|
||||
if(!storage_animation) {
|
||||
storage_animation = malloc(sizeof(StorageAnimation));
|
||||
storage_animation->external = true;
|
||||
|
||||
bool result = false;
|
||||
result =
|
||||
animation_storage_load_single_manifest_info(&storage_animation->manifest_info, name);
|
||||
if(result) {
|
||||
storage_animation->animation = animation_storage_load_animation(name);
|
||||
result = !!storage_animation->animation;
|
||||
}
|
||||
if(!result) {
|
||||
animation_storage_free_storage_animation(&storage_animation);
|
||||
}
|
||||
}
|
||||
|
||||
return storage_animation;
|
||||
}
|
||||
|
||||
StorageAnimationManifestInfo* animation_storage_get_meta(StorageAnimation* storage_animation) {
|
||||
furi_assert(storage_animation);
|
||||
return &storage_animation->manifest_info;
|
||||
}
|
||||
|
||||
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(storage_animation->manifest_info.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void animation_storage_free_animation(BubbleAnimation** animation) {
|
||||
furi_assert(animation);
|
||||
|
||||
if(*animation) {
|
||||
animation_storage_free_bubbles(*animation);
|
||||
animation_storage_free_frames(*animation);
|
||||
if((*animation)->frame_order) {
|
||||
free((void*)(*animation)->frame_order);
|
||||
}
|
||||
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);
|
||||
|
||||
if((*storage_animation)->manifest_info.name) {
|
||||
free((void*)(*storage_animation)->manifest_info.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);
|
||||
|
||||
const Icon* icon = &animation->icon_animation;
|
||||
for(int i = 0; i < icon->frame_count; ++i) {
|
||||
if(icon->frames[i]) {
|
||||
free((void*)icon->frames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
free((void*)icon->frames);
|
||||
}
|
||||
|
||||
static bool animation_storage_load_frames(
|
||||
Storage* storage,
|
||||
const char* name,
|
||||
BubbleAnimation* animation,
|
||||
uint32_t* frame_order,
|
||||
uint8_t width,
|
||||
uint8_t height) {
|
||||
uint16_t frame_order_count = animation->passive_frames + animation->active_frames;
|
||||
|
||||
/* The frames should go in order (0...N), without omissions */
|
||||
size_t max_frame_count = 0;
|
||||
for(int i = 0; i < frame_order_count; ++i) {
|
||||
max_frame_count = MAX(max_frame_count, frame_order[i]);
|
||||
}
|
||||
|
||||
if((max_frame_count >= frame_order_count) || (max_frame_count >= 256 /* max uint8_t */)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Icon* icon = (Icon*)&animation->icon_animation;
|
||||
FURI_CONST_ASSIGN(icon->frame_count, max_frame_count + 1);
|
||||
FURI_CONST_ASSIGN(icon->frame_rate, 0);
|
||||
FURI_CONST_ASSIGN(icon->height, height);
|
||||
FURI_CONST_ASSIGN(icon->width, width);
|
||||
icon->frames = malloc(sizeof(const uint8_t*) * icon->frame_count);
|
||||
|
||||
bool frames_ok = false;
|
||||
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 < icon->frame_count; ++i) {
|
||||
frames_ok = false;
|
||||
string_printf(filename, ANIMATION_DIR "/%s/frame_%d.bm", name, 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;
|
||||
}
|
||||
|
||||
FURI_CONST_ASSIGN_PTR(icon->frames[i], malloc(file_info.size));
|
||||
if(storage_file_read(file, (void*)icon->frames[i], file_info.size) != file_info.size) {
|
||||
FURI_LOG_E(TAG, "Read failed: \'%s\'", string_get_cstr(filename));
|
||||
break;
|
||||
}
|
||||
storage_file_close(file);
|
||||
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);
|
||||
} else {
|
||||
furi_check(animation->icon_animation.frames);
|
||||
for(int i = 0; i < animation->icon_animation.frame_count; ++i) {
|
||||
furi_check(animation->icon_animation.frames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
string_clear(filename);
|
||||
|
||||
return frames_ok;
|
||||
}
|
||||
|
||||
static bool animation_storage_load_bubbles(BubbleAnimation* animation, FlipperFormat* ff) {
|
||||
uint32_t u32value;
|
||||
string_t str;
|
||||
string_init(str);
|
||||
bool success = false;
|
||||
furi_assert(!animation->frame_bubble_sequences);
|
||||
|
||||
do {
|
||||
if(!flipper_format_read_uint32(ff, "Bubble slots", &u32value, 1)) break;
|
||||
if(u32value > 20) break;
|
||||
animation->frame_bubble_sequences_count = u32value;
|
||||
if(animation->frame_bubble_sequences_count == 0) {
|
||||
animation->frame_bubble_sequences = NULL;
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
animation->frame_bubble_sequences =
|
||||
malloc(sizeof(FrameBubble*) * animation->frame_bubble_sequences_count);
|
||||
|
||||
int32_t current_slot = 0;
|
||||
for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) {
|
||||
FURI_CONST_ASSIGN_PTR(
|
||||
animation->frame_bubble_sequences[i], malloc(sizeof(FrameBubble)));
|
||||
}
|
||||
|
||||
const FrameBubble* bubble = animation->frame_bubble_sequences[0];
|
||||
int8_t index = -1;
|
||||
for(;;) {
|
||||
if(!flipper_format_read_int32(ff, "Slot", ¤t_slot, 1)) break;
|
||||
if((current_slot != 0) && (index == -1)) break;
|
||||
|
||||
if(current_slot == index) {
|
||||
FURI_CONST_ASSIGN_PTR(bubble->next_bubble, malloc(sizeof(FrameBubble)));
|
||||
bubble = bubble->next_bubble;
|
||||
} else if(current_slot == index + 1) {
|
||||
++index;
|
||||
bubble = animation->frame_bubble_sequences[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_bubble_sequences_count) break;
|
||||
|
||||
if(!flipper_format_read_uint32(ff, "X", &u32value, 1)) break;
|
||||
FURI_CONST_ASSIGN(bubble->bubble.x, u32value);
|
||||
if(!flipper_format_read_uint32(ff, "Y", &u32value, 1)) break;
|
||||
FURI_CONST_ASSIGN(bubble->bubble.y, u32value);
|
||||
|
||||
if(!flipper_format_read_string(ff, "Text", str)) break;
|
||||
if(string_size(str) > 100) break;
|
||||
|
||||
string_replace_all_str(str, "\\n", "\n");
|
||||
|
||||
FURI_CONST_ASSIGN_PTR(bubble->bubble.text, malloc(string_size(str) + 1));
|
||||
strcpy((char*)bubble->bubble.text, string_get_cstr(str));
|
||||
|
||||
if(!flipper_format_read_string(ff, "AlignH", str)) break;
|
||||
if(!animation_storage_cast_align(str, (Align*)&bubble->bubble.align_h)) break;
|
||||
if(!flipper_format_read_string(ff, "AlignV", str)) break;
|
||||
if(!animation_storage_cast_align(str, (Align*)&bubble->bubble.align_v)) break;
|
||||
|
||||
if(!flipper_format_read_uint32(ff, "StartFrame", &u32value, 1)) break;
|
||||
FURI_CONST_ASSIGN(bubble->start_frame, u32value);
|
||||
if(!flipper_format_read_uint32(ff, "EndFrame", &u32value, 1)) break;
|
||||
FURI_CONST_ASSIGN(bubble->end_frame, u32value);
|
||||
}
|
||||
success = (index + 1) == animation->frame_bubble_sequences_count;
|
||||
} while(0);
|
||||
|
||||
if(!success) {
|
||||
if(animation->frame_bubble_sequences) {
|
||||
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 = malloc(sizeof(BubbleAnimation));
|
||||
|
||||
uint32_t height = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t* u32array = NULL;
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* ff = flipper_format_file_alloc(storage);
|
||||
/* Forbid skipping fields */
|
||||
flipper_format_set_strict_mode(ff, true);
|
||||
string_t str;
|
||||
string_init(str);
|
||||
animation->frame_bubble_sequences = 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_format_file_open_existing(ff, string_get_cstr(str))) break;
|
||||
if(!flipper_format_read_header(ff, str, &u32value)) break;
|
||||
if(string_cmp_str(str, "Flipper Animation")) break;
|
||||
|
||||
if(!flipper_format_read_uint32(ff, "Width", &width, 1)) break;
|
||||
if(!flipper_format_read_uint32(ff, "Height", &height, 1)) break;
|
||||
|
||||
if(!flipper_format_read_uint32(ff, "Passive frames", &u32value, 1)) break;
|
||||
animation->passive_frames = u32value;
|
||||
if(!flipper_format_read_uint32(ff, "Active frames", &u32value, 1)) break;
|
||||
animation->active_frames = u32value;
|
||||
|
||||
uint8_t frames = animation->passive_frames + animation->active_frames;
|
||||
uint32_t count = 0;
|
||||
if(!flipper_format_get_value_count(ff, "Frames order", &count)) break;
|
||||
if(count != frames) {
|
||||
FURI_LOG_E(TAG, "Error loading animation: frames order");
|
||||
break;
|
||||
}
|
||||
u32array = malloc(sizeof(uint32_t) * frames);
|
||||
if(!flipper_format_read_uint32(ff, "Frames order", u32array, frames)) break;
|
||||
animation->frame_order = malloc(sizeof(uint8_t) * frames);
|
||||
for(int i = 0; i < frames; ++i) {
|
||||
FURI_CONST_ASSIGN(animation->frame_order[i], u32array[i]);
|
||||
}
|
||||
|
||||
/* 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_format_read_uint32(ff, "Active cycles", &u32value, 1)) break;
|
||||
animation->active_cycles = u32value;
|
||||
if(!flipper_format_read_uint32(ff, "Frame rate", &u32value, 1)) break;
|
||||
FURI_CONST_ASSIGN(animation->icon_animation.frame_rate, u32value);
|
||||
if(!flipper_format_read_uint32(ff, "Duration", &u32value, 1)) break;
|
||||
animation->duration = u32value;
|
||||
if(!flipper_format_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_format_free(ff);
|
||||
if(u32array) {
|
||||
free(u32array);
|
||||
}
|
||||
|
||||
if(!success) {
|
||||
if(animation->frame_order) {
|
||||
free((void*)animation->frame_order);
|
||||
}
|
||||
free(animation);
|
||||
animation = NULL;
|
||||
}
|
||||
|
||||
return animation;
|
||||
}
|
||||
|
||||
static void animation_storage_free_bubbles(BubbleAnimation* animation) {
|
||||
if(!animation->frame_bubble_sequences) return;
|
||||
|
||||
for(int i = 0; i < animation->frame_bubble_sequences_count;) {
|
||||
const FrameBubble* const* bubble = &animation->frame_bubble_sequences[i];
|
||||
|
||||
if((*bubble) == NULL) break;
|
||||
|
||||
while((*bubble)->next_bubble != NULL) {
|
||||
bubble = &(*bubble)->next_bubble;
|
||||
}
|
||||
|
||||
if((*bubble)->bubble.text) {
|
||||
free((void*)(*bubble)->bubble.text);
|
||||
}
|
||||
if((*bubble) == animation->frame_bubble_sequences[i]) {
|
||||
++i;
|
||||
}
|
||||
free((void*)*bubble);
|
||||
FURI_CONST_ASSIGN_PTR(*bubble, NULL);
|
||||
}
|
||||
free((void*)animation->frame_bubble_sequences);
|
||||
animation->frame_bubble_sequences = NULL;
|
||||
}
|
89
applications/services/desktop/animations/animation_storage.h
Normal file
89
applications/services/desktop/animations/animation_storage.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <m-list.h>
|
||||
#include "views/bubble_animation_view.h"
|
||||
#include <m-string.h>
|
||||
|
||||
/** 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 {
|
||||
const char* name;
|
||||
uint8_t min_butthurt;
|
||||
uint8_t max_butthurt;
|
||||
uint8_t min_level;
|
||||
uint8_t max_level;
|
||||
uint8_t weight;
|
||||
} StorageAnimationManifestInfo;
|
||||
|
||||
/** 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
|
||||
*/
|
||||
StorageAnimationManifestInfo* 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);
|
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include "animation_storage.h"
|
||||
#include "animation_manager.h"
|
||||
|
||||
struct StorageAnimation {
|
||||
const BubbleAnimation* animation;
|
||||
bool external;
|
||||
StorageAnimationManifestInfo manifest_info;
|
||||
};
|
@@ -0,0 +1,409 @@
|
||||
|
||||
#include "../animation_manager.h"
|
||||
#include "../animation_storage.h"
|
||||
#include "bubble_animation_view.h"
|
||||
|
||||
#include <furi_hal.h>
|
||||
#include <furi.h>
|
||||
#include <gui/canvas.h>
|
||||
#include <gui/elements.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/icon_i.h>
|
||||
#include <input/input.h>
|
||||
#include <stdint.h>
|
||||
#include <core/dangerous_defines.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;
|
||||
FuriTimer* 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_frame_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 animation->frame_order[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);
|
||||
|
||||
uint8_t index = bubble_animation_get_frame_index(model);
|
||||
uint8_t width = icon_get_width(&animation->icon_animation);
|
||||
uint8_t height = icon_get_height(&animation->icon_animation);
|
||||
uint8_t y_offset = canvas_height(canvas) - height;
|
||||
canvas_draw_bitmap(
|
||||
canvas, 0, y_offset, width, height, animation->icon_animation.frames[index]);
|
||||
|
||||
const FrameBubble* bubble = model->current_bubble;
|
||||
if(bubble) {
|
||||
if((model->current_frame >= bubble->start_frame) &&
|
||||
(model->current_frame <= bubble->end_frame)) {
|
||||
const Bubble* b = &bubble->bubble;
|
||||
elements_bubble_str(canvas, b->x, b->y, b->text, b->align_h, b->align_v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const FrameBubble*
|
||||
bubble_animation_pick_bubble(BubbleAnimationViewModel* model, bool active) {
|
||||
const FrameBubble* bubble = NULL;
|
||||
|
||||
if((model->active_bubbles == 0) && (model->passive_bubbles == 0)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t index =
|
||||
furi_hal_random_get() % (active ? model->active_bubbles : model->passive_bubbles);
|
||||
const BubbleAnimation* animation = model->current;
|
||||
|
||||
for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) {
|
||||
if((animation->frame_bubble_sequences[i]->start_frame < animation->passive_frames) ^
|
||||
active) {
|
||||
if(!index) {
|
||||
bubble = animation->frame_bubble_sequences[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 == NULL) {
|
||||
activate = false;
|
||||
} else if(model->freeze_frame) {
|
||||
activate = false;
|
||||
} else if(model->current->active_frames == 0) {
|
||||
activate = false;
|
||||
}
|
||||
|
||||
if(model->current != NULL) {
|
||||
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->icon_animation.frame_rate;
|
||||
}
|
||||
view_commit_model(view->view, true);
|
||||
|
||||
if(frame_rate) {
|
||||
furi_timer_start(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->end_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);
|
||||
}
|
||||
}
|
||||
|
||||
/* always freeze first passive frame, because
|
||||
* animation is always activated at unfreezing and played
|
||||
* passive frame first, and 2 frames after - active
|
||||
*/
|
||||
static Icon* bubble_animation_clone_first_frame(const Icon* icon_orig) {
|
||||
furi_assert(icon_orig);
|
||||
furi_assert(icon_orig->frames);
|
||||
furi_assert(icon_orig->frames[0]);
|
||||
|
||||
Icon* icon_clone = malloc(sizeof(Icon));
|
||||
memcpy(icon_clone, icon_orig, sizeof(Icon));
|
||||
|
||||
icon_clone->frames = malloc(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;
|
||||
FURI_CONST_ASSIGN_PTR(icon_clone->frames[0], malloc(max_bitmap_size));
|
||||
memcpy((void*)icon_clone->frames[0], icon_orig->frames[0], max_bitmap_size);
|
||||
FURI_CONST_ASSIGN(icon_clone->frame_count, 1);
|
||||
|
||||
return icon_clone;
|
||||
}
|
||||
|
||||
static void bubble_animation_release_frame(Icon** icon) {
|
||||
furi_assert(icon);
|
||||
furi_assert(*icon);
|
||||
|
||||
free((void*)(*icon)->frames[0]);
|
||||
free((void*)(*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 = 0;
|
||||
if(model->current != NULL) {
|
||||
frame_rate = model->current->icon_animation.frame_rate;
|
||||
}
|
||||
view_commit_model(view->view, false);
|
||||
|
||||
if(frame_rate) {
|
||||
furi_timer_start(view->timer, 1000 / frame_rate);
|
||||
}
|
||||
}
|
||||
|
||||
static void bubble_animation_exit(void* context) {
|
||||
furi_assert(context);
|
||||
BubbleAnimationView* view = context;
|
||||
furi_timer_stop(view->timer);
|
||||
}
|
||||
|
||||
BubbleAnimationView* bubble_animation_view_alloc(void) {
|
||||
BubbleAnimationView* view = malloc(sizeof(BubbleAnimationView));
|
||||
view->view = view_alloc();
|
||||
view->interact_callback = NULL;
|
||||
view->timer = furi_timer_alloc(bubble_animation_timer_callback, FuriTimerTypePeriodic, view);
|
||||
|
||||
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_bubble_sequences_count; ++i) {
|
||||
if(new_animation->frame_bubble_sequences[i]->start_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);
|
||||
|
||||
furi_timer_start(view->timer, 1000 / new_animation->icon_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);
|
||||
model->freeze_frame = bubble_animation_clone_first_frame(&model->current->icon_animation);
|
||||
model->current = NULL;
|
||||
view_commit_model(view->view, false);
|
||||
furi_timer_stop(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);
|
||||
frame_rate = model->current->icon_animation.frame_rate;
|
||||
view_commit_model(view->view, true);
|
||||
|
||||
furi_timer_start(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);
|
@@ -0,0 +1,130 @@
|
||||
|
||||
#include "one_shot_animation_view.h"
|
||||
#include <furi.h>
|
||||
#include <portmacro.h>
|
||||
#include <gui/canvas.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/icon_i.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void (*OneShotInteractCallback)(void*);
|
||||
|
||||
struct OneShotView {
|
||||
View* view;
|
||||
TimerHandle_t update_timer;
|
||||
OneShotInteractCallback interact_callback;
|
||||
void* interact_callback_context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const Icon* icon;
|
||||
uint32_t index;
|
||||
bool block_input;
|
||||
} OneShotViewModel;
|
||||
|
||||
static void one_shot_view_update_timer_callback(TimerHandle_t xTimer) {
|
||||
OneShotView* view = (void*)pvTimerGetTimerID(xTimer);
|
||||
|
||||
OneShotViewModel* model = view_get_model(view->view);
|
||||
if((model->index + 1) < model->icon->frame_count) {
|
||||
++model->index;
|
||||
} else {
|
||||
model->block_input = false;
|
||||
model->index = model->icon->frame_count - 2;
|
||||
}
|
||||
view_commit_model(view->view, true);
|
||||
}
|
||||
|
||||
static void one_shot_view_draw(Canvas* canvas, void* model_) {
|
||||
furi_assert(canvas);
|
||||
furi_assert(model_);
|
||||
|
||||
OneShotViewModel* model = model_;
|
||||
furi_check(model->index < model->icon->frame_count);
|
||||
uint8_t y_offset = canvas_height(canvas) - model->icon->height;
|
||||
canvas_draw_bitmap(
|
||||
canvas,
|
||||
0,
|
||||
y_offset,
|
||||
model->icon->width,
|
||||
model->icon->height,
|
||||
model->icon->frames[model->index]);
|
||||
}
|
||||
|
||||
static bool one_shot_view_input(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
furi_assert(event);
|
||||
|
||||
OneShotView* view = context;
|
||||
bool consumed = false;
|
||||
|
||||
OneShotViewModel* model = view_get_model(view->view);
|
||||
consumed = model->block_input;
|
||||
view_commit_model(view->view, false);
|
||||
|
||||
if(!consumed) {
|
||||
if(event->key == InputKeyRight) {
|
||||
/* Right button reserved for animation activation, so consume */
|
||||
consumed = true;
|
||||
if(event->type == InputTypeShort) {
|
||||
if(view->interact_callback) {
|
||||
view->interact_callback(view->interact_callback_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
OneShotView* one_shot_view_alloc(void) {
|
||||
OneShotView* view = malloc(sizeof(OneShotView));
|
||||
view->view = view_alloc();
|
||||
view->update_timer =
|
||||
xTimerCreate(NULL, 1000, pdTRUE, view, one_shot_view_update_timer_callback);
|
||||
|
||||
view_allocate_model(view->view, ViewModelTypeLocking, sizeof(OneShotViewModel));
|
||||
view_set_context(view->view, view);
|
||||
view_set_draw_callback(view->view, one_shot_view_draw);
|
||||
view_set_input_callback(view->view, one_shot_view_input);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
void one_shot_view_free(OneShotView* view) {
|
||||
furi_assert(view);
|
||||
|
||||
xTimerDelete(view->update_timer, portMAX_DELAY);
|
||||
view_free(view->view);
|
||||
view->view = NULL;
|
||||
free(view);
|
||||
}
|
||||
|
||||
void one_shot_view_set_interact_callback(
|
||||
OneShotView* view,
|
||||
OneShotInteractCallback callback,
|
||||
void* context) {
|
||||
furi_assert(view);
|
||||
|
||||
view->interact_callback_context = context;
|
||||
view->interact_callback = callback;
|
||||
}
|
||||
|
||||
void one_shot_view_start_animation(OneShotView* view, const Icon* icon) {
|
||||
furi_assert(view);
|
||||
furi_assert(icon);
|
||||
furi_check(icon->frame_count >= 2);
|
||||
|
||||
OneShotViewModel* model = view_get_model(view->view);
|
||||
model->index = 0;
|
||||
model->icon = icon;
|
||||
model->block_input = true;
|
||||
view_commit_model(view->view, true);
|
||||
xTimerChangePeriod(view->update_timer, 1000 / model->icon->frame_rate, portMAX_DELAY);
|
||||
}
|
||||
|
||||
View* one_shot_view_get_view(OneShotView* view) {
|
||||
furi_assert(view);
|
||||
|
||||
return view->view;
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/view.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void (*OneShotInteractCallback)(void*);
|
||||
typedef struct OneShotView OneShotView;
|
||||
|
||||
OneShotView* one_shot_view_alloc(void);
|
||||
void one_shot_view_free(OneShotView* view);
|
||||
void one_shot_view_set_interact_callback(
|
||||
OneShotView* view,
|
||||
OneShotInteractCallback callback,
|
||||
void* context);
|
||||
void one_shot_view_start_animation(OneShotView* view, const Icon* icon);
|
||||
View* one_shot_view_get_view(OneShotView* view);
|
Reference in New Issue
Block a user