From 4687bc9d380fe3f6a76a29cf2dc85060eb0db0ac Mon Sep 17 00:00:00 2001 From: DeFiDude <59237470+DeFiDude@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:22:16 -0600 Subject: [PATCH] Remove legacy dead code: pre-LVGL screens and unused stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete 24 files (2487 lines): - 18 legacy screen files (BootScreen, HomeScreen, NodesScreen, MessagesScreen, MessageView, SettingsScreen, HelpOverlay, NameInputScreen, MapScreen) replaced by Lv-prefixed LVGL versions - PowerManager (superseded by hal/Power) - hal/Audio (ES7210 stub, working audio is audio/AudioNotify) - hal/GPS (stub, GPS deprioritized) Move shared types (SettingType, SettingItem, SettingsCategory) from deleted SettingsScreen.h into LvSettingsScreen.h. Clean main.cpp: remove legacy includes, instances, and tab mapping. Note: StatusBar and TabBar retained — still used by UIManager/main.cpp. --- src/hal/Audio.cpp | 13 - src/hal/Audio.h | 15 - src/hal/GPS.cpp | 13 - src/hal/GPS.h | 19 - src/main.cpp | 23 - src/power/PowerManager.cpp | 59 -- src/power/PowerManager.h | 35 - src/ui/screens/BootScreen.cpp | 55 -- src/ui/screens/BootScreen.h | 15 - src/ui/screens/HelpOverlay.cpp | 52 -- src/ui/screens/HelpOverlay.h | 17 - src/ui/screens/HomeScreen.cpp | 81 -- src/ui/screens/HomeScreen.h | 30 - src/ui/screens/LvSettingsScreen.h | 39 +- src/ui/screens/MapScreen.cpp | 25 - src/ui/screens/MapScreen.h | 9 - src/ui/screens/MessageView.cpp | 329 --------- src/ui/screens/MessageView.h | 63 -- src/ui/screens/MessagesScreen.cpp | 103 --- src/ui/screens/MessagesScreen.h | 31 - src/ui/screens/NameInputScreen.cpp | 102 --- src/ui/screens/NameInputScreen.h | 20 - src/ui/screens/NodesScreen.cpp | 104 --- src/ui/screens/NodesScreen.h | 28 - src/ui/screens/SettingsScreen.cpp | 1096 ---------------------------- src/ui/screens/SettingsScreen.h | 142 ---- 26 files changed, 37 insertions(+), 2481 deletions(-) delete mode 100644 src/hal/Audio.cpp delete mode 100644 src/hal/Audio.h delete mode 100644 src/hal/GPS.cpp delete mode 100644 src/hal/GPS.h delete mode 100644 src/power/PowerManager.cpp delete mode 100644 src/power/PowerManager.h delete mode 100644 src/ui/screens/BootScreen.cpp delete mode 100644 src/ui/screens/BootScreen.h delete mode 100644 src/ui/screens/HelpOverlay.cpp delete mode 100644 src/ui/screens/HelpOverlay.h delete mode 100644 src/ui/screens/HomeScreen.cpp delete mode 100644 src/ui/screens/HomeScreen.h delete mode 100644 src/ui/screens/MapScreen.cpp delete mode 100644 src/ui/screens/MapScreen.h delete mode 100644 src/ui/screens/MessageView.cpp delete mode 100644 src/ui/screens/MessageView.h delete mode 100644 src/ui/screens/MessagesScreen.cpp delete mode 100644 src/ui/screens/MessagesScreen.h delete mode 100644 src/ui/screens/NameInputScreen.cpp delete mode 100644 src/ui/screens/NameInputScreen.h delete mode 100644 src/ui/screens/NodesScreen.cpp delete mode 100644 src/ui/screens/NodesScreen.h delete mode 100644 src/ui/screens/SettingsScreen.cpp delete mode 100644 src/ui/screens/SettingsScreen.h diff --git a/src/hal/Audio.cpp b/src/hal/Audio.cpp deleted file mode 100644 index d577e47..0000000 --- a/src/hal/Audio.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "Audio.h" -#include "config/BoardConfig.h" - -bool Audio::begin() { - // TODO: Initialize ES7210 codec via I2S - // Pins: I2S_WS=5, I2S_DOUT=6, I2S_BCK=7, I2S_DIN=14, I2S_SCK=47, I2S_MCLK=48 - Serial.println("[AUDIO] ES7210 init (stub)"); - return true; -} - -void Audio::setVolume(uint8_t vol) { - _volume = vol; -} diff --git a/src/hal/Audio.h b/src/hal/Audio.h deleted file mode 100644 index c7d0fc9..0000000 --- a/src/hal/Audio.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include - -// ES7210 I2S audio codec driver for T-Deck Plus -// Stub — full I2S implementation in Phase 6 -class Audio { -public: - bool begin(); - void setVolume(uint8_t vol); - uint8_t volume() const { return _volume; } - -private: - uint8_t _volume = 80; -}; diff --git a/src/hal/GPS.cpp b/src/hal/GPS.cpp deleted file mode 100644 index 12c0558..0000000 --- a/src/hal/GPS.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "GPS.h" -#include "config/BoardConfig.h" - -bool GPS::begin() { - // GPS is deprioritized — stub only - // UBlox MIA-M10Q on UART: TX=43, RX=44, 115200 baud - Serial.println("[GPS] Disabled (deprioritized)"); - return false; -} - -void GPS::loop() { - // No-op -} diff --git a/src/hal/GPS.h b/src/hal/GPS.h deleted file mode 100644 index cc8ad49..0000000 --- a/src/hal/GPS.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -// UBlox MIA-M10Q GPS driver for T-Deck Plus -// Stub — GPS is deprioritized per plan -class GPS { -public: - bool begin(); - void loop(); - - bool hasFix() const { return false; } - double latitude() const { return 0; } - double longitude() const { return 0; } - int satellites() const { return 0; } - -private: - bool _enabled = false; -}; diff --git a/src/main.cpp b/src/main.cpp index 58e96fe..b5b879a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,15 +21,6 @@ #include "ui/UIManager.h" #include "ui/LvTabBar.h" #include "ui/LvInput.h" -#include "ui/screens/BootScreen.h" -#include "ui/screens/HomeScreen.h" -#include "ui/screens/NodesScreen.h" -#include "ui/screens/MessagesScreen.h" -#include "ui/screens/MessageView.h" -#include "ui/screens/SettingsScreen.h" -#include "ui/screens/HelpOverlay.h" -// MapScreen removed -#include "ui/screens/NameInputScreen.h" #include "ui/screens/LvBootScreen.h" #include "ui/screens/LvHomeScreen.h" #include "ui/screens/LvNodesScreen.h" @@ -98,17 +89,6 @@ Power powerMgr; AudioNotify audio; IdentityManager identityMgr; -// --- Legacy Screens (kept for fallback during migration) --- -BootScreen bootScreen; -HomeScreen homeScreen; -NodesScreen nodesScreen; -MessagesScreen messagesScreen; -MessageView messageView; -SettingsScreen settingsScreen; -HelpOverlay helpOverlay; -// MapScreen removed -NameInputScreen nameInputScreen; - // --- LVGL Screens --- LvBootScreen lvBootScreen; LvHomeScreen lvHomeScreen; @@ -124,9 +104,6 @@ LvNameInputScreen lvNameInputScreen; // Tab-screen mapping (4 tabs) — LVGL versions LvScreen* lvTabScreens[LvTabBar::TAB_COUNT] = {}; -// Legacy tab mapping (kept for reference) -Screen* tabScreens[LvTabBar::TAB_COUNT] = {}; - // --- State --- bool radioOnline = false; bool bootComplete = false; diff --git a/src/power/PowerManager.cpp b/src/power/PowerManager.cpp deleted file mode 100644 index eb81066..0000000 --- a/src/power/PowerManager.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "PowerManager.h" -#include "hal/Display.h" - -void PowerManager::begin(Display* display) { - _display = display; - _lastActivity = millis(); - _state = ACTIVE; - if (_display) _display->setBrightness(_fullBrightness); -} - -void PowerManager::activity() { - _lastActivity = millis(); - if (_state != ACTIVE) { - setState(ACTIVE); - } -} - -void PowerManager::loop() { - unsigned long elapsed = millis() - _lastActivity; - - switch (_state) { - case ACTIVE: - if (_offTimeout > 0 && elapsed >= _offTimeout) { - setState(SCREEN_OFF); - } else if (_dimTimeout > 0 && elapsed >= _dimTimeout) { - setState(DIMMED); - } - break; - - case DIMMED: - if (_offTimeout > 0 && elapsed >= _offTimeout) { - setState(SCREEN_OFF); - } - break; - - case SCREEN_OFF: - // Stay off until activity() - break; - } -} - -void PowerManager::setState(State newState) { - if (newState == _state) return; - _state = newState; - - if (!_display) return; - - switch (_state) { - case ACTIVE: - _display->setBrightness(_fullBrightness); - break; - case DIMMED: - _display->setBrightness(DIM_BRIGHTNESS); - break; - case SCREEN_OFF: - _display->setBrightness(0); - break; - } -} diff --git a/src/power/PowerManager.h b/src/power/PowerManager.h deleted file mode 100644 index 7a40536..0000000 --- a/src/power/PowerManager.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include - -class Display; - -class PowerManager { -public: - enum State { ACTIVE, DIMMED, SCREEN_OFF }; - - void begin(Display* display); - void loop(); - - // Call on any user activity (keypress, touch, trackball) - void activity(); - - // Configuration (seconds) - void setDimTimeout(uint16_t seconds) { _dimTimeout = seconds * 1000UL; } - void setOffTimeout(uint16_t seconds) { _offTimeout = seconds * 1000UL; } - void setBrightness(uint8_t brightness) { _fullBrightness = brightness; } - - State state() const { return _state; } - bool isScreenOn() const { return _state != SCREEN_OFF; } - -private: - void setState(State newState); - - Display* _display = nullptr; - State _state = ACTIVE; - unsigned long _lastActivity = 0; - unsigned long _dimTimeout = 30000; // 30s - unsigned long _offTimeout = 60000; // 60s - uint8_t _fullBrightness = 255; - static constexpr uint8_t DIM_BRIGHTNESS = 64; -}; diff --git a/src/ui/screens/BootScreen.cpp b/src/ui/screens/BootScreen.cpp deleted file mode 100644 index 4f9206d..0000000 --- a/src/ui/screens/BootScreen.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "BootScreen.h" -#include "ui/Theme.h" -#include "config/Config.h" -#include "hal/Display.h" - -void BootScreen::draw(LGFX_TDeck& gfx) { - int cx = Theme::SCREEN_W / 2; - - // Title - gfx.setTextSize(2); - gfx.setTextColor(Theme::PRIMARY, Theme::BG); - const char* title = "RATDECK"; - int tw = strlen(title) * 12; // 12px per char at size 2 - gfx.setCursor(cx - tw / 2, 70); - gfx.print(title); - - // Version - gfx.setTextSize(1); - gfx.setTextColor(Theme::SECONDARY, Theme::BG); - char ver[32]; - snprintf(ver, sizeof(ver), "v%s", RATDECK_VERSION_STRING); - int vw = strlen(ver) * 6; - gfx.setCursor(cx - vw / 2, 95); - gfx.print(ver); - - // Progress bar background - int barX = cx - 100; - int barY = 120; - int barW = 200; - int barH = 10; - gfx.fillRect(barX, barY, barW, barH, Theme::BORDER); - - // Progress bar fill - int fillW = (int)(barW * _progress); - if (fillW > 0) { - gfx.fillRect(barX, barY, fillW, barH, Theme::PRIMARY); - } - - // Status text - gfx.setTextSize(1); - gfx.setTextColor(Theme::SECONDARY, Theme::BG); - int sw = strlen(_status) * 6; - gfx.setCursor(cx - sw / 2, 145); - gfx.print(_status); -} - -void BootScreen::setProgress(float progress, const char* status) { - _progress = progress; - strncpy(_status, status, sizeof(_status) - 1); - _status[sizeof(_status) - 1] = '\0'; - markDirty(); - - // Force immediate render during boot (no main loop running yet) - // Caller (main.cpp) must call ui.render() after this -} diff --git a/src/ui/screens/BootScreen.h b/src/ui/screens/BootScreen.h deleted file mode 100644 index 21d9ef6..0000000 --- a/src/ui/screens/BootScreen.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" - -class BootScreen : public Screen { -public: - void setProgress(float progress, const char* status); - - const char* title() const override { return "Boot"; } - void draw(LGFX_TDeck& gfx) override; - -private: - float _progress = 0; - char _status[64] = "Starting..."; -}; diff --git a/src/ui/screens/HelpOverlay.cpp b/src/ui/screens/HelpOverlay.cpp deleted file mode 100644 index 31af1bb..0000000 --- a/src/ui/screens/HelpOverlay.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "HelpOverlay.h" -#include "ui/Theme.h" -#include "hal/Display.h" - -void HelpOverlay::draw(LGFX_TDeck& gfx) { - if (!_visible) return; - - // Semi-transparent overlay box - int bx = 20, by = Theme::CONTENT_Y + 10; - int bw = Theme::CONTENT_W - 40; - int bh = Theme::CONTENT_H - 20; - - gfx.fillRect(bx, by, bw, bh, Theme::BG); - gfx.drawRect(bx, by, bw, bh, Theme::ACCENT); - - gfx.setTextSize(1); - int x = bx + 8; - int y = by + 8; - int lineH = 12; - - // Title - gfx.setTextColor(Theme::ACCENT, Theme::BG); - gfx.setCursor(x, y); - gfx.print("HOTKEYS"); - y += lineH + 4; - - const char* lines[] = { - "Ctrl+H This help", - "Ctrl+M Messages", - "Ctrl+N New message", - "Ctrl+S Settings", - "Ctrl+A Force announce", - "Ctrl+D Diagnostics (serial)", - "Ctrl+T Radio test TX", - "Ctrl+R RSSI monitor", - ", / Cycle tabs", - "; . Scroll up/down", - "Esc Back", - }; - - gfx.setTextColor(Theme::PRIMARY, Theme::BG); - for (const char* line : lines) { - gfx.setCursor(x, y); - gfx.print(line); - y += lineH; - } -} - -bool HelpOverlay::handleKey(const KeyEvent& event) { - _visible = false; - return true; -} diff --git a/src/ui/screens/HelpOverlay.h b/src/ui/screens/HelpOverlay.h deleted file mode 100644 index a3c23da..0000000 --- a/src/ui/screens/HelpOverlay.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" - -class HelpOverlay : public Screen { -public: - bool handleKey(const KeyEvent& event) override; - - void toggle() { _visible = !_visible; } - bool isVisible() const { return _visible; } - - const char* title() const override { return "Help"; } - void draw(LGFX_TDeck& gfx) override; - -private: - bool _visible = false; -}; diff --git a/src/ui/screens/HomeScreen.cpp b/src/ui/screens/HomeScreen.cpp deleted file mode 100644 index a11df0f..0000000 --- a/src/ui/screens/HomeScreen.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "HomeScreen.h" -#include "ui/Theme.h" -#include "hal/Display.h" -#include "reticulum/ReticulumManager.h" -#include "radio/SX1262.h" -#include "config/UserConfig.h" -#include -#include - -void HomeScreen::update() { - // Only redraw when minute changes or heap changes significantly - unsigned long upMins = millis() / 60000; - uint32_t heap = ESP.getFreeHeap() / 1024; - if (upMins != _lastUptime || heap != _lastHeap) { - _lastUptime = upMins; - _lastHeap = heap; - markDirty(); - } -} - -bool HomeScreen::handleKey(const KeyEvent& event) { - if (event.enter || event.character == '\n' || event.character == '\r') { - if (_announceCb) _announceCb(); - return true; - } - return false; -} - -void HomeScreen::draw(LGFX_TDeck& gfx) { - int x = 4; - int y = Theme::CONTENT_Y + 4; - int lineH = 12; - - gfx.setTextSize(1); - - auto drawLine = [&](uint32_t col, const char* fmt, ...) { - char buf[80]; - va_list args; - va_start(args, fmt); - vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - gfx.setTextColor(col, Theme::BG); - gfx.setCursor(x, y); - gfx.print(buf); - y += lineH; - }; - - if (_rns) { - drawLine(Theme::PRIMARY, "LXMF: %s", _rns->destinationHashStr().c_str()); - drawLine(Theme::PRIMARY, "Transport: %s", - _rns->isTransportActive() ? "ACTIVE" : "OFFLINE"); - drawLine(Theme::PRIMARY, "Paths: %d Links: %d", - (int)_rns->pathCount(), (int)_rns->linkCount()); - } else { - drawLine(Theme::MUTED, "Identity: ---"); - drawLine(Theme::MUTED, "Transport: OFFLINE"); - drawLine(Theme::MUTED, "Paths: 0 Links: 0"); - } - - if (_radio && _radio->isRadioOnline()) { - drawLine(Theme::PRIMARY, "LoRa: SF%d BW%luk %ddBm", - _radio->getSpreadingFactor(), - (unsigned long)(_radio->getSignalBandwidth() / 1000), - _radio->getTxPower()); - } else { - drawLine(Theme::ERROR_CLR, "Radio: OFFLINE"); - } - - drawLine(Theme::PRIMARY, "Heap: %lukB free", - (unsigned long)(ESP.getFreeHeap() / 1024)); - - drawLine(Theme::PRIMARY, "PSRAM: %lukB free", - (unsigned long)(ESP.getFreePsram() / 1024)); - - unsigned long mins = millis() / 60000; - if (mins >= 60) { - drawLine(Theme::PRIMARY, "Uptime: %luh %lum", mins / 60, mins % 60); - } else { - drawLine(Theme::PRIMARY, "Uptime: %lum", mins); - } -} diff --git a/src/ui/screens/HomeScreen.h b/src/ui/screens/HomeScreen.h deleted file mode 100644 index ff1616d..0000000 --- a/src/ui/screens/HomeScreen.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include - -class ReticulumManager; -class SX1262; -class UserConfig; - -class HomeScreen : public Screen { -public: - void update() override; - bool handleKey(const KeyEvent& event) override; - - void setReticulumManager(ReticulumManager* rns) { _rns = rns; } - void setRadio(SX1262* radio) { _radio = radio; } - void setUserConfig(UserConfig* cfg) { _cfg = cfg; } - void setAnnounceCallback(std::function cb) { _announceCb = cb; } - - const char* title() const override { return "Home"; } - void draw(LGFX_TDeck& gfx) override; - -private: - ReticulumManager* _rns = nullptr; - SX1262* _radio = nullptr; - UserConfig* _cfg = nullptr; - unsigned long _lastUptime = 0; - uint32_t _lastHeap = 0; - std::function _announceCb; -}; diff --git a/src/ui/screens/LvSettingsScreen.h b/src/ui/screens/LvSettingsScreen.h index 8c251ae..de78da6 100644 --- a/src/ui/screens/LvSettingsScreen.h +++ b/src/ui/screens/LvSettingsScreen.h @@ -16,8 +16,43 @@ class TCPClientInterface; class ReticulumManager; class IdentityManager; -// Reuse existing SettingType, SettingItem, SettingsCategory from SettingsScreen.h -#include "SettingsScreen.h" +enum class SettingType : uint8_t { + READONLY, + INTEGER, + TOGGLE, + ENUM_CHOICE, + ACTION, + TEXT_INPUT +}; + +enum class SettingsView : uint8_t { + CATEGORY_LIST, + ITEM_LIST, + WIFI_PICKER +}; + +struct SettingItem { + const char* label; + SettingType type; + std::function getter; + std::function setter; + std::function formatter; + int minVal = 0; + int maxVal = 1; + int step = 1; + std::vector enumLabels; + std::function action; + std::function textGetter; + std::function textSetter; + int maxTextLen = 16; +}; + +struct SettingsCategory { + const char* name; + int startIdx; + int count; + std::function summary; +}; class LvSettingsScreen : public LvScreen { public: diff --git a/src/ui/screens/MapScreen.cpp b/src/ui/screens/MapScreen.cpp deleted file mode 100644 index 5fca616..0000000 --- a/src/ui/screens/MapScreen.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "MapScreen.h" -#include "ui/Theme.h" -#include "hal/Display.h" - -void MapScreen::draw(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - gfx.setTextColor(Theme::MUTED, Theme::BG); - - const char* lines[] = { - "Map", - "", - "Coming soon", - "", - "Node topology view", - "will appear here" - }; - - int y = Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 36; - for (const char* line : lines) { - int tw = strlen(line) * 6; - gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, y); - gfx.print(line); - y += 12; - } -} diff --git a/src/ui/screens/MapScreen.h b/src/ui/screens/MapScreen.h deleted file mode 100644 index cdf5047..0000000 --- a/src/ui/screens/MapScreen.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" - -class MapScreen : public Screen { -public: - const char* title() const override { return "Map"; } - void draw(LGFX_TDeck& gfx) override; -}; diff --git a/src/ui/screens/MessageView.cpp b/src/ui/screens/MessageView.cpp deleted file mode 100644 index e00a205..0000000 --- a/src/ui/screens/MessageView.cpp +++ /dev/null @@ -1,329 +0,0 @@ -#include "MessageView.h" -#include "ui/Theme.h" -#include "hal/Display.h" -#include "reticulum/LXMFManager.h" -#include "reticulum/AnnounceManager.h" -#include -#include - -void MessageView::onEnter() { - if (_lxmf) _lxmf->markRead(_peerHex); - _lastMsgCount = -1; - _scrollPixels = 0; - _lastRefreshMs = 0; - refreshMessages(); - rebuildLayout(); - markDirty(); -} - -void MessageView::onExit() { - _inputText.clear(); - _cachedMsgs.clear(); - _layout.clear(); -} - -void MessageView::refreshMessages() { - if (!_lxmf) return; - _cachedMsgs = _lxmf->getMessages(_peerHex); - _lastRefreshMs = millis(); -} - -void MessageView::update() { - if (!_lxmf) return; - unsigned long now = millis(); - if (now - _lastRefreshMs >= REFRESH_INTERVAL_MS) { - int oldCount = (int)_cachedMsgs.size(); - refreshMessages(); - if ((int)_cachedMsgs.size() != oldCount) { - _lastMsgCount = (int)_cachedMsgs.size(); - rebuildLayout(); - _scrollPixels = 0; // snap to bottom on new message - markDirty(); - } - } -} - -// ============================================================================= -// Word wrap + layout -// ============================================================================= -std::vector MessageView::wordWrap(const std::string& text, int maxChars) { - std::vector lines; - if (text.empty()) { lines.push_back(""); return lines; } - - size_t pos = 0; - while (pos < text.size()) { - size_t remaining = text.size() - pos; - if ((int)remaining <= maxChars) { - lines.push_back(text.substr(pos)); - break; - } - // Find last space within maxChars - int breakAt = maxChars; - for (int i = maxChars; i > maxChars / 2; i--) { - if (text[pos + i] == ' ') { breakAt = i; break; } - } - lines.push_back(text.substr(pos, breakAt)); - pos += breakAt; - // Skip the space we broke at - if (pos < text.size() && text[pos] == ' ') pos++; - } - return lines; -} - -std::string MessageView::formatTime(double timestamp) { - if (timestamp < 1700000000) return ""; - time_t t = (time_t)timestamp; - struct tm* tm = localtime(&t); - if (!tm) return ""; - char buf[8]; - snprintf(buf, sizeof(buf), "%02d:%02d", tm->tm_hour, tm->tm_min); - return buf; -} - -void MessageView::rebuildLayout() { - _layout.clear(); - _totalContentH = 0; - - for (int i = 0; i < (int)_cachedMsgs.size(); i++) { - const auto& msg = _cachedMsgs[i]; - MsgLayout ml; - ml.msgIdx = i; - ml.incoming = msg.incoming; - ml.lines = wordWrap(msg.content, MAX_BUBBLE_CHARS); - ml.timeStr = formatTime(msg.timestamp); - - int textH = (int)ml.lines.size() * LINE_H; - ml.totalHeight = textH + BUBBLE_PAD * 2 + BUBBLE_GAP; - // Add time line height if we have a timestamp - if (!ml.timeStr.empty()) ml.totalHeight += 8; - - _totalContentH += ml.totalHeight; - _layout.push_back(ml); - } -} - -// ============================================================================= -// Drawing -// ============================================================================= -void MessageView::draw(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - - // Header — peer name - gfx.setTextColor(Theme::ACCENT, Theme::BG); - gfx.setCursor(4, Theme::CONTENT_Y + 2); - std::string headerName; - if (_am) { - const DiscoveredNode* node = _am->findNodeByHex(_peerHex); - if (node && !node->name.empty()) headerName = node->name; - } - if (headerName.empty()) headerName = _peerHex.substr(0, 12); - char header[48]; - snprintf(header, sizeof(header), "< %s", headerName.c_str()); - gfx.print(header); - - int headerBottom = Theme::CONTENT_Y + 12; - gfx.drawFastHLine(0, headerBottom, Theme::SCREEN_W, Theme::BORDER); - - // Input area - int inputY = Theme::SCREEN_H - Theme::TAB_BAR_H - 16; - gfx.drawFastHLine(0, inputY - 2, Theme::SCREEN_W, Theme::BORDER); - gfx.fillRect(0, inputY, Theme::SCREEN_W, 16, Theme::BG); - - gfx.setCursor(4, inputY + 4); - if (_inputText.empty()) { - gfx.setTextColor(Theme::MUTED, Theme::BG); - gfx.print("Type message..."); - } else { - gfx.setTextColor(Theme::PRIMARY, Theme::BG); - int maxShow = (Theme::SCREEN_W - 44) / CHAR_W; - if ((int)_inputText.length() > maxShow) { - gfx.print(_inputText.substr(_inputText.length() - maxShow).c_str()); - } else { - gfx.print(_inputText.c_str()); - } - } - - // Cursor blink - int maxShow = (Theme::SCREEN_W - 44) / CHAR_W; - int cursorX = 4 + (int)std::min(_inputText.length(), (size_t)maxShow) * CHAR_W; - if ((millis() / 500) % 2 == 0) { - gfx.fillRect(cursorX, inputY + 2, 2, 10, Theme::ACCENT); - } - - // Send button - gfx.setTextColor(Theme::PRIMARY, Theme::SELECTION_BG); - gfx.fillRect(Theme::SCREEN_W - 32, inputY, 30, 14, Theme::SELECTION_BG); - gfx.setCursor(Theme::SCREEN_W - 30, inputY + 3); - gfx.print("Send"); - - // Message area with bubbles - int msgTop = headerBottom + 2; - int msgBottom = inputY - 3; - int msgAreaH = msgBottom - msgTop; - - if (_layout.empty()) return; - - // Calculate visible region — draw from bottom up - // _scrollPixels == 0 means showing the latest messages at bottom - int contentBottom = _totalContentH; - int viewBottom = contentBottom - _scrollPixels; - int viewTop = viewBottom - msgAreaH; - - int y = msgTop; // screen Y cursor - int contentY = 0; // content space Y cursor - - for (int li = 0; li < (int)_layout.size(); li++) { - const auto& ml = _layout[li]; - int blockTop = contentY; - int blockBottom = contentY + ml.totalHeight; - - // Skip blocks entirely above viewport - if (blockBottom <= viewTop) { - contentY = blockBottom; - continue; - } - // Stop if we've passed the viewport - if (blockTop >= viewBottom) break; - - // Map content Y to screen Y - int screenY = msgTop + (blockTop - viewTop); - - const auto& msg = _cachedMsgs[ml.msgIdx]; - int textH = (int)ml.lines.size() * LINE_H; - int bubbleH = textH + BUBBLE_PAD * 2; - - // Calculate bubble width from longest line - int maxLineLen = 0; - for (auto& line : ml.lines) { - if ((int)line.length() > maxLineLen) maxLineLen = line.length(); - } - int bubbleW = maxLineLen * CHAR_W + BUBBLE_PAD * 2 + 2; - if (bubbleW < 30) bubbleW = 30; - - int bubbleX; - uint32_t bubbleBg, textColor; - - if (ml.incoming) { - bubbleX = 4; - bubbleBg = Theme::MSG_IN_BG; - textColor = Theme::ACCENT; - } else { - bubbleX = Theme::SCREEN_W - bubbleW - 4; - bubbleBg = Theme::MSG_OUT_BG; - textColor = Theme::PRIMARY; - } - - // Clip to message area - if (screenY >= msgTop && screenY + bubbleH <= msgBottom) { - // Draw bubble background - gfx.fillRoundRect(bubbleX, screenY, bubbleW, bubbleH, 3, bubbleBg); - - // Draw text lines - gfx.setTextColor(textColor, bubbleBg); - for (int j = 0; j < (int)ml.lines.size(); j++) { - gfx.setCursor(bubbleX + BUBBLE_PAD + 1, screenY + BUBBLE_PAD + j * LINE_H); - gfx.print(ml.lines[j].c_str()); - } - - // Status indicator for outgoing - if (!ml.incoming) { - const char* ind = "~"; - uint32_t indColor = Theme::MUTED; - if (msg.status == LXMFStatus::SENT || msg.status == LXMFStatus::DELIVERED) { - ind = "*"; indColor = Theme::ACCENT; - } else if (msg.status == LXMFStatus::FAILED) { - ind = "!"; indColor = Theme::ERROR_CLR; - } - gfx.setTextColor(indColor, bubbleBg); - gfx.setCursor(bubbleX + bubbleW - CHAR_W - BUBBLE_PAD, screenY + bubbleH - LINE_H - BUBBLE_PAD + 1); - gfx.print(ind); - } - - // Timestamp below bubble - if (!ml.timeStr.empty()) { - int timeX = ml.incoming ? bubbleX + 2 : bubbleX + bubbleW - (int)ml.timeStr.length() * CHAR_W - 2; - gfx.setTextColor(Theme::MUTED, Theme::BG); - gfx.setCursor(timeX, screenY + bubbleH + 1); - gfx.print(ml.timeStr.c_str()); - } - } - - contentY = blockBottom; - } - - // Scroll indicator - if (_totalContentH > msgAreaH) { - int thumbH = std::max(8, msgAreaH * msgAreaH / _totalContentH); - int maxScroll = _totalContentH - msgAreaH; - int thumbY = msgTop + (msgAreaH - thumbH) * (_totalContentH - _scrollPixels - msgAreaH) / std::max(1, maxScroll); - thumbY = std::max(msgTop, std::min(thumbY, msgBottom - thumbH)); - gfx.fillRect(Theme::SCREEN_W - 2, msgTop, 2, msgAreaH, Theme::BORDER); - gfx.fillRect(Theme::SCREEN_W - 2, thumbY, 2, thumbH, Theme::SECONDARY); - } -} - -// ============================================================================= -// Input handling -// ============================================================================= -void MessageView::sendCurrentMessage() { - if (!_lxmf || _peerHex.empty() || _inputText.empty()) return; - - RNS::Bytes destHash; - destHash.assignHex(_peerHex.c_str()); - _lxmf->sendMessage(destHash, _inputText.c_str()); - - _inputText.clear(); - // Immediately refresh to show sent message - refreshMessages(); - rebuildLayout(); - _scrollPixels = 0; - markDirty(); -} - -bool MessageView::handleKey(const KeyEvent& event) { - if (event.character == 0x1B) { - if (_onBack) _onBack(); - return true; - } - - if (event.del || event.character == 0x08) { - if (!_inputText.empty()) { - _inputText.pop_back(); - markDirty(); - } else { - if (_onBack) _onBack(); - } - return true; - } - - if (event.enter || event.character == '\n' || event.character == '\r') { - sendCurrentMessage(); - return true; - } - - // Scroll - if (event.up) { - int maxScroll = std::max(0, _totalContentH - 100); - if (_scrollPixels < maxScroll) { - _scrollPixels += 20; - markDirty(); - } - return true; - } - if (event.down) { - if (_scrollPixels > 0) { - _scrollPixels -= 20; - if (_scrollPixels < 0) _scrollPixels = 0; - markDirty(); - } - return true; - } - - if (event.character >= 0x20 && event.character < 0x7F) { - _inputText += (char)event.character; - markDirty(); - return true; - } - - return false; -} diff --git a/src/ui/screens/MessageView.h b/src/ui/screens/MessageView.h deleted file mode 100644 index 4e19991..0000000 --- a/src/ui/screens/MessageView.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include "reticulum/LXMFMessage.h" -#include -#include -#include - -class LXMFManager; -class AnnounceManager; - -// Pre-computed layout for a single message (may span multiple lines) -struct MsgLayout { - int msgIdx; // index into _cachedMsgs - std::vector lines; // word-wrapped lines - std::string timeStr; // "12:34" or "HH:MM" - bool incoming; - int totalHeight; // pixel height for this message block -}; - -class MessageView : public Screen { -public: - using BackCallback = std::function; - - void update() override; - void onEnter() override; - void onExit() override; - bool handleKey(const KeyEvent& event) override; - - void setPeerHex(const std::string& hex) { _peerHex = hex; } - void setLXMFManager(LXMFManager* lxmf) { _lxmf = lxmf; } - void setAnnounceManager(AnnounceManager* am) { _am = am; } - void setBackCallback(BackCallback cb) { _onBack = cb; } - - const char* title() const override { return "Chat"; } - void draw(LGFX_TDeck& gfx) override; - -private: - void sendCurrentMessage(); - void refreshMessages(); - void rebuildLayout(); - - static std::vector wordWrap(const std::string& text, int maxChars); - static std::string formatTime(double timestamp); - - LXMFManager* _lxmf = nullptr; - AnnounceManager* _am = nullptr; - BackCallback _onBack; - std::string _peerHex; - std::string _inputText; - int _lastMsgCount = -1; - int _scrollPixels = 0; // pixel scroll from bottom - int _totalContentH = 0; // total height of all messages - std::vector _cachedMsgs; - std::vector _layout; - unsigned long _lastRefreshMs = 0; - static constexpr unsigned long REFRESH_INTERVAL_MS = 500; - static constexpr int CHAR_W = 6; - static constexpr int LINE_H = 10; - static constexpr int BUBBLE_PAD = 3; - static constexpr int BUBBLE_GAP = 4; - static constexpr int MAX_BUBBLE_CHARS = 38; -}; diff --git a/src/ui/screens/MessagesScreen.cpp b/src/ui/screens/MessagesScreen.cpp deleted file mode 100644 index 4fc3f82..0000000 --- a/src/ui/screens/MessagesScreen.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "MessagesScreen.h" -#include "ui/Theme.h" -#include "hal/Display.h" -#include "reticulum/LXMFManager.h" -#include "reticulum/AnnounceManager.h" -#include - -void MessagesScreen::onEnter() { - _lastConvCount = -1; - _selectedIdx = 0; - markDirty(); -} - -void MessagesScreen::update() { - if (!_lxmf) return; - int convCount = (int)_lxmf->conversations().size(); - if (convCount != _lastConvCount) { - _lastConvCount = convCount; - markDirty(); - } -} - -void MessagesScreen::draw(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - - if (!_lxmf || _lxmf->conversations().empty()) { - gfx.setTextColor(Theme::MUTED, Theme::BG); - const char* msg = "No conversations"; - int tw = strlen(msg) * 6; - gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 4); - gfx.print(msg); - return; - } - - const auto& convs = _lxmf->conversations(); - int y = Theme::CONTENT_Y + 2; - int rowH = 20; - - for (size_t i = 0; i < convs.size(); i++) { - if (y + rowH > Theme::SCREEN_H - Theme::TAB_BAR_H) break; - - const auto& peerHex = convs[i]; - int unread = _lxmf->unreadCount(peerHex); - - // Selection highlight - if ((int)i == _selectedIdx) { - gfx.fillRect(0, y, Theme::SCREEN_W, rowH, Theme::SELECTION_BG); - } - - // Peer name (lookup from AnnounceManager) or fallback to hex - std::string displayName; - if (_am) { - const DiscoveredNode* node = _am->findNodeByHex(peerHex); - if (node && !node->name.empty()) displayName = node->name; - } - if (displayName.empty()) displayName = peerHex.substr(0, 16); - - gfx.setTextColor(Theme::PRIMARY, (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG); - gfx.setCursor(4, y + 6); - gfx.print(displayName.c_str()); - - // Unread badge - if (unread > 0) { - char badge[8]; - snprintf(badge, sizeof(badge), "(%d)", unread); - gfx.setTextColor(Theme::BADGE_BG, (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG); - gfx.setCursor(Theme::SCREEN_W - 30, y + 6); - gfx.print(badge); - } - - // Separator - gfx.drawFastHLine(0, y + rowH - 1, Theme::SCREEN_W, Theme::BORDER); - y += rowH; - } -} - -bool MessagesScreen::handleKey(const KeyEvent& event) { - if (!_lxmf) return false; - int count = (int)_lxmf->conversations().size(); - if (count == 0) return false; - - if (event.up) { - if (_selectedIdx > 0) { - _selectedIdx--; - markDirty(); - } - return true; - } - if (event.down) { - if (_selectedIdx < count - 1) { - _selectedIdx++; - markDirty(); - } - return true; - } - if (event.enter || event.character == '\n' || event.character == '\r') { - if (_selectedIdx < count && _onOpen) { - _onOpen(_lxmf->conversations()[_selectedIdx]); - } - return true; - } - return false; -} diff --git a/src/ui/screens/MessagesScreen.h b/src/ui/screens/MessagesScreen.h deleted file mode 100644 index 358e919..0000000 --- a/src/ui/screens/MessagesScreen.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include -#include - -class LXMFManager; -class AnnounceManager; - -class MessagesScreen : public Screen { -public: - using OpenCallback = std::function; - - void update() override; - void onEnter() override; - bool handleKey(const KeyEvent& event) override; - - void setLXMFManager(LXMFManager* lxmf) { _lxmf = lxmf; } - void setAnnounceManager(AnnounceManager* am) { _am = am; } - void setOpenCallback(OpenCallback cb) { _onOpen = cb; } - - const char* title() const override { return "Messages"; } - void draw(LGFX_TDeck& gfx) override; - -private: - LXMFManager* _lxmf = nullptr; - AnnounceManager* _am = nullptr; - OpenCallback _onOpen; - int _lastConvCount = -1; - int _selectedIdx = 0; -}; diff --git a/src/ui/screens/NameInputScreen.cpp b/src/ui/screens/NameInputScreen.cpp deleted file mode 100644 index 069e6dd..0000000 --- a/src/ui/screens/NameInputScreen.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "NameInputScreen.h" -#include "ui/Theme.h" -#include "config/Config.h" -#include "hal/Display.h" - -void NameInputScreen::draw(LGFX_TDeck& gfx) { - int cx = Theme::SCREEN_W / 2; - - // Ratspeak branding - gfx.setTextSize(2); - gfx.setTextColor(Theme::PRIMARY, Theme::BG); - const char* brand = "RATSPEAK"; - int bw = strlen(brand) * 12; - gfx.setCursor(cx - bw / 2, 30); - gfx.print(brand); - - // .org subtitle - gfx.setTextSize(1); - gfx.setTextColor(Theme::ACCENT, Theme::BG); - const char* sub = "ratspeak.org"; - int sw = strlen(sub) * 6; - gfx.setCursor(cx - sw / 2, 52); - gfx.print(sub); - - // Prompt - gfx.setTextColor(Theme::SECONDARY, Theme::BG); - const char* prompt = "Enter your display name"; - int pw = strlen(prompt) * 6; - gfx.setCursor(cx - pw / 2, 85); - gfx.print(prompt); - - gfx.setTextColor(Theme::MUTED, Theme::BG); - const char* opt = "(Optional - press Enter to skip)"; - int ow = strlen(opt) * 6; - gfx.setCursor(cx - ow / 2, 100); - gfx.print(opt); - - // Text input field - int fieldW = 200; - int fieldH = 20; - int fieldX = cx - fieldW / 2; - int fieldY = 125; - gfx.drawRect(fieldX, fieldY, fieldW, fieldH, Theme::PRIMARY); - gfx.fillRect(fieldX + 1, fieldY + 1, fieldW - 2, fieldH - 2, Theme::SELECTION_BG); - - // Name text - gfx.setTextSize(1); - gfx.setTextColor(Theme::PRIMARY, Theme::SELECTION_BG); - gfx.setCursor(fieldX + 6, fieldY + 6); - gfx.print(_name); - - // Cursor blink (simple solid cursor) - int cursorX = fieldX + 6 + _nameLen * 6; - if (cursorX < fieldX + fieldW - 8) { - bool blink = (millis() / 500) % 2 == 0; - if (blink) { - gfx.fillRect(cursorX, fieldY + 4, 6, 12, Theme::PRIMARY); - } - } - - // OK hint - gfx.setTextColor(Theme::ACCENT, Theme::BG); - const char* hint = "[Enter] OK"; - int hw = strlen(hint) * 6; - gfx.setCursor(cx - hw / 2, 160); - gfx.print(hint); - - // Version at bottom - gfx.setTextColor(Theme::MUTED, Theme::BG); - char ver[32]; - snprintf(ver, sizeof(ver), "Ratdeck v%s", RATDECK_VERSION_STRING); - int vw = strlen(ver) * 6; - gfx.setCursor(cx - vw / 2, 190); - gfx.print(ver); -} - -bool NameInputScreen::handleKey(const KeyEvent& event) { - if (event.enter || event.character == '\n' || event.character == '\r') { - if (_doneCb) _doneCb(String(_name)); - return true; - } - - if (event.del || event.character == 8) { - if (_nameLen > 0) { - _nameLen--; - _name[_nameLen] = '\0'; - markDirty(); - } - return true; - } - - // Printable characters - if (event.character >= 0x20 && event.character <= 0x7E && _nameLen < MAX_NAME_LEN) { - _name[_nameLen] = event.character; - _nameLen++; - _name[_nameLen] = '\0'; - markDirty(); - return true; - } - - return true; // Consume all keys on this screen -} diff --git a/src/ui/screens/NameInputScreen.h b/src/ui/screens/NameInputScreen.h deleted file mode 100644 index 8f94fca..0000000 --- a/src/ui/screens/NameInputScreen.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include - -class NameInputScreen : public Screen { -public: - bool handleKey(const KeyEvent& event) override; - const char* title() const override { return "Setup"; } - void draw(LGFX_TDeck& gfx) override; - - void setDoneCallback(std::function cb) { _doneCb = cb; } - - static constexpr int MAX_NAME_LEN = 16; - -private: - char _name[MAX_NAME_LEN + 1] = {0}; - int _nameLen = 0; - std::function _doneCb; -}; diff --git a/src/ui/screens/NodesScreen.cpp b/src/ui/screens/NodesScreen.cpp deleted file mode 100644 index 1fbb7bd..0000000 --- a/src/ui/screens/NodesScreen.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "NodesScreen.h" -#include "ui/Theme.h" -#include "hal/Display.h" -#include "reticulum/AnnounceManager.h" -#include - -void NodesScreen::onEnter() { - _lastNodeCount = -1; - _selectedIdx = 0; - markDirty(); -} - -void NodesScreen::update() { - if (!_am) return; - if (_am->nodeCount() != _lastNodeCount) { - _lastNodeCount = _am->nodeCount(); - markDirty(); - } -} - -void NodesScreen::draw(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - - if (!_am || _am->nodeCount() == 0) { - gfx.setTextColor(Theme::MUTED, Theme::BG); - const char* msg = "No nodes discovered"; - int tw = strlen(msg) * 6; - gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 4); - gfx.print(msg); - return; - } - - const auto& nodes = _am->nodes(); - int y = Theme::CONTENT_Y + 2; - int rowH = 18; - - for (size_t i = 0; i < nodes.size(); i++) { - if (y + rowH > Theme::SCREEN_H - Theme::TAB_BAR_H) break; - - const auto& node = nodes[i]; - - // Selection highlight - if ((int)i == _selectedIdx) { - gfx.fillRect(0, y, Theme::SCREEN_W, rowH, Theme::SELECTION_BG); - } - - uint32_t bgCol = (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG; - - // Name + identity hash (formatted with colons like own identity) - std::string displayHash; - if (!node.identityHex.empty() && node.identityHex.size() >= 12) { - displayHash = node.identityHex.substr(0, 4) + ":" + - node.identityHex.substr(4, 4) + ":" + - node.identityHex.substr(8, 4); - } else { - displayHash = node.hash.toHex().substr(0, 8); - } - char buf[64]; - snprintf(buf, sizeof(buf), "%s [%s]", node.name.c_str(), displayHash.c_str()); - gfx.setTextColor(node.saved ? Theme::ACCENT : Theme::PRIMARY, bgCol); - gfx.setCursor(4, y + 5); - gfx.print(buf); - - // Hops + age (right side) - unsigned long ageSec = (millis() - node.lastSeen) / 1000; - char infoBuf[24]; - if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%dhop %lus", node.hops, ageSec); - else snprintf(infoBuf, sizeof(infoBuf), "%dhop %lum", node.hops, ageSec / 60); - int tw = strlen(infoBuf) * 6; - gfx.setTextColor(Theme::SECONDARY, bgCol); - gfx.setCursor(Theme::SCREEN_W - tw - 4, y + 5); - gfx.print(infoBuf); - - y += rowH; - } -} - -bool NodesScreen::handleKey(const KeyEvent& event) { - if (!_am) return false; - int count = _am->nodeCount(); - if (count == 0) return false; - - if (event.up) { - if (_selectedIdx > 0) { - _selectedIdx--; - markDirty(); - } - return true; - } - if (event.down) { - if (_selectedIdx < count - 1) { - _selectedIdx++; - markDirty(); - } - return true; - } - if (event.enter || event.character == '\n' || event.character == '\r') { - if (_selectedIdx < count && _onSelect) { - _onSelect(_am->nodes()[_selectedIdx].hash.toHex()); - } - return true; - } - return false; -} diff --git a/src/ui/screens/NodesScreen.h b/src/ui/screens/NodesScreen.h deleted file mode 100644 index c82a92e..0000000 --- a/src/ui/screens/NodesScreen.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include -#include - -class AnnounceManager; - -class NodesScreen : public Screen { -public: - using NodeSelectedCallback = std::function; - - void update() override; - void onEnter() override; - bool handleKey(const KeyEvent& event) override; - - void setAnnounceManager(AnnounceManager* am) { _am = am; } - void setNodeSelectedCallback(NodeSelectedCallback cb) { _onSelect = cb; } - - const char* title() const override { return "Nodes"; } - void draw(LGFX_TDeck& gfx) override; - -private: - AnnounceManager* _am = nullptr; - NodeSelectedCallback _onSelect; - int _lastNodeCount = -1; - int _selectedIdx = 0; -}; diff --git a/src/ui/screens/SettingsScreen.cpp b/src/ui/screens/SettingsScreen.cpp deleted file mode 100644 index f4e82c6..0000000 --- a/src/ui/screens/SettingsScreen.cpp +++ /dev/null @@ -1,1096 +0,0 @@ -#include "SettingsScreen.h" -#include "ui/Theme.h" -#include "hal/Display.h" -#include "config/Config.h" -#include "config/UserConfig.h" -#include "storage/FlashStore.h" -#include "storage/SDStore.h" -#include "radio/SX1262.h" -#include "audio/AudioNotify.h" -#include "hal/Power.h" -#include "transport/WiFiInterface.h" -#include "reticulum/ReticulumManager.h" -#include "reticulum/IdentityManager.h" -#include -#include -#include - -// Radio presets -struct RadioPreset { - const char* name; - uint8_t sf; - uint32_t bw; - uint8_t cr; - int8_t txPower; - long preamble; -}; - -static const RadioPreset PRESETS[] = { - {"Balanced", 9, 125000, 5, 17, 18}, - {"Long Range", 12, 62500, 8, 22, 18}, - {"Fast", 7, 250000, 5, 14, 18}, -}; -static constexpr int NUM_PRESETS = 3; - -bool SettingsScreen::isEditable(int idx) const { - if (idx < 0 || idx >= (int)_items.size()) return false; - auto t = _items[idx].type; - return t == SettingType::INTEGER || t == SettingType::TOGGLE - || t == SettingType::ENUM_CHOICE || t == SettingType::ACTION - || t == SettingType::TEXT_INPUT; -} - -void SettingsScreen::skipToNextEditable(int dir) { - int n = _catRangeEnd; - int start = _selectedIdx; - for (int i = 0; i < (n - _catRangeStart); i++) { - _selectedIdx += dir; - if (_selectedIdx < _catRangeStart) _selectedIdx = _catRangeStart; - if (_selectedIdx >= n) _selectedIdx = n - 1; - if (isEditable(_selectedIdx)) return; - if (_selectedIdx == _catRangeStart && dir < 0) return; - if (_selectedIdx == n - 1 && dir > 0) return; - } - _selectedIdx = start; -} - -int SettingsScreen::detectPreset() const { - if (!_cfg) return -1; - auto& s = _cfg->settings(); - for (int i = 0; i < NUM_PRESETS; i++) { - if (s.loraSF == PRESETS[i].sf && s.loraBW == PRESETS[i].bw - && s.loraCR == PRESETS[i].cr && s.loraTxPower == PRESETS[i].txPower) { - return i; - } - } - return -1; -} - -void SettingsScreen::applyPreset(int presetIdx) { - if (!_cfg || presetIdx < 0 || presetIdx >= NUM_PRESETS) return; - auto& s = _cfg->settings(); - const auto& p = PRESETS[presetIdx]; - s.loraSF = p.sf; - s.loraBW = p.bw; - s.loraCR = p.cr; - s.loraTxPower = p.txPower; - s.loraPreamble = p.preamble; -} - -// ============================================================================= -// Build items — no HEADER items, just flat list grouped by category -// ============================================================================= -void SettingsScreen::buildItems() { - _items.clear(); - _categories.clear(); - if (!_cfg) return; - auto& s = _cfg->settings(); - int idx = 0; - - // ─── Device ─── - int devStart = idx; - _items.push_back({"Version", SettingType::READONLY, nullptr, nullptr, - [](int) { return String(RATDECK_VERSION_STRING); }}); - idx++; - _items.push_back({"Identity", SettingType::READONLY, nullptr, nullptr, - [this](int) { return _identityHash.substring(0, 16); }}); - idx++; - { - SettingItem nameItem; - nameItem.label = "Display Name"; - nameItem.type = SettingType::TEXT_INPUT; - nameItem.textGetter = [&s]() { return s.displayName; }; - nameItem.textSetter = [&s](const String& v) { s.displayName = v; }; - nameItem.maxTextLen = 16; - _items.push_back(nameItem); - idx++; - } - // Identity switcher (if IdentityManager available) - if (_idMgr && _idMgr->count() > 0) { - SettingItem idSwitch; - idSwitch.label = "Active Identity"; - idSwitch.type = SettingType::ENUM_CHOICE; - idSwitch.getter = [this]() { return _idMgr->activeIndex(); }; - idSwitch.setter = [this](int v) { - // Identity switch requires reboot - if (v == _idMgr->activeIndex()) return; - RNS::Identity newId; - if (_idMgr->switchTo(v, newId)) { - if (_ui) _ui->statusBar().showToast("Identity switched! Rebooting...", 2000); - applyAndSave(); - delay(1000); - ESP.restart(); - } else { - if (_ui) _ui->statusBar().showToast("Switch failed!", 1500); - } - }; - idSwitch.formatter = nullptr; - idSwitch.minVal = 0; - idSwitch.maxVal = _idMgr->count() - 1; - idSwitch.step = 1; - // Build labels from identity slots - for (int i = 0; i < _idMgr->count(); i++) { - auto& slot = _idMgr->identities()[i]; - static char labelBufs[8][32]; - if (!slot.displayName.isEmpty()) { - snprintf(labelBufs[i], sizeof(labelBufs[i]), "%s", slot.displayName.c_str()); - } else { - snprintf(labelBufs[i], sizeof(labelBufs[i]), "%.12s", slot.hash.c_str()); - } - idSwitch.enumLabels.push_back(labelBufs[i]); - } - _items.push_back(idSwitch); - idx++; - } - { - SettingItem newId; - newId.label = "New Identity"; - newId.type = SettingType::ACTION; - newId.formatter = [](int) { return String("[Enter]"); }; - newId.action = [this, &s]() { - if (!_idMgr) { - if (_ui) _ui->statusBar().showToast("Not available", 1200); - return; - } - if (_idMgr->count() >= 8) { - if (_ui) _ui->statusBar().showToast("Max 8 identities!", 1200); - return; - } - int idx = _idMgr->createIdentity(s.displayName); - if (idx >= 0) { - if (_ui) _ui->statusBar().showToast("Identity created!", 1200); - buildItems(); // Rebuild to show new identity in switcher - markDirty(); - } - }; - _items.push_back(newId); - idx++; - } - _categories.push_back({"Device", devStart, idx - devStart, - [&s]() { return s.displayName.isEmpty() ? String("(unnamed)") : s.displayName; }}); - - // ─── Display & Input ─── - int dispStart = idx; - _items.push_back({"Brightness", SettingType::INTEGER, - [&s]() { return s.brightness; }, - [&s](int v) { s.brightness = v; }, - [](int v) { return String(v); }, - 16, 255, 16}); - idx++; - _items.push_back({"Dim Timeout", SettingType::INTEGER, - [&s]() { return s.screenDimTimeout; }, - [&s](int v) { s.screenDimTimeout = v; }, - [](int v) { return String(v) + "s"; }, - 5, 300, 5}); - idx++; - _items.push_back({"Off Timeout", SettingType::INTEGER, - [&s]() { return s.screenOffTimeout; }, - [&s](int v) { s.screenOffTimeout = v; }, - [](int v) { return String(v) + "s"; }, - 10, 600, 10}); - idx++; - _items.push_back({"Trackball Speed", SettingType::INTEGER, - [&s]() { return s.trackballSpeed; }, - [&s](int v) { s.trackballSpeed = v; }, - [](int v) { return String(v); }, - 1, 5, 1}); - idx++; - _categories.push_back({"Display & Input", dispStart, idx - dispStart, - [&s]() { return String(s.brightness * 100 / 255) + "%"; }}); - - // ─── Radio ─── - int radioStart = idx; - { - SettingItem presetItem; - presetItem.label = "Preset"; - presetItem.type = SettingType::ENUM_CHOICE; - presetItem.getter = [this]() { - int p = detectPreset(); - return (p >= 0) ? p : NUM_PRESETS; - }; - presetItem.setter = [this](int v) { - if (v >= 0 && v < NUM_PRESETS) applyPreset(v); - }; - presetItem.formatter = nullptr; - presetItem.minVal = 0; - presetItem.maxVal = NUM_PRESETS; - presetItem.step = 1; - presetItem.enumLabels = {"Balanced", "Long Range", "Fast", "Custom"}; - _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++; - _categories.push_back({"Radio", radioStart, idx - radioStart, - [this]() { - int p = detectPreset(); - return (p >= 0) ? String(PRESETS[p].name) : String("Custom"); - }}); - - // ─── Network ─── - int netStart = idx; - _items.push_back({"WiFi Mode", SettingType::ENUM_CHOICE, - [&s]() { return (int)s.wifiMode; }, - [&s](int v) { s.wifiMode = (RatWiFiMode)v; }, - nullptr, 0, 2, 1, {"OFF", "AP", "STA"}}); - idx++; - { - SettingItem ssidItem; - ssidItem.label = "WiFi SSID"; - ssidItem.type = SettingType::TEXT_INPUT; - ssidItem.textGetter = [&s]() { return s.wifiSTASSID; }; - ssidItem.textSetter = [&s](const String& v) { s.wifiSTASSID = v; }; - ssidItem.maxTextLen = 32; - _items.push_back(ssidItem); - idx++; - } - { - SettingItem passItem; - passItem.label = "WiFi Password"; - passItem.type = SettingType::TEXT_INPUT; - passItem.textGetter = [&s]() { return s.wifiSTAPassword; }; - passItem.textSetter = [&s](const String& v) { s.wifiSTAPassword = v; }; - passItem.maxTextLen = 32; - _items.push_back(passItem); - idx++; - } - { - SettingItem tcpPreset; - tcpPreset.label = "TCP Server"; - tcpPreset.type = SettingType::ENUM_CHOICE; - tcpPreset.getter = [&s]() { - for (auto& ep : s.tcpConnections) { - if (ep.host == "rns.ratspeak.org") return 1; - } - if (!s.tcpConnections.empty()) return 2; - return 0; - }; - tcpPreset.setter = [&s](int v) { - if (v == 0) { - s.tcpConnections.clear(); - } else if (v == 1) { - s.tcpConnections.clear(); - TCPEndpoint ep; - ep.host = "rns.ratspeak.org"; - ep.port = TCP_DEFAULT_PORT; - ep.autoConnect = true; - s.tcpConnections.push_back(ep); - } - }; - tcpPreset.formatter = nullptr; - tcpPreset.minVal = 0; - tcpPreset.maxVal = 2; - tcpPreset.step = 1; - tcpPreset.enumLabels = {"None", "Ratspeak Hub", "Custom"}; - _items.push_back(tcpPreset); - idx++; - } - { - SettingItem tcpHost; - tcpHost.label = "TCP Host"; - tcpHost.type = SettingType::TEXT_INPUT; - tcpHost.textGetter = [&s]() { - return s.tcpConnections.empty() ? String("") : s.tcpConnections[0].host; - }; - tcpHost.textSetter = [&s](const String& v) { - if (s.tcpConnections.empty()) { - TCPEndpoint ep; - ep.host = v; - ep.port = TCP_DEFAULT_PORT; - ep.autoConnect = true; - s.tcpConnections.push_back(ep); - } else { - s.tcpConnections[0].host = v; - } - }; - tcpHost.maxTextLen = 40; - _items.push_back(tcpHost); - idx++; - } - _items.push_back({"TCP Port", SettingType::INTEGER, - [&s]() { return s.tcpConnections.empty() ? TCP_DEFAULT_PORT : (int)s.tcpConnections[0].port; }, - [&s](int v) { - if (s.tcpConnections.empty()) { - TCPEndpoint ep; - ep.port = v; - ep.autoConnect = true; - s.tcpConnections.push_back(ep); - } else { - s.tcpConnections[0].port = v; - } - }, - [](int v) { return String(v); }, - 1, 65535, 1}); - idx++; - _items.push_back({"Transport Node", SettingType::TOGGLE, - [&s]() { return s.transportEnabled ? 1 : 0; }, - [&s](int v) { s.transportEnabled = (v != 0); }, - [](int v) { return v ? String("ON") : String("OFF"); }}); - idx++; - _items.push_back({"BLE", SettingType::TOGGLE, - [&s]() { return s.bleEnabled ? 1 : 0; }, - [&s](int v) { s.bleEnabled = (v != 0); }, - [](int v) { return v ? String("ON") : String("OFF"); }}); - idx++; - _categories.push_back({"Network", netStart, idx - netStart, - [&s]() { - const char* modes[] = {"OFF", "AP", "STA"}; - return String(modes[constrain((int)s.wifiMode, 0, 2)]); - }}); - - // ─── Audio ─── - int audioStart = idx; - _items.push_back({"Audio", SettingType::TOGGLE, - [&s]() { return s.audioEnabled ? 1 : 0; }, - [&s](int v) { s.audioEnabled = (v != 0); }, - [](int v) { return v ? String("ON") : String("OFF"); }}); - idx++; - _items.push_back({"Volume", SettingType::INTEGER, - [&s]() { return s.audioVolume; }, - [&s](int v) { s.audioVolume = v; }, - [](int v) { return String(v) + "%"; }, - 0, 100, 10}); - idx++; - _categories.push_back({"Audio", audioStart, idx - audioStart, - [&s]() { return s.audioEnabled ? (String(s.audioVolume) + "%") : String("OFF"); }}); - - // ─── System ─── - int sysStart = idx; - _items.push_back({"Free Heap", SettingType::READONLY, nullptr, nullptr, - [](int) { return String((unsigned long)(ESP.getFreeHeap() / 1024)) + " KB"; }}); - idx++; - _items.push_back({"Free PSRAM", SettingType::READONLY, nullptr, nullptr, - [](int) { return String((unsigned long)(ESP.getFreePsram() / 1024)) + " KB"; }}); - idx++; - _items.push_back({"Flash", SettingType::READONLY, nullptr, nullptr, - [this](int) { return _flash && _flash->exists("/ratputer") ? String("Mounted") : String("Error"); }}); - idx++; - _items.push_back({"SD Card", SettingType::READONLY, nullptr, nullptr, - [this](int) { return _sd && _sd->isReady() ? String("Ready") : String("Not Found"); }}); - idx++; - { - SettingItem announceItem; - announceItem.label = "Send Announce"; - announceItem.type = SettingType::ACTION; - announceItem.formatter = [](int) { return String("[Enter]"); }; - announceItem.action = [this]() { - if (_rns && _cfg) { - const String& name = _cfg->settings().displayName; - RNS::Bytes appData; - if (!name.isEmpty()) { - size_t len = name.length(); - if (len > 31) len = 31; - uint8_t buf[2 + 31]; - buf[0] = 0x91; - buf[1] = 0xA0 | (uint8_t)len; - memcpy(buf + 2, name.c_str(), len); - appData = RNS::Bytes(buf, 2 + len); - } - _rns->announce(appData); - if (_ui) { - _ui->statusBar().flashAnnounce(); - _ui->statusBar().showToast("Announce sent!"); - } - } else { - if (_ui) _ui->statusBar().showToast("RNS not ready"); - } - }; - _items.push_back(announceItem); - idx++; - } - { - SettingItem initSD; - initSD.label = "Init SD Card"; - initSD.type = SettingType::ACTION; - initSD.formatter = [this](int) { - return (_sd && _sd->isReady()) ? String("[Enter]") : String("No Card"); - }; - initSD.action = [this]() { - if (!_sd || !_sd->isReady()) { - if (_ui) _ui->statusBar().showToast("No SD card!", 1200); - return; - } - if (_ui) _ui->statusBar().showToast("Initializing SD...", 2000); - bool ok = _sd->formatForRatputer(); - if (_ui) _ui->statusBar().showToast(ok ? "SD initialized!" : "SD init failed!", 1500); - }; - _items.push_back(initSD); - idx++; - } - { - SettingItem wipeSD; - wipeSD.label = "Wipe SD Data"; - wipeSD.type = SettingType::ACTION; - wipeSD.formatter = [this](int) { - return (_sd && _sd->isReady()) ? String("[Enter]") : String("No Card"); - }; - wipeSD.action = [this]() { - if (!_sd || !_sd->isReady()) { - if (_ui) _ui->statusBar().showToast("No SD card!", 1200); - return; - } - if (_ui) _ui->statusBar().showToast("Wiping SD data...", 2000); - bool ok = _sd->wipeRatputer(); - if (_ui) _ui->statusBar().showToast(ok ? "SD wiped & reinit!" : "Wipe failed!", 1500); - }; - _items.push_back(wipeSD); - idx++; - } - { - SettingItem factoryReset; - factoryReset.label = "Factory Reset"; - factoryReset.type = SettingType::ACTION; - factoryReset.formatter = [this](int) { - return _confirmingReset ? String("[Confirm?]") : String("[Enter]"); - }; - factoryReset.action = [this]() { - if (!_confirmingReset) { - _confirmingReset = true; - if (_ui) _ui->statusBar().showToast("Press again to confirm!", 2000); - markDirty(); - return; - } - _confirmingReset = false; - if (_ui) _ui->statusBar().showToast("Factory resetting...", 3000); - Serial.println("[SETTINGS] Factory reset initiated"); - if (_sd && _sd->isReady()) _sd->wipeRatputer(); - if (_flash) _flash->format(); - nvs_flash_erase(); - delay(500); - ESP.restart(); - }; - _items.push_back(factoryReset); - idx++; - } - { - SettingItem rebootItem; - rebootItem.label = "Reboot Device"; - rebootItem.type = SettingType::ACTION; - rebootItem.formatter = [](int) { return String("[Enter]"); }; - rebootItem.action = [this]() { - if (_ui) _ui->statusBar().showToast("Rebooting...", 1500); - Serial.println("[SETTINGS] Reboot requested"); - delay(500); - ESP.restart(); - }; - _items.push_back(rebootItem); - idx++; - } - _categories.push_back({"System", sysStart, idx - sysStart, - [](){ return String((unsigned long)(ESP.getFreeHeap() / 1024)) + " KB free"; }}); -} - -// ============================================================================= -// Lifecycle -// ============================================================================= -void SettingsScreen::onEnter() { - buildItems(); - _view = SettingsView::CATEGORY_LIST; - _categoryIdx = 0; - _categoryScroll = 0; - _selectedIdx = 0; - _itemScroll = 0; - _editing = false; - _textEditing = false; - _confirmingReset = false; - markDirty(); -} - -void SettingsScreen::update() { -} - -// ============================================================================= -// Category navigation -// ============================================================================= -void SettingsScreen::enterCategory(int catIdx) { - if (catIdx < 0 || catIdx >= (int)_categories.size()) return; - _categoryIdx = catIdx; - auto& cat = _categories[catIdx]; - _catRangeStart = cat.startIdx; - _catRangeEnd = cat.startIdx + cat.count; - _selectedIdx = _catRangeStart; - _itemScroll = 0; - _editing = false; - _textEditing = false; - // Skip to first editable item - if (!isEditable(_selectedIdx)) skipToNextEditable(1); - _view = SettingsView::ITEM_LIST; - markDirty(); -} - -void SettingsScreen::exitToCategories() { - _view = SettingsView::CATEGORY_LIST; - _editing = false; - _textEditing = false; - _confirmingReset = false; - markDirty(); -} - -// ============================================================================= -// Drawing -// ============================================================================= -void SettingsScreen::draw(LGFX_TDeck& gfx) { - switch (_view) { - case SettingsView::CATEGORY_LIST: drawCategoryList(gfx); break; - case SettingsView::ITEM_LIST: drawItemList(gfx); break; - case SettingsView::WIFI_PICKER: drawWifiPicker(gfx); break; - } -} - -void SettingsScreen::drawCategoryList(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - int lineH = 28; // Taller rows for categories - int y = Theme::CONTENT_Y + 4; - - // Title - gfx.setTextColor(Theme::ACCENT, Theme::BG); - gfx.setCursor(4, y); - gfx.print("SETTINGS"); - gfx.drawFastHLine(0, y + 12, Theme::SCREEN_W, Theme::BORDER); - y += 18; - - int visibleStart = _categoryScroll; - int maxVisible = (Theme::CONTENT_H - 22) / lineH; - - // Scroll adjustment - if (_categoryIdx < _categoryScroll) _categoryScroll = _categoryIdx; - if (_categoryIdx >= _categoryScroll + maxVisible) _categoryScroll = _categoryIdx - maxVisible + 1; - visibleStart = _categoryScroll; - - for (int i = visibleStart; i < (int)_categories.size(); i++) { - int row = i - visibleStart; - int rowY = y + row * lineH; - if (rowY + lineH > Theme::SCREEN_H - Theme::TAB_BAR_H) break; - - bool selected = (i == _categoryIdx); - auto& cat = _categories[i]; - - if (selected) { - gfx.fillRect(0, rowY, Theme::SCREEN_W, lineH - 2, Theme::SELECTION_BG); - } - - uint32_t bg = selected ? Theme::SELECTION_BG : Theme::BG; - - // Category name - gfx.setTextColor(selected ? Theme::ACCENT : Theme::PRIMARY, bg); - gfx.setCursor(12, rowY + 4); - gfx.print(cat.name); - - // Item count - gfx.setTextColor(Theme::MUTED, bg); - char countStr[8]; - snprintf(countStr, sizeof(countStr), "(%d)", cat.count); - gfx.setCursor(12 + strlen(cat.name) * 6 + 6, rowY + 4); - gfx.print(countStr); - - // Summary on second line - if (cat.summary) { - String sum = cat.summary(); - gfx.setTextColor(Theme::MUTED, bg); - gfx.setCursor(20, rowY + 15); - gfx.print(sum.c_str()); - } - - // Arrow indicator - gfx.setTextColor(selected ? Theme::ACCENT : Theme::MUTED, bg); - gfx.setCursor(Theme::SCREEN_W - 14, rowY + 8); - gfx.print(">"); - } -} - -void SettingsScreen::drawItemList(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - int lineH = 14; - - // Category header - int y = Theme::CONTENT_Y + 2; - gfx.setTextColor(Theme::ACCENT, Theme::BG); - gfx.setCursor(4, y); - gfx.print("< "); - gfx.print(_categories[_categoryIdx].name); - gfx.drawFastHLine(0, y + 12, Theme::SCREEN_W, Theme::BORDER); - int listY = y + 16; - - int visibleLines = (Theme::SCREEN_H - Theme::TAB_BAR_H - listY) / lineH; - int relIdx = _selectedIdx - _catRangeStart; - - // Scroll adjustment - if (relIdx < _itemScroll) _itemScroll = relIdx; - if (relIdx >= _itemScroll + visibleLines) _itemScroll = relIdx - visibleLines + 1; - - int valX = 160; - - for (int r = 0; r < visibleLines; r++) { - int itemRel = _itemScroll + r; - int itemAbs = _catRangeStart + itemRel; - if (itemAbs >= _catRangeEnd) break; - - int rowY = listY + r * lineH; - const auto& item = _items[itemAbs]; - bool selected = (itemAbs == _selectedIdx); - bool editable = isEditable(itemAbs); - - if (selected && editable) { - gfx.fillRect(0, rowY, Theme::SCREEN_W, lineH, Theme::SELECTION_BG); - } - - uint32_t bgCol = (selected && editable) ? Theme::SELECTION_BG : Theme::BG; - - if (item.type == SettingType::ACTION) { - gfx.setTextColor(selected ? Theme::ACCENT : Theme::PRIMARY, bgCol); - gfx.setCursor(4, rowY + 3); - gfx.print(item.label); - if (item.formatter) { - String hint = item.formatter(0); - gfx.setTextColor(Theme::MUTED, bgCol); - gfx.setCursor(valX, rowY + 3); - gfx.print(hint.c_str()); - } - } else if (item.type == SettingType::TEXT_INPUT) { - gfx.setTextColor(Theme::SECONDARY, bgCol); - gfx.setCursor(4, rowY + 3); - gfx.print(item.label); - - if (_textEditing && selected) { - gfx.setTextColor(Theme::WARNING_CLR, bgCol); - gfx.setCursor(valX, rowY + 3); - // Show tail of text if too long - int maxChars = (Theme::SCREEN_W - valX - 10) / 6; - if ((int)_editText.length() > maxChars) { - gfx.print(_editText.substring(_editText.length() - maxChars).c_str()); - } else { - gfx.print(_editText.c_str()); - } - int curX = valX + std::min((int)_editText.length(), maxChars) * 6; - if ((millis() / 500) % 2 == 0) { - gfx.fillRect(curX, rowY + 3, 6, 8, Theme::WARNING_CLR); - } - } else { - String val = item.textGetter ? item.textGetter() : ""; - gfx.setTextColor(val.isEmpty() ? Theme::MUTED : Theme::PRIMARY, bgCol); - gfx.setCursor(valX, rowY + 3); - gfx.print(val.isEmpty() ? "(not set)" : val.c_str()); - } - } else { - // Label - gfx.setTextColor(Theme::SECONDARY, bgCol); - gfx.setCursor(4, rowY + 3); - gfx.print(item.label); - - // Value - String valStr; - if (_editing && selected) { - if (item.type == SettingType::ENUM_CHOICE && !item.enumLabels.empty()) { - int vi = constrain(_editValue, 0, (int)item.enumLabels.size() - 1); - valStr = item.enumLabels[vi]; - } else if (item.formatter) { - valStr = item.formatter(_editValue); - } else { - valStr = String(_editValue); - } - gfx.setTextColor(Theme::WARNING_CLR, bgCol); - gfx.setCursor(valX - 12, rowY + 3); - gfx.print("<"); - gfx.setCursor(valX, rowY + 3); - gfx.print(valStr.c_str()); - int endX = valX + (int)valStr.length() * 6 + 4; - gfx.setCursor(endX, rowY + 3); - gfx.print(">"); - } else { - if (item.type == SettingType::READONLY) { - valStr = item.formatter ? item.formatter(0) : ""; - gfx.setTextColor(Theme::MUTED, bgCol); - } else if (item.type == SettingType::ENUM_CHOICE && !item.enumLabels.empty()) { - int vi = item.getter ? constrain(item.getter(), 0, (int)item.enumLabels.size() - 1) : 0; - valStr = item.enumLabels[vi]; - gfx.setTextColor(Theme::PRIMARY, bgCol); - } else { - int val = item.getter ? item.getter() : 0; - valStr = item.formatter ? item.formatter(val) : String(val); - gfx.setTextColor(Theme::PRIMARY, bgCol); - } - gfx.setCursor(valX, rowY + 3); - gfx.print(valStr.c_str()); - } - } - } - - // Scroll indicator - int totalItems = _catRangeEnd - _catRangeStart; - if (totalItems > visibleLines) { - int barH = visibleLines * lineH; - int thumbH = max(8, barH * visibleLines / totalItems); - int thumbY = listY + (barH - thumbH) * _itemScroll / max(1, totalItems - visibleLines); - gfx.fillRect(Theme::SCREEN_W - 2, listY, 2, barH, Theme::BORDER); - gfx.fillRect(Theme::SCREEN_W - 2, thumbY, 2, thumbH, Theme::SECONDARY); - } -} - -void SettingsScreen::drawWifiPicker(LGFX_TDeck& gfx) { - gfx.setTextSize(1); - int lineH = 16; - int y = Theme::CONTENT_Y + 2; - - // Header - gfx.setTextColor(Theme::ACCENT, Theme::BG); - gfx.setCursor(4, y); - gfx.print("< Select WiFi Network"); - gfx.drawFastHLine(0, y + 12, Theme::SCREEN_W, Theme::BORDER); - int listY = y + 16; - - if (_wifiScanning) { - gfx.setTextColor(Theme::WARNING_CLR, Theme::BG); - gfx.setCursor(4, listY + 20); - gfx.print(" Scanning for networks..."); - // Animated dots - int dots = (millis() / 400) % 4; - for (int d = 0; d < dots; d++) gfx.print("."); - return; - } - - if (_wifiResults.empty()) { - gfx.setTextColor(Theme::MUTED, Theme::BG); - gfx.setCursor(4, listY + 4); - gfx.print("No networks found"); - return; - } - - int visibleLines = (Theme::SCREEN_H - Theme::TAB_BAR_H - listY) / lineH; - - // Scroll - if (_wifiPickerIdx < _wifiPickerScroll) _wifiPickerScroll = _wifiPickerIdx; - if (_wifiPickerIdx >= _wifiPickerScroll + visibleLines) _wifiPickerScroll = _wifiPickerIdx - visibleLines + 1; - - for (int r = 0; r < visibleLines; r++) { - int i = _wifiPickerScroll + r; - if (i >= (int)_wifiResults.size()) break; - - int rowY = listY + r * lineH; - bool selected = (i == _wifiPickerIdx); - auto& net = _wifiResults[i]; - - if (selected) { - gfx.fillRect(0, rowY, Theme::SCREEN_W, lineH - 1, Theme::SELECTION_BG); - } - uint32_t bg = selected ? Theme::SELECTION_BG : Theme::BG; - - // Lock icon for encrypted - gfx.setTextColor(Theme::MUTED, bg); - gfx.setCursor(4, rowY + 4); - gfx.print(net.encrypted ? "*" : " "); - - // SSID - gfx.setTextColor(selected ? Theme::ACCENT : Theme::PRIMARY, bg); - gfx.setCursor(14, rowY + 4); - gfx.print(net.ssid.c_str()); - - // Signal bars - int bars = 1; - if (net.rssi > -50) bars = 4; - else if (net.rssi > -65) bars = 3; - else if (net.rssi > -80) bars = 2; - char sig[8]; - snprintf(sig, sizeof(sig), "%ddBm", net.rssi); - gfx.setTextColor(Theme::MUTED, bg); - gfx.setCursor(Theme::SCREEN_W - 50, rowY + 4); - gfx.print(sig); - - // Draw signal bars - int barX = Theme::SCREEN_W - 58; - for (int b = 0; b < 4; b++) { - int barH2 = 3 + b * 2; - int barY2 = rowY + 12 - barH2; - uint32_t barCol = (b < bars) ? Theme::ACCENT : Theme::BORDER; - gfx.fillRect(barX + b * 4, barY2, 3, barH2, barCol); - } - } - - // Bottom hint - int hintY = Theme::SCREEN_H - Theme::TAB_BAR_H - 12; - gfx.setTextColor(Theme::MUTED, Theme::BG); - gfx.setCursor(4, hintY); - gfx.print("Enter=Select Bksp=Back"); -} - -// ============================================================================= -// Key handling -// ============================================================================= -bool SettingsScreen::handleKey(const KeyEvent& event) { - switch (_view) { - case SettingsView::CATEGORY_LIST: return handleCategoryKeys(event); - case SettingsView::ITEM_LIST: return handleItemKeys(event); - case SettingsView::WIFI_PICKER: return handleWifiPickerKeys(event); - } - return false; -} - -bool SettingsScreen::handleCategoryKeys(const KeyEvent& event) { - if (event.up) { - if (_categoryIdx > 0) { _categoryIdx--; markDirty(); } - return true; - } - if (event.down) { - if (_categoryIdx < (int)_categories.size() - 1) { _categoryIdx++; markDirty(); } - return true; - } - if (event.enter || event.character == '\n' || event.character == '\r') { - enterCategory(_categoryIdx); - return true; - } - return false; -} - -bool SettingsScreen::handleItemKeys(const KeyEvent& event) { - if (_items.empty()) return false; - - // Text edit mode - if (_textEditing) { - auto& item = _items[_selectedIdx]; - if (event.enter || event.character == '\n' || event.character == '\r') { - if (item.textSetter) item.textSetter(_editText); - _textEditing = false; - applyAndSave(); - markDirty(); - return true; - } - if (event.del || event.character == 8) { - if (_editText.length() > 0) { - _editText.remove(_editText.length() - 1); - markDirty(); - } - return true; - } - if (event.character == 0x1B) { - _textEditing = false; - markDirty(); - return true; - } - if (event.character >= 0x20 && event.character <= 0x7E - && (int)_editText.length() < item.maxTextLen) { - _editText += (char)event.character; - markDirty(); - return true; - } - return true; - } - - // Value edit mode - if (_editing) { - auto& item = _items[_selectedIdx]; - if (event.left) { - _editValue -= item.step; - if (_editValue < item.minVal) _editValue = item.minVal; - markDirty(); - return true; - } - if (event.right) { - _editValue += item.step; - if (_editValue > item.maxVal) _editValue = item.maxVal; - markDirty(); - return true; - } - if (event.enter || event.character == '\n' || event.character == '\r') { - if (item.setter) item.setter(_editValue); - _editing = false; - applyAndSave(); - markDirty(); - return true; - } - if (event.del || event.character == 8 || event.character == 0x1B) { - _editing = false; - markDirty(); - return true; - } - return true; - } - - // Browse mode - if (event.up) { - int prev = _selectedIdx; - skipToNextEditable(-1); - if (_selectedIdx != prev) markDirty(); - return true; - } - if (event.down) { - int prev = _selectedIdx; - skipToNextEditable(1); - if (_selectedIdx != prev) markDirty(); - return true; - } - - // Back to categories - if (event.del || event.character == 8 || event.character == 0x1B) { - exitToCategories(); - return true; - } - - if (event.enter || event.character == '\n' || event.character == '\r') { - if (!isEditable(_selectedIdx)) return true; - auto& item = _items[_selectedIdx]; - - if (item.type == SettingType::ACTION) { - if (item.action) item.action(); - markDirty(); - } else if (item.type == SettingType::TEXT_INPUT) { - // WiFi SSID: open scanner - if (strcmp(item.label, "WiFi SSID") == 0) { - // Show scanning screen, render it, then scan (blocking) - _wifiResults.clear(); - _wifiPickerIdx = 0; - _wifiPickerScroll = 0; - _wifiScanning = true; - _view = SettingsView::WIFI_PICKER; - // Force immediate render of "Scanning..." before blocking - if (_ui) { - markDirty(); - _ui->render(); - } - // Now do the blocking scan - _wifiScanning = false; - _wifiResults = WiFiInterface::scanNetworks(); - if (_wifiResults.empty()) { - if (_ui) _ui->statusBar().showToast("No networks found", 1500); - _view = SettingsView::ITEM_LIST; - } - markDirty(); - return true; - } - // Normal text input - _textEditing = true; - _editText = item.textGetter ? item.textGetter() : ""; - markDirty(); - } else if (item.type == SettingType::TOGGLE) { - int val = item.getter ? item.getter() : 0; - if (item.setter) item.setter(val ? 0 : 1); - applyAndSave(); - markDirty(); - } else { - // Enter value edit mode - _editing = true; - _editValue = item.getter ? item.getter() : 0; - markDirty(); - } - return true; - } - return false; -} - -bool SettingsScreen::handleWifiPickerKeys(const KeyEvent& event) { - if (event.up) { - if (_wifiPickerIdx > 0) { _wifiPickerIdx--; markDirty(); } - return true; - } - if (event.down) { - if (_wifiPickerIdx < (int)_wifiResults.size() - 1) { _wifiPickerIdx++; markDirty(); } - return true; - } - if (event.enter || event.character == '\n' || event.character == '\r') { - if (_wifiPickerIdx < (int)_wifiResults.size()) { - auto& net = _wifiResults[_wifiPickerIdx]; - if (_cfg) { - _cfg->settings().wifiSTASSID = net.ssid; - applyAndSave(); - } - if (_ui) _ui->statusBar().showToast(("Selected: " + net.ssid).c_str(), 1500); - } - _view = SettingsView::ITEM_LIST; - markDirty(); - return true; - } - if (event.del || event.character == 8 || event.character == 0x1B) { - _view = SettingsView::ITEM_LIST; - markDirty(); - return true; - } - return false; -} - -// ============================================================================= -// Apply & Save -// ============================================================================= -void SettingsScreen::applyAndSave() { - if (!_cfg) return; - auto& s = _cfg->settings(); - - if (_power) { - _power->setBrightness(s.brightness); - _power->setDimTimeout(s.screenDimTimeout); - _power->setOffTimeout(s.screenOffTimeout); - } - if (_radio && _radio->isRadioOnline()) { - _radio->setFrequency(s.loraFrequency); - _radio->setTxPower(s.loraTxPower); - _radio->setSpreadingFactor(s.loraSF); - _radio->setSignalBandwidth(s.loraBW); - _radio->setCodingRate4(s.loraCR); - _radio->setPreambleLength(s.loraPreamble); - _radio->receive(); - } - if (_audio) { - _audio->setEnabled(s.audioEnabled); - _audio->setVolume(s.audioVolume); - } - - bool saved = false; - if (_saveCallback) { - saved = _saveCallback(); - } else if (_sd && _flash) { - saved = _cfg->save(*_sd, *_flash); - } else if (_flash) { - saved = _cfg->save(*_flash); - } - - if (_ui) { - _ui->statusBar().showToast(saved ? "Saved" : "Applied", 800); - } - - Serial.printf("[SETTINGS] Applied, save=%s\n", saved ? "OK" : "FAILED"); -} diff --git a/src/ui/screens/SettingsScreen.h b/src/ui/screens/SettingsScreen.h deleted file mode 100644 index a89c558..0000000 --- a/src/ui/screens/SettingsScreen.h +++ /dev/null @@ -1,142 +0,0 @@ -#pragma once - -#include "ui/UIManager.h" -#include "transport/WiFiInterface.h" -#include -#include -#include - -class UserConfig; -class FlashStore; -class SDStore; -class SX1262; -class AudioNotify; -class Power; -class WiFiInterface; -class TCPClientInterface; -class ReticulumManager; -class IdentityManager; - -enum class SettingType : uint8_t { - READONLY, - INTEGER, - TOGGLE, - ENUM_CHOICE, - ACTION, - TEXT_INPUT -}; - -enum class SettingsView : uint8_t { - CATEGORY_LIST, - ITEM_LIST, - WIFI_PICKER -}; - -struct SettingItem { - const char* label; - SettingType type; - std::function getter; - std::function setter; - std::function formatter; - int minVal = 0; - int maxVal = 1; - int step = 1; - std::vector enumLabels; - std::function action; - std::function textGetter; - std::function textSetter; - int maxTextLen = 16; -}; - -struct SettingsCategory { - const char* name; - int startIdx; - int count; - std::function summary; -}; - -class SettingsScreen : public Screen { -public: - void onEnter() override; - void update() override; - bool handleKey(const KeyEvent& event) override; - - void setUserConfig(UserConfig* cfg) { _cfg = cfg; } - void setFlashStore(FlashStore* fs) { _flash = fs; } - void setSDStore(SDStore* sd) { _sd = sd; } - void setRadio(SX1262* radio) { _radio = radio; } - void setAudio(AudioNotify* audio) { _audio = audio; } - void setPower(Power* power) { _power = power; } - void setWiFi(WiFiInterface* wifi) { _wifi = wifi; } - void setTCPClients(std::vector* tcp) { _tcp = tcp; } - void setRNS(ReticulumManager* rns) { _rns = rns; } - void setIdentityManager(IdentityManager* idm) { _idMgr = idm; } - void setUIManager(UIManager* ui) { _ui = ui; } - void setIdentityHash(const String& hash) { _identityHash = hash; } - void setSaveCallback(std::function cb) { _saveCallback = cb; } - - const char* title() const override { return "Settings"; } - void draw(LGFX_TDeck& gfx) override; - -private: - void buildItems(); - void applyAndSave(); - void applyPreset(int presetIdx); - int detectPreset() const; - - void drawCategoryList(LGFX_TDeck& gfx); - void drawItemList(LGFX_TDeck& gfx); - void drawWifiPicker(LGFX_TDeck& gfx); - - bool handleCategoryKeys(const KeyEvent& event); - bool handleItemKeys(const KeyEvent& event); - bool handleWifiPickerKeys(const KeyEvent& event); - - void enterCategory(int catIdx); - void exitToCategories(); - void skipToNextEditable(int dir); - bool isEditable(int idx) const; - - UserConfig* _cfg = nullptr; - FlashStore* _flash = nullptr; - SDStore* _sd = nullptr; - SX1262* _radio = nullptr; - AudioNotify* _audio = nullptr; - Power* _power = nullptr; - WiFiInterface* _wifi = nullptr; - std::vector* _tcp = nullptr; - ReticulumManager* _rns = nullptr; - IdentityManager* _idMgr = nullptr; - UIManager* _ui = nullptr; - String _identityHash; - - std::function _saveCallback; - - // View state - SettingsView _view = SettingsView::CATEGORY_LIST; - - // Categories - std::vector _categories; - int _categoryIdx = 0; - int _categoryScroll = 0; - - // Items (within current category) - std::vector _items; - int _selectedIdx = 0; - int _itemScroll = 0; - int _catRangeStart = 0; - int _catRangeEnd = 0; - - // Edit state - bool _editing = false; - int _editValue = 0; - bool _textEditing = false; - String _editText; - bool _confirmingReset = false; - - // WiFi picker - std::vector _wifiResults; - int _wifiPickerIdx = 0; - int _wifiPickerScroll = 0; - bool _wifiScanning = false; -};