ui: QR sharing overlay + Share My QR header on Contacts tab

Adds an lxma://<hash>:<pubkey> QR overlay reachable from Settings and
from a new header row at the top of the Contacts tab. Including the
public key lets Columba/Sideband skip the PENDING_IDENTITY round-trip.

Contacts list stays visible even with no saved contacts so the header
row is always reachable; the empty-state floats above it.
This commit is contained in:
drkhsh
2026-05-05 20:39:58 +02:00
parent 00782336f3
commit d9c92abf6c
8 changed files with 187 additions and 8 deletions
+3 -2
View File
@@ -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
+20
View File
@@ -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://<destHash>:<publicKey>` 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.
+47 -6
View File
@@ -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();
+2
View File
@@ -18,6 +18,7 @@ public:
void setAnnounceManager(AnnounceManager* am) { _am = am; }
void setNodeSelectedCallback(NodeSelectedCallback cb) { _onSelect = cb; }
void setShowQrCallback(std::function<void()> 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<void()> _showQrCb;
bool _confirmDelete = false;
bool _focusActive = false;
int _deleteIdx = -1;
+82
View File
@@ -0,0 +1,82 @@
#include "LvQrOverlay.h"
#include "ui/Theme.h"
#include "ui/LvTheme.h"
#include <lvgl.h>
#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://<hash>:<pubkey> — 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;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "ui/UIManager.h"
// Fullscreen QR overlay — encodes lxma://<hash>:<pubkey> 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;
};
+12
View File
@@ -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";
+2
View File
@@ -80,6 +80,7 @@ public:
void setSaveCallback(std::function<bool()> cb) { _saveCallback = cb; }
void setTCPChangeCallback(std::function<void()> cb) { _tcpChangeCb = cb; }
void setGPSChangeCallback(std::function<void(bool enabled)> cb) { _gpsChangeCb = cb; }
void setShowQrCallback(std::function<void()> cb) { _showQrCb = cb; }
const char* title() const override { return "Settings"; }
@@ -136,6 +137,7 @@ private:
std::function<bool()> _saveCallback;
std::function<void()> _tcpChangeCb;
std::function<void(bool)> _gpsChangeCb;
std::function<void()> _showQrCb;
bool _gpsSnapEnabled = true;
SettingsView _view = SettingsView::CATEGORY_LIST;