mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-05-14 11:25:05 +00:00
Merge pull request #52 from drkhsh/feat/qr-share
ui: QR sharing overlay + Share My QR header on Contacts tab
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user