You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
7.4 KiB
209 lines
7.4 KiB
#include "accountlist.h" |
|
#include "accountdelegate.h" |
|
#include "memberlistmodel.h" |
|
#include "memberpropsdialog.h" |
|
#include "../db/db_manager.h" |
|
#include <QVBoxLayout> |
|
#include <QLabel> |
|
#include <QDateTime> |
|
#include <QFont> |
|
#include "../../../lib/platform_compat.h" |
|
|
|
QString AccountList::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 "?"; |
|
} |
|
|
|
QString AccountList::relativeTimeStr(qint64 timestamp) { |
|
if (timestamp <= 0) return ""; |
|
qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp; |
|
if (diff < 0) diff = 0; |
|
if (diff < 60) return QString("%1s").arg(diff); |
|
if (diff < 3600) return QString("%1m").arg(diff / 60); |
|
if (diff < 86400) return QString("%1h").arg(diff / 3600); |
|
return QString("%1d").arg(diff / 86400); |
|
} |
|
|
|
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 QTextEdit(this); |
|
m_detailText->setReadOnly(true); |
|
m_detailText->setFont(QFont("monospace", 10)); |
|
m_detailText->setStyleSheet( |
|
"QTextEdit { 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); |
|
connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked); |
|
|
|
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); |
|
|
|
m_detailTimer = new QTimer(this); |
|
m_detailTimer->setInterval(5000); |
|
connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick); |
|
} |
|
|
|
void AccountList::setChannel(const QString& channelId) { |
|
m_channelId = channelId; |
|
m_model->setChannel(channelId); |
|
m_detailTimer->stop(); |
|
m_detailNodeId = 0; |
|
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; |
|
|
|
m_detailTimer->stop(); |
|
m_detailNodeId = nodeId; |
|
m_detailName.clear(); |
|
m_detailOnline = false; |
|
m_detailLastSeen = 0; |
|
m_detailCreated = 0; |
|
m_detailAddrs.clear(); |
|
|
|
sqlite3* d = m_db->m_db; |
|
sqlite3_stmt* st = nullptr; |
|
if (sqlite3_prepare_v2(d, |
|
"SELECT name, online, last_seen_at, created_at FROM nodes WHERE node_id=?", |
|
-1, &st, nullptr) == SQLITE_OK) { |
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); |
|
if (sqlite3_step(st) == SQLITE_ROW) { |
|
m_detailName = DbManager::colText(st, 0); |
|
m_detailOnline = sqlite3_column_int(st, 1) != 0; |
|
m_detailLastSeen = sqlite3_column_int64(st, 2); |
|
m_detailCreated = sqlite3_column_int64(st, 3); |
|
} |
|
sqlite3_finalize(st); |
|
} |
|
|
|
if (m_detailName.isEmpty()) |
|
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper(); |
|
|
|
sqlite3_stmt* na = nullptr; |
|
sqlite3_prepare_v2(d, |
|
"SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port", |
|
-1, &na, nullptr); |
|
if (na) { |
|
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId); |
|
while (sqlite3_step(na) == SQLITE_ROW) { |
|
DetailAddress a; |
|
a.family = sqlite3_column_int(na, 0); |
|
a.protocol = sqlite3_column_int(na, 1); |
|
const void* adr = sqlite3_column_blob(na, 2); |
|
int sz = sqlite3_column_bytes(na, 2); |
|
if (adr && sz > 0) a.addr = QByteArray((const char*)adr, sz); |
|
a.port = sqlite3_column_int(na, 3); |
|
if (!a.addr.isEmpty()) m_detailAddrs.append(a); |
|
} |
|
sqlite3_finalize(na); |
|
} |
|
|
|
m_detailTimer->start(); |
|
updateDetailDisplay(); |
|
} |
|
|
|
void AccountList::updateDetailDisplay() { |
|
QStringList lines; |
|
|
|
lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0'))); |
|
|
|
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E"; |
|
lines.append(QString("<span style='color:%1;'>●</span> %2") |
|
.arg(dotColor, m_detailName.toHtmlEscaped())); |
|
|
|
if (m_detailLastSeen > 0) |
|
lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen))); |
|
|
|
if (m_detailCreated > 0) |
|
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated))); |
|
|
|
for (const auto& a : m_detailAddrs) { |
|
QString proto; |
|
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; } |
|
lines.append(QString("%1:%2:%3").arg(proto, addrStr(a.addr.constData(), a.family)).arg(a.port)); |
|
} |
|
|
|
m_detailText->setHtml(lines.join("<br>")); |
|
} |
|
|
|
void AccountList::onDetailTimerTick() { |
|
if (m_detailNodeId != 0) updateDetailDisplay(); |
|
} |
|
|
|
void AccountList::onMemberDoubleClicked(const QModelIndex& index) { |
|
if (!index.isValid()) return; |
|
quint64 nodeId = m_model->nodeIdAt(index.row()); |
|
if (!nodeId) return; |
|
QString name = m_model->data(index, Qt::DisplayRole).toString(); |
|
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this); |
|
dlg.exec(); |
|
} |
|
|
|
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(); |
|
}
|
|
|