Show the album cover on a media tile, in the same strip as the live camera pictures (0.2.92)

A single or double-width media player tile takes Display -> Album cover
on a Guition: the cover of what plays in the icon's place, like Home
Assistant's own tile. It rides in the page's strip of 0.2.91: the request
and the answer are the same, a media player is simply among the tiles.
The app fetches a player's picture only when its address changes; a page
with a camera loads at the camera's pace with the cover reused, a page of
media tiles alone loads once, and a new track (the picture's mark in the
state, part of the page's wish) asks again at once. A player without a
picture keeps its icon and is not asked for; the double-width tile keeps
its controls; the tile over the whole page keeps the card's big cover.
Firmware 0.2.78.

Host harness 31 checks (docs/TEST_RESULTS_0292.md). CYD firmware
1,633,760 B, 89.0 %.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-20 13:41:21 +02:00
parent 306636434b
commit efbab04b39
45 changed files with 352 additions and 85 deletions
+5
View File
@@ -217,6 +217,11 @@ struct Tile {
// icon's place, loaded again every `refresh` seconds.
uint16_t refresh = 15;
bool live() const { const auto d = domain(); return display == "live" && (d == "camera" || d == "image"); }
// A media player's album cover in the icon's place (firmware 0.2.78+): "display": "cover" on a single or double-width
// tile, while the player has a picture; the tile over the whole page keeps the card's big cover.
bool cover_tile() const { return display == "cover" && domain() == "media_player" && !full && !extra().media_picture.empty(); }
// A tile that draws its picture out of the page's strip (runtime_tiles.h, live_*).
bool pictured() const { return live() || cover_tile(); }
// Double width takes a row; full (firmware 0.2.62+) takes the whole page, all six slots, and is also wide.
bool wide = false, full = false;
// Direct control set on a wide card (firmware 0.2.19+); empty keeps the plain card.
+20 -17
View File
@@ -247,7 +247,7 @@ struct Widgets {
lv_obj_t *busy{}, *spinner{}; bool busy_drawn=false;
// Page fill skeleton: a sheet in the card's colour over its contents until the card is drawn.
lv_obj_t *veil{};
// A camera tile's live picture in the icon's place (firmware 0.2.77+).
// A camera tile's live picture (firmware 0.2.77+) or a media tile's album cover (0.2.78+) in the icon's place.
lv_obj_t *picture{};
};
constexpr unsigned POINT_BUFFER = 128;
@@ -4177,15 +4177,16 @@ inline void cover_tick(uint32_t now) {
}
}
// ---- Live pictures on camera tiles (firmware 0.2.77+) ----
// A camera tile with "display": "live" shows a small picture of its camera in the icon's place. The camera tiles of
// the page on screen share one image: the screen asks for them together (the entities in slot order, the size of the
// icon's circle and the colour of each tile behind the rounded corners) and the app serves one strip of squares, top
// to bottom, that every tile takes its own square out of (LVGL's image offset). One download per page at the pace of
// the fastest of those tiles, 15 or 30 s, in the board's third online_image. It waits for the alert's picture, a
// cover or the camera full screen: one picture loads at a time. A page turn, a card over the page or another look
// changes what is wanted: the strip is dropped and asked for again.
struct LiveWish { std::string entities, grounds; int size = 0; uint32_t every = 15000; };
// ---- Live pictures on camera tiles (firmware 0.2.77+) and album covers on media tiles (0.2.78+) ----
// A camera tile with "display": "live" shows a small picture of its camera in the icon's place, a media tile with
// "display": "cover" the album cover of what plays. The pictured tiles of the page on screen share one image: the
// screen asks for them together (the entities in slot order, the size of the icon's circle and the colour of each tile
// behind the rounded corners) and the app serves one strip of squares, top to bottom, that every tile takes its own
// square out of (LVGL's image offset). One download per page at the pace of the fastest camera, 15 or 30 s, in the
// board's third online_image; a page of covers alone loads once. It waits for the alert's picture, a cover or the
// camera full screen: one picture loads at a time. A page turn, a card over the page, another look or another track
// (the picture's mark in the media state) changes what is wanted: the strip is dropped and asked for again.
struct LiveWish { std::string entities, grounds, marks; int size = 0; uint32_t every = 15000; bool cameras = false; };
inline LiveWish live_wish;
inline camera_view::Feed live; // entity: the list asked for
inline std::string live_have; // the list the strip on screen holds, "" for a tile without a picture
@@ -4221,14 +4222,15 @@ inline LiveWish live_wanted() {
for (auto &w : widgets) {
if (!w.tile || lv_obj_has_flag(w.tile, LV_OBJ_FLAG_HIDDEN) || w.index >= model.count) continue;
const auto &t = model.tiles[w.index];
if (!t.live()) continue;
if (!want.entities.empty()) { want.entities += ','; want.grounds += ','; }
if (!t.pictured()) continue;
if (!want.entities.empty()) { want.entities += ','; want.grounds += ','; want.marks += ','; }
want.entities += t.entity;
char ground[8];
snprintf(ground, sizeof(ground), "%06X", (unsigned) (t.transparent ? theme::hex(theme::PAGE) : theme::surface(t.background)));
want.grounds += ground;
if (t.cover_tile()) want.marks += t.extra().media_picture;
if (!want.size) want.size = lv_obj_get_style_width(w.circle, LV_PART_MAIN);
want.every = std::min<uint32_t>(want.every, t.refresh * 1000u);
if (t.live()) { want.cameras = true; want.every = std::min<uint32_t>(want.every, t.refresh * 1000u); }
}
return want;
}
@@ -4259,7 +4261,7 @@ inline void live_release() {
inline void live_place(Widgets &w, const Tile &t, int size, int x, int y) {
#if LV_USE_IMAGE
int square = -1;
lv_image_dsc_t *src = t.live() ? live_ready(t.entity, size, square) : nullptr;
lv_image_dsc_t *src = t.pictured() ? live_ready(t.entity, size, square) : nullptr;
if (src) {
if (!w.picture) {
w.picture = lv_image_create(w.tile);
@@ -4301,10 +4303,11 @@ inline void live_request() {
inline void live_tick(uint32_t now) {
if (!live_supported()) return;
LiveWish want = live_wanted();
if (want.entities != live_wish.entities || want.grounds != live_wish.grounds || want.size != live_wish.size) {
if (want.entities != live_wish.entities || want.grounds != live_wish.grounds || want.marks != live_wish.marks || want.size != live_wish.size) {
live_wish = want;
live_release();
if (!want.entities.empty()) live.open(want.entities, false, want.every);
// Covers alone load once per link (a new track is a new wish); a camera sets the pace.
if (!want.entities.empty()) live.open(want.entities, !want.cameras, want.every);
}
if (!live.open() || camera_root || card_open() || !awake()) return;
if (alert_image_due() || alert_thumb_loading || cover.loading || camera.loading) return; // one picture at a time
@@ -4325,7 +4328,7 @@ inline void live_loaded(bool cached) {
live.finish(esphome::millis(), true);
ESP_LOGI("camera", "live tiles %s", cached ? "unchanged" : "loaded");
for (auto &w : widgets)
if (w.tile && !lv_obj_has_flag(w.tile, LV_OBJ_FLAG_HIDDEN) && w.index < model.count && model.tiles[w.index].live()) refresh_tile(w.index);
if (w.tile && !lv_obj_has_flag(w.tile, LV_OBJ_FLAG_HIDDEN) && w.index < model.count && model.tiles[w.index].pictured()) refresh_tile(w.index);
}
inline void live_failed() {
if (!live.loading) return;