mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-10-22 17:31:14 +00:00
TCP scaling, GUI pooling, dev mode, protocol improvements
- TCP: wider drain budgets, TCP_NODELAY, no flush, PSRAM buffers, queue announces until hub ID - GUI: object pool Messages/Contacts screens, partial MessageView status, targeted tab refresh - LVGL throttled to 5fps when dimmed, non-blocking RSSI monitor, bounded I2S writes - LXMF batch drain, BLE frame mutex, LoRa interrupt-driven RX, name cache cap 300 - Developer mode in settings: custom radio params (freq/txp/SF/BW/CR/preamble) behind warning
This commit is contained in:
1 parent
02d7dedcce
commit
fc91f8214e
30 files changed
+604
-238
No files matched your search
+18
-12
@@ -60,22 +60,28 @@ void LvTabBar::cycleTab(int direction) {
|
||||
|
||||
void LvTabBar::setUnreadCount(int tab, int count) {
|
||||
if (tab < 0 || tab >= TAB_COUNT) return;
|
||||
if (_unread[tab] == count) return; // No change
|
||||
_unread[tab] = count;
|
||||
refreshTabs();
|
||||
refreshTab(tab);
|
||||
}
|
||||
|
||||
void LvTabBar::refreshTab(int idx) {
|
||||
if (idx < 0 || idx >= TAB_COUNT || !_tabs[idx]) return;
|
||||
bool active = (idx == _activeTab);
|
||||
lv_obj_set_style_text_color(_tabs[idx],
|
||||
lv_color_hex(active ? Theme::TAB_ACTIVE : Theme::TAB_INACTIVE), 0);
|
||||
|
||||
char buf[24];
|
||||
if (_unread[idx] > 0) {
|
||||
snprintf(buf, sizeof(buf), "%s(%d)", TAB_NAMES[idx], _unread[idx]);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s", TAB_NAMES[idx]);
|
||||
}
|
||||
lv_label_set_text(_tabs[idx], buf);
|
||||
}
|
||||
|
||||
void LvTabBar::refreshTabs() {
|
||||
for (int i = 0; i < TAB_COUNT; i++) {
|
||||
bool active = (i == _activeTab);
|
||||
lv_obj_set_style_text_color(_tabs[i],
|
||||
lv_color_hex(active ? Theme::TAB_ACTIVE : Theme::TAB_INACTIVE), 0);
|
||||
|
||||
char buf[24];
|
||||
if (_unread[i] > 0) {
|
||||
snprintf(buf, sizeof(buf), "%s(%d)", TAB_NAMES[i], _unread[i]);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s", TAB_NAMES[i]);
|
||||
}
|
||||
lv_label_set_text(_tabs[i], buf);
|
||||
refreshTab(i);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
|
||||
private:
|
||||
void refreshTabs();
|
||||
void refreshTab(int idx);
|
||||
|
||||
lv_obj_t* _bar = nullptr;
|
||||
lv_obj_t* _tabs[TAB_COUNT] = {};
|
||||
|
||||
@@ -28,6 +28,31 @@ void LvContactsScreen::createUI(lv_obj_t* parent) {
|
||||
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
|
||||
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Pre-allocate row pool
|
||||
const lv_font_t* font = &lv_font_montserrat_14;
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 28);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
lv_obj_t* lbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(lbl, font, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(lbl, "");
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
|
||||
_poolRows[i] = row;
|
||||
_poolNameLabels[i] = lbl;
|
||||
}
|
||||
|
||||
_lastContactCount = -1;
|
||||
rebuildList();
|
||||
}
|
||||
@@ -35,6 +60,7 @@ void LvContactsScreen::createUI(lv_obj_t* parent) {
|
||||
void LvContactsScreen::onEnter() {
|
||||
_lastContactCount = -1;
|
||||
_selectedIdx = 0;
|
||||
_viewportStart = 0;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
@@ -48,20 +74,13 @@ void LvContactsScreen::refreshUI() {
|
||||
}
|
||||
|
||||
void LvContactsScreen::updateSelection(int oldIdx, int newIdx) {
|
||||
if (oldIdx >= 0 && oldIdx < (int)_rows.size()) {
|
||||
lv_obj_set_style_bg_color(_rows[oldIdx], lv_color_hex(Theme::BG), 0);
|
||||
}
|
||||
if (newIdx >= 0 && newIdx < (int)_rows.size()) {
|
||||
lv_obj_set_style_bg_color(_rows[newIdx], lv_color_hex(Theme::SELECTION_BG), 0);
|
||||
lv_obj_scroll_to_view(_rows[newIdx], LV_ANIM_OFF);
|
||||
}
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
void LvContactsScreen::rebuildList() {
|
||||
if (!_am || !_list) return;
|
||||
_rows.clear();
|
||||
_contactIndices.clear();
|
||||
lv_obj_clean(_list);
|
||||
|
||||
const auto& nodes = _am->nodes();
|
||||
for (int i = 0; i < (int)nodes.size(); i++) {
|
||||
@@ -73,6 +92,7 @@ void LvContactsScreen::rebuildList() {
|
||||
if (count == 0) {
|
||||
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,32 +102,47 @@ void LvContactsScreen::rebuildList() {
|
||||
if (_selectedIdx >= count) _selectedIdx = count - 1;
|
||||
if (_selectedIdx < 0) _selectedIdx = 0;
|
||||
|
||||
const lv_font_t* font = &lv_font_montserrat_14;
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const auto& node = nodes[_contactIndices[i]];
|
||||
bool selected = (i == _selectedIdx);
|
||||
void LvContactsScreen::syncVisibleRows() {
|
||||
if (!_am || !_list) return;
|
||||
int count = (int)_contactIndices.size();
|
||||
const auto& nodes = _am->nodes();
|
||||
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 28);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(
|
||||
selected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
if (count == 0) {
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display name only
|
||||
lv_obj_t* lbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(lbl, font, 0);
|
||||
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(lbl, node.name.c_str());
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
// Compute viewport centered on selection
|
||||
int halfPool = ROW_POOL_SIZE / 2;
|
||||
_viewportStart = _selectedIdx - halfPool;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
if (_viewportStart + ROW_POOL_SIZE > count) {
|
||||
_viewportStart = count - ROW_POOL_SIZE;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
}
|
||||
|
||||
_rows.push_back(row);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
int contactIdx = _viewportStart + i;
|
||||
if (contactIdx >= count) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
|
||||
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
int nodeIdx = _contactIndices[contactIdx];
|
||||
if (nodeIdx < 0 || nodeIdx >= (int)nodes.size()) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
const auto& node = nodes[nodeIdx];
|
||||
bool isSelected = (contactIdx == _selectedIdx);
|
||||
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
|
||||
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_label_set_text(_poolNameLabels[i], node.name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,17 +183,15 @@ bool LvContactsScreen::handleKey(const KeyEvent& event) {
|
||||
|
||||
if (event.up) {
|
||||
if (_selectedIdx > 0) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx--;
|
||||
updateSelection(prev, _selectedIdx);
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.down) {
|
||||
if (_selectedIdx < count - 1) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx++;
|
||||
updateSelection(prev, _selectedIdx);
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
|
||||
private:
|
||||
void rebuildList();
|
||||
void syncVisibleRows();
|
||||
void updateSelection(int oldIdx, int newIdx);
|
||||
|
||||
AnnounceManager* _am = nullptr;
|
||||
@@ -33,9 +34,15 @@ private:
|
||||
bool _confirmDelete = false;
|
||||
int _lastContactCount = -1;
|
||||
int _selectedIdx = 0;
|
||||
int _viewportStart = 0;
|
||||
std::vector<int> _contactIndices; // Maps row -> node index in _am->nodes()
|
||||
|
||||
lv_obj_t* _list = nullptr;
|
||||
lv_obj_t* _lblEmpty = nullptr;
|
||||
std::vector<lv_obj_t*> _rows;
|
||||
|
||||
// Object pool
|
||||
static constexpr int ROW_POOL_SIZE = 10;
|
||||
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
|
||||
};
|
||||
@@ -111,14 +111,14 @@ void LvMessageView::onEnter() {
|
||||
_lxmf->markRead(_peerHex);
|
||||
// Update unread badge on Messages tab
|
||||
if (_ui) _ui->lvTabBar().setUnreadCount(LvTabBar::TAB_MSGS, _lxmf->unreadCount());
|
||||
// Register status callback — update cached message status in-place
|
||||
// Register status callback — partial update without full rebuild
|
||||
std::string peer = _peerHex;
|
||||
_lxmf->setStatusCallback([this, peer](const std::string& peerHex, double, LXMFStatus newStatus) {
|
||||
if (peerHex != peer) return;
|
||||
for (int i = _cachedMsgs.size() - 1; i >= 0; i--) {
|
||||
if (!_cachedMsgs[i].incoming && _cachedMsgs[i].status == LXMFStatus::QUEUED) {
|
||||
_cachedMsgs[i].status = newStatus;
|
||||
rebuildMessages();
|
||||
updateMessageStatus(i, newStatus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,8 @@ void LvMessageView::rebuildMessages() {
|
||||
_lastMsgCount = (int)_cachedMsgs.size();
|
||||
_lastRefreshMs = millis();
|
||||
lv_obj_clean(_msgScroll);
|
||||
_statusLabels.clear();
|
||||
_textLabels.clear();
|
||||
|
||||
const lv_font_t* font = &lv_font_montserrat_12;
|
||||
int maxBubbleW = Theme::CONTENT_W * 3 / 4;
|
||||
@@ -229,7 +231,7 @@ void LvMessageView::rebuildMessages() {
|
||||
lv_obj_set_width(lbl, maxBubbleW - 14);
|
||||
lv_label_set_text(lbl, msg.content.c_str());
|
||||
|
||||
// Status indicator for outgoing
|
||||
// Status indicator for outgoing (tracked for partial updates)
|
||||
if (!msg.incoming) {
|
||||
const char* ind = "~";
|
||||
uint32_t indColor = Theme::MUTED;
|
||||
@@ -243,6 +245,11 @@ void LvMessageView::rebuildMessages() {
|
||||
lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0);
|
||||
lv_label_set_text(statusLbl, ind);
|
||||
lv_obj_align(statusLbl, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
||||
_statusLabels.push_back(statusLbl);
|
||||
_textLabels.push_back(lbl);
|
||||
} else {
|
||||
_statusLabels.push_back(nullptr);
|
||||
_textLabels.push_back(nullptr);
|
||||
}
|
||||
|
||||
// Timestamp below bubble
|
||||
@@ -269,6 +276,35 @@ void LvMessageView::rebuildMessages() {
|
||||
lv_obj_scroll_to_y(_msgScroll, LV_COORD_MAX, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
void LvMessageView::updateMessageStatus(int msgIdx, LXMFStatus status) {
|
||||
if (msgIdx < 0 || msgIdx >= (int)_statusLabels.size()) return;
|
||||
lv_obj_t* statusLbl = _statusLabels[msgIdx];
|
||||
lv_obj_t* textLbl = _textLabels[msgIdx];
|
||||
if (!statusLbl) return; // Incoming message, no status label
|
||||
|
||||
// Update status indicator
|
||||
const char* ind = "~";
|
||||
uint32_t indColor = Theme::MUTED;
|
||||
if (status == LXMFStatus::SENT || status == LXMFStatus::DELIVERED) {
|
||||
ind = "*"; indColor = Theme::ACCENT;
|
||||
} else if (status == LXMFStatus::FAILED) {
|
||||
ind = "!"; indColor = Theme::ERROR_CLR;
|
||||
}
|
||||
lv_obj_set_style_text_color(statusLbl, lv_color_hex(indColor), 0);
|
||||
lv_label_set_text(statusLbl, ind);
|
||||
|
||||
// Update text color to match status
|
||||
if (textLbl) {
|
||||
uint32_t textColor = Theme::PRIMARY;
|
||||
if (status == LXMFStatus::QUEUED || status == LXMFStatus::SENDING) {
|
||||
textColor = Theme::WARNING_CLR;
|
||||
} else if (status == LXMFStatus::FAILED) {
|
||||
textColor = Theme::ERROR_CLR;
|
||||
}
|
||||
lv_obj_set_style_text_color(textLbl, lv_color_hex(textColor), 0);
|
||||
}
|
||||
}
|
||||
|
||||
void LvMessageView::sendCurrentMessage() {
|
||||
if (!_lxmf || _peerHex.empty() || _inputText.empty()) return;
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ private:
|
||||
unsigned long _lastRefreshMs = 0;
|
||||
std::vector<LXMFMessage> _cachedMsgs;
|
||||
|
||||
void updateMessageStatus(int msgIdx, LXMFStatus status);
|
||||
|
||||
// LVGL widgets
|
||||
lv_obj_t* _header = nullptr;
|
||||
lv_obj_t* _lblHeader = nullptr;
|
||||
@@ -51,5 +53,9 @@ private:
|
||||
lv_obj_t* _textarea = nullptr;
|
||||
lv_obj_t* _btnSend = nullptr;
|
||||
|
||||
// Per-message status labels for partial updates (avoids full rebuild)
|
||||
std::vector<lv_obj_t*> _statusLabels;
|
||||
std::vector<lv_obj_t*> _textLabels;
|
||||
|
||||
static constexpr unsigned long REFRESH_INTERVAL_MS = 2000; // Check for new messages every 2s
|
||||
};
|
||||
@@ -32,6 +32,62 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
|
||||
lv_obj_set_layout(_list, LV_LAYOUT_FLEX);
|
||||
lv_obj_set_flex_flow(_list, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Pre-allocate row pool (following LvNodesScreen pattern)
|
||||
const lv_font_t* nameFont = &lv_font_montserrat_14;
|
||||
const lv_font_t* smallFont = &lv_font_montserrat_12;
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 38);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Unread dot
|
||||
lv_obj_t* dot = lv_obj_create(row);
|
||||
lv_obj_set_size(dot, 6, 6);
|
||||
lv_obj_set_style_radius(dot, 3, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(dot, 0, 0);
|
||||
lv_obj_set_style_pad_all(dot, 0, 0);
|
||||
lv_obj_set_pos(dot, 5, 8);
|
||||
lv_obj_add_flag(dot, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Name label (line 1)
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, "");
|
||||
lv_obj_align(nameLbl, LV_ALIGN_TOP_LEFT, 14, 2);
|
||||
|
||||
// Time label (line 1, right)
|
||||
lv_obj_t* timeLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(timeLbl, &lv_font_montserrat_10, 0);
|
||||
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(timeLbl, "");
|
||||
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 4);
|
||||
|
||||
// Preview label (line 2)
|
||||
lv_obj_t* prevLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
|
||||
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(prevLbl, "");
|
||||
lv_obj_align(prevLbl, LV_ALIGN_BOTTOM_LEFT, 14, -4);
|
||||
|
||||
_poolRows[i] = row;
|
||||
_poolDots[i] = dot;
|
||||
_poolNameLabels[i] = nameLbl;
|
||||
_poolTimeLabels[i] = timeLbl;
|
||||
_poolPreviewLabels[i] = prevLbl;
|
||||
}
|
||||
|
||||
_lastConvCount = -1;
|
||||
rebuildList();
|
||||
}
|
||||
@@ -39,6 +95,7 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
|
||||
void LvMessagesScreen::onEnter() {
|
||||
_lastConvCount = -1;
|
||||
_selectedIdx = 0;
|
||||
_viewportStart = 0;
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
@@ -51,15 +108,9 @@ void LvMessagesScreen::refreshUI() {
|
||||
}
|
||||
}
|
||||
|
||||
// Update only the selection highlight without rebuilding widgets
|
||||
// Update only the selection highlight via pool sync
|
||||
void LvMessagesScreen::updateSelection(int oldIdx, int newIdx) {
|
||||
if (oldIdx >= 0 && oldIdx < (int)_rows.size()) {
|
||||
lv_obj_set_style_bg_color(_rows[oldIdx], lv_color_hex(Theme::BG), 0);
|
||||
}
|
||||
if (newIdx >= 0 && newIdx < (int)_rows.size()) {
|
||||
lv_obj_set_style_bg_color(_rows[newIdx], lv_color_hex(Theme::SELECTION_BG), 0);
|
||||
lv_obj_scroll_to_view(_rows[newIdx], LV_ANIM_OFF);
|
||||
}
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
void LvMessagesScreen::rebuildList() {
|
||||
@@ -69,30 +120,21 @@ void LvMessagesScreen::rebuildList() {
|
||||
int count = (int)convs.size();
|
||||
_lastConvCount = count;
|
||||
_lastUnreadTotal = _lxmf->unreadCount();
|
||||
_rows.clear();
|
||||
_sortedPeers.clear();
|
||||
lv_obj_clean(_list);
|
||||
_sortedConvs.clear();
|
||||
|
||||
if (count == 0) {
|
||||
lv_obj_clear_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_add_flag(_lblEmpty, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Build conversation info for sorting by most recent
|
||||
struct ConvInfo {
|
||||
std::string peerHex;
|
||||
double lastTs = 0;
|
||||
std::string preview;
|
||||
bool hasUnread = false;
|
||||
};
|
||||
|
||||
std::vector<ConvInfo> sorted;
|
||||
sorted.reserve(count);
|
||||
|
||||
// Build sorted conversation info
|
||||
_sortedConvs.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
ConvInfo ci;
|
||||
ci.peerHex = convs[i];
|
||||
@@ -102,85 +144,85 @@ void LvMessagesScreen::rebuildList() {
|
||||
ci.preview = s->lastPreview;
|
||||
ci.hasUnread = s->unreadCount > 0;
|
||||
}
|
||||
sorted.push_back(ci);
|
||||
// Resolve display name
|
||||
std::string peerName;
|
||||
if (_am) peerName = _am->lookupName(ci.peerHex);
|
||||
ci.displayName = !peerName.empty() ? peerName.substr(0, 15) : ci.peerHex.substr(0, 12);
|
||||
_sortedConvs.push_back(ci);
|
||||
}
|
||||
|
||||
std::sort(sorted.begin(), sorted.end(), [](const ConvInfo& a, const ConvInfo& b) {
|
||||
std::sort(_sortedConvs.begin(), _sortedConvs.end(), [](const ConvInfo& a, const ConvInfo& b) {
|
||||
return a.lastTs > b.lastTs;
|
||||
});
|
||||
|
||||
for (auto& ci : sorted) _sortedPeers.push_back(ci.peerHex);
|
||||
for (auto& ci : _sortedConvs) _sortedPeers.push_back(ci.peerHex);
|
||||
|
||||
if (_selectedIdx >= count) _selectedIdx = count - 1;
|
||||
if (_selectedIdx < 0) _selectedIdx = 0;
|
||||
|
||||
const lv_font_t* nameFont = &lv_font_montserrat_14;
|
||||
const lv_font_t* smallFont = &lv_font_montserrat_12;
|
||||
syncVisibleRows();
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int)sorted.size(); i++) {
|
||||
const auto& ci = sorted[i];
|
||||
void LvMessagesScreen::syncVisibleRows() {
|
||||
if (!_list) return;
|
||||
int count = (int)_sortedConvs.size();
|
||||
|
||||
lv_obj_t* row = lv_obj_create(_list);
|
||||
lv_obj_set_size(row, Theme::CONTENT_W, 38);
|
||||
lv_obj_set_style_bg_color(row, lv_color_hex(
|
||||
i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(row, lv_color_hex(Theme::BORDER), 0);
|
||||
lv_obj_set_style_border_width(row, 1, 0);
|
||||
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
||||
lv_obj_set_style_pad_all(row, 0, 0);
|
||||
lv_obj_set_style_radius(row, 0, 0);
|
||||
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
if (count == 0) {
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// Green dot for unread
|
||||
if (ci.hasUnread) {
|
||||
lv_obj_t* dot = lv_obj_create(row);
|
||||
lv_obj_set_size(dot, 6, 6);
|
||||
lv_obj_set_style_radius(dot, 3, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(dot, 0, 0);
|
||||
lv_obj_set_style_pad_all(dot, 0, 0);
|
||||
lv_obj_set_pos(dot, 5, 8);
|
||||
// Compute viewport centered on selection
|
||||
int halfPool = ROW_POOL_SIZE / 2;
|
||||
_viewportStart = _selectedIdx - halfPool;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
if (_viewportStart + ROW_POOL_SIZE > count) {
|
||||
_viewportStart = count - ROW_POOL_SIZE;
|
||||
if (_viewportStart < 0) _viewportStart = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ROW_POOL_SIZE; i++) {
|
||||
int convIdx = _viewportStart + i;
|
||||
if (convIdx >= count) {
|
||||
lv_obj_add_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Peer name (line 1)
|
||||
std::string peerName;
|
||||
if (_am) peerName = _am->lookupName(ci.peerHex);
|
||||
std::string displayName = !peerName.empty() ?
|
||||
peerName.substr(0, 15) : ci.peerHex.substr(0, 12);
|
||||
lv_obj_clear_flag(_poolRows[i], LV_OBJ_FLAG_HIDDEN);
|
||||
const auto& ci = _sortedConvs[convIdx];
|
||||
bool isSelected = (convIdx == _selectedIdx);
|
||||
|
||||
lv_obj_t* nameLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(nameLbl, nameFont, 0);
|
||||
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(nameLbl, displayName.c_str());
|
||||
lv_obj_align(nameLbl, LV_ALIGN_TOP_LEFT, 14, 2);
|
||||
// Selection highlight
|
||||
lv_obj_set_style_bg_color(_poolRows[i], lv_color_hex(
|
||||
isSelected ? Theme::SELECTION_BG : Theme::BG), 0);
|
||||
|
||||
// Timestamp (line 1, right)
|
||||
// Unread dot
|
||||
if (ci.hasUnread) {
|
||||
lv_obj_clear_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(_poolDots[i], LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// Name
|
||||
lv_label_set_text(_poolNameLabels[i], ci.displayName.c_str());
|
||||
|
||||
// Time
|
||||
if (ci.lastTs > 1700000000) {
|
||||
time_t t = (time_t)ci.lastTs;
|
||||
struct tm* tm = localtime(&t);
|
||||
if (tm) {
|
||||
char timeBuf[8];
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tm->tm_hour, tm->tm_min);
|
||||
lv_obj_t* timeLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(timeLbl, &lv_font_montserrat_10, 0);
|
||||
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(timeLbl, timeBuf);
|
||||
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -4, 4);
|
||||
lv_label_set_text(_poolTimeLabels[i], timeBuf);
|
||||
} else {
|
||||
lv_label_set_text(_poolTimeLabels[i], "");
|
||||
}
|
||||
} else {
|
||||
lv_label_set_text(_poolTimeLabels[i], "");
|
||||
}
|
||||
|
||||
// Preview (line 2)
|
||||
if (!ci.preview.empty()) {
|
||||
lv_obj_t* prevLbl = lv_label_create(row);
|
||||
lv_obj_set_style_text_font(prevLbl, smallFont, 0);
|
||||
lv_obj_set_style_text_color(prevLbl, lv_color_hex(Theme::MUTED), 0);
|
||||
lv_label_set_text(prevLbl, ci.preview.c_str());
|
||||
lv_obj_align(prevLbl, LV_ALIGN_BOTTOM_LEFT, 14, -4);
|
||||
}
|
||||
|
||||
_rows.push_back(row);
|
||||
// Preview
|
||||
lv_label_set_text(_poolPreviewLabels[i], ci.preview.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +318,7 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
if (_selectedIdx > 0) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx--;
|
||||
updateSelection(prev, _selectedIdx);
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -284,7 +326,7 @@ bool LvMessagesScreen::handleKey(const KeyEvent& event) {
|
||||
if (_selectedIdx < count - 1) {
|
||||
int prev = _selectedIdx;
|
||||
_selectedIdx++;
|
||||
updateSelection(prev, _selectedIdx);
|
||||
syncVisibleRows();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
|
||||
private:
|
||||
void rebuildList();
|
||||
void syncVisibleRows();
|
||||
void updateSelection(int oldIdx, int newIdx);
|
||||
|
||||
LXMFManager* _lxmf = nullptr;
|
||||
@@ -36,6 +37,7 @@ private:
|
||||
int _lastConvCount = -1;
|
||||
int _lastUnreadTotal = 0;
|
||||
int _selectedIdx = 0;
|
||||
int _viewportStart = 0;
|
||||
std::vector<std::string> _sortedPeers;
|
||||
enum LongPressState { LP_NONE, LP_MENU, LP_CONFIRM_DELETE };
|
||||
LongPressState _lpState = LP_NONE;
|
||||
@@ -44,4 +46,22 @@ private:
|
||||
lv_obj_t* _list = nullptr;
|
||||
lv_obj_t* _lblEmpty = nullptr;
|
||||
std::vector<lv_obj_t*> _rows;
|
||||
|
||||
// Object pool for conversation rows
|
||||
static constexpr int ROW_POOL_SIZE = 10;
|
||||
lv_obj_t* _poolRows[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolNameLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolPreviewLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolTimeLabels[ROW_POOL_SIZE] = {};
|
||||
lv_obj_t* _poolDots[ROW_POOL_SIZE] = {};
|
||||
|
||||
// Cached sorted conversation data
|
||||
struct ConvInfo {
|
||||
std::string peerHex;
|
||||
double lastTs = 0;
|
||||
std::string preview;
|
||||
std::string displayName;
|
||||
bool hasUnread = false;
|
||||
};
|
||||
std::vector<ConvInfo> _sortedConvs;
|
||||
};
|
||||
@@ -165,6 +165,39 @@ void LvSettingsScreen::buildItems() {
|
||||
_items.push_back(newId);
|
||||
idx++;
|
||||
}
|
||||
{
|
||||
SettingItem devModeItem;
|
||||
devModeItem.label = "Developer Mode";
|
||||
devModeItem.type = SettingType::ACTION;
|
||||
devModeItem.formatter = [&s](int) { return s.devMode ? String("ON") : String("OFF"); };
|
||||
devModeItem.action = [this, &s]() {
|
||||
if (s.devMode) {
|
||||
// Already on — just turn it off
|
||||
s.devMode = false;
|
||||
_confirmingDevMode = false;
|
||||
applyAndSave();
|
||||
buildItems();
|
||||
enterCategory(_categoryIdx);
|
||||
if (_ui) _ui->lvStatusBar().showToast("Developer mode OFF", 1200);
|
||||
return;
|
||||
}
|
||||
if (!_confirmingDevMode) {
|
||||
_confirmingDevMode = true;
|
||||
if (_ui) _ui->lvStatusBar().showToast("WARNING: KNOW YOUR LAWS! Enter=Enable", 5000);
|
||||
rebuildItemList();
|
||||
return;
|
||||
}
|
||||
// Confirmed
|
||||
_confirmingDevMode = false;
|
||||
s.devMode = true;
|
||||
applyAndSave();
|
||||
buildItems();
|
||||
enterCategory(_categoryIdx);
|
||||
if (_ui) _ui->lvStatusBar().showToast("Developer mode ON", 1200);
|
||||
};
|
||||
_items.push_back(devModeItem);
|
||||
idx++;
|
||||
}
|
||||
_categories.push_back({"Device", devStart, idx - devStart,
|
||||
[&s]() { return s.displayName.isEmpty() ? String("(unnamed)") : s.displayName; }});
|
||||
|
||||
@@ -202,48 +235,47 @@ void LvSettingsScreen::buildItems() {
|
||||
_items.push_back(presetItem);
|
||||
idx++;
|
||||
}
|
||||
_items.push_back({"Frequency", SettingType::ENUM_CHOICE,
|
||||
[&s]() {
|
||||
if (s.loraFrequency <= 868000000) return 0;
|
||||
if (s.loraFrequency <= 906000000) return 1;
|
||||
if (s.loraFrequency <= 915000000) return 2;
|
||||
return 3;
|
||||
},
|
||||
[&s](int v) {
|
||||
static const uint32_t freqs[] = {868000000, 906000000, 915000000, 923000000};
|
||||
s.loraFrequency = freqs[constrain(v, 0, 3)];
|
||||
},
|
||||
nullptr, 0, 3, 1, {"868 MHz", "906 MHz", "915 MHz", "923 MHz"}});
|
||||
idx++;
|
||||
_items.push_back({"TX Power", SettingType::INTEGER,
|
||||
[&s]() { return s.loraTxPower; }, [&s](int v) { s.loraTxPower = v; },
|
||||
[](int v) { return String(v) + " dBm"; }, -9, 22, 1});
|
||||
idx++;
|
||||
_items.push_back({"Spread Factor", SettingType::INTEGER,
|
||||
[&s]() { return s.loraSF; }, [&s](int v) { s.loraSF = v; },
|
||||
[](int v) { return String("SF") + String(v); }, 5, 12, 1});
|
||||
idx++;
|
||||
_items.push_back({"Bandwidth", SettingType::ENUM_CHOICE,
|
||||
[&s]() {
|
||||
if (s.loraBW <= 62500) return 0;
|
||||
if (s.loraBW <= 125000) return 1;
|
||||
if (s.loraBW <= 250000) return 2;
|
||||
return 3;
|
||||
},
|
||||
[&s](int v) {
|
||||
static const uint32_t bws[] = {62500, 125000, 250000, 500000};
|
||||
s.loraBW = bws[constrain(v, 0, 3)];
|
||||
},
|
||||
nullptr, 0, 3, 1, {"62.5k", "125k", "250k", "500k"}});
|
||||
idx++;
|
||||
_items.push_back({"Coding Rate", SettingType::INTEGER,
|
||||
[&s]() { return s.loraCR; }, [&s](int v) { s.loraCR = v; },
|
||||
[](int v) { return String("4/") + String(v); }, 5, 8, 1});
|
||||
idx++;
|
||||
_items.push_back({"Preamble", SettingType::INTEGER,
|
||||
[&s]() { return (int)s.loraPreamble; }, [&s](int v) { s.loraPreamble = v; },
|
||||
[](int v) { return String(v); }, 6, 65, 1});
|
||||
idx++;
|
||||
// Custom radio parameters — only visible in Developer Mode
|
||||
if (s.devMode) {
|
||||
_items.push_back({"Frequency", SettingType::INTEGER,
|
||||
[&s]() { return (int)(s.loraFrequency / 1000); },
|
||||
[&s](int v) { s.loraFrequency = (uint32_t)v * 1000; },
|
||||
[](int v) -> String {
|
||||
char buf[16]; snprintf(buf, sizeof(buf), "%d.%03d MHz", v / 1000, v % 1000);
|
||||
return String(buf);
|
||||
},
|
||||
137000, 1020000, 125});
|
||||
idx++;
|
||||
_items.push_back({"TX Power", SettingType::INTEGER,
|
||||
[&s]() { return s.loraTxPower; }, [&s](int v) { s.loraTxPower = v; },
|
||||
[](int v) { return String(v) + " dBm"; }, -9, 22, 1});
|
||||
idx++;
|
||||
_items.push_back({"Spread Factor", SettingType::INTEGER,
|
||||
[&s]() { return s.loraSF; }, [&s](int v) { s.loraSF = v; },
|
||||
[](int v) { return String("SF") + String(v); }, 5, 12, 1});
|
||||
idx++;
|
||||
_items.push_back({"Bandwidth", SettingType::ENUM_CHOICE,
|
||||
[&s]() {
|
||||
if (s.loraBW <= 62500) return 0;
|
||||
if (s.loraBW <= 125000) return 1;
|
||||
if (s.loraBW <= 250000) return 2;
|
||||
return 3;
|
||||
},
|
||||
[&s](int v) {
|
||||
static const uint32_t bws[] = {62500, 125000, 250000, 500000};
|
||||
s.loraBW = bws[constrain(v, 0, 3)];
|
||||
},
|
||||
nullptr, 0, 3, 1, {"62.5k", "125k", "250k", "500k"}});
|
||||
idx++;
|
||||
_items.push_back({"Coding Rate", SettingType::INTEGER,
|
||||
[&s]() { return s.loraCR; }, [&s](int v) { s.loraCR = v; },
|
||||
[](int v) { return String("4/") + String(v); }, 5, 8, 1});
|
||||
idx++;
|
||||
_items.push_back({"Preamble", SettingType::INTEGER,
|
||||
[&s]() { return (int)s.loraPreamble; }, [&s](int v) { s.loraPreamble = v; },
|
||||
[](int v) { return String(v); }, 6, 65, 1});
|
||||
idx++;
|
||||
}
|
||||
_categories.push_back({"Radio", radioStart, idx - radioStart,
|
||||
[this]() { int p = detectPreset(); return (p >= 0) ? String(LV_PRESETS[p].name) : String("Custom"); }});
|
||||
|
||||
@@ -590,6 +622,7 @@ void LvSettingsScreen::onEnter() {
|
||||
_editing = false;
|
||||
_textEditing = false;
|
||||
_confirmingReset = false;
|
||||
_confirmingDevMode = false;
|
||||
rebuildCategoryList();
|
||||
}
|
||||
|
||||
@@ -901,6 +934,7 @@ void LvSettingsScreen::exitToCategories() {
|
||||
_editing = false;
|
||||
_textEditing = false;
|
||||
_confirmingReset = false;
|
||||
_confirmingDevMode = false;
|
||||
rebuildCategoryList();
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ private:
|
||||
bool _textEditing = false;
|
||||
String _editText;
|
||||
bool _confirmingReset = false;
|
||||
bool _confirmingDevMode = false;
|
||||
|
||||
// WiFi picker
|
||||
std::vector<WiFiInterface::ScanResult> _wifiResults;
|
||||
|
||||
Reference in new issue
Block a user