mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Merge 4ad392d705 into 76fad1de59
This commit is contained in:
commit
376a1d139a
2 changed files with 331 additions and 28 deletions
|
|
@ -4,21 +4,31 @@
|
|||
#include "abstract_client.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/response_user_alts.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QDateTime>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_ban_history.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_warn_history.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_ban.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_warning.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -38,6 +48,21 @@ constexpr int COL_ALTS_ACTIVE = 10;
|
|||
constexpr int COL_ALTS_COUNT = 11;
|
||||
|
||||
constexpr int MAX_ALT_DEPTH = 3;
|
||||
// Starting default only (adjustable 1-100000): shared VPN exits routinely
|
||||
// cover dozens of users, so 10 hides the obvious VPN noise but keeps
|
||||
// small shared networks visible.
|
||||
constexpr int DEFAULT_VPN_THRESHOLD = 10;
|
||||
|
||||
void configureAltTable(QTableWidget *table)
|
||||
{
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser)
|
||||
|
|
@ -70,27 +95,80 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
|
|||
topBar->addWidget(depthSpin);
|
||||
topBar->addWidget(investigateButton);
|
||||
|
||||
emailCheck = new QCheckBox;
|
||||
emailCheck->setChecked(true);
|
||||
connect(emailCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
clientIdCheck = new QCheckBox;
|
||||
clientIdCheck->setChecked(true);
|
||||
connect(clientIdCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
ipCheck = new QCheckBox;
|
||||
ipCheck->setChecked(true);
|
||||
connect(ipCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
vpnLabel = new QLabel;
|
||||
vpnSpin = new QSpinBox;
|
||||
vpnSpin->setMinimum(1);
|
||||
vpnSpin->setMaximum(100000);
|
||||
vpnSpin->setValue(DEFAULT_VPN_THRESHOLD);
|
||||
connect(vpnSpin, &QSpinBox::valueChanged, this, &TabAlts::onFilterChanged);
|
||||
|
||||
auto *filterBar = new QHBoxLayout;
|
||||
filterBar->addWidget(emailCheck);
|
||||
filterBar->addWidget(clientIdCheck);
|
||||
filterBar->addWidget(ipCheck);
|
||||
filterBar->addWidget(vpnLabel);
|
||||
filterBar->addWidget(vpnSpin);
|
||||
filterBar->addStretch();
|
||||
|
||||
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
|
||||
altsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
altsTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
altsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
altsTable->verticalHeader()->setVisible(false);
|
||||
altsTable->setAlternatingRowColors(true);
|
||||
altsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
altsTable->horizontalHeader()->setStretchLastSection(true);
|
||||
configureAltTable(altsTable);
|
||||
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
|
||||
auto *userItem = altsTable->item(row, COL_ALTS_USER);
|
||||
if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) {
|
||||
investigate(userItem->text());
|
||||
}
|
||||
});
|
||||
connect(altsTable, &QTableWidget::itemSelectionChanged, this, &TabAlts::onAltSelected);
|
||||
|
||||
detailNameValue = new QLabel;
|
||||
detailCountsValue = new QLabel;
|
||||
detailNotesEdit = new QTextEdit;
|
||||
detailNotesEdit->setReadOnly(true);
|
||||
// Compact cap so the warn/ban tables stay visible without scrolling the tab.
|
||||
detailNotesEdit->setMaximumHeight(60);
|
||||
warnsTable = new QTableWidget(0, 3);
|
||||
configureAltTable(warnsTable);
|
||||
bansTable = new QTableWidget(0, 4);
|
||||
configureAltTable(bansTable);
|
||||
|
||||
auto *detailGrid = new QGridLayout;
|
||||
detailNameLabel = new QLabel;
|
||||
detailCountsLabel = new QLabel;
|
||||
detailNotesLabel = new QLabel;
|
||||
detailGrid->addWidget(detailNameLabel, 0, 0);
|
||||
detailGrid->addWidget(detailNameValue, 0, 1);
|
||||
detailGrid->addWidget(detailCountsLabel, 0, 2);
|
||||
detailGrid->addWidget(detailCountsValue, 0, 3);
|
||||
detailGrid->addWidget(detailNotesLabel, 1, 0);
|
||||
detailGrid->addWidget(detailNotesEdit, 1, 1, 1, 3);
|
||||
|
||||
auto *detailTables = new QHBoxLayout;
|
||||
detailTables->addWidget(warnsTable);
|
||||
detailTables->addWidget(bansTable);
|
||||
|
||||
detailsGroup = new QGroupBox;
|
||||
auto *detailsLayout = new QVBoxLayout(detailsGroup);
|
||||
detailsLayout->addLayout(detailGrid);
|
||||
detailsLayout->addLayout(detailTables);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addLayout(topBar);
|
||||
mainLayout->addLayout(filterBar);
|
||||
mainLayout->addWidget(statusLabel);
|
||||
mainLayout->addWidget(altsTable);
|
||||
mainLayout->addWidget(altsTable, 3);
|
||||
mainLayout->addWidget(detailsGroup, 2);
|
||||
|
||||
retranslateUi();
|
||||
clearDetails();
|
||||
investigate(initialUser);
|
||||
}
|
||||
|
||||
|
|
@ -106,9 +184,22 @@ void TabAlts::retranslateUi()
|
|||
statusLabel->setText(tr("Enter a user name to investigate."));
|
||||
}
|
||||
|
||||
emailCheck->setText(tr("Email"));
|
||||
clientIdCheck->setText(tr("Client ID"));
|
||||
ipCheck->setText(tr("IP"));
|
||||
vpnLabel->setText(tr("Hide IPs shared by more than"));
|
||||
vpnSpin->setSuffix(tr(" users"));
|
||||
|
||||
altsTable->setHorizontalHeaderLabels({tr("User"), tr("Via"), tr("Depth"), tr("Shared"), tr("eMail"),
|
||||
tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"),
|
||||
tr("Active")});
|
||||
warnsTable->setHorizontalHeaderLabels({tr("Warning time"), tr("Moderator"), tr("Reason")});
|
||||
bansTable->setHorizontalHeaderLabels({tr("Ban time"), tr("Moderator"), tr("Length"), tr("Reason")});
|
||||
|
||||
detailsGroup->setTitle(tr("Selected user"));
|
||||
detailNameLabel->setText(tr("Name:"));
|
||||
detailCountsLabel->setText(tr("Counts:"));
|
||||
detailNotesLabel->setText(tr("Admin notes:"));
|
||||
}
|
||||
|
||||
QString TabAlts::formatEpoch(quint64 ts) const
|
||||
|
|
@ -152,6 +243,9 @@ void TabAlts::investigateUser()
|
|||
return;
|
||||
}
|
||||
currentUser = userName;
|
||||
lastAlts.clear();
|
||||
selectedUser.clear();
|
||||
clearDetails();
|
||||
altsTable->setRowCount(0);
|
||||
statusLabel->setText(tr("Loading alts for %1...").arg(currentUser));
|
||||
requestAlts(userName, depthSpin->value());
|
||||
|
|
@ -173,7 +267,7 @@ void TabAlts::requestAlts(const QString &userName, int depth)
|
|||
|
||||
void TabAlts::altsResponse(const Response &response)
|
||||
{
|
||||
altsTable->setRowCount(0);
|
||||
lastAlts.clear();
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("Could not load alts for %1.").arg(currentUser));
|
||||
return;
|
||||
|
|
@ -186,31 +280,207 @@ void TabAlts::altsResponse(const Response &response)
|
|||
return;
|
||||
}
|
||||
|
||||
altsTable->setRowCount(resp.alts_size());
|
||||
int relatedCount = 0;
|
||||
for (int i = 0; i < resp.alts_size(); ++i) {
|
||||
const ServerInfo_UserAlt &alt = resp.alts(i);
|
||||
// The server appends the frontier user as a depth-0 seed row.
|
||||
if (!alt.has_depth() || alt.depth() > 0) {
|
||||
++relatedCount;
|
||||
lastAlts << resp.alts(i);
|
||||
}
|
||||
renderTable();
|
||||
}
|
||||
|
||||
bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const
|
||||
{
|
||||
const QString name = QString::fromStdString(alt.user_name());
|
||||
if (name == currentUser) {
|
||||
return true;
|
||||
}
|
||||
if (!alt.has_match_reason() || alt.match_reason().empty()) {
|
||||
return true;
|
||||
}
|
||||
const QString reason = QString::fromStdString(alt.match_reason());
|
||||
if (reason == QStringLiteral("email")) {
|
||||
return emailCheck->isChecked();
|
||||
}
|
||||
if (reason.startsWith(QStringLiteral("clientid:"))) {
|
||||
return clientIdCheck->isChecked();
|
||||
}
|
||||
if (reason.startsWith(QStringLiteral("ip:"))) {
|
||||
return ipCheck->isChecked() &&
|
||||
(!alt.has_shared_count() || alt.shared_count() <= static_cast<unsigned>(vpnSpin->value()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TabAlts::renderTable()
|
||||
{
|
||||
altsTable->setRowCount(0);
|
||||
int hidden = 0;
|
||||
int row = 0;
|
||||
for (const ServerInfo_UserAlt &alt : lastAlts) {
|
||||
if (!rowVisible(alt)) {
|
||||
++hidden;
|
||||
continue;
|
||||
}
|
||||
altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
|
||||
altsTable->setItem(i, COL_ALTS_VIA,
|
||||
altsTable->insertRow(row);
|
||||
altsTable->setItem(row, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
|
||||
altsTable->setItem(row, COL_ALTS_VIA,
|
||||
new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty()
|
||||
? QString::fromStdString(alt.match_reason())
|
||||
: QStringLiteral("-")));
|
||||
altsTable->setItem(i, COL_ALTS_DEPTH,
|
||||
altsTable->setItem(row, COL_ALTS_DEPTH,
|
||||
new QTableWidgetItem(alt.has_depth() ? QString::number(alt.depth()) : QStringLiteral("-")));
|
||||
altsTable->setItem(
|
||||
i, COL_ALTS_SHARED,
|
||||
row, COL_ALTS_SHARED,
|
||||
new QTableWidgetItem(alt.has_shared_count() ? QString::number(alt.shared_count()) : QStringLiteral("-")));
|
||||
altsTable->setItem(i, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email())));
|
||||
altsTable->setItem(i, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid())));
|
||||
altsTable->setItem(i, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time())));
|
||||
altsTable->setItem(i, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login())));
|
||||
altsTable->setItem(i, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
|
||||
altsTable->setItem(i, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
|
||||
altsTable->setItem(i, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
|
||||
altsTable->setItem(row, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email())));
|
||||
altsTable->setItem(row, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid())));
|
||||
altsTable->setItem(row, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time())));
|
||||
altsTable->setItem(row, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login())));
|
||||
altsTable->setItem(row, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
|
||||
altsTable->setItem(row, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
|
||||
altsTable->setItem(row, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
|
||||
++row;
|
||||
}
|
||||
// The server appends the frontier user as a depth-0 seed row; it is the
|
||||
// investigated user, not one of their related accounts.
|
||||
int relatedCount = 0;
|
||||
for (const ServerInfo_UserAlt &alt : lastAlts) {
|
||||
if (!alt.has_depth() || alt.depth() > 0) {
|
||||
++relatedCount;
|
||||
}
|
||||
}
|
||||
if (hidden == 0) {
|
||||
statusLabel->setText(tr("%1 account(s) related to %2.").arg(relatedCount).arg(currentUser));
|
||||
} else {
|
||||
statusLabel->setText(tr("%1 shown, %2 hidden by filters (%3 total) related to %4.")
|
||||
.arg(row)
|
||||
.arg(hidden)
|
||||
.arg(relatedCount)
|
||||
.arg(currentUser));
|
||||
}
|
||||
}
|
||||
|
||||
void TabAlts::onFilterChanged()
|
||||
{
|
||||
if (lastAlts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
renderTable();
|
||||
}
|
||||
|
||||
void TabAlts::onAltSelected()
|
||||
{
|
||||
const auto selected = altsTable->selectedItems();
|
||||
if (selected.isEmpty()) {
|
||||
selectedUser.clear();
|
||||
clearDetails();
|
||||
return;
|
||||
}
|
||||
auto *userItem = altsTable->item(selected.first()->row(), COL_ALTS_USER);
|
||||
if (userItem == nullptr || userItem->text().isEmpty() || userItem->text() == selectedUser) {
|
||||
return;
|
||||
}
|
||||
selectedUser = userItem->text();
|
||||
requestDetails(selectedUser);
|
||||
}
|
||||
|
||||
void TabAlts::requestDetails(const QString &userName)
|
||||
{
|
||||
detailNameValue->setText(userName);
|
||||
detailCountsValue->setText(tr("Loading..."));
|
||||
detailNotesEdit->clear();
|
||||
warnsTable->setRowCount(0);
|
||||
bansTable->setRowCount(0);
|
||||
|
||||
Command_ReportUserInfo infoCmd;
|
||||
infoCmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *infoPend = client->prepareModeratorCommand(infoCmd);
|
||||
connect(infoPend, &PendingCommand::finished, this, [this, userName](const Response &response) {
|
||||
if (userName == selectedUser) {
|
||||
userDetailsResponse(response);
|
||||
}
|
||||
});
|
||||
client->sendCommand(infoPend);
|
||||
|
||||
Command_GetWarnHistory warnCmd;
|
||||
warnCmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *warnPend = client->prepareModeratorCommand(warnCmd);
|
||||
connect(warnPend, &PendingCommand::finished, this, [this, userName](const Response &response) {
|
||||
if (userName == selectedUser) {
|
||||
warnHistoryResponse(response);
|
||||
}
|
||||
});
|
||||
client->sendCommand(warnPend);
|
||||
|
||||
Command_GetBanHistory banCmd;
|
||||
banCmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *banPend = client->prepareModeratorCommand(banCmd);
|
||||
connect(banPend, &PendingCommand::finished, this, [this, userName](const Response &response) {
|
||||
if (userName == selectedUser) {
|
||||
banHistoryResponse(response);
|
||||
}
|
||||
});
|
||||
client->sendCommand(banPend);
|
||||
}
|
||||
|
||||
void TabAlts::clearDetails()
|
||||
{
|
||||
detailNameValue->clear();
|
||||
detailCountsValue->clear();
|
||||
detailNotesEdit->clear();
|
||||
warnsTable->setRowCount(0);
|
||||
bansTable->setRowCount(0);
|
||||
}
|
||||
|
||||
void TabAlts::userDetailsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
detailCountsValue->setText(tr("Error loading user info."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext);
|
||||
detailCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_bans())
|
||||
.arg(resp.total_warns()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
detailNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
} else {
|
||||
detailNotesEdit->setPlainText(tr("(none)"));
|
||||
}
|
||||
}
|
||||
|
||||
void TabAlts::warnHistoryResponse(const Response &response)
|
||||
{
|
||||
warnsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_WarnHistory &resp = response.GetExtension(Response_WarnHistory::ext);
|
||||
warnsTable->setRowCount(resp.warn_list_size());
|
||||
for (int i = 0; i < resp.warn_list_size(); ++i) {
|
||||
const ServerInfo_Warning &warn = resp.warn_list(i);
|
||||
warnsTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
|
||||
warnsTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
|
||||
warnsTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.reason())));
|
||||
}
|
||||
}
|
||||
|
||||
void TabAlts::banHistoryResponse(const Response &response)
|
||||
{
|
||||
bansTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_BanHistory &resp = response.GetExtension(Response_BanHistory::ext);
|
||||
bansTable->setRowCount(resp.ban_list_size());
|
||||
for (int i = 0; i < resp.ban_list_size(); ++i) {
|
||||
const ServerInfo_Ban &ban = resp.ban_list(i);
|
||||
bansTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
|
||||
bansTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
|
||||
bansTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
|
||||
bansTable->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
|
||||
}
|
||||
statusLabel->setText(tr("%1 account(s) related to %2.").arg(relatedCount).arg(currentUser));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@
|
|||
|
||||
#include "tab.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user_alt.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSpinBox;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class Response;
|
||||
class TabSupervisor;
|
||||
|
||||
|
|
@ -20,6 +24,7 @@ class TabSupervisor;
|
|||
* Staff alt-graph investigation tool. Shows accounts related to the
|
||||
* investigated user (shared email, client id, or recent IP address),
|
||||
* including transitive alts-of-alts, with per-row match evidence.
|
||||
* Selecting a row loads that account's warnings, bans, and admin notes.
|
||||
*/
|
||||
class TabAlts : public Tab
|
||||
{
|
||||
|
|
@ -37,15 +42,26 @@ private slots:
|
|||
void investigateUser();
|
||||
void goBack();
|
||||
void altsResponse(const Response &response);
|
||||
void onFilterChanged();
|
||||
void onAltSelected();
|
||||
void userDetailsResponse(const Response &response);
|
||||
void warnHistoryResponse(const Response &response);
|
||||
void banHistoryResponse(const Response &response);
|
||||
|
||||
private:
|
||||
void loadUser(const QString &userName);
|
||||
void requestAlts(const QString &userName, int depth);
|
||||
void requestDetails(const QString &userName);
|
||||
void clearDetails();
|
||||
void renderTable();
|
||||
[[nodiscard]] bool rowVisible(const ServerInfo_UserAlt &alt) const;
|
||||
[[nodiscard]] QString formatEpoch(quint64 ts) const;
|
||||
|
||||
AbstractClient *client;
|
||||
QString currentUser;
|
||||
QString selectedUser;
|
||||
QList<QString> backStack;
|
||||
QList<ServerInfo_UserAlt> lastAlts;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QSpinBox *depthSpin;
|
||||
|
|
@ -53,6 +69,23 @@ private:
|
|||
QPushButton *backButton;
|
||||
QLabel *statusLabel;
|
||||
QTableWidget *altsTable;
|
||||
|
||||
QCheckBox *emailCheck;
|
||||
QCheckBox *clientIdCheck;
|
||||
QCheckBox *ipCheck;
|
||||
QLabel *vpnLabel;
|
||||
QSpinBox *vpnSpin;
|
||||
|
||||
QLabel *detailNameValue;
|
||||
QLabel *detailCountsValue;
|
||||
QTextEdit *detailNotesEdit;
|
||||
QTableWidget *warnsTable;
|
||||
QTableWidget *bansTable;
|
||||
|
||||
QGroupBox *detailsGroup;
|
||||
QLabel *detailNameLabel;
|
||||
QLabel *detailCountsLabel;
|
||||
QLabel *detailNotesLabel;
|
||||
};
|
||||
|
||||
#endif // TAB_ALTS_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue