Fixes
This commit is contained in:
parent
a430110f94
commit
e22979c1c7
Binary file not shown.
Binary file not shown.
@ -131,12 +131,18 @@ void WidgetConfig::serialize(uint8_t* buf) const {
|
|||||||
buf[pos++] = (sb.knxAddrRead >> 8) & 0xFF;
|
buf[pos++] = (sb.knxAddrRead >> 8) & 0xFF;
|
||||||
buf[pos++] = sb.knxAddrWrite & 0xFF;
|
buf[pos++] = sb.knxAddrWrite & 0xFF;
|
||||||
buf[pos++] = (sb.knxAddrWrite >> 8) & 0xFF;
|
buf[pos++] = (sb.knxAddrWrite >> 8) & 0xFF;
|
||||||
buf[pos++] = sb.colorOn.r;
|
buf[pos++] = sb.bgColorOn.r;
|
||||||
buf[pos++] = sb.colorOn.g;
|
buf[pos++] = sb.bgColorOn.g;
|
||||||
buf[pos++] = sb.colorOn.b;
|
buf[pos++] = sb.bgColorOn.b;
|
||||||
buf[pos++] = sb.colorOff.r;
|
buf[pos++] = sb.bgColorOff.r;
|
||||||
buf[pos++] = sb.colorOff.g;
|
buf[pos++] = sb.bgColorOff.g;
|
||||||
buf[pos++] = sb.colorOff.b;
|
buf[pos++] = sb.bgColorOff.b;
|
||||||
|
buf[pos++] = sb.iconColorOn.r;
|
||||||
|
buf[pos++] = sb.iconColorOn.g;
|
||||||
|
buf[pos++] = sb.iconColorOn.b;
|
||||||
|
buf[pos++] = sb.iconColorOff.r;
|
||||||
|
buf[pos++] = sb.iconColorOff.g;
|
||||||
|
buf[pos++] = sb.iconColorOff.b;
|
||||||
buf[pos++] = static_cast<uint8_t>(sb.position);
|
buf[pos++] = static_cast<uint8_t>(sb.position);
|
||||||
buf[pos++] = static_cast<uint8_t>(sb.action);
|
buf[pos++] = static_cast<uint8_t>(sb.action);
|
||||||
buf[pos++] = sb.targetScreen;
|
buf[pos++] = sb.targetScreen;
|
||||||
@ -307,7 +313,7 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
|
|||||||
|
|
||||||
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
SubButtonConfig& sb = subButtons[i];
|
SubButtonConfig& sb = subButtons[i];
|
||||||
if (pos + 24 <= SERIALIZED_SIZE) {
|
if (pos + 30 <= SERIALIZED_SIZE) {
|
||||||
sb.iconCodepointOff = buf[pos] | (buf[pos + 1] << 8) |
|
sb.iconCodepointOff = buf[pos] | (buf[pos + 1] << 8) |
|
||||||
(buf[pos + 2] << 16) | (buf[pos + 3] << 24);
|
(buf[pos + 2] << 16) | (buf[pos + 3] << 24);
|
||||||
pos += 4;
|
pos += 4;
|
||||||
@ -316,12 +322,18 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
|
|||||||
pos += 4;
|
pos += 4;
|
||||||
sb.knxAddrRead = buf[pos] | (buf[pos + 1] << 8); pos += 2;
|
sb.knxAddrRead = buf[pos] | (buf[pos + 1] << 8); pos += 2;
|
||||||
sb.knxAddrWrite = buf[pos] | (buf[pos + 1] << 8); pos += 2;
|
sb.knxAddrWrite = buf[pos] | (buf[pos + 1] << 8); pos += 2;
|
||||||
sb.colorOn.r = buf[pos++];
|
sb.bgColorOn.r = buf[pos++];
|
||||||
sb.colorOn.g = buf[pos++];
|
sb.bgColorOn.g = buf[pos++];
|
||||||
sb.colorOn.b = buf[pos++];
|
sb.bgColorOn.b = buf[pos++];
|
||||||
sb.colorOff.r = buf[pos++];
|
sb.bgColorOff.r = buf[pos++];
|
||||||
sb.colorOff.g = buf[pos++];
|
sb.bgColorOff.g = buf[pos++];
|
||||||
sb.colorOff.b = buf[pos++];
|
sb.bgColorOff.b = buf[pos++];
|
||||||
|
sb.iconColorOn.r = buf[pos++];
|
||||||
|
sb.iconColorOn.g = buf[pos++];
|
||||||
|
sb.iconColorOn.b = buf[pos++];
|
||||||
|
sb.iconColorOff.r = buf[pos++];
|
||||||
|
sb.iconColorOff.g = buf[pos++];
|
||||||
|
sb.iconColorOff.b = buf[pos++];
|
||||||
sb.position = static_cast<SubButtonPosition>(buf[pos++]);
|
sb.position = static_cast<SubButtonPosition>(buf[pos++]);
|
||||||
sb.action = static_cast<SubButtonAction>(buf[pos++]);
|
sb.action = static_cast<SubButtonAction>(buf[pos++]);
|
||||||
sb.targetScreen = buf[pos++];
|
sb.targetScreen = buf[pos++];
|
||||||
@ -566,6 +578,8 @@ void GuiConfig::clear() {
|
|||||||
knxDateAddress = 0;
|
knxDateAddress = 0;
|
||||||
knxDateTimeAddress = 0;
|
knxDateTimeAddress = 0;
|
||||||
knxNightModeAddress = 0;
|
knxNightModeAddress = 0;
|
||||||
|
screenAnimType = ScreenAnimType::FADE;
|
||||||
|
screenAnimDuration = 300; // Default 300ms
|
||||||
for (size_t i = 0; i < MAX_SCREENS; i++) {
|
for (size_t i = 0; i < MAX_SCREENS; i++) {
|
||||||
screens[i].clear(static_cast<uint8_t>(i), nullptr);
|
screens[i].clear(static_cast<uint8_t>(i), nullptr);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,20 +169,22 @@ enum class SubButtonAction : uint8_t {
|
|||||||
NAVIGATE = 1, // Navigate to screen
|
NAVIGATE = 1, // Navigate to screen
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sub-button configuration for RoomCard (24 bytes)
|
// Sub-button configuration for RoomCard (30 bytes)
|
||||||
struct SubButtonConfig {
|
struct SubButtonConfig {
|
||||||
uint32_t iconCodepointOff; // 4 bytes - Icon codepoint when OFF
|
uint32_t iconCodepointOff; // 4 bytes - Icon codepoint when OFF
|
||||||
uint32_t iconCodepointOn; // 4 bytes - Icon codepoint when ON (0 = use iconCodepointOff)
|
uint32_t iconCodepointOn; // 4 bytes - Icon codepoint when ON (0 = use iconCodepointOff)
|
||||||
uint16_t knxAddrRead; // 2 bytes - KNX address to read status
|
uint16_t knxAddrRead; // 2 bytes - KNX address to read status
|
||||||
uint16_t knxAddrWrite; // 2 bytes - KNX address to write on click
|
uint16_t knxAddrWrite; // 2 bytes - KNX address to write on click
|
||||||
Color colorOn; // 3 bytes - Color when ON
|
Color bgColorOn; // 3 bytes - Background color when ON
|
||||||
Color colorOff; // 3 bytes - Color when OFF
|
Color bgColorOff; // 3 bytes - Background color when OFF
|
||||||
|
Color iconColorOn; // 3 bytes - Icon color when ON
|
||||||
|
Color iconColorOff; // 3 bytes - Icon color when OFF
|
||||||
SubButtonPosition position; // 1 byte - Position around bubble
|
SubButtonPosition position; // 1 byte - Position around bubble
|
||||||
SubButtonAction action; // 1 byte - Action type
|
SubButtonAction action; // 1 byte - Action type
|
||||||
uint8_t targetScreen; // 1 byte - Target screen for navigate
|
uint8_t targetScreen; // 1 byte - Target screen for navigate
|
||||||
bool enabled; // 1 byte - Is this sub-button active?
|
bool enabled; // 1 byte - Is this sub-button active?
|
||||||
uint8_t _padding[2]; // 2 bytes - Alignment padding
|
uint8_t _padding[2]; // 2 bytes - Alignment padding
|
||||||
// Total: 24 bytes per SubButton
|
// Total: 30 bytes per SubButton
|
||||||
};
|
};
|
||||||
|
|
||||||
// Text line configuration for RoomCard (24 bytes)
|
// Text line configuration for RoomCard (24 bytes)
|
||||||
@ -297,8 +299,8 @@ struct WidgetConfig {
|
|||||||
uint8_t arcValueFontSize; // Center value font size index
|
uint8_t arcValueFontSize; // Center value font size index
|
||||||
|
|
||||||
// Serialization size (fixed for NVS storage)
|
// Serialization size (fixed for NVS storage)
|
||||||
// 345 + 24 (6 subbuttons * 4 bytes for iconCodepointOn) = 369
|
// 369 + 36 (6 subbuttons * 6 bytes for icon colors) = 405
|
||||||
static constexpr size_t SERIALIZED_SIZE = 369;
|
static constexpr size_t SERIALIZED_SIZE = 405;
|
||||||
|
|
||||||
void serialize(uint8_t* buf) const;
|
void serialize(uint8_t* buf) const;
|
||||||
void deserialize(const uint8_t* buf);
|
void deserialize(const uint8_t* buf);
|
||||||
@ -341,6 +343,20 @@ struct ScreenConfig {
|
|||||||
const WidgetConfig* findWidget(uint8_t id) const;
|
const WidgetConfig* findWidget(uint8_t id) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Screen transition animation types
|
||||||
|
enum class ScreenAnimType : uint8_t {
|
||||||
|
NONE = 0,
|
||||||
|
FADE = 1,
|
||||||
|
SLIDE_LEFT = 2,
|
||||||
|
SLIDE_RIGHT = 3,
|
||||||
|
SLIDE_UP = 4,
|
||||||
|
SLIDE_DOWN = 5,
|
||||||
|
OVER_LEFT = 6,
|
||||||
|
OVER_RIGHT = 7,
|
||||||
|
OVER_UP = 8,
|
||||||
|
OVER_DOWN = 9,
|
||||||
|
};
|
||||||
|
|
||||||
struct GuiConfig {
|
struct GuiConfig {
|
||||||
uint8_t screenCount;
|
uint8_t screenCount;
|
||||||
ScreenConfig screens[MAX_SCREENS];
|
ScreenConfig screens[MAX_SCREENS];
|
||||||
@ -353,6 +369,10 @@ struct GuiConfig {
|
|||||||
uint16_t knxDateTimeAddress;
|
uint16_t knxDateTimeAddress;
|
||||||
uint16_t knxNightModeAddress;
|
uint16_t knxNightModeAddress;
|
||||||
|
|
||||||
|
// Screen transition animation
|
||||||
|
ScreenAnimType screenAnimType;
|
||||||
|
uint16_t screenAnimDuration; // Duration in ms (0 = default 300ms)
|
||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
ScreenConfig* findScreen(uint8_t id);
|
ScreenConfig* findScreen(uint8_t id);
|
||||||
const ScreenConfig* findScreen(uint8_t id) const;
|
const ScreenConfig* findScreen(uint8_t id) const;
|
||||||
|
|||||||
@ -359,6 +359,23 @@ void WidgetManager::applyScreen(uint8_t screenId) {
|
|||||||
esp_lv_adapter_unlock();
|
esp_lv_adapter_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to convert ScreenAnimType to LVGL animation type (LVGL 9.x API)
|
||||||
|
static lv_screen_load_anim_t screenAnimToLvgl(ScreenAnimType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ScreenAnimType::NONE: return LV_SCREEN_LOAD_ANIM_NONE;
|
||||||
|
case ScreenAnimType::FADE: return LV_SCREEN_LOAD_ANIM_FADE_IN;
|
||||||
|
case ScreenAnimType::SLIDE_LEFT: return LV_SCREEN_LOAD_ANIM_MOVE_LEFT;
|
||||||
|
case ScreenAnimType::SLIDE_RIGHT: return LV_SCREEN_LOAD_ANIM_MOVE_RIGHT;
|
||||||
|
case ScreenAnimType::SLIDE_UP: return LV_SCREEN_LOAD_ANIM_MOVE_TOP;
|
||||||
|
case ScreenAnimType::SLIDE_DOWN: return LV_SCREEN_LOAD_ANIM_MOVE_BOTTOM;
|
||||||
|
case ScreenAnimType::OVER_LEFT: return LV_SCREEN_LOAD_ANIM_OVER_LEFT;
|
||||||
|
case ScreenAnimType::OVER_RIGHT: return LV_SCREEN_LOAD_ANIM_OVER_RIGHT;
|
||||||
|
case ScreenAnimType::OVER_UP: return LV_SCREEN_LOAD_ANIM_OVER_TOP;
|
||||||
|
case ScreenAnimType::OVER_DOWN: return LV_SCREEN_LOAD_ANIM_OVER_BOTTOM;
|
||||||
|
default: return LV_SCREEN_LOAD_ANIM_FADE_IN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
||||||
ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
|
ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
|
||||||
|
|
||||||
@ -375,35 +392,40 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
|||||||
closeModalLocked();
|
closeModalLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_display_t* disp = lv_display_get_default();
|
// SAFE DESTRUCTION of C++ widgets:
|
||||||
if (disp) {
|
|
||||||
lv_display_enable_invalidation(disp, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFE DESTRUCTION:
|
|
||||||
// 1. Mark all C++ widgets as "LVGL object already gone"
|
// 1. Mark all C++ widgets as "LVGL object already gone"
|
||||||
for (auto& widget : widgets_) {
|
for (auto& widget : widgets_) {
|
||||||
if (widget) widget->clearLvglObject();
|
if (widget) widget->clearLvglObject();
|
||||||
}
|
}
|
||||||
|
// 2. Clean layer_top (sub-buttons etc)
|
||||||
// 2. Delete all LVGL objects on layers we use
|
|
||||||
lv_obj_clean(lv_scr_act());
|
|
||||||
lv_obj_clean(lv_layer_top());
|
lv_obj_clean(lv_layer_top());
|
||||||
|
// 3. Destroy C++ objects (their destructors won't call lv_obj_delete)
|
||||||
// 3. Now destroy C++ objects (their destructors won't call lv_obj_delete)
|
|
||||||
destroyAllWidgets();
|
destroyAllWidgets();
|
||||||
|
|
||||||
|
// Create a NEW screen object for the transition
|
||||||
|
lv_obj_t* newScreen = lv_obj_create(NULL);
|
||||||
|
lv_obj_remove_style_all(newScreen);
|
||||||
|
lv_obj_set_size(newScreen, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_clear_flag(newScreen, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
|
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
|
||||||
screen->name, screen->widgetCount);
|
screen->name, screen->widgetCount);
|
||||||
lv_obj_t* root = lv_scr_act();
|
createAllWidgets(*screen, newScreen);
|
||||||
createAllWidgets(*screen, root);
|
|
||||||
ESP_LOGI(TAG, "Widgets created");
|
ESP_LOGI(TAG, "Widgets created");
|
||||||
applyCachedValuesToWidgets();
|
applyCachedValuesToWidgets();
|
||||||
|
|
||||||
if (disp) {
|
// Get animation settings
|
||||||
lv_display_enable_invalidation(disp, true);
|
ESP_LOGI(TAG, "Animation config: type=%d, duration=%u",
|
||||||
}
|
(int)config_->screenAnimType, config_->screenAnimDuration);
|
||||||
lv_obj_invalidate(lv_scr_act());
|
lv_screen_load_anim_t animType = screenAnimToLvgl(config_->screenAnimType);
|
||||||
|
uint32_t animDuration = config_->screenAnimDuration > 0 ? config_->screenAnimDuration : 300;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Loading screen with animation: lvgl_type=%d, duration=%lums",
|
||||||
|
(int)animType, (unsigned long)animDuration);
|
||||||
|
|
||||||
|
// Load the new screen with animation (old screen auto-deleted after animation)
|
||||||
|
// Parameters: new_scr, anim_type, time_ms, delay_ms, auto_del_old_scr
|
||||||
|
lv_screen_load_anim(newScreen, animType, animDuration, 0, true);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId);
|
ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId);
|
||||||
}
|
}
|
||||||
@ -1503,6 +1525,11 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
|||||||
cJSON_AddNumberToObject(knx, "dateTime", config_->knxDateTimeAddress);
|
cJSON_AddNumberToObject(knx, "dateTime", config_->knxDateTimeAddress);
|
||||||
cJSON_AddNumberToObject(knx, "night", config_->knxNightModeAddress);
|
cJSON_AddNumberToObject(knx, "night", config_->knxNightModeAddress);
|
||||||
|
|
||||||
|
// Screen transition animation
|
||||||
|
cJSON* anim = cJSON_AddObjectToObject(root, "screenAnim");
|
||||||
|
cJSON_AddNumberToObject(anim, "type", static_cast<int>(config_->screenAnimType));
|
||||||
|
cJSON_AddNumberToObject(anim, "duration", config_->screenAnimDuration);
|
||||||
|
|
||||||
cJSON* screens = cJSON_AddArrayToObject(root, "screens");
|
cJSON* screens = cJSON_AddArrayToObject(root, "screens");
|
||||||
|
|
||||||
for (uint8_t s = 0; s < config_->screenCount; s++) {
|
for (uint8_t s = 0; s < config_->screenCount; s++) {
|
||||||
@ -1722,13 +1749,19 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
|||||||
cJSON_AddNumberToObject(sbJson, "action", static_cast<int>(sb.action));
|
cJSON_AddNumberToObject(sbJson, "action", static_cast<int>(sb.action));
|
||||||
cJSON_AddNumberToObject(sbJson, "target", sb.targetScreen);
|
cJSON_AddNumberToObject(sbJson, "target", sb.targetScreen);
|
||||||
|
|
||||||
char colorOnStr[8], colorOffStr[8];
|
char bgColorOnStr[8], bgColorOffStr[8], iconColorOnStr[8], iconColorOffStr[8];
|
||||||
snprintf(colorOnStr, sizeof(colorOnStr), "#%02X%02X%02X",
|
snprintf(bgColorOnStr, sizeof(bgColorOnStr), "#%02X%02X%02X",
|
||||||
sb.colorOn.r, sb.colorOn.g, sb.colorOn.b);
|
sb.bgColorOn.r, sb.bgColorOn.g, sb.bgColorOn.b);
|
||||||
snprintf(colorOffStr, sizeof(colorOffStr), "#%02X%02X%02X",
|
snprintf(bgColorOffStr, sizeof(bgColorOffStr), "#%02X%02X%02X",
|
||||||
sb.colorOff.r, sb.colorOff.g, sb.colorOff.b);
|
sb.bgColorOff.r, sb.bgColorOff.g, sb.bgColorOff.b);
|
||||||
cJSON_AddStringToObject(sbJson, "colorOn", colorOnStr);
|
snprintf(iconColorOnStr, sizeof(iconColorOnStr), "#%02X%02X%02X",
|
||||||
cJSON_AddStringToObject(sbJson, "colorOff", colorOffStr);
|
sb.iconColorOn.r, sb.iconColorOn.g, sb.iconColorOn.b);
|
||||||
|
snprintf(iconColorOffStr, sizeof(iconColorOffStr), "#%02X%02X%02X",
|
||||||
|
sb.iconColorOff.r, sb.iconColorOff.g, sb.iconColorOff.b);
|
||||||
|
cJSON_AddStringToObject(sbJson, "bgColorOn", bgColorOnStr);
|
||||||
|
cJSON_AddStringToObject(sbJson, "bgColorOff", bgColorOffStr);
|
||||||
|
cJSON_AddStringToObject(sbJson, "iconColorOn", iconColorOnStr);
|
||||||
|
cJSON_AddStringToObject(sbJson, "iconColorOff", iconColorOffStr);
|
||||||
|
|
||||||
cJSON_AddItemToArray(subButtons, sbJson);
|
cJSON_AddItemToArray(subButtons, sbJson);
|
||||||
}
|
}
|
||||||
@ -2199,14 +2232,42 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
|||||||
sb.targetScreen = target->valueint;
|
sb.targetScreen = target->valueint;
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON* colorOn = cJSON_GetObjectItem(sbItem, "colorOn");
|
// Background colors
|
||||||
if (cJSON_IsString(colorOn)) {
|
cJSON* bgColorOn = cJSON_GetObjectItem(sbItem, "bgColorOn");
|
||||||
sb.colorOn = Color::fromHex(parseHexColor(colorOn->valuestring));
|
if (cJSON_IsString(bgColorOn)) {
|
||||||
|
sb.bgColorOn = Color::fromHex(parseHexColor(bgColorOn->valuestring));
|
||||||
|
} else {
|
||||||
|
// Backward compatibility: try old "colorOn" field
|
||||||
|
cJSON* colorOn = cJSON_GetObjectItem(sbItem, "colorOn");
|
||||||
|
if (cJSON_IsString(colorOn)) {
|
||||||
|
sb.bgColorOn = Color::fromHex(parseHexColor(colorOn->valuestring));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON* colorOff = cJSON_GetObjectItem(sbItem, "colorOff");
|
cJSON* bgColorOff = cJSON_GetObjectItem(sbItem, "bgColorOff");
|
||||||
if (cJSON_IsString(colorOff)) {
|
if (cJSON_IsString(bgColorOff)) {
|
||||||
sb.colorOff = Color::fromHex(parseHexColor(colorOff->valuestring));
|
sb.bgColorOff = Color::fromHex(parseHexColor(bgColorOff->valuestring));
|
||||||
|
} else {
|
||||||
|
// Backward compatibility: try old "colorOff" field
|
||||||
|
cJSON* colorOff = cJSON_GetObjectItem(sbItem, "colorOff");
|
||||||
|
if (cJSON_IsString(colorOff)) {
|
||||||
|
sb.bgColorOff = Color::fromHex(parseHexColor(colorOff->valuestring));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon colors (default to white if not specified)
|
||||||
|
cJSON* iconColorOn = cJSON_GetObjectItem(sbItem, "iconColorOn");
|
||||||
|
if (cJSON_IsString(iconColorOn)) {
|
||||||
|
sb.iconColorOn = Color::fromHex(parseHexColor(iconColorOn->valuestring));
|
||||||
|
} else {
|
||||||
|
sb.iconColorOn = {255, 255, 255}; // Default white
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* iconColorOff = cJSON_GetObjectItem(sbItem, "iconColorOff");
|
||||||
|
if (cJSON_IsString(iconColorOff)) {
|
||||||
|
sb.iconColorOff = Color::fromHex(parseHexColor(iconColorOff->valuestring));
|
||||||
|
} else {
|
||||||
|
sb.iconColorOff = {255, 255, 255}; // Default white
|
||||||
}
|
}
|
||||||
|
|
||||||
sbIdx++;
|
sbIdx++;
|
||||||
@ -2410,6 +2471,19 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
|||||||
if (cJSON_IsNumber(nightAddr)) newConfig->knxNightModeAddress = nightAddr->valueint;
|
if (cJSON_IsNumber(nightAddr)) newConfig->knxNightModeAddress = nightAddr->valueint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screen transition animation
|
||||||
|
cJSON* anim = cJSON_GetObjectItem(root, "screenAnim");
|
||||||
|
if (cJSON_IsObject(anim)) {
|
||||||
|
cJSON* animType = cJSON_GetObjectItem(anim, "type");
|
||||||
|
if (cJSON_IsNumber(animType)) {
|
||||||
|
newConfig->screenAnimType = static_cast<ScreenAnimType>(animType->valueint);
|
||||||
|
}
|
||||||
|
cJSON* animDuration = cJSON_GetObjectItem(anim, "duration");
|
||||||
|
if (cJSON_IsNumber(animDuration)) {
|
||||||
|
newConfig->screenAnimDuration = static_cast<uint16_t>(animDuration->valueint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newConfig->screenCount == 0) {
|
if (newConfig->screenCount == 0) {
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -150,7 +150,10 @@ void RoomCardWidgetBase::applySubButtonStyle() {
|
|||||||
for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
|
for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
|
||||||
if (subButtonIcons_[i] && subBtnIconFont) {
|
if (subButtonIcons_[i] && subBtnIconFont) {
|
||||||
lv_obj_set_style_text_font(subButtonIcons_[i], subBtnIconFont, 0);
|
lv_obj_set_style_text_font(subButtonIcons_[i], subBtnIconFont, 0);
|
||||||
lv_obj_set_style_text_color(subButtonIcons_[i], lv_color_white(), 0);
|
// Set initial icon color based on state (OFF by default)
|
||||||
|
const Color& iconColor = subButtonStates_[i] ?
|
||||||
|
config_.subButtons[i].iconColorOn : config_.subButtons[i].iconColorOff;
|
||||||
|
lv_obj_set_style_text_color(subButtonIcons_[i], lv_color_hex(iconColor.toLvColor()), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,11 +163,12 @@ void RoomCardWidgetBase::updateSubButtonColor(uint8_t index) {
|
|||||||
|
|
||||||
const SubButtonConfig& cfg = config_.subButtons[index];
|
const SubButtonConfig& cfg = config_.subButtons[index];
|
||||||
bool isOn = subButtonStates_[index];
|
bool isOn = subButtonStates_[index];
|
||||||
const Color& color = isOn ? cfg.colorOn : cfg.colorOff;
|
|
||||||
|
|
||||||
lv_obj_set_style_bg_color(subButtonObjs_[index], lv_color_hex(color.toLvColor()), 0);
|
// Update background color
|
||||||
|
const Color& bgColor = isOn ? cfg.bgColorOn : cfg.bgColorOff;
|
||||||
|
lv_obj_set_style_bg_color(subButtonObjs_[index], lv_color_hex(bgColor.toLvColor()), 0);
|
||||||
|
|
||||||
// Update icon based on state
|
// Update icon and icon color based on state
|
||||||
if (subButtonIcons_[index]) {
|
if (subButtonIcons_[index]) {
|
||||||
uint32_t iconCodepoint = isOn && cfg.iconCodepointOn > 0 ? cfg.iconCodepointOn : cfg.iconCodepointOff;
|
uint32_t iconCodepoint = isOn && cfg.iconCodepointOn > 0 ? cfg.iconCodepointOn : cfg.iconCodepointOff;
|
||||||
if (iconCodepoint > 0) {
|
if (iconCodepoint > 0) {
|
||||||
@ -172,6 +176,10 @@ void RoomCardWidgetBase::updateSubButtonColor(uint8_t index) {
|
|||||||
encodeUtf8(iconCodepoint, iconText);
|
encodeUtf8(iconCodepoint, iconText);
|
||||||
lv_label_set_text(subButtonIcons_[index], iconText);
|
lv_label_set_text(subButtonIcons_[index], iconText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set icon color
|
||||||
|
const Color& iconColor = isOn ? cfg.iconColorOn : cfg.iconColorOff;
|
||||||
|
lv_obj_set_style_text_color(subButtonIcons_[index], lv_color_hex(iconColor.toLvColor()), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2861,7 +2861,7 @@ CONFIG_LV_USE_BUILTIN_SPRINTF=y
|
|||||||
#
|
#
|
||||||
# HAL Settings
|
# HAL Settings
|
||||||
#
|
#
|
||||||
CONFIG_LV_DEF_REFR_PERIOD=33
|
CONFIG_LV_DEF_REFR_PERIOD=16
|
||||||
CONFIG_LV_DPI_DEF=130
|
CONFIG_LV_DPI_DEF=130
|
||||||
# end of HAL Settings
|
# end of HAL Settings
|
||||||
|
|
||||||
@ -2933,12 +2933,12 @@ CONFIG_LV_USE_DRAW_SW_ASM=0
|
|||||||
#
|
#
|
||||||
CONFIG_LV_USE_LOG=y
|
CONFIG_LV_USE_LOG=y
|
||||||
# CONFIG_LV_LOG_LEVEL_TRACE is not set
|
# CONFIG_LV_LOG_LEVEL_TRACE is not set
|
||||||
CONFIG_LV_LOG_LEVEL_INFO=y
|
# CONFIG_LV_LOG_LEVEL_INFO is not set
|
||||||
# CONFIG_LV_LOG_LEVEL_WARN is not set
|
# CONFIG_LV_LOG_LEVEL_WARN is not set
|
||||||
# CONFIG_LV_LOG_LEVEL_ERROR is not set
|
CONFIG_LV_LOG_LEVEL_ERROR=y
|
||||||
# CONFIG_LV_LOG_LEVEL_USER is not set
|
# CONFIG_LV_LOG_LEVEL_USER is not set
|
||||||
# CONFIG_LV_LOG_LEVEL_NONE is not set
|
# CONFIG_LV_LOG_LEVEL_NONE is not set
|
||||||
CONFIG_LV_LOG_LEVEL=1
|
CONFIG_LV_LOG_LEVEL=3
|
||||||
CONFIG_LV_LOG_PRINTF=y
|
CONFIG_LV_LOG_PRINTF=y
|
||||||
CONFIG_LV_LOG_USE_TIMESTAMP=y
|
CONFIG_LV_LOG_USE_TIMESTAMP=y
|
||||||
CONFIG_LV_LOG_USE_FILE_LINE=y
|
CONFIG_LV_LOG_USE_FILE_LINE=y
|
||||||
|
|||||||
@ -30,6 +30,28 @@
|
|||||||
<input class="flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent" type="number" min="0" v-model.number="store.config.standby.minutes">
|
<input class="flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent" type="number" min="0" v-model.number="store.config.standby.minutes">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2.5">
|
||||||
|
<div class="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">Screen-Animation</div>
|
||||||
|
<div class="flex items-center justify-between gap-2.5">
|
||||||
|
<label class="text-[12px] text-muted">Typ</label>
|
||||||
|
<select class="flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent" v-model.number="store.config.screenAnim.type">
|
||||||
|
<option :value="0">Keine</option>
|
||||||
|
<option :value="1">Einblenden</option>
|
||||||
|
<option :value="2">Schieben Links</option>
|
||||||
|
<option :value="3">Schieben Rechts</option>
|
||||||
|
<option :value="4">Schieben Hoch</option>
|
||||||
|
<option :value="5">Schieben Runter</option>
|
||||||
|
<option :value="6">Ueberlagern Links</option>
|
||||||
|
<option :value="7">Ueberlagern Rechts</option>
|
||||||
|
<option :value="8">Ueberlagern Hoch</option>
|
||||||
|
<option :value="9">Ueberlagern Runter</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2.5">
|
||||||
|
<label class="text-[12px] text-muted">Dauer (ms)</label>
|
||||||
|
<input class="flex-1 bg-panel-2 border border-border rounded-lg px-2 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent" type="number" min="0" max="2000" v-model.number="store.config.screenAnim.duration">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-2.5">
|
<div class="flex flex-col gap-2.5">
|
||||||
<div class="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">KNX Zeit</div>
|
<div class="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">KNX Zeit</div>
|
||||||
<div class="flex items-center justify-between gap-2.5">
|
<div class="flex items-center justify-between gap-2.5">
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
class="absolute rounded-full flex items-center justify-center shadow-md"
|
class="absolute rounded-full flex items-center justify-center shadow-md"
|
||||||
:style="getSubButtonStyle(sb, idx)"
|
:style="getSubButtonStyle(sb, idx)"
|
||||||
>
|
>
|
||||||
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
<span v-if="getSubButtonIcon(sb)" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
||||||
{{ String.fromCodePoint(sb.icon) }}
|
{{ String.fromCodePoint(getSubButtonIcon(sb)) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -56,8 +56,8 @@
|
|||||||
class="absolute rounded-full flex items-center justify-center shadow-md"
|
class="absolute rounded-full flex items-center justify-center shadow-md"
|
||||||
:style="getSubButtonStyleTile(sb, idx)"
|
:style="getSubButtonStyleTile(sb, idx)"
|
||||||
>
|
>
|
||||||
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
<span v-if="getSubButtonIcon(sb)" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
||||||
{{ String.fromCodePoint(sb.icon) }}
|
{{ String.fromCodePoint(getSubButtonIcon(sb)) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -162,13 +162,15 @@ const getSubButtonStyleTile = (sb, idx) => {
|
|||||||
const btnSize = (props.widget.subButtonSize || 40) * s;
|
const btnSize = (props.widget.subButtonSize || 40) * s;
|
||||||
const gap = 10 * s;
|
const gap = 10 * s;
|
||||||
const padding = 12 * s;
|
const padding = 12 * s;
|
||||||
|
// Use bgColorOff with fallback to old colorOff for compatibility
|
||||||
|
const bgColor = sb.bgColorOff || sb.colorOff || '#666666';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: btnSize + 'px',
|
width: btnSize + 'px',
|
||||||
height: btnSize + 'px',
|
height: btnSize + 'px',
|
||||||
right: padding + 'px',
|
right: padding + 'px',
|
||||||
top: (padding + idx * (btnSize + gap)) + 'px',
|
top: (padding + idx * (btnSize + gap)) + 'px',
|
||||||
backgroundColor: sb.colorOff || '#666666',
|
backgroundColor: bgColor,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,7 +243,8 @@ function getSubButtonStyle(sb, idx) {
|
|||||||
const angle = (pos * (Math.PI / 4)) - (Math.PI / 2);
|
const angle = (pos * (Math.PI / 4)) - (Math.PI / 2);
|
||||||
const x = centerX + orbitRadius * Math.cos(angle) - subBtnSize / 2;
|
const x = centerX + orbitRadius * Math.cos(angle) - subBtnSize / 2;
|
||||||
const y = centerY + orbitRadius * Math.sin(angle) - subBtnSize / 2;
|
const y = centerY + orbitRadius * Math.sin(angle) - subBtnSize / 2;
|
||||||
const bgColor = sb.colorOff || '#666666';
|
// Use bgColorOff with fallback to old colorOff for compatibility
|
||||||
|
const bgColor = sb.bgColorOff || sb.colorOff || '#666666';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: `${subBtnSize}px`,
|
width: `${subBtnSize}px`,
|
||||||
@ -257,12 +260,19 @@ function getSubButtonIconStyle(sb) {
|
|||||||
const s = props.scale;
|
const s = props.scale;
|
||||||
const subBtnSize = (props.widget.subButtonSize || 40) * s;
|
const subBtnSize = (props.widget.subButtonSize || 40) * s;
|
||||||
const iconSize = subBtnSize * 0.5;
|
const iconSize = subBtnSize * 0.5;
|
||||||
|
// Use iconColorOff with fallback for compatibility
|
||||||
|
const iconColor = sb.iconColorOff || '#ffffff';
|
||||||
return {
|
return {
|
||||||
fontSize: `${iconSize}px`,
|
fontSize: `${iconSize}px`,
|
||||||
color: '#ffffff'
|
color: iconColor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSubButtonIcon(sb) {
|
||||||
|
// Use iconOff with fallback to old icon field for compatibility
|
||||||
|
return sb.iconOff || sb.icon || 0;
|
||||||
|
}
|
||||||
|
|
||||||
const computedStyle = computed(() => {
|
const computedStyle = computed(() => {
|
||||||
const w = props.widget;
|
const w = props.widget;
|
||||||
const s = props.scale;
|
const s = props.scale;
|
||||||
|
|||||||
@ -212,12 +212,19 @@
|
|||||||
<option v-for="s in store.config.screens" :key="s.id" :value="s.id">{{ s.name }}</option>
|
<option v-for="s in store.config.screens" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-[11px] text-muted">
|
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||||
<label class="w-[50px]">Farben</label>
|
<label class="w-[50px]">Hintergr.</label>
|
||||||
<span class="text-[10px]">An:</span>
|
<span class="text-[10px]">An:</span>
|
||||||
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.colorOn">
|
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.bgColorOn">
|
||||||
<span class="text-[10px]">Aus:</span>
|
<span class="text-[10px]">Aus:</span>
|
||||||
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.colorOff">
|
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.bgColorOff">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-[11px] text-muted">
|
||||||
|
<label class="w-[50px]">Icon</label>
|
||||||
|
<span class="text-[10px]">An:</span>
|
||||||
|
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.iconColorOn">
|
||||||
|
<span class="text-[10px]">Aus:</span>
|
||||||
|
<input class="h-[22px] w-[28px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="sb.iconColorOff">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -331,8 +338,10 @@ const subButtonCount = computed({
|
|||||||
knxWrite: 0,
|
knxWrite: 0,
|
||||||
action: 0,
|
action: 0,
|
||||||
target: 0,
|
target: 0,
|
||||||
colorOn: '#FFCC00',
|
bgColorOn: '#FFCC00',
|
||||||
colorOff: '#666666'
|
bgColorOff: '#666666',
|
||||||
|
iconColorOn: '#FFFFFF',
|
||||||
|
iconColorOff: '#FFFFFF'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (props.widget.subButtons.length > target) {
|
if (props.widget.subButtons.length > target) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
startScreen: 0,
|
startScreen: 0,
|
||||||
standby: { enabled: false, screen: -1, minutes: 5 },
|
standby: { enabled: false, screen: -1, minutes: 5 },
|
||||||
knx: { time: 0, date: 0, dateTime: 0, night: 0 },
|
knx: { time: 0, date: 0, dateTime: 0, night: 0 },
|
||||||
|
screenAnim: { type: 1, duration: 300 }, // 1 = Fade, 300ms
|
||||||
screens: []
|
screens: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,6 +210,12 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
if (config.knx.dateTime === undefined) config.knx.dateTime = 0;
|
if (config.knx.dateTime === undefined) config.knx.dateTime = 0;
|
||||||
if (config.knx.night === undefined) config.knx.night = 0;
|
if (config.knx.night === undefined) config.knx.night = 0;
|
||||||
}
|
}
|
||||||
|
if (!config.screenAnim) {
|
||||||
|
config.screenAnim = { type: 1, duration: 300 };
|
||||||
|
} else {
|
||||||
|
if (config.screenAnim.type === undefined) config.screenAnim.type = 1;
|
||||||
|
if (config.screenAnim.duration === undefined) config.screenAnim.duration = 300;
|
||||||
|
}
|
||||||
mapLegacyKnxAddresses();
|
mapLegacyKnxAddresses();
|
||||||
|
|
||||||
// Recalculate IDs
|
// Recalculate IDs
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user