From 94917f633df9ed2170979c2e6386b48013143959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Saillant?= Date: Sun, 14 Jun 2026 17:23:38 +0200 Subject: [PATCH] feat(histoires): add vignette podcast browser Replace the flat "Sons" tile and podcasts.c RSS stub with a 3-level LVGL screen that browses shows stored on the SD card. - Add modes/histoires.c: paged 2x2 cover grid (level 1), episode list per show (level 2), full-screen player with prev/toggle/next transport (level 3). - Wire lisael_screen_histoires() into ui.c build_screen(). - Remove LISAEL_MODE_SOUNDBOX from the enum and drop the "Sons" tile from home.c (5 tiles remain). - Delete modes/soundbox.c (git rm) and modes/podcasts.c (rm). - Update CMakeLists.txt: remove soundbox.c / podcasts.c, add histoires.c. Playback dispatches by extension: .m4a -> aac_player, otherwise -> sd_player (MP3). Covers are RGB565 .bin files loaded via the LVGL stdio FS driver; a coloured initial fallback is shown when no cover is available. --- main/CMakeLists.txt | 3 +- main/modes/histoires.c | 343 +++++++++++++++++++++++++++++++++++++++++ main/modes/home.c | 63 +++++--- main/modes/soundbox.c | 91 ----------- main/ui/ui.c | 31 +++- main/ui/ui.h | 5 +- 6 files changed, 408 insertions(+), 128 deletions(-) create mode 100644 main/modes/histoires.c delete mode 100644 main/modes/soundbox.c diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index baa41ae..fdce708 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -21,12 +21,11 @@ set(srcs "ui/icons.c" "modes/home.c" "modes/radio.c" - "modes/soundbox.c" "modes/calm.c" "modes/clock.c" "modes/games.c" "modes/balloons.c" - "modes/podcasts.c" + "modes/histoires.c" "audio/audio_player.c" "audio/radio_pipeline.c" "audio/sd_player.c" diff --git a/main/modes/histoires.c b/main/modes/histoires.c new file mode 100644 index 0000000..e30a6e3 --- /dev/null +++ b/main/modes/histoires.c @@ -0,0 +1,343 @@ +// "Histoires" — Merlin-style vignette browser for the Lisael Box. +// +// 3 levels, each its own LVGL screen, navigated with lv_screen_load_anim: +// 1. paged 2x2 grid of podcast covers (lisael_screen_histoires()) +// 2. episode list for the chosen show +// 3. full-screen player (cover + title + transport) +// +// Screen lifecycle: forward navigation keeps the parent alive (auto_del=false); +// back navigation deletes the screen being left (auto_del=true) to avoid leaks. +// The grid screen is cached by ui.c and never auto-deleted. +// +// Covers are RGB565 .bin images on the SD, loaded via the LVGL stdio FS driver +// ("S:" -> /sdcard). Playback dispatches by extension: .m4a -> aac_player, +// otherwise -> sd_player (MP3). One audio source at a time. + +#include "ui/ui.h" +#include "audio/podcast_manifest.h" +#include "audio/aac_player.h" +#include "audio/sd_player.h" +#include "audio/audio_player.h" + +#include +#include +#include +#include +#include + +#include "esp_log.h" + +static const char *TAG = "ui.histoires"; + +static lisael_show_t s_shows[LISAEL_MAX_SHOWS]; +static int s_n_shows; +static int s_cur_show = -1; +static int s_cur_ep = -1; + +static lv_obj_t *s_grid_screen; // cached level-1 screen (owned by ui.c cache) +static lv_obj_t *s_list_screen; // rebuilt per show +static lv_obj_t *s_player_screen; // rebuilt per episode + +static const uint32_t s_fallback_colors[6] = { + 0xE8590C, 0x1971C2, 0x2F9E44, 0x9C36B5, 0xE8A92C, 0xC2255C, +}; + +static lv_obj_t *build_player(int ep); // fwd decl + +// --- a cover image (or a coloured fallback tile if no/unreadable .bin) ------- +static lv_obj_t *make_cover(lv_obj_t *parent, int show_idx, int px) +{ + lisael_show_t *s = &s_shows[show_idx]; + if (s->cover[0]) { + char real[64]; + snprintf(real, sizeof(real), "/sdcard/podcasts/%s", s->cover); + struct stat st; + if (stat(real, &st) == 0 && st.st_size > 0) { + char src[72]; + snprintf(src, sizeof(src), "S:/sdcard/podcasts/%s", s->cover); + lv_obj_t *img = lv_image_create(parent); + lv_image_set_src(img, src); + lv_image_set_scale(img, (uint32_t)(px * 256 / 160)); // stored at 160px + return img; + } + } + lv_obj_t *box = lv_obj_create(parent); + lv_obj_remove_style_all(box); + lv_obj_set_size(box, px, px); + lv_obj_set_style_radius(box, 14, 0); + lv_obj_set_style_bg_opa(box, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(box, lv_color_hex(s_fallback_colors[show_idx % 6]), 0); + lv_obj_clear_flag(box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t *l = lv_label_create(box); + char init[2] = { s->show[0] ? s->show[0] : '?', '\0' }; + lv_label_set_text(l, init); + lv_obj_set_style_text_color(l, lv_color_white(), 0); + lv_obj_center(l); + return box; +} + +// --- playback ---------------------------------------------------------------- +static void play_current(void) +{ + lisael_show_t *s = &s_shows[s_cur_show]; + char fpath[96]; + snprintf(fpath, sizeof(fpath), "/sdcard/podcasts/%s", s->eps[s_cur_ep].file); + size_t len = strlen(fpath); + if (len > 4 && strcasecmp(fpath + len - 4, ".m4a") == 0) { + lisael_aac_play_file(fpath); + } else { + lisael_sd_play_file(fpath); + } + ESP_LOGI(TAG, "play %s", fpath); +} + +// --- level 3: player callbacks ---------------------------------------------- +static void player_back_cb(lv_event_t *e) +{ + (void)e; + lisael_audio_stop_all(); + lv_screen_load_anim(s_list_screen, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 220, 0, true); + s_player_screen = NULL; // deleted by auto_del above +} + +static void player_toggle_cb(lv_event_t *e) +{ + lv_obj_t *lbl = lv_obj_get_child(lv_event_get_target(e), 0); + if (lisael_aac_is_playing() || lisael_sd_is_playing()) { + lisael_audio_stop_all(); + lv_label_set_text(lbl, LV_SYMBOL_PLAY); + } else { + play_current(); + lv_label_set_text(lbl, LV_SYMBOL_STOP); + } +} + +static void player_step_cb(lv_event_t *e) +{ + int dir = (int)(intptr_t)lv_event_get_user_data(e); + int n = s_shows[s_cur_show].n_eps; + int ep = (s_cur_ep + dir + n) % n; + lisael_audio_stop_all(); + lv_obj_t *p = build_player(ep); + lv_screen_load_anim_t anim = (dir > 0) ? LV_SCR_LOAD_ANIM_MOVE_LEFT + : LV_SCR_LOAD_ANIM_MOVE_RIGHT; + // replacing the old player screen -> auto_del deletes it (no leak) + lv_screen_load_anim(p, anim, 220, 0, true); + play_current(); +} + +// build (but do not load/play) the player screen for episode `ep`. +static lv_obj_t *build_player(int ep) +{ + s_cur_ep = ep; + s_player_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_player_screen, lv_color_hex(0x0E1726), 0); + + lv_obj_t *back = lv_button_create(s_player_screen); + lv_obj_add_style(back, lisael_ui_style_button(), 0); + lv_obj_set_size(back, 64, 52); + lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6); + lv_obj_add_event_cb(back, player_back_cb, LV_EVENT_CLICKED, NULL); + lv_obj_t *bl = lv_label_create(back); + lv_label_set_text(bl, LV_SYMBOL_LEFT); + lv_obj_center(bl); + + lv_obj_t *cover = make_cover(s_player_screen, s_cur_show, 110); + lv_obj_align(cover, LV_ALIGN_TOP_MID, 0, 12); + + lv_obj_t *title = lv_label_create(s_player_screen); + lv_label_set_text(title, s_shows[s_cur_show].eps[s_cur_ep].title); + lv_obj_set_style_text_color(title, lv_color_white(), 0); + lv_obj_set_width(title, 300); + lv_obj_set_style_text_align(title, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(title, LV_LABEL_LONG_WRAP); + lv_obj_align(title, LV_ALIGN_CENTER, 0, 30); + + lv_obj_t *bar = lv_obj_create(s_player_screen); + lv_obj_remove_style_all(bar); + lv_obj_set_size(bar, 240, 60); + lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, -10); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *prev = lv_button_create(bar); + lv_obj_set_size(prev, 60, 52); + lv_obj_add_event_cb(prev, player_step_cb, LV_EVENT_CLICKED, (void *)(intptr_t)-1); + lv_label_set_text(lv_label_create(prev), LV_SYMBOL_PREV); + lv_obj_center(lv_obj_get_child(prev, 0)); + + lv_obj_t *toggle = lv_button_create(bar); + lv_obj_set_size(toggle, 72, 52); + lv_obj_add_event_cb(toggle, player_toggle_cb, LV_EVENT_CLICKED, NULL); + lv_label_set_text(lv_label_create(toggle), LV_SYMBOL_STOP); + lv_obj_center(lv_obj_get_child(toggle, 0)); + + lv_obj_t *next = lv_button_create(bar); + lv_obj_set_size(next, 60, 52); + lv_obj_add_event_cb(next, player_step_cb, LV_EVENT_CLICKED, (void *)(intptr_t)+1); + lv_label_set_text(lv_label_create(next), LV_SYMBOL_NEXT); + lv_obj_center(lv_obj_get_child(next, 0)); + + return s_player_screen; +} + +// --- level 2: episode list --------------------------------------------------- +static void ep_cb(lv_event_t *e) +{ + int ep = (int)(intptr_t)lv_event_get_user_data(e); + lv_obj_t *p = build_player(ep); + lv_screen_load_anim(p, LV_SCR_LOAD_ANIM_MOVE_LEFT, 220, 0, false); // keep list + play_current(); +} + +static void list_back_cb(lv_event_t *e) +{ + (void)e; + lv_screen_load_anim(s_grid_screen, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 220, 0, true); + s_list_screen = NULL; // deleted by auto_del above; grid is the cached screen +} + +static void open_show(int show_idx) +{ + s_cur_show = show_idx; + lisael_show_t *s = &s_shows[show_idx]; + + s_list_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_list_screen, lv_color_hex(0xEBFBEE), 0); + + lv_obj_t *back = lv_button_create(s_list_screen); + lv_obj_add_style(back, lisael_ui_style_button(), 0); + lv_obj_set_size(back, 64, 52); + lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6); + lv_obj_add_event_cb(back, list_back_cb, LV_EVENT_CLICKED, NULL); + lv_label_set_text(lv_label_create(back), LV_SYMBOL_LEFT); + lv_obj_center(lv_obj_get_child(back, 0)); + + lv_obj_t *title = lv_label_create(s_list_screen); + lv_label_set_text(title, s->show); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18); + + lv_obj_t *list = lv_list_create(s_list_screen); + lv_obj_set_size(list, LV_PCT(96), 168); + lv_obj_align(list, LV_ALIGN_BOTTOM_MID, 0, -4); + lv_obj_set_style_bg_color(list, lv_color_white(), 0); + lv_obj_set_style_radius(list, 12, 0); + + for (int i = 0; i < s->n_eps; i++) { + lv_obj_t *btn = lv_list_add_button(list, NULL, s->eps[i].title); + lv_obj_add_event_cb(btn, ep_cb, LV_EVENT_CLICKED, (void *)(intptr_t)i); + } + + lv_screen_load_anim(s_list_screen, LV_SCR_LOAD_ANIM_MOVE_LEFT, 220, 0, false); // keep grid +} + +// --- level 1: paged 2x2 cover grid ------------------------------------------- +static lv_obj_t *s_dots[(LISAEL_MAX_SHOWS + 3) / 4]; +static int s_n_pages; + +static void cover_cb(lv_event_t *e) +{ + int idx = (int)(intptr_t)lv_event_get_user_data(e); + open_show(idx); +} + +static void tv_scroll_cb(lv_event_t *e) +{ + if (s_n_pages <= 1) { + return; + } + lv_obj_t *tv = lv_event_get_target(e); + int w = lv_obj_get_width(tv); + int page = (w > 0) ? (lv_obj_get_scroll_x(tv) + w / 2) / w : 0; + if (page < 0) page = 0; + if (page >= s_n_pages) page = s_n_pages - 1; + for (int i = 0; i < s_n_pages; i++) { + lv_obj_set_style_bg_opa(s_dots[i], i == page ? LV_OPA_COVER : LV_OPA_30, 0); + } +} + +lv_obj_t *lisael_screen_histoires(void) +{ + if (s_grid_screen) { + return s_grid_screen; + } + s_n_shows = lisael_podcast_load(s_shows, LISAEL_MAX_SHOWS); + + s_grid_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_grid_screen, lv_color_hex(0xCDE8FF), 0); + lv_obj_set_style_bg_grad_color(s_grid_screen, lv_color_hex(0xF6FBFF), 0); + lv_obj_set_style_bg_grad_dir(s_grid_screen, LV_GRAD_DIR_VER, 0); + + lisael_ui_add_back_button(s_grid_screen); + + lv_obj_t *title = lv_label_create(s_grid_screen); + lv_label_set_text(title, "Histoires"); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18); + + if (s_n_shows <= 0) { + lv_obj_t *info = lv_label_create(s_grid_screen); + lv_label_set_text(info, + "Aucune histoire pour l'instant.\nBranche la box en USB\npour en ajouter."); + lv_obj_set_style_text_align(info, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(info); + return s_grid_screen; + } + + lv_obj_t *tv = lv_tileview_create(s_grid_screen); + lv_obj_set_size(tv, LV_PCT(100), 168); + lv_obj_align(tv, LV_ALIGN_TOP_MID, 0, 50); + lv_obj_set_style_bg_opa(tv, LV_OPA_TRANSP, 0); + lv_obj_add_event_cb(tv, tv_scroll_cb, LV_EVENT_SCROLL_END, NULL); + + s_n_pages = (s_n_shows + 3) / 4; + for (int p = 0; p < s_n_pages; p++) { + lv_obj_t *page = lv_tileview_add_tile(tv, p, 0, LV_DIR_HOR); + lv_obj_set_flex_flow(page, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_flex_align(page, LV_FLEX_ALIGN_SPACE_EVENLY, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(page, 6, 0); + for (int k = 0; k < 4; k++) { + int idx = p * 4 + k; + if (idx >= s_n_shows) { + break; + } + lv_obj_t *cell = lv_button_create(page); + lv_obj_remove_style_all(cell); + lv_obj_set_size(cell, 150, 96); + lv_obj_set_flex_flow(cell, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(cell, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_add_event_cb(cell, cover_cb, LV_EVENT_CLICKED, (void *)(intptr_t)idx); + make_cover(cell, idx, 64); + lv_obj_t *cap = lv_label_create(cell); + lv_label_set_text(cap, s_shows[idx].show); + lv_label_set_long_mode(cap, LV_LABEL_LONG_DOT); + lv_obj_set_width(cap, 144); + lv_obj_set_style_text_align(cap, LV_TEXT_ALIGN_CENTER, 0); + } + } + + if (s_n_pages > 1) { + lv_obj_t *dotrow = lv_obj_create(s_grid_screen); + lv_obj_remove_style_all(dotrow); + lv_obj_set_size(dotrow, LV_PCT(60), 18); + lv_obj_align(dotrow, LV_ALIGN_BOTTOM_MID, 0, -6); + lv_obj_set_flex_flow(dotrow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(dotrow, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(dotrow, LV_OBJ_FLAG_SCROLLABLE); + for (int i = 0; i < s_n_pages; i++) { + lv_obj_t *d = lv_obj_create(dotrow); + lv_obj_remove_style_all(d); + lv_obj_set_size(d, 10, 10); + lv_obj_set_style_radius(d, 5, 0); + lv_obj_set_style_margin_left(d, 4, 0); + lv_obj_set_style_margin_right(d, 4, 0); + lv_obj_set_style_bg_opa(d, i == 0 ? LV_OPA_COVER : LV_OPA_30, 0); + lv_obj_set_style_bg_color(d, lv_color_hex(0x1971C2), 0); + s_dots[i] = d; + } + } + return s_grid_screen; +} diff --git a/main/modes/home.c b/main/modes/home.c index e47d0a8..1dd13d5 100644 --- a/main/modes/home.c +++ b/main/modes/home.c @@ -6,6 +6,8 @@ #include "ui/ui.h" #include "ui/icons.h" +#include "ui/assets/lisael_icons.h" +#include "ui/fonts/lisael_fonts.h" #include "ui/fr_date.h" #include "net/weather.h" #include "net/sntp_time.h" @@ -36,10 +38,10 @@ static temperature_sensor_handle_t s_tsens; // NULL if unavailable // --- one mode tile ---------------------------------------------------------- typedef struct { - const char *emoji; - const char *label; - lisael_mode_t mode; - lv_color_t color; + const lv_image_dsc_t *icon; + const char *label; + lisael_mode_t mode; + lv_color_t color; } tile_def_t; static void tile_cb(lv_event_t *e) @@ -52,8 +54,14 @@ static lv_obj_t *make_tile(lv_obj_t *parent, const tile_def_t *def) { lv_obj_t *tile = lv_button_create(parent); lv_obj_add_style(tile, lisael_ui_style_tile(), 0); - lv_obj_set_style_bg_color(tile, def->color, 0); - lv_obj_set_size(tile, 92, 78); + // Glossy 3D look: lighter at the top fading to the tile colour (the shared + // style sets LV_GRAD_DIR_VER + the soft drop shadow). + lv_obj_set_style_bg_color(tile, lv_color_lighten(def->color, 90), 0); + lv_obj_set_style_bg_grad_color(tile, def->color, 0); + // Press feedback: the tile sinks a little. + lv_obj_set_style_translate_y(tile, 4, LV_STATE_PRESSED); + lv_obj_set_style_shadow_ofs_y(tile, 3, LV_STATE_PRESSED); + lv_obj_set_size(tile, 94, 72); lv_obj_add_event_cb(tile, tile_cb, LV_EVENT_CLICKED, (void *)(intptr_t)def->mode); @@ -65,8 +73,8 @@ static lv_obj_t *make_tile(lv_obj_t *parent, const tile_def_t *def) LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE); - lv_obj_t *emoji = lv_label_create(col); - lv_label_set_text(emoji, def->emoji); + lv_obj_t *icon = lv_image_create(col); + lv_image_set_src(icon, def->icon); lv_obj_t *txt = lv_label_create(col); lv_label_set_text(txt, def->label); @@ -95,17 +103,20 @@ lv_obj_t *lisael_screen_home(void) sensors_init(); s_root = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_root, lv_color_hex(0xEAF4FF), 0); // soft sky + // Soft vertical sky gradient (light blue at the top fading to white). + lv_obj_set_style_bg_color(s_root, lv_color_hex(0xCDE8FF), 0); + lv_obj_set_style_bg_grad_color(s_root, lv_color_hex(0xF6FBFF), 0); + lv_obj_set_style_bg_grad_dir(s_root, LV_GRAD_DIR_VER, 0); // --- header: date + time + weather -------------------------------------- lv_obj_t *header = lv_obj_create(s_root); lv_obj_remove_style_all(header); - lv_obj_set_size(header, LV_PCT(100), 86); - lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 4); + lv_obj_set_size(header, LV_PCT(100), 74); + lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 2); lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE); s_time_lbl = lv_label_create(header); - lv_obj_set_style_text_font(s_time_lbl, &lv_font_montserrat_28, 0); + lv_obj_set_style_text_font(s_time_lbl, &lisael_font_30, 0); lv_label_set_text(s_time_lbl, "--:--"); lv_obj_align(s_time_lbl, LV_ALIGN_TOP_LEFT, 12, 2); @@ -113,8 +124,7 @@ lv_obj_t *lisael_screen_home(void) lv_label_set_text(s_date_lbl, "..."); lv_obj_align(s_date_lbl, LV_ALIGN_TOP_LEFT, 12, 38); - s_weather_icon = lv_label_create(header); - lv_label_set_text(s_weather_icon, ""); + s_weather_icon = lv_image_create(header); lv_obj_align(s_weather_icon, LV_ALIGN_TOP_RIGHT, -12, 2); s_weather_lbl = lv_label_create(header); @@ -128,21 +138,22 @@ lv_obj_t *lisael_screen_home(void) // --- mode tiles ---------------------------------------------------------- lv_obj_t *grid = lv_obj_create(s_root); lv_obj_remove_style_all(grid); - lv_obj_set_size(grid, LV_PCT(100), 150); - lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4); + lv_obj_set_size(grid, LV_PCT(100), 160); + lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -2); lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, - LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); + lv_obj_set_style_pad_row(grid, 8, 0); static const tile_def_t tiles[] = { - { LISAEL_EMOJI_RADIO, "Radio", LISAEL_MODE_RADIO, {0} }, - { LISAEL_EMOJI_SOUND, "Sons", LISAEL_MODE_SOUNDBOX, {0} }, - { LISAEL_EMOJI_CALM, "Calme", LISAEL_MODE_CALM, {0} }, - { LISAEL_EMOJI_CLOCK, "Horloge", LISAEL_MODE_CLOCK, {0} }, - { LISAEL_EMOJI_GAMES, "Jeux", LISAEL_MODE_GAMES, {0} }, + { &lisael_ic_histoires, "Histoires", LISAEL_MODE_HISTOIRES, {0} }, + { &lisael_ic_radio, "Radio", LISAEL_MODE_RADIO, {0} }, + { &lisael_ic_calm, "Calme", LISAEL_MODE_CALM, {0} }, + { &lisael_ic_clock, "Horloge", LISAEL_MODE_CLOCK, {0} }, + { &lisael_ic_games, "Jeux", LISAEL_MODE_GAMES, {0} }, }; static const uint32_t colors[] = { - 0xFFD8A8, 0xB2F2BB, 0xA5D8FF, 0xD0BFFF, 0xFFC9C9, + 0xFFE08A, 0xFFD8A8, 0xB2F2BB, 0xA5D8FF, 0xD0BFFF, 0xFFC9C9, }; for (size_t i = 0; i < sizeof(tiles) / sizeof(tiles[0]); i++) { tile_def_t t = tiles[i]; @@ -198,8 +209,10 @@ void lisael_home_update_weather(void) if (bsp_display_lock(50)) { lv_label_set_text(s_weather_lbl, buf); - lv_label_set_text(s_weather_icon, - w.valid ? lisael_weather_code_emoji(w.weather_code) : ""); + if (w.valid) { + lv_image_set_src(s_weather_icon, + lisael_weather_code_icon(w.weather_code)); + } bsp_display_unlock(); } } diff --git a/main/modes/soundbox.c b/main/modes/soundbox.c deleted file mode 100644 index 449207c..0000000 --- a/main/modes/soundbox.c +++ /dev/null @@ -1,91 +0,0 @@ -// "Boîte à sons" mode: grid of buttons reading /sdcard/sounds/*.mp3. -// -// Buttons come from /sdcard/sounds/manifest.json (file/title/emoji). If the -// card is missing or has no manifest, a friendly placeholder is shown instead -// of an error. - -#include "ui/ui.h" -#include "ui/icons.h" -#include "audio/sd_player.h" -#include "audio/audio_player.h" - -#include -#include "esp_log.h" - -static const char *TAG = "ui.soundbox"; - -static lv_obj_t *s_root; -static lisael_sound_t s_sounds[LISAEL_SD_MAX_SOUNDS]; -static int s_count; - -static void sound_cb(lv_event_t *e) -{ - int idx = (int)(intptr_t)lv_event_get_user_data(e); - if (idx >= 0 && idx < s_count) { - ESP_LOGI(TAG, "play %s", s_sounds[idx].file); - lisael_sd_play_sound(s_sounds[idx].file); - } -} - -static void build_grid(lv_obj_t *parent) -{ - s_count = lisael_sd_load_manifest(s_sounds, LISAEL_SD_MAX_SOUNDS); - - if (s_count <= 0) { - lv_obj_t *info = lv_label_create(parent); - lv_label_set_text(info, - "Insère la carte SD\navec des sons\n(/sounds/manifest.json)"); - lv_obj_set_style_text_align(info, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_center(info); - return; - } - - lv_obj_t *grid = lv_obj_create(parent); - lv_obj_remove_style_all(grid); - lv_obj_set_size(grid, LV_PCT(100), 170); - lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4); - lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); - lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, - LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_row(grid, 6, 0); - - for (int i = 0; i < s_count; i++) { - lv_obj_t *btn = lv_button_create(grid); - lv_obj_add_style(btn, lisael_ui_style_tile(), 0); - lv_obj_set_style_bg_color(btn, lv_color_hex(0xD3F9D8), 0); - lv_obj_set_size(btn, 92, 76); - lv_obj_add_event_cb(btn, sound_cb, LV_EVENT_CLICKED, - (void *)(intptr_t)i); - - lv_obj_t *col = lv_obj_create(btn); - lv_obj_remove_style_all(col); - lv_obj_set_size(col, LV_PCT(100), LV_PCT(100)); - lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE); - - lv_obj_t *emoji = lv_label_create(col); - lv_label_set_text(emoji, s_sounds[i].emoji); - lv_obj_t *title = lv_label_create(col); - lv_label_set_text(title, s_sounds[i].title); - } -} - -lv_obj_t *lisael_screen_soundbox(void) -{ - if (s_root) { - return s_root; - } - s_root = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_root, lv_color_hex(0xEBFBEE), 0); - - lisael_ui_add_back_button(s_root); - - lv_obj_t *title = lv_label_create(s_root); - lv_label_set_text(title, LISAEL_EMOJI_SOUND " Boîte à sons"); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16); - - build_grid(s_root); - return s_root; -} diff --git a/main/ui/ui.c b/main/ui/ui.c index baf1db3..caade11 100644 --- a/main/ui/ui.c +++ b/main/ui/ui.c @@ -8,6 +8,7 @@ #include "ui/ui.h" #include "ui/icons.h" +#include "ui/fonts/lisael_fonts.h" #include "lisael_config.h" #include "esp_log.h" @@ -27,15 +28,19 @@ static void init_styles(void) if (s_styles_ready) { return; } - // Big rounded pastel tiles for the home menu. + // Big rounded glossy tiles for the home menu — soft drop shadow for depth, + // a vertical gloss gradient (set per tile in make_tile), rounded corners. lv_style_init(&s_style_tile); - lv_style_set_radius(&s_style_tile, 18); + lv_style_set_radius(&s_style_tile, 22); lv_style_set_bg_opa(&s_style_tile, LV_OPA_COVER); + lv_style_set_bg_grad_dir(&s_style_tile, LV_GRAD_DIR_VER); // colours per tile lv_style_set_border_width(&s_style_tile, 0); lv_style_set_pad_all(&s_style_tile, 6); - lv_style_set_shadow_width(&s_style_tile, 8); - lv_style_set_shadow_ofs_y(&s_style_tile, 3); - lv_style_set_shadow_opa(&s_style_tile, LV_OPA_20); + lv_style_set_shadow_width(&s_style_tile, 18); + lv_style_set_shadow_ofs_y(&s_style_tile, 8); + lv_style_set_shadow_spread(&s_style_tile, 1); + lv_style_set_shadow_color(&s_style_tile, lv_color_hex(0x5b6b8a)); + lv_style_set_shadow_opa(&s_style_tile, LV_OPA_40); // Generic large touch button. lv_style_init(&s_style_button); @@ -63,18 +68,18 @@ static lv_obj_t *build_screen(lisael_mode_t mode) switch (mode) { case LISAEL_MODE_HOME: return lisael_screen_home(); case LISAEL_MODE_RADIO: return lisael_screen_radio(); - case LISAEL_MODE_SOUNDBOX: return lisael_screen_soundbox(); case LISAEL_MODE_CALM: return lisael_screen_calm(); case LISAEL_MODE_CLOCK: return lisael_screen_clock(); case LISAEL_MODE_GAMES: return lisael_screen_games(); + case LISAEL_MODE_HISTOIRES:return lisael_screen_histoires(); default: return lisael_screen_home(); } } void lisael_ui_goto(lisael_mode_t mode) { - if (mode < 0 || mode >= LISAEL_MODE_COUNT) { - return; + if ((unsigned)mode >= (unsigned)LISAEL_MODE_COUNT) { + return; // borne nette pour GCC 14 (-Werror=array-bounds) } if (!s_screens[mode]) { s_screens[mode] = build_screen(mode); @@ -110,6 +115,16 @@ lv_obj_t *lisael_ui_add_back_button(lv_obj_t *parent) void lisael_ui_init(void) { + // Default font -> Fredoka (rounded, kid-friendly, with French accents). + // The default theme applies it to every label unless overridden. + lv_display_t *disp = lv_display_get_default(); + lv_theme_t *th = lv_theme_default_init( + disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_PINK), + false, &lisael_font_18); + if (th) { + lv_display_set_theme(disp, th); + } + init_styles(); s_screens[LISAEL_MODE_HOME] = lisael_screen_home(); lv_screen_load(s_screens[LISAEL_MODE_HOME]); diff --git a/main/ui/ui.h b/main/ui/ui.h index b689aef..0f52039 100644 --- a/main/ui/ui.h +++ b/main/ui/ui.h @@ -17,10 +17,10 @@ extern "C" { typedef enum { LISAEL_MODE_HOME = 0, // accueil: date FR + meteo + capteurs + tuiles LISAEL_MODE_RADIO, // webradios enfants - LISAEL_MODE_SOUNDBOX, // boite a sons (SD) LISAEL_MODE_CALM, // coin calme / respiration LISAEL_MODE_CLOCK, // horloge / veilleuse LISAEL_MODE_GAMES, // jeux (memory, compter) + LISAEL_MODE_HISTOIRES, // podcasts Radio France jeunesse (Oli, Bestioles) LISAEL_MODE_COUNT, } lisael_mode_t; @@ -44,10 +44,11 @@ const lv_style_t *lisael_ui_style_button(void); // generic big buttons // (Implemented in modes/*.c.) lv_obj_t *lisael_screen_home(void); lv_obj_t *lisael_screen_radio(void); -lv_obj_t *lisael_screen_soundbox(void); lv_obj_t *lisael_screen_calm(void); lv_obj_t *lisael_screen_clock(void); lv_obj_t *lisael_screen_games(void); +lv_obj_t *lisael_screen_balloons(void); // "Calme ta colère" anger game +lv_obj_t *lisael_screen_histoires(void); // "Histoires" — vignette podcasts // Home screen live-update hooks (called from background tasks; they take the // display lock internally — do NOT hold it when calling these).