diff --git a/main/modes/routine.c b/main/modes/routine.c index fb24dd5..6403b70 100644 --- a/main/modes/routine.c +++ b/main/modes/routine.c @@ -230,10 +230,15 @@ static void step_home_cb(lv_event_t *e) { (void)e; lisael_aac_stop(); - lisael_ui_goto(LISAEL_MODE_HOME); - // The step screen is rebuilt fresh on next open_routine(); drop the ref so - // the next build replaces it. (It is auto-deleted on the next load_anim.) + lv_obj_t *old = s_step_screen; s_step_screen = NULL; + lisael_ui_goto(LISAEL_MODE_HOME); + // lisael_ui_goto uses auto_del=false (cached mode screens), so the transient + // step screen would leak without an explicit delete. Schedule it after the + // 250 ms slide animation finishes so it is off-screen before deletion. + if (old) { + lv_obj_delete_delayed(old, 350); + } } static void done_cb(lv_event_t *e) @@ -331,10 +336,113 @@ static void open_routine(int ridx) build_step_screen(); } -// Provisional: real body lands in Task 4c (the Bravo screen). -static void show_bravo(void) +// --- Bravo screen (Task 4c) -------------------------------------------------- + +// One-shot timer pointer shared between the auto-return timer and the tap +// handler so each can cancel the other without a dangling pointer. +static lv_timer_t *s_bravo_timer; + +// Return HOME from the bravo screen. Used by both the tap handler and the +// auto-return timer. Idempotent (second call is a no-op). +static void bravo_go_home(void) { + // Cancel the auto-return timer if the tap fired first, and vice versa. + if (s_bravo_timer) { + lv_timer_delete(s_bravo_timer); + s_bravo_timer = NULL; + } + // Grab and clear s_step_screen BEFORE the goto to prevent re-entry. + lv_obj_t *old = s_step_screen; + s_step_screen = NULL; lisael_aac_stop(); lisael_ui_goto(LISAEL_MODE_HOME); - s_step_screen = NULL; + // lisael_ui_goto uses auto_del=false for cached mode screens, so the + // transient bravo screen must be freed explicitly. Schedule after 350 ms + // (> 250 ms slide anim) so it is already off-screen before deletion. + if (old) { + lv_obj_delete_delayed(old, 350); + } +} + +static void bravo_home_cb(lv_event_t *e) +{ + (void)e; + bravo_go_home(); +} + +static void bravo_auto_return_cb(lv_timer_t *t) +{ + (void)t; + // Timer self-deletes when it is one-shot (repeat_count=1). Clear the + // pointer first so bravo_go_home() does not try to delete it again. + s_bravo_timer = NULL; + bravo_go_home(); +} + +// "Pop-in" scale animation for the star. Uses the style transform approach +// (pattern from balloons.c pop_scale_cb) — NOT lv_image_set_scale, which +// leaves the layout box at full size and overflows its cell (see histoires.c). +static void bravo_scale_cb(void *var, int32_t scale) +{ + lv_obj_set_style_transform_scale_x((lv_obj_t *)var, scale, 0); + lv_obj_set_style_transform_scale_y((lv_obj_t *)var, scale, 0); +} + +static void show_bravo(void) +{ + lv_obj_t *scr = lv_obj_create(NULL); + lv_obj_set_style_bg_color(scr, lv_color_hex(0xFFF4C2), 0); + lv_obj_set_style_bg_grad_color(scr, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_bg_grad_dir(scr, LV_GRAD_DIR_VER, 0); + + // Star image: pop-in from 0.33x to 1x with overshoot (bouncy feel). + lv_obj_t *star = lv_image_create(scr); + lv_image_set_src(star, &lisael_ic_star); + lv_obj_set_size(star, 140, 140); + lv_image_set_inner_align(star, LV_IMAGE_ALIGN_CONTAIN); + lv_obj_align(star, LV_ALIGN_CENTER, 0, -40); + // Pivot at centre so the scale grows in-place. + lv_obj_set_style_transform_pivot_x(star, 70, 0); + lv_obj_set_style_transform_pivot_y(star, 70, 0); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, star); + lv_anim_set_exec_cb(&a, bravo_scale_cb); + lv_anim_set_values(&a, LV_SCALE_NONE / 3, LV_SCALE_NONE); // 0.33x → 1x + lv_anim_set_duration(&a, 400); + lv_anim_set_path_cb(&a, lv_anim_path_overshoot); + lv_anim_start(&a); + + // "Bravo !" label (Fredoka large, centred below the star). + lv_obj_t *txt = lv_label_create(scr); + lv_label_set_text(txt, "Bravo !"); + lv_obj_set_style_text_font(txt, &lisael_font_30, 0); + lv_obj_align(txt, LV_ALIGN_CENTER, 0, 50); + + // "Accueil" button: tap returns HOME immediately (also cancels the timer). + lv_obj_t *home = lv_button_create(scr); + lv_obj_add_style(home, lisael_ui_style_button(), 0); + lv_obj_set_style_bg_color(home, lv_color_hex(0x1971C2), 0); + lv_obj_set_size(home, 200, 60); + lv_obj_align(home, LV_ALIGN_BOTTOM_MID, 0, -16); + lv_obj_add_event_cb(home, bravo_home_cb, LV_EVENT_CLICKED, NULL); + lv_obj_t *hl = lv_label_create(home); + lv_label_set_text(hl, LV_SYMBOL_HOME " Accueil"); + lv_obj_set_style_text_color(hl, lv_color_white(), 0); + lv_obj_set_style_text_font(hl, &lisael_font_24, 0); + lv_obj_center(hl); + + // Celebratory sound (routine asset; silent fallback if absent on SD). + // provided by Tower routine seed + lisael_aac_play_file("/sdcard/routines/bravo.mp3"); + + // Load with fade; auto_del=true here deletes the outgoing STEP screen, + // which LVGL was already tracking as the active screen. + lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_FADE_IN, 250, 0, true); + s_step_screen = scr; // track the bravo screen for explicit cleanup on return + + // Auto-return after 2.2 s (one-shot timer). The tap handler deletes it. + s_bravo_timer = lv_timer_create(bravo_auto_return_cb, 2200, NULL); + lv_timer_set_repeat_count(s_bravo_timer, 1); }