This commit is contained in:
Thomas Peterson 2026-02-10 11:54:01 +01:00
parent a430110f94
commit e22979c1c7
11 changed files with 234 additions and 70 deletions

View File

@ -131,12 +131,18 @@ void WidgetConfig::serialize(uint8_t* buf) const {
buf[pos++] = (sb.knxAddrRead >> 8) & 0xFF;
buf[pos++] = sb.knxAddrWrite & 0xFF;
buf[pos++] = (sb.knxAddrWrite >> 8) & 0xFF;
buf[pos++] = sb.colorOn.r;
buf[pos++] = sb.colorOn.g;
buf[pos++] = sb.colorOn.b;
buf[pos++] = sb.colorOff.r;
buf[pos++] = sb.colorOff.g;
buf[pos++] = sb.colorOff.b;
buf[pos++] = sb.bgColorOn.r;
buf[pos++] = sb.bgColorOn.g;
buf[pos++] = sb.bgColorOn.b;
buf[pos++] = sb.bgColorOff.r;
buf[pos++] = sb.bgColorOff.g;
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.action);
buf[pos++] = sb.targetScreen;
@ -307,7 +313,7 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
SubButtonConfig& sb = subButtons[i];
if (pos + 24 <= SERIALIZED_SIZE) {
if (pos + 30 <= SERIALIZED_SIZE) {
sb.iconCodepointOff = buf[pos] | (buf[pos + 1] << 8) |
(buf[pos + 2] << 16) | (buf[pos + 3] << 24);
pos += 4;
@ -316,12 +322,18 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
pos += 4;
sb.knxAddrRead = buf[pos] | (buf[pos + 1] << 8); pos += 2;
sb.knxAddrWrite = buf[pos] | (buf[pos + 1] << 8); pos += 2;
sb.colorOn.r = buf[pos++];
sb.colorOn.g = buf[pos++];
sb.colorOn.b = buf[pos++];
sb.colorOff.r = buf[pos++];
sb.colorOff.g = buf[pos++];
sb.colorOff.b = buf[pos++];
sb.bgColorOn.r = buf[pos++];
sb.bgColorOn.g = buf[pos++];
sb.bgColorOn.b = buf[pos++];
sb.bgColorOff.r = buf[pos++];
sb.bgColorOff.g = 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.action = static_cast<SubButtonAction>(buf[pos++]);
sb.targetScreen = buf[pos++];
@ -566,6 +578,8 @@ void GuiConfig::clear() {
knxDateAddress = 0;
knxDateTimeAddress = 0;
knxNightModeAddress = 0;
screenAnimType = ScreenAnimType::FADE;
screenAnimDuration = 300; // Default 300ms
for (size_t i = 0; i < MAX_SCREENS; i++) {
screens[i].clear(static_cast<uint8_t>(i), nullptr);
}

View File

@ -169,20 +169,22 @@ enum class SubButtonAction : uint8_t {
NAVIGATE = 1, // Navigate to screen
};
// Sub-button configuration for RoomCard (24 bytes)
// Sub-button configuration for RoomCard (30 bytes)
struct SubButtonConfig {
uint32_t iconCodepointOff; // 4 bytes - Icon codepoint when OFF
uint32_t iconCodepointOn; // 4 bytes - Icon codepoint when ON (0 = use iconCodepointOff)
uint16_t knxAddrRead; // 2 bytes - KNX address to read status
uint16_t knxAddrWrite; // 2 bytes - KNX address to write on click
Color colorOn; // 3 bytes - Color when ON
Color colorOff; // 3 bytes - Color when OFF
Color bgColorOn; // 3 bytes - Background color when ON
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
SubButtonAction action; // 1 byte - Action type
uint8_t targetScreen; // 1 byte - Target screen for navigate
bool enabled; // 1 byte - Is this sub-button active?
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)
@ -297,8 +299,8 @@ struct WidgetConfig {
uint8_t arcValueFontSize; // Center value font size index
// Serialization size (fixed for NVS storage)
// 345 + 24 (6 subbuttons * 4 bytes for iconCodepointOn) = 369
static constexpr size_t SERIALIZED_SIZE = 369;
// 369 + 36 (6 subbuttons * 6 bytes for icon colors) = 405
static constexpr size_t SERIALIZED_SIZE = 405;
void serialize(uint8_t* buf) const;
void deserialize(const uint8_t* buf);
@ -341,6 +343,20 @@ struct ScreenConfig {
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 {
uint8_t screenCount;
ScreenConfig screens[MAX_SCREENS];
@ -353,6 +369,10 @@ struct GuiConfig {
uint16_t knxDateTimeAddress;
uint16_t knxNightModeAddress;
// Screen transition animation
ScreenAnimType screenAnimType;
uint16_t screenAnimDuration; // Duration in ms (0 = default 300ms)
void clear();
ScreenConfig* findScreen(uint8_t id);
const ScreenConfig* findScreen(uint8_t id) const;

View File

@ -359,6 +359,23 @@ void WidgetManager::applyScreen(uint8_t screenId) {
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) {
ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
@ -375,35 +392,40 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
closeModalLocked();
}
lv_display_t* disp = lv_display_get_default();
if (disp) {
lv_display_enable_invalidation(disp, false);
}
// SAFE DESTRUCTION:
// SAFE DESTRUCTION of C++ widgets:
// 1. Mark all C++ widgets as "LVGL object already gone"
for (auto& widget : widgets_) {
if (widget) widget->clearLvglObject();
}
// 2. Delete all LVGL objects on layers we use
lv_obj_clean(lv_scr_act());
// 2. Clean layer_top (sub-buttons etc)
lv_obj_clean(lv_layer_top());
// 3. Now destroy C++ objects (their destructors won't call lv_obj_delete)
// 3. Destroy C++ objects (their destructors won't call lv_obj_delete)
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)...",
screen->name, screen->widgetCount);
lv_obj_t* root = lv_scr_act();
createAllWidgets(*screen, root);
createAllWidgets(*screen, newScreen);
ESP_LOGI(TAG, "Widgets created");
applyCachedValuesToWidgets();
if (disp) {
lv_display_enable_invalidation(disp, true);
}
lv_obj_invalidate(lv_scr_act());
// Get animation settings
ESP_LOGI(TAG, "Animation config: type=%d, duration=%u",
(int)config_->screenAnimType, config_->screenAnimDuration);
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);
}
@ -1503,6 +1525,11 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
cJSON_AddNumberToObject(knx, "dateTime", config_->knxDateTimeAddress);
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");
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, "target", sb.targetScreen);
char colorOnStr[8], colorOffStr[8];
snprintf(colorOnStr, sizeof(colorOnStr), "#%02X%02X%02X",
sb.colorOn.r, sb.colorOn.g, sb.colorOn.b);
snprintf(colorOffStr, sizeof(colorOffStr), "#%02X%02X%02X",
sb.colorOff.r, sb.colorOff.g, sb.colorOff.b);
cJSON_AddStringToObject(sbJson, "colorOn", colorOnStr);
cJSON_AddStringToObject(sbJson, "colorOff", colorOffStr);
char bgColorOnStr[8], bgColorOffStr[8], iconColorOnStr[8], iconColorOffStr[8];
snprintf(bgColorOnStr, sizeof(bgColorOnStr), "#%02X%02X%02X",
sb.bgColorOn.r, sb.bgColorOn.g, sb.bgColorOn.b);
snprintf(bgColorOffStr, sizeof(bgColorOffStr), "#%02X%02X%02X",
sb.bgColorOff.r, sb.bgColorOff.g, sb.bgColorOff.b);
snprintf(iconColorOnStr, sizeof(iconColorOnStr), "#%02X%02X%02X",
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);
}
@ -2199,14 +2232,42 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
sb.targetScreen = target->valueint;
}
cJSON* colorOn = cJSON_GetObjectItem(sbItem, "colorOn");
if (cJSON_IsString(colorOn)) {
sb.colorOn = Color::fromHex(parseHexColor(colorOn->valuestring));
// Background colors
cJSON* bgColorOn = cJSON_GetObjectItem(sbItem, "bgColorOn");
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");
if (cJSON_IsString(colorOff)) {
sb.colorOff = Color::fromHex(parseHexColor(colorOff->valuestring));
cJSON* bgColorOff = cJSON_GetObjectItem(sbItem, "bgColorOff");
if (cJSON_IsString(bgColorOff)) {
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++;
@ -2410,6 +2471,19 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
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) {
cJSON_Delete(root);
return false;

View File

@ -150,7 +150,10 @@ void RoomCardWidgetBase::applySubButtonStyle() {
for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
if (subButtonIcons_[i] && subBtnIconFont) {
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];
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]) {
uint32_t iconCodepoint = isOn && cfg.iconCodepointOn > 0 ? cfg.iconCodepointOn : cfg.iconCodepointOff;
if (iconCodepoint > 0) {
@ -172,6 +176,10 @@ void RoomCardWidgetBase::updateSubButtonColor(uint8_t index) {
encodeUtf8(iconCodepoint, 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);
}
}

View File

@ -2861,7 +2861,7 @@ CONFIG_LV_USE_BUILTIN_SPRINTF=y
#
# HAL Settings
#
CONFIG_LV_DEF_REFR_PERIOD=33
CONFIG_LV_DEF_REFR_PERIOD=16
CONFIG_LV_DPI_DEF=130
# end of HAL Settings
@ -2933,12 +2933,12 @@ CONFIG_LV_USE_DRAW_SW_ASM=0
#
CONFIG_LV_USE_LOG=y
# 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_ERROR is not set
CONFIG_LV_LOG_LEVEL_ERROR=y
# CONFIG_LV_LOG_LEVEL_USER 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_USE_TIMESTAMP=y
CONFIG_LV_LOG_USE_FILE_LINE=y

View File

@ -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">
</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="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">KNX Zeit</div>
<div class="flex items-center justify-between gap-2.5">

View File

@ -24,8 +24,8 @@
class="absolute rounded-full flex items-center justify-center shadow-md"
:style="getSubButtonStyle(sb, idx)"
>
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
{{ String.fromCodePoint(sb.icon) }}
<span v-if="getSubButtonIcon(sb)" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
{{ String.fromCodePoint(getSubButtonIcon(sb)) }}
</span>
</div>
</template>
@ -56,8 +56,8 @@
class="absolute rounded-full flex items-center justify-center shadow-md"
:style="getSubButtonStyleTile(sb, idx)"
>
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
{{ String.fromCodePoint(sb.icon) }}
<span v-if="getSubButtonIcon(sb)" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
{{ String.fromCodePoint(getSubButtonIcon(sb)) }}
</span>
</div>
</template>
@ -162,13 +162,15 @@ const getSubButtonStyleTile = (sb, idx) => {
const btnSize = (props.widget.subButtonSize || 40) * s;
const gap = 10 * s;
const padding = 12 * s;
// Use bgColorOff with fallback to old colorOff for compatibility
const bgColor = sb.bgColorOff || sb.colorOff || '#666666';
return {
width: btnSize + 'px',
height: btnSize + 'px',
right: padding + '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 x = centerX + orbitRadius * Math.cos(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 {
width: `${subBtnSize}px`,
@ -257,12 +260,19 @@ function getSubButtonIconStyle(sb) {
const s = props.scale;
const subBtnSize = (props.widget.subButtonSize || 40) * s;
const iconSize = subBtnSize * 0.5;
// Use iconColorOff with fallback for compatibility
const iconColor = sb.iconColorOff || '#ffffff';
return {
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 w = props.widget;
const s = props.scale;

View File

@ -212,12 +212,19 @@
<option v-for="s in store.config.screens" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
</div>
<div class="flex items-center gap-2 text-[11px] text-muted">
<label class="w-[50px]">Farben</label>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[50px]">Hintergr.</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.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>
<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>
@ -331,8 +338,10 @@ const subButtonCount = computed({
knxWrite: 0,
action: 0,
target: 0,
colorOn: '#FFCC00',
colorOff: '#666666'
bgColorOn: '#FFCC00',
bgColorOff: '#666666',
iconColorOn: '#FFFFFF',
iconColorOff: '#FFFFFF'
});
}
if (props.widget.subButtons.length > target) {

View File

@ -8,6 +8,7 @@ export const useEditorStore = defineStore('editor', () => {
startScreen: 0,
standby: { enabled: false, screen: -1, minutes: 5 },
knx: { time: 0, date: 0, dateTime: 0, night: 0 },
screenAnim: { type: 1, duration: 300 }, // 1 = Fade, 300ms
screens: []
});
@ -209,6 +210,12 @@ export const useEditorStore = defineStore('editor', () => {
if (config.knx.dateTime === undefined) config.knx.dateTime = 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();
// Recalculate IDs