#include "memberlistmodel.h" #include "../db/db_manager.h" #include #include #include MemberListModel::MemberListModel(DbManager* db, QObject* parent) : QAbstractListModel(parent), m_db(db) {} int MemberListModel::rowCount(const QModelIndex& parent) const { (void)parent; return m_totalCount; } QVariant MemberListModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_totalCount) return {}; auto it = m_cache.find(index.row()); if (it != m_cache.end()) { if (role == Qt::DisplayRole) return it->localNick.isEmpty() ? it->name : it->localNick; if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar)); if (role == Qt::UserRole + 1) { QString sub; if (it->rtt >= 0) sub += QString("rtt=%1ms").arg(it->rtt / 10); if (it->natType >= 0) { static const char* nat[] = {"NET","DIR","EIM","STR"}; const char* nt = (it->natType <= 3) ? nat[it->natType] : "?"; if (!sub.isEmpty()) sub += " "; sub += nt; } return sub; } if (role == Qt::ToolTipRole) return QString(it->online ? "online" : "offline") + " | 0x" + QString("%1").arg(it->nodeId, 16, 16, QChar('0')); if (role == Qt::UserRole + 2) return it->isSelf; return {}; } if (role == Qt::DisplayRole) { scheduleBatch(index.row()); return QStringLiteral("\u2026"); } return {}; } void MemberListModel::setChannel(const QString& channelId) { beginResetModel(); m_channelId = channelId; m_db->cleanOrphanedPeers(channelId); m_totalCount = m_db ? m_db->getChannelMemberCount(channelId) : 0; m_cache.clear(); m_pendingFrom = -1; endResetModel(); if (m_totalCount > 0) scheduleBatch(0); } void MemberListModel::refresh() { if (!m_channelId.isEmpty()) setChannel(m_channelId); } quint64 MemberListModel::nodeIdAt(int row) const { auto it = m_cache.find(row); return it != m_cache.end() ? it->nodeId : 0; } void MemberListModel::scheduleBatch(int centerRow) const { if (m_pendingFrom >= 0) return; int from = centerRow - BATCH / 2; if (from < 0) from = 0; int maxFrom = m_totalCount - BATCH; if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0; const_cast(this)->m_pendingFrom = from; const_cast(this)->m_pendingCount = BATCH; QTimer::singleShot(0, const_cast(this), &MemberListModel::loadPendingBatch); } void MemberListModel::loadPendingBatch() { if (m_pendingFrom < 0) return; int from = m_pendingFrom, count = m_pendingCount; m_pendingFrom = -1; loadBatch(from, count); } void MemberListModel::loadBatch(int from, int count) { if (!m_db || m_channelId.isEmpty()) return; int maxFrom = m_totalCount - count; if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0; auto members = m_db->getChannelMembers(m_channelId, from, count); for (int i = 0; i < members.size(); i++) { int row = from + i; MemberListItem& item = m_cache[row]; item.nodeId = members[i].nodeId; item.isSelf = (item.nodeId == m_db->myNodeId()); item.online = members[i].online; QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper(); item.name = members[i].name.isEmpty() ? shortId : members[i].name; item.localNick = members[i].localNick; QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E"); item.avatar = makeAvatar(c, item.name[0]); } // batch-загрузка RTT и NAT типа из node_addresses QStringList idList; for (int i = 0; i < members.size(); i++) idList.append(QString::number(members[i].nodeId)); QString ids = idList.isEmpty() ? QString("0") : idList.join(","); QByteArray sql = QString( "SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses" " WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id" ).arg(ids).toUtf8(); sqlite3_stmt* na = nullptr; if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) == SQLITE_OK) { while (sqlite3_step(na) == SQLITE_ROW) { quint64 nid = (quint64)sqlite3_column_int64(na, 0); int rttVal = sqlite3_column_int(na, 1); int at = sqlite3_column_int(na, 2); for (int i = 0; i < members.size(); i++) { if (members[i].nodeId == nid) { int row = from + i; auto it = m_cache.find(row); if (it != m_cache.end()) { it->rtt = rttVal; it->natType = at; } break; } } } sqlite3_finalize(na); } evictDistant(from + BATCH / 2); int last = from + (int)members.size() - 1; if (last >= from) emit dataChanged(index(from), index(last)); } void MemberListModel::refreshRtt() { if (m_cache.isEmpty() || !m_db || !m_db->m_db) return; QStringList idList; for (auto it = m_cache.begin(); it != m_cache.end(); ++it) idList.append(QString::number(it->nodeId)); if (idList.isEmpty()) return; QString ids = idList.join(","); QByteArray sql = QString( "SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses" " WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id" ).arg(ids).toUtf8(); sqlite3_stmt* na = nullptr; if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) != SQLITE_OK) return; QList changed; while (sqlite3_step(na) == SQLITE_ROW) { quint64 nid = (quint64)sqlite3_column_int64(na, 0); int rttVal = sqlite3_column_int(na, 1); int at = sqlite3_column_int(na, 2); for (auto it = m_cache.begin(); it != m_cache.end(); ++it) { if (it->nodeId == nid) { if (it->rtt != rttVal || it->natType != at) { it->rtt = rttVal; it->natType = at; changed.append(it.key()); } break; } } } sqlite3_finalize(na); for (int row : changed) emit dataChanged(index(row), index(row), {Qt::UserRole + 1}); } void MemberListModel::evictDistant(int centerRow) { if (m_cache.size() <= MAX_CACHE) return; int half = MAX_CACHE / 2; QList toRemove; for (auto it = m_cache.begin(); it != m_cache.end(); ++it) if (qAbs(it.key() - centerRow) > half) toRemove.append(it.key()); for (int k : toRemove) m_cache.remove(k); } QPixmap MemberListModel::makeAvatar(QColor color, QChar letter) const { QPixmap pm(24, 24); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setBrush(color); p.setPen(Qt::NoPen); p.drawEllipse(2, 2, 20, 20); p.setPen(Qt::white); QFont f; f.setBold(true); f.setPixelSize(12); p.setFont(f); p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, QString(letter)); p.end(); return pm; }