#include "accountlist.h" #include "accountdelegate.h" #include "memberlistmodel.h" #include "memberpropsdialog.h" #include "../db/db_manager.h" #include #include #include #include #include "../../../lib/platform_compat.h" static QString shortenKey(const QByteArray& key) { if (key.size() < 8) return key.toHex(' ').toUpper(); return key.left(4).toHex(' ').toUpper() + ".." + key.right(4).toHex(' ').toUpper(); } static QString addrStr(const void* addr, int family) { if (family == 4) { const uint8_t* a = (const uint8_t*)addr; return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]); } if (family == 6) { const uint16_t* a = (const uint16_t*)addr; return QString("%1:%2:%3:%4:%5:%6:%7:%8") .arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16) .arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16) .arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16) .arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16); } return "?"; } static QString protoStr(int protocol) { switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; case 3: return "UDP+TCP"; default: return QString::number(protocol); } } static QString addrTypeStr(int t) { switch (t) { case 0: return "NETIF"; case 1: return "DIRECT"; case 2: return "NAT_EIM"; case 3: return "NAT_STRICT"; default: return QString("?%1").arg(t); } } AccountList::AccountList(DbManager* db, QWidget *parent) : QWidget(parent) , m_listView(new QListView(this)) , m_model(new MemberListModel(db, this)) , m_db(db) , m_detailText(nullptr) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); auto *header = new QLabel("Members", this); header->setAlignment(Qt::AlignCenter); header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);"); layout->addWidget(header); m_listView->setModel(m_model); m_listView->setItemDelegate(new AccountDelegate(m_listView)); m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_listView->setSelectionMode(QAbstractItemView::SingleSelection); m_listView->setIconSize(QSize(24, 24)); m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_listView->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_listView, 3); m_detailText = new QPlainTextEdit(this); m_detailText->setReadOnly(true); m_detailText->setFont(QFont("monospace", 9)); m_detailText->setStyleSheet( "QPlainTextEdit { border-top: 1px solid palette(mid); border-bottom: none;" " border-left: none; border-right: none; padding: 6px; background: palette(base); }"); m_detailText->setPlaceholderText("Select a member"); layout->addWidget(m_detailText, 2); connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked); m_refreshTimer = new QTimer(this); m_refreshTimer->start(2000); connect(m_refreshTimer, &QTimer::timeout, m_model, &MemberListModel::refreshRtt); connect(m_listView, &QWidget::customContextMenuRequested, this, &AccountList::onMemberContextMenu); } void AccountList::setChannel(const QString& channelId) { m_channelId = channelId; m_model->setChannel(channelId); m_detailText->clear(); } void AccountList::refresh() { m_model->refresh(); } void AccountList::onMemberClicked(const QModelIndex& index) { if (!index.isValid()) return; quint64 nodeId = m_model->nodeIdAt(index.row()); if (nodeId) showMemberDetail(nodeId); } void AccountList::showMemberDetail(quint64 nodeId) { if (!m_db || !m_db->m_db) return; sqlite3* d = m_db->m_db; QString text; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(d, "SELECT node_id, name, online, x25519_pubkey, ed25519_pubkey," " last_seen_at, created_at FROM nodes WHERE node_id=?", -1, &st, nullptr) != SQLITE_OK) { m_detailText->clear(); return; } sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); if (sqlite3_step(st) == SQLITE_ROW) { QString name = DbManager::colText(st, 1); int online = sqlite3_column_int(st, 2); QByteArray x25 = DbManager::colBlob(st, 3); QByteArray ed = DbManager::colBlob(st, 4); qint64 last_seen = sqlite3_column_int64(st, 5); qint64 created = sqlite3_column_int64(st, 6); text += QString("Node: 0x%1").arg(nodeId, 16, 16, QChar('0')); if (!name.isEmpty()) text += QString(" (%1)").arg(name); text += QString("\n online: %1\n").arg(online ? "yes" : "no"); text += QString(" x25519: %1\n").arg(x25.isEmpty() ? "-" : shortenKey(x25)); text += QString(" ed25519: %1\n").arg(ed.isEmpty() ? "-" : shortenKey(ed)); if (last_seen > 0) text += QString(" last_seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss")); if (created > 0) text += QString(" created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss")); } else { text = QString("Node 0x%1 not found in database.").arg(nodeId, 16, 16, QChar('0')); } sqlite3_finalize(st); sqlite3_stmt* na = nullptr; sqlite3_prepare_v2(d, "SELECT family, protocol, address, port, rtt, addr_type" " FROM node_addresses WHERE node_id=? ORDER BY family, port", -1, &na, nullptr); if (na) { sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId); int ac = 0; while (sqlite3_step(na) == SQLITE_ROW) { if (ac == 0) text += " addresses:\n"; int family = sqlite3_column_int(na, 0); int proto = sqlite3_column_int(na, 1); const void* addr = sqlite3_column_blob(na, 2); int addrSz = sqlite3_column_bytes(na, 2); int port = sqlite3_column_int(na, 3); int rtt = sqlite3_column_int(na, 4); int addrType = sqlite3_column_int(na, 5); if (addr && addrSz > 0) text += QString(" %1:%2 %3 %4 rtt=%5ms\n") .arg(addrStr(addr, family)) .arg(port) .arg(protoStr(proto)) .arg(addrTypeStr(addrType)) .arg(rtt > 0 ? QString::number(rtt) : "-"); ac++; } if (ac == 0) text += " addresses: none\n"; sqlite3_finalize(na); } m_detailText->setPlainText(text); } void AccountList::onMemberContextMenu(const QPoint& pos) { QModelIndex index = m_listView->indexAt(pos); if (!index.isValid()) return; quint64 nodeId = m_model->nodeIdAt(index.row()); if (!nodeId) return; QString name = m_model->data(index, Qt::DisplayRole).toString(); QMenu menu(m_listView); QAction* editAction = menu.addAction("Edit"); QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos)); if (chosen != editAction) return; MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this); dlg.exec(); }