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:
+1
-2
@@ -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"
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user