chore: format codebase and update changelog

This commit is contained in:
liu weikai
2026-02-02 15:04:17 +08:00
parent 4d941c5ca6
commit 4b68c2f565
135 changed files with 67429 additions and 3377 deletions
+31
View File
@@ -18,6 +18,37 @@ versions.
- Meshtastic encryption (AES/PSK) and broader protocol compatibility / Meshtastic 加密(AES/PSK)与更完整协议兼容
- Real LoRa field tests, performance, and power tuning / 实机 LoRa 测试、性能与功耗调优
## [0.1.4-alpha] - 2026-02-02
### Added / 新增
- T-Deck board support and new build env / T-Deck 板级支持与新构建环境
- GPS tracker/track recorder with Tracker screen and assets / GPS 轨迹记录与 Tracker 页面及资源
- Team NFC support, team chat protocol, and team UI store / Team NFC 支持、团队聊天协议与 Team UI 存储
- Toast widget and LVGL lifetime notes / Toast 组件与 LVGL 生命周期说明
### Changed / 变更
- UI lifecycle refactor with compose flow / UI 生命周期重构与 compose 流程
- GPS page layout/styles and map overlay refactor / GPS 页面布局/样式与地图覆盖层重构
- UI controller and app screen wiring adjustments / UI 控制器与页面接线调整
### Fixed / 修复
- Meshtastic node id handling error / Meshtastic 节点 ID 处理错误
- UI glitches across chat/contacts/gps/team / Chat/Contacts/GPS/Team 页面 UI 问题修复
## [0.1.3-alpha] - 2026-01-23
### Added / 新增
- Team management stack (domain/protocol/service) and Team screen / Team 管理模块(领域/协议/服务)与 Team 页面
- Team docs/assets / Team 文档与资源
- Send-status UI and message preview truncation / 发送状态 UI 与消息预览截断
### Changed / 变更
- Mesh persistence and PKI handling (node store/backoff) / Mesh 持久化与 PKI 处理(节点存储/退避)
- Chat storage improvements across RAM/Flash/log stores / 聊天存储改进(RAM/Flash/日志)
- Diagnostics and timestamp utilities / 诊断信息与时间戳工具改进
### Fixed / 修复
- Messaging reliability across reboots / 重启后消息可靠性
- UI timezone offset application / UI 时区偏移应用
## [0.1.2-alpha] - 2026-01-21
### Added / 新增
- Meshtastic routing ACK decoding with reason mapping, ack timeouts, and richer RX/TX diagnostics / Meshtastic 路由 ACK 解码,增加原因映射、ACK 超时和更详细的收发日志
+3 -3
View File
@@ -7,10 +7,10 @@
#include "../chat/infra/protocol_factory.h"
#include "../gps/usecase/gps_service.h"
#include "../sys/event_bus.h"
#include "../team/protocol/team_chat.h"
#include "../ui/ui_common.h"
#include "../ui/ui_team.h"
#include "../ui/widgets/system_notification.h"
#include "../ui/ui_common.h"
#include "../team/protocol/team_chat.h"
#ifdef USING_ST25R3916
#include "../team/infra/nfc/team_nfc.h"
#endif
@@ -21,7 +21,7 @@ namespace app
{
bool AppContext::init(BoardBase& board, LoraBoard* lora_board, GpsBoard* gps_board, MotionBoard* motion_board,
bool use_mock_adapter, uint32_t disable_hw_init)
bool use_mock_adapter, uint32_t disable_hw_init)
{
// Store board reference for hardware access
board_ = &board;
+4 -4
View File
@@ -17,15 +17,15 @@ namespace app
{
class AppContext;
}
#include "../board/BoardBase.h"
#include "../board/GpsBoard.h"
#include "../board/LoraBoard.h"
#include "../board/MotionBoard.h"
#include "../chat/infra/contact_store.h"
#include "../chat/infra/meshtastic/node_store.h"
#include "../chat/infra/store/flash_store.h"
#include "../chat/infra/store/log_store.h"
#include "../chat/infra/store/ram_store.h"
#include "../board/BoardBase.h"
#include "../board/LoraBoard.h"
#include "../board/GpsBoard.h"
#include "../board/MotionBoard.h"
#include "../chat/ports/i_chat_store.h"
#include "../chat/ports/i_contact_store.h"
#include "../chat/ports/i_mesh_adapter.h"
-1
View File
@@ -16,4 +16,3 @@ class GpsBoard
virtual void powerControl(PowerCtrlChannel_t ch, bool enable) = 0;
virtual bool syncTimeFromGPS(uint32_t gps_task_interval_ms = 0) = 0;
};
+1 -3
View File
@@ -1,8 +1,7 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstdint>
// LoRa ӿڣṩ RadioLib ķʡ
class LoraBoard
@@ -12,7 +11,6 @@ class LoraBoard
virtual bool isRadioOnline() const = 0;
virtual int transmitRadio(const uint8_t* data, size_t len) = 0;
virtual int startRadioReceive() = 0;
virtual uint32_t getRadioIrqFlags() = 0;
-1
View File
@@ -11,4 +11,3 @@ class MotionBoard
virtual SensorBHI260AP& getMotionSensor() = 0;
virtual bool isSensorReady() const = 0;
};
-1
View File
@@ -9,4 +9,3 @@ class SdBoard
virtual void uninstallSD() = 0;
virtual bool isCardReady() = 0;
};
+3 -3
View File
@@ -442,9 +442,9 @@ RotaryMsg_t TDeckBoard::getRotary()
return msg;
}
const uint32_t repeat_ms = 70; // Even more responsive trackball repeat
const uint32_t click_ms = 110; // Click debounce
const uint8_t stable_polls = 2; // Increase sensitivity on T-Deck trackball
const uint32_t repeat_ms = 70; // Even more responsive trackball repeat
const uint32_t click_ms = 110; // Click debounce
const uint8_t stable_polls = 2; // Increase sensitivity on T-Deck trackball
#if defined(TRACKBALL_RIGHT) && defined(TRACKBALL_LEFT)
const bool right_pressed = digitalRead(TRACKBALL_RIGHT) == LOW;
+4 -5
View File
@@ -7,9 +7,9 @@
#include "freertos/task.h"
#include "freertos/timers.h"
#include <cstring>
#include <driver/gpio.h>
#include <esp_sleep.h>
#include <cstring>
#include "display/drivers/ST7796.h"
#include "pins_arduino.h"
@@ -454,8 +454,7 @@ uint32_t TLoRaPagerBoard::begin(uint32_t disable_hw_init)
Serial.printf("[TLoRaPagerBoard::begin] ===== HARDWARE INITIALIZATION COMPLETE =====\n");
Serial.printf("[TLoRaPagerBoard::begin] devices_probe=0x%08X\n", devices_probe);
const char* gps_state =
(devices_probe & HW_GPS_ONLINE) ? "YES" :
((disable_hw_init & NO_HW_GPS) ? "SKIPPED" : "DEFERRED");
(devices_probe & HW_GPS_ONLINE) ? "YES" : ((disable_hw_init & NO_HW_GPS) ? "SKIPPED" : "DEFERRED");
Serial.printf("[TLoRaPagerBoard::begin] GPS online: %s\n", gps_state);
Serial.printf("[TLoRaPagerBoard::begin] NFC online: %s (HW_NFC_ONLINE=0x%08X)\n",
(devices_probe & HW_NFC_ONLINE) ? "YES" : "NO", HW_NFC_ONLINE);
@@ -921,7 +920,7 @@ bool TLoRaPagerBoard::startNFCDiscovery(uint16_t techs2Find, uint16_t totalDurat
if (techs2Find & RFAL_NFC_LISTEN_TECH_A)
{
static bool nfcid_init = false;
static uint8_t nfcid[RFAL_NFCID1_TRIPLE_LEN] = { 0 };
static uint8_t nfcid[RFAL_NFCID1_TRIPLE_LEN] = {0};
if (!nfcid_init)
{
nfcid[0] = static_cast<uint8_t>(random(1, 255));
@@ -1966,7 +1965,7 @@ void TLoRaPagerBoard::softwareShutdown()
return;
}
log_i("Shutdown conditions met - entering Power OFF mode (26µA)");
log_i("Shutdown conditions met - entering Power OFF mode (26µA)");
shutdown(true);
}
+1 -1
View File
@@ -428,7 +428,7 @@ class TLoRaPagerBoard : public BoardBase,
// Two-stage power-off implementation
bool isUsbPresent_bestEffort();
uint32_t devices_probe = 0; ///< Hardware detection status bitmask
uint32_t devices_probe = 0; ///< Hardware detection status bitmask
uint8_t _haptic_effects = 15; ///< Default haptic effect (strong buzz for message notification)
};
+1 -1
View File
@@ -21,7 +21,6 @@
#include <ndef_type_wifi.h>
#include <ndef_types.h>
#include <nfc_utils.h>
#include <rfal_rf.h>
#include <rfal_isoDep.h>
#include <rfal_nfc.h>
#include <rfal_nfcDep.h>
@@ -29,6 +28,7 @@
#include <rfal_nfcb.h>
#include <rfal_nfcf.h>
#include <rfal_nfcv.h>
#include <rfal_rf.h>
#ifdef rfalRunBlocking
#undef rfalRunBlocking
#endif
+51 -38
View File
@@ -5,22 +5,22 @@
#include "mt_adapter.h"
#include "../../../sys/event_bus.h"
#include "../../../team/protocol/team_portnum.h"
#include "../../domain/contact_types.h"
#include "../../ports/i_node_store.h"
#include "../../../team/protocol/team_portnum.h"
#include <Arduino.h>
#include <Crypto.h>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <ctime>
#include <string>
#define TEST_CURVE25519_FIELD_OPS
#include "../../../board/TLoRaPagerTypes.h"
#include "generated/meshtastic/config.pb.h"
#include "node_persist.h"
#include "mt_region.h"
#include "node_persist.h"
#include <AES.h>
#include <Curve25519.h>
#include <Preferences.h>
@@ -52,13 +52,13 @@ constexpr const char* kSecondaryChannelName = "Squad";
constexpr uint8_t kLoraSyncWord = 0x2b;
constexpr uint16_t kLoraPreambleLen = 16;
constexpr uint8_t kBitfieldWantResponseMask = 0x02;
using chat::meshtastic::PersistedNodeEntry;
using chat::meshtastic::kPersistMaxNodes;
using chat::meshtastic::kPersistNodesKey;
using chat::meshtastic::kPersistNodesKeyCrc;
using chat::meshtastic::kPersistNodesKeyVer;
using chat::meshtastic::kPersistNodesNs;
using chat::meshtastic::kPersistVersion;
using chat::meshtastic::PersistedNodeEntry;
bool allowPkiForPortnum(uint32_t portnum)
{
@@ -610,11 +610,11 @@ static void initPkiNonce(uint32_t from, uint64_t packet_id, uint32_t extra_nonce
// Use protobuf codec and wire packet functions
using chat::meshtastic::buildWirePacket;
using chat::meshtastic::decodeTextMessage;
using chat::meshtastic::decodeKeyVerificationMessage;
using chat::meshtastic::decodeTextMessage;
using chat::meshtastic::decryptPayload;
using chat::meshtastic::encodeNodeInfoMessage;
using chat::meshtastic::encodeAppData;
using chat::meshtastic::encodeNodeInfoMessage;
using chat::meshtastic::encodeTextMessage;
using chat::meshtastic::PacketHeaderWire;
using chat::meshtastic::parseWirePacket;
@@ -636,19 +636,19 @@ MtAdapter::MtAdapter(LoraBoard& board)
primary_psk_len_(0),
secondary_channel_hash_(0),
secondary_psk_{0},
secondary_psk_len_(0),
pki_ready_(false),
pki_public_key_{},
pki_private_key_{},
kv_state_(KeyVerificationState::Idle),
kv_nonce_(0),
kv_nonce_ms_(0),
kv_security_number_(0),
kv_remote_node_(0),
kv_hash1_{},
kv_hash2_{},
last_raw_packet_len_(0),
has_pending_raw_packet_(false)
secondary_psk_len_(0),
pki_ready_(false),
pki_public_key_{},
pki_private_key_{},
kv_state_(KeyVerificationState::Idle),
kv_nonce_(0),
kv_nonce_ms_(0),
kv_security_number_(0),
kv_remote_node_(0),
kv_hash1_{},
kv_hash2_{},
last_raw_packet_len_(0),
has_pending_raw_packet_(false)
{
config_ = MeshConfig(); // Default config
initNodeIdentity();
@@ -904,11 +904,16 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
header.flags,
(unsigned)payload_size);
const char* channel_kind = "UNKNOWN";
if (header.channel == 0) {
if (header.channel == 0)
{
channel_kind = "PKI";
} else if (header.channel == primary_channel_hash_) {
}
else if (header.channel == primary_channel_hash_)
{
channel_kind = "PRIMARY";
} else if (header.channel == secondary_channel_hash_) {
}
else if (header.channel == secondary_channel_hash_)
{
channel_kind = "SECONDARY";
}
LORA_LOG("[LORA] RX channel kind=%s hash=0x%02X\n", channel_kind, header.channel);
@@ -1090,10 +1095,13 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size)
auto it = nodeinfo_last_seen_ms_.find(node_id);
bool allow_reply = true;
// 直连 NodeInfo 总是允许回复(不受时间抑制)
if (header.to == 0xFFFFFFFF) { // 只有广播 NodeInfo 才检查时间抑制
if (it != nodeinfo_last_seen_ms_.end()) {
if (header.to == 0xFFFFFFFF)
{ // 只有广播 NodeInfo 才检查时间抑制
if (it != nodeinfo_last_seen_ms_.end())
{
uint32_t since = now_ms - it->second;
if (since < NODEINFO_REPLY_SUPPRESS_MS) {
if (since < NODEINFO_REPLY_SUPPRESS_MS)
{
allow_reply = false;
}
}
@@ -1730,7 +1738,7 @@ void MtAdapter::configureRadio()
#if defined(ARDUINO_LILYGO_LORA_SX1262) || defined(ARDUINO_LILYGO_LORA_SX1280)
board_.configureLoraRadio(freq_mhz, bw_khz, sf, cr_denom, config_.tx_power,
kLoraPreambleLen, kLoraSyncWord, 2);
kLoraPreambleLen, kLoraSyncWord, 2);
#endif
ready_ = true;
@@ -1776,13 +1784,12 @@ void MtAdapter::initNodeIdentity()
// 你的原逻辑:取 MAC 的后 4 字节作为 node_id
node_id_ = (static_cast<uint32_t>(mac_addr_[2]) << 24) |
(static_cast<uint32_t>(mac_addr_[3]) << 16) |
(static_cast<uint32_t>(mac_addr_[4]) << 8) |
(static_cast<uint32_t>(mac_addr_[5]) << 0);
(static_cast<uint32_t>(mac_addr_[4]) << 8) |
(static_cast<uint32_t>(mac_addr_[5]) << 0);
LORA_LOG("[LORA] node_id=0x%08X\n", static_cast<unsigned>(node_id_));
}
void MtAdapter::updateChannelKeys()
{
if (isZeroKey(config_.primary_key, sizeof(config_.primary_key)))
@@ -1904,11 +1911,13 @@ bool MtAdapter::initPkiKeys()
void MtAdapter::loadPkiNodeKeys()
{
struct PkiKeyEntry {
struct PkiKeyEntry
{
uint32_t node_id;
uint8_t key[32];
};
struct PkiKeyEntryV2 {
struct PkiKeyEntryV2
{
uint32_t node_id;
uint32_t last_seen;
uint8_t key[32];
@@ -1917,7 +1926,8 @@ void MtAdapter::loadPkiNodeKeys()
std::vector<PkiKeyEntryV2> entries_v2;
size_t actual = 0;
bool needs_migrate = false;
auto loadFromNs = [&](const char* ns) -> bool {
auto loadFromNs = [&](const char* ns) -> bool
{
Preferences prefs;
if (!prefs.begin(ns, true))
{
@@ -2041,7 +2051,8 @@ void MtAdapter::loadPkiNodeKeys()
void MtAdapter::savePkiNodeKey(uint32_t node_id, const uint8_t* key, size_t key_len)
{
if (node_id == 0 || !key || key_len != 32) {
if (node_id == 0 || !key || key_len != 32)
{
return;
}
std::array<uint8_t, 32> key_copy{};
@@ -2053,7 +2064,8 @@ void MtAdapter::savePkiNodeKey(uint32_t node_id, const uint8_t* key, size_t key_
void MtAdapter::savePkiKeysToPrefs()
{
struct PkiKeyEntryV2 {
struct PkiKeyEntryV2
{
uint32_t node_id;
uint32_t last_seen;
uint8_t key[32];
@@ -2073,7 +2085,8 @@ void MtAdapter::savePkiKeysToPrefs()
if (entries.size() > kMaxPkiNodes)
{
std::sort(entries.begin(), entries.end(),
[](const PkiKeyEntryV2& a, const PkiKeyEntryV2& b) {
[](const PkiKeyEntryV2& a, const PkiKeyEntryV2& b)
{
return a.last_seen < b.last_seen;
});
size_t drop = entries.size() - kMaxPkiNodes;
@@ -2175,8 +2188,8 @@ bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id,
}
bool MtAdapter::encryptPkiPayload(uint32_t dest, uint32_t packet_id,
const uint8_t* plain, size_t plain_len,
uint8_t* out_cipher, size_t* out_cipher_len)
const uint8_t* plain, size_t plain_len,
uint8_t* out_cipher, size_t* out_cipher_len)
{
if (!plain || !out_cipher || !out_cipher_len) return false;
if (!pki_ready_) return false;
+12 -9
View File
@@ -5,19 +5,22 @@
#include "protocol_factory.h"
#include "../../board/LoraBoard.h"
#include "meshtastic/mt_adapter.h"
#include "meshcore/meshcore_adapter.h"
#include "meshtastic/mt_adapter.h"
namespace chat {
namespace chat
{
std::unique_ptr<IMeshAdapter> ProtocolFactory::createAdapter(MeshProtocol protocol,
LoraBoard& board) {
switch (protocol) {
case MeshProtocol::MeshCore:
return std::unique_ptr<IMeshAdapter>(new chat::meshcore::MeshCoreAdapter(board));
case MeshProtocol::Meshtastic:
default:
return std::unique_ptr<IMeshAdapter>(new chat::meshtastic::MtAdapter(board));
LoraBoard& board)
{
switch (protocol)
{
case MeshProtocol::MeshCore:
return std::unique_ptr<IMeshAdapter>(new chat::meshcore::MeshCoreAdapter(board));
case MeshProtocol::Meshtastic:
default:
return std::unique_ptr<IMeshAdapter>(new chat::meshtastic::MtAdapter(board));
}
}
+6 -4
View File
@@ -5,18 +5,20 @@
#pragma once
#include "../../board/LoraBoard.h"
#include "../domain/chat_types.h"
#include "../ports/i_mesh_adapter.h"
#include "../../board/LoraBoard.h"
#include <memory>
namespace chat {
namespace chat
{
/**
* @brief Factory class for creating mesh adapters based on protocol selection
*/
class ProtocolFactory {
public:
class ProtocolFactory
{
public:
ProtocolFactory() = delete;
~ProtocolFactory() = delete;
+4 -2
View File
@@ -177,7 +177,8 @@ std::vector<ConversationMeta> FlashStore::loadConversationPage(size_t offset,
list.push_back(meta);
}
std::sort(list.begin(), list.end(),
[](const ConversationMeta& a, const ConversationMeta& b) {
[](const ConversationMeta& a, const ConversationMeta& b)
{
return a.last_timestamp > b.last_timestamp;
});
if (total)
@@ -248,7 +249,8 @@ bool FlashStore::updateMessageStatus(MessageId msg_id, MessageStatus status)
if (rec.msg_id != msg_id) continue;
if (rec.from != 0) continue; // only update outgoing messages
uint8_t new_status = static_cast<uint8_t>(status);
if (rec.status == new_status) {
if (rec.status == new_status)
{
return true;
}
rec.status = new_status;
+4 -2
View File
@@ -181,7 +181,8 @@ std::vector<ConversationMeta> LogStore::loadConversationPage(size_t offset,
}
std::sort(entries.begin(), entries.end(),
[](const IndexEntry& a, const IndexEntry& b) {
[](const IndexEntry& a, const IndexEntry& b)
{
return a.last_timestamp > b.last_timestamp;
});
@@ -280,7 +281,8 @@ void LogStore::clearConversation(const ConversationId& conv)
}
entries.erase(std::remove_if(entries.begin(), entries.end(),
[&](const IndexEntry& entry) {
[&](const IndexEntry& entry)
{
return entry.peer == conv.peer &&
entry.channel == static_cast<uint8_t>(conv.channel);
}),
+4 -3
View File
@@ -50,8 +50,8 @@ std::vector<ChatMessage> RamStore::loadRecent(const ConversationId& conv, size_t
}
std::vector<ConversationMeta> RamStore::loadConversationPage(size_t offset,
size_t limit,
size_t* total)
size_t limit,
size_t* total)
{
std::vector<ConversationMeta> list;
list.reserve(conversations_.size());
@@ -89,7 +89,8 @@ std::vector<ConversationMeta> RamStore::loadConversationPage(size_t offset,
}
std::sort(list.begin(), list.end(),
[](const ConversationMeta& a, const ConversationMeta& b) {
[](const ConversationMeta& a, const ConversationMeta& b)
{
return a.last_timestamp > b.last_timestamp;
});
if (total)
+4 -2
View File
@@ -9,7 +9,8 @@
#include <cstdint>
#include <ctime>
namespace chat {
namespace chat
{
constexpr uint32_t kMinValidEpochSeconds = 1577836800U; // 2020-01-01
@@ -21,7 +22,8 @@ inline bool is_valid_epoch(uint32_t ts)
inline uint32_t now_epoch_seconds()
{
time_t now = time(nullptr);
if (now < 0) {
if (now < 0)
{
return 0;
}
return static_cast<uint32_t>(now);
+6 -3
View File
@@ -4,8 +4,8 @@
*/
#include "chat_service.h"
#include "../time_utils.h"
#include "../../sys/event_bus.h"
#include "../time_utils.h"
namespace chat
{
@@ -114,9 +114,12 @@ void ChatService::processIncoming()
ChatMessage msg;
msg.channel = incoming.channel;
msg.from = incoming.from;
if (incoming.to == 0xFFFFFFFF) {
if (incoming.to == 0xFFFFFFFF)
{
msg.peer = 0;
} else {
}
else
{
msg.peer = incoming.from;
}
msg.msg_id = incoming.msg_id;
+19 -21
View File
@@ -8,27 +8,25 @@ namespace drivers
// Command sequence derived from LilyGo T-Deck TFT_eSPI (INIT_SEQUENCE_2).
// The high bit (0x80) in len requests a post-command delay in DisplayInterface.
static const CommandTable_t kInit[] = {
{0x11, {0}, 0x80}, // SLPOUT + delay
{0x13, {0}, 0}, // NORON
{0x36, {0x00}, 1}, // MADCTL (RGB order)
{0x3A, {0x55}, 1}, // COLMOD (RGB565)
{0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5}, // PORCTRL
{0xB7, {0x75}, 1}, // GCTRL
{0xBB, {0x1A}, 1}, // VCOMS
{0xC0, {0x2C}, 1}, // LCMCTRL
{0xC2, {0x01}, 1}, // VDVVRHEN
{0xC3, {0x13}, 1}, // VRHS
{0xC4, {0x20}, 1}, // VDVSET
{0xC6, {0x0F}, 1}, // FRCTR2
{0xD0, {0xA4, 0xA1}, 2}, // PWCTRL1
{0xE0, {0xD0, 0x0D, 0x14, 0x0D, 0x0D, 0x09, 0x38, 0x44,
0x4E, 0x3A, 0x17, 0x18, 0x2F, 0x30}, 14}, // PVGAMCTRL
{0xE1, {0xD0, 0x09, 0x0F, 0x08, 0x07, 0x14, 0x37, 0x44,
0x4D, 0x38, 0x15, 0x16, 0x2C, 0x3E}, 14}, // NVGAMCTRL
{0x21, {0}, 0}, // INVON
{0x2A, {0x00, 0x00, 0x00, 0xEF}, 4}, // CASET 0..239
{0x2B, {0x00, 0x00, 0x01, 0x3F}, 4}, // RASET 0..319
{0x29, {0}, 0x80}, // DISPON + delay
{0x11, {0}, 0x80}, // SLPOUT + delay
{0x13, {0}, 0}, // NORON
{0x36, {0x00}, 1}, // MADCTL (RGB order)
{0x3A, {0x55}, 1}, // COLMOD (RGB565)
{0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5}, // PORCTRL
{0xB7, {0x75}, 1}, // GCTRL
{0xBB, {0x1A}, 1}, // VCOMS
{0xC0, {0x2C}, 1}, // LCMCTRL
{0xC2, {0x01}, 1}, // VDVVRHEN
{0xC3, {0x13}, 1}, // VRHS
{0xC4, {0x20}, 1}, // VDVSET
{0xC6, {0x0F}, 1}, // FRCTR2
{0xD0, {0xA4, 0xA1}, 2}, // PWCTRL1
{0xE0, {0xD0, 0x0D, 0x14, 0x0D, 0x0D, 0x09, 0x38, 0x44, 0x4E, 0x3A, 0x17, 0x18, 0x2F, 0x30}, 14}, // PVGAMCTRL
{0xE1, {0xD0, 0x09, 0x0F, 0x08, 0x07, 0x14, 0x37, 0x44, 0x4D, 0x38, 0x15, 0x16, 0x2C, 0x3E}, 14}, // NVGAMCTRL
{0x21, {0}, 0}, // INVON
{0x2A, {0x00, 0x00, 0x00, 0xEF}, 4}, // CASET 0..239
{0x2B, {0x00, 0x00, 0x01, 0x3F}, 4}, // RASET 0..319
{0x29, {0}, 0x80}, // DISPON + delay
};
const CommandTable_t* ST7789TDeck::getInitCommands()
-1
View File
@@ -21,4 +21,3 @@ class ST7789TDeck
} // namespace drivers
} // namespace display
+6 -6
View File
@@ -307,16 +307,16 @@ void GpsService::gpsTask(void* pvParameters)
service->gps_state_.lng = service->gps_adapter_.longitude();
service->gps_state_.has_alt = service->gps_adapter_.hasAltitude();
service->gps_state_.alt_m = service->gps_state_.has_alt
? service->gps_adapter_.altitude()
: 0.0;
? service->gps_adapter_.altitude()
: 0.0;
service->gps_state_.has_speed = service->gps_adapter_.hasSpeed();
service->gps_state_.speed_mps = service->gps_state_.has_speed
? service->gps_adapter_.speed()
: 0.0;
? service->gps_adapter_.speed()
: 0.0;
service->gps_state_.has_course = service->gps_adapter_.hasCourse();
service->gps_state_.course_deg = service->gps_state_.has_course
? service->gps_adapter_.course()
: 0.0;
? service->gps_adapter_.course()
: 0.0;
service->gps_state_.satellites = sat_count;
service->gps_state_.valid = true;
service->gps_last_update_time_ = millis();
+6 -2
View File
@@ -60,7 +60,9 @@ bool TrackRecorder::ensureDir() const
String TrackRecorder::makeTrackPath() const
{
time_t now = time(nullptr);
struct tm tm_utc{};
struct tm tm_utc
{
};
char time_buf[32] = {0};
if (now > 0 && gmtime_r(&now, &tm_utc))
{
@@ -83,7 +85,9 @@ String TrackRecorder::isoTime(time_t t)
{
t = time(nullptr);
}
struct tm tm_utc{};
struct tm tm_utc
{
};
char buf[32] = {0};
if (t > 0 && gmtime_r(&t, &tm_utc))
{
+1 -1
View File
@@ -1,7 +1,7 @@
#include "hal/hal_gps.h"
#include "board/GpsBoard.h"
#include "../gps/GPS.h"
#include "board/GpsBoard.h"
#include "board/TLoRaPagerTypes.h"
#include "pins_arduino.h"
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include <Arduino.h>
#include "board/GpsBoard.h"
#include <Arduino.h>
namespace hal
{
+1 -1
View File
@@ -1,9 +1,9 @@
#pragma once
#include "board/MotionBoard.h"
#include "bosch/BoschParseCallbackManager.hpp"
#include <Arduino.h>
#include <SensorBHI260AP.hpp>
#include "board/MotionBoard.h"
namespace hal
{
+9 -6
View File
@@ -14,8 +14,8 @@
#include "app/app_context.h"
#include "display/DisplayConfig.h"
#include "ui/assets/images.h"
#include "ui/app_screen.h"
#include "ui/assets/images.h"
#include "ui/ui_common.h"
#include "ui/widgets/system_notification.h"
@@ -110,8 +110,9 @@ bool format_menu_time(char* out, size_t out_len)
}
// App function types (like factory example)
class FunctionAppScreen : public AppScreen {
public:
class FunctionAppScreen : public AppScreen
{
public:
FunctionAppScreen(const char* name,
const lv_image_dsc_t* icon,
void (*enter)(lv_obj_t*),
@@ -123,19 +124,21 @@ public:
void enter(lv_obj_t* parent) override
{
if (enter_) {
if (enter_)
{
enter_(parent);
}
}
void exit(lv_obj_t* parent) override
{
if (exit_) {
if (exit_)
{
exit_(parent);
}
}
private:
private:
const char* name_;
const lv_image_dsc_t* icon_;
void (*enter_)(lv_obj_t*);
+21 -21
View File
@@ -22,30 +22,30 @@ namespace sys
*/
enum class EventType
{
ChatNewMessage, // New message received
ChatSendResult, // Message send result
ChatUnreadChanged, // Unread count changed
ChatChannelSwitched, // Channel switched
NodeInfoUpdate, // Node info updated (from mesh network)
NodeProtocolUpdate, // Node protocol update (from message)
ChatNewMessage, // New message received
ChatSendResult, // Message send result
ChatUnreadChanged, // Unread count changed
ChatChannelSwitched, // Channel switched
NodeInfoUpdate, // Node info updated (from mesh network)
NodeProtocolUpdate, // Node protocol update (from message)
KeyVerificationNumberRequest, // PKI key verification number requested
KeyVerificationNumberInform, // PKI key verification number provided
KeyVerificationFinal, // PKI key verification final confirmation
TeamAdvertise, // Team advertise received
TeamJoinRequest, // Team join request received
TeamJoinAccept, // Team join accept received
TeamJoinConfirm, // Team join confirm received
TeamJoinDecision, // Team join decision received
TeamKick, // Team kick received
TeamTransferLeader, // Team transfer leader received
TeamKeyDist, // Team key distribution received
TeamStatus, // Team status received
TeamPosition, // Team position received
TeamWaypoint, // Team waypoint received
TeamChat, // Team chat received
TeamError, // Team protocol error
InputEvent, // Input event (keyboard/rotary)
SystemTick // System tick (for periodic tasks)
TeamAdvertise, // Team advertise received
TeamJoinRequest, // Team join request received
TeamJoinAccept, // Team join accept received
TeamJoinConfirm, // Team join confirm received
TeamJoinDecision, // Team join decision received
TeamKick, // Team kick received
TeamTransferLeader, // Team transfer leader received
TeamKeyDist, // Team key distribution received
TeamStatus, // Team status received
TeamPosition, // Team position received
TeamWaypoint, // Team waypoint received
TeamChat, // Team chat received
TeamError, // Team protocol error
InputEvent, // Input event (keyboard/rotary)
SystemTick // System tick (for periodic tasks)
};
/**
+1 -1
View File
@@ -1,8 +1,8 @@
#pragma once
#include "team_types.h"
#include "../protocol/team_chat.h"
#include "../protocol/team_mgmt.h"
#include "team_types.h"
#include <cstdint>
#include <vector>
+17 -17
View File
@@ -5,8 +5,8 @@
#include "team_nfc.h"
#include <Arduino.h>
#include <AES.h>
#include <Arduino.h>
#include <GCM.h>
#include <SHA256.h>
#include <cstring>
@@ -20,13 +20,13 @@ namespace team::nfc
{
namespace
{
constexpr uint8_t kMagic[4] = { 'T', 'N', 'F', '1' };
constexpr uint8_t kMagic[4] = {'T', 'N', 'F', '1'};
constexpr size_t kDerivedKeyLen = 16;
constexpr uint32_t kPbkdf2Iterations = 10000;
constexpr size_t kHeaderSize = 4 + 1 + team::proto::kTeamIdSize + 4 + 4 + kNfcSaltSize + kNfcNonceSize;
constexpr char kMimeType[] = "application/vnd.trailmate.teamkey";
constexpr uint8_t kT4tCcFileId[2] = { 0xE1, 0x03 };
constexpr uint8_t kT4tNdefFileId[2] = { 0xE1, 0x04 };
constexpr uint8_t kT4tCcFileId[2] = {0xE1, 0x03};
constexpr uint8_t kT4tNdefFileId[2] = {0xE1, 0x04};
constexpr size_t kT4tCcFileLen = 15;
#ifndef TEAM_NFC_LOG_ENABLE
@@ -278,9 +278,9 @@ bool write_ndef_message(const std::vector<uint8_t>& payload)
return false;
}
ndefConstBuffer8 type_buf{ reinterpret_cast<const uint8_t*>(kMimeType),
static_cast<uint8_t>(sizeof(kMimeType) - 1) };
ndefConstBuffer payload_buf{ payload.data(), static_cast<uint32_t>(payload.size()) };
ndefConstBuffer8 type_buf{reinterpret_cast<const uint8_t*>(kMimeType),
static_cast<uint8_t>(sizeof(kMimeType) - 1)};
ndefConstBuffer payload_buf{payload.data(), static_cast<uint32_t>(payload.size())};
NdefClass ndef(board->nfc);
if (ndef.ndefPollerContextInitializationWrapper(dev) != ERR_NONE)
@@ -309,7 +309,7 @@ bool write_ndef_message(const std::vector<uint8_t>& payload)
}
uint8_t raw_buf[256];
ndefBuffer raw{ raw_buf, sizeof(raw_buf) };
ndefBuffer raw{raw_buf, sizeof(raw_buf)};
if (ndefMessageEncode(&message, &raw) != ERR_NONE)
{
TEAM_NFC_LOG("[NFC] write_ndef_message encode_failed\n");
@@ -369,7 +369,7 @@ bool read_ndef_message(std::vector<uint8_t>& out_payload)
ndefMessage message{};
ndefMessageInit(&message);
ndefConstBuffer msg_buf{ raw_buf, rcvd_len };
ndefConstBuffer msg_buf{raw_buf, rcvd_len};
if (ndefMessageDecode(&msg_buf, &message) != ERR_NONE)
{
TEAM_NFC_LOG("[NFC] read_ndef_message decode_failed\n");
@@ -377,7 +377,7 @@ bool read_ndef_message(std::vector<uint8_t>& out_payload)
}
static const uint8_t kMimeType[] = "application/vnd.trailmate.teamkey";
ndefConstBuffer8 type_buf{ kMimeType, static_cast<uint8_t>(sizeof(kMimeType) - 1) };
ndefConstBuffer8 type_buf{kMimeType, static_cast<uint8_t>(sizeof(kMimeType) - 1)};
for (ndefRecord* rec = ndefMessageGetFirstRecord(&message); rec; rec = ndefMessageGetNextRecord(rec))
{
@@ -477,11 +477,11 @@ bool build_t4t_files(const std::vector<uint8_t>& payload)
s_ndef_file.insert(s_ndef_file.end(), payload.begin(), payload.end());
const uint16_t ndef_file_size = static_cast<uint16_t>(s_ndef_file.size());
s_cc_file = { 0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF,
0x04, 0x06, kT4tNdefFileId[0], kT4tNdefFileId[1],
static_cast<uint8_t>((ndef_file_size >> 8) & 0xFF),
static_cast<uint8_t>(ndef_file_size & 0xFF),
0x00, 0xFF };
s_cc_file = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF,
0x04, 0x06, kT4tNdefFileId[0], kT4tNdefFileId[1],
static_cast<uint8_t>((ndef_file_size >> 8) & 0xFF),
static_cast<uint8_t>(ndef_file_size & 0xFF),
0x00, 0xFF};
TEAM_NFC_LOG("[NFC] build_t4t_files ok ndef_file_size=%u\n",
static_cast<unsigned>(ndef_file_size));
log_hex("cc_file", s_cc_file.data(), s_cc_file.size());
@@ -551,8 +551,8 @@ void handle_apdu(const uint8_t* apdu, size_t len, std::vector<uint8_t>& response
const uint8_t* data = apdu + 5;
if (p1 == 0x04)
{
static const uint8_t kAidV2[] = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 };
static const uint8_t kAidV1[] = { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x00 };
static const uint8_t kAidV2[] = {0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01};
static const uint8_t kAidV1[] = {0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x00};
bool match_v2 = (lc == sizeof(kAidV2)) && (memcmp(data, kAidV2, sizeof(kAidV2)) == 0);
bool match_v1 = (lc == sizeof(kAidV1)) && (memcmp(data, kAidV1, sizeof(kAidV1)) == 0);
if (!match_v2 && !match_v1)
+1 -1
View File
@@ -2,9 +2,9 @@
#include "../../chat/ports/i_mesh_adapter.h"
#include "../domain/team_types.h"
#include "../protocol/team_chat.h"
#include "../ports/i_team_crypto.h"
#include "../ports/i_team_event_sink.h"
#include "../protocol/team_chat.h"
#include <vector>
namespace team
+13 -6
View File
@@ -57,28 +57,35 @@ static void disp_flush(lv_display_t* disp_drv, const lv_area_t* area, uint8_t* c
#if LV_TEST_FLUSH_LOG
#if LV_TEST_FLUSH_SAMPLE
bool all_zero_sample = false;
if (color_p && len > 0) {
if (color_p && len > 0)
{
const uint16_t* pix = reinterpret_cast<const uint16_t*>(color_p);
size_t step = len / 8;
if (step == 0) step = 1;
all_zero_sample = true;
for (size_t i = 0, idx = 0; i < 8 && idx < len; ++i, idx += step) {
if (pix[idx] != 0) {
for (size_t i = 0, idx = 0; i < 8 && idx < len; ++i, idx += step)
{
if (pix[idx] != 0)
{
all_zero_sample = false;
break;
}
}
}
if (all_zero_sample) {
if (all_zero_sample)
{
s_zero_streak++;
} else {
}
else
{
s_zero_streak = 0;
}
#endif
s_flush_count++;
s_last_area = *area;
uint32_t now_ms = millis();
if (now_ms - s_flush_last_ms >= 1000) {
if (now_ms - s_flush_last_ms >= 1000)
{
lv_obj_t* scr = lv_screen_active();
unsigned child_cnt = scr ? (unsigned)lv_obj_get_child_cnt(scr) : 0;
Serial.printf("[LVGL] flush/s=%lu last_area=%d,%d-%d,%d children=%u",
+3 -2
View File
@@ -2,8 +2,9 @@
#include "lvgl.h"
class AppScreen {
public:
class AppScreen
{
public:
virtual ~AppScreen() = default;
virtual const char* name() const = 0;
virtual const lv_image_dsc_t* icon() const = 0;
+12303 -144
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* @file images.h
* @brief Image asset declarations for LVGL v9
*
*
* This header declares external image descriptors that are defined
* in the corresponding .c files in this directory.
*/
+12303 -144
View File
File diff suppressed because it is too large Load Diff
+1743 -64
View File
File diff suppressed because it is too large Load Diff
+12303 -144
View File
File diff suppressed because it is too large Load Diff
+12303 -144
View File
File diff suppressed because it is too large Load Diff
+12303 -144
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,20 +1,22 @@
#pragma once
#include "lvgl.h"
#include "../chat/domain/chat_types.h"
#include "lvgl.h"
#include "screens/chat/chat_compose_components.h"
#include "widgets/ime/ime_widget.h"
#include <memory>
namespace chat {
namespace chat
{
class ChatService;
}
namespace chat::ui {
namespace chat::ui
{
class ComposeFlow
{
public:
public:
struct Callbacks
{
void (*on_closed)(bool sent, bool ok, bool timeout, void* user_data) = nullptr;
@@ -36,7 +38,7 @@ public:
bool isOpen() const;
ChatComposeScreen* screen() const;
private:
private:
static void on_action(ChatComposeScreen::ActionIntent intent, void* user_data);
static void on_back(void* user_data);
static void on_send_done(bool ok, bool timeout, void* user_data);
+14 -11
View File
@@ -115,7 +115,8 @@ static void fit_btn_to_label(lv_obj_t* btn, int pad_lr)
void ChatComposeScreen::setEnabled(bool enabled)
{
if (!impl_) return;
auto set = [enabled](lv_obj_t* o) {
auto set = [enabled](lv_obj_t* o)
{
if (!o) return;
if (enabled) lv_obj_clear_state(o, LV_STATE_DISABLED);
else lv_obj_add_state(o, LV_STATE_DISABLED);
@@ -251,9 +252,12 @@ ChatComposeScreen::ChatComposeScreen(lv_obj_t* parent, chat::ConversationId conv
: conv_(conv)
{
lv_obj_t* active = lv_screen_active();
if (!active) {
if (!active)
{
CHAT_COMPOSE_LOG("[ChatCompose] WARNING: lv_screen_active() is null\n");
} else {
}
else
{
CHAT_COMPOSE_LOG("[ChatCompose] init: active=%p parent=%p\n", active, parent);
}
@@ -300,10 +304,12 @@ ChatComposeScreen::ChatComposeScreen(lv_obj_t* parent, chat::ConversationId conv
input::bind_textarea_events(impl_->w, this, on_key, on_text_changed);
input::setup_default_group_focus(impl_->w);
if (impl_->w.container && !lv_obj_is_valid(impl_->w.container)) {
if (impl_->w.container && !lv_obj_is_valid(impl_->w.container))
{
CHAT_COMPOSE_LOG("[ChatCompose] WARNING: container invalid\n");
}
if (impl_->w.textarea && !lv_obj_is_valid(impl_->w.textarea)) {
if (impl_->w.textarea && !lv_obj_is_valid(impl_->w.textarea))
{
CHAT_COMPOSE_LOG("[ChatCompose] WARNING: textarea invalid\n");
}
@@ -410,9 +416,9 @@ void ChatComposeScreen::clearText()
}
void ChatComposeScreen::beginSend(chat::ChatService* service,
chat::MessageId msg_id,
void (*done_cb)(bool ok, bool timeout, void*),
void* user_data)
chat::MessageId msg_id,
void (*done_cb)(bool ok, bool timeout, void*),
void* user_data)
{
if (!impl_ || !impl_->guard || !impl_->guard->alive) return;
if (impl_->send_state == SendState::Waiting) return;
@@ -448,8 +454,6 @@ void ChatComposeScreen::beginSend(chat::ChatService* service,
}
}
void ChatComposeScreen::finishSend(bool ok, bool timeout, const char* message)
{
if (!impl_ || !impl_->guard || !impl_->guard->alive) return;
@@ -471,7 +475,6 @@ void ChatComposeScreen::finishSend(bool ok, bool timeout, const char* message)
schedule_done_async(impl_->guard, done_cb, done_user, ok, timeout);
}
void ChatComposeScreen::setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data)
{
action_cb_ = cb;
+14 -9
View File
@@ -1,24 +1,29 @@
#pragma once
#include "lvgl.h"
#include "../../chat/domain/chat_types.h"
#include "../../widgets/top_bar.h"
#include "lvgl.h"
#include <string>
namespace ui {
namespace widgets {
namespace ui
{
namespace widgets
{
class ImeWidget;
} // namespace widgets
} // namespace ui
namespace chat {
namespace chat
{
class ChatService;
}
namespace chat::ui {
namespace chat::ui
{
class ChatComposeScreen {
public:
class ChatComposeScreen
{
public:
enum class ActionIntent
{
Send,
@@ -50,13 +55,13 @@ public:
lv_obj_t* getObj() const;
private:
private:
chat::ConversationId conv_;
void setEnabled(bool enabled);
lv_obj_t* toastHost() const;
void showSendToast(bool ok, bool timeout, const char* message);
void (*action_cb_)(ActionIntent intent, void*) = nullptr;
void* action_cb_user_data_ = nullptr;
+10 -5
View File
@@ -1,12 +1,16 @@
#include "chat_compose_input.h"
namespace chat::ui::compose::input {
namespace chat::ui::compose::input
{
void setup_default_group_focus(const layout::Widgets& w) {
if (lv_group_t* g = lv_group_get_default()) {
void setup_default_group_focus(const layout::Widgets& w)
{
if (lv_group_t* g = lv_group_get_default())
{
lv_group_add_obj(g, w.textarea);
lv_group_add_obj(g, w.send_btn);
if (w.position_btn && !lv_obj_has_flag(w.position_btn, LV_OBJ_FLAG_HIDDEN)) {
if (w.position_btn && !lv_obj_has_flag(w.position_btn, LV_OBJ_FLAG_HIDDEN))
{
lv_group_add_obj(g, w.position_btn);
}
lv_group_add_obj(g, w.cancel_btn);
@@ -16,7 +20,8 @@ void setup_default_group_focus(const layout::Widgets& w) {
}
void bind_textarea_events(const layout::Widgets& w, void* user_data,
lv_event_cb_t key_cb, lv_event_cb_t text_cb) {
lv_event_cb_t key_cb, lv_event_cb_t text_cb)
{
lv_obj_add_event_cb(w.textarea, text_cb, LV_EVENT_VALUE_CHANGED, user_data);
lv_obj_add_event_cb(w.textarea, key_cb, LV_EVENT_KEY, user_data);
}
+5 -3
View File
@@ -1,10 +1,12 @@
#pragma once
#include "lvgl.h"
#include "chat_compose_layout.h"
#include "lvgl.h"
namespace chat::ui::compose::input {
namespace chat::ui::compose::input
{
struct State {
struct State
{
bool encoder_enter_focus_send = true;
};
+6 -3
View File
@@ -29,9 +29,11 @@
*/
#include "chat_compose_layout.h"
namespace chat::ui::compose::layout {
namespace chat::ui::compose::layout
{
static lv_obj_t* create_btn_with_label(lv_obj_t* parent, int w, int h, const char* text) {
static lv_obj_t* create_btn_with_label(lv_obj_t* parent, int w, int h, const char* text)
{
lv_obj_t* btn = lv_btn_create(parent);
lv_obj_set_size(btn, w, h);
@@ -41,7 +43,8 @@ static lv_obj_t* create_btn_with_label(lv_obj_t* parent, int w, int h, const cha
return btn;
}
void create(lv_obj_t* parent, const Spec& spec, Widgets& w) {
void create(lv_obj_t* parent, const Spec& spec, Widgets& w)
{
w.container = lv_obj_create(parent);
lv_obj_set_size(w.container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(w.container, LV_FLEX_FLOW_COLUMN);
+7 -4
View File
@@ -1,10 +1,12 @@
#pragma once
#include "lvgl.h"
#include "../../widgets/top_bar.h"
#include "lvgl.h"
namespace chat::ui::compose::layout {
namespace chat::ui::compose::layout
{
struct Spec {
struct Spec
{
int action_bar_h = 30;
int action_pad_lr = 10;
int action_pad_tb = 4;
@@ -19,7 +21,8 @@ struct Spec {
int btn_gap = 10;
};
struct Widgets {
struct Widgets
{
lv_obj_t* container = nullptr;
lv_obj_t* content = nullptr;
lv_obj_t* textarea = nullptr;
+6 -3
View File
@@ -1,7 +1,8 @@
#include "chat_compose_styles.h"
#include "../../assets/fonts/fonts.h"
namespace chat::ui::compose::styles {
namespace chat::ui::compose::styles
{
static bool inited = false;
@@ -13,7 +14,8 @@ static lv_style_t s_btn_basic;
static lv_style_t s_btn_focused;
static lv_style_t s_len_label;
void init_once() {
void init_once()
{
if (inited) return;
inited = true;
@@ -59,7 +61,8 @@ void init_once() {
lv_style_set_text_color(&s_len_label, lv_color_hex(0x606060));
}
void apply_all(const layout::Widgets& w) {
void apply_all(const layout::Widgets& w)
{
init_once();
lv_obj_add_style(w.container, &s_container, 0);
+3 -2
View File
@@ -1,8 +1,9 @@
#pragma once
#include "lvgl.h"
#include "chat_compose_layout.h"
#include "lvgl.h"
namespace chat::ui::compose::styles {
namespace chat::ui::compose::styles
{
void init_once();
void apply_all(const layout::Widgets& w);
@@ -4,22 +4,25 @@
*/
#include "chat_conversation_components.h"
#include "../ui_common.h"
#include "../../../app/app_context.h"
#include "../ui_common.h"
#include "chat_conversation_input.h"
#include "chat_conversation_layout.h"
#include "chat_conversation_styles.h"
#include "chat_conversation_input.h"
#include <Arduino.h>
#include <algorithm>
#include <ctime>
#include <cstdio>
#include <ctime>
namespace chat {
namespace ui {
namespace chat
{
namespace ui
{
// Keep original constant meaning (bubble max width and 70% rule)
namespace {
namespace
{
constexpr lv_coord_t kBubbleMaxWidth = 322; // same as original
constexpr uint32_t kSecondsPerDay = 24U * 60U * 60U;
constexpr uint32_t kSecondsPerMonth = 30U * kSecondsPerDay;
@@ -43,7 +46,8 @@ static void format_hms(char* out, size_t out_len, uint32_t seconds)
static void format_message_time(char* out, size_t out_len, uint32_t ts)
{
if (!out || out_len == 0) return;
if (ts == 0) {
if (ts == 0)
{
snprintf(out, out_len, "--");
return;
}
@@ -52,32 +56,40 @@ static void format_message_time(char* out, size_t out_len, uint32_t ts)
bool ts_is_epoch = is_valid_epoch_ts(ts);
bool now_is_epoch = is_valid_epoch_ts(now_epoch);
uint32_t now_secs = now_is_epoch ? now_epoch : static_cast<uint32_t>(millis() / 1000U);
if (ts_is_epoch && !now_is_epoch) {
if (ts_is_epoch && !now_is_epoch)
{
ts_is_epoch = false;
}
if (now_secs < ts) {
if (now_secs < ts)
{
now_secs = ts;
}
uint32_t diff = now_secs - ts;
if (!ts_is_epoch) {
if (diff < 60U) {
if (!ts_is_epoch)
{
if (diff < 60U)
{
snprintf(out, out_len, "now");
return;
}
if (diff < 3600U) {
if (diff < 3600U)
{
snprintf(out, out_len, "%um", static_cast<unsigned>(diff / 60U));
return;
}
if (diff < kSecondsPerDay) {
if (diff < kSecondsPerDay)
{
snprintf(out, out_len, "%uh", static_cast<unsigned>(diff / 3600U));
return;
}
if (diff < kSecondsPerMonth) {
if (diff < kSecondsPerMonth)
{
snprintf(out, out_len, "%ud", static_cast<unsigned>(diff / kSecondsPerDay));
return;
}
if (diff < kSecondsPerYear) {
if (diff < kSecondsPerYear)
{
snprintf(out, out_len, "%umo", static_cast<unsigned>(diff / kSecondsPerMonth));
return;
}
@@ -87,9 +99,12 @@ static void format_message_time(char* out, size_t out_len, uint32_t ts)
time_t t = ui_apply_timezone_offset(static_cast<time_t>(ts));
struct tm* info = gmtime(&t);
if (info) {
if (info)
{
strftime(out, out_len, "%H:%M", info);
} else {
}
else
{
snprintf(out, out_len, "--");
}
}
@@ -102,9 +117,12 @@ ChatConversationScreen::ChatConversationScreen(lv_obj_t* parent, chat::Conversat
guard_->pending_async = 0;
lv_obj_t* active = lv_screen_active();
if (!active) {
if (!active)
{
Serial.printf("[ChatConversation] WARNING: lv_screen_active() is null\n");
} else {
}
else
{
Serial.printf("[ChatConversation] init: active=%p parent=%p\n", active, parent);
}
@@ -131,18 +149,22 @@ ChatConversationScreen::ChatConversationScreen(lv_obj_t* parent, chat::Conversat
::ui::widgets::top_bar_set_title(top_bar_, title);
::ui::widgets::top_bar_set_right_text(top_bar_, "");
::ui::widgets::top_bar_set_back_callback(top_bar_, handle_back, this);
if (top_bar_.container) {
if (top_bar_.container)
{
lv_obj_move_to_index(top_bar_.container, 0);
}
if (container_) {
if (container_)
{
lv_obj_add_event_cb(container_, on_root_deleted, LV_EVENT_DELETE, this);
}
if (container_ && !lv_obj_is_valid(container_)) {
if (container_ && !lv_obj_is_valid(container_))
{
Serial.printf("[ChatConversation] WARNING: container invalid\n");
}
if (msg_list_ && !lv_obj_is_valid(msg_list_)) {
if (msg_list_ && !lv_obj_is_valid(msg_list_))
{
Serial.printf("[ChatConversation] WARNING: msg_list invalid\n");
}
@@ -157,12 +179,15 @@ ChatConversationScreen::ChatConversationScreen(lv_obj_t* parent, chat::Conversat
ChatConversationScreen::~ChatConversationScreen()
{
if (container_ && lv_obj_is_valid(container_)) {
if (container_ && lv_obj_is_valid(container_))
{
lv_obj_del(container_);
}
if (guard_) {
if (guard_)
{
guard_->alive = false;
if (guard_->pending_async == 0) {
if (guard_->pending_async == 0)
{
delete guard_;
}
guard_ = nullptr;
@@ -171,12 +196,15 @@ ChatConversationScreen::~ChatConversationScreen()
void ChatConversationScreen::addMessage(const chat::ChatMessage& msg)
{
if (!guard_ || !guard_->alive || !msg_list_ || !lv_obj_is_valid(msg_list_)) {
if (!guard_ || !guard_->alive || !msg_list_ || !lv_obj_is_valid(msg_list_))
{
return;
}
if (messages_.size() >= MAX_DISPLAY_MESSAGES) {
if (messages_.size() >= MAX_DISPLAY_MESSAGES)
{
MessageItem& oldest = messages_[0];
if (oldest.container) {
if (oldest.container)
{
lv_obj_del(oldest.container);
}
messages_.erase(messages_.begin());
@@ -188,11 +216,14 @@ void ChatConversationScreen::addMessage(const chat::ChatMessage& msg)
void ChatConversationScreen::clearMessages()
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
for (auto& item : messages_) {
if (item.container) {
for (auto& item : messages_)
{
if (item.container)
{
lv_obj_del(item.container);
}
}
@@ -201,14 +232,16 @@ void ChatConversationScreen::clearMessages()
void ChatConversationScreen::scrollToBottom()
{
if (guard_ && guard_->alive && msg_list_) {
if (guard_ && guard_->alive && msg_list_)
{
lv_obj_scroll_to_y(msg_list_, LV_COORD_MAX, LV_ANIM_OFF);
}
}
void ChatConversationScreen::setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
action_cb_ = cb;
@@ -217,18 +250,21 @@ void ChatConversationScreen::setActionCallback(void (*cb)(ActionIntent intent, v
void ChatConversationScreen::setHeaderText(const char* title, const char* status)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
::ui::widgets::top_bar_set_title(top_bar_, title);
if (status != nullptr) {
if (status != nullptr)
{
::ui::widgets::top_bar_set_right_text(top_bar_, status);
}
}
void ChatConversationScreen::updateBatteryFromBoard()
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
ui_update_top_bar_battery(top_bar_);
@@ -236,7 +272,8 @@ void ChatConversationScreen::updateBatteryFromBoard()
void ChatConversationScreen::setBackCallback(void (*cb)(void*), void* user_data)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
back_cb_ = cb;
@@ -245,7 +282,8 @@ void ChatConversationScreen::setBackCallback(void (*cb)(void*), void* user_data)
void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
{
if (!guard_ || !guard_->alive || !msg_list_) {
if (!guard_ || !guard_->alive || !msg_list_)
{
return;
}
MessageItem item;
@@ -260,12 +298,14 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
// Compute bubble max width (same logic as original)
lv_coord_t max_bubble_w = kBubbleMaxWidth;
lv_coord_t list_w = chat::ui::layout::get_msg_list_content_width(msg_list_);
if (list_w > 0) {
if (list_w > 0)
{
// original: candidate = (list_w - 2 * kPadX) * 7 / 10
// kPadX lives in styles; but for behavior parity we replicate formula using known 8px.
constexpr lv_coord_t kPadX = 8;
lv_coord_t candidate = (list_w - 2 * kPadX) * 7 / 10;
if (candidate > 0 && candidate < max_bubble_w) {
if (candidate > 0 && candidate < max_bubble_w)
{
max_bubble_w = candidate;
}
}
@@ -278,13 +318,18 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
chat::ui::conversation::styles::apply_bubble_time(item.time_label);
char time_buf[16];
format_message_time(time_buf, sizeof(time_buf), msg.timestamp);
if (conv_.peer == 0) {
if (conv_.peer == 0)
{
std::string sender;
if (msg.from == 0) {
if (msg.from == 0)
{
sender = app::AppContext::getInstance().getConfig().short_name;
} else {
}
else
{
sender = app::AppContext::getInstance().getContactService().getContactName(msg.from);
if (sender.empty()) {
if (sender.empty())
{
char buf[16];
snprintf(buf, sizeof(buf), "%04lX",
static_cast<unsigned long>(msg.from & 0xFFFF));
@@ -293,7 +338,9 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
}
std::string line = sender + " " + time_buf;
lv_label_set_text(item.time_label, line.c_str());
} else {
}
else
{
lv_label_set_text(item.time_label, time_buf);
}
@@ -323,11 +370,13 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg)
void ChatConversationScreen::action_event_cb(lv_event_t* e)
{
auto* ctx = static_cast<ActionContext*>(lv_event_get_user_data(e));
if (!ctx || !ctx->screen) {
if (!ctx || !ctx->screen)
{
return;
}
ChatConversationScreen* screen = ctx->screen;
if (!screen->guard_ || !screen->guard_->alive) {
if (!screen->guard_ || !screen->guard_->alive)
{
return;
}
screen->schedule_action_async(ctx->intent);
@@ -336,16 +385,20 @@ void ChatConversationScreen::action_event_cb(lv_event_t* e)
void ChatConversationScreen::async_action_cb(void* user_data)
{
auto* payload = static_cast<ActionPayload*>(user_data);
if (!payload) {
if (!payload)
{
return;
}
LifetimeGuard* guard = payload->guard;
if (guard && guard->alive && payload->action_cb) {
if (guard && guard->alive && payload->action_cb)
{
payload->action_cb(payload->intent, payload->user_data);
}
if (guard && guard->pending_async > 0) {
if (guard && guard->pending_async > 0)
{
guard->pending_async--;
if (!guard->alive && guard->pending_async == 0) {
if (!guard->alive && guard->pending_async == 0)
{
delete guard;
}
}
@@ -355,7 +408,8 @@ void ChatConversationScreen::async_action_cb(void* user_data)
void ChatConversationScreen::on_root_deleted(lv_event_t* e)
{
auto* screen = static_cast<ChatConversationScreen*>(lv_event_get_user_data(e));
if (!screen) {
if (!screen)
{
return;
}
screen->handle_root_deleted();
@@ -364,7 +418,8 @@ void ChatConversationScreen::on_root_deleted(lv_event_t* e)
void ChatConversationScreen::handle_back(void* user_data)
{
ChatConversationScreen* screen = static_cast<ChatConversationScreen*>(user_data);
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
screen->schedule_back_async();
@@ -373,16 +428,20 @@ void ChatConversationScreen::handle_back(void* user_data)
void ChatConversationScreen::async_back_cb(void* user_data)
{
auto* payload = static_cast<BackPayload*>(user_data);
if (!payload) {
if (!payload)
{
return;
}
LifetimeGuard* guard = payload->guard;
if (guard && guard->alive && payload->back_cb) {
if (guard && guard->alive && payload->back_cb)
{
payload->back_cb(payload->user_data);
}
if (guard && guard->pending_async > 0) {
if (guard && guard->pending_async > 0)
{
guard->pending_async--;
if (!guard->alive && guard->pending_async == 0) {
if (!guard->alive && guard->pending_async == 0)
{
delete guard;
}
}
@@ -394,11 +453,13 @@ lv_timer_t* ChatConversationScreen::add_timer(lv_timer_cb_t cb,
void* user_data,
TimerDomain domain)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return nullptr;
}
lv_timer_t* timer = lv_timer_create(cb, period_ms, user_data);
if (timer) {
if (timer)
{
TimerEntry entry;
entry.timer = timer;
entry.domain = domain;
@@ -409,25 +470,31 @@ lv_timer_t* ChatConversationScreen::add_timer(lv_timer_cb_t cb,
void ChatConversationScreen::clear_timers(TimerDomain domain)
{
if (timers_.empty()) {
if (timers_.empty())
{
return;
}
for (auto& entry : timers_) {
if (entry.timer && entry.domain == domain) {
for (auto& entry : timers_)
{
if (entry.timer && entry.domain == domain)
{
lv_timer_del(entry.timer);
entry.timer = nullptr;
}
}
timers_.erase(
std::remove_if(timers_.begin(), timers_.end(),
[](const TimerEntry& entry) { return entry.timer == nullptr; }),
[](const TimerEntry& entry)
{ return entry.timer == nullptr; }),
timers_.end());
}
void ChatConversationScreen::clear_all_timers()
{
for (auto& entry : timers_) {
if (entry.timer) {
for (auto& entry : timers_)
{
if (entry.timer)
{
lv_timer_del(entry.timer);
entry.timer = nullptr;
}
@@ -437,11 +504,13 @@ void ChatConversationScreen::clear_all_timers()
void ChatConversationScreen::handle_root_deleted()
{
if ((!guard_ || !guard_->alive) && container_ == nullptr && msg_list_ == nullptr) {
if ((!guard_ || !guard_->alive) && container_ == nullptr && msg_list_ == nullptr)
{
return;
}
if (guard_) {
if (guard_)
{
guard_->alive = false;
}
action_cb_ = nullptr;
@@ -453,7 +522,8 @@ void ChatConversationScreen::handle_root_deleted()
chat::ui::conversation::input::cleanup(&input_binding_);
clear_all_timers();
if (top_bar_.back_btn) {
if (top_bar_.back_btn)
{
::ui::widgets::top_bar_set_back_callback(top_bar_, nullptr, nullptr);
}
@@ -466,7 +536,8 @@ void ChatConversationScreen::handle_root_deleted()
void ChatConversationScreen::schedule_action_async(ActionIntent intent)
{
if (!guard_ || !guard_->alive || !action_cb_) {
if (!guard_ || !guard_->alive || !action_cb_)
{
return;
}
auto* payload = new ActionPayload();
@@ -480,7 +551,8 @@ void ChatConversationScreen::schedule_action_async(ActionIntent intent)
void ChatConversationScreen::schedule_back_async()
{
if (!guard_ || !guard_->alive || !back_cb_) {
if (!guard_ || !guard_->alive || !back_cb_)
{
return;
}
auto* payload = new BackPayload();
@@ -4,18 +4,22 @@
*/
#pragma once
#include "lvgl.h"
#include "../../chat/domain/chat_types.h"
#include "../../widgets/top_bar.h"
#include "chat_conversation_input.h"
#include "lvgl.h"
#include <vector>
namespace chat {
namespace ui {
namespace chat
{
namespace ui
{
class ChatConversationScreen {
public:
enum class ActionIntent {
class ChatConversationScreen
{
public:
enum class ActionIntent
{
Reply
};
@@ -40,35 +44,41 @@ public:
void updateBatteryFromBoard();
void setBackCallback(void (*cb)(void*), void* user_data);
private:
enum class TimerDomain {
private:
enum class TimerDomain
{
ScreenGeneral,
Input
};
struct TimerEntry {
struct TimerEntry
{
lv_timer_t* timer = nullptr;
TimerDomain domain = TimerDomain::ScreenGeneral;
};
struct LifetimeGuard {
struct LifetimeGuard
{
bool alive = false;
int pending_async = 0;
};
struct ActionContext {
struct ActionContext
{
ChatConversationScreen* screen = nullptr;
ActionIntent intent = ActionIntent::Reply;
};
struct ActionPayload {
struct ActionPayload
{
LifetimeGuard* guard = nullptr;
void (*action_cb)(ActionIntent intent, void*) = nullptr;
void* user_data = nullptr;
ActionIntent intent = ActionIntent::Reply;
};
struct BackPayload {
struct BackPayload
{
LifetimeGuard* guard = nullptr;
void (*back_cb)(void*) = nullptr;
void* user_data = nullptr;
@@ -88,12 +98,13 @@ private:
void (*back_cb_)(void*) = nullptr;
void* back_cb_user_data_ = nullptr;
struct MessageItem {
struct MessageItem
{
chat::ChatMessage msg;
lv_obj_t* container = nullptr; // row
lv_obj_t* text_label = nullptr; // inside bubble
lv_obj_t* time_label = nullptr; // reserved (not used)
lv_obj_t* status_label = nullptr;// reserved (not used)
lv_obj_t* container = nullptr; // row
lv_obj_t* text_label = nullptr; // inside bubble
lv_obj_t* time_label = nullptr; // reserved (not used)
lv_obj_t* status_label = nullptr; // reserved (not used)
};
std::vector<MessageItem> messages_;
+43 -22
View File
@@ -1,6 +1,6 @@
#include <Arduino.h>
#include "chat_conversation_input.h"
#include "chat_conversation_components.h"
#include <Arduino.h>
#define CHAT_CONV_INPUT_DEBUG 1
#if CHAT_CONV_INPUT_DEBUG
@@ -9,13 +9,15 @@
#define CHAT_CONV_INPUT_LOG(...)
#endif
namespace chat::ui::conversation::input {
namespace chat::ui::conversation::input
{
namespace {
namespace
{
constexpr int kEncoderKeyRotateUp = 19;
constexpr int kEncoderKeyRotateDown = 20;
constexpr lv_coord_t kScrollStep = 24;
}
} // namespace
static void on_msg_list_key(lv_event_t* e)
{
@@ -24,25 +26,33 @@ static void on_msg_list_key(lv_event_t* e)
uint32_t key = lv_event_get_key(e);
if (lv_group_t* g = lv_group_get_default()) {
if (key == LV_KEY_ENTER) {
if (lv_group_t* g = lv_group_get_default())
{
if (key == LV_KEY_ENTER)
{
lv_group_set_editing(g, !lv_group_get_editing(g));
lv_event_stop_processing(e);
return;
}
if (!lv_group_get_editing(g)) {
if (!lv_group_get_editing(g))
{
return;
}
lv_coord_t delta = 0;
if (key == LV_KEY_UP || key == kEncoderKeyRotateUp) {
if (key == LV_KEY_UP || key == kEncoderKeyRotateUp)
{
delta = -kScrollStep;
} else if (key == LV_KEY_DOWN || key == kEncoderKeyRotateDown) {
}
else if (key == LV_KEY_DOWN || key == kEncoderKeyRotateDown)
{
delta = kScrollStep;
}
if (delta != 0) {
if (lv_obj_t* msg_list = screen->getMsgList()) {
if (delta != 0)
{
if (lv_obj_t* msg_list = screen->getMsgList())
{
lv_obj_scroll_by(msg_list, 0, delta, LV_ANIM_OFF);
lv_event_stop_processing(e);
}
@@ -52,7 +62,8 @@ static void on_msg_list_key(lv_event_t* e)
void init(ChatConversationScreen* screen, Binding* binding)
{
if (!binding) {
if (!binding)
{
return;
}
binding->bound = false;
@@ -61,25 +72,30 @@ void init(ChatConversationScreen* screen, Binding* binding)
binding->back_btn = screen ? screen->getBackBtn() : nullptr;
binding->group = lv_group_get_default();
if (!screen) {
if (!screen)
{
CHAT_CONV_INPUT_LOG("[ChatConversationInput] init (no screen)\n");
return;
}
if (!binding->group) {
if (!binding->group)
{
CHAT_CONV_INPUT_LOG("[ChatConversationInput] init (no group)\n");
return;
}
if (binding->msg_list) {
if (binding->msg_list)
{
lv_group_add_obj(binding->group, binding->msg_list);
lv_group_focus_obj(binding->msg_list);
lv_group_set_editing(binding->group, true);
lv_obj_add_event_cb(binding->msg_list, on_msg_list_key, LV_EVENT_KEY, screen);
}
if (binding->reply_btn) {
if (binding->reply_btn)
{
lv_group_add_obj(binding->group, binding->reply_btn);
}
if (binding->back_btn) {
if (binding->back_btn)
{
lv_group_add_obj(binding->group, binding->back_btn);
}
binding->bound = true;
@@ -88,17 +104,22 @@ void init(ChatConversationScreen* screen, Binding* binding)
void cleanup(Binding* binding)
{
if (!binding || !binding->bound) {
if (!binding || !binding->bound)
{
return;
}
if (binding->group) {
if (binding->msg_list) {
if (binding->group)
{
if (binding->msg_list)
{
lv_group_remove_obj(binding->msg_list);
}
if (binding->reply_btn) {
if (binding->reply_btn)
{
lv_group_remove_obj(binding->reply_btn);
}
if (binding->back_btn) {
if (binding->back_btn)
{
lv_group_remove_obj(binding->back_btn);
}
}
@@ -2,13 +2,16 @@
#include "lvgl.h"
namespace chat::ui {
namespace chat::ui
{
class ChatConversationScreen;
namespace conversation::input {
namespace conversation::input
{
struct Binding {
struct Binding
{
lv_obj_t* msg_list = nullptr;
lv_obj_t* reply_btn = nullptr;
lv_obj_t* back_btn = nullptr;
@@ -40,10 +40,11 @@
* - Visual style (colors/radius/padding) lives in styles.*.
*/
#include <Arduino.h>
#include "chat_conversation_layout.h"
#include <Arduino.h>
namespace chat::ui::layout {
namespace chat::ui::layout
{
static void make_non_scrollable(lv_obj_t* obj)
{
@@ -140,11 +141,14 @@ void align_message_row(lv_obj_t* row, bool is_self)
{
// Match original behavior:
// self -> row aligns to END, other -> START
if (is_self) {
if (is_self)
{
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_END,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
} else {
}
else
{
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
@@ -1,11 +1,13 @@
#pragma once
#include "lvgl.h"
#include "../../chat/domain/chat_types.h"
#include "lvgl.h"
namespace chat::ui::layout {
namespace chat::ui::layout
{
struct ConversationWidgets {
struct ConversationWidgets
{
lv_obj_t* root = nullptr;
lv_obj_t* msg_list = nullptr;
@@ -1,7 +1,8 @@
#include "chat_conversation_styles.h"
#include "../../assets/fonts/fonts.h"
namespace chat::ui::conversation::styles {
namespace chat::ui::conversation::styles
{
static bool inited = false;
@@ -30,8 +31,8 @@ static constexpr lv_coord_t kBubblePadY = 6;
static constexpr lv_coord_t kBubbleRadius = 12;
static const lv_color_t kBubbleOther = lv_color_hex(0xE5F0FF);
static const lv_color_t kBubbleSelf = lv_color_hex(0xFFF4E0);
static const lv_color_t kTextColor = lv_color_hex(0x202020);
static const lv_color_t kBubbleSelf = lv_color_hex(0xFFF4E0);
static const lv_color_t kTextColor = lv_color_hex(0x202020);
void init_once()
{
@@ -2,7 +2,8 @@
#include "lvgl.h"
namespace chat::ui::conversation::styles {
namespace chat::ui::conversation::styles
{
void init_once();
@@ -6,21 +6,24 @@
#include "chat_message_list_components.h"
#include "../ui_common.h"
#include "chat_message_list_input.h"
#include "chat_message_list_layout.h"
#include "chat_message_list_styles.h"
#include "chat_message_list_input.h"
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdio>
namespace chat {
namespace ui {
namespace chat
{
namespace ui
{
// ---- small helper (logic only, unchanged) ----
namespace {
namespace
{
constexpr uint32_t kMinValidEpochSeconds = 1577836800U; // 2020-01-01
constexpr uint32_t kSecondsPerDay = 24U * 60U * 60U;
constexpr uint32_t kSecondsPerMonth = 30U * kSecondsPerDay;
@@ -34,37 +37,54 @@ static bool is_valid_epoch_ts(uint32_t ts)
static void format_time_hhmm(char out[16], uint32_t ts)
{
if (ts == 0) {
if (ts == 0)
{
snprintf(out, 16, "--:--");
return;
}
if (!is_valid_epoch_ts(ts)) {
if (!is_valid_epoch_ts(ts))
{
uint32_t now_epoch = static_cast<uint32_t>(time(nullptr));
uint32_t now_secs = is_valid_epoch_ts(now_epoch) ? now_epoch : static_cast<uint32_t>(millis() / 1000U);
if (now_secs < ts) {
if (now_secs < ts)
{
now_secs = ts;
}
uint32_t diff = now_secs - ts;
if (diff < 60U) {
if (diff < 60U)
{
snprintf(out, 16, "now");
} else if (diff < 3600U) {
}
else if (diff < 3600U)
{
snprintf(out, 16, "%um", static_cast<unsigned>(diff / 60U));
} else if (diff < kSecondsPerDay) {
}
else if (diff < kSecondsPerDay)
{
snprintf(out, 16, "%uh", static_cast<unsigned>(diff / 3600U));
} else if (diff < kSecondsPerMonth) {
}
else if (diff < kSecondsPerMonth)
{
snprintf(out, 16, "%ud", static_cast<unsigned>(diff / kSecondsPerDay));
} else if (diff < kSecondsPerYear) {
}
else if (diff < kSecondsPerYear)
{
snprintf(out, 16, "%umo", static_cast<unsigned>(diff / kSecondsPerMonth));
} else {
}
else
{
snprintf(out, 16, "%uy", static_cast<unsigned>(diff / kSecondsPerYear));
}
return;
}
time_t t = ui_apply_timezone_offset(static_cast<time_t>(ts));
struct tm* info = gmtime(&t);
if (info) {
if (info)
{
strftime(out, 16, "%H:%M", info);
} else {
}
else
{
snprintf(out, 16, "--:--");
}
}
@@ -108,9 +128,12 @@ ChatMessageListScreen::ChatMessageListScreen(lv_obj_t* parent)
guard_->pending_async = 0;
lv_obj_t* active = lv_screen_active();
if (!active) {
if (!active)
{
Serial.printf("[ChatMessageList] WARNING: lv_screen_active() is null\n");
} else {
}
else
{
Serial.printf("[ChatMessageList] init: active=%p parent=%p\n", active, parent);
}
@@ -139,33 +162,40 @@ ChatMessageListScreen::ChatMessageListScreen(lv_obj_t* parent)
::ui::widgets::top_bar_set_title(top_bar_, "MESSAGES");
::ui::widgets::top_bar_set_right_text(top_bar_, "--:-- --%");
::ui::widgets::top_bar_set_back_callback(top_bar_, handle_back, this);
if (top_bar_.container) {
if (top_bar_.container)
{
lv_obj_move_to_index(top_bar_.container, 0);
}
if (container_) {
if (container_)
{
lv_obj_add_event_cb(container_, on_root_deleted, LV_EVENT_DELETE, this);
}
// ---------- Filter events ----------
if (direct_btn_) {
if (direct_btn_)
{
lv_obj_add_event_cb(direct_btn_, filter_focus_cb, LV_EVENT_FOCUSED, this);
lv_obj_add_event_cb(direct_btn_, filter_click_cb, LV_EVENT_CLICKED, this);
}
if (broadcast_btn_) {
if (broadcast_btn_)
{
lv_obj_add_event_cb(broadcast_btn_, filter_focus_cb, LV_EVENT_FOCUSED, this);
lv_obj_add_event_cb(broadcast_btn_, filter_click_cb, LV_EVENT_CLICKED, this);
}
if (team_btn_) {
if (team_btn_)
{
lv_obj_add_event_cb(team_btn_, filter_focus_cb, LV_EVENT_FOCUSED, this);
lv_obj_add_event_cb(team_btn_, filter_click_cb, LV_EVENT_CLICKED, this);
}
updateFilterHighlight();
if (container_ && !lv_obj_is_valid(container_)) {
if (container_ && !lv_obj_is_valid(container_))
{
Serial.printf("[ChatMessageList] WARNING: container invalid\n");
}
if (list_panel_ && !lv_obj_is_valid(list_panel_)) {
if (list_panel_ && !lv_obj_is_valid(list_panel_))
{
Serial.printf("[ChatMessageList] WARNING: list_panel invalid\n");
}
@@ -177,14 +207,17 @@ ChatMessageListScreen::ChatMessageListScreen(lv_obj_t* parent)
ChatMessageListScreen::~ChatMessageListScreen()
{
if (container_ && lv_obj_is_valid(container_)) {
if (container_ && lv_obj_is_valid(container_))
{
lv_obj_del(container_);
}
if (guard_) {
if (guard_)
{
guard_->alive = false;
guard_->owner_dead = true;
if (guard_->pending_async == 0) {
if (guard_->pending_async == 0)
{
delete guard_;
}
guard_ = nullptr;
@@ -193,25 +226,33 @@ ChatMessageListScreen::~ChatMessageListScreen()
void ChatMessageListScreen::setConversations(const std::vector<chat::ConversationMeta>& convs)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
convs_ = convs;
bool has_team = false;
for (const auto& conv : convs_) {
if (is_team_conversation(conv.id)) {
for (const auto& conv : convs_)
{
if (is_team_conversation(conv.id))
{
has_team = true;
break;
}
}
if (team_btn_) {
if (has_team) {
if (team_btn_)
{
if (has_team)
{
lv_obj_clear_flag(team_btn_, LV_OBJ_FLAG_HIDDEN);
} else {
}
else
{
lv_obj_add_flag(team_btn_, LV_OBJ_FLAG_HIDDEN);
}
}
if (!has_team && filter_mode_ == FilterMode::Team) {
if (!has_team && filter_mode_ == FilterMode::Team)
{
filter_mode_ = FilterMode::Broadcast;
}
updateFilterHighlight();
@@ -220,22 +261,27 @@ void ChatMessageListScreen::setConversations(const std::vector<chat::Conversatio
void ChatMessageListScreen::setSelected(int index)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
if (index >= 0 && index < static_cast<int>(items_.size()) &&
items_[index].btn != nullptr) {
items_[index].btn != nullptr)
{
selected_index_ = index;
}
}
void ChatMessageListScreen::setSelectedConversation(const chat::ConversationId& conv)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
for (size_t i = 0; i < items_.size(); ++i) {
if (items_[i].conv == conv) {
for (size_t i = 0; i < items_.size(); ++i)
{
if (items_[i].conv == conv)
{
setSelected(static_cast<int>(i));
return;
}
@@ -244,11 +290,13 @@ void ChatMessageListScreen::setSelectedConversation(const chat::ConversationId&
chat::ConversationId ChatMessageListScreen::getSelectedConversation() const
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return chat::ConversationId();
}
if (selected_index_ >= 0 &&
selected_index_ < static_cast<int>(items_.size())) {
selected_index_ < static_cast<int>(items_.size()))
{
return items_[selected_index_].conv;
}
return chat::ConversationId();
@@ -256,10 +304,12 @@ chat::ConversationId ChatMessageListScreen::getSelectedConversation() const
lv_obj_t* ChatMessageListScreen::getItemButton(size_t index) const
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return nullptr;
}
if (index >= items_.size()) {
if (index >= items_.size())
{
return nullptr;
}
return items_[index].btn;
@@ -269,7 +319,8 @@ void ChatMessageListScreen::setActionCallback(
void (*cb)(ActionIntent intent, const chat::ConversationId& conv, void*),
void* user_data)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
action_cb_ = cb;
@@ -278,7 +329,8 @@ void ChatMessageListScreen::setActionCallback(
void ChatMessageListScreen::updateBatteryFromBoard()
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
ui_update_top_bar_battery(top_bar_);
@@ -289,7 +341,8 @@ void ChatMessageListScreen::updateBatteryFromBoard()
// ------------------------------------------------
void ChatMessageListScreen::rebuildList()
{
if (!guard_ || !guard_->alive || !list_panel_ || !lv_obj_is_valid(list_panel_)) {
if (!guard_ || !guard_->alive || !list_panel_ || !lv_obj_is_valid(list_panel_))
{
return;
}
// Same behavior: clear and rebuild
@@ -299,21 +352,28 @@ void ChatMessageListScreen::rebuildList()
std::vector<chat::ConversationMeta> filtered;
filtered.reserve(convs_.size());
for (const auto& conv : convs_) {
if (is_team_conversation(conv.id)) {
if (filter_mode_ == FilterMode::Team) {
for (const auto& conv : convs_)
{
if (is_team_conversation(conv.id))
{
if (filter_mode_ == FilterMode::Team)
{
filtered.push_back(conv);
}
continue;
}
if (filter_mode_ == FilterMode::Direct && conv.id.peer != 0) {
if (filter_mode_ == FilterMode::Direct && conv.id.peer != 0)
{
filtered.push_back(conv);
} else if (filter_mode_ == FilterMode::Broadcast && conv.id.peer == 0) {
}
else if (filter_mode_ == FilterMode::Broadcast && conv.id.peer == 0)
{
filtered.push_back(conv);
}
}
for (const auto& conv : filtered) {
for (const auto& conv : filtered)
{
MessageItem item{};
item.conv = conv.id;
item.unread_count = conv.unread;
@@ -343,11 +403,14 @@ void ChatMessageListScreen::rebuildList()
format_time_hhmm(time_buf, conv.last_timestamp);
lv_label_set_text(item.time_label, time_buf);
if (conv.unread > 0) {
if (conv.unread > 0)
{
char unread_str[16];
snprintf(unread_str, sizeof(unread_str), "%d", conv.unread);
lv_label_set_text(item.unread_label, unread_str);
} else {
}
else
{
lv_label_set_text(item.unread_label, "");
}
@@ -357,7 +420,8 @@ void ChatMessageListScreen::rebuildList()
items_.push_back(item);
}
if (items_.empty()) {
if (items_.empty())
{
lv_obj_t* placeholder = chat::ui::layout::create_placeholder(list_panel_);
chat::ui::message_list::styles::apply_label_placeholder(placeholder);
lv_label_set_text(placeholder, "No messages");
@@ -375,7 +439,8 @@ void ChatMessageListScreen::rebuildList()
lv_obj_center(back_label);
lv_obj_add_event_cb(list_back_btn_, list_back_event_cb, LV_EVENT_CLICKED, this);
if (!items_.empty()) {
if (!items_.empty())
{
setSelected(0);
}
@@ -386,13 +451,16 @@ void ChatMessageListScreen::item_event_cb(lv_event_t* e)
{
auto* screen =
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
lv_obj_t* btn = static_cast<lv_obj_t*>(lv_event_get_target(e));
for (size_t i = 0; i < screen->items_.size(); i++) {
if (screen->items_[i].btn == btn) {
for (size_t i = 0; i < screen->items_.size(); i++)
{
if (screen->items_[i].btn == btn)
{
screen->setSelected(static_cast<int>(i));
screen->schedule_action_async(ActionIntent::SelectConversation,
screen->items_[i].conv);
@@ -405,7 +473,8 @@ void ChatMessageListScreen::list_back_event_cb(lv_event_t* e)
{
auto* screen =
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
chat::ui::message_list::input::focus_filter(&screen->input_binding_);
@@ -415,15 +484,21 @@ void ChatMessageListScreen::filter_focus_cb(lv_event_t* e)
{
auto* screen =
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
lv_obj_t* tgt = static_cast<lv_obj_t*>(lv_event_get_target(e));
if (tgt == screen->direct_btn_) {
if (tgt == screen->direct_btn_)
{
screen->setFilterMode(FilterMode::Direct);
} else if (tgt == screen->broadcast_btn_) {
}
else if (tgt == screen->broadcast_btn_)
{
screen->setFilterMode(FilterMode::Broadcast);
} else if (tgt == screen->team_btn_) {
}
else if (tgt == screen->team_btn_)
{
screen->setFilterMode(FilterMode::Team);
}
}
@@ -432,15 +507,21 @@ void ChatMessageListScreen::filter_click_cb(lv_event_t* e)
{
auto* screen =
static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
lv_obj_t* tgt = static_cast<lv_obj_t*>(lv_event_get_target(e));
if (tgt == screen->direct_btn_) {
if (tgt == screen->direct_btn_)
{
screen->setFilterMode(FilterMode::Direct);
} else if (tgt == screen->broadcast_btn_) {
}
else if (tgt == screen->broadcast_btn_)
{
screen->setFilterMode(FilterMode::Broadcast);
} else if (tgt == screen->team_btn_) {
}
else if (tgt == screen->team_btn_)
{
screen->setFilterMode(FilterMode::Team);
}
chat::ui::message_list::input::focus_list(&screen->input_binding_);
@@ -448,29 +529,38 @@ void ChatMessageListScreen::filter_click_cb(lv_event_t* e)
void ChatMessageListScreen::updateFilterHighlight()
{
if (!direct_btn_ || !broadcast_btn_) {
if (!direct_btn_ || !broadcast_btn_)
{
return;
}
lv_obj_clear_state(direct_btn_, LV_STATE_CHECKED);
lv_obj_clear_state(broadcast_btn_, LV_STATE_CHECKED);
if (team_btn_) {
if (team_btn_)
{
lv_obj_clear_state(team_btn_, LV_STATE_CHECKED);
}
if (filter_mode_ == FilterMode::Direct) {
if (filter_mode_ == FilterMode::Direct)
{
lv_obj_add_state(direct_btn_, LV_STATE_CHECKED);
} else if (filter_mode_ == FilterMode::Broadcast) {
}
else if (filter_mode_ == FilterMode::Broadcast)
{
lv_obj_add_state(broadcast_btn_, LV_STATE_CHECKED);
} else if (team_btn_) {
}
else if (team_btn_)
{
lv_obj_add_state(team_btn_, LV_STATE_CHECKED);
}
}
void ChatMessageListScreen::setFilterMode(FilterMode mode)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return;
}
if (filter_mode_ == mode) {
if (filter_mode_ == mode)
{
return;
}
filter_mode_ = mode;
@@ -482,7 +572,8 @@ void ChatMessageListScreen::setFilterMode(FilterMode mode)
void ChatMessageListScreen::handle_back(void* user_data)
{
auto* screen = static_cast<ChatMessageListScreen*>(user_data);
if (!screen || !screen->guard_ || !screen->guard_->alive) {
if (!screen || !screen->guard_ || !screen->guard_->alive)
{
return;
}
screen->schedule_action_async(ActionIntent::Back, chat::ConversationId());
@@ -491,16 +582,20 @@ void ChatMessageListScreen::handle_back(void* user_data)
void ChatMessageListScreen::async_action_cb(void* user_data)
{
auto* payload = static_cast<ActionPayload*>(user_data);
if (!payload) {
if (!payload)
{
return;
}
LifetimeGuard* guard = payload->guard;
if (guard && guard->alive && payload->action_cb) {
if (guard && guard->alive && payload->action_cb)
{
payload->action_cb(payload->intent, payload->conv, payload->user_data);
}
if (guard && guard->pending_async > 0) {
if (guard && guard->pending_async > 0)
{
guard->pending_async--;
if (guard->owner_dead && !guard->alive && guard->pending_async == 0) {
if (guard->owner_dead && !guard->alive && guard->pending_async == 0)
{
delete guard;
}
}
@@ -510,7 +605,8 @@ void ChatMessageListScreen::async_action_cb(void* user_data)
void ChatMessageListScreen::on_root_deleted(lv_event_t* e)
{
auto* screen = static_cast<ChatMessageListScreen*>(lv_event_get_user_data(e));
if (!screen) {
if (!screen)
{
return;
}
screen->handle_root_deleted();
@@ -518,11 +614,13 @@ void ChatMessageListScreen::on_root_deleted(lv_event_t* e)
void ChatMessageListScreen::handle_root_deleted()
{
if ((!guard_ || !guard_->alive) && container_ == nullptr) {
if ((!guard_ || !guard_->alive) && container_ == nullptr)
{
return;
}
if (guard_) {
if (guard_)
{
guard_->alive = false;
}
action_cb_ = nullptr;
@@ -531,7 +629,8 @@ void ChatMessageListScreen::handle_root_deleted()
chat::ui::message_list::input::cleanup(&input_binding_);
clear_all_timers();
if (top_bar_.back_btn) {
if (top_bar_.back_btn)
{
::ui::widgets::top_bar_set_back_callback(top_bar_, nullptr, nullptr);
}
@@ -544,13 +643,13 @@ void ChatMessageListScreen::handle_root_deleted()
direct_btn_ = nullptr;
broadcast_btn_ = nullptr;
list_back_btn_ = nullptr;
}
void ChatMessageListScreen::schedule_action_async(ActionIntent intent,
const chat::ConversationId& conv)
{
if (!guard_ || !guard_->alive || !action_cb_) {
if (!guard_ || !guard_->alive || !action_cb_)
{
return;
}
auto* payload = new ActionPayload();
@@ -568,11 +667,13 @@ lv_timer_t* ChatMessageListScreen::add_timer(lv_timer_cb_t cb,
void* user_data,
TimerDomain domain)
{
if (!guard_ || !guard_->alive) {
if (!guard_ || !guard_->alive)
{
return nullptr;
}
lv_timer_t* timer = lv_timer_create(cb, period_ms, user_data);
if (timer) {
if (timer)
{
TimerEntry entry;
entry.timer = timer;
entry.domain = domain;
@@ -583,25 +684,31 @@ lv_timer_t* ChatMessageListScreen::add_timer(lv_timer_cb_t cb,
void ChatMessageListScreen::clear_timers(TimerDomain domain)
{
if (timers_.empty()) {
if (timers_.empty())
{
return;
}
for (auto& entry : timers_) {
if (entry.timer && entry.domain == domain) {
for (auto& entry : timers_)
{
if (entry.timer && entry.domain == domain)
{
lv_timer_del(entry.timer);
entry.timer = nullptr;
}
}
timers_.erase(
std::remove_if(timers_.begin(), timers_.end(),
[](const TimerEntry& entry) { return entry.timer == nullptr; }),
[](const TimerEntry& entry)
{ return entry.timer == nullptr; }),
timers_.end());
}
void ChatMessageListScreen::clear_all_timers()
{
for (auto& entry : timers_) {
if (entry.timer) {
for (auto& entry : timers_)
{
if (entry.timer)
{
lv_timer_del(entry.timer);
entry.timer = nullptr;
}
@@ -5,19 +5,23 @@
#pragma once
#include "lvgl.h"
#include "../../chat/domain/chat_types.h"
#include "../../widgets/top_bar.h"
#include "chat_message_list_input.h"
#include <vector>
#include "lvgl.h"
#include <string>
#include <vector>
namespace chat {
namespace ui {
namespace chat
{
namespace ui
{
class ChatMessageListScreen {
public:
enum class ActionIntent {
class ChatMessageListScreen
{
public:
enum class ActionIntent
{
SelectConversation,
Back
};
@@ -57,24 +61,28 @@ public:
lv_obj_t* getItemButton(size_t index) const;
int getSelectedIndex() const { return selected_index_; }
private:
enum class TimerDomain {
private:
enum class TimerDomain
{
ScreenGeneral,
Input
};
struct TimerEntry {
struct TimerEntry
{
lv_timer_t* timer = nullptr;
TimerDomain domain = TimerDomain::ScreenGeneral;
};
struct LifetimeGuard {
struct LifetimeGuard
{
bool alive = false;
int pending_async = 0;
bool owner_dead = false;
};
struct ActionPayload {
struct ActionPayload
{
LifetimeGuard* guard = nullptr;
void (*action_cb)(ActionIntent intent,
const chat::ConversationId& conv,
@@ -84,7 +92,8 @@ private:
chat::ConversationId conv{};
};
enum class FilterMode {
enum class FilterMode
{
Direct,
Broadcast,
Team
@@ -107,7 +116,8 @@ private:
void*) = nullptr;
void* action_cb_user_data_ = nullptr;
struct MessageItem {
struct MessageItem
{
chat::ConversationId conv{};
lv_obj_t* btn = nullptr;
lv_obj_t* name_label = nullptr;
+97 -46
View File
@@ -9,10 +9,10 @@
* - Back/ESC returns to filter column.
*/
#include <Arduino.h> // keep Arduino first if your build requires it
#include "chat_message_list_input.h"
#include "chat_message_list_components.h"
#include "../ui_common.h"
#include "chat_message_list_components.h"
#include <Arduino.h> // keep Arduino first if your build requires it
#define CHAT_INPUT_DEBUG 1
#if CHAT_INPUT_DEBUG
@@ -21,9 +21,11 @@
#define CHAT_INPUT_LOG(...)
#endif
namespace chat::ui::message_list::input {
namespace chat::ui::message_list::input
{
namespace {
namespace
{
static bool is_encoder_active()
{
@@ -64,15 +66,20 @@ static void clear_list_focus_states(Binding* binding)
if (!binding || !binding->screen) return;
lv_state_t clear_mask = (lv_state_t)(LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
size_t count = binding->screen->getItemCount();
for (size_t i = 0; i < count; ++i) {
if (lv_obj_t* btn = binding->screen->getItemButton(i)) {
if (lv_obj_is_valid(btn)) {
for (size_t i = 0; i < count; ++i)
{
if (lv_obj_t* btn = binding->screen->getItemButton(i))
{
if (lv_obj_is_valid(btn))
{
lv_obj_clear_state(btn, clear_mask);
}
}
}
if (lv_obj_t* back = binding->screen->getListBackButton()) {
if (lv_obj_is_valid(back)) {
if (lv_obj_t* back = binding->screen->getListBackButton())
{
if (lv_obj_is_valid(back))
{
lv_obj_clear_state(back, clear_mask);
}
}
@@ -85,51 +92,70 @@ static void bind_filter_column(Binding* binding)
group_clear_all(binding->group);
clear_list_focus_states(binding);
if (lv_obj_t* back = binding->screen->getBackButton()) {
if (lv_obj_t* back = binding->screen->getBackButton())
{
group_add_if_valid(binding, back);
}
if (lv_obj_t* direct = binding->screen->getDirectButton()) {
if (lv_obj_t* direct = binding->screen->getDirectButton())
{
group_add_if_valid(binding, direct);
}
if (lv_obj_t* broadcast = binding->screen->getBroadcastButton()) {
if (lv_obj_t* broadcast = binding->screen->getBroadcastButton())
{
group_add_if_valid(binding, broadcast);
}
if (lv_obj_t* team = binding->screen->getTeamButton()) {
if (is_visible(team)) {
if (lv_obj_t* team = binding->screen->getTeamButton())
{
if (is_visible(team))
{
group_add_if_valid(binding, team);
}
}
lv_group_focus_freeze(binding->group, false);
if (lv_obj_t* direct = binding->screen->getDirectButton()) {
if (lv_obj_has_state(direct, LV_STATE_CHECKED)) {
if (lv_obj_t* direct = binding->screen->getDirectButton())
{
if (lv_obj_has_state(direct, LV_STATE_CHECKED))
{
focus_first_valid(binding, direct);
return;
}
}
if (lv_obj_t* broadcast = binding->screen->getBroadcastButton()) {
if (lv_obj_has_state(broadcast, LV_STATE_CHECKED)) {
if (lv_obj_t* broadcast = binding->screen->getBroadcastButton())
{
if (lv_obj_has_state(broadcast, LV_STATE_CHECKED))
{
focus_first_valid(binding, broadcast);
return;
}
}
if (lv_obj_t* team = binding->screen->getTeamButton()) {
if (is_visible(team) && lv_obj_has_state(team, LV_STATE_CHECKED)) {
if (lv_obj_t* team = binding->screen->getTeamButton())
{
if (is_visible(team) && lv_obj_has_state(team, LV_STATE_CHECKED))
{
focus_first_valid(binding, team);
return;
}
}
if (lv_obj_t* direct = binding->screen->getDirectButton()) {
if (lv_obj_t* direct = binding->screen->getDirectButton())
{
focus_first_valid(binding, direct);
} else if (lv_obj_t* broadcast = binding->screen->getBroadcastButton()) {
}
else if (lv_obj_t* broadcast = binding->screen->getBroadcastButton())
{
focus_first_valid(binding, broadcast);
} else if (lv_obj_t* team = binding->screen->getTeamButton()) {
if (is_visible(team)) {
}
else if (lv_obj_t* team = binding->screen->getTeamButton())
{
if (is_visible(team))
{
focus_first_valid(binding, team);
}
} else if (lv_obj_t* back = binding->screen->getBackButton()) {
}
else if (lv_obj_t* back = binding->screen->getBackButton())
{
focus_first_valid(binding, back);
}
}
@@ -141,27 +167,38 @@ static void bind_list_column(Binding* binding)
group_clear_all(binding->group);
size_t count = binding->screen->getItemCount();
for (size_t i = 0; i < count; ++i) {
if (lv_obj_t* btn = binding->screen->getItemButton(i)) {
for (size_t i = 0; i < count; ++i)
{
if (lv_obj_t* btn = binding->screen->getItemButton(i))
{
group_add_if_valid(binding, btn);
}
}
if (lv_obj_t* back = binding->screen->getListBackButton()) {
if (lv_obj_t* back = binding->screen->getListBackButton())
{
group_add_if_valid(binding, back);
}
lv_group_focus_freeze(binding->group, false);
if (count > 0) {
if (count > 0)
{
int selected = binding->screen->getSelectedIndex();
if (selected >= 0 && static_cast<size_t>(selected) < count) {
if (selected >= 0 && static_cast<size_t>(selected) < count)
{
focus_first_valid(binding, binding->screen->getItemButton(static_cast<size_t>(selected)));
} else {
}
else
{
focus_first_valid(binding, binding->screen->getItemButton(0));
}
} else if (lv_obj_t* back = binding->screen->getListBackButton()) {
}
else if (lv_obj_t* back = binding->screen->getListBackButton())
{
focus_first_valid(binding, back);
} else {
}
else
{
binding->col = FocusColumn::Filter;
bind_filter_column(binding);
}
@@ -170,9 +207,12 @@ static void bind_list_column(Binding* binding)
static void rebind_by_column(Binding* binding)
{
if (!binding) return;
if (binding->col == FocusColumn::Filter) {
if (binding->col == FocusColumn::Filter)
{
bind_filter_column(binding);
} else {
}
else
{
bind_list_column(binding);
}
}
@@ -184,7 +224,8 @@ static void root_key_event_cb(lv_event_t* e)
if (!screen_alive(binding)) return;
uint32_t key = lv_event_get_key(e);
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE) {
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE)
{
binding->col = FocusColumn::Filter;
rebind_by_column(binding);
return;
@@ -195,8 +236,10 @@ static void root_key_event_cb(lv_event_t* e)
lv_obj_t* focused = binding->group ? lv_group_get_focused(binding->group) : nullptr;
if (!focused || !binding->screen) return;
if (binding->col == FocusColumn::Filter) {
if (focused == binding->screen->getBackButton()) {
if (binding->col == FocusColumn::Filter)
{
if (focused == binding->screen->getBackButton())
{
lv_obj_send_event(focused, LV_EVENT_CLICKED, nullptr);
return;
}
@@ -205,8 +248,10 @@ static void root_key_event_cb(lv_event_t* e)
return;
}
if (binding->col == FocusColumn::List) {
if (focused == binding->screen->getListBackButton()) {
if (binding->col == FocusColumn::List)
{
if (focused == binding->screen->getListBackButton())
{
binding->col = FocusColumn::Filter;
rebind_by_column(binding);
return;
@@ -218,10 +263,12 @@ static void root_key_event_cb(lv_event_t* e)
void init(ChatMessageListScreen* screen, Binding* binding)
{
if (!binding) {
if (!binding)
{
return;
}
if (binding->bound) {
if (binding->bound)
{
cleanup(binding);
}
@@ -233,7 +280,8 @@ void init(ChatMessageListScreen* screen, Binding* binding)
rebind_by_column(binding);
set_default_group(binding->group);
if (binding->screen && binding->screen->getObj()) {
if (binding->screen && binding->screen->getObj())
{
lv_obj_add_event_cb(binding->screen->getObj(), root_key_event_cb, LV_EVENT_KEY, binding);
}
binding->bound = true;
@@ -243,16 +291,19 @@ void init(ChatMessageListScreen* screen, Binding* binding)
void cleanup(Binding* binding)
{
if (!binding || !binding->bound) {
if (!binding || !binding->bound)
{
return;
}
binding->screen = nullptr;
if (binding->group) {
if (binding->group)
{
set_default_group(nullptr);
lv_group_del(binding->group);
binding->group = nullptr;
}
if (binding->prev_group) {
if (binding->prev_group)
{
set_default_group(binding->prev_group);
}
binding->prev_group = nullptr;
@@ -11,18 +11,22 @@
#include "lvgl.h"
namespace chat::ui {
namespace chat::ui
{
class ChatMessageListScreen;
namespace message_list::input {
namespace message_list::input
{
enum class FocusColumn {
enum class FocusColumn
{
Filter = 0,
List = 1
};
struct Binding {
struct Binding
{
ChatMessageListScreen* screen = nullptr;
lv_group_t* group = nullptr;
lv_group_t* prev_group = nullptr;
@@ -55,10 +55,11 @@
* - Styles are applied via chat_message_list_styles.*.
*/
#include <Arduino.h>
#include "chat_message_list_layout.h"
#include <Arduino.h>
namespace chat::ui::layout {
namespace chat::ui::layout
{
static void make_non_scrollable(lv_obj_t* obj)
{
@@ -66,7 +67,8 @@ static void make_non_scrollable(lv_obj_t* obj)
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
}
namespace {
namespace
{
constexpr int kFilterPanelWidth = 80;
constexpr int kButtonHeight = 32;
constexpr int kPanelGap = 0;
@@ -1,10 +1,12 @@
#pragma once
#include "lvgl.h"
#include "../../chat/domain/chat_types.h"
#include "lvgl.h"
namespace chat::ui::layout {
namespace chat::ui::layout
{
struct MessageListLayout {
struct MessageListLayout
{
lv_obj_t* root;
lv_obj_t* content;
lv_obj_t* filter_panel;
@@ -21,7 +23,8 @@ lv_obj_t* create_root(lv_obj_t* parent);
MessageListLayout create_layout(lv_obj_t* parent);
// message list item (button with labels)
struct MessageItemWidgets {
struct MessageItemWidgets
{
lv_obj_t* btn;
lv_obj_t* name_label;
lv_obj_t* preview_label;
@@ -1,7 +1,8 @@
#include "chat_message_list_styles.h"
#include "../../assets/fonts/fonts.h"
namespace chat::ui::message_list::styles {
namespace chat::ui::message_list::styles
{
static constexpr uint32_t kPrimary = 0xEBA341;
static constexpr uint32_t kPrimaryLight = 0xF1B65A;
@@ -136,10 +137,30 @@ void apply_filter_btn(lv_obj_t* btn)
lv_obj_add_style(btn, &s_filter_btn_checked, LV_STATE_CHECKED);
}
void apply_label_name(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_name, 0); }
void apply_label_preview(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_preview, 0); }
void apply_label_time(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_time, 0); }
void apply_label_unread(lv_obj_t* label) { init_once(); lv_obj_add_style(label, &s_label_unread, 0); }
void apply_label_placeholder(lv_obj_t* label){ init_once(); lv_obj_add_style(label, &s_label_placeholder, 0); }
void apply_label_name(lv_obj_t* label)
{
init_once();
lv_obj_add_style(label, &s_label_name, 0);
}
void apply_label_preview(lv_obj_t* label)
{
init_once();
lv_obj_add_style(label, &s_label_preview, 0);
}
void apply_label_time(lv_obj_t* label)
{
init_once();
lv_obj_add_style(label, &s_label_time, 0);
}
void apply_label_unread(lv_obj_t* label)
{
init_once();
lv_obj_add_style(label, &s_label_unread, 0);
}
void apply_label_placeholder(lv_obj_t* label)
{
init_once();
lv_obj_add_style(label, &s_label_placeholder, 0);
}
} // namespace chat::ui::message_list::styles
@@ -1,7 +1,8 @@
#pragma once
#include "lvgl.h"
namespace chat::ui::message_list::styles {
namespace chat::ui::message_list::styles
{
void init_once();
File diff suppressed because it is too large Load Diff
+193 -82
View File
@@ -3,10 +3,10 @@
* @brief Contacts page input handling implementation
*/
#include <Arduino.h> // Must be first for library compilation
#include <Preferences.h> // Required for contacts_state.h -> contact_service.h dependency chain
#include "contacts_page_input.h"
#include "contacts_state.h"
#include <Arduino.h> // Must be first for library compilation
#include <Preferences.h> // Required for contacts_state.h -> contact_service.h dependency chain
#define CONTACTS_DEBUG 1
#if CONTACTS_DEBUG
@@ -17,11 +17,13 @@
using namespace contacts::ui;
namespace {
namespace
{
enum class FocusColumn {
enum class FocusColumn
{
Filter = 0,
List = 1,
List = 1,
Action = 2,
};
@@ -32,8 +34,10 @@ static lv_indev_t* s_encoder = nullptr;
static lv_indev_t* find_encoder_indev()
{
lv_indev_t* indev = nullptr;
while ((indev = lv_indev_get_next(indev)) != nullptr) {
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER) {
while ((indev = lv_indev_get_next(indev)) != nullptr)
{
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER)
{
return indev;
}
}
@@ -82,72 +86,109 @@ static void bind_filter_column(bool keep_mode_focus)
clear_action_focus_states();
// ① TopBar Back —— 第一列“出口”,必须可达
if (g_contacts_state.top_bar.back_btn) {
if (g_contacts_state.top_bar.back_btn)
{
lv_group_add_obj(s_group, g_contacts_state.top_bar.back_btn);
}
// ② Filter buttons
if (is_visible(g_contacts_state.contacts_btn)) lv_group_add_obj(s_group, g_contacts_state.contacts_btn);
if (is_visible(g_contacts_state.nearby_btn)) lv_group_add_obj(s_group, g_contacts_state.nearby_btn);
if (is_visible(g_contacts_state.nearby_btn)) lv_group_add_obj(s_group, g_contacts_state.nearby_btn);
if (is_visible(g_contacts_state.broadcast_btn)) lv_group_add_obj(s_group, g_contacts_state.broadcast_btn);
if (is_visible(g_contacts_state.team_btn)) lv_group_add_obj(s_group, g_contacts_state.team_btn);
// Focus preference:
// - keep_mode_focus: focus current mode button (better for rotate-to-switch-mode UX)
// - else: focus current mode button; fallback to back
if (keep_mode_focus) {
if (g_contacts_state.current_mode == ContactsMode::Contacts && g_contacts_state.contacts_btn) {
if (keep_mode_focus)
{
if (g_contacts_state.current_mode == ContactsMode::Contacts && g_contacts_state.contacts_btn)
{
focus_first_valid(g_contacts_state.contacts_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Nearby && g_contacts_state.nearby_btn) {
}
else if (g_contacts_state.current_mode == ContactsMode::Nearby && g_contacts_state.nearby_btn)
{
focus_first_valid(g_contacts_state.nearby_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Broadcast && is_visible(g_contacts_state.broadcast_btn)) {
}
else if (g_contacts_state.current_mode == ContactsMode::Broadcast && is_visible(g_contacts_state.broadcast_btn))
{
focus_first_valid(g_contacts_state.broadcast_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Team && is_visible(g_contacts_state.team_btn)) {
focus_first_valid(g_contacts_state.team_btn);
} else if (g_contacts_state.top_bar.back_btn) {
focus_first_valid(g_contacts_state.top_bar.back_btn);
} else if (is_visible(g_contacts_state.contacts_btn)) {
focus_first_valid(g_contacts_state.contacts_btn);
} else if (is_visible(g_contacts_state.nearby_btn)) {
focus_first_valid(g_contacts_state.nearby_btn);
} else if (is_visible(g_contacts_state.broadcast_btn)) {
focus_first_valid(g_contacts_state.broadcast_btn);
} else if (is_visible(g_contacts_state.team_btn)) {
}
else if (g_contacts_state.current_mode == ContactsMode::Team && is_visible(g_contacts_state.team_btn))
{
focus_first_valid(g_contacts_state.team_btn);
}
} else {
else if (g_contacts_state.top_bar.back_btn)
{
focus_first_valid(g_contacts_state.top_bar.back_btn);
}
else if (is_visible(g_contacts_state.contacts_btn))
{
focus_first_valid(g_contacts_state.contacts_btn);
}
else if (is_visible(g_contacts_state.nearby_btn))
{
focus_first_valid(g_contacts_state.nearby_btn);
}
else if (is_visible(g_contacts_state.broadcast_btn))
{
focus_first_valid(g_contacts_state.broadcast_btn);
}
else if (is_visible(g_contacts_state.team_btn))
{
focus_first_valid(g_contacts_state.team_btn);
}
}
else
{
// first entry: prefer current mode button
if (g_contacts_state.current_mode == ContactsMode::Contacts && is_visible(g_contacts_state.contacts_btn)) {
if (g_contacts_state.current_mode == ContactsMode::Contacts && is_visible(g_contacts_state.contacts_btn))
{
focus_first_valid(g_contacts_state.contacts_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Nearby && is_visible(g_contacts_state.nearby_btn)) {
}
else if (g_contacts_state.current_mode == ContactsMode::Nearby && is_visible(g_contacts_state.nearby_btn))
{
focus_first_valid(g_contacts_state.nearby_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Broadcast && is_visible(g_contacts_state.broadcast_btn)) {
}
else if (g_contacts_state.current_mode == ContactsMode::Broadcast && is_visible(g_contacts_state.broadcast_btn))
{
focus_first_valid(g_contacts_state.broadcast_btn);
} else if (g_contacts_state.current_mode == ContactsMode::Team && is_visible(g_contacts_state.team_btn)) {
}
else if (g_contacts_state.current_mode == ContactsMode::Team && is_visible(g_contacts_state.team_btn))
{
focus_first_valid(g_contacts_state.team_btn);
} else if (is_visible(g_contacts_state.contacts_btn)) {
}
else if (is_visible(g_contacts_state.contacts_btn))
{
focus_first_valid(g_contacts_state.contacts_btn);
} else if (is_visible(g_contacts_state.nearby_btn)) {
}
else if (is_visible(g_contacts_state.nearby_btn))
{
focus_first_valid(g_contacts_state.nearby_btn);
} else if (is_visible(g_contacts_state.broadcast_btn)) {
}
else if (is_visible(g_contacts_state.broadcast_btn))
{
focus_first_valid(g_contacts_state.broadcast_btn);
} else if (is_visible(g_contacts_state.team_btn)) {
}
else if (is_visible(g_contacts_state.team_btn))
{
focus_first_valid(g_contacts_state.team_btn);
} else if (g_contacts_state.top_bar.back_btn) {
}
else if (g_contacts_state.top_bar.back_btn)
{
focus_first_valid(g_contacts_state.top_bar.back_btn);
}
}
}
static void bind_list_column()
{
if (!s_group) return;
group_clear_all(s_group);
clear_action_focus_states();
for (auto* item : g_contacts_state.list_items) {
for (auto* item : g_contacts_state.list_items)
{
if (item) lv_group_add_obj(s_group, item);
}
@@ -155,13 +196,20 @@ static void bind_list_column()
if (g_contacts_state.next_btn) lv_group_add_obj(s_group, g_contacts_state.next_btn);
if (g_contacts_state.back_btn) lv_group_add_obj(s_group, g_contacts_state.back_btn);
if (!g_contacts_state.list_items.empty() && g_contacts_state.list_items[0]) {
if (!g_contacts_state.list_items.empty() && g_contacts_state.list_items[0])
{
focus_first_valid(g_contacts_state.list_items[0]);
} else if (g_contacts_state.back_btn) {
}
else if (g_contacts_state.back_btn)
{
focus_first_valid(g_contacts_state.back_btn);
} else if (g_contacts_state.prev_btn) {
}
else if (g_contacts_state.prev_btn)
{
focus_first_valid(g_contacts_state.prev_btn);
} else if (g_contacts_state.next_btn) {
}
else if (g_contacts_state.next_btn)
{
focus_first_valid(g_contacts_state.next_btn);
}
@@ -177,36 +225,69 @@ static void bind_action_column()
bool any = false;
// ① Actions
if (g_contacts_state.chat_btn) { lv_group_add_obj(s_group, g_contacts_state.chat_btn); any = true; }
if (g_contacts_state.chat_btn)
{
lv_group_add_obj(s_group, g_contacts_state.chat_btn);
any = true;
}
if (g_contacts_state.current_mode == ContactsMode::Contacts) {
if (g_contacts_state.edit_btn) { lv_group_add_obj(s_group, g_contacts_state.edit_btn); any = true; }
if (g_contacts_state.del_btn) { lv_group_add_obj(s_group, g_contacts_state.del_btn); any = true; }
} else if (g_contacts_state.current_mode == ContactsMode::Nearby) {
if (g_contacts_state.add_btn) { lv_group_add_obj(s_group, g_contacts_state.add_btn); any = true; }
} else if (g_contacts_state.current_mode == ContactsMode::Team) {
if (g_contacts_state.position_btn) { lv_group_add_obj(s_group, g_contacts_state.position_btn); any = true; }
if (g_contacts_state.current_mode == ContactsMode::Contacts)
{
if (g_contacts_state.edit_btn)
{
lv_group_add_obj(s_group, g_contacts_state.edit_btn);
any = true;
}
if (g_contacts_state.del_btn)
{
lv_group_add_obj(s_group, g_contacts_state.del_btn);
any = true;
}
}
else if (g_contacts_state.current_mode == ContactsMode::Nearby)
{
if (g_contacts_state.add_btn)
{
lv_group_add_obj(s_group, g_contacts_state.add_btn);
any = true;
}
}
else if (g_contacts_state.current_mode == ContactsMode::Team)
{
if (g_contacts_state.position_btn)
{
lv_group_add_obj(s_group, g_contacts_state.position_btn);
any = true;
}
}
if (g_contacts_state.current_mode != ContactsMode::Broadcast &&
g_contacts_state.current_mode != ContactsMode::Team) {
if (g_contacts_state.info_btn) { lv_group_add_obj(s_group, g_contacts_state.info_btn); any = true; }
g_contacts_state.current_mode != ContactsMode::Team)
{
if (g_contacts_state.info_btn)
{
lv_group_add_obj(s_group, g_contacts_state.info_btn);
any = true;
}
}
// ② Back (added last, not the default focus)
if (g_contacts_state.action_back_btn) {
if (g_contacts_state.action_back_btn)
{
lv_group_add_obj(s_group, g_contacts_state.action_back_btn);
any = true;
}
if (!any) {
if (!any)
{
s_col = FocusColumn::List;
bind_list_column();
return;
}
// Only allow action focus when a list item is selected
if (g_contacts_state.selected_index < 0) {
if (g_contacts_state.selected_index < 0)
{
clear_action_focus_states();
s_col = FocusColumn::List;
bind_list_column();
@@ -214,13 +295,20 @@ static void bind_action_column()
}
// Default focus to the first action button (Chat) instead of Back
if (g_contacts_state.chat_btn) {
if (g_contacts_state.chat_btn)
{
focus_first_valid(g_contacts_state.chat_btn);
} else if (g_contacts_state.position_btn) {
}
else if (g_contacts_state.position_btn)
{
focus_first_valid(g_contacts_state.position_btn);
} else if (g_contacts_state.info_btn) {
}
else if (g_contacts_state.info_btn)
{
focus_first_valid(g_contacts_state.info_btn);
} else if (g_contacts_state.action_back_btn) {
}
else if (g_contacts_state.action_back_btn)
{
focus_first_valid(g_contacts_state.action_back_btn);
}
@@ -229,10 +317,17 @@ static void bind_action_column()
static void rebind_by_column()
{
switch (s_col) {
case FocusColumn::Filter: bind_filter_column(true); break;
case FocusColumn::List: bind_list_column(); break;
case FocusColumn::Action: bind_action_column(); break;
switch (s_col)
{
case FocusColumn::Filter:
bind_filter_column(true);
break;
case FocusColumn::List:
bind_list_column();
break;
case FocusColumn::Action:
bind_action_column();
break;
}
}
@@ -252,7 +347,8 @@ static void root_key_event_cb(lv_event_t* e)
uint32_t key = lv_event_get_key(e);
// Back one column
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE) {
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE)
{
if (s_col == FocusColumn::Action) s_col = FocusColumn::List;
else if (s_col == FocusColumn::List) s_col = FocusColumn::Filter;
else s_col = FocusColumn::Filter;
@@ -264,32 +360,39 @@ static void root_key_event_cb(lv_event_t* e)
lv_obj_t* focused = s_group ? lv_group_get_focused(s_group) : nullptr;
if (s_col == FocusColumn::Filter) {
if (s_col == FocusColumn::Filter)
{
s_col = FocusColumn::List;
rebind_by_column();
return;
}
if (s_col == FocusColumn::List) {
if (focused == g_contacts_state.back_btn) {
if (s_col == FocusColumn::List)
{
if (focused == g_contacts_state.back_btn)
{
s_col = FocusColumn::Filter;
rebind_by_column();
return;
}
if (focused == g_contacts_state.prev_btn) {
if (focused == g_contacts_state.prev_btn)
{
if (g_contacts_state.prev_btn) lv_obj_send_event(g_contacts_state.prev_btn, LV_EVENT_CLICKED, nullptr);
if (g_contacts_state.prev_btn) focus_first_valid(g_contacts_state.prev_btn);
return;
}
if (focused == g_contacts_state.next_btn) {
if (focused == g_contacts_state.next_btn)
{
if (g_contacts_state.next_btn) lv_obj_send_event(g_contacts_state.next_btn, LV_EVENT_CLICKED, nullptr);
if (g_contacts_state.next_btn) focus_first_valid(g_contacts_state.next_btn);
return;
}
for (size_t i = 0; i < g_contacts_state.list_items.size(); ++i) {
if (focused == g_contacts_state.list_items[i]) {
for (size_t i = 0; i < g_contacts_state.list_items.size(); ++i)
{
if (focused == g_contacts_state.list_items[i])
{
// selected_index stored in user_data by refresh_ui()
g_contacts_state.selected_index = (int)(intptr_t)lv_obj_get_user_data(focused);
s_col = FocusColumn::Action;
@@ -300,8 +403,10 @@ static void root_key_event_cb(lv_event_t* e)
return;
}
if (s_col == FocusColumn::Action) {
if (focused) {
if (s_col == FocusColumn::Action)
{
if (focused)
{
lv_obj_send_event(focused, LV_EVENT_CLICKED, nullptr);
}
return;
@@ -312,26 +417,31 @@ static void root_key_event_cb(lv_event_t* e)
void init_contacts_input()
{
if (s_group) {
if (s_group)
{
cleanup_contacts_input();
}
s_group = lv_group_create();
s_encoder = find_encoder_indev();
if (s_encoder) {
if (s_encoder)
{
lv_indev_set_group(s_encoder, s_group);
} else {
}
else
{
CONTACTS_LOG("[Contacts][Input] WARNING: no encoder indev found\n");
}
s_col = FocusColumn::Filter;
rebind_by_column();
lv_obj_t* key_target = g_contacts_state.root
? g_contacts_state.root
: (g_contacts_state.list_panel ? g_contacts_state.list_panel
: g_contacts_state.filter_panel);
? g_contacts_state.root
: (g_contacts_state.list_panel ? g_contacts_state.list_panel
: g_contacts_state.filter_panel);
if (key_target) {
if (key_target)
{
lv_obj_add_event_cb(key_target, root_key_event_cb, LV_EVENT_KEY, nullptr);
}
@@ -340,10 +450,12 @@ void init_contacts_input()
void cleanup_contacts_input()
{
if (s_group) {
if (s_group)
{
// 只解绑我们自己绑定过的 encoder
if (s_encoder && lv_indev_get_group(s_encoder) == s_group) {
if (s_encoder && lv_indev_get_group(s_encoder) == s_group)
{
lv_indev_set_group(s_encoder, nullptr);
}
@@ -356,7 +468,6 @@ void cleanup_contacts_input()
CONTACTS_LOG("[Contacts][Input] cleaned up\n");
}
void contacts_input_on_ui_refreshed()
{
if (!s_group) return;
@@ -1,12 +1,15 @@
#pragma once
#include "contacts_page_styles.h" // style::apply_*
#include "contacts_state.h" // g_contacts_state, ContactsMode
#include "lvgl.h"
#include "contacts_state.h" // g_contacts_state, ContactsMode
#include "contacts_page_styles.h" // style::apply_*
namespace contacts {
namespace ui {
namespace layout {
namespace contacts
{
namespace ui
{
namespace layout
{
/**
* @brief Create root container for contacts page
@@ -1,20 +1,21 @@
#include "contacts_page_styles.h"
namespace contacts::ui::style {
namespace contacts::ui::style
{
// ---------- Colors (keep all visual tokens here) ----------
static constexpr uint32_t kGrayPanel = 0xF5F5F5;
static constexpr uint32_t kWhite = 0xFFFFFF;
static constexpr uint32_t kWhite = 0xFFFFFF;
static constexpr uint32_t kBtnBg = 0xF4C77A;
static constexpr uint32_t kBtnBgSel = 0xEBA341;
static constexpr uint32_t kBtnBg = 0xF4C77A;
static constexpr uint32_t kBtnBgSel = 0xEBA341;
static constexpr uint32_t kBtnBorder = 0xEBA341;
static constexpr uint32_t kItemBg = 0xF7DCA8;
static constexpr uint32_t kItemBg = 0xF7DCA8;
static constexpr uint32_t kItemBgFoc = 0xF1B65A;
static constexpr uint32_t kItemBorder= 0xEBA341;
static constexpr uint32_t kItemBorder = 0xEBA341;
static constexpr uint32_t kTextMain = 0x202020;
static constexpr uint32_t kTextMain = 0x202020;
static constexpr uint32_t kTextMuted = 0x606060;
// ---------- Styles ----------
@@ -25,10 +26,10 @@ static lv_style_t s_panel_main;
static lv_style_t s_container_white;
static lv_style_t s_btn_basic;
static lv_style_t s_btn_filter_checked; // applied via LV_STATE_CHECKED
static lv_style_t s_btn_filter_checked; // applied via LV_STATE_CHECKED
static lv_style_t s_item_base;
static lv_style_t s_item_focused; // applied via LV_STATE_FOCUSED
static lv_style_t s_item_focused; // applied via LV_STATE_FOCUSED
static lv_style_t s_label_primary;
static lv_style_t s_label_muted;
+10 -9
View File
@@ -1,25 +1,26 @@
#pragma once
#include "lvgl.h"
namespace contacts::ui::style {
namespace contacts::ui::style
{
// Must be called before applying any styles (safe to call repeatedly)
void init_once();
/* Panels */
void apply_panel_side(lv_obj_t* obj); // filter_panel / action_panel (gray)
void apply_panel_main(lv_obj_t* obj); // list_panel (white)
void apply_container_white(lv_obj_t* obj); // sub_container / bottom_container (white)
void apply_panel_side(lv_obj_t* obj); // filter_panel / action_panel (gray)
void apply_panel_main(lv_obj_t* obj); // list_panel (white)
void apply_container_white(lv_obj_t* obj); // sub_container / bottom_container (white)
/* Buttons */
void apply_btn_basic(lv_obj_t* btn); // common button style
void apply_btn_filter(lv_obj_t* btn); // filter buttons (same as basic, but supports CHECKED state highlight)
void apply_btn_basic(lv_obj_t* btn); // common button style
void apply_btn_filter(lv_obj_t* btn); // filter buttons (same as basic, but supports CHECKED state highlight)
/* List item */
void apply_list_item(lv_obj_t* item); // item base + focused behavior
void apply_list_item(lv_obj_t* item); // item base + focused behavior
/* Labels */
void apply_label_primary(lv_obj_t* label); // name
void apply_label_muted(lv_obj_t* label); // status/snr
void apply_label_primary(lv_obj_t* label); // name
void apply_label_muted(lv_obj_t* label); // status/snr
} // namespace contacts::ui::style
+5 -3
View File
@@ -5,10 +5,12 @@
#include "contacts_state.h"
namespace contacts {
namespace ui {
namespace contacts
{
namespace ui
{
ContactsPageState g_contacts_state; // 这里是“唯一的定义”
ContactsPageState g_contacts_state; // 这里是“唯一的定义”
} // namespace ui
} // namespace contacts
+43 -34
View File
@@ -5,61 +5,70 @@
#pragma once
#include "lvgl.h"
#include "../../widgets/top_bar.h"
#include "../../../chat/domain/contact_types.h"
#include "../../widgets/top_bar.h"
#include "lvgl.h"
#include <vector>
// Forward declaration
namespace chat {
namespace contacts {
class ContactService;
namespace chat
{
namespace contacts
{
class ContactService;
} // namespace contacts
namespace ui {
class ChatComposeScreen;
class ChatConversationScreen;
namespace ui
{
class ChatComposeScreen;
class ChatConversationScreen;
} // namespace ui
class ChatService;
} // namespace chat
namespace ui {
namespace widgets {
class ImeWidget;
namespace ui
{
namespace widgets
{
class ImeWidget;
} // namespace widgets
} // namespace ui
namespace contacts {
namespace ui {
namespace contacts
{
namespace ui
{
enum class ContactsMode {
enum class ContactsMode
{
Contacts, // Show contacts (nodes with nicknames)
Nearby, // Show nearby nodes (nodes without nicknames)
Broadcast, // Show broadcast channels
Team // Show team (if joined)
};
struct ContactsPageState {
struct ContactsPageState
{
lv_obj_t* root = nullptr;
lv_obj_t* page = nullptr; // Content container (second column)
lv_obj_t* page = nullptr; // Content container (second column)
::ui::widgets::TopBar top_bar;
// First column: Filter buttons
lv_obj_t* filter_panel = nullptr;
lv_obj_t* contacts_btn = nullptr;
lv_obj_t* nearby_btn = nullptr;
lv_obj_t* broadcast_btn = nullptr;
lv_obj_t* team_btn = nullptr;
// Second column: Node list
lv_obj_t* list_panel = nullptr;
lv_obj_t* sub_container = nullptr; // Container for list items and pagination
lv_obj_t* bottom_container = nullptr; // Container for bottom buttons (Prev/Next/Back)
std::vector<lv_obj_t*> list_items; // Contact/Node rows
lv_obj_t* sub_container = nullptr; // Container for list items and pagination
lv_obj_t* bottom_container = nullptr; // Container for bottom buttons (Prev/Next/Back)
std::vector<lv_obj_t*> list_items; // Contact/Node rows
lv_obj_t* prev_btn = nullptr;
lv_obj_t* next_btn = nullptr;
lv_obj_t* back_btn = nullptr; // Return to first column
lv_obj_t* back_btn = nullptr; // Return to first column
// Third column: Action buttons
lv_obj_t* action_panel = nullptr;
lv_obj_t* chat_btn = nullptr;
@@ -68,22 +77,22 @@ struct ContactsPageState {
lv_obj_t* del_btn = nullptr;
lv_obj_t* add_btn = nullptr;
lv_obj_t* info_btn = nullptr;
lv_obj_t* action_back_btn = nullptr; // third column back (to list)
lv_obj_t* action_back_btn = nullptr; // third column back (to list)
// Current state
ContactsMode current_mode = ContactsMode::Contacts;
ContactsMode last_action_mode = ContactsMode::Contacts;
int selected_index = -1; // Selected item in list
int current_page = 0; // Current page (0-based)
size_t total_items = 0; // Total items in current mode
int selected_index = -1; // Selected item in list
int current_page = 0; // Current page (0-based)
size_t total_items = 0; // Total items in current mode
// Data (using forward declaration, full type in .cpp)
std::vector<chat::contacts::NodeInfo> contacts_list;
std::vector<chat::contacts::NodeInfo> nearby_list;
// Timers
lv_timer_t* refresh_timer = nullptr;
// Modal windows
lv_obj_t* add_edit_modal = nullptr;
lv_obj_t* add_edit_textarea = nullptr;
@@ -94,7 +103,7 @@ struct ContactsPageState {
lv_group_t* prev_group = nullptr;
uint32_t modal_node_id = 0;
bool modal_is_edit = false;
// Compose screen (Chat button)
chat::ui::ChatComposeScreen* compose_screen = nullptr;
::ui::widgets::ImeWidget* compose_ime = nullptr;
@@ -104,7 +113,7 @@ struct ContactsPageState {
// Services (owned by AppContext)
chat::contacts::ContactService* contact_service = nullptr;
chat::ChatService* chat_service = nullptr;
bool initialized = false;
bool exiting = false;
};
+3 -2
View File
@@ -1,6 +1,7 @@
#pragma once
namespace gps_ui {
namespace gps_ui
{
constexpr int kMapPanStep = 32;
constexpr int kDefaultZoom = 12;
@@ -13,4 +14,4 @@ constexpr double kDefaultLng = -0.1278;
// UI policy: do not show a blocking loading overlay on the GPS map.
constexpr bool kShowLoadingOverlay = false;
} // namespace gps_ui
} // namespace gps_ui
+57 -42
View File
@@ -4,10 +4,10 @@
*/
#include "gps_modal.h"
#include "../../LV_Helper.h"
#include "../../ui_common.h"
#include "gps_page_lifetime.h"
#include "gps_page_styles.h"
#include "../../ui_common.h"
#include "../../LV_Helper.h"
#include <Arduino.h>
// Configuration
@@ -31,13 +31,15 @@ using gps::ui::lifetime::is_alive;
*/
lv_indev_t* get_encoder_indev()
{
if (g_encoder_indev != NULL) {
if (g_encoder_indev != NULL)
{
return g_encoder_indev;
}
// Use helper function from LV_Helper.h
g_encoder_indev = lv_get_encoder_indev();
if (g_encoder_indev == NULL) {
if (g_encoder_indev == NULL)
{
GPS_LOG("[GPS] WARNING: encoder indev is NULL - popup may not receive input\n");
}
return g_encoder_indev;
@@ -46,10 +48,11 @@ lv_indev_t* get_encoder_indev()
/**
* Bind encoder input device to a group
*/
void bind_encoder_to_group(lv_group_t *g)
void bind_encoder_to_group(lv_group_t* g)
{
lv_indev_t *encoder = get_encoder_indev();
if (encoder) {
lv_indev_t* encoder = get_encoder_indev();
if (encoder)
{
lv_indev_set_group(encoder, g);
}
}
@@ -59,59 +62,67 @@ void bind_encoder_to_group(lv_group_t *g)
*/
bool modal_open(Modal& m, lv_obj_t* content_root, lv_group_t* focus_group)
{
if (!is_alive()) {
if (!is_alive())
{
return false;
}
if (m.is_open()) {
if (m.is_open())
{
GPS_LOG("[GPS] Modal already open\n");
return false;
}
// Debounce check
uint32_t now = millis();
if (m.close_ms > 0 && (now - m.close_ms) < POPUP_DEBOUNCE_MS) {
if (m.close_ms > 0 && (now - m.close_ms) < POPUP_DEBOUNCE_MS)
{
GPS_LOG("[GPS] Ignoring modal open request (debounce: %dms since close)\n", now - m.close_ms);
return false;
}
(void)focus_group;
lv_obj_t* screen = content_root ? content_root : lv_screen_active();
if (screen == NULL) {
if (screen == NULL)
{
GPS_LOG("[GPS] ERROR: screen is NULL, cannot create modal\n");
return false;
}
// Create modal background
m.bg = lv_obj_create(screen);
if (m.bg == NULL) {
if (m.bg == NULL)
{
GPS_LOG("[GPS] ERROR: Failed to create modal background\n");
return false;
}
lv_coord_t screen_w = lv_obj_get_width(screen);
lv_coord_t screen_h = lv_obj_get_height(screen);
lv_obj_set_size(m.bg, screen_w, screen_h);
lv_obj_set_pos(m.bg, 0, 0);
gps::ui::styles::apply_modal_bg(m.bg);
lv_obj_move_to_index(m.bg, -1);
// Modal bg only swallows pointer events, not KEY/ROTARY
lv_obj_add_event_cb(m.bg, [](lv_event_t *e) {
lv_obj_add_event_cb(
m.bg, [](lv_event_t* e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED || code == LV_EVENT_PRESSED || code == LV_EVENT_RELEASED) {
lv_event_stop_bubbling(e);
}
}, LV_EVENT_ALL, NULL);
} },
LV_EVENT_ALL, NULL);
// Create modal window
m.win = lv_obj_create(m.bg);
if (m.win == NULL) {
if (m.win == NULL)
{
GPS_LOG("[GPS] ERROR: Failed to create modal window\n");
lv_obj_del(m.bg);
m.bg = nullptr;
return false;
}
// Center modal window
lv_coord_t win_w = 250;
lv_coord_t win_h = 150;
@@ -119,27 +130,28 @@ bool modal_open(Modal& m, lv_obj_t* content_root, lv_group_t* focus_group)
lv_coord_t win_y = (screen_h - win_h) / 2;
lv_obj_set_size(m.win, win_w, win_h);
lv_obj_set_pos(m.win, win_x, win_y);
gps::ui::styles::apply_modal_win(m.win);
lv_obj_move_to_index(m.win, -1);
// Setup group management
extern lv_group_t *app_g;
extern lv_group_t* app_g;
// Get current default group (via lv_group_get_default) instead of assuming app_g
m.prev_default = lv_group_get_default() ? lv_group_get_default() : app_g;
if (m.group == NULL) {
if (m.group == NULL)
{
m.group = lv_group_create();
}
// Clear group - caller (show_zoom_popup) will add popup_label and configure everything
lv_group_remove_all_objs(m.group);
// Don't bind encoder here - let show_zoom_popup do it after popup_label is added and focused
GPS_LOG("[GPS] Modal group created (empty), group=%p\n", m.group);
m.close_ms = 0; // Reset close timestamp
m.close_ms = 0; // Reset close timestamp
m.open = true;
GPS_LOG("[GPS] Modal opened successfully\n");
return true;
}
@@ -149,31 +161,34 @@ bool modal_open(Modal& m, lv_obj_t* content_root, lv_group_t* focus_group)
*/
void modal_close(Modal& m)
{
if (!m.is_open()) {
if (!m.is_open())
{
return;
}
if (is_alive()) {
if (is_alive())
{
// Restore default group and encoder binding BEFORE deleting objects
extern lv_group_t *app_g;
extern lv_group_t* app_g;
lv_group_t* restore = m.prev_default ? m.prev_default : app_g;
set_default_group(restore);
bind_encoder_to_group(restore);
}
// Only delete bg (win is child of bg, will be deleted automatically)
if (m.bg != NULL) {
if (m.bg != NULL)
{
lv_obj_del(m.bg);
m.bg = nullptr;
m.win = nullptr; // Clear pointer (object already deleted)
m.win = nullptr; // Clear pointer (object already deleted)
}
m.close_ms = millis();
m.open = false;
GPS_LOG("[GPS] Modal closed\n");
}
bool modal_is_open(const Modal &m)
bool modal_is_open(const Modal& m)
{
return m.open;
}
+12 -11
View File
@@ -3,23 +3,24 @@
#include "lvgl.h"
typedef struct {
lv_obj_t *bg;
lv_obj_t *win;
lv_group_t *group;
lv_indev_t *indev;
lv_group_t *prev_default;
typedef struct
{
lv_obj_t* bg;
lv_obj_t* win;
lv_group_t* group;
lv_indev_t* indev;
lv_group_t* prev_default;
uint32_t close_ms;
bool open;
bool is_open() const { return open; }
} Modal;
lv_indev_t *get_encoder_indev();
void bind_encoder_to_group(lv_group_t *g);
lv_indev_t* get_encoder_indev();
void bind_encoder_to_group(lv_group_t* g);
bool modal_open(Modal &m, lv_obj_t *content_root, lv_group_t *focus_group);
void modal_close(Modal &m);
bool modal_is_open(const Modal &m);
bool modal_open(Modal& m, lv_obj_t* content_root, lv_group_t* focus_group);
void modal_close(Modal& m);
bool modal_is_open(const Modal& m);
#endif // GPS_MODAL_H
+78 -42
View File
@@ -1,11 +1,11 @@
#include "gps_page_components.h"
#include "../../ui_common.h"
#include "gps_modal.h"
#include "gps_page_input.h"
#include "gps_page_lifetime.h"
#include "gps_page_styles.h"
#include "gps_modal.h"
#include "gps_state.h"
#include "../../ui_common.h"
#include "lvgl.h"
#include <Arduino.h>
@@ -28,10 +28,12 @@ using gps::ui::lifetime::is_alive;
void show_loading()
{
if (!gps_ui::kShowLoadingOverlay) {
if (!gps_ui::kShowLoadingOverlay)
{
return;
}
if (!is_alive() || g_gps_state.loading_msgbox != NULL || g_gps_state.page == NULL) {
if (!is_alive() || g_gps_state.loading_msgbox != NULL || g_gps_state.page == NULL)
{
return;
}
@@ -48,11 +50,13 @@ void show_loading()
void hide_loading()
{
if (!gps_ui::kShowLoadingOverlay) {
if (!gps_ui::kShowLoadingOverlay)
{
g_gps_state.loading_msgbox = NULL;
return;
}
if (g_gps_state.loading_msgbox != NULL) {
if (g_gps_state.loading_msgbox != NULL)
{
lv_obj_del(g_gps_state.loading_msgbox);
g_gps_state.loading_msgbox = NULL;
}
@@ -65,7 +69,8 @@ void hide_loading()
static void toast_timer_cb(lv_timer_t* timer)
{
(void)timer;
if (!is_alive()) {
if (!is_alive())
{
return;
}
hide_toast();
@@ -73,13 +78,15 @@ static void toast_timer_cb(lv_timer_t* timer)
void show_toast(const char* message, uint32_t duration_ms)
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
hide_toast();
if (g_gps_state.page == NULL) {
if (g_gps_state.page == NULL)
{
return;
}
@@ -93,25 +100,29 @@ void show_toast(const char* message, uint32_t duration_ms)
gps::ui::styles::apply_toast_label(toast_label);
lv_obj_center(toast_label);
if (g_gps_state.toast_timer != NULL) {
if (g_gps_state.toast_timer != NULL)
{
gps::ui::lifetime::remove_timer(g_gps_state.toast_timer);
g_gps_state.toast_timer = nullptr;
}
g_gps_state.toast_timer = gps::ui::lifetime::add_timer(toast_timer_cb, duration_ms, NULL);
if (g_gps_state.toast_timer) {
if (g_gps_state.toast_timer)
{
lv_timer_set_repeat_count(g_gps_state.toast_timer, 1);
}
}
void hide_toast()
{
if (g_gps_state.toast_timer != NULL) {
if (g_gps_state.toast_timer != NULL)
{
gps::ui::lifetime::remove_timer(g_gps_state.toast_timer);
g_gps_state.toast_timer = nullptr;
}
if (g_gps_state.toast_msgbox != NULL) {
if (g_gps_state.toast_msgbox != NULL)
{
lv_obj_del(g_gps_state.toast_msgbox);
g_gps_state.toast_msgbox = NULL;
}
@@ -123,13 +134,15 @@ void hide_toast()
void show_pan_h_indicator()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
GPS_LOG("[GPS] show_pan_h_indicator: called, pan_h_indicator=%p, map=%p\n",
g_gps_state.pan_h_indicator, g_gps_state.map);
if (g_gps_state.pan_h_indicator != NULL || g_gps_state.map == NULL) {
if (g_gps_state.pan_h_indicator != NULL || g_gps_state.map == NULL)
{
GPS_LOG("[GPS] show_pan_h_indicator: early return (already shown or map not ready)\n");
return;
}
@@ -151,12 +164,13 @@ void show_pan_h_indicator()
set_control_id(g_gps_state.pan_h_indicator, ControlId::PanHIndicator);
extern lv_group_t* app_g;
if (app_g != NULL) {
if (app_g != NULL)
{
lv_group_add_obj(app_g, g_gps_state.pan_h_indicator);
}
extern void pan_indicator_event_cb(lv_event_t* e);
extern void on_ui_event(lv_event_t* e);
extern void pan_indicator_event_cb(lv_event_t * e);
extern void on_ui_event(lv_event_t * e);
lv_obj_add_event_cb(g_gps_state.pan_h_indicator, pan_indicator_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(g_gps_state.pan_h_indicator, on_ui_event, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(g_gps_state.pan_h_indicator, on_ui_event, LV_EVENT_PRESSED, NULL);
@@ -171,12 +185,14 @@ void show_pan_h_indicator()
void hide_pan_h_indicator()
{
if (g_gps_state.pan_h_indicator == NULL) {
if (g_gps_state.pan_h_indicator == NULL)
{
return;
}
extern lv_group_t* app_g;
if (app_g != NULL) {
if (app_g != NULL)
{
lv_group_remove_obj(g_gps_state.pan_h_indicator);
}
@@ -186,13 +202,15 @@ void hide_pan_h_indicator()
void show_pan_v_indicator()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
GPS_LOG("[GPS] show_pan_v_indicator: called, pan_v_indicator=%p, map=%p\n",
g_gps_state.pan_v_indicator, g_gps_state.map);
if (g_gps_state.pan_v_indicator != NULL || g_gps_state.map == NULL) {
if (g_gps_state.pan_v_indicator != NULL || g_gps_state.map == NULL)
{
GPS_LOG("[GPS] show_pan_v_indicator: early return (already shown or map not ready)\n");
return;
}
@@ -214,12 +232,13 @@ void show_pan_v_indicator()
set_control_id(g_gps_state.pan_v_indicator, ControlId::PanVIndicator);
extern lv_group_t* app_g;
if (app_g != NULL) {
if (app_g != NULL)
{
lv_group_add_obj(app_g, g_gps_state.pan_v_indicator);
}
extern void pan_indicator_event_cb(lv_event_t* e);
extern void on_ui_event(lv_event_t* e);
extern void pan_indicator_event_cb(lv_event_t * e);
extern void on_ui_event(lv_event_t * e);
lv_obj_add_event_cb(g_gps_state.pan_v_indicator, pan_indicator_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(g_gps_state.pan_v_indicator, on_ui_event, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(g_gps_state.pan_v_indicator, on_ui_event, LV_EVENT_PRESSED, NULL);
@@ -234,12 +253,14 @@ void show_pan_v_indicator()
void hide_pan_v_indicator()
{
if (g_gps_state.pan_v_indicator == NULL) {
if (g_gps_state.pan_v_indicator == NULL)
{
return;
}
extern lv_group_t* app_g;
if (app_g != NULL) {
if (app_g != NULL)
{
lv_group_remove_obj(g_gps_state.pan_v_indicator);
}
@@ -253,7 +274,8 @@ void hide_pan_v_indicator()
static void build_zoom_popup_ui(lv_obj_t* win)
{
if (!is_alive() || !win) {
if (!is_alive() || !win)
{
return;
}
@@ -287,12 +309,14 @@ static void build_zoom_popup_ui(lv_obj_t* win)
void show_zoom_popup()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
extern lv_group_t* app_g;
if (!modal_open(g_gps_state.zoom_modal, lv_screen_active(), app_g)) {
if (!modal_open(g_gps_state.zoom_modal, lv_screen_active(), app_g))
{
return;
}
@@ -300,7 +324,8 @@ void show_zoom_popup()
build_zoom_popup_ui(g_gps_state.zoom_modal.win);
if (g_gps_state.popup_label != NULL && g_gps_state.zoom_modal.group != NULL) {
if (g_gps_state.popup_label != NULL && g_gps_state.zoom_modal.group != NULL)
{
lv_group_remove_all_objs(g_gps_state.zoom_modal.group);
lv_group_add_obj(g_gps_state.zoom_modal.group, g_gps_state.popup_label);
set_default_group(g_gps_state.zoom_modal.group);
@@ -310,13 +335,15 @@ void show_zoom_popup()
lv_obj_invalidate(g_gps_state.popup_label);
}
if (g_gps_state.popup_label != NULL) {
if (g_gps_state.popup_label != NULL)
{
set_control_id(g_gps_state.popup_label, ControlId::ZoomValueLabel);
lv_obj_add_event_cb(g_gps_state.popup_label, on_ui_event, LV_EVENT_ROTARY, NULL);
lv_obj_add_event_cb(g_gps_state.popup_label, on_ui_event, LV_EVENT_KEY, NULL);
}
if (!g_gps_state.zoom_win_cb_bound) {
if (!g_gps_state.zoom_win_cb_bound)
{
set_control_id(g_gps_state.zoom_modal.win, ControlId::ZoomWin);
lv_obj_add_event_cb(g_gps_state.zoom_modal.win, on_ui_event, LV_EVENT_ROTARY, NULL);
lv_obj_add_event_cb(g_gps_state.zoom_modal.win, on_ui_event, LV_EVENT_KEY, NULL);
@@ -327,7 +354,8 @@ void show_zoom_popup()
void hide_zoom_popup()
{
if (!g_gps_state.zoom_modal.is_open()) {
if (!g_gps_state.zoom_modal.is_open())
{
return;
}
@@ -335,12 +363,14 @@ void hide_zoom_popup()
g_gps_state.popup_label = nullptr;
extern lv_group_t* app_g;
if (app_g != NULL) {
if (app_g != NULL)
{
lv_group_set_editing(app_g, false);
set_default_group(app_g);
bind_encoder_to_group(app_g);
if (g_gps_state.zoom != NULL) {
if (g_gps_state.zoom != NULL)
{
lv_group_focus_obj(g_gps_state.zoom);
}
}
@@ -352,31 +382,37 @@ void hide_zoom_popup()
void fix_ui_elements_position()
{
if (!is_alive() || g_gps_state.map == NULL) {
if (!is_alive() || g_gps_state.map == NULL)
{
return;
}
if (g_gps_state.panel != NULL) {
if (g_gps_state.panel != NULL)
{
lv_obj_align(g_gps_state.panel, LV_ALIGN_TOP_RIGHT, 0, 3);
lv_obj_move_foreground(g_gps_state.panel);
}
if (g_gps_state.member_panel != NULL) {
if (g_gps_state.member_panel != NULL)
{
lv_obj_align(g_gps_state.member_panel, LV_ALIGN_TOP_LEFT, 0, 3);
lv_obj_move_foreground(g_gps_state.member_panel);
}
if (g_gps_state.resolution_label != NULL) {
if (g_gps_state.resolution_label != NULL)
{
lv_obj_align(g_gps_state.resolution_label, LV_ALIGN_BOTTOM_LEFT, 10, -10);
lv_obj_move_foreground(g_gps_state.resolution_label);
}
if (g_gps_state.pan_h_indicator != NULL) {
if (g_gps_state.pan_h_indicator != NULL)
{
lv_obj_align(g_gps_state.pan_h_indicator, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_move_foreground(g_gps_state.pan_h_indicator);
}
if (g_gps_state.pan_v_indicator != NULL) {
if (g_gps_state.pan_v_indicator != NULL)
{
lv_obj_align(g_gps_state.pan_v_indicator, LV_ALIGN_LEFT_MID, 20, 0);
lv_obj_move_foreground(g_gps_state.pan_v_indicator);
}
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -6,10 +6,11 @@
// Encoder rotation keycodes (from LVGL encoder driver)
// These are the actual keycodes sent when rotary encoder rotates
#define ENCODER_KEY_ROTATE_DOWN 20 // 向下滚(顺时针)
#define ENCODER_KEY_ROTATE_UP 19 // 向上滚(逆时针)
#define ENCODER_KEY_ROTATE_DOWN 20 // 向下滚(顺时针)
#define ENCODER_KEY_ROTATE_UP 19 // 向上滚(逆时针)
enum class ControlId : uint8_t {
enum class ControlId : uint8_t
{
BackBtn,
ZoomBtn,
PosBtn,
@@ -24,13 +25,14 @@ enum class ControlId : uint8_t {
Page
};
struct ControlTag {
struct ControlTag
{
ControlId id;
};
ControlId ctrl_id(lv_obj_t* obj);
void set_control_id(lv_obj_t* obj, ControlId id);
void reset_control_tags(); // 重置 control tag 池(在页面进入时调用)
void reset_control_tags(); // 重置 control tag 池(在页面进入时调用)
void on_ui_event(lv_event_t* e);
void pan_indicator_event_cb(lv_event_t* e);
+4 -2
View File
@@ -1,6 +1,7 @@
#include "gps_page_layout.h"
namespace gps::ui::layout {
namespace gps::ui::layout
{
/**
* Wireframe (structure only; styles are applied elsewhere)
@@ -88,7 +89,8 @@ void create(lv_obj_t* parent, const Spec& spec, Widgets& w)
lv_obj_set_scrollbar_mode(w.member_panel, LV_SCROLLBAR_MODE_OFF);
lv_obj_align(w.member_panel, LV_ALIGN_TOP_LEFT, spec.member_panel_left_offset, spec.member_panel_top_offset);
auto create_control = [&](lv_obj_t*& btn, lv_obj_t*& label, const char* text) {
auto create_control = [&](lv_obj_t*& btn, lv_obj_t*& label, const char* text)
{
btn = lv_btn_create(w.panel);
lv_obj_set_size(btn, spec.control_btn_w, spec.control_btn_h);
label = lv_label_create(btn);
+7 -4
View File
@@ -1,11 +1,13 @@
#pragma once
#include "lvgl.h"
#include "../../widgets/top_bar.h"
#include "lvgl.h"
namespace gps::ui::layout {
namespace gps::ui::layout
{
struct Spec {
struct Spec
{
int panel_width = 85;
int panel_top_offset = 3;
int panel_row_gap = 3;
@@ -23,7 +25,8 @@ struct Spec {
int control_btn_h = 32;
};
struct Widgets {
struct Widgets
{
lv_obj_t* root = nullptr;
lv_obj_t* header = nullptr;
lv_obj_t* content = nullptr;
+31 -16
View File
@@ -1,23 +1,27 @@
#include "gps_page_lifetime.h"
#include "../../widgets/map/map_tiles.h"
#include "gps_modal.h"
#include "gps_page_map.h"
#include "gps_state.h"
#include "gps_tracker_overlay.h"
#include "../../widgets/map/map_tiles.h"
#include <algorithm>
namespace gps::ui::lifetime {
namespace gps::ui::lifetime
{
namespace {
namespace
{
void clear_modal_groups()
{
if (g_gps_state.zoom_modal.group) {
if (g_gps_state.zoom_modal.group)
{
lv_group_del(g_gps_state.zoom_modal.group);
g_gps_state.zoom_modal.group = nullptr;
}
if (g_gps_state.tracker_modal.group) {
if (g_gps_state.tracker_modal.group)
{
lv_group_del(g_gps_state.tracker_modal.group);
g_gps_state.tracker_modal.group = nullptr;
}
@@ -26,12 +30,15 @@ void clear_modal_groups()
void detach_group_objs()
{
lv_group_t* group = g_gps_state.app_group;
if (!group) {
if (!group)
{
return;
}
auto remove_if = [group](lv_obj_t* obj) {
if (obj) {
auto remove_if = [group](lv_obj_t* obj)
{
if (obj)
{
lv_group_remove_obj(obj);
}
};
@@ -54,7 +61,8 @@ void on_root_deleted(lv_event_t* e)
{
(void)e;
if (!g_gps_state.alive) {
if (!g_gps_state.alive)
{
return;
}
@@ -74,7 +82,8 @@ void on_root_deleted(lv_event_t* e)
g_gps_state.popup_label = nullptr;
// Close modals that are not children of the root container.
if (g_gps_state.zoom_modal.is_open()) {
if (g_gps_state.zoom_modal.is_open())
{
modal_close(g_gps_state.zoom_modal);
}
gps_tracker_cleanup();
@@ -114,7 +123,8 @@ bool is_alive()
void bind_root_delete_hook()
{
if (!g_gps_state.root || g_gps_state.delete_hook_bound) {
if (!g_gps_state.root || g_gps_state.delete_hook_bound)
{
return;
}
lv_obj_add_event_cb(g_gps_state.root, on_root_deleted, LV_EVENT_DELETE, nullptr);
@@ -123,11 +133,13 @@ void bind_root_delete_hook()
lv_timer_t* add_timer(lv_timer_cb_t cb, uint32_t period_ms, void* user_data)
{
if (!is_alive()) {
if (!is_alive())
{
return nullptr;
}
lv_timer_t* timer = lv_timer_create(cb, period_ms, user_data);
if (timer) {
if (timer)
{
g_gps_state.timers.push_back(timer);
}
return timer;
@@ -135,8 +147,10 @@ lv_timer_t* add_timer(lv_timer_cb_t cb, uint32_t period_ms, void* user_data)
void clear_timers()
{
for (lv_timer_t* timer : g_gps_state.timers) {
if (timer) {
for (lv_timer_t* timer : g_gps_state.timers)
{
if (timer)
{
lv_timer_del(timer);
}
}
@@ -145,7 +159,8 @@ void clear_timers()
void remove_timer(lv_timer_t* timer)
{
if (!timer) {
if (!timer)
{
return;
}
+2 -1
View File
@@ -2,7 +2,8 @@
#include "lvgl.h"
namespace gps::ui::lifetime {
namespace gps::ui::lifetime
{
void mark_alive(lv_obj_t* root, lv_group_t* app_group);
bool is_alive();
+178 -118
View File
@@ -1,24 +1,25 @@
#include "gps_page_map.h"
#include "gps_page_lifetime.h"
#include "gps_state.h"
#include "gps_page_components.h"
#include "gps_page_styles.h"
#include "../../widgets/map/map_tiles.h"
#include "gps_constants.h"
#include "../../gps/gps_hw_status.h"
#include "../team/team_ui_store.h"
#include "../team/team_state.h"
#include "../../../app/app_context.h"
#include "../../gps/gps_hw_status.h"
#include "../../ui_common.h"
#include "../../widgets/map/map_tiles.h"
#include "../team/team_state.h"
#include "../team/team_ui_store.h"
#include "gps_constants.h"
#include "gps_page_components.h"
#include "gps_page_lifetime.h"
#include "gps_page_styles.h"
#include "gps_state.h"
#include "lvgl.h"
#include <Arduino.h>
#include <algorithm>
#include <cstdint>
#include <cmath>
#include <cstdint>
#include <cstdio>
// GPS marker icon (room-24px), defined in C image file
extern "C" {
extern "C"
{
extern const lv_image_dsc_t room_24px;
}
@@ -39,7 +40,8 @@ using gps::ui::lifetime::is_alive;
using GPSData = gps::GpsState;
// Cached state for title/status updates (moved from presenter)
static struct {
static struct
{
bool cached_has_fix;
int cached_zoom;
bool cached_sd_ready;
@@ -80,7 +82,8 @@ constexpr uint32_t kInvalidMemberId = 0xFFFFFFFFu;
uint32_t hash_member_list(const std::vector<team::ui::TeamMemberUi>& members)
{
uint32_t h = 2166136261u;
auto mix = [&](uint32_t v) {
auto mix = [&](uint32_t v)
{
h ^= v;
h *= 16777619u;
};
@@ -138,7 +141,8 @@ bool load_team_data(team::TeamId& out_id, std::vector<team::ui::TeamMemberUi>& o
bool member_exists(const std::vector<team::ui::TeamMemberUi>& members, uint32_t member_id)
{
return std::find_if(members.begin(), members.end(),
[&](const team::ui::TeamMemberUi& m) {
[&](const team::ui::TeamMemberUi& m)
{
return m.node_id == member_id;
}) != members.end();
}
@@ -146,7 +150,8 @@ bool member_exists(const std::vector<team::ui::TeamMemberUi>& members, uint32_t
const team::ui::TeamMemberUi* find_member(const std::vector<team::ui::TeamMemberUi>& members, uint32_t member_id)
{
auto it = std::find_if(members.begin(), members.end(),
[&](const team::ui::TeamMemberUi& m) {
[&](const team::ui::TeamMemberUi& m)
{
return m.node_id == member_id;
});
if (it == members.end())
@@ -159,7 +164,8 @@ const team::ui::TeamMemberUi* find_member(const std::vector<team::ui::TeamMember
uint32_t resolve_member_color(const std::vector<team::ui::TeamMemberUi>& members, uint32_t member_id)
{
auto it = std::find_if(members.begin(), members.end(),
[&](const team::ui::TeamMemberUi& m) {
[&](const team::ui::TeamMemberUi& m)
{
return m.node_id == member_id;
});
if (it == members.end())
@@ -180,7 +186,8 @@ lv_color_t marker_text_color(uint32_t color)
uint8_t b = static_cast<uint8_t>(color & 0xFF);
uint32_t lum = (static_cast<uint32_t>(r) * 299 +
static_cast<uint32_t>(g) * 587 +
static_cast<uint32_t>(b) * 114) / 1000;
static_cast<uint32_t>(b) * 114) /
1000;
return (lum > 160) ? lv_color_black() : lv_color_white();
}
@@ -281,60 +288,74 @@ int find_team_marker_index(uint32_t member_id)
void update_resolution_display()
{
if (!is_alive() || g_gps_state.resolution_label == NULL) {
if (!is_alive() || g_gps_state.resolution_label == NULL)
{
return;
}
double lat = g_gps_state.has_fix ? g_gps_state.lat : gps_ui::kDefaultLat;
double resolution_m = gps::calculate_map_resolution(g_gps_state.zoom_level, lat);
char resolution_text[32];
if (resolution_m < 1000.0) {
if (resolution_m < 1.0) {
if (resolution_m < 1000.0)
{
if (resolution_m < 1.0)
{
snprintf(resolution_text, sizeof(resolution_text), "%.2f m", resolution_m);
} else {
}
else
{
snprintf(resolution_text, sizeof(resolution_text), "%.0f m", resolution_m);
}
} else {
}
else
{
double resolution_km = resolution_m / 1000.0;
if (resolution_km < 10.0) {
if (resolution_km < 10.0)
{
snprintf(resolution_text, sizeof(resolution_text), "%.2f km", resolution_km);
} else if (resolution_km < 100.0) {
}
else if (resolution_km < 100.0)
{
snprintf(resolution_text, sizeof(resolution_text), "%.1f km", resolution_km);
} else {
}
else
{
snprintf(resolution_text, sizeof(resolution_text), "%.0f km", resolution_km);
}
}
lv_label_set_text(g_gps_state.resolution_label, resolution_text);
}
void update_title_and_status()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
bool sd_ready = sd_hw_is_ready();
bool gps_ready = gps_hw_is_ready();
GPSData gps_data = gps::gps_get_data();
uint8_t satellites = gps_data.satellites;
bool state_changed = !last_status_state.initialized ||
(last_status_state.cached_has_fix != g_gps_state.has_fix ||
last_status_state.cached_sd_ready != sd_ready ||
last_status_state.cached_gps_ready != gps_ready ||
last_status_state.cached_has_map_data != g_gps_state.has_visible_map_data ||
last_status_state.cached_satellites != satellites);
if (!state_changed) {
last_status_state.cached_sd_ready != sd_ready ||
last_status_state.cached_gps_ready != gps_ready ||
last_status_state.cached_has_map_data != g_gps_state.has_visible_map_data ||
last_status_state.cached_satellites != satellites);
if (!state_changed)
{
GPS_LOG("[GPS] State unchanged, skipping title update (will be handled by 30s timer)\n");
return;
}
GPS_LOG("[GPS] State changed, updating title: has_fix=%d, gps_ready=%d, sd_ready=%d, has_map=%d\n",
g_gps_state.has_fix, gps_ready, sd_ready, g_gps_state.has_visible_map_data);
last_status_state.cached_has_fix = g_gps_state.has_fix;
last_status_state.cached_zoom = g_gps_state.zoom_level;
last_status_state.cached_sd_ready = sd_ready;
@@ -342,29 +363,37 @@ void update_title_and_status()
last_status_state.cached_has_map_data = g_gps_state.has_visible_map_data;
last_status_state.cached_satellites = satellites;
last_status_state.initialized = true;
// Update shared top bar title; layout no longer depends on lv_menu
static char title_buffer[64];
if (g_gps_state.has_fix && gps_ready) {
if (g_gps_state.has_fix && gps_ready)
{
snprintf(title_buffer, sizeof(title_buffer), "GPS - %.4f,%.4f", g_gps_state.lat, g_gps_state.lng);
} else if (!sd_ready) {
}
else if (!sd_ready)
{
snprintf(title_buffer, sizeof(title_buffer), "GPS - No SD Card");
} else if (!g_gps_state.has_visible_map_data) {
}
else if (!g_gps_state.has_visible_map_data)
{
snprintf(title_buffer, sizeof(title_buffer), "GPS - No Map Data");
} else {
}
else
{
snprintf(title_buffer, sizeof(title_buffer), "GPS - no gps data");
}
GPS_LOG("[GPS] Setting page title to: '%s' (page=%p)\n",
GPS_LOG("[GPS] Setting page title to: '%s' (page=%p)\n",
title_buffer, g_gps_state.page);
if (g_gps_state.top_bar.container != nullptr) {
if (g_gps_state.top_bar.container != nullptr)
{
::ui::widgets::top_bar_set_title(g_gps_state.top_bar, title_buffer);
// Also update shared top bar battery from board state
ui_update_top_bar_battery(g_gps_state.top_bar);
}
update_zoom_btn();
}
@@ -380,49 +409,55 @@ void reset_title_status_cache()
void update_map_anchor()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
::update_map_anchor(g_gps_state.tile_ctx,
g_gps_state.lat, g_gps_state.lng,
g_gps_state.zoom_level,
g_gps_state.pan_x, g_gps_state.pan_y,
g_gps_state.has_fix);
::update_map_anchor(g_gps_state.tile_ctx,
g_gps_state.lat, g_gps_state.lng,
g_gps_state.zoom_level,
g_gps_state.pan_x, g_gps_state.pan_y,
g_gps_state.has_fix);
}
void update_map_tiles(bool lightweight)
{
if (!is_alive() || g_gps_state.map == NULL) {
if (!is_alive() || g_gps_state.map == NULL)
{
return;
}
::calculate_required_tiles(g_gps_state.tile_ctx,
g_gps_state.lat, g_gps_state.lng,
g_gps_state.zoom_level,
g_gps_state.pan_x, g_gps_state.pan_y,
::calculate_required_tiles(g_gps_state.tile_ctx,
g_gps_state.lat, g_gps_state.lng,
g_gps_state.zoom_level,
g_gps_state.pan_x, g_gps_state.pan_y,
g_gps_state.has_fix);
if (!lightweight) {
if (!lightweight)
{
fix_ui_elements_position();
}
if (!lightweight) {
if (!lightweight)
{
bool zoom_changed = (g_gps_state.last_resolution_zoom != g_gps_state.zoom_level);
bool lat_changed = (fabs(g_gps_state.last_resolution_lat - g_gps_state.lat) > 0.001);
if (zoom_changed || lat_changed) {
if (zoom_changed || lat_changed)
{
update_resolution_display();
g_gps_state.last_resolution_zoom = g_gps_state.zoom_level;
g_gps_state.last_resolution_lat = g_gps_state.lat;
}
// Update GPS marker position after map tiles are updated
// This ensures marker is rendered on top and moves with the map
if (g_gps_state.gps_marker != NULL) {
if (g_gps_state.gps_marker != NULL)
{
update_gps_marker_position();
}
update_team_marker_positions();
}
lv_obj_invalidate(g_gps_state.map);
}
@@ -432,24 +467,29 @@ void update_map_tiles(bool lightweight)
*/
void update_gps_marker_position()
{
if (!is_alive() || g_gps_state.gps_marker == NULL || g_gps_state.map == NULL) {
if (!is_alive() || g_gps_state.gps_marker == NULL || g_gps_state.map == NULL)
{
return;
}
if (!g_gps_state.has_fix || !g_gps_state.tile_ctx.anchor || !g_gps_state.tile_ctx.anchor->valid) {
if (!g_gps_state.has_fix || !g_gps_state.tile_ctx.anchor || !g_gps_state.tile_ctx.anchor->valid)
{
lv_obj_add_flag(g_gps_state.gps_marker, LV_OBJ_FLAG_HIDDEN);
return;
}
// Calculate screen position for GPS coordinates
int screen_x, screen_y;
if (gps_screen_pos(g_gps_state.tile_ctx, g_gps_state.lat, g_gps_state.lng, screen_x, screen_y)) {
if (gps_screen_pos(g_gps_state.tile_ctx, g_gps_state.lat, g_gps_state.lng, screen_x, screen_y))
{
// Center marker on GPS position (marker is typically 24x24, so offset by half)
const int marker_size = 24;
lv_obj_set_pos(g_gps_state.gps_marker, screen_x - marker_size / 2, screen_y - marker_size / 2);
lv_obj_clear_flag(g_gps_state.gps_marker, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(g_gps_state.gps_marker);
} else {
}
else
{
lv_obj_add_flag(g_gps_state.gps_marker, LV_OBJ_FLAG_HIDDEN);
}
}
@@ -460,26 +500,28 @@ void update_gps_marker_position()
*/
void create_gps_marker()
{
if (!is_alive() || !g_gps_state.has_fix || g_gps_state.map == NULL) {
if (!is_alive() || !g_gps_state.has_fix || g_gps_state.map == NULL)
{
return;
}
// If marker already exists, just update its position
if (g_gps_state.gps_marker != NULL) {
if (g_gps_state.gps_marker != NULL)
{
update_gps_marker_position();
return;
}
// Create marker image with room icon
g_gps_state.gps_marker = lv_image_create(g_gps_state.map);
lv_image_set_src(g_gps_state.gps_marker, &room_24px);
// Set marker size (24x24 pixels)
lv_obj_set_size(g_gps_state.gps_marker, 24, 24);
// Set initial position
update_gps_marker_position();
GPS_LOG("[GPS] GPS marker created at lat=%.6f, lng=%.6f\n", g_gps_state.lat, g_gps_state.lng);
}
@@ -488,10 +530,12 @@ void create_gps_marker()
*/
void hide_gps_marker()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
if (g_gps_state.gps_marker != NULL) {
if (g_gps_state.gps_marker != NULL)
{
lv_obj_add_flag(g_gps_state.gps_marker, LV_OBJ_FLAG_HIDDEN);
}
}
@@ -799,7 +843,8 @@ void refresh_team_markers_from_posring()
return;
}
auto sample_it = std::find_if(samples.begin(), samples.end(),
[&](const team::ui::TeamPosSample& s) {
[&](const team::ui::TeamPosSample& s)
{
return s.member_id == g_gps_state.selected_member_id;
});
if (sample_it == samples.end())
@@ -808,7 +853,7 @@ void refresh_team_markers_from_posring()
return;
}
for (auto it = g_gps_state.team_markers.begin(); it != g_gps_state.team_markers.end(); )
for (auto it = g_gps_state.team_markers.begin(); it != g_gps_state.team_markers.end();)
{
if (it->member_id != g_gps_state.selected_member_id)
{
@@ -867,25 +912,26 @@ void refresh_team_markers_from_posring()
void tick_loader()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
static bool prev_has_visible_map_data = false;
if (!g_gps_state.initial_tiles_loaded && g_gps_state.map != NULL) {
if (!g_gps_state.initial_tiles_loaded && g_gps_state.map != NULL)
{
g_gps_state.initial_tiles_loaded = true;
update_map_tiles(false);
g_gps_state.initial_load_ms = millis();
}
::tile_loader_step(g_gps_state.tile_ctx);
}
void tick_gps_update(bool allow_map_refresh)
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
GPSData gps_data = gps::gps_get_data();
@@ -904,15 +950,16 @@ void tick_gps_update(bool allow_map_refresh)
static uint32_t last_refresh_ms = 0;
const uint32_t TITLE_UPDATE_INTERVAL_MS = 30000;
const double MOVE_THRESHOLD_M = 15.0; // 忽略小抖动(可调 10~30m)
const uint32_t REFRESH_INTERVAL_MS = 2000; // 即使移动很慢也定期刷新
const double MOVE_THRESHOLD_M = 15.0; // 忽略小抖动(可调 10~30m)
const uint32_t REFRESH_INTERVAL_MS = 2000; // 即使移动很慢也定期刷新
bool gps_ready = gps_hw_is_ready();
bool sd_ready = sd_hw_is_ready();
uint32_t now_ms = millis();
bool gps_state_changed = false;
if (gps_data.valid) {
if (gps_data.valid)
{
double new_lat = gps_data.lat;
double new_lng = gps_data.lng;
@@ -921,14 +968,16 @@ void tick_gps_update(bool allow_map_refresh)
// 始终更新坐标,让状态显示保持最新
if (just_got_fix ||
fabs(new_lat - g_gps_state.lat) > 0.0001 ||
fabs(new_lng - g_gps_state.lng) > 0.0001) {
fabs(new_lng - g_gps_state.lng) > 0.0001)
{
g_gps_state.lat = new_lat;
g_gps_state.lng = new_lng;
g_gps_state.has_fix = true;
gps_state_changed = true;
}
if (just_got_fix && g_gps_state.zoom_level == 0) {
if (just_got_fix && g_gps_state.zoom_level == 0)
{
g_gps_state.zoom_level = gps_ui::kDefaultZoom;
g_gps_state.last_resolution_zoom = g_gps_state.zoom_level;
g_gps_state.last_resolution_lat = g_gps_state.lat;
@@ -936,11 +985,15 @@ void tick_gps_update(bool allow_map_refresh)
}
// 只对“地图刷新”加抖动过滤
if (allow_map_refresh) {
if (allow_map_refresh)
{
bool moved_enough = false;
if (!last_refresh_valid) {
if (!last_refresh_valid)
{
moved_enough = true;
} else {
}
else
{
double dist_m = approx_distance_m(
last_refresh_lat, last_refresh_lng, new_lat, new_lng);
moved_enough = dist_m >= MOVE_THRESHOLD_M;
@@ -948,7 +1001,8 @@ void tick_gps_update(bool allow_map_refresh)
bool time_due = (now_ms - last_refresh_ms) >= REFRESH_INTERVAL_MS;
if (just_got_fix || moved_enough || time_due) {
if (just_got_fix || moved_enough || time_due)
{
g_gps_state.pan_x = 0;
g_gps_state.pan_y = 0;
@@ -961,8 +1015,11 @@ void tick_gps_update(bool allow_map_refresh)
last_refresh_valid = true;
}
}
} else {
if (g_gps_state.has_fix) {
}
else
{
if (g_gps_state.has_fix)
{
g_gps_state.has_fix = false;
g_gps_state.zoom_level = 0;
@@ -976,38 +1033,41 @@ void tick_gps_update(bool allow_map_refresh)
last_refresh_valid = false;
if (allow_map_refresh) {
if (allow_map_refresh)
{
g_gps_state.pan_x = 0;
g_gps_state.pan_y = 0;
update_map_tiles(false);
}
}
}
bool state_changed = (prev_has_fix != g_gps_state.has_fix ||
prev_has_visible_map_data != g_gps_state.has_visible_map_data ||
prev_gps_ready != gps_ready ||
prev_sd_ready != sd_ready ||
prev_satellites != gps_data.satellites ||
gps_state_changed);
prev_has_visible_map_data != g_gps_state.has_visible_map_data ||
prev_gps_ready != gps_ready ||
prev_sd_ready != sd_ready ||
prev_satellites != gps_data.satellites ||
gps_state_changed);
bool time_elapsed = (now_ms - last_title_update_ms) >= TITLE_UPDATE_INTERVAL_MS;
if (state_changed || time_elapsed) {
GPS_LOG("[GPS] tick_gps_update: Updating title (state_changed=%d, time_elapsed=%d, has_fix=%d, has_map=%d)\n",
state_changed, time_elapsed, g_gps_state.has_fix, g_gps_state.has_visible_map_data);
if (state_changed || time_elapsed)
{
GPS_LOG("[GPS] tick_gps_update: Updating title (state_changed=%d, time_elapsed=%d, has_fix=%d, has_map=%d)\n",
state_changed, time_elapsed, g_gps_state.has_fix, g_gps_state.has_visible_map_data);
reset_title_status_cache();
update_title_and_status();
last_title_update_ms = now_ms;
prev_has_fix = g_gps_state.has_fix;
prev_has_visible_map_data = g_gps_state.has_visible_map_data;
prev_gps_ready = gps_ready;
prev_sd_ready = sd_ready;
prev_satellites = gps_data.satellites;
}
// Update GPS marker position if marker exists and GPS data changed
if (gps_state_changed && g_gps_state.gps_marker != NULL) {
if (gps_state_changed && g_gps_state.gps_marker != NULL)
{
update_gps_marker_position();
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ void refresh_member_panel(bool force = false);
// UI status and title updates (moved from presenter)
void update_title_and_status();
void update_resolution_display();
void reset_title_status_cache(); // Reset cached state to force next update
void update_zoom_btn(); // Placeholder for future zoom button updates
void reset_title_status_cache(); // Reset cached state to force next update
void update_zoom_btn(); // Placeholder for future zoom button updates
#endif // GPS_PAGE_MAP_H
+6 -3
View File
@@ -1,8 +1,10 @@
#include "gps_page_styles.h"
namespace gps::ui::styles {
namespace gps::ui::styles
{
namespace {
namespace
{
bool s_inited = false;
@@ -59,7 +61,8 @@ constexpr uint32_t kZoomValueFocusedBg = 0xF0F0F0;
void init_once()
{
if (s_inited) {
if (s_inited)
{
return;
}
+3 -2
View File
@@ -1,9 +1,10 @@
#pragma once
#include "lvgl.h"
#include "gps_page_layout.h"
#include "lvgl.h"
namespace gps::ui::styles {
namespace gps::ui::styles
{
void init_once();
void apply_all(const layout::Widgets& w, const layout::Spec& spec);
+52 -51
View File
@@ -6,50 +6,51 @@
#ifndef GPS_STATE_H
#define GPS_STATE_H
#include "lvgl.h"
#include "../../widgets/map/map_tiles.h"
#include "gps_modal.h"
#include "gps_constants.h"
#include "../../widgets/top_bar.h"
#include <vector>
#include "gps_constants.h"
#include "gps_modal.h"
#include "lvgl.h"
#include <cstdint>
#include <string>
#include <vector>
/**
* GPS Page State - consolidate all static state variables
* Makes cleanup/exit logic simpler and prevents state leaks
* Note: No macro aliases - use g_gps_state.member directly
*/
struct GPSPageState {
struct TrackOverlayPoint {
struct GPSPageState
{
struct TrackOverlayPoint
{
double lat = 0.0;
double lng = 0.0;
};
// UI refs
lv_obj_t *root = nullptr;
lv_obj_t *header = nullptr;
lv_obj_t *menu = nullptr;
lv_obj_t *page = nullptr;
lv_obj_t *map = nullptr;
lv_obj_t *resolution_label = nullptr; // Resolution display label (bottom-left)
lv_obj_t *panel = nullptr;
lv_obj_t *member_panel = nullptr;
lv_obj_t *zoom = nullptr;
lv_obj_t *pos = nullptr;
lv_obj_t *pan_h = nullptr; // Horizontal pan button
lv_obj_t *pan_v = nullptr; // Vertical pan button
lv_obj_t *tracker_btn = nullptr; // Tracker button
lv_obj_t *pan_h_indicator = nullptr; // Horizontal pan indicator (line with arrows at bottom)
lv_obj_t *pan_v_indicator = nullptr; // Vertical pan indicator (line with arrows on right)
lv_obj_t *popup_label = nullptr; // zoom_popup_label
lv_obj_t *loading_msgbox = nullptr;
lv_obj_t *toast_msgbox = nullptr; // Toast notification message box
lv_timer_t* toast_timer = nullptr; // Timer to auto-hide toast
lv_obj_t *gps_marker = nullptr; // GPS position marker (rendered on map)
ui::widgets::TopBar top_bar; // Shared header
lv_obj_t* root = nullptr;
lv_obj_t* header = nullptr;
lv_obj_t* menu = nullptr;
lv_obj_t* page = nullptr;
lv_obj_t* map = nullptr;
lv_obj_t* resolution_label = nullptr; // Resolution display label (bottom-left)
lv_obj_t* panel = nullptr;
lv_obj_t* member_panel = nullptr;
lv_obj_t* zoom = nullptr;
lv_obj_t* pos = nullptr;
lv_obj_t* pan_h = nullptr; // Horizontal pan button
lv_obj_t* pan_v = nullptr; // Vertical pan button
lv_obj_t* tracker_btn = nullptr; // Tracker button
lv_obj_t* pan_h_indicator = nullptr; // Horizontal pan indicator (line with arrows at bottom)
lv_obj_t* pan_v_indicator = nullptr; // Vertical pan indicator (line with arrows on right)
lv_obj_t* popup_label = nullptr; // zoom_popup_label
lv_obj_t* loading_msgbox = nullptr;
lv_obj_t* toast_msgbox = nullptr; // Toast notification message box
lv_timer_t* toast_timer = nullptr; // Timer to auto-hide toast
lv_obj_t* gps_marker = nullptr; // GPS position marker (rendered on map)
ui::widgets::TopBar top_bar; // Shared header
// GPS/map
int zoom_level = gps_ui::kDefaultZoom;
double lat = 0.0;
@@ -57,35 +58,35 @@ struct GPSPageState {
bool has_fix = false;
int pan_x = 0;
int pan_y = 0;
// tile/cache (actual storage, not pointers)
MapAnchor anchor = {0};
std::vector<MapTile> tiles;
TileContext tile_ctx; // Context for tile operations
TileContext tile_ctx; // Context for tile operations
uint32_t initial_load_ms = 0;
bool initial_tiles_loaded = false;
// popup
Modal zoom_modal;
Modal tracker_modal;
int popup_zoom = gps_ui::kDefaultZoom;
bool zoom_win_cb_bound = false;
// misc
lv_timer_t* timer = nullptr; // Main timer for tile loading and GPS updates
lv_timer_t* loader_timer = nullptr; // Tile loader timer (higher frequency)
lv_timer_t* timer = nullptr; // Main timer for tile loading and GPS updates
lv_timer_t* loader_timer = nullptr; // Tile loader timer (higher frequency)
lv_timer_t* title_timer = nullptr; // Separate timer for title updates (30s)
std::vector<lv_timer_t*> timers; // Lifetime-managed timers for this screen
std::vector<lv_timer_t*> timers; // Lifetime-managed timers for this screen
lv_indev_t* encoder = nullptr;
lv_group_t* app_group = nullptr; // App-level focus group captured at enter
lv_group_t* app_group = nullptr; // App-level focus group captured at enter
// flags
bool alive = false; // Hard lifetime guard: false after root delete hook runs
bool delete_hook_bound = false; // Ensure root delete hook is only bound once
bool exiting = false; // Prevent re-entrant exit while async exit is pending
bool has_map_data = false; // Global: any tile ever loaded
bool has_visible_map_data = false; // Viewport: current visible tiles have PNG
bool alive = false; // Hard lifetime guard: false after root delete hook runs
bool delete_hook_bound = false; // Ensure root delete hook is only bound once
bool exiting = false; // Prevent re-entrant exit while async exit is pending
bool has_map_data = false; // Global: any tile ever loaded
bool has_visible_map_data = false; // Viewport: current visible tiles have PNG
// Tracker overlay
bool tracker_overlay_active = false;
@@ -112,18 +113,18 @@ struct GPSPageState {
};
std::vector<TeamMarker> team_markers;
uint32_t team_marker_last_ms = 0;
// pan button editing state (for toggle behavior) - DEPRECATED, use edit_mode instead
bool pan_h_editing = false; // Horizontal pan button in editing mode (rotary scrolls map)
bool pan_v_editing = false; // Vertical pan button in editing mode (rotary scrolls map)
bool pan_h_editing = false; // Horizontal pan button in editing mode (rotary scrolls map)
bool pan_v_editing = false; // Vertical pan button in editing mode (rotary scrolls map)
// Edit mode state machine
uint8_t edit_mode = 0; // 0=None, 1=PanH, 2=PanV, 3=ZoomPopup
uint8_t edit_mode = 0; // 0=None, 1=PanH, 2=PanV, 3=ZoomPopup
// refresh optimization
bool pending_refresh = false; // Flag to indicate map needs refresh (for batched updates)
double last_resolution_lat = 0.0; // Last latitude used for resolution calculation
int last_resolution_zoom = -1; // Last zoom level used for resolution calculation
bool pending_refresh = false; // Flag to indicate map needs refresh (for batched updates)
double last_resolution_lat = 0.0; // Last latitude used for resolution calculation
int last_resolution_zoom = -1; // Last zoom level used for resolution calculation
};
// Global state instance (defined in gps_page.cpp)
+24 -15
View File
@@ -1,18 +1,18 @@
#include "gps_tracker_overlay.h"
#include "gps_modal.h"
#include "gps_page_map.h"
#include "gps_page_lifetime.h"
#include "gps_page_styles.h"
#include "gps_state.h"
#include "../../../gps/gps_service_api.h"
#include "../../ui_common.h"
#include "../../widgets/map/map_tiles.h"
#include "gps_modal.h"
#include "gps_page_lifetime.h"
#include "gps_page_map.h"
#include "gps_page_styles.h"
#include "gps_state.h"
#include <SD.h>
#include <esp_system.h>
#include <algorithm>
#include <cmath>
#include <esp_system.h>
#include <vector>
// Implemented in gps_page_components.cpp
@@ -146,7 +146,8 @@ bool load_gpx_points(const char* path, int zoom, std::vector<GPSPageState::Track
void compute_screen_points()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
auto& s = g_gps_state;
@@ -212,7 +213,8 @@ void compute_screen_points()
void apply_tracker_view_defaults()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
auto& s = g_gps_state;
@@ -241,7 +243,8 @@ void apply_tracker_view_defaults()
void close_tracker_modal()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
if (!modal_is_open(g_gps_state.tracker_modal))
@@ -254,7 +257,8 @@ void close_tracker_modal()
void on_track_selected(lv_event_t* e)
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
const uintptr_t idx = (uintptr_t)lv_event_get_user_data(e);
@@ -285,7 +289,8 @@ void on_track_selected(lv_event_t* e)
void build_tracker_modal()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
auto& s = g_gps_state;
@@ -346,7 +351,8 @@ void build_tracker_modal()
void gps_tracker_open_modal()
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
if (SD.cardType() == CARD_NONE)
@@ -365,7 +371,8 @@ void gps_tracker_open_modal()
void gps_tracker_draw_event(lv_event_t* e)
{
if (!is_alive()) {
if (!is_alive())
{
return;
}
if (!e || lv_event_get_code(e) != LV_EVENT_DRAW_POST)
@@ -425,8 +432,10 @@ void gps_tracker_draw_event(lv_event_t* e)
void gps_tracker_cleanup()
{
if (!is_alive()) {
if (modal_is_open(g_gps_state.tracker_modal)) {
if (!is_alive())
{
if (modal_is_open(g_gps_state.tracker_modal))
{
modal_close(g_gps_state.tracker_modal);
}
g_gps_state.tracker_overlay_active = false;
-1
View File
@@ -6,4 +6,3 @@
void gps_tracker_open_modal();
void gps_tracker_draw_event(lv_event_t* e);
void gps_tracker_cleanup();
@@ -7,7 +7,8 @@
#include "lvgl.h"
namespace settings::ui::components {
namespace settings::ui::components
{
void create(lv_obj_t* parent);
void destroy();
+59 -28
View File
@@ -3,15 +3,18 @@
* @brief Settings input handling implementation
*/
#include <Arduino.h>
#include "settings_page_input.h"
#include "settings_state.h"
#include <Arduino.h>
namespace settings::ui::input {
namespace settings::ui::input
{
namespace {
namespace
{
enum class FocusColumn {
enum class FocusColumn
{
Filter = 0,
List = 1,
};
@@ -23,8 +26,10 @@ static FocusColumn s_col = FocusColumn::Filter;
static lv_indev_t* find_encoder_indev()
{
lv_indev_t* indev = nullptr;
while ((indev = lv_indev_get_next(indev)) != nullptr) {
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER) {
while ((indev = lv_indev_get_next(indev)) != nullptr)
{
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_ENCODER)
{
return indev;
}
}
@@ -54,19 +59,25 @@ static void bind_filter_column()
if (!s_group) return;
group_clear_all(s_group);
if (g_state.top_bar.back_btn) {
if (g_state.top_bar.back_btn)
{
lv_group_add_obj(s_group, g_state.top_bar.back_btn);
}
for (size_t i = 0; i < g_state.filter_count; ++i) {
if (g_state.filter_buttons[i]) {
for (size_t i = 0; i < g_state.filter_count; ++i)
{
if (g_state.filter_buttons[i])
{
lv_group_add_obj(s_group, g_state.filter_buttons[i]);
}
}
if (g_state.filter_buttons[g_state.current_category]) {
if (g_state.filter_buttons[g_state.current_category])
{
focus_first_valid(g_state.filter_buttons[g_state.current_category]);
} else if (g_state.top_bar.back_btn) {
}
else if (g_state.top_bar.back_btn)
{
focus_first_valid(g_state.top_bar.back_btn);
}
}
@@ -76,29 +87,40 @@ static void bind_list_column()
if (!s_group) return;
group_clear_all(s_group);
for (size_t i = 0; i < g_state.item_count; ++i) {
if (g_state.item_widgets[i].btn) {
for (size_t i = 0; i < g_state.item_count; ++i)
{
if (g_state.item_widgets[i].btn)
{
lv_group_add_obj(s_group, g_state.item_widgets[i].btn);
}
}
if (g_state.list_back_btn) {
if (g_state.list_back_btn)
{
lv_group_add_obj(s_group, g_state.list_back_btn);
}
if (g_state.item_count > 0 && g_state.item_widgets[0].btn) {
if (g_state.item_count > 0 && g_state.item_widgets[0].btn)
{
focus_first_valid(g_state.item_widgets[0].btn);
} else if (g_state.list_back_btn) {
}
else if (g_state.list_back_btn)
{
focus_first_valid(g_state.list_back_btn);
} else if (g_state.top_bar.back_btn) {
}
else if (g_state.top_bar.back_btn)
{
focus_first_valid(g_state.top_bar.back_btn);
}
}
static void rebind_by_column()
{
if (s_col == FocusColumn::Filter) {
if (s_col == FocusColumn::Filter)
{
bind_filter_column();
} else {
}
else
{
bind_list_column();
}
}
@@ -108,22 +130,26 @@ static void root_key_event_cb(lv_event_t* e)
if (!is_encoder_active()) return;
uint32_t key = lv_event_get_key(e);
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE) {
if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE)
{
s_col = FocusColumn::Filter;
rebind_by_column();
return;
}
if (key != LV_KEY_ENTER) return;
if (s_col == FocusColumn::Filter) {
if (s_col == FocusColumn::Filter)
{
s_col = FocusColumn::List;
rebind_by_column();
return;
}
if (s_col == FocusColumn::List) {
if (s_col == FocusColumn::List)
{
lv_obj_t* focused = s_group ? lv_group_get_focused(s_group) : nullptr;
if (focused == g_state.list_back_btn) {
if (focused == g_state.list_back_btn)
{
s_col = FocusColumn::Filter;
rebind_by_column();
return;
@@ -135,27 +161,32 @@ static void root_key_event_cb(lv_event_t* e)
void init()
{
if (s_group) {
if (s_group)
{
cleanup();
}
s_group = lv_group_create();
s_encoder = find_encoder_indev();
if (s_encoder) {
if (s_encoder)
{
lv_indev_set_group(s_encoder, s_group);
}
s_col = FocusColumn::Filter;
rebind_by_column();
lv_obj_t* key_target = g_state.root ? g_state.root : g_state.list_panel;
if (key_target) {
if (key_target)
{
lv_obj_add_event_cb(key_target, root_key_event_cb, LV_EVENT_KEY, nullptr);
}
}
void cleanup()
{
if (s_group) {
if (s_encoder && lv_indev_get_group(s_encoder) == s_group) {
if (s_group)
{
if (s_encoder && lv_indev_get_group(s_encoder) == s_group)
{
lv_indev_set_group(s_encoder, nullptr);
}
lv_group_del(s_group);
@@ -7,7 +7,8 @@
#include "lvgl.h"
namespace settings::ui::input {
namespace settings::ui::input
{
void init();
void cleanup();
@@ -4,11 +4,12 @@
*/
#include "settings_page_layout.h"
#include "settings_state.h"
#include "settings_page_styles.h"
#include "../../ui_common.h"
#include "settings_page_styles.h"
#include "settings_state.h"
namespace settings::ui::layout {
namespace settings::ui::layout
{
static constexpr int kFilterPanelWidth = 120;
static constexpr int kTopBarContentGap = 3;
@@ -7,7 +7,8 @@
#include "lvgl.h"
namespace settings::ui::layout {
namespace settings::ui::layout
{
lv_obj_t* create_root(lv_obj_t* parent);
lv_obj_t* create_header(lv_obj_t* root, void (*back_callback)(void*), void* user_data);
@@ -5,7 +5,8 @@
#include "settings_page_styles.h"
namespace settings::ui::style {
namespace settings::ui::style
{
static constexpr uint32_t kPrimary = 0xEBA341;
static constexpr uint32_t kPrimaryLight = 0xF1B65A;
@@ -7,7 +7,8 @@
#include "lvgl.h"
namespace settings::ui::style {
namespace settings::ui::style
{
void init_once();
+4 -3
View File
@@ -5,9 +5,10 @@
#include "settings_state.h"
namespace settings::ui {
namespace settings::ui
{
SettingsData g_settings {};
UiState g_state {};
SettingsData g_settings{};
UiState g_state{};
} // namespace settings::ui
+48 -41
View File
@@ -4,27 +4,27 @@
*/
#include "team_page_components.h"
#include "team_page_layout.h"
#include "team_page_styles.h"
#include "team_page_input.h"
#include "team_state.h"
#include "team_ui_store.h"
#include "../../ui_common.h"
#include "../../../app/app_context.h"
#include "../../../sys/event_bus.h"
#include "../../../team/infra/nfc/team_nfc.h"
#include "../../../team/protocol/team_mgmt.h"
#include "../../../team/usecase/team_service.h"
#include "../../../team/infra/nfc/team_nfc.h"
#include "../../../sys/event_bus.h"
#include "../../ui_common.h"
#include "../../widgets/system_notification.h"
#include "meshtastic/mesh.pb.h"
#include "pb_decode.h"
#include "team_page_input.h"
#include "team_page_layout.h"
#include "team_page_styles.h"
#include "team_state.h"
#include "team_ui_store.h"
#include <Arduino.h>
#include <cstdio>
#include <string>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <string>
namespace team
{
@@ -174,7 +174,8 @@ void mark_keydist_confirmed(uint32_t node_id, uint32_t key_id)
{
s_keydist_pending.erase(
std::remove_if(s_keydist_pending.begin(), s_keydist_pending.end(),
[&](const KeyDistPending& item) {
[&](const KeyDistPending& item)
{
return item.node_id == node_id && item.key_id == key_id;
}),
s_keydist_pending.end());
@@ -198,7 +199,7 @@ void process_keydist_retries()
return;
}
for (auto it = s_keydist_pending.begin(); it != s_keydist_pending.end(); )
for (auto it = s_keydist_pending.begin(); it != s_keydist_pending.end();)
{
if (now < it->next_retry_s)
{
@@ -624,7 +625,6 @@ std::string format_signal(uint8_t bars)
return std::string(buf);
}
std::string format_invite_code(const std::string& code)
{
if (code.empty())
@@ -649,7 +649,8 @@ std::string generate_invite_code()
{
static const char kAlphabet[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
constexpr size_t kAlphaLen = sizeof(kAlphabet) - 1;
auto pick = []() {
auto pick = []()
{
return kAlphabet[random(0, kAlphaLen)];
};
std::string code;
@@ -1030,7 +1031,9 @@ void handle_team_join_request(const team::TeamJoinRequestEvent& ev)
lv_obj_t* accept_label = lv_label_create(accept_btn);
lv_label_set_text(accept_label, "Accept");
lv_obj_center(accept_label);
lv_obj_add_event_cb(accept_btn, [](lv_event_t*) {
lv_obj_add_event_cb(
accept_btn, [](lv_event_t*)
{
app::AppContext& app_ctx = app::AppContext::getInstance();
team::TeamController* controller = app_ctx.getTeamController();
if (controller)
@@ -1169,8 +1172,8 @@ void handle_team_join_request(const team::TeamJoinRequestEvent& ev)
touch_member(g_team_state.pending_join_node_id, now_secs());
g_team_state.last_update_s = now_secs();
save_state_to_store();
close_join_request_modal();
}, LV_EVENT_CLICKED, nullptr);
close_join_request_modal(); },
LV_EVENT_CLICKED, nullptr);
lv_obj_t* reject_btn = lv_btn_create(btn_row);
lv_obj_set_size(reject_btn, 90, 32);
@@ -1178,7 +1181,9 @@ void handle_team_join_request(const team::TeamJoinRequestEvent& ev)
lv_obj_t* reject_label = lv_label_create(reject_btn);
lv_label_set_text(reject_label, "Reject");
lv_obj_center(reject_label);
lv_obj_add_event_cb(reject_btn, [](lv_event_t*) {
lv_obj_add_event_cb(
reject_btn, [](lv_event_t*)
{
app::AppContext& app_ctx = app::AppContext::getInstance();
team::TeamController* controller = app_ctx.getTeamController();
if (controller)
@@ -1193,8 +1198,8 @@ void handle_team_join_request(const team::TeamJoinRequestEvent& ev)
notify_send_failed("Decision", false);
}
}
close_join_request_modal();
}, LV_EVENT_CLICKED, nullptr);
close_join_request_modal(); },
LV_EVENT_CLICKED, nullptr);
lv_group_add_obj(g_team_state.modal_group, accept_btn);
lv_group_add_obj(g_team_state.modal_group, reject_btn);
@@ -1383,21 +1388,21 @@ void handle_team_status(const team::TeamStatusEvent& ev)
{
g_team_state.security_round = ev.ctx.key_id;
}
if (ev.msg.key_id != 0)
if (ev.msg.key_id != 0)
{
if (ev.msg.key_id > g_team_state.security_round)
{
if (ev.msg.key_id > g_team_state.security_round)
{
g_team_state.waiting_new_keys = true;
}
else if (ev.msg.key_id == g_team_state.security_round)
{
g_team_state.waiting_new_keys = false;
}
if (ev.ctx.from != 0)
{
mark_keydist_confirmed(ev.ctx.from, ev.msg.key_id);
}
g_team_state.waiting_new_keys = true;
}
else if (ev.msg.key_id == g_team_state.security_round)
{
g_team_state.waiting_new_keys = false;
}
if (ev.ctx.from != 0)
{
mark_keydist_confirmed(ev.ctx.from, ev.msg.key_id);
}
}
if (ev.msg.key_id != 0 && ev.msg.key_id > prev_round)
{
std::vector<uint8_t> payload;
@@ -1713,7 +1718,7 @@ void handle_create(lv_event_t*)
{
for (size_t i = 0; i < g_team_state.team_psk.size(); ++i)
{
g_team_state.team_psk[i] = static_cast<uint8_t>(random(0, 256));
g_team_state.team_psk[i] = static_cast<uint8_t>(random(0, 256));
}
g_team_state.has_team_psk = true;
}
@@ -1891,9 +1896,10 @@ void handle_leave(lv_event_t*)
lv_obj_t* cancel_label = lv_label_create(cancel_btn);
lv_label_set_text(cancel_label, "Cancel");
lv_obj_center(cancel_label);
lv_obj_add_event_cb(cancel_btn, [](lv_event_t*) {
close_leave_confirm_modal();
}, LV_EVENT_CLICKED, nullptr);
lv_obj_add_event_cb(
cancel_btn, [](lv_event_t*)
{ close_leave_confirm_modal(); },
LV_EVENT_CLICKED, nullptr);
lv_obj_t* leave_btn = lv_btn_create(btn_row);
lv_obj_set_size(leave_btn, 90, 32);
@@ -1901,10 +1907,12 @@ void handle_leave(lv_event_t*)
lv_obj_t* leave_label = lv_label_create(leave_btn);
lv_label_set_text(leave_label, "Leave");
lv_obj_center(leave_label);
lv_obj_add_event_cb(leave_btn, [](lv_event_t*) {
lv_obj_add_event_cb(
leave_btn, [](lv_event_t*)
{
close_leave_confirm_modal();
perform_leave();
}, LV_EVENT_CLICKED, nullptr);
perform_leave(); },
LV_EVENT_CLICKED, nullptr);
lv_group_add_obj(g_team_state.modal_group, cancel_btn);
lv_group_add_obj(g_team_state.modal_group, leave_btn);
@@ -2275,7 +2283,6 @@ void handle_join_cancel(lv_event_t*)
nav_back();
}
void handle_join_retry(lv_event_t*)
{
handle_join_enter_code(nullptr);
+3 -3
View File
@@ -3,11 +3,11 @@
* @brief Team page input handling
*/
#include <Arduino.h>
#include "team_page_input.h"
#include "team_state.h"
#include "../../ui_common.h"
#include "../../../app/app_context.h"
#include "../../ui_common.h"
#include "team_state.h"
#include <Arduino.h>
extern lv_group_t* app_g;
+5 -5
View File
@@ -13,12 +13,12 @@ namespace style
{
namespace
{
static constexpr uint32_t kBtnBg = 0xF4C77A;
static constexpr uint32_t kBtnBgFoc = 0xEBA341;
static constexpr uint32_t kBtnBg = 0xF4C77A;
static constexpr uint32_t kBtnBgFoc = 0xEBA341;
static constexpr uint32_t kBtnBorder = 0xEBA341;
static constexpr uint32_t kTextMain = 0x202020;
static constexpr uint32_t kTextSub = 0x4A4A4A;
static constexpr uint32_t kListBg = 0xFFFFFF;
static constexpr uint32_t kTextMain = 0x202020;
static constexpr uint32_t kTextSub = 0x4A4A4A;
static constexpr uint32_t kListBg = 0xFFFFFF;
static constexpr uint32_t kListBorder = 0xD2D2D2;
static lv_style_t s_root;

Some files were not shown because too many files have changed in this diff Show More