ce78cad14c
Match Histoires and home: replace the column of station buttons with the Lunii-style horizontal carousel (one big station card per slide, radio icon + name, swipe/arrows, a dot per station, tap = play). Keep the Stop button and status line at the bottom.
248 lines
9.1 KiB
C
248 lines
9.1 KiB
C
// Webradio mode screen for Lisael Box.
|
|
//
|
|
// Soft-gradient screen with a "← Accueil" back button, a "Radio" title, a
|
|
// Lunii-style horizontal carousel of station cards (one big card per slide,
|
|
// swipe/arrows to browse, tap = lisael_radio_play(url)), a Stop button
|
|
// (lisael_radio_stop()) and a small status label echoing the current action.
|
|
//
|
|
// The station table below is local on purpose (radio_pipeline.h is the only
|
|
// audio contract we depend on). Each entry is a live HTTP/HTTPS MP3 stream.
|
|
//
|
|
// ⚠️ STREAM URLs — VERIFY ON HARDWARE BEFORE SHIPPING ⚠️
|
|
// French kids' webradios are mostly hosted on RadioKing / Zeno.FM, whose direct
|
|
// stream endpoints are loaded dynamically by their JS players. The URLs below
|
|
// use the real listen.radioking.com MP3 mounts (content-type audio/mpeg). If a
|
|
// station changes, re-read it from play.radioking.io/<slug>. All HTTPS (handled
|
|
// by the certificate bundle in radio_pipeline.c).
|
|
|
|
#include "ui/ui.h"
|
|
#include "ui/fonts/lisael_fonts.h"
|
|
#include "ui/assets/lisael_icons.h"
|
|
#include "audio/radio_pipeline.h"
|
|
#include "audio/audio_player.h"
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include "esp_log.h"
|
|
|
|
static const char *TAG = "ui.radio";
|
|
|
|
// --- Station table (name + live MP3 stream URL) -----------------------------
|
|
typedef struct {
|
|
const char *name;
|
|
const char *url;
|
|
} radio_station_t;
|
|
|
|
// DIRECT MP3 stream mounts (content-type audio/mpeg), verified 2026-06-14 from
|
|
// the RadioKing players. The /play/<slug> pages are HTML — these are the real
|
|
// listen.radioking.com mounts.
|
|
static const radio_station_t k_stations[] = {
|
|
// Radio Doudou — comptines & berceuses pour les tout-petits.
|
|
{ "Radio Doudou", "https://listen.radioking.com/radio/11565/stream/22961" },
|
|
// Radio Pomme d'Api (Bayard) — la radio des 3-7 ans.
|
|
{ "Pomme d'Api", "https://listen.radioking.com/radio/361753/stream/411352" },
|
|
};
|
|
|
|
#define K_STATIONS_COUNT ((int)(sizeof(k_stations) / sizeof(k_stations[0])))
|
|
|
|
static const uint32_t k_colors[] = {
|
|
0xFFC078, 0xFFA8A8, 0xFFD43B, 0x99E9F2, 0xB197FC, 0x8CE99A,
|
|
};
|
|
|
|
#define RADIO_SLIDE_W 320
|
|
|
|
static lv_obj_t *s_root;
|
|
static lv_obj_t *s_status_lbl;
|
|
static lv_obj_t *s_track;
|
|
static lv_obj_t *s_dots[8];
|
|
static int s_cur;
|
|
|
|
static void set_status(const char *txt)
|
|
{
|
|
if (s_status_lbl) {
|
|
lv_label_set_text(s_status_lbl, txt);
|
|
}
|
|
}
|
|
|
|
// --- carousel mechanics ------------------------------------------------------
|
|
static void dots_update(int idx)
|
|
{
|
|
for (int i = 0; i < K_STATIONS_COUNT; i++) {
|
|
if (s_dots[i]) {
|
|
lv_obj_set_style_bg_opa(s_dots[i], i == idx ? LV_OPA_COVER : LV_OPA_30, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void scroll_cb(lv_event_t *e)
|
|
{
|
|
lv_obj_t *t = lv_event_get_target(e);
|
|
int idx = (lv_obj_get_scroll_x(t) + RADIO_SLIDE_W / 2) / RADIO_SLIDE_W;
|
|
if (idx < 0) idx = 0;
|
|
if (idx >= K_STATIONS_COUNT) idx = K_STATIONS_COUNT - 1;
|
|
s_cur = idx;
|
|
dots_update(idx);
|
|
}
|
|
|
|
static void arrow_cb(lv_event_t *e)
|
|
{
|
|
int idx = s_cur + (int)(intptr_t)lv_event_get_user_data(e);
|
|
if (idx < 0) idx = 0;
|
|
if (idx >= K_STATIONS_COUNT) idx = K_STATIONS_COUNT - 1;
|
|
s_cur = idx;
|
|
lv_obj_scroll_to_x(s_track, idx * RADIO_SLIDE_W, LV_ANIM_ON);
|
|
dots_update(idx);
|
|
}
|
|
|
|
static void make_arrow(lv_obj_t *parent, const char *sym, lv_align_t al, int dir)
|
|
{
|
|
lv_obj_t *b = lv_button_create(parent);
|
|
lv_obj_remove_style_all(b);
|
|
lv_obj_set_size(b, 38, 60);
|
|
lv_obj_set_style_radius(b, 19, 0);
|
|
lv_obj_set_style_bg_opa(b, LV_OPA_40, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_white(), 0);
|
|
lv_obj_align(b, al, (al == LV_ALIGN_LEFT_MID) ? 2 : -2, -10);
|
|
lv_obj_add_event_cb(b, arrow_cb, LV_EVENT_CLICKED, (void *)(intptr_t)dir);
|
|
lv_obj_t *l = lv_label_create(b);
|
|
lv_label_set_text(l, sym);
|
|
lv_obj_set_style_text_color(l, lv_color_hex(0x8A4B16), 0);
|
|
lv_obj_center(l);
|
|
}
|
|
|
|
// --- playback ---------------------------------------------------------------
|
|
static void station_cb(lv_event_t *e)
|
|
{
|
|
const radio_station_t *st = (const radio_station_t *)lv_event_get_user_data(e);
|
|
ESP_LOGI(TAG, "station: %s", st->name);
|
|
esp_err_t err = lisael_radio_play(st->url);
|
|
if (s_status_lbl) {
|
|
lv_label_set_text_fmt(s_status_lbl, "%s %s",
|
|
err == ESP_OK ? "Écoute :" : "Oups :", st->name);
|
|
}
|
|
}
|
|
|
|
static void stop_cb(lv_event_t *e)
|
|
{
|
|
(void)e;
|
|
lisael_radio_stop();
|
|
set_status("Silence");
|
|
}
|
|
|
|
static void add_station_slide(lv_obj_t *track, const radio_station_t *st, uint32_t color)
|
|
{
|
|
lv_obj_t *slide = lv_obj_create(track);
|
|
lv_obj_remove_style_all(slide);
|
|
lv_obj_set_size(slide, RADIO_SLIDE_W, LV_PCT(100));
|
|
lv_obj_clear_flag(slide, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_flex_flow(slide, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(slide, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
|
|
lv_obj_t *card = lv_button_create(slide);
|
|
lv_obj_add_style(card, lisael_ui_style_tile(), 0);
|
|
lv_obj_set_style_bg_color(card, lv_color_lighten(lv_color_hex(color), 90), 0);
|
|
lv_obj_set_style_bg_grad_color(card, lv_color_hex(color), 0);
|
|
lv_obj_set_style_translate_y(card, 4, LV_STATE_PRESSED);
|
|
lv_obj_set_style_shadow_ofs_y(card, 3, LV_STATE_PRESSED);
|
|
lv_obj_set_size(card, 188, 116);
|
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_row(card, 6, 0);
|
|
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_event_cb(card, station_cb, LV_EVENT_CLICKED, (void *)st);
|
|
|
|
lv_obj_t *icon = lv_image_create(card);
|
|
lv_image_set_src(icon, &lisael_ic_radio);
|
|
lv_obj_set_size(icon, 56, 56); // scale-to-fit by size (reliable)
|
|
lv_image_set_inner_align(icon, LV_IMAGE_ALIGN_CONTAIN);
|
|
|
|
lv_obj_t *name = lv_label_create(card);
|
|
lv_label_set_text(name, st->name);
|
|
lv_obj_set_style_text_font(name, &lisael_font_24, 0);
|
|
}
|
|
|
|
lv_obj_t *lisael_screen_radio(void)
|
|
{
|
|
if (s_root) {
|
|
return s_root;
|
|
}
|
|
s_root = lv_obj_create(NULL);
|
|
lv_obj_set_style_bg_color(s_root, lv_color_hex(0xFFE3C2), 0);
|
|
lv_obj_set_style_bg_grad_color(s_root, lv_color_hex(0xFFF7EE), 0);
|
|
lv_obj_set_style_bg_grad_dir(s_root, LV_GRAD_DIR_VER, 0);
|
|
lv_obj_clear_flag(s_root, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lisael_ui_add_back_button(s_root);
|
|
|
|
lv_obj_t *title = lv_label_create(s_root);
|
|
lv_label_set_text(title, "Radio");
|
|
lv_obj_set_style_text_font(title, &lisael_font_24, 0);
|
|
lv_obj_set_style_text_color(title, lv_color_hex(0x8A4B16), 0);
|
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12);
|
|
|
|
// Carousel of station cards.
|
|
s_track = lv_obj_create(s_root);
|
|
lv_obj_remove_style_all(s_track);
|
|
lv_obj_set_size(s_track, RADIO_SLIDE_W, 124);
|
|
lv_obj_align(s_track, LV_ALIGN_TOP_MID, 0, 42);
|
|
lv_obj_set_flex_flow(s_track, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_all(s_track, 0, 0);
|
|
lv_obj_set_style_pad_column(s_track, 0, 0);
|
|
lv_obj_set_scroll_dir(s_track, LV_DIR_HOR);
|
|
lv_obj_set_scroll_snap_x(s_track, LV_SCROLL_SNAP_CENTER);
|
|
lv_obj_set_scrollbar_mode(s_track, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_add_event_cb(s_track, scroll_cb, LV_EVENT_SCROLL_END, NULL);
|
|
|
|
for (int i = 0; i < K_STATIONS_COUNT; i++) {
|
|
add_station_slide(s_track, &k_stations[i],
|
|
k_colors[i % (int)(sizeof(k_colors) / sizeof(k_colors[0]))]);
|
|
}
|
|
|
|
// Dots.
|
|
lv_obj_t *dotrow = lv_obj_create(s_root);
|
|
lv_obj_remove_style_all(dotrow);
|
|
lv_obj_set_size(dotrow, LV_PCT(90), 14);
|
|
lv_obj_align(dotrow, LV_ALIGN_TOP_MID, 0, 170);
|
|
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 < K_STATIONS_COUNT; i++) {
|
|
lv_obj_t *d = lv_obj_create(dotrow);
|
|
lv_obj_remove_style_all(d);
|
|
lv_obj_set_size(d, 9, 9);
|
|
lv_obj_set_style_radius(d, 5, 0);
|
|
lv_obj_set_style_margin_left(d, 3, 0);
|
|
lv_obj_set_style_margin_right(d, 3, 0);
|
|
lv_obj_set_style_bg_color(d, lv_color_hex(0xE8590C), 0);
|
|
lv_obj_set_style_bg_opa(d, i == 0 ? LV_OPA_COVER : LV_OPA_30, 0);
|
|
s_dots[i] = d;
|
|
}
|
|
|
|
if (K_STATIONS_COUNT > 1) {
|
|
make_arrow(s_root, LV_SYMBOL_LEFT, LV_ALIGN_LEFT_MID, -1);
|
|
make_arrow(s_root, LV_SYMBOL_RIGHT, LV_ALIGN_RIGHT_MID, +1);
|
|
}
|
|
|
|
// Status line + Stop button at the bottom.
|
|
s_status_lbl = lv_label_create(s_root);
|
|
lv_obj_set_style_text_color(s_status_lbl, lv_color_hex(0x8A4B16), 0);
|
|
lv_label_set_text(s_status_lbl, "");
|
|
lv_obj_align(s_status_lbl, LV_ALIGN_BOTTOM_MID, 0, -60);
|
|
|
|
lv_obj_t *stop = lv_button_create(s_root);
|
|
lv_obj_add_style(stop, lisael_ui_style_button(), 0);
|
|
lv_obj_set_size(stop, 130, 48);
|
|
lv_obj_align(stop, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
lv_obj_add_event_cb(stop, stop_cb, LV_EVENT_CLICKED, NULL);
|
|
lv_obj_t *stop_lbl = lv_label_create(stop);
|
|
lv_label_set_text(stop_lbl, LV_SYMBOL_STOP " Stop");
|
|
lv_obj_center(stop_lbl);
|
|
|
|
s_cur = 0;
|
|
dots_update(0);
|
|
return s_root;
|
|
}
|