mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-09-16 08:52:37 +00:00
chore: format codebase and update changelog
This commit is contained in:
@@ -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 超时和更详细的收发日志
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -11,4 +11,3 @@ class MotionBoard
|
||||
virtual SensorBHI260AP& getMotionSensor() = 0;
|
||||
virtual bool isSensorReady() const = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,4 +9,3 @@ class SdBoard
|
||||
virtual void uninstallSD() = 0;
|
||||
virtual bool isCardReady() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -21,4 +21,3 @@ class ST7789TDeck
|
||||
|
||||
} // namespace drivers
|
||||
} // namespace display
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "board/GpsBoard.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace hal
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+1743
-64
File diff suppressed because it is too large
Load Diff
+12303
-144
File diff suppressed because it is too large
Load Diff
+12303
-144
File diff suppressed because it is too large
Load Diff
+12303
-144
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user