Public Access
Icons: small images of their own size for pages
Bitmaps that only pages use (backgrounds, images by state) are now icons: own width and height, stored in /icons, not in the page lists or the Home Assistant dropdowns. Page backgrounds and image elements use icons or bitmaps; the renderer gets icons expanded at the top left, so pages look the same. Web UI: Icons tab with an editor, Make icon / Make bitmap, Save as icon on the Draw tab, icons in the backup. A one-time migration in time-boxed steps moves the uploaded bitmaps that only pages use, after the new firmware is marked good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1704cbf17b
commit
ab001e5d18
37 files changed
+955
-78
No files matched your search
+185
-3
@@ -7,6 +7,7 @@
|
||||
#include "config.h"
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "display/icon_store.h"
|
||||
#include "display/page_resources.h"
|
||||
#include "display/page_store.h"
|
||||
#include "logic/page_selector.h"
|
||||
@@ -274,6 +275,8 @@ const char *appSaveBitmap(const char *name, const uint8_t *bitmap)
|
||||
{
|
||||
if (pageExists(name))
|
||||
return "A page has this name";
|
||||
if (iconExists(name))
|
||||
return "An icon has this name";
|
||||
const char *error = saveBitmap(name, bitmap);
|
||||
if (error)
|
||||
return error;
|
||||
@@ -297,7 +300,7 @@ const char *appDeleteBitmap(const char *name)
|
||||
{
|
||||
if (scheduleUses(page_schedule, name))
|
||||
return USED_BY_SCHEDULE;
|
||||
String user = pageUsingBitmap(name);
|
||||
String user = pageUsingImage(name);
|
||||
if (user.length())
|
||||
return usedBy(user);
|
||||
const char *error = deleteBitmap(name);
|
||||
@@ -314,9 +317,11 @@ const char *appSavePage(const char *name, const DynamicPage &page)
|
||||
{
|
||||
if (bitmapExists(name))
|
||||
return "A bitmap has this name";
|
||||
if (iconExists(name))
|
||||
return "An icon has this name";
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
if (page.slots[i].bitmap[0] && !bitmapExists(page.slots[i].bitmap))
|
||||
return "Background bitmap not found";
|
||||
if (page.slots[i].bitmap[0] && !bitmapExists(page.slots[i].bitmap) && !iconExists(page.slots[i].bitmap))
|
||||
return "Background bitmap or icon not found";
|
||||
for (int i = 0; i < page.element_count; i++)
|
||||
if (!fontExists(page.elements[i].font))
|
||||
return "Font not found";
|
||||
@@ -340,6 +345,183 @@ const char *appDeletePage(const char *name)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appSaveIcon(const char *name, const Icon &icon)
|
||||
{
|
||||
if (pageExists(name))
|
||||
return "A page has this name";
|
||||
if (bitmapExists(name))
|
||||
return "A bitmap has this name";
|
||||
const char *error = saveIcon(name, icon);
|
||||
if (error)
|
||||
return error;
|
||||
reloadPage(); // the page may use it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appDeleteIcon(const char *name)
|
||||
{
|
||||
String user = pageUsingImage(name);
|
||||
if (user.length())
|
||||
return usedBy(user);
|
||||
const char *error = deleteIcon(name);
|
||||
if (error)
|
||||
return error;
|
||||
reloadPage(); // an image by state may have shown it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Why the bitmap can't become an icon (it is shown on its own), or nullptr
|
||||
static const char *shownOnItsOwn(const char *name)
|
||||
{
|
||||
if (builtinBitmap(name))
|
||||
return "Built-in bitmaps can't become icons";
|
||||
if (scheduleUses(page_schedule, name))
|
||||
return "Shown by the schedule or a rule: change that first";
|
||||
if (sameName(settings.page, name))
|
||||
return "It is the chosen page: choose another one first";
|
||||
if (sameName(settings.off_page, name))
|
||||
return "It is the off image: choose another one first";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Without redrawing: the pages look the same
|
||||
static const char *moveToIcon(const char *name)
|
||||
{
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
if (!loadBitmap(name, bitmap))
|
||||
return "Bitmap not found";
|
||||
Icon icon;
|
||||
iconFromBitmap(bitmap, icon);
|
||||
const char *error = saveIcon(name, icon);
|
||||
if (error)
|
||||
return error;
|
||||
error = deleteBitmap(name);
|
||||
if (error)
|
||||
deleteIcon(name); // only one of them
|
||||
return error;
|
||||
}
|
||||
|
||||
const char *appBitmapToIcon(const char *name)
|
||||
{
|
||||
const char *error = shownOnItsOwn(name);
|
||||
if (error)
|
||||
return error;
|
||||
error = moveToIcon(name);
|
||||
if (error)
|
||||
return error;
|
||||
reloadPage();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appIconToBitmap(const char *name)
|
||||
{
|
||||
Icon icon;
|
||||
if (!loadIcon(name, icon))
|
||||
return "Icon not found";
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
iconToBitmap(icon, bitmap);
|
||||
const char *error = saveBitmap(name, bitmap);
|
||||
if (error)
|
||||
return error;
|
||||
error = deleteIcon(name);
|
||||
if (error)
|
||||
{
|
||||
deleteBitmap(name); // only one of them
|
||||
return error;
|
||||
}
|
||||
reloadPage();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The one-time move, in steps over many loop() passes: LittleFS is slow (listing a
|
||||
// directory of 40 files takes about a second), and loop() must never get near the
|
||||
// watchdog. Each pass works for about MIGRATION_STEP_MS.
|
||||
enum class Migration
|
||||
{
|
||||
Start,
|
||||
Pages, // which candidates the pages use (one page after the other)
|
||||
Move,
|
||||
Done,
|
||||
};
|
||||
static const uint32_t MIGRATION_STEP_MS = 1000;
|
||||
static Migration migration = Migration::Start;
|
||||
static std::vector<String> migration_bitmaps; // uploaded bitmaps not shown on their own
|
||||
static std::vector<bool> migration_used;
|
||||
static std::vector<String> migration_pages;
|
||||
static size_t migration_next = 0;
|
||||
static int migration_moved = 0;
|
||||
static uint32_t migration_started = 0;
|
||||
|
||||
void appMigrateIcons()
|
||||
{
|
||||
if (migration == Migration::Done)
|
||||
return;
|
||||
uint32_t pass = millis();
|
||||
auto time_left = [pass]() { return millis() - pass < MIGRATION_STEP_MS; };
|
||||
|
||||
if (migration == Migration::Start)
|
||||
{
|
||||
if (iconMigrationDone())
|
||||
{
|
||||
migration = Migration::Done;
|
||||
return;
|
||||
}
|
||||
migration_started = pass;
|
||||
for (const BitmapInfo &b : listBitmaps())
|
||||
if (!b.builtin && !shownOnItsOwn(b.name.c_str()))
|
||||
migration_bitmaps.push_back(b.name);
|
||||
migration_used.assign(migration_bitmaps.size(), false);
|
||||
for (const PageInfo &p : listPages())
|
||||
migration_pages.push_back(p.name);
|
||||
migration_next = 0;
|
||||
migration = Migration::Pages;
|
||||
return;
|
||||
}
|
||||
|
||||
if (migration == Migration::Pages)
|
||||
{
|
||||
DynamicPage *page = new DynamicPage;
|
||||
while (migration_next < migration_pages.size() && time_left())
|
||||
if (loadPage(migration_pages[migration_next++].c_str(), *page))
|
||||
for (size_t i = 0; i < migration_bitmaps.size(); i++)
|
||||
if (pageUsesImage(*page, migration_bitmaps[i].c_str(), true))
|
||||
migration_used[i] = true;
|
||||
delete page;
|
||||
if (migration_next >= migration_pages.size())
|
||||
{
|
||||
migration_next = 0;
|
||||
migration = Migration::Move;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (migration_next < migration_bitmaps.size() && time_left())
|
||||
{
|
||||
size_t i = migration_next++;
|
||||
const char *name = migration_bitmaps[i].c_str();
|
||||
// Checked again: the web UI may have changed something in the meantime
|
||||
if (!migration_used[i] || shownOnItsOwn(name))
|
||||
continue;
|
||||
const char *error = moveToIcon(name);
|
||||
if (error)
|
||||
Serial.printf("Bitmap \"%s\" stays a bitmap: %s\n", name, error);
|
||||
else
|
||||
migration_moved++;
|
||||
}
|
||||
if (migration_next < migration_bitmaps.size())
|
||||
return;
|
||||
|
||||
setIconMigrationDone();
|
||||
Serial.printf("%d bitmaps that only pages use became icons (%lu ms)\n", migration_moved,
|
||||
(unsigned long)(millis() - migration_started));
|
||||
migration_bitmaps.clear();
|
||||
migration_used.clear();
|
||||
migration_pages.clear();
|
||||
migration = Migration::Done;
|
||||
if (migration_moved)
|
||||
reloadPage();
|
||||
}
|
||||
|
||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size)
|
||||
{
|
||||
const char *error = saveFont(name, data, size);
|
||||
|
||||
Reference in new issue
Block a user