Backup
This commit is contained in:
parent
ae8bb5a01f
commit
6a8b74a652
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -108,6 +108,8 @@ void WidgetConfig::serialize(uint8_t* buf) const {
|
|||||||
buf[pos++] = subButtonCount;
|
buf[pos++] = subButtonCount;
|
||||||
buf[pos++] = subButtonSize;
|
buf[pos++] = subButtonSize;
|
||||||
buf[pos++] = subButtonDistance;
|
buf[pos++] = subButtonDistance;
|
||||||
|
buf[pos++] = subButtonOpacity;
|
||||||
|
buf[pos++] = cardStyle;
|
||||||
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
const SubButtonConfig& sb = subButtons[i];
|
const SubButtonConfig& sb = subButtons[i];
|
||||||
buf[pos++] = sb.iconCodepoint & 0xFF;
|
buf[pos++] = sb.iconCodepoint & 0xFF;
|
||||||
@ -253,17 +255,22 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RoomCard sub-buttons
|
// RoomCard sub-buttons
|
||||||
if (pos + 3 <= SERIALIZED_SIZE) {
|
if (pos + 5 <= SERIALIZED_SIZE) {
|
||||||
subButtonCount = buf[pos++];
|
subButtonCount = buf[pos++];
|
||||||
if (subButtonCount > MAX_SUBBUTTONS) subButtonCount = MAX_SUBBUTTONS;
|
if (subButtonCount > MAX_SUBBUTTONS) subButtonCount = MAX_SUBBUTTONS;
|
||||||
subButtonSize = buf[pos++];
|
subButtonSize = buf[pos++];
|
||||||
if (subButtonSize == 0) subButtonSize = 40; // Default
|
if (subButtonSize == 0) subButtonSize = 40; // Default
|
||||||
subButtonDistance = buf[pos++];
|
subButtonDistance = buf[pos++];
|
||||||
if (subButtonDistance == 0) subButtonDistance = 80; // Default 80px
|
if (subButtonDistance == 0) subButtonDistance = 80; // Default 80px
|
||||||
|
subButtonOpacity = buf[pos++];
|
||||||
|
if (subButtonOpacity == 0) subButtonOpacity = 255; // Default fully opaque
|
||||||
|
cardStyle = buf[pos++]; // 0=Bubble, 1=Tile
|
||||||
} else {
|
} else {
|
||||||
subButtonCount = 0;
|
subButtonCount = 0;
|
||||||
subButtonSize = 40;
|
subButtonSize = 40;
|
||||||
subButtonDistance = 80;
|
subButtonDistance = 80;
|
||||||
|
subButtonOpacity = 255;
|
||||||
|
cardStyle = 0; // Default to Bubble style
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
|
|||||||
@ -256,11 +256,13 @@ struct WidgetConfig {
|
|||||||
uint8_t subButtonCount;
|
uint8_t subButtonCount;
|
||||||
uint8_t subButtonSize; // Sub-button size in pixels (default 40)
|
uint8_t subButtonSize; // Sub-button size in pixels (default 40)
|
||||||
uint8_t subButtonDistance; // Distance from center in pixels (default 80)
|
uint8_t subButtonDistance; // Distance from center in pixels (default 80)
|
||||||
|
uint8_t subButtonOpacity; // Sub-button opacity 0-255 (default 255)
|
||||||
|
uint8_t cardStyle; // 0=Bubble (round), 1=Tile (rectangular)
|
||||||
SubButtonConfig subButtons[MAX_SUBBUTTONS];
|
SubButtonConfig subButtons[MAX_SUBBUTTONS];
|
||||||
|
|
||||||
// Serialization size (fixed for NVS storage)
|
// Serialization size (fixed for NVS storage)
|
||||||
// 197 + 1 (subButtonCount) + 1 (subButtonSize) + 1 (subButtonDistance) + 120 (6 subButtons * 20) = 320
|
// 197 + 1 (subButtonCount) + 1 (subButtonSize) + 1 (subButtonDistance) + 1 (subButtonOpacity) + 1 (cardStyle) + 120 (6 subButtons * 20) = 322
|
||||||
static constexpr size_t SERIALIZED_SIZE = 320;
|
static constexpr size_t SERIALIZED_SIZE = 322;
|
||||||
|
|
||||||
void serialize(uint8_t* buf) const;
|
void serialize(uint8_t* buf) const;
|
||||||
void deserialize(const uint8_t* buf);
|
void deserialize(const uint8_t* buf);
|
||||||
|
|||||||
@ -1608,6 +1608,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
|||||||
if (w.type == WidgetType::ROOMCARD) {
|
if (w.type == WidgetType::ROOMCARD) {
|
||||||
cJSON_AddNumberToObject(widget, "subButtonSize", w.subButtonSize);
|
cJSON_AddNumberToObject(widget, "subButtonSize", w.subButtonSize);
|
||||||
cJSON_AddNumberToObject(widget, "subButtonDistance", w.subButtonDistance);
|
cJSON_AddNumberToObject(widget, "subButtonDistance", w.subButtonDistance);
|
||||||
|
cJSON_AddNumberToObject(widget, "subButtonOpacity", w.subButtonOpacity);
|
||||||
|
cJSON_AddNumberToObject(widget, "cardStyle", w.cardStyle);
|
||||||
}
|
}
|
||||||
if (w.type == WidgetType::ROOMCARD && w.subButtonCount > 0) {
|
if (w.type == WidgetType::ROOMCARD && w.subButtonCount > 0) {
|
||||||
cJSON* subButtons = cJSON_AddArrayToObject(widget, "subButtons");
|
cJSON* subButtons = cJSON_AddArrayToObject(widget, "subButtons");
|
||||||
@ -1956,6 +1958,18 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
|||||||
} else {
|
} else {
|
||||||
w.subButtonDistance = 80; // Default 80px
|
w.subButtonDistance = 80; // Default 80px
|
||||||
}
|
}
|
||||||
|
cJSON* subButtonOpacity = cJSON_GetObjectItem(widget, "subButtonOpacity");
|
||||||
|
if (cJSON_IsNumber(subButtonOpacity)) {
|
||||||
|
w.subButtonOpacity = subButtonOpacity->valueint;
|
||||||
|
} else {
|
||||||
|
w.subButtonOpacity = 255; // Default fully opaque
|
||||||
|
}
|
||||||
|
cJSON* cardStyle = cJSON_GetObjectItem(widget, "cardStyle");
|
||||||
|
if (cJSON_IsNumber(cardStyle)) {
|
||||||
|
w.cardStyle = cardStyle->valueint;
|
||||||
|
} else {
|
||||||
|
w.cardStyle = 0; // Default to Bubble style
|
||||||
|
}
|
||||||
|
|
||||||
// RoomCard sub-buttons
|
// RoomCard sub-buttons
|
||||||
cJSON* subButtons = cJSON_GetObjectItem(widget, "subButtons");
|
cJSON* subButtons = cJSON_GetObjectItem(widget, "subButtons");
|
||||||
|
|||||||
@ -14,11 +14,39 @@ RoomCardWidget::RoomCardWidget(const WidgetConfig& config)
|
|||||||
{
|
{
|
||||||
roomName_[0] = '\0';
|
roomName_[0] = '\0';
|
||||||
tempFormat_[0] = '\0';
|
tempFormat_[0] = '\0';
|
||||||
|
subtitle_[0] = '\0';
|
||||||
|
humidityFormat_[0] = '\0';
|
||||||
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
subButtonStates_[i] = false;
|
subButtonStates_[i] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoomCardWidget::~RoomCardWidget() {
|
||||||
|
// Sub-buttons are on screen parent, not on obj_, so delete them explicitly
|
||||||
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
|
if (subButtonObjs_[i] && lv_obj_is_valid(subButtonObjs_[i])) {
|
||||||
|
lv_obj_delete(subButtonObjs_[i]);
|
||||||
|
subButtonObjs_[i] = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomCardWidget::clearLvglObject() {
|
||||||
|
// Clear sub-button pointers (they'll be deleted when screen is cleaned)
|
||||||
|
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
|
||||||
|
subButtonObjs_[i] = nullptr;
|
||||||
|
subButtonIcons_[i] = nullptr;
|
||||||
|
}
|
||||||
|
bubble_ = nullptr;
|
||||||
|
roomIcon_ = nullptr;
|
||||||
|
roomLabel_ = nullptr;
|
||||||
|
tempLabel_ = nullptr;
|
||||||
|
subtitleLabel_ = nullptr;
|
||||||
|
humidityLabel_ = nullptr;
|
||||||
|
decorIcon_ = nullptr;
|
||||||
|
obj_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
int RoomCardWidget::encodeUtf8(uint32_t codepoint, char* buf) {
|
int RoomCardWidget::encodeUtf8(uint32_t codepoint, char* buf) {
|
||||||
if (codepoint < 0x80) {
|
if (codepoint < 0x80) {
|
||||||
buf[0] = static_cast<char>(codepoint);
|
buf[0] = static_cast<char>(codepoint);
|
||||||
@ -50,7 +78,11 @@ int RoomCardWidget::encodeUtf8(uint32_t codepoint, char* buf) {
|
|||||||
void RoomCardWidget::parseText() {
|
void RoomCardWidget::parseText() {
|
||||||
roomName_[0] = '\0';
|
roomName_[0] = '\0';
|
||||||
tempFormat_[0] = '\0';
|
tempFormat_[0] = '\0';
|
||||||
|
subtitle_[0] = '\0';
|
||||||
|
humidityFormat_[0] = '\0';
|
||||||
|
|
||||||
|
// Format for Bubble: "RoomName\nTempFormat"
|
||||||
|
// Format for Tile: "RoomName\nSubtitle" (temp/humidity via text2/text3)
|
||||||
const char* text = config_.text;
|
const char* text = config_.text;
|
||||||
if (text && text[0] != '\0') {
|
if (text && text[0] != '\0') {
|
||||||
const char* newline = strchr(text, '\n');
|
const char* newline = strchr(text, '\n');
|
||||||
@ -59,13 +91,33 @@ void RoomCardWidget::parseText() {
|
|||||||
if (nameLen >= MAX_TEXT_LEN) nameLen = MAX_TEXT_LEN - 1;
|
if (nameLen >= MAX_TEXT_LEN) nameLen = MAX_TEXT_LEN - 1;
|
||||||
memcpy(roomName_, text, nameLen);
|
memcpy(roomName_, text, nameLen);
|
||||||
roomName_[nameLen] = '\0';
|
roomName_[nameLen] = '\0';
|
||||||
strncpy(tempFormat_, newline + 1, MAX_TEXT_LEN - 1);
|
|
||||||
tempFormat_[MAX_TEXT_LEN - 1] = '\0';
|
if (config_.cardStyle == 0) {
|
||||||
|
// Bubble: second line is temp format
|
||||||
|
strncpy(tempFormat_, newline + 1, MAX_TEXT_LEN - 1);
|
||||||
|
tempFormat_[MAX_TEXT_LEN - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
// Tile: second line is subtitle
|
||||||
|
strncpy(subtitle_, newline + 1, MAX_TEXT_LEN - 1);
|
||||||
|
subtitle_[MAX_TEXT_LEN - 1] = '\0';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
strncpy(roomName_, text, MAX_TEXT_LEN - 1);
|
strncpy(roomName_, text, MAX_TEXT_LEN - 1);
|
||||||
roomName_[MAX_TEXT_LEN - 1] = '\0';
|
roomName_[MAX_TEXT_LEN - 1] = '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For Tile style, use text2 for temp format and text3 for humidity
|
||||||
|
if (config_.cardStyle == 1) {
|
||||||
|
if (config_.text2[0] != '\0') {
|
||||||
|
strncpy(tempFormat_, config_.text2, MAX_FORMAT_LEN - 1);
|
||||||
|
tempFormat_[MAX_FORMAT_LEN - 1] = '\0';
|
||||||
|
}
|
||||||
|
if (config_.text3[0] != '\0') {
|
||||||
|
strncpy(humidityFormat_, config_.text3, MAX_FORMAT_LEN - 1);
|
||||||
|
humidityFormat_[MAX_FORMAT_LEN - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomCardWidget::calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y) {
|
void RoomCardWidget::calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y) {
|
||||||
@ -86,26 +138,46 @@ void RoomCardWidget::calculateSubButtonPosition(SubButtonPosition pos, int16_t&
|
|||||||
y = centerY + static_cast<int16_t>(orbitRadius * sinf(angle)) - subBtnSize / 2;
|
y = centerY + static_cast<int16_t>(orbitRadius * sinf(angle)) - subBtnSize / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoomCardWidget::calculateSubButtonPositionTile(uint8_t index, int16_t& x, int16_t& y) {
|
||||||
|
// For Tile layout: buttons are stacked vertically on the right side
|
||||||
|
int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40;
|
||||||
|
int16_t gap = 10; // Gap between buttons
|
||||||
|
int16_t padding = 12; // Padding from edge
|
||||||
|
|
||||||
|
x = config_.width - subBtnSize - padding;
|
||||||
|
y = padding + index * (subBtnSize + gap);
|
||||||
|
}
|
||||||
|
|
||||||
lv_obj_t* RoomCardWidget::create(lv_obj_t* parent) {
|
lv_obj_t* RoomCardWidget::create(lv_obj_t* parent) {
|
||||||
parseText();
|
parseText();
|
||||||
|
|
||||||
|
// Store parent for sub-buttons (they're created on screen level to avoid clipping)
|
||||||
|
screenParent_ = parent;
|
||||||
|
|
||||||
// Create main container
|
// Create main container
|
||||||
obj_ = lv_obj_create(parent);
|
obj_ = lv_obj_create(parent);
|
||||||
lv_obj_remove_style_all(obj_);
|
lv_obj_remove_style_all(obj_);
|
||||||
lv_obj_set_pos(obj_, config_.x, config_.y);
|
lv_obj_set_pos(obj_, config_.x, config_.y);
|
||||||
lv_obj_set_size(obj_, config_.width, config_.height);
|
lv_obj_set_size(obj_, config_.width, config_.height);
|
||||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
// Create central bubble first
|
// Add click handler for navigation
|
||||||
createCentralBubble();
|
lv_obj_add_event_cb(obj_, bubbleClickCallback, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
// Create sub-buttons
|
// Create layout based on card style
|
||||||
createSubButtons();
|
if (config_.cardStyle == 1) {
|
||||||
|
createTileLayout();
|
||||||
|
createSubButtonsTile();
|
||||||
|
} else {
|
||||||
|
createBubbleLayout();
|
||||||
|
createSubButtons();
|
||||||
|
}
|
||||||
|
|
||||||
return obj_;
|
return obj_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomCardWidget::createCentralBubble() {
|
void RoomCardWidget::createBubbleLayout() {
|
||||||
// Calculate bubble size (80% of widget size, circular)
|
// Calculate bubble size (80% of widget size, circular)
|
||||||
int16_t minSide = config_.width < config_.height ? config_.width : config_.height;
|
int16_t minSide = config_.width < config_.height ? config_.width : config_.height;
|
||||||
int16_t bubbleSize = (minSide * 80) / 100;
|
int16_t bubbleSize = (minSide * 80) / 100;
|
||||||
@ -154,6 +226,94 @@ void RoomCardWidget::createCentralBubble() {
|
|||||||
lv_obj_add_event_cb(bubble_, bubbleClickCallback, LV_EVENT_CLICKED, this);
|
lv_obj_add_event_cb(bubble_, bubbleClickCallback, LV_EVENT_CLICKED, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoomCardWidget::createTileLayout() {
|
||||||
|
// Tile style: rectangular card with rounded corners
|
||||||
|
lv_obj_set_style_radius(obj_, config_.borderRadius > 0 ? config_.borderRadius : 16, 0);
|
||||||
|
lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0);
|
||||||
|
lv_obj_set_style_bg_color(obj_, lv_color_hex(config_.bgColor.toLvColor()), 0);
|
||||||
|
lv_obj_set_style_clip_corner(obj_, true, 0);
|
||||||
|
|
||||||
|
int16_t padding = 16;
|
||||||
|
|
||||||
|
// Room name (top-left, large)
|
||||||
|
roomLabel_ = lv_label_create(obj_);
|
||||||
|
lv_obj_clear_flag(roomLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_label_set_text(roomLabel_, roomName_);
|
||||||
|
lv_obj_set_pos(roomLabel_, padding, padding);
|
||||||
|
|
||||||
|
// Subtitle (below room name, smaller)
|
||||||
|
if (subtitle_[0] != '\0') {
|
||||||
|
subtitleLabel_ = lv_label_create(obj_);
|
||||||
|
lv_obj_clear_flag(subtitleLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_label_set_text(subtitleLabel_, subtitle_);
|
||||||
|
lv_obj_set_pos(subtitleLabel_, padding, padding + 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature row (with thermometer icon)
|
||||||
|
if (tempFormat_[0] != '\0') {
|
||||||
|
// Container for temp icon + value
|
||||||
|
lv_obj_t* tempRow = lv_obj_create(obj_);
|
||||||
|
lv_obj_remove_style_all(tempRow);
|
||||||
|
lv_obj_set_size(tempRow, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_pos(tempRow, padding, padding + 56);
|
||||||
|
lv_obj_set_flex_flow(tempRow, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(tempRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_gap(tempRow, 4, 0);
|
||||||
|
lv_obj_clear_flag(tempRow, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_clear_flag(tempRow, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
|
// Thermometer icon (U+E1FF or similar)
|
||||||
|
if (Fonts::hasIconFont()) {
|
||||||
|
lv_obj_t* tempIcon = lv_label_create(tempRow);
|
||||||
|
lv_obj_clear_flag(tempIcon, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
char iconText[5];
|
||||||
|
encodeUtf8(0xf076, iconText); // thermometer icon
|
||||||
|
lv_label_set_text(tempIcon, iconText);
|
||||||
|
}
|
||||||
|
|
||||||
|
tempLabel_ = lv_label_create(tempRow);
|
||||||
|
lv_obj_clear_flag(tempLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_label_set_text(tempLabel_, "--");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Humidity row (with water drop icon)
|
||||||
|
if (humidityFormat_[0] != '\0') {
|
||||||
|
lv_obj_t* humRow = lv_obj_create(obj_);
|
||||||
|
lv_obj_remove_style_all(humRow);
|
||||||
|
lv_obj_set_size(humRow, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_pos(humRow, padding + 80, padding + 56); // Next to temp
|
||||||
|
lv_obj_set_flex_flow(humRow, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(humRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_gap(humRow, 4, 0);
|
||||||
|
lv_obj_clear_flag(humRow, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_clear_flag(humRow, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
|
// Water drop icon
|
||||||
|
if (Fonts::hasIconFont()) {
|
||||||
|
lv_obj_t* humIcon = lv_label_create(humRow);
|
||||||
|
lv_obj_clear_flag(humIcon, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
char iconText[5];
|
||||||
|
encodeUtf8(0xe798, iconText); // humidity/water icon
|
||||||
|
lv_label_set_text(humIcon, iconText);
|
||||||
|
}
|
||||||
|
|
||||||
|
humidityLabel_ = lv_label_create(humRow);
|
||||||
|
lv_obj_clear_flag(humidityLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_label_set_text(humidityLabel_, "--");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large decorative icon (bottom-left, partially visible)
|
||||||
|
if (config_.iconCodepoint > 0 && Fonts::hasIconFont()) {
|
||||||
|
decorIcon_ = lv_label_create(obj_);
|
||||||
|
lv_obj_clear_flag(decorIcon_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
char iconText[5];
|
||||||
|
encodeUtf8(config_.iconCodepoint, iconText);
|
||||||
|
lv_label_set_text(decorIcon_, iconText);
|
||||||
|
// Position at bottom-left, slightly outside
|
||||||
|
lv_obj_set_pos(decorIcon_, -20, config_.height - 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RoomCardWidget::createSubButtons() {
|
void RoomCardWidget::createSubButtons() {
|
||||||
// Sub-button size (configurable, default 40)
|
// Sub-button size (configurable, default 40)
|
||||||
int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40;
|
int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40;
|
||||||
@ -162,8 +322,8 @@ void RoomCardWidget::createSubButtons() {
|
|||||||
const SubButtonConfig& cfg = config_.subButtons[i];
|
const SubButtonConfig& cfg = config_.subButtons[i];
|
||||||
if (!cfg.enabled) continue;
|
if (!cfg.enabled) continue;
|
||||||
|
|
||||||
// Create sub-button object
|
// Create sub-button on screen parent (not on widget container) to avoid clipping
|
||||||
lv_obj_t* btn = lv_obj_create(obj_);
|
lv_obj_t* btn = lv_obj_create(screenParent_);
|
||||||
lv_obj_remove_style_all(btn);
|
lv_obj_remove_style_all(btn);
|
||||||
lv_obj_set_size(btn, subBtnSize, subBtnSize);
|
lv_obj_set_size(btn, subBtnSize, subBtnSize);
|
||||||
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
@ -171,12 +331,64 @@ void RoomCardWidget::createSubButtons() {
|
|||||||
|
|
||||||
// Circular shape
|
// Circular shape
|
||||||
lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0);
|
lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0);
|
||||||
lv_obj_set_style_bg_opa(btn, 255, 0);
|
lv_obj_set_style_bg_opa(btn, config_.subButtonOpacity, 0);
|
||||||
|
|
||||||
// Position using circle geometry
|
// Position using circle geometry (relative to widget center)
|
||||||
int16_t x, y;
|
int16_t relX, relY;
|
||||||
calculateSubButtonPosition(cfg.position, x, y);
|
calculateSubButtonPosition(cfg.position, relX, relY);
|
||||||
lv_obj_set_pos(btn, x, y);
|
// Convert to absolute screen position
|
||||||
|
int16_t absX = config_.x + relX;
|
||||||
|
int16_t absY = config_.y + relY;
|
||||||
|
lv_obj_set_pos(btn, absX, absY);
|
||||||
|
|
||||||
|
// Create icon
|
||||||
|
if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) {
|
||||||
|
lv_obj_t* icon = lv_label_create(btn);
|
||||||
|
lv_obj_clear_flag(icon, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
char iconText[5];
|
||||||
|
encodeUtf8(cfg.iconCodepoint, iconText);
|
||||||
|
lv_label_set_text(icon, iconText);
|
||||||
|
lv_obj_center(icon);
|
||||||
|
subButtonIcons_[i] = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store index in user_data for click handler
|
||||||
|
lv_obj_set_user_data(btn, reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
|
||||||
|
lv_obj_add_event_cb(btn, subButtonClickCallback, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
|
subButtonObjs_[i] = btn;
|
||||||
|
subButtonStates_[i] = false;
|
||||||
|
|
||||||
|
// Apply initial color (OFF state)
|
||||||
|
updateSubButtonColor(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoomCardWidget::createSubButtonsTile() {
|
||||||
|
// For Tile layout: buttons are positioned on the right side, vertically stacked
|
||||||
|
int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
|
||||||
|
const SubButtonConfig& cfg = config_.subButtons[i];
|
||||||
|
if (!cfg.enabled) continue;
|
||||||
|
|
||||||
|
// Create sub-button on screen parent
|
||||||
|
lv_obj_t* btn = lv_obj_create(screenParent_);
|
||||||
|
lv_obj_remove_style_all(btn);
|
||||||
|
lv_obj_set_size(btn, subBtnSize, subBtnSize);
|
||||||
|
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
|
// Circular shape
|
||||||
|
lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0);
|
||||||
|
lv_obj_set_style_bg_opa(btn, config_.subButtonOpacity, 0);
|
||||||
|
|
||||||
|
// Position on right side
|
||||||
|
int16_t relX, relY;
|
||||||
|
calculateSubButtonPositionTile(i, relX, relY);
|
||||||
|
int16_t absX = config_.x + relX;
|
||||||
|
int16_t absY = config_.y + relY;
|
||||||
|
lv_obj_set_pos(btn, absX, absY);
|
||||||
|
|
||||||
// Create icon
|
// Create icon
|
||||||
if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) {
|
if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) {
|
||||||
@ -204,42 +416,109 @@ void RoomCardWidget::createSubButtons() {
|
|||||||
void RoomCardWidget::applyStyle() {
|
void RoomCardWidget::applyStyle() {
|
||||||
if (!obj_) return;
|
if (!obj_) return;
|
||||||
|
|
||||||
// Apply shadow to bubble if enabled
|
|
||||||
if (bubble_ && config_.shadow.enabled) {
|
|
||||||
lv_obj_set_style_shadow_width(bubble_, config_.shadow.blur, 0);
|
|
||||||
lv_obj_set_style_shadow_ofs_x(bubble_, config_.shadow.offsetX, 0);
|
|
||||||
lv_obj_set_style_shadow_ofs_y(bubble_, config_.shadow.offsetY, 0);
|
|
||||||
lv_obj_set_style_shadow_color(bubble_, lv_color_hex(config_.shadow.color.toLvColor()), 0);
|
|
||||||
lv_obj_set_style_shadow_opa(bubble_, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font sizes
|
|
||||||
const lv_font_t* iconFont = Fonts::iconFont(config_.iconSize);
|
|
||||||
const lv_font_t* textFont = Fonts::bySizeIndex(config_.fontSize);
|
|
||||||
const lv_font_t* labelFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0);
|
|
||||||
lv_color_t textColor = lv_color_hex(config_.textColor.toLvColor());
|
lv_color_t textColor = lv_color_hex(config_.textColor.toLvColor());
|
||||||
|
|
||||||
// Style room icon
|
if (config_.cardStyle == 1) {
|
||||||
if (roomIcon_ && iconFont) {
|
// Tile style
|
||||||
lv_obj_set_style_text_font(roomIcon_, iconFont, 0);
|
// Apply border if shadow is enabled (used as accent border)
|
||||||
lv_obj_set_style_text_color(roomIcon_, textColor, 0);
|
if (config_.shadow.enabled) {
|
||||||
}
|
lv_obj_set_style_border_width(obj_, 3, 0);
|
||||||
|
lv_obj_set_style_border_color(obj_, lv_color_hex(config_.shadow.color.toLvColor()), 0);
|
||||||
|
lv_obj_set_style_border_opa(obj_, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Style temperature
|
// Room name - large font
|
||||||
if (tempLabel_ && textFont) {
|
if (roomLabel_) {
|
||||||
lv_obj_set_style_text_font(tempLabel_, textFont, 0);
|
const lv_font_t* nameFont = Fonts::bySizeIndex(config_.fontSize);
|
||||||
lv_obj_set_style_text_color(tempLabel_, textColor, 0);
|
lv_obj_set_style_text_font(roomLabel_, nameFont, 0);
|
||||||
}
|
lv_obj_set_style_text_color(roomLabel_, textColor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Style room label
|
// Subtitle - smaller, dimmed
|
||||||
if (roomLabel_ && labelFont) {
|
if (subtitleLabel_) {
|
||||||
lv_obj_set_style_text_font(roomLabel_, labelFont, 0);
|
const lv_font_t* subFont = Fonts::bySizeIndex(config_.fontSize > 1 ? config_.fontSize - 2 : 0);
|
||||||
lv_obj_set_style_text_color(roomLabel_, lv_color_hex(config_.textColor.toLvColor()), 0);
|
lv_obj_set_style_text_font(subtitleLabel_, subFont, 0);
|
||||||
lv_obj_set_style_text_opa(roomLabel_, 180, 0); // Slightly dimmed
|
lv_obj_set_style_text_color(subtitleLabel_, textColor, 0);
|
||||||
|
lv_obj_set_style_text_opa(subtitleLabel_, 150, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temp and humidity - medium font
|
||||||
|
const lv_font_t* valueFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0);
|
||||||
|
const lv_font_t* iconFont = Fonts::iconFont(1); // Small icons
|
||||||
|
|
||||||
|
if (tempLabel_) {
|
||||||
|
lv_obj_set_style_text_font(tempLabel_, valueFont, 0);
|
||||||
|
lv_obj_set_style_text_color(tempLabel_, textColor, 0);
|
||||||
|
// Style the icon in the same row
|
||||||
|
lv_obj_t* parent = lv_obj_get_parent(tempLabel_);
|
||||||
|
if (parent && lv_obj_get_child_count(parent) > 0) {
|
||||||
|
lv_obj_t* iconLabel = lv_obj_get_child(parent, 0);
|
||||||
|
if (iconLabel && iconFont) {
|
||||||
|
lv_obj_set_style_text_font(iconLabel, iconFont, 0);
|
||||||
|
lv_obj_set_style_text_color(iconLabel, textColor, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (humidityLabel_) {
|
||||||
|
lv_obj_set_style_text_font(humidityLabel_, valueFont, 0);
|
||||||
|
lv_obj_set_style_text_color(humidityLabel_, textColor, 0);
|
||||||
|
lv_obj_t* parent = lv_obj_get_parent(humidityLabel_);
|
||||||
|
if (parent && lv_obj_get_child_count(parent) > 0) {
|
||||||
|
lv_obj_t* iconLabel = lv_obj_get_child(parent, 0);
|
||||||
|
if (iconLabel && iconFont) {
|
||||||
|
lv_obj_set_style_text_font(iconLabel, iconFont, 0);
|
||||||
|
lv_obj_set_style_text_color(iconLabel, textColor, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large decorative icon
|
||||||
|
if (decorIcon_) {
|
||||||
|
const lv_font_t* bigIconFont = Fonts::iconFont(5); // Largest icon
|
||||||
|
if (bigIconFont) {
|
||||||
|
lv_obj_set_style_text_font(decorIcon_, bigIconFont, 0);
|
||||||
|
lv_obj_set_style_text_color(decorIcon_, textColor, 0);
|
||||||
|
lv_obj_set_style_text_opa(decorIcon_, 60, 0); // Very transparent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bubble style
|
||||||
|
// Apply shadow to bubble if enabled
|
||||||
|
if (bubble_ && config_.shadow.enabled) {
|
||||||
|
lv_obj_set_style_shadow_width(bubble_, config_.shadow.blur, 0);
|
||||||
|
lv_obj_set_style_shadow_ofs_x(bubble_, config_.shadow.offsetX, 0);
|
||||||
|
lv_obj_set_style_shadow_ofs_y(bubble_, config_.shadow.offsetY, 0);
|
||||||
|
lv_obj_set_style_shadow_color(bubble_, lv_color_hex(config_.shadow.color.toLvColor()), 0);
|
||||||
|
lv_obj_set_style_shadow_opa(bubble_, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font sizes
|
||||||
|
const lv_font_t* iconFont = Fonts::iconFont(config_.iconSize);
|
||||||
|
const lv_font_t* textFont = Fonts::bySizeIndex(config_.fontSize);
|
||||||
|
const lv_font_t* labelFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0);
|
||||||
|
|
||||||
|
// Style room icon
|
||||||
|
if (roomIcon_ && iconFont) {
|
||||||
|
lv_obj_set_style_text_font(roomIcon_, iconFont, 0);
|
||||||
|
lv_obj_set_style_text_color(roomIcon_, textColor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style temperature
|
||||||
|
if (tempLabel_ && textFont) {
|
||||||
|
lv_obj_set_style_text_font(tempLabel_, textFont, 0);
|
||||||
|
lv_obj_set_style_text_color(tempLabel_, textColor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style room label
|
||||||
|
if (roomLabel_ && labelFont) {
|
||||||
|
lv_obj_set_style_text_font(roomLabel_, labelFont, 0);
|
||||||
|
lv_obj_set_style_text_color(roomLabel_, textColor, 0);
|
||||||
|
lv_obj_set_style_text_opa(roomLabel_, 180, 0); // Slightly dimmed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style sub-buttons - adjust icon size based on button size
|
// Style sub-buttons - adjust icon size based on button size
|
||||||
// subButtonSize: 30-40 -> font 1, 41-55 -> font 2, 56+ -> font 3
|
|
||||||
uint8_t subBtnFontIdx = 1; // Default small
|
uint8_t subBtnFontIdx = 1; // Default small
|
||||||
if (config_.subButtonSize > 55) {
|
if (config_.subButtonSize > 55) {
|
||||||
subBtnFontIdx = 3;
|
subBtnFontIdx = 3;
|
||||||
@ -272,6 +551,14 @@ void RoomCardWidget::updateTemperature(float value) {
|
|||||||
lv_label_set_text(tempLabel_, buf);
|
lv_label_set_text(tempLabel_, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RoomCardWidget::updateHumidity(float value) {
|
||||||
|
if (!humidityLabel_ || humidityFormat_[0] == '\0') return;
|
||||||
|
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), humidityFormat_, value);
|
||||||
|
lv_label_set_text(humidityLabel_, buf);
|
||||||
|
}
|
||||||
|
|
||||||
void RoomCardWidget::onKnxValue(float value) {
|
void RoomCardWidget::onKnxValue(float value) {
|
||||||
cachedPrimaryValue_ = value;
|
cachedPrimaryValue_ = value;
|
||||||
hasCachedPrimary_ = true;
|
hasCachedPrimary_ = true;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
class RoomCardWidget : public Widget {
|
class RoomCardWidget : public Widget {
|
||||||
public:
|
public:
|
||||||
explicit RoomCardWidget(const WidgetConfig& config);
|
explicit RoomCardWidget(const WidgetConfig& config);
|
||||||
|
~RoomCardWidget() override;
|
||||||
lv_obj_t* create(lv_obj_t* parent) override;
|
lv_obj_t* create(lv_obj_t* parent) override;
|
||||||
void applyStyle() override;
|
void applyStyle() override;
|
||||||
void onKnxValue(float value) override; // Temperature update
|
void onKnxValue(float value) override; // Temperature update
|
||||||
@ -14,13 +15,24 @@ public:
|
|||||||
void onSubButtonStatus(uint8_t index, bool value);
|
void onSubButtonStatus(uint8_t index, bool value);
|
||||||
void sendSubButtonToggle(uint8_t index);
|
void sendSubButtonToggle(uint8_t index);
|
||||||
|
|
||||||
|
// Override to also clear sub-buttons
|
||||||
|
void clearLvglObject();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Central bubble elements
|
// Parent (screen) for sub-buttons to avoid clipping
|
||||||
|
lv_obj_t* screenParent_ = nullptr;
|
||||||
|
|
||||||
|
// Bubble style elements (cardStyle=0)
|
||||||
lv_obj_t* bubble_ = nullptr;
|
lv_obj_t* bubble_ = nullptr;
|
||||||
lv_obj_t* roomIcon_ = nullptr;
|
lv_obj_t* roomIcon_ = nullptr;
|
||||||
lv_obj_t* roomLabel_ = nullptr;
|
lv_obj_t* roomLabel_ = nullptr;
|
||||||
lv_obj_t* tempLabel_ = nullptr;
|
lv_obj_t* tempLabel_ = nullptr;
|
||||||
|
|
||||||
|
// Tile style elements (cardStyle=1)
|
||||||
|
lv_obj_t* subtitleLabel_ = nullptr;
|
||||||
|
lv_obj_t* humidityLabel_ = nullptr;
|
||||||
|
lv_obj_t* decorIcon_ = nullptr; // Large decorative icon bottom-left
|
||||||
|
|
||||||
// Sub-button elements
|
// Sub-button elements
|
||||||
lv_obj_t* subButtonObjs_[MAX_SUBBUTTONS] = {};
|
lv_obj_t* subButtonObjs_[MAX_SUBBUTTONS] = {};
|
||||||
lv_obj_t* subButtonIcons_[MAX_SUBBUTTONS] = {};
|
lv_obj_t* subButtonIcons_[MAX_SUBBUTTONS] = {};
|
||||||
@ -29,16 +41,22 @@ private:
|
|||||||
// Cached config values
|
// Cached config values
|
||||||
char roomName_[MAX_TEXT_LEN] = {0};
|
char roomName_[MAX_TEXT_LEN] = {0};
|
||||||
char tempFormat_[MAX_TEXT_LEN] = {0};
|
char tempFormat_[MAX_TEXT_LEN] = {0};
|
||||||
|
char subtitle_[MAX_TEXT_LEN] = {0};
|
||||||
|
char humidityFormat_[MAX_FORMAT_LEN] = {0};
|
||||||
|
|
||||||
// Layout helpers
|
// Layout helpers
|
||||||
void parseText();
|
void parseText();
|
||||||
void createCentralBubble();
|
void createBubbleLayout(); // Bubble style (round)
|
||||||
|
void createTileLayout(); // Tile style (rectangular)
|
||||||
void createSubButtons();
|
void createSubButtons();
|
||||||
|
void createSubButtonsTile(); // SubButtons for tile (right side)
|
||||||
void updateSubButtonColor(uint8_t index);
|
void updateSubButtonColor(uint8_t index);
|
||||||
void updateTemperature(float value);
|
void updateTemperature(float value);
|
||||||
|
void updateHumidity(float value);
|
||||||
|
|
||||||
// Geometry calculations
|
// Geometry calculations
|
||||||
void calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y);
|
void calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y);
|
||||||
|
void calculateSubButtonPositionTile(uint8_t index, int16_t& x, int16_t& y);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
static void bubbleClickCallback(lv_event_t* e);
|
static void bubbleClickCallback(lv_event_t* e);
|
||||||
|
|||||||
@ -395,6 +395,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Card Style -->
|
||||||
|
<h4 :class="headingClass">Karten-Stil</h4>
|
||||||
|
<div :class="rowClass">
|
||||||
|
<label :class="labelClass">Layout</label>
|
||||||
|
<select :class="inputClass" v-model.number="w.cardStyle">
|
||||||
|
<option :value="0">Bubble (rund)</option>
|
||||||
|
<option :value="1">Tile (rechteckig)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sub-Buttons -->
|
<!-- Sub-Buttons -->
|
||||||
<h4 :class="headingClass">Sub-Buttons</h4>
|
<h4 :class="headingClass">Sub-Buttons</h4>
|
||||||
<div :class="rowClass">
|
<div :class="rowClass">
|
||||||
@ -419,6 +429,10 @@
|
|||||||
<input :class="inputClass" type="number" min="40" max="200" v-model.number="w.subButtonDistance">
|
<input :class="inputClass" type="number" min="40" max="200" v-model.number="w.subButtonDistance">
|
||||||
<span class="text-[10px] text-muted">px</span>
|
<span class="text-[10px] text-muted">px</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="subButtonCount > 0" :class="rowClass">
|
||||||
|
<label :class="labelClass">Btn Opacity</label>
|
||||||
|
<input :class="inputClass" type="number" min="0" max="255" v-model.number="w.subButtonOpacity">
|
||||||
|
</div>
|
||||||
<div v-for="(sb, idx) in subButtons" :key="idx" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
|
<div v-for="(sb, idx) in subButtons" :key="idx" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
|
||||||
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||||
<label class="w-[50px]">Btn {{ idx + 1 }}</label>
|
<label class="w-[50px]">Btn {{ idx + 1 }}</label>
|
||||||
@ -585,7 +599,7 @@
|
|||||||
<div :class="rowClass"><label :class="labelClass">Ringfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
<div :class="rowClass"><label :class="labelClass">Ringfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
||||||
<div :class="rowClass"><label :class="labelClass">Ring Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
<div :class="rowClass"><label :class="labelClass">Ring Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="key !== 'roomcard'">
|
||||||
<h4 :class="headingClass">Stil</h4>
|
<h4 :class="headingClass">Stil</h4>
|
||||||
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
||||||
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
||||||
|
|||||||
@ -138,8 +138,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- RoomCard Widget -->
|
<!-- RoomCard Widget - Bubble Style -->
|
||||||
<template v-else-if="isRoomCard">
|
<template v-else-if="isRoomCard && widget.cardStyle !== 1">
|
||||||
<!-- Central Bubble -->
|
<!-- Central Bubble -->
|
||||||
<div class="absolute rounded-full flex flex-col items-center justify-center" :style="roomCardBubbleStyle">
|
<div class="absolute rounded-full flex flex-col items-center justify-center" :style="roomCardBubbleStyle">
|
||||||
<span v-if="widget.iconCodepoint" class="material-symbols-outlined" :style="roomCardIconStyle">
|
<span v-if="widget.iconCodepoint" class="material-symbols-outlined" :style="roomCardIconStyle">
|
||||||
@ -148,7 +148,7 @@
|
|||||||
<span v-if="roomCardParts.name" class="leading-tight font-semibold" :style="roomCardNameStyle">{{ roomCardParts.name }}</span>
|
<span v-if="roomCardParts.name" class="leading-tight font-semibold" :style="roomCardNameStyle">{{ roomCardParts.name }}</span>
|
||||||
<span v-if="roomCardParts.format" class="leading-tight opacity-70" :style="roomCardTempStyle">{{ roomCardParts.format }}</span>
|
<span v-if="roomCardParts.format" class="leading-tight opacity-70" :style="roomCardTempStyle">{{ roomCardParts.format }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sub-Buttons -->
|
<!-- Sub-Buttons (circular orbit) -->
|
||||||
<div
|
<div
|
||||||
v-for="(sb, idx) in roomCardSubButtons"
|
v-for="(sb, idx) in roomCardSubButtons"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
@ -161,6 +161,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- RoomCard Widget - Tile Style -->
|
||||||
|
<template v-else-if="isRoomCard && widget.cardStyle === 1">
|
||||||
|
<div class="absolute inset-0 overflow-hidden" :style="roomCardTileContainerStyle">
|
||||||
|
<!-- Room name (top-left) -->
|
||||||
|
<div class="absolute" :style="{ left: '16px', top: '12px', color: widget.textColor, fontSize: fontSizes[widget.fontSize || 2] + 'px', fontWeight: 600 }">
|
||||||
|
{{ roomCardParts.name }}
|
||||||
|
</div>
|
||||||
|
<!-- Subtitle -->
|
||||||
|
<div v-if="roomCardParts.subtitle" class="absolute opacity-60" :style="{ left: '16px', top: '38px', color: widget.textColor, fontSize: fontSizes[Math.max(0, (widget.fontSize || 2) - 2)] + 'px' }">
|
||||||
|
{{ roomCardParts.subtitle }}
|
||||||
|
</div>
|
||||||
|
<!-- Temperature + Humidity row -->
|
||||||
|
<div class="absolute flex items-center gap-4" :style="{ left: '16px', top: '64px', color: widget.textColor, fontSize: fontSizes[Math.max(0, (widget.fontSize || 2) - 1)] + 'px' }">
|
||||||
|
<span v-if="widget.text2" class="flex items-center gap-1">
|
||||||
|
<span class="material-symbols-outlined text-[14px]">device_thermostat</span>
|
||||||
|
<span>{{ widget.text2 || '--' }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="widget.text3" class="flex items-center gap-1">
|
||||||
|
<span class="material-symbols-outlined text-[14px]">humidity_percentage</span>
|
||||||
|
<span>{{ widget.text3 || '--' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Large decorative icon (bottom-left) -->
|
||||||
|
<span v-if="widget.iconCodepoint" class="material-symbols-outlined absolute opacity-20" :style="{ left: '-20px', bottom: '-20px', fontSize: '120px', color: widget.textColor }">
|
||||||
|
{{ iconChar }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Sub-Buttons (right side, vertical) -->
|
||||||
|
<div
|
||||||
|
v-for="(sb, idx) in roomCardSubButtons"
|
||||||
|
:key="idx"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Icon-only Widget -->
|
<!-- Icon-only Widget -->
|
||||||
<template v-else-if="isIcon">
|
<template v-else-if="isIcon">
|
||||||
<span class="material-symbols-outlined flex items-center justify-center" :style="iconOnlyStyle">
|
<span class="material-symbols-outlined flex items-center justify-center" :style="iconOnlyStyle">
|
||||||
@ -481,16 +521,57 @@ const powerFlowBgStyle = computed(() => {
|
|||||||
|
|
||||||
// RoomCard computed properties
|
// RoomCard computed properties
|
||||||
const roomCardParts = computed(() => {
|
const roomCardParts = computed(() => {
|
||||||
if (!props.widget.text) return { name: '', format: '' };
|
if (!props.widget.text) return { name: '', format: '', subtitle: '' };
|
||||||
const parts = props.widget.text.split('\n');
|
const parts = props.widget.text.split('\n');
|
||||||
|
// For Tile style: second line is subtitle, temp/humidity come from text2/text3
|
||||||
|
// For Bubble style: second line is temp format
|
||||||
|
if (props.widget.cardStyle === 1) {
|
||||||
|
return {
|
||||||
|
name: parts[0] || '',
|
||||||
|
subtitle: parts[1] || '',
|
||||||
|
format: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: parts[0] || '',
|
name: parts[0] || '',
|
||||||
format: parts[1] || ''
|
format: parts[1] || '',
|
||||||
|
subtitle: ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const roomCardSubButtons = computed(() => props.widget.subButtons || []);
|
const roomCardSubButtons = computed(() => props.widget.subButtons || []);
|
||||||
|
|
||||||
|
// Tile style container
|
||||||
|
const roomCardTileContainerStyle = computed(() => {
|
||||||
|
const alpha = (props.widget.bgOpacity ?? 255) / 255;
|
||||||
|
const style = {
|
||||||
|
backgroundColor: hexToRgba(props.widget.bgColor || '#333333', alpha),
|
||||||
|
borderRadius: (props.widget.radius || 16) + 'px',
|
||||||
|
};
|
||||||
|
// Border from shadow settings
|
||||||
|
if (props.widget.shadow?.enabled) {
|
||||||
|
style.border = `3px solid ${props.widget.shadow.color || '#ff6b6b'}`;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sub-button positioning for Tile (right side, vertical)
|
||||||
|
const getSubButtonStyleTile = (sb, idx) => {
|
||||||
|
const s = props.scale;
|
||||||
|
const btnSize = (props.widget.subButtonSize || 40) * s;
|
||||||
|
const gap = 10 * s;
|
||||||
|
const padding = 12 * s;
|
||||||
|
const w = props.widget.w * s;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: btnSize + 'px',
|
||||||
|
height: btnSize + 'px',
|
||||||
|
right: padding + 'px',
|
||||||
|
top: (padding + idx * (btnSize + gap)) + 'px',
|
||||||
|
backgroundColor: sb.colorOff || '#666666',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const roomCardBubbleStyle = computed(() => {
|
const roomCardBubbleStyle = computed(() => {
|
||||||
const s = props.scale;
|
const s = props.scale;
|
||||||
const w = props.widget.w;
|
const w = props.widget.w;
|
||||||
@ -723,8 +804,19 @@ const computedStyle = computed(() => {
|
|||||||
style.left = '0';
|
style.left = '0';
|
||||||
style.top = '0';
|
style.top = '0';
|
||||||
} else if (isRoomCard.value) {
|
} else if (isRoomCard.value) {
|
||||||
// RoomCard container - transparent, children handle rendering
|
if (w.cardStyle === 1) {
|
||||||
style.overflow = 'visible';
|
// Tile style - rectangular with rounded corners
|
||||||
|
const alpha = clamp((w.bgOpacity ?? 255) / 255, 0, 1);
|
||||||
|
style.background = hexToRgba(w.bgColor || '#333333', alpha);
|
||||||
|
style.borderRadius = `${(w.radius || 16) * s}px`;
|
||||||
|
style.overflow = 'hidden';
|
||||||
|
if (w.shadow && w.shadow.enabled) {
|
||||||
|
style.border = `3px solid ${w.shadow.color || '#ff6b6b'}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bubble style - container transparent, children handle rendering
|
||||||
|
style.overflow = 'visible';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
|
|||||||
@ -454,6 +454,8 @@ export const WIDGET_DEFAULTS = {
|
|||||||
iconGap: 8,
|
iconGap: 8,
|
||||||
subButtonSize: 40, // Sub-button size in pixels
|
subButtonSize: 40, // Sub-button size in pixels
|
||||||
subButtonDistance: 80, // Distance from center in pixels
|
subButtonDistance: 80, // Distance from center in pixels
|
||||||
|
subButtonOpacity: 255, // Sub-button opacity (0-255)
|
||||||
|
cardStyle: 0, // 0=Bubble (round), 1=Tile (rectangular)
|
||||||
subButtons: []
|
subButtons: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -61,6 +61,14 @@ export function normalizeWidget(w, nextWidgetIdRef) {
|
|||||||
if (w.subButtonDistance === undefined || w.subButtonDistance === null) {
|
if (w.subButtonDistance === undefined || w.subButtonDistance === null) {
|
||||||
w.subButtonDistance = defaults.subButtonDistance || 80;
|
w.subButtonDistance = defaults.subButtonDistance || 80;
|
||||||
}
|
}
|
||||||
|
// Ensure subButtonOpacity has a default
|
||||||
|
if (w.subButtonOpacity === undefined || w.subButtonOpacity === null) {
|
||||||
|
w.subButtonOpacity = defaults.subButtonOpacity || 255;
|
||||||
|
}
|
||||||
|
// Ensure cardStyle has a default
|
||||||
|
if (w.cardStyle === undefined || w.cardStyle === null) {
|
||||||
|
w.cardStyle = defaults.cardStyle || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!w.shadow) {
|
if (!w.shadow) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user