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.
This commit is contained in:
Clément Saillant
2026-06-14 17:23:38 +02:00
parent dcbc23c374
commit 94917f633d
6 changed files with 408 additions and 128 deletions
+1 -2
View File
@@ -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"
+343
View File
@@ -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 <stdint.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <sys/stat.h>
#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;
}
+38 -25
View File
@@ -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();
}
}
-91
View File
@@ -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 <stdint.h>
#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;
}
+23 -8
View File
@@ -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]);
+3 -2
View File
@@ -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).