diff --git a/lv_conf.h b/lv_conf.h index f2bf047..5d4104f 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -63,14 +63,15 @@ extern const lv_font_t lv_font_ratdeck_14; #define LV_USE_ROLLER 1 #define LV_USE_TABLE 0 #define LV_USE_TABVIEW 0 -#define LV_USE_IMG 1 +#define LV_USE_IMG 1 // required by LV_USE_CANVAS / LV_USE_QRCODE #define LV_USE_LINE 0 #define LV_USE_ARC 0 #define LV_USE_SPINNER 0 #define LV_USE_MSGBOX 0 #define LV_USE_KEYBOARD 0 #define LV_USE_CHECKBOX 0 -#define LV_USE_CANVAS 1 +#define LV_USE_CANVAS 1 // required by LV_USE_QRCODE +#define LV_USE_QRCODE 1 // Layouts #define LV_USE_FLEX 1 diff --git a/src/main.cpp b/src/main.cpp index e02d384..7c7817e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,6 +33,7 @@ #include "ui/screens/LvContactsScreen.h" #include "ui/screens/LvSettingsScreen.h" #include "ui/screens/LvHelpOverlay.h" +#include "ui/screens/LvQrOverlay.h" // Map screen removed #include "ui/screens/LvNameInputScreen.h" #include "ui/screens/LvTimezoneScreen.h" @@ -117,6 +118,7 @@ LvContactsScreen lvContactsScreen; LvMessageView lvMessageView; LvSettingsScreen lvSettingsScreen; LvHelpOverlay lvHelpOverlay; +LvQrOverlay lvQrOverlay; // LvMapScreen removed LvNameInputScreen lvNameInputScreen; LvTimezoneScreen lvTimezoneScreen; @@ -1071,8 +1073,22 @@ void setup() { }); #endif + auto showQr = []() { + // Encode `lxma://:` so Columba/Sideband + // scanners get a full identity (no PENDING_IDENTITY round-trip). + String destHex = rns.destinationHashHex(); + String pubHex; + if (auto identity = rns.destination().identity()) { + pubHex = String(identity.get_public_key().toHex().c_str()); + } + lvQrOverlay.show(destHex, pubHex); + }; + lvSettingsScreen.setShowQrCallback(showQr); + lvContactsScreen.setShowQrCallback(showQr); + // LVGL help overlay lvHelpOverlay.create(); + lvQrOverlay.create(); // Tab bar callbacks — LVGL lvTabScreens[LvTabBar::TAB_HOME] = &lvHomeScreen; @@ -1250,6 +1266,10 @@ void loop() { if (lvHelpOverlay.isVisible()) { lvHelpOverlay.handleKey(evt); } + // QR overlay also dismisses on any keypress while visible + else if (lvQrOverlay.isVisible()) { + lvQrOverlay.handleKey(evt); + } else { // Screen-local input owns the keyboard. This keeps message and // settings text entry from being preempted by global shortcuts. diff --git a/src/ui/screens/LvContactsScreen.cpp b/src/ui/screens/LvContactsScreen.cpp index 7f8bcf4..ce5ffc0 100644 --- a/src/ui/screens/LvContactsScreen.cpp +++ b/src/ui/screens/LvContactsScreen.cpp @@ -156,14 +156,55 @@ void LvContactsScreen::rebuildList() { _lastContactCount = count; _avatarBuffers.reserve(count); - if (count == 0) { - if (_emptyState) lv_obj_clear_flag(_emptyState, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(_list, LV_OBJ_FLAG_HIDDEN); - return; + // Keep list visible even with no contacts so the QR header row is reachable. + lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN); + if (_emptyState) { + if (count == 0) { + lv_obj_clear_flag(_emptyState, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(_emptyState); + } else { + lv_obj_add_flag(_emptyState, LV_OBJ_FLAG_HIDDEN); + } } - if (_emptyState) lv_obj_add_flag(_emptyState, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(_list, LV_OBJ_FLAG_HIDDEN); + { + lv_obj_t* qrRow = lv_obj_create(_list); + lv_obj_set_size(qrRow, Theme::CONTENT_W, 28); + lv_obj_add_style(qrRow, LvTheme::styleListBtn(), 0); + lv_obj_add_style(qrRow, LvTheme::styleListBtnFocused(), LV_STATE_FOCUSED); + lv_obj_set_style_bg_color(qrRow, lv_color_hex(Theme::PRIMARY_SUBTLE), 0); + lv_obj_set_style_bg_opa(qrRow, LV_OPA_COVER, 0); + lv_obj_set_style_border_side(qrRow, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_border_width(qrRow, 1, 0); + lv_obj_set_style_border_color(qrRow, lv_color_hex(Theme::BORDER), 0); + lv_obj_set_style_pad_all(qrRow, 0, 0); + lv_obj_clear_flag(qrRow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(qrRow, LV_OBJ_FLAG_CLICKABLE); + // Sentinel marks this as not-a-contact for handleLongPress(). + lv_obj_set_user_data(qrRow, (void*)(intptr_t)-1); + + lv_obj_add_event_cb(qrRow, [](lv_event_t* e) { + auto* self = (LvContactsScreen*)lv_event_get_user_data(e); + if (self->_showQrCb) self->_showQrCb(); + }, LV_EVENT_CLICKED, this); + + lv_group_add_obj(LvInput::group(), qrRow); + lv_obj_add_event_cb(qrRow, [](lv_event_t* e) { + lv_obj_scroll_to_view(lv_event_get_target(e), LV_ANIM_ON); + }, LV_EVENT_FOCUSED, nullptr); + + lv_obj_t* qrLbl = lv_label_create(qrRow); + lv_obj_set_style_text_font(qrLbl, &lv_font_ratdeck_14, 0); + lv_obj_set_style_text_color(qrLbl, lv_color_hex(Theme::ACCENT), 0); + lv_label_set_text(qrLbl, "Share My QR"); + lv_obj_align(qrLbl, LV_ALIGN_LEFT_MID, 12, 0); + + lv_obj_t* hintLbl = lv_label_create(qrRow); + lv_obj_set_style_text_font(hintLbl, &lv_font_ratdeck_10, 0); + lv_obj_set_style_text_color(hintLbl, lv_color_hex(Theme::TEXT_MUTED), 0); + lv_label_set_text(hintLbl, "Enter"); + lv_obj_align(hintLbl, LV_ALIGN_RIGHT_MID, -12, 0); + } unsigned long now = millis(); diff --git a/src/ui/screens/LvContactsScreen.h b/src/ui/screens/LvContactsScreen.h index 98a3f76..1f0c305 100644 --- a/src/ui/screens/LvContactsScreen.h +++ b/src/ui/screens/LvContactsScreen.h @@ -18,6 +18,7 @@ public: void setAnnounceManager(AnnounceManager* am) { _am = am; } void setNodeSelectedCallback(NodeSelectedCallback cb) { _onSelect = cb; } + void setShowQrCallback(std::function cb) { _showQrCb = cb; } void setUIManager(class UIManager* ui) { _ui = ui; } bool handleLongPress() override; @@ -29,6 +30,7 @@ private: AnnounceManager* _am = nullptr; class UIManager* _ui = nullptr; NodeSelectedCallback _onSelect; + std::function _showQrCb; bool _confirmDelete = false; bool _focusActive = false; int _deleteIdx = -1; diff --git a/src/ui/screens/LvQrOverlay.cpp b/src/ui/screens/LvQrOverlay.cpp new file mode 100644 index 0000000..3c45078 --- /dev/null +++ b/src/ui/screens/LvQrOverlay.cpp @@ -0,0 +1,82 @@ +#include "LvQrOverlay.h" +#include "ui/Theme.h" +#include "ui/LvTheme.h" +#include +#include "fonts/fonts.h" + +void LvQrOverlay::create() { + if (_overlay) return; + _overlay = lv_obj_create(lv_layer_top()); + lv_obj_set_size(_overlay, Theme::SCREEN_W, Theme::SCREEN_H); + lv_obj_align(_overlay, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_bg_color(_overlay, lv_color_hex(Theme::BG), 0); + lv_obj_set_style_bg_opa(_overlay, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(_overlay, 0, 0); + lv_obj_set_style_pad_all(_overlay, 6, 0); + lv_obj_set_style_pad_row(_overlay, 4, 0); + lv_obj_set_layout(_overlay, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(_overlay, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(_overlay, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(_overlay, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* title = lv_label_create(_overlay); + lv_obj_set_style_text_font(title, &lv_font_ratdeck_14, 0); + lv_obj_set_style_text_color(title, lv_color_hex(Theme::ACCENT), 0); + lv_label_set_text(title, "Scan to add me"); + + // 180px; dark-on-light for standard scanner contrast on 320x240. + _qr = lv_qrcode_create(_overlay, + 180, + lv_color_hex(0x000000), + lv_color_hex(0xFFFFFF)); + + _lblAddr = lv_label_create(_overlay); + lv_obj_set_style_text_font(_lblAddr, &lv_font_ratdeck_10, 0); + lv_obj_set_style_text_color(_lblAddr, lv_color_hex(Theme::TEXT_MUTED), 0); + lv_label_set_long_mode(_lblAddr, LV_LABEL_LONG_WRAP); + lv_obj_set_width(_lblAddr, Theme::SCREEN_W - 12); + lv_obj_set_style_text_align(_lblAddr, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(_lblAddr, ""); + + lv_obj_t* hint = lv_label_create(_overlay); + lv_obj_set_style_text_font(hint, &lv_font_ratdeck_10, 0); + lv_obj_set_style_text_color(hint, lv_color_hex(Theme::TEXT_MUTED), 0); + lv_label_set_text(hint, "Any key to close"); + + lv_obj_add_flag(_overlay, LV_OBJ_FLAG_HIDDEN); +} + +void LvQrOverlay::show(const String& destHashHex, const String& publicKeyHex) { + if (!_overlay) create(); + + // lxma://: — Columba/Sideband format; pubkey skips + // the PENDING_IDENTITY round-trip on the scanning end. + String payload = "lxma://" + destHashHex; + if (publicKeyHex.length() > 0) { + payload += ":" + publicKeyHex; + } + + if (_qr) { + if (lv_qrcode_update(_qr, payload.c_str(), payload.length()) != LV_RES_OK) { + return; + } + } + if (_lblAddr) { + lv_label_set_text(_lblAddr, destHashHex.c_str()); + } + + lv_obj_clear_flag(_overlay, LV_OBJ_FLAG_HIDDEN); + _visible = true; +} + +void LvQrOverlay::hide() { + if (_overlay) lv_obj_add_flag(_overlay, LV_OBJ_FLAG_HIDDEN); + _visible = false; +} + +bool LvQrOverlay::handleKey(const KeyEvent& event) { + if (!_visible) return false; + (void)event; + hide(); + return true; +} diff --git a/src/ui/screens/LvQrOverlay.h b/src/ui/screens/LvQrOverlay.h new file mode 100644 index 0000000..68ab22b --- /dev/null +++ b/src/ui/screens/LvQrOverlay.h @@ -0,0 +1,19 @@ +#pragma once + +#include "ui/UIManager.h" + +// Fullscreen QR overlay — encodes lxma://: for Columba/Sideband. +class LvQrOverlay { +public: + void create(); + void show(const String& destHashHex, const String& publicKeyHex); + void hide(); + bool isVisible() const { return _visible; } + bool handleKey(const KeyEvent& event); + +private: + lv_obj_t* _overlay = nullptr; + lv_obj_t* _qr = nullptr; + lv_obj_t* _lblAddr = nullptr; + bool _visible = false; +}; diff --git a/src/ui/screens/LvSettingsScreen.cpp b/src/ui/screens/LvSettingsScreen.cpp index 883c0c2..4f8d3ea 100644 --- a/src/ui/screens/LvSettingsScreen.cpp +++ b/src/ui/screens/LvSettingsScreen.cpp @@ -957,6 +957,18 @@ void LvSettingsScreen::buildItems() { _items.push_back(announceItem); idx++; } + { + SettingItem showQrItem; + showQrItem.label = "Show QR Code"; + showQrItem.type = SettingType::ACTION; + showQrItem.formatter = [](int) { return String("[Enter]"); }; + showQrItem.action = [this]() { + if (_showQrCb) _showQrCb(); + else if (_ui) _ui->lvStatusBar().showToast("QR not available"); + }; + _items.push_back(showQrItem); + idx++; + } { SettingItem initSD; initSD.label = "Format SD Card"; diff --git a/src/ui/screens/LvSettingsScreen.h b/src/ui/screens/LvSettingsScreen.h index f1ba549..51447ac 100644 --- a/src/ui/screens/LvSettingsScreen.h +++ b/src/ui/screens/LvSettingsScreen.h @@ -80,6 +80,7 @@ public: void setSaveCallback(std::function cb) { _saveCallback = cb; } void setTCPChangeCallback(std::function cb) { _tcpChangeCb = cb; } void setGPSChangeCallback(std::function cb) { _gpsChangeCb = cb; } + void setShowQrCallback(std::function cb) { _showQrCb = cb; } const char* title() const override { return "Settings"; } @@ -136,6 +137,7 @@ private: std::function _saveCallback; std::function _tcpChangeCb; std::function _gpsChangeCb; + std::function _showQrCb; bool _gpsSnapEnabled = true; SettingsView _view = SettingsView::CATEGORY_LIST;