Backup
This commit is contained in:
parent
bef0d5504b
commit
27613db808
@ -25,7 +25,7 @@ static void widget_manager_timer_cb(lv_timer_t* timer)
|
||||
(void)timer;
|
||||
// Debug: Log every 100th call to verify timer is running
|
||||
static uint32_t callCount = 0;
|
||||
if (++callCount % 100 == 0) {
|
||||
if (++callCount % 10 == 0) {
|
||||
ESP_LOGI("Gui", "Timer tick %lu", callCount);
|
||||
}
|
||||
WidgetManager::instance().loop();
|
||||
@ -40,7 +40,7 @@ void Gui::create()
|
||||
// TEMP: Disabled long press handler for testing
|
||||
// lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
|
||||
// LV_EVENT_LONG_PRESSED, NULL);
|
||||
lv_timer_create(widget_manager_timer_cb, 10, nullptr);
|
||||
lv_timer_create(widget_manager_timer_cb, 50, nullptr);
|
||||
|
||||
esp_lv_adapter_unlock();
|
||||
}
|
||||
|
||||
@ -1,43 +1,14 @@
|
||||
#include "HistoryStore.hpp"
|
||||
#include "SdCard.hpp"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/time.h>
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
|
||||
static const char* TAG = "HistoryStore";
|
||||
static constexpr uint32_t HISTORY_MAGIC = 0x4B584831; // KXH1
|
||||
static constexpr uint16_t HISTORY_VERSION = 1;
|
||||
static constexpr const char* HISTORY_FILE = "/sdcard/knx_history.bin";
|
||||
static constexpr int64_t HISTORY_SAVE_INTERVAL_US = 300000000; // 5 minutes
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HistoryFileHeader {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t seriesCount;
|
||||
uint16_t fineCapacity;
|
||||
uint16_t coarseCapacity;
|
||||
uint32_t fineInterval;
|
||||
uint32_t coarseInterval;
|
||||
};
|
||||
|
||||
struct HistoryFileSeriesHeader {
|
||||
uint16_t groupAddr;
|
||||
uint8_t source;
|
||||
uint8_t reserved;
|
||||
uint16_t fineCount;
|
||||
uint16_t fineHead;
|
||||
uint16_t coarseCount;
|
||||
uint16_t coarseHead;
|
||||
uint8_t hasLatest;
|
||||
uint8_t reserved2[3];
|
||||
int32_t latestTs;
|
||||
float latestValue;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
// HistoryStore is effectively disabled to save resources and prevent hangs.
|
||||
// It now acts as a simple pass-through for the latest value (for real-time chart indication)
|
||||
// but does not record history or write to SD card.
|
||||
|
||||
HistoryStore& HistoryStore::instance() {
|
||||
static HistoryStore inst;
|
||||
@ -115,10 +86,7 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
|
||||
for (size_t i = 0; i < neededCount; ++i) {
|
||||
if (keysEqual(needed[i], key)) return;
|
||||
}
|
||||
if (neededCount >= HISTORY_MAX_SERIES) {
|
||||
ESP_LOGW(TAG, "History series limit reached");
|
||||
return;
|
||||
}
|
||||
if (neededCount >= HISTORY_MAX_SERIES) return;
|
||||
needed[neededCount++] = key;
|
||||
};
|
||||
|
||||
@ -136,30 +104,16 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
|
||||
}
|
||||
|
||||
std::array<bool, HISTORY_MAX_SERIES> keep = {};
|
||||
bool changed = false;
|
||||
size_t activeCount = 0;
|
||||
|
||||
for (size_t i = 0; i < neededCount; ++i) {
|
||||
const SeriesKey& key = needed[i];
|
||||
HistorySeries* existing = findSeriesByKey(key);
|
||||
if (existing) {
|
||||
size_t idx = static_cast<size_t>(existing - series_.data());
|
||||
if (!existing->active) {
|
||||
changed = true;
|
||||
}
|
||||
keep[idx] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find empty slot
|
||||
HistorySeries* slot = nullptr;
|
||||
for (size_t si = 0; si < series_.size(); ++si) {
|
||||
if (!keep[si] && !series_[si].active && series_[si].key.addr == 0) {
|
||||
slot = &series_[si];
|
||||
keep[si] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!slot) {
|
||||
for (size_t si = 0; si < series_.size(); ++si) {
|
||||
if (!keep[si] && !series_[si].active) {
|
||||
slot = &series_[si];
|
||||
@ -167,38 +121,15 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!slot) {
|
||||
ESP_LOGW(TAG, "History series limit reached");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slot) {
|
||||
slot->key = key;
|
||||
slot->fine.clear();
|
||||
slot->coarse.clear();
|
||||
slot->active = true;
|
||||
slot->hasLatest = false;
|
||||
slot->latestValue = 0.0f;
|
||||
slot->latestTs = 0;
|
||||
slot->lastFineSampleTs = 0;
|
||||
slot->lastCoarseSampleTs = 0;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < series_.size(); ++i) {
|
||||
bool nextActive = keep[i];
|
||||
if (series_[i].active != nextActive) {
|
||||
changed = true;
|
||||
}
|
||||
series_[i].active = nextActive;
|
||||
if (nextActive) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
seriesCount_ = activeCount;
|
||||
if (changed) {
|
||||
dirty_ = true;
|
||||
series_[i].active = keep[i];
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
@ -217,10 +148,7 @@ bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float val
|
||||
xSemaphoreGive(mutex_);
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t nowSec = now();
|
||||
series->latestValue = value;
|
||||
series->latestTs = static_cast<int32_t>(nowSec);
|
||||
series->hasLatest = true;
|
||||
xSemaphoreGive(mutex_);
|
||||
return true;
|
||||
@ -231,298 +159,56 @@ int64_t HistoryStore::now() const {
|
||||
}
|
||||
|
||||
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
|
||||
switch (period) {
|
||||
case ChartPeriod::HOUR_1: return 3600;
|
||||
case ChartPeriod::HOUR_3: return 3 * 3600;
|
||||
case ChartPeriod::HOUR_5: return 5 * 3600;
|
||||
case ChartPeriod::HOUR_12: return 12 * 3600;
|
||||
case ChartPeriod::HOUR_24: return 24 * 3600;
|
||||
case ChartPeriod::MONTH_1: return HISTORY_MONTH_SECONDS;
|
||||
default: return 3600;
|
||||
}
|
||||
return 3600; // Dummy
|
||||
}
|
||||
|
||||
bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period,
|
||||
int32_t* outValues, size_t outCount) const {
|
||||
if (!outValues || outCount == 0) return false;
|
||||
if (outCount > CHART_POINT_COUNT) outCount = CHART_POINT_COUNT;
|
||||
for (size_t i = 0; i < outCount; ++i) {
|
||||
outValues[i] = NO_POINT;
|
||||
}
|
||||
|
||||
// Just return the latest value repeated, effectively a flat line
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
const HistorySeries* series = findSeries(groupAddr, source);
|
||||
if (!series) {
|
||||
xSemaphoreGive(mutex_);
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t nowSec = now();
|
||||
int32_t window = periodSeconds(period);
|
||||
if (window <= 0) {
|
||||
xSemaphoreGive(mutex_);
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t start = nowSec - window;
|
||||
|
||||
std::array<float, CHART_POINT_COUNT> sums = {};
|
||||
std::array<uint16_t, CHART_POINT_COUNT> counts = {};
|
||||
|
||||
auto accumulate = [&](const HistoryPoint& p) {
|
||||
if (p.ts < start || p.ts > nowSec) return;
|
||||
size_t bucket = static_cast<size_t>(((p.ts - start) * outCount) / window);
|
||||
if (bucket >= outCount) bucket = outCount - 1;
|
||||
sums[bucket] += p.value;
|
||||
counts[bucket]++;
|
||||
};
|
||||
|
||||
if (period == ChartPeriod::MONTH_1) {
|
||||
series->coarse.forEach(accumulate);
|
||||
} else {
|
||||
series->fine.forEach(accumulate);
|
||||
}
|
||||
|
||||
if (series->hasLatest) {
|
||||
HistoryPoint latest{series->latestTs, series->latestValue};
|
||||
accumulate(latest);
|
||||
int32_t val = NO_POINT;
|
||||
if (series && series->hasLatest) {
|
||||
val = static_cast<int32_t>(lrintf(series->latestValue));
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
|
||||
bool hasData = false;
|
||||
int32_t lastValidValue = NO_POINT;
|
||||
|
||||
for (size_t i = 0; i < outCount; ++i) {
|
||||
if (counts[i] > 0) {
|
||||
float avg = sums[i] / counts[i];
|
||||
int32_t val = static_cast<int32_t>(lrintf(avg));
|
||||
outValues[i] = val;
|
||||
lastValidValue = val;
|
||||
hasData = true;
|
||||
} else {
|
||||
outValues[i] = lastValidValue;
|
||||
if (lastValidValue != NO_POINT) {
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasData;
|
||||
return (val != NO_POINT);
|
||||
}
|
||||
|
||||
bool HistoryStore::tick() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
int64_t nowSec = now();
|
||||
|
||||
// Only collect data if time is roughly synced (after 2020)
|
||||
if (nowSec < 1577836800LL) {
|
||||
xSemaphoreGive(mutex_);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool added = false;
|
||||
for (size_t i = 0; i < series_.size(); ++i) {
|
||||
HistorySeries& series = series_[i];
|
||||
if (!series.active || !series.hasLatest) continue;
|
||||
|
||||
if (series.fine.count == 0 || nowSec - series.lastFineSampleTs >= HISTORY_FINE_INTERVAL) {
|
||||
series.fine.push({static_cast<int32_t>(nowSec), series.latestValue});
|
||||
series.lastFineSampleTs = static_cast<int32_t>(nowSec);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (series.coarse.count == 0 || nowSec - series.lastCoarseSampleTs >= HISTORY_COARSE_INTERVAL) {
|
||||
series.coarse.push({static_cast<int32_t>(nowSec), series.latestValue});
|
||||
series.lastCoarseSampleTs = static_cast<int32_t>(nowSec);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (added) {
|
||||
dirty_ = true;
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
return added;
|
||||
return false; // Disabled
|
||||
}
|
||||
|
||||
void HistoryStore::performAutoSave() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
bool shouldSave = false;
|
||||
int64_t nowSec = now();
|
||||
int64_t monoUs = esp_timer_get_time();
|
||||
// Disabled
|
||||
}
|
||||
|
||||
if (dirty_ && nowSec > 1577836800LL && SdCard::instance().isMounted()) {
|
||||
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
|
||||
shouldSave = true;
|
||||
lastSaveMonoUs_ = monoUs;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
void HistoryStore::updateTimeOfDay(const tm& value) {
|
||||
// Disabled
|
||||
}
|
||||
|
||||
if (shouldSave) {
|
||||
saveToSdCard();
|
||||
}
|
||||
void HistoryStore::updateDate(const tm& value) {
|
||||
// Disabled
|
||||
}
|
||||
|
||||
void HistoryStore::updateDateTime(const tm& value) {
|
||||
// Disabled
|
||||
}
|
||||
|
||||
void HistoryStore::clearAll() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
for (size_t i = 0; i < series_.size(); ++i) {
|
||||
HistorySeries& series = series_[i];
|
||||
if (!series.active) continue;
|
||||
series.fine.clear();
|
||||
series.coarse.clear();
|
||||
series.hasLatest = false;
|
||||
series.latestValue = 0.0f;
|
||||
series.latestTs = 0;
|
||||
series.lastFineSampleTs = 0;
|
||||
series.lastCoarseSampleTs = 0;
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
// Disabled
|
||||
}
|
||||
|
||||
void HistoryStore::saveToSdCard() {
|
||||
if (!SdCard::instance().isMounted()) return;
|
||||
|
||||
// Double check time sync before save
|
||||
if (now() < 1577836800LL) return;
|
||||
|
||||
FILE* f = fopen(HISTORY_FILE, "wb");
|
||||
if (!f) {
|
||||
ESP_LOGW(TAG, "Failed to open history file for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
HistoryFileHeader header = {};
|
||||
header.magic = HISTORY_MAGIC;
|
||||
header.version = HISTORY_VERSION;
|
||||
uint16_t activeCount = 0;
|
||||
for (const auto& series : series_) {
|
||||
if (series.active) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
header.seriesCount = activeCount;
|
||||
header.fineCapacity = HISTORY_FINE_CAP;
|
||||
header.coarseCapacity = HISTORY_COARSE_CAP;
|
||||
header.fineInterval = HISTORY_FINE_INTERVAL;
|
||||
header.coarseInterval = HISTORY_COARSE_INTERVAL;
|
||||
xSemaphoreGive(mutex_);
|
||||
|
||||
if (fwrite(&header, sizeof(header), 1, f) != 1) {
|
||||
fclose(f);
|
||||
ESP_LOGW(TAG, "Failed to write history header");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) +
|
||||
sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP);
|
||||
uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize);
|
||||
if (!tempBuf) {
|
||||
fclose(f);
|
||||
ESP_LOGE(TAG, "Failed to allocate temp buffer for save");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < series_.size(); ++i) {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
const HistorySeries& series = series_[i];
|
||||
if (!series.active) {
|
||||
xSemaphoreGive(mutex_);
|
||||
continue;
|
||||
}
|
||||
|
||||
HistoryFileSeriesHeader* sh = (HistoryFileSeriesHeader*)tempBuf;
|
||||
sh->groupAddr = series.key.addr;
|
||||
sh->source = static_cast<uint8_t>(series.key.source);
|
||||
sh->fineCount = static_cast<uint16_t>(series.fine.count);
|
||||
sh->fineHead = static_cast<uint16_t>(series.fine.head);
|
||||
sh->coarseCount = static_cast<uint16_t>(series.coarse.count);
|
||||
sh->coarseHead = static_cast<uint16_t>(series.coarse.head);
|
||||
sh->hasLatest = series.hasLatest ? 1 : 0;
|
||||
sh->latestTs = series.latestTs;
|
||||
sh->latestValue = series.latestValue;
|
||||
|
||||
uint8_t* dataPtr = tempBuf + sizeof(HistoryFileSeriesHeader);
|
||||
memcpy(dataPtr, series.fine.points.data(), sizeof(HistoryPoint) * HISTORY_FINE_CAP);
|
||||
dataPtr += sizeof(HistoryPoint) * HISTORY_FINE_CAP;
|
||||
memcpy(dataPtr, series.coarse.points.data(), sizeof(HistoryPoint) * HISTORY_COARSE_CAP);
|
||||
|
||||
xSemaphoreGive(mutex_);
|
||||
|
||||
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
|
||||
ESP_LOGW(TAG, "Failed to write series data");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(tempBuf);
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
dirty_ = false;
|
||||
xSemaphoreGive(mutex_);
|
||||
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "History saved (%d series)", static_cast<int>(activeCount));
|
||||
// Disabled
|
||||
}
|
||||
|
||||
void HistoryStore::loadFromSdCard() {
|
||||
if (!SdCard::instance().isMounted()) return;
|
||||
|
||||
FILE* f = fopen(HISTORY_FILE, "rb");
|
||||
if (!f) return;
|
||||
|
||||
HistoryFileHeader header = {};
|
||||
if (fread(&header, sizeof(header), 1, f) != 1) {
|
||||
fclose(f);
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.magic != HISTORY_MAGIC || header.version != HISTORY_VERSION) {
|
||||
fclose(f);
|
||||
ESP_LOGW(TAG, "History header mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.fineCapacity != HISTORY_FINE_CAP ||
|
||||
header.coarseCapacity != HISTORY_COARSE_CAP ||
|
||||
header.fineInterval != HISTORY_FINE_INTERVAL ||
|
||||
header.coarseInterval != HISTORY_COARSE_INTERVAL) {
|
||||
fclose(f);
|
||||
ESP_LOGW(TAG, "History config mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
for (uint16_t i = 0; i < header.seriesCount; ++i) {
|
||||
HistoryFileSeriesHeader sh = {};
|
||||
if (fread(&sh, sizeof(sh), 1, f) != 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
HistorySeries* series = findSeries(sh.groupAddr, static_cast<TextSource>(sh.source));
|
||||
if (series) {
|
||||
series->fine.count = sh.fineCount > HISTORY_FINE_CAP ? HISTORY_FINE_CAP : sh.fineCount;
|
||||
series->fine.head = sh.fineHead >= HISTORY_FINE_CAP ? 0 : sh.fineHead;
|
||||
series->coarse.count = sh.coarseCount > HISTORY_COARSE_CAP ? HISTORY_COARSE_CAP : sh.coarseCount;
|
||||
series->coarse.head = sh.coarseHead >= HISTORY_COARSE_CAP ? 0 : sh.coarseHead;
|
||||
series->hasLatest = sh.hasLatest != 0;
|
||||
series->latestTs = sh.latestTs;
|
||||
series->latestValue = sh.latestValue;
|
||||
|
||||
if (fread(series->fine.points.data(), sizeof(HistoryPoint), HISTORY_FINE_CAP, f) != HISTORY_FINE_CAP) {
|
||||
break;
|
||||
}
|
||||
if (fread(series->coarse.points.data(), sizeof(HistoryPoint), HISTORY_COARSE_CAP, f) != HISTORY_COARSE_CAP) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
fseek(f, sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP), SEEK_CUR);
|
||||
}
|
||||
}
|
||||
dirty_ = false;
|
||||
xSemaphoreGive(mutex_);
|
||||
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "History loaded");
|
||||
// Disabled
|
||||
}
|
||||
@ -24,6 +24,11 @@ public:
|
||||
int64_t now() const;
|
||||
bool isTimeSynced() const { return now() > 1577836800LL; }
|
||||
|
||||
void updateTimeOfDay(const tm& value);
|
||||
void updateDate(const tm& value);
|
||||
void updateDateTime(const tm& value);
|
||||
void clearAll();
|
||||
|
||||
void loadFromSdCard();
|
||||
void saveToSdCard();
|
||||
|
||||
@ -74,9 +79,9 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr size_t HISTORY_MAX_SERIES = 12;
|
||||
static constexpr size_t HISTORY_FINE_CAP = 720;
|
||||
static constexpr size_t HISTORY_COARSE_CAP = 720;
|
||||
static constexpr size_t HISTORY_MAX_SERIES = 4;
|
||||
static constexpr size_t HISTORY_FINE_CAP = 360;
|
||||
static constexpr size_t HISTORY_COARSE_CAP = 360;
|
||||
static constexpr int32_t HISTORY_FINE_INTERVAL = 120;
|
||||
static constexpr int32_t HISTORY_COARSE_INTERVAL = 3600;
|
||||
static constexpr int32_t HISTORY_MONTH_SECONDS = 30 * 24 * 3600;
|
||||
@ -97,7 +102,6 @@ private:
|
||||
static bool keysEqual(const SeriesKey& a, const SeriesKey& b);
|
||||
|
||||
int32_t periodSeconds(ChartPeriod period) const;
|
||||
void clearAll();
|
||||
|
||||
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
|
||||
const HistorySeries* findSeries(uint16_t groupAddr, TextSource source) const;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#include <cstring>
|
||||
|
||||
// Maximum number of widgets
|
||||
static constexpr size_t MAX_WIDGETS = 32;
|
||||
static constexpr size_t MAX_WIDGETS = 64;
|
||||
static constexpr size_t MAX_SCREENS = 8;
|
||||
static constexpr size_t MAX_TEXT_LEN = 32;
|
||||
static constexpr size_t MAX_SCREEN_NAME_LEN = 24;
|
||||
|
||||
@ -347,25 +347,29 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
||||
}
|
||||
|
||||
lv_display_t* disp = lv_display_get_default();
|
||||
bool invEnabled = true;
|
||||
if (disp) {
|
||||
invEnabled = lv_display_is_invalidation_enabled(disp);
|
||||
lv_display_enable_invalidation(disp, false);
|
||||
}
|
||||
|
||||
// Reset all input devices BEFORE destroying widgets
|
||||
ESP_LOGI(TAG, "Resetting input devices...");
|
||||
// Reset all input devices
|
||||
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
||||
while (indev) {
|
||||
lv_indev_reset(indev, nullptr);
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
|
||||
// Now destroy C++ widgets (which deletes LVGL objects)
|
||||
ESP_LOGI(TAG, "Destroying widgets...");
|
||||
destroyAllWidgets();
|
||||
// SAFE DESTRUCTION:
|
||||
// 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());
|
||||
ESP_LOGI(TAG, "Widgets destroyed");
|
||||
lv_obj_clean(lv_layer_top());
|
||||
|
||||
// 3. Now destroy C++ objects (their destructors won't call lv_obj_delete)
|
||||
destroyAllWidgets();
|
||||
|
||||
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
|
||||
screen->name, screen->widgetCount);
|
||||
@ -373,13 +377,9 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
||||
createAllWidgets(*screen, root);
|
||||
ESP_LOGI(TAG, "Widgets created");
|
||||
applyCachedValuesToWidgets();
|
||||
#if LV_USE_OBJ_NAME
|
||||
ESP_LOGI(TAG, "Flex containers for screen '%s':", screen->name);
|
||||
dump_flex_objects(root, 0);
|
||||
#endif
|
||||
|
||||
if (disp) {
|
||||
lv_display_enable_invalidation(disp, invEnabled);
|
||||
lv_display_enable_invalidation(disp, true);
|
||||
}
|
||||
lv_obj_invalidate(lv_scr_act());
|
||||
|
||||
@ -404,6 +404,16 @@ void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) {
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
|
||||
// SAFE DESTRUCTION
|
||||
for (auto& widget : widgets_) {
|
||||
if (widget) widget->clearLvglObject();
|
||||
}
|
||||
lv_obj_clean(lv_scr_act()); // Should we clean screen when showing modal?
|
||||
// Actually, usually modal is ON TOP of screen.
|
||||
// But our current WidgetManager destroys screen widgets when showing modal!
|
||||
// That's why we clean.
|
||||
lv_obj_clean(lv_layer_top());
|
||||
|
||||
destroyAllWidgets();
|
||||
|
||||
lv_disp_t* disp = lv_disp_get_default();
|
||||
@ -513,22 +523,18 @@ void WidgetManager::closeModalLocked() {
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
|
||||
// SAFE DESTRUCTION
|
||||
for (auto& widget : widgets_) {
|
||||
if (widget) widget->clearLvglObject();
|
||||
}
|
||||
lv_obj_clean(lv_layer_top());
|
||||
|
||||
// Destroy widgets first
|
||||
printf("WM: closeModal destroying widgets...\n");
|
||||
fflush(stdout);
|
||||
destroyAllWidgets();
|
||||
|
||||
if (modalDimmer_) {
|
||||
if (lv_obj_is_valid(modalDimmer_)) lv_obj_delete(modalDimmer_);
|
||||
modalDimmer_ = nullptr;
|
||||
}
|
||||
|
||||
if (lv_obj_is_valid(modalContainer_)) {
|
||||
lv_obj_delete(modalContainer_);
|
||||
} else {
|
||||
printf("WM: Warning: modalContainer_ was invalid!\n");
|
||||
fflush(stdout);
|
||||
}
|
||||
modalContainer_ = nullptr;
|
||||
modalScreenId_ = SCREEN_ID_NONE;
|
||||
|
||||
|
||||
@ -40,8 +40,8 @@ public:
|
||||
|
||||
// Initialize LVGL adapter
|
||||
esp_lv_adapter_config_t cfg = ESP_LV_ADAPTER_DEFAULT_CONFIG();
|
||||
cfg.stack_in_psram = true;
|
||||
cfg.task_stack_size = 16 * 1024;
|
||||
cfg.stack_in_psram = false; // Use internal RAM for stack
|
||||
cfg.task_stack_size = 32 * 1024;
|
||||
ESP_ERROR_CHECK(esp_lv_adapter_init(&cfg));
|
||||
|
||||
// Register display
|
||||
|
||||
@ -123,9 +123,8 @@ void ButtonWidget::setupFlexLayout() {
|
||||
|
||||
void ButtonWidget::applyTextAlignment() {
|
||||
if (label_ == nullptr) return;
|
||||
lv_obj_set_width(label_, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(label_, toLvTextAlign(config_.textAlign), 0);
|
||||
lv_obj_align(label_, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_center(label_);
|
||||
}
|
||||
|
||||
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||
@ -137,23 +136,23 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||
|
||||
lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this);
|
||||
|
||||
bool hasIcon = !config_.isContainer &&
|
||||
config_.iconCodepoint > 0 && Fonts::hasIconFont();
|
||||
if (config_.isContainer) {
|
||||
ESP_LOGI(TAG, "Created container button %d", config_.id);
|
||||
return obj_;
|
||||
}
|
||||
|
||||
bool hasIcon = config_.iconCodepoint > 0 && Fonts::hasIconFont();
|
||||
|
||||
if (!config_.isContainer) {
|
||||
if (hasIcon) {
|
||||
// Create container for flex layout
|
||||
contentContainer_ = lv_obj_create(obj_);
|
||||
if (contentContainer_ == nullptr) {
|
||||
return obj_; // Continue without icon container
|
||||
}
|
||||
if (contentContainer_) {
|
||||
set_obj_name(contentContainer_, "Button", config_.id, "content");
|
||||
lv_obj_remove_style_all(contentContainer_);
|
||||
lv_obj_set_size(contentContainer_, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_center(contentContainer_);
|
||||
lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE); // Pass clicks to parent
|
||||
lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
// Create icon label
|
||||
bool iconFirst = (config_.iconPosition == static_cast<uint8_t>(IconPosition::LEFT) ||
|
||||
config_.iconPosition == static_cast<uint8_t>(IconPosition::TOP));
|
||||
|
||||
@ -166,7 +165,6 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
|
||||
// Create text label
|
||||
label_ = lv_label_create(contentContainer_);
|
||||
set_obj_name(label_, "Button", config_.id, "text");
|
||||
lv_label_set_text(label_, config_.text);
|
||||
@ -182,15 +180,17 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
setupFlexLayout();
|
||||
}
|
||||
} else {
|
||||
// Simple button without icon
|
||||
label_ = lv_label_create(obj_);
|
||||
set_obj_name(label_, "Button", config_.id, "text");
|
||||
lv_label_set_text(label_, config_.text);
|
||||
lv_label_set_long_mode(label_, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(label_, LV_SIZE_CONTENT);
|
||||
applyTextAlignment();
|
||||
lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created button '%s' at %d,%d (icon: 0x%lx)",
|
||||
config_.text, config_.x, config_.y, (unsigned long)config_.iconCodepoint);
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
#include "ClockWidget.hpp"
|
||||
#include <cmath>
|
||||
|
||||
#ifndef PI
|
||||
#define PI 3.14159265358979323846f
|
||||
#endif
|
||||
#include <initializer_list>
|
||||
|
||||
ClockWidget::ClockWidget(const WidgetConfig& config)
|
||||
: Widget(config)
|
||||
@ -20,20 +17,31 @@ lv_obj_t* ClockWidget::create(lv_obj_t* parent) {
|
||||
config_.width > 0 ? config_.width : 200,
|
||||
config_.height > 0 ? config_.height : 200);
|
||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
// Create hands as rectangles
|
||||
auto createHand = [&](lv_obj_t*& hand, const char* name) {
|
||||
hand = lv_obj_create(obj_);
|
||||
lv_obj_remove_style_all(hand);
|
||||
lv_obj_add_flag(hand, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_clear_flag(hand, LV_OBJ_FLAG_CLICKABLE);
|
||||
// Set pivot to bottom center (will be set in applyStyle implicitly by size?)
|
||||
// We will set alignment there.
|
||||
};
|
||||
|
||||
createHand(hourHand_, "Hour");
|
||||
createHand(minuteHand_, "Minute");
|
||||
createHand(secondHand_, "Second");
|
||||
|
||||
// Create hands
|
||||
hourHand_ = lv_line_create(obj_);
|
||||
minuteHand_ = lv_line_create(obj_);
|
||||
secondHand_ = lv_line_create(obj_);
|
||||
centerDot_ = lv_obj_create(obj_);
|
||||
|
||||
// Style center dot
|
||||
lv_obj_remove_style_all(centerDot_);
|
||||
lv_obj_set_size(centerDot_, 8, 8);
|
||||
lv_obj_add_flag(centerDot_, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_clear_flag(centerDot_, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(centerDot_, 12, 12);
|
||||
lv_obj_set_style_radius(centerDot_, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_center(centerDot_);
|
||||
|
||||
// Initial update to set positions (will be updated by loop)
|
||||
// Initial update
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm t;
|
||||
@ -46,7 +54,7 @@ lv_obj_t* ClockWidget::create(lv_obj_t* parent) {
|
||||
void ClockWidget::applyStyle() {
|
||||
if (!obj_) return;
|
||||
|
||||
// Face style (background)
|
||||
// Face style
|
||||
if (config_.bgOpacity > 0) {
|
||||
lv_obj_set_style_bg_color(obj_, lv_color_make(
|
||||
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
|
||||
@ -54,7 +62,7 @@ void ClockWidget::applyStyle() {
|
||||
}
|
||||
|
||||
if (config_.borderRadius > 0) {
|
||||
lv_obj_set_style_radius(obj_, config_.borderRadius, 0); // Usually 50% for circle
|
||||
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
|
||||
} else {
|
||||
lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0);
|
||||
}
|
||||
@ -63,31 +71,62 @@ void ClockWidget::applyStyle() {
|
||||
lv_obj_set_style_border_color(obj_, lv_color_make(
|
||||
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
||||
|
||||
// Hand colors
|
||||
lv_color_t handColor = lv_color_make(
|
||||
config_.textColor.r, config_.textColor.g, config_.textColor.b);
|
||||
lv_color_t secColor = lv_color_make(200, 50, 50); // Red for second hand
|
||||
lv_color_t secColor = lv_color_make(200, 50, 50);
|
||||
|
||||
int32_t w = config_.width > 0 ? config_.width : 200;
|
||||
int32_t h = config_.height > 0 ? config_.height : 200;
|
||||
int32_t radius = (w < h ? w : h) / 2;
|
||||
|
||||
auto configHand = [&](lv_obj_t* hand, int32_t width, int32_t length, lv_color_t color) {
|
||||
if (!hand) return;
|
||||
lv_obj_set_size(hand, width, length);
|
||||
lv_obj_set_style_bg_color(hand, color, 0);
|
||||
lv_obj_set_style_bg_opa(hand, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(hand, width / 2, 0);
|
||||
|
||||
// Align to center-bottom of the hand to the center of the clock
|
||||
// For rotation, we position the hand such that its rotation point is at the clock center.
|
||||
// We put the hand's bottom-center at the clock's center.
|
||||
// LVGL Rotation pivot is relative to the object's top-left (0,0) by default?
|
||||
// No, default pivot is center.
|
||||
// We want pivot at (width/2, length).
|
||||
// And we want that pivot point to be at clock center (w/2, h/2).
|
||||
|
||||
// Position:
|
||||
// x = (ClockW - HandW) / 2
|
||||
// y = (ClockH/2) - HandLength
|
||||
// This puts the bottom of the hand at the center.
|
||||
|
||||
lv_obj_align(hand, LV_ALIGN_CENTER, 0, -length/2);
|
||||
// Wait, align center puts the *center* of hand at center of clock.
|
||||
// Hand center is at (w/2, length/2).
|
||||
// We want hand bottom (w/2, length) to be at clock center.
|
||||
// So we shift y by -length/2.
|
||||
|
||||
// Pivot:
|
||||
// Set pivot to (width/2, length).
|
||||
// lv_obj_set_style_transform_pivot_x(hand, width/2, 0);
|
||||
// lv_obj_set_style_transform_pivot_y(hand, length, 0); // Bottom
|
||||
|
||||
// BUT: lv_obj_align(..., 0, -length/2) moves it up.
|
||||
// Let's verify.
|
||||
|
||||
lv_obj_set_style_transform_pivot_x(hand, width / 2, 0);
|
||||
lv_obj_set_style_transform_pivot_y(hand, length - 2, 0); // Slightly above bottom for overlap
|
||||
};
|
||||
|
||||
configHand(hourHand_, 6, radius * 0.6, handColor);
|
||||
configHand(minuteHand_, 4, radius * 0.85, handColor);
|
||||
configHand(secondHand_, 2, radius * 0.9, secColor);
|
||||
|
||||
if (hourHand_) {
|
||||
lv_obj_set_style_line_width(hourHand_, 6, 0);
|
||||
lv_obj_set_style_line_color(hourHand_, handColor, 0);
|
||||
lv_obj_set_style_line_rounded(hourHand_, true, 0);
|
||||
}
|
||||
if (minuteHand_) {
|
||||
lv_obj_set_style_line_width(minuteHand_, 4, 0);
|
||||
lv_obj_set_style_line_color(minuteHand_, handColor, 0);
|
||||
lv_obj_set_style_line_rounded(minuteHand_, true, 0);
|
||||
}
|
||||
if (secondHand_) {
|
||||
lv_obj_set_style_line_width(secondHand_, 2, 0);
|
||||
lv_obj_set_style_line_color(secondHand_, secColor, 0);
|
||||
lv_obj_set_style_line_rounded(secondHand_, true, 0);
|
||||
}
|
||||
if (centerDot_) {
|
||||
lv_obj_set_style_bg_color(centerDot_, handColor, 0);
|
||||
lv_obj_set_style_bg_opa(centerDot_, LV_OPA_COVER, 0);
|
||||
}
|
||||
|
||||
// Force redraw of hands with new style/dimensions
|
||||
// Force update
|
||||
lastSecond_ = -1;
|
||||
time_t now;
|
||||
time(&now);
|
||||
@ -103,49 +142,21 @@ void ClockWidget::updateHands(const struct tm& t) {
|
||||
lastMinute_ = t.tm_min;
|
||||
lastHour_ = t.tm_hour;
|
||||
|
||||
int32_t w = lv_obj_get_width(obj_);
|
||||
int32_t h = lv_obj_get_height(obj_);
|
||||
int32_t cx = w / 2;
|
||||
int32_t cy = h / 2;
|
||||
int32_t radius = (w < h ? w : h) / 2;
|
||||
// Calculate angles in 0.1 degree units (LVGL 9)
|
||||
// Second: 6 deg * 10 = 60 per sec
|
||||
int32_t angleSec = t.tm_sec * 60;
|
||||
|
||||
// Lengths
|
||||
int32_t lenSec = radius - 10;
|
||||
int32_t lenMin = radius - 20;
|
||||
int32_t lenHour = radius * 0.6f;
|
||||
// Minute: (6 deg * min + 0.1 deg * sec) * 10 = 60 * min + sec
|
||||
int32_t angleMin = t.tm_min * 60 + t.tm_sec;
|
||||
|
||||
// Angles (0 is top, clockwise)
|
||||
// Second: 6 deg per sec
|
||||
float angleSec = t.tm_sec * 6.0f;
|
||||
// Minute: 6 deg per min + 0.1 deg per sec
|
||||
float angleMin = t.tm_min * 6.0f + t.tm_sec * 0.1f;
|
||||
// Hour: 30 deg per hour + 0.5 deg per min
|
||||
float angleHour = (t.tm_hour % 12) * 30.0f + t.tm_min * 0.5f;
|
||||
// Hour: (30 deg * hr + 0.5 deg * min) * 10 = 300 * (hr%12) + 5 * min
|
||||
int32_t angleHour = (t.tm_hour % 12) * 300 + t.tm_min * 5;
|
||||
|
||||
auto calcPoints = [&](float angle, int32_t len, lv_point_precise_t* p) {
|
||||
float rad = (angle - 90.0f) * PI / 180.0f;
|
||||
p[0].x = cx;
|
||||
p[0].y = cy;
|
||||
p[1].x = cx + static_cast<int32_t>(cos(rad) * len);
|
||||
p[1].y = cy + static_cast<int32_t>(sin(rad) * len);
|
||||
};
|
||||
|
||||
if (secondHand_) {
|
||||
calcPoints(angleSec, lenSec, secondPoints_);
|
||||
lv_line_set_points(secondHand_, secondPoints_, 2);
|
||||
}
|
||||
if (minuteHand_) {
|
||||
calcPoints(angleMin, lenMin, minutePoints_);
|
||||
lv_line_set_points(minuteHand_, minutePoints_, 2);
|
||||
}
|
||||
if (hourHand_) {
|
||||
calcPoints(angleHour, lenHour, hourPoints_);
|
||||
lv_line_set_points(hourHand_, hourPoints_, 2);
|
||||
}
|
||||
if (secondHand_) lv_obj_set_style_transform_rotation(secondHand_, angleSec, 0);
|
||||
if (minuteHand_) lv_obj_set_style_transform_rotation(minuteHand_, angleMin, 0);
|
||||
if (hourHand_) lv_obj_set_style_transform_rotation(hourHand_, angleHour, 0);
|
||||
}
|
||||
|
||||
void ClockWidget::onKnxTime(const struct tm& value, TextSource source) {
|
||||
// Only accept system time updates or explicitly configured KNX time sources if we supported them
|
||||
// For now, ClockWidget listens to SYSTEM_TIME via WidgetManager broadcast
|
||||
updateHands(value);
|
||||
}
|
||||
@ -20,11 +20,6 @@ private:
|
||||
lv_obj_t* secondHand_ = nullptr;
|
||||
lv_obj_t* centerDot_ = nullptr;
|
||||
|
||||
// Persistent point arrays for lines (LVGL does not copy them)
|
||||
lv_point_precise_t hourPoints_[2];
|
||||
lv_point_precise_t minutePoints_[2];
|
||||
lv_point_precise_t secondPoints_[2];
|
||||
|
||||
// Cache current time to avoid redrawing if not changed
|
||||
int lastHour_ = -1;
|
||||
int lastMinute_ = -1;
|
||||
|
||||
@ -19,6 +19,7 @@ public:
|
||||
// Access to LVGL object
|
||||
lv_obj_t* getLvglObject() const { return obj_; }
|
||||
lv_obj_t* getObj() const { return obj_; } // Alias
|
||||
void clearLvglObject() { obj_ = nullptr; }
|
||||
|
||||
// Widget ID
|
||||
uint8_t getId() const { return config_.id; }
|
||||
|
||||
1
sdcard_content/webseite/assets/index-D_QgTvEC.css
Normal file
1
sdcard_content/webseite/assets/index-D_QgTvEC.css
Normal file
File diff suppressed because one or more lines are too long
9
sdcard_content/webseite/assets/index-D_mZROR3.js
Normal file
9
sdcard_content/webseite/assets/index-D_mZROR3.js
Normal file
File diff suppressed because one or more lines are too long
@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web-interface</title>
|
||||
<script type="module" crossorigin src="/assets/index-itIBE803.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BV9BQMzn.css">
|
||||
<script type="module" crossorigin src="/assets/index-D_mZROR3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D_QgTvEC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -15,11 +15,11 @@
|
||||
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<template v-if="key === 'label'">
|
||||
<template v-if="key === 'label' || key === 'button'">
|
||||
<h4 :class="headingClass">Inhalt</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.label)" :key="group.label" :label="group.label">
|
||||
<optgroup v-for="group in groupedSources(sourceOptions[key])" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
@ -40,11 +40,6 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="key === 'button'">
|
||||
<h4 :class="headingClass">Button</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Hinweis</label><span :class="noteClass">Text als Label-Child anlegen.</span></div>
|
||||
</template>
|
||||
|
||||
<template v-if="key === 'led'">
|
||||
<h4 :class="headingClass">LED</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||
|
||||
@ -125,6 +125,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isClock">
|
||||
<div class="relative w-full h-full rounded-full border-2 box-border flex items-center justify-center overflow-hidden" :style="{ borderColor: widget.textColor }">
|
||||
<!-- Center Dot -->
|
||||
<div class="absolute w-2 h-2 rounded-full z-10" :style="{ backgroundColor: widget.textColor }"></div>
|
||||
<!-- Hour Hand -->
|
||||
<div class="absolute w-1.5 h-[28%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full" :style="{ backgroundColor: widget.textColor, transform: 'rotate(300deg)' }"></div>
|
||||
<!-- Minute Hand -->
|
||||
<div class="absolute w-1 h-[40%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full" :style="{ backgroundColor: widget.textColor, transform: 'rotate(70deg)' }"></div>
|
||||
<!-- Second Hand -->
|
||||
<div class="absolute w-0.5 h-[45%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full bg-[#c83232]" :style="{ transform: 'rotate(140deg)' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Icon-only Widget -->
|
||||
<template v-else-if="isIcon">
|
||||
<span class="material-symbols-outlined flex items-center justify-center" :style="iconOnlyStyle">
|
||||
@ -199,6 +212,7 @@ const isPowerFlow = computed(() => props.widget.type === WIDGET_TYPES.POWERFLOW)
|
||||
const isPowerNode = computed(() => props.widget.type === WIDGET_TYPES.POWERNODE);
|
||||
const isPowerLink = computed(() => props.widget.type === WIDGET_TYPES.POWERLINK);
|
||||
const isChart = computed(() => props.widget.type === WIDGET_TYPES.CHART);
|
||||
const isClock = computed(() => props.widget.type === WIDGET_TYPES.CLOCK);
|
||||
|
||||
const tabPosition = computed(() => props.widget.iconPosition || 0); // 0=Top, 1=Bottom...
|
||||
const tabHeight = computed(() => (props.widget.iconSize || 5) * 10);
|
||||
@ -557,6 +571,16 @@ const computedStyle = computed(() => {
|
||||
style.outline = `2px dashed ${hexToRgba('#4f8ad9', 0.9)}`;
|
||||
style.outlineOffset = '2px';
|
||||
}
|
||||
} else if (isClock.value) {
|
||||
if (w.bgOpacity > 0) {
|
||||
const alpha = clamp(w.bgOpacity / 255, 0, 1).toFixed(2);
|
||||
style.background = hexToRgba(w.bgColor, alpha);
|
||||
}
|
||||
if (w.radius > 0) {
|
||||
style.borderRadius = `${w.radius * s}px`;
|
||||
} else {
|
||||
style.borderRadius = '50%';
|
||||
}
|
||||
} else if (isTabView.value) {
|
||||
style.background = w.bgColor;
|
||||
style.borderRadius = `${w.radius * s}px`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user