This commit is contained in:
Thomas Peterson 2026-02-10 21:40:55 +01:00
parent 6c67516a4c
commit 4a2e2dcf63
14 changed files with 242 additions and 34 deletions

View File

@ -164,6 +164,9 @@ void WidgetConfig::serialize(uint8_t* buf) const {
buf[pos++] = arcValueColor.g; buf[pos++] = arcValueColor.g;
buf[pos++] = arcValueColor.b; buf[pos++] = arcValueColor.b;
buf[pos++] = arcValueFontSize; buf[pos++] = arcValueFontSize;
// Theme
buf[pos++] = themeFixed ? 1 : 0;
} }
void WidgetConfig::deserialize(const uint8_t* buf) { void WidgetConfig::deserialize(const uint8_t* buf) {
@ -376,6 +379,13 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
arcValueColor = textColor; arcValueColor = textColor;
arcValueFontSize = fontSize; arcValueFontSize = fontSize;
} }
// Theme
if (pos + 1 <= SERIALIZED_SIZE) {
themeFixed = buf[pos++] != 0;
} else {
themeFixed = false;
}
} }
WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) {
@ -408,6 +418,7 @@ WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const c
cfg.arcScaleColor = cfg.textColor; cfg.arcScaleColor = cfg.textColor;
cfg.arcValueColor = cfg.textColor; cfg.arcValueColor = cfg.textColor;
cfg.arcValueFontSize = cfg.fontSize; cfg.arcValueFontSize = cfg.fontSize;
cfg.themeFixed = false;
cfg.shadow.enabled = false; cfg.shadow.enabled = false;
// Icon defaults // Icon defaults
cfg.iconCodepoint = 0; cfg.iconCodepoint = 0;
@ -469,6 +480,7 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y,
cfg.arcScaleColor = cfg.textColor; cfg.arcScaleColor = cfg.textColor;
cfg.arcValueColor = cfg.textColor; cfg.arcValueColor = cfg.textColor;
cfg.arcValueFontSize = cfg.fontSize; cfg.arcValueFontSize = cfg.fontSize;
cfg.themeFixed = false;
cfg.shadow.enabled = true; cfg.shadow.enabled = true;
cfg.shadow.offsetX = 2; cfg.shadow.offsetX = 2;
cfg.shadow.offsetY = 2; cfg.shadow.offsetY = 2;
@ -507,6 +519,7 @@ void ScreenConfig::clear(uint8_t newId, const char* newName) {
id = newId; id = newId;
mode = ScreenMode::FULLSCREEN; mode = ScreenMode::FULLSCREEN;
backgroundColor = {26, 26, 46}; // Dark blue background backgroundColor = {26, 26, 46}; // Dark blue background
nightBackgroundColor = {14, 18, 23}; // Darker night background
bgImagePath[0] = '\0'; // No background image bgImagePath[0] = '\0'; // No background image
bgImageMode = BgImageMode::STRETCH; bgImageMode = BgImageMode::STRETCH;
widgetCount = 0; widgetCount = 0;

View File

@ -107,6 +107,49 @@ struct Color {
.b = static_cast<uint8_t>(hex & 0xFF) .b = static_cast<uint8_t>(hex & 0xFF)
}; };
} }
// Invert lightness via HSL: light colors become dark, dark become light
Color invertLightness() const {
float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f;
float maxC = rf > gf ? (rf > bf ? rf : bf) : (gf > bf ? gf : bf);
float minC = rf < gf ? (rf < bf ? rf : bf) : (gf < bf ? gf : bf);
float delta = maxC - minC;
float L = (maxC + minC) * 0.5f;
float S = 0.0f, H = 0.0f;
if (delta > 0.0001f) {
S = L < 0.5f ? delta / (maxC + minC) : delta / (2.0f - maxC - minC);
if (maxC == rf) H = (gf - bf) / delta + (gf < bf ? 6.0f : 0.0f);
else if (maxC == gf) H = (bf - rf) / delta + 2.0f;
else H = (rf - gf) / delta + 4.0f;
H /= 6.0f;
}
// Invert lightness
L = 1.0f - L;
// HSL to RGB
auto hue2rgb = [](float p, float q, float t) -> float {
if (t < 0.0f) t += 1.0f;
if (t > 1.0f) t -= 1.0f;
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
if (t < 0.5f) return q;
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
return p;
};
float ro, go, bo;
if (S < 0.0001f) {
ro = go = bo = L;
} else {
float q = L < 0.5f ? L * (1.0f + S) : L + S - L * S;
float p = 2.0f * L - q;
ro = hue2rgb(p, q, H + 1.0f / 3.0f);
go = hue2rgb(p, q, H);
bo = hue2rgb(p, q, H - 1.0f / 3.0f);
}
return {
.r = static_cast<uint8_t>(ro * 255.0f + 0.5f),
.g = static_cast<uint8_t>(go * 255.0f + 0.5f),
.b = static_cast<uint8_t>(bo * 255.0f + 0.5f)
};
}
}; };
// Condition operator for conditional styling // Condition operator for conditional styling
@ -298,9 +341,11 @@ struct WidgetConfig {
Color arcValueColor; // Center value text color Color arcValueColor; // Center value text color
uint8_t arcValueFontSize; // Center value font size index uint8_t arcValueFontSize; // Center value font size index
// Theme
bool themeFixed; // true = colors stay fixed in night mode
// Serialization size (fixed for NVS storage) // Serialization size (fixed for NVS storage)
// 369 + 36 (6 subbuttons * 6 bytes for icon colors) = 405 static constexpr size_t SERIALIZED_SIZE = 406;
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);
@ -323,6 +368,7 @@ struct ScreenConfig {
char name[MAX_SCREEN_NAME_LEN]; char name[MAX_SCREEN_NAME_LEN];
ScreenMode mode; ScreenMode mode;
Color backgroundColor; Color backgroundColor;
Color nightBackgroundColor; // Background color for night mode
char bgImagePath[MAX_BG_IMAGE_PATH_LEN]; // Background image path (e.g., "/images/bg.png") char bgImagePath[MAX_BG_IMAGE_PATH_LEN]; // Background image path (e.g., "/images/bg.png")
BgImageMode bgImageMode; // 0=none, 1=stretch, 2=center, 3=tile BgImageMode bgImageMode; // 0=none, 1=stretch, 2=center, 3=tile
uint8_t widgetCount; uint8_t widgetCount;

View File

@ -1137,6 +1137,22 @@ void WidgetManager::refreshChartWidgets() {
refreshChartWidgetsLocked(); refreshChartWidgetsLocked();
} }
void WidgetManager::applyNightMode(bool night) {
// Update screen background
const ScreenConfig* screen = activeScreen();
if (screen && screen_) {
Color bg = night ? screen->nightBackgroundColor : screen->backgroundColor;
lv_obj_set_style_bg_color(screen_, lv_color_make(bg.r, bg.g, bg.b), 0);
}
// Re-apply style to all widgets (they check nightMode_ internally)
for (auto& widget : widgets_) {
if (widget) {
widget->applyStyle();
}
}
}
void WidgetManager::updateSystemTimeWidgets() { void WidgetManager::updateSystemTimeWidgets() {
int64_t nowUs = esp_timer_get_time(); int64_t nowUs = esp_timer_get_time();
if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms
@ -1222,7 +1238,10 @@ void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) {
} }
if (config_->knxNightModeAddress != 0 && groupAddr == config_->knxNightModeAddress) { if (config_->knxNightModeAddress != 0 && groupAddr == config_->knxNightModeAddress) {
nightMode_ = value; if (nightMode_ != value) {
nightMode_ = value;
applyNightMode(value);
}
} }
} }
@ -1545,6 +1564,11 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b); screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b);
cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr); cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr);
char nightBgColorStr[8];
snprintf(nightBgColorStr, sizeof(nightBgColorStr), "#%02X%02X%02X",
screen.nightBackgroundColor.r, screen.nightBackgroundColor.g, screen.nightBackgroundColor.b);
cJSON_AddStringToObject(screenJson, "nightBgColor", nightBgColorStr);
// Background image // Background image
if (screen.bgImagePath[0] != '\0') { if (screen.bgImagePath[0] != '\0') {
cJSON_AddStringToObject(screenJson, "bgImage", screen.bgImagePath); cJSON_AddStringToObject(screenJson, "bgImage", screen.bgImagePath);
@ -1638,6 +1662,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
cJSON_AddStringToObject(widget, "arcValueColor", arcValueColorStr); cJSON_AddStringToObject(widget, "arcValueColor", arcValueColorStr);
cJSON_AddNumberToObject(widget, "arcValueFontSize", w.arcValueFontSize); cJSON_AddNumberToObject(widget, "arcValueFontSize", w.arcValueFontSize);
cJSON_AddBoolToObject(widget, "themeFixed", w.themeFixed);
cJSON_AddNumberToObject(widget, "parentId", w.parentId); cJSON_AddNumberToObject(widget, "parentId", w.parentId);
// Secondary KNX address (left value) // Secondary KNX address (left value)
@ -2004,6 +2030,9 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
cJSON* arcValueFontSize = cJSON_GetObjectItem(widget, "arcValueFontSize"); cJSON* arcValueFontSize = cJSON_GetObjectItem(widget, "arcValueFontSize");
if (cJSON_IsNumber(arcValueFontSize)) w.arcValueFontSize = arcValueFontSize->valueint; if (cJSON_IsNumber(arcValueFontSize)) w.arcValueFontSize = arcValueFontSize->valueint;
cJSON* themeFixed = cJSON_GetObjectItem(widget, "themeFixed");
if (cJSON_IsBool(themeFixed)) w.themeFixed = cJSON_IsTrue(themeFixed);
cJSON* parentId = cJSON_GetObjectItem(widget, "parentId"); cJSON* parentId = cJSON_GetObjectItem(widget, "parentId");
if (cJSON_IsNumber(parentId)) { if (cJSON_IsNumber(parentId)) {
w.parentId = static_cast<int8_t>(parentId->valueint); w.parentId = static_cast<int8_t>(parentId->valueint);
@ -2362,6 +2391,11 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring)); screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
} }
cJSON* nightBgColor = cJSON_GetObjectItem(screenJson, "nightBgColor");
if (cJSON_IsString(nightBgColor)) {
screen.nightBackgroundColor = Color::fromHex(parseHexColor(nightBgColor->valuestring));
}
// Parse background image // Parse background image
cJSON* bgImage = cJSON_GetObjectItem(screenJson, "bgImage"); cJSON* bgImage = cJSON_GetObjectItem(screenJson, "bgImage");
if (cJSON_IsString(bgImage)) { if (cJSON_IsString(bgImage)) {

View File

@ -66,6 +66,9 @@ public:
// KNX write (for RoomCard sub-buttons etc.) // KNX write (for RoomCard sub-buttons etc.)
void sendKnxSwitch(uint16_t groupAddr, bool value); void sendKnxSwitch(uint16_t groupAddr, bool value);
// Night mode
bool isNightMode() const { return nightMode_; }
// Direct config access // Direct config access
GuiConfig& getConfig() { return *config_; } GuiConfig& getConfig() { return *config_; }
const GuiConfig& getConfig() const { return *config_; } const GuiConfig& getConfig() const { return *config_; }
@ -148,6 +151,7 @@ private:
static bool isNumericTextSource(TextSource source); static bool isNumericTextSource(TextSource source);
void refreshChartWidgetsLocked(); void refreshChartWidgetsLocked();
void refreshChartWidgets(); void refreshChartWidgets();
void applyNightMode(bool night);
void createDefaultConfig(); void createDefaultConfig();
void applyScreen(uint8_t screenId); void applyScreen(uint8_t screenId);

View File

@ -82,11 +82,12 @@ void ArcWidget::applyStyle() {
if (arcWidth < 2) arcWidth = 2; if (arcWidth < 2) arcWidth = 2;
if (arcWidth > 48) arcWidth = 48; if (arcWidth > 48) arcWidth = 48;
lv_color_t trackColor = lv_color_make(config_.bgColor.r, config_.bgColor.g, config_.bgColor.b); Color trackCol = themeColor(config_.bgColor);
lv_color_t trackColor = lv_color_make(trackCol.r, trackCol.g, trackCol.b);
uint8_t trackOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast<uint8_t>(LV_OPA_40); uint8_t trackOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast<uint8_t>(LV_OPA_40);
lv_color_t indicatorColor = lv_color_make( Color indicatorCol = themeColor(config_.textColor);
config_.textColor.r, config_.textColor.g, config_.textColor.b); lv_color_t indicatorColor = lv_color_make(indicatorCol.r, indicatorCol.g, indicatorCol.b);
lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_MAIN); lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_MAIN);
lv_obj_set_style_arc_color(obj_, trackColor, LV_PART_MAIN); lv_obj_set_style_arc_color(obj_, trackColor, LV_PART_MAIN);
@ -135,8 +136,8 @@ void ArcWidget::applyStyle() {
lv_obj_set_style_arc_width(scale_, 1, LV_PART_MAIN); lv_obj_set_style_arc_width(scale_, 1, LV_PART_MAIN);
lv_obj_set_style_arc_opa(scale_, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_style_arc_opa(scale_, LV_OPA_TRANSP, LV_PART_MAIN);
lv_color_t scaleColor = lv_color_make( Color scaleCol = themeColor(config_.arcScaleColor);
config_.arcScaleColor.r, config_.arcScaleColor.g, config_.arcScaleColor.b); lv_color_t scaleColor = lv_color_make(scaleCol.r, scaleCol.g, scaleCol.b);
lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_INDICATOR); lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_INDICATOR);
lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_ITEMS); lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_ITEMS);
lv_obj_set_style_line_width(scale_, 2, LV_PART_INDICATOR); lv_obj_set_style_line_width(scale_, 2, LV_PART_INDICATOR);
@ -155,8 +156,8 @@ void ArcWidget::applyStyle() {
} }
if (valueLabel_) { if (valueLabel_) {
lv_color_t valueColor = lv_color_make( Color valueCol = themeColor(config_.arcValueColor);
config_.arcValueColor.r, config_.arcValueColor.g, config_.arcValueColor.b); lv_color_t valueColor = lv_color_make(valueCol.r, valueCol.g, valueCol.b);
lv_obj_set_style_text_color(valueLabel_, valueColor, 0); lv_obj_set_style_text_color(valueLabel_, valueColor, 0);
lv_obj_set_style_text_font(valueLabel_, getFontBySize(config_.arcValueFontSize), 0); lv_obj_set_style_text_font(valueLabel_, getFontBySize(config_.arcValueFontSize), 0);
lv_obj_center(valueLabel_); lv_obj_center(valueLabel_);

View File

@ -1,5 +1,6 @@
#include "Widget.hpp" #include "Widget.hpp"
#include "../Fonts.hpp" #include "../Fonts.hpp"
#include "../WidgetManager.hpp"
#include "lvgl.h" #include "lvgl.h"
Widget::Widget(const WidgetConfig& config) Widget::Widget(const WidgetConfig& config)
@ -161,20 +162,30 @@ void Widget::applyConditionStyle(const ConditionStyle& style) {
} }
} }
bool Widget::shouldTransformColors() const {
return WidgetManager::instance().isNightMode() && !config_.themeFixed;
}
Color Widget::themeColor(const Color& c) const {
return shouldTransformColors() ? c.invertLightness() : c;
}
void Widget::applyCommonStyle() { void Widget::applyCommonStyle() {
if (obj_ == nullptr) return; if (obj_ == nullptr) return;
Color textCol = themeColor(config_.textColor);
Color bgCol = themeColor(config_.bgColor);
Color borderCol = themeColor(config_.borderColor);
// Text color // Text color
lv_obj_set_style_text_color(obj_, lv_color_make( lv_obj_set_style_text_color(obj_, lv_color_make(textCol.r, textCol.g, textCol.b), 0);
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
// Font // Font
lv_obj_set_style_text_font(obj_, getFontBySize(config_.fontSize), 0); lv_obj_set_style_text_font(obj_, getFontBySize(config_.fontSize), 0);
// Background (for buttons and labels with bg) // Background (for buttons and labels with bg)
if (config_.bgOpacity > 0) { if (config_.bgOpacity > 0) {
lv_obj_set_style_bg_color(obj_, lv_color_make( lv_obj_set_style_bg_color(obj_, lv_color_make(bgCol.r, bgCol.g, bgCol.b), 0);
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0); lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0);
} }
@ -186,8 +197,7 @@ void Widget::applyCommonStyle() {
// Border // Border
if (config_.borderWidth > 0 && config_.borderOpacity > 0) { if (config_.borderWidth > 0 && config_.borderOpacity > 0) {
lv_obj_set_style_border_width(obj_, config_.borderWidth, 0); lv_obj_set_style_border_width(obj_, config_.borderWidth, 0);
lv_obj_set_style_border_color(obj_, lv_color_make( lv_obj_set_style_border_color(obj_, lv_color_make(borderCol.r, borderCol.g, borderCol.b), 0);
config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), 0);
lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0); lv_obj_set_style_border_opa(obj_, config_.borderOpacity, 0);
} else { } else {
lv_obj_set_style_border_width(obj_, 0, 0); lv_obj_set_style_border_width(obj_, 0, 0);
@ -213,8 +223,8 @@ void Widget::applyShadowStyle() {
if (spread < 0) spread = 0; if (spread < 0) spread = 0;
if (blur == 0) return; if (blur == 0) return;
lv_color_t shadowColor = lv_color_make( Color sc = themeColor(config_.shadow.color);
config_.shadow.color.r, config_.shadow.color.g, config_.shadow.color.b); lv_color_t shadowColor = lv_color_make(sc.r, sc.g, sc.b);
// Default state shadow // Default state shadow
lv_obj_set_style_shadow_color(obj_, shadowColor, 0); lv_obj_set_style_shadow_color(obj_, shadowColor, 0);

View File

@ -4,6 +4,8 @@
#include "lvgl.h" #include "lvgl.h"
#include <ctime> #include <ctime>
class WidgetManager;
class Widget { class Widget {
public: public:
explicit Widget(const WidgetConfig& config); explicit Widget(const WidgetConfig& config);
@ -70,6 +72,10 @@ protected:
void applyConditionStyle(const ConditionStyle& style); void applyConditionStyle(const ConditionStyle& style);
static const lv_font_t* getFontBySize(uint8_t sizeIndex); static const lv_font_t* getFontBySize(uint8_t sizeIndex);
// Night mode color transformation
bool shouldTransformColors() const;
Color themeColor(const Color& c) const;
const WidgetConfig& config_; const WidgetConfig& config_;
lv_obj_t* obj_ = nullptr; lv_obj_t* obj_ = nullptr;

View File

@ -8,6 +8,14 @@
<span class="text-[11px] text-muted">{{ store.config.screens.length }}</span> <span class="text-[11px] text-muted">{{ store.config.screens.length }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button
class="border px-2.5 py-1.5 rounded-[10px] text-[12px] font-semibold transition hover:-translate-y-0.5 active:translate-y-0.5"
:class="store.nightPreview ? 'bg-[#2f3b4a] text-white border-[#4a5568]' : 'border-border bg-panel-2 text-text hover:bg-[#e4ebf2]'"
@click="store.nightPreview = !store.nightPreview"
title="Nachtmodus-Vorschau"
>
<span class="material-symbols-outlined text-[16px] align-middle">{{ store.nightPreview ? 'dark_mode' : 'light_mode' }}</span>
</button>
<button class="border border-border bg-panel-2 text-text px-2.5 py-1.5 rounded-[10px] text-[12px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="emit('open-screen-settings')">Einstellungen</button> <button class="border border-border bg-panel-2 text-text px-2.5 py-1.5 rounded-[10px] text-[12px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="emit('open-screen-settings')">Einstellungen</button>
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="store.addScreen">+</button> <button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="store.addScreen">+</button>
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="screensOpen = !screensOpen"> <button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="screensOpen = !screensOpen">
@ -94,7 +102,9 @@ const canvasStyle = computed(() => {
const style = { const style = {
width: `${canvasW.value * store.canvasScale}px`, width: `${canvasW.value * store.canvasScale}px`,
height: `${canvasH.value * store.canvasScale}px`, height: `${canvasH.value * store.canvasScale}px`,
backgroundColor: store.activeScreen?.bgColor || '#1A1A2E' backgroundColor: store.nightPreview
? (store.activeScreen?.nightBgColor || '#0E1217')
: (store.activeScreen?.bgColor || '#1A1A2E')
}; };
// Add background image if set // Add background image if set

View File

@ -16,6 +16,10 @@
<label class="text-[12px] text-muted">Hintergrund</label> <label class="text-[12px] text-muted">Hintergrund</label>
<input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="screen.bgColor"> <input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="screen.bgColor">
</div> </div>
<div class="flex items-center justify-between gap-2.5">
<label class="text-[12px] text-muted">Nacht-Hintergrund</label>
<input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="screen.nightBgColor">
</div>
<div class="flex items-center justify-between gap-2.5"> <div class="flex items-center justify-between gap-2.5">
<label class="text-[12px] text-muted">Modus</label> <label class="text-[12px] text-muted">Modus</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="screen.mode"> <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="screen.mode">

View File

@ -13,6 +13,7 @@
<div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" v-model.number="w.w"></div> <div :class="rowClass"><label :class="labelClass">Breite</label><input :class="inputClass" type="number" v-model.number="w.w"></div>
<div :class="rowClass"><label :class="labelClass">Hoehe</label><input :class="inputClass" type="number" v-model.number="w.h"></div> <div :class="rowClass"><label :class="labelClass">Hoehe</label><input :class="inputClass" type="number" v-model.number="w.h"></div>
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div> <div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
<div :class="rowClass"><label :class="labelClass">Farbe fixieren</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.themeFixed"></div>
<!-- Widget-specific settings --> <!-- Widget-specific settings -->
<component <component

View File

@ -1,7 +1,7 @@
<template> <template>
<component <component
:is="widgetComponent" :is="widgetComponent"
:widget="widget" :widget="themedWidget"
:scale="scale" :scale="scale"
:selected="selected" :selected="selected"
@select="$emit('select')" @select="$emit('select')"
@ -12,6 +12,8 @@
<script setup> <script setup>
import { computed, markRaw } from 'vue'; import { computed, markRaw } from 'vue';
import { useEditorStore } from '../stores/editor';
import { invertLightness } from '../utils';
import { WIDGET_TYPES } from '../constants'; import { WIDGET_TYPES } from '../constants';
// Import all widget element components // Import all widget element components
@ -56,7 +58,26 @@ const componentMap = {
[WIDGET_TYPES.BUTTONMATRIX]: markRaw(ButtonMatrixElement) [WIDGET_TYPES.BUTTONMATRIX]: markRaw(ButtonMatrixElement)
}; };
const store = useEditorStore();
const widgetComponent = computed(() => { const widgetComponent = computed(() => {
return componentMap[props.widget.type] || LabelElement; return componentMap[props.widget.type] || LabelElement;
}); });
const colorKeys = ['textColor', 'bgColor', 'borderColor', 'arcScaleColor', 'arcValueColor'];
const themedWidget = computed(() => {
const w = props.widget;
if (!store.nightPreview || w.themeFixed) return w;
const copy = { ...w };
for (const key of colorKeys) {
if (copy[key] && typeof copy[key] === 'string' && copy[key].startsWith('#')) {
copy[key] = invertLightness(copy[key]);
}
}
if (copy.shadow && copy.shadow.enabled && copy.shadow.color) {
copy.shadow = { ...copy.shadow, color: invertLightness(copy.shadow.color) };
}
return copy;
});
</script> </script>

View File

@ -208,7 +208,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 8 iconGap: 8,
themeFixed: false
}, },
button: { button: {
w: 130, w: 130,
@ -232,6 +233,7 @@ export const WIDGET_DEFAULTS = {
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 8, iconGap: 8,
themeFixed: false,
conditions: [] conditions: []
}, },
led: { led: {
@ -254,7 +256,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 8 iconGap: 8,
themeFixed: false
}, },
icon: { icon: {
w: 48, w: 48,
@ -276,7 +279,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0xe88a, iconCodepoint: 0xe88a,
iconPosition: 0, iconPosition: 0,
iconSize: 3, iconSize: 3,
iconGap: 8 iconGap: 8,
themeFixed: false
}, },
tabview: { tabview: {
w: 400, w: 400,
@ -298,7 +302,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, // 0=Top, 1=Bottom, 2=Left, 3=Right iconPosition: 0, // 0=Top, 1=Bottom, 2=Left, 3=Right
iconSize: 5, // Used for Tab Height (x10) -> 50px iconSize: 5, // Used for Tab Height (x10) -> 50px
iconGap: 0 iconGap: 0,
themeFixed: false
}, },
tabpage: { tabpage: {
w: 0, // Ignored w: 0, // Ignored
@ -320,7 +325,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 0 iconGap: 0,
themeFixed: false
}, },
powerflow: { powerflow: {
w: 720, w: 720,
@ -342,7 +348,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 0 iconGap: 0,
themeFixed: false
}, },
powernode: { powernode: {
w: 120, w: 120,
@ -374,6 +381,7 @@ export const WIDGET_DEFAULTS = {
text3: '', text3: '',
knxAddr3: 0, knxAddr3: 0,
// Conditions // Conditions
themeFixed: false,
conditions: [] conditions: []
}, },
powerlink: { powerlink: {
@ -396,7 +404,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 0, iconSize: 0,
iconGap: 6 iconGap: 6,
themeFixed: false
}, },
chart: { chart: {
w: 360, w: 360,
@ -419,6 +428,7 @@ export const WIDGET_DEFAULTS = {
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 0, iconGap: 0,
themeFixed: false,
chart: { chart: {
period: 0, period: 0,
series: [ series: [
@ -446,7 +456,8 @@ export const WIDGET_DEFAULTS = {
iconCodepoint: 0, iconCodepoint: 0,
iconPosition: 0, iconPosition: 0,
iconSize: 1, iconSize: 1,
iconGap: 0 iconGap: 0,
themeFixed: false
}, },
roomcard: { roomcard: {
w: 200, w: 200,
@ -474,7 +485,8 @@ export const WIDGET_DEFAULTS = {
subButtonOpacity: 255, // Sub-button opacity (0-255) subButtonOpacity: 255, // Sub-button opacity (0-255)
cardStyle: 0, // 0=Bubble (round), 1=Tile (rectangular) cardStyle: 0, // 0=Bubble (round), 1=Tile (rectangular)
subButtons: [], subButtons: [],
textLines: [] // Variable text lines with icon, text, textSrc, knxAddr, fontSize textLines: [], // Variable text lines with icon, text, textSrc, knxAddr, fontSize
themeFixed: false
}, },
rectangle: { rectangle: {
w: 220, w: 220,
@ -501,7 +513,8 @@ export const WIDGET_DEFAULTS = {
iconSize: 1, iconSize: 1,
iconGap: 0, iconGap: 0,
iconPositionX: 0, iconPositionX: 0,
iconPositionY: 0 iconPositionY: 0,
themeFixed: false
}, },
arc: { arc: {
w: 180, w: 180,
@ -536,7 +549,8 @@ export const WIDGET_DEFAULTS = {
iconSize: 1, iconSize: 1,
iconGap: 0, iconGap: 0,
iconPositionX: 0, iconPositionX: 0,
iconPositionY: 0 iconPositionY: 0,
themeFixed: false
}, },
buttonmatrix: { buttonmatrix: {
w: 240, w: 240,
@ -563,6 +577,7 @@ export const WIDGET_DEFAULTS = {
iconSize: 1, iconSize: 1,
iconGap: 0, iconGap: 0,
iconPositionX: 0, iconPositionX: 0,
iconPositionY: 0 iconPositionY: 0,
themeFixed: false
} }
}; };

View File

@ -19,6 +19,7 @@ export const useEditorStore = defineStore('editor', () => {
const showGrid = ref(true); const showGrid = ref(true);
const snapToGrid = ref(true); const snapToGrid = ref(true);
const gridSize = ref(20); const gridSize = ref(20);
const nightPreview = ref(false);
const powerLinkMode = reactive({ active: false, powerflowId: null, fromNodeId: null }); const powerLinkMode = reactive({ active: false, powerflowId: null, fromNodeId: null });
const nextScreenId = ref(0); const nextScreenId = ref(0);
@ -273,6 +274,7 @@ export const useEditorStore = defineStore('editor', () => {
name: `Screen ${id}`, name: `Screen ${id}`,
mode: 0, mode: 0,
bgColor: '#1A1A2E', bgColor: '#1A1A2E',
nightBgColor: '#0E1217',
bgImage: '', bgImage: '',
bgImageMode: 1, bgImageMode: 1,
widgets: [] widgets: []
@ -406,7 +408,8 @@ export const useEditorStore = defineStore('editor', () => {
iconSize: defaults.iconSize || 1, iconSize: defaults.iconSize || 1,
iconGap: defaults.iconGap || 8, iconGap: defaults.iconGap || 8,
iconPositionX: defaults.iconPositionX || 8, iconPositionX: defaults.iconPositionX || 8,
iconPositionY: defaults.iconPositionY || 8 iconPositionY: defaults.iconPositionY || 8,
themeFixed: defaults.themeFixed ?? false
}; };
if (defaults.conditions !== undefined) { if (defaults.conditions !== undefined) {
@ -656,6 +659,7 @@ export const useEditorStore = defineStore('editor', () => {
knxAddresses, knxAddresses,
selectedWidgetId, selectedWidgetId,
activeScreenId, activeScreenId,
nightPreview,
canvasScale, canvasScale,
showGrid, showGrid,
snapToGrid, snapToGrid,

View File

@ -4,6 +4,44 @@ export function typeKeyFor(type) {
return TYPE_KEYS[type] || 'label'; return TYPE_KEYS[type] || 'label';
} }
export function invertLightness(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const maxC = Math.max(r, g, b);
const minC = Math.min(r, g, b);
const delta = maxC - minC;
let L = (maxC + minC) * 0.5;
let S = 0, H = 0;
if (delta > 0.0001) {
S = L < 0.5 ? delta / (maxC + minC) : delta / (2 - maxC - minC);
if (maxC === r) H = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
else if (maxC === g) H = ((b - r) / delta + 2) / 6;
else H = ((r - g) / delta + 4) / 6;
}
L = 1 - L;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let ro, go, bo;
if (S < 0.0001) {
ro = go = bo = L;
} else {
const q = L < 0.5 ? L * (1 + S) : L + S - L * S;
const p = 2 * L - q;
ro = hue2rgb(p, q, H + 1/3);
go = hue2rgb(p, q, H);
bo = hue2rgb(p, q, H - 1/3);
}
const toHex = v => Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, '0');
return `#${toHex(ro)}${toHex(go)}${toHex(bo)}`;
}
export function clamp(value, min, max) { export function clamp(value, min, max) {
if (Number.isNaN(value)) return min; if (Number.isNaN(value)) return min;
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
@ -171,6 +209,7 @@ export function normalizeScreen(screen, nextScreenIdRef, nextWidgetIdRef) {
if (!screen.name) screen.name = `Screen ${screen.id}`; if (!screen.name) screen.name = `Screen ${screen.id}`;
if (screen.mode === undefined || screen.mode === null) screen.mode = 0; if (screen.mode === undefined || screen.mode === null) screen.mode = 0;
if (!screen.bgColor) screen.bgColor = '#1A1A2E'; if (!screen.bgColor) screen.bgColor = '#1A1A2E';
if (!screen.nightBgColor) screen.nightBgColor = '#0E1217';
if (screen.bgImage === undefined) screen.bgImage = ''; if (screen.bgImage === undefined) screen.bgImage = '';
if (screen.bgImageMode === undefined) screen.bgImageMode = 1; if (screen.bgImageMode === undefined) screen.bgImageMode = 1;
if (!Array.isArray(screen.widgets)) screen.widgets = []; if (!Array.isArray(screen.widgets)) screen.widgets = [];