(&QComboBox::currentIndexChanged), this,
+ &GeneralSettingsPage::cardLanguageBoxChanged);
+
auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1);
- languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
+ languageGrid->addWidget(&cardLanguageLabel, 1, 0);
+ languageGrid->addWidget(&cardLanguageBox, 1, 1);
+ languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
+ languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
+
+ cardLanguageNoteLabel.setWordWrap(true);
+ cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
languageGroupBox = new QGroupBox;
languageGroupBox->setLayout(languageGrid);
@@ -412,6 +435,52 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
}
+void GeneralSettingsPage::cardLanguageBoxChanged(int index)
+{
+ const QString lang = cardLanguageBox.itemData(index).toString();
+ SettingsCache::instance().cardsDisplay().setCardLang(lang);
+
+ // Switching to a non-default language only takes effect after the card
+ // database is re-imported with that language selected; English data is always
+ // present, so switching back to English needs no prompt.
+ if (lang == "en") {
+ return;
+ }
+
+ // The binary cache does not track the language its entries were imported in,
+ // and the downloaded pictures were fetched with English art names, so both are
+ // stale until Oracle re-imports the database in the new language: drop them.
+ QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache");
+ CardPictureLoader::clearNetworkCache();
+ CardPictureLoader::clearPixmapCache();
+
+ // Art is resolved by the translated card name for non-English languages, so the
+ // matching Scryfall URL is added to the top of the download list. It stays
+ // visible in the deck editor settings, where it can be removed or reordered.
+ const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl();
+
+ QString message = tr("The card database only contains English card data. To see cards in %1, "
+ "Oracle must run once with this language selected and re-import the card "
+ "database.
"
+ "The cached database and the downloaded card pictures have been cleared, so a "
+ "re-import is picked up without stale entries.
")
+ .arg(cardLanguageBox.itemText(index));
+ if (localizedUrlAdded) {
+ message += tr("The Scryfall URL that resolves card art by translated name was added to the top of your "
+ "download list. You can remove or reorder it any time.
");
+ }
+ message += tr("Run Oracle now?
");
+
+ const QMessageBox::StandardButton answer = QMessageBox::question(
+ this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ // The answer only controls whether Oracle starts right away; the caches stay
+ // cleared so the next import or launch rebuilds them in the new language.
+ if (answer == QMessageBox::Yes) {
+ emit cardDatabaseUpdateRequested();
+ }
+}
+
void GeneralSettingsPage::updateStartupServerControlsVisibility()
{
const int index = startupTabSelector.currentIndex();
@@ -429,6 +498,10 @@ void GeneralSettingsPage::retranslateUi()
languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:"));
+ cardLanguageBox.setItemText(0, tr("English"));
+ cardLanguageLabel.setText(tr("Card text & images language:"));
+ cardLanguageNoteLabel.setText(
+ tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
advertiseTranslationPageLabel.setText(
QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
index e0c1a47bf..7afba158f 100644
--- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
@@ -23,6 +23,10 @@ public:
static QStringList findQmFiles();
static QString languageName(const QString &lang);
+signals:
+ /// Request to re-import the card database with the newly selected card language
+ void cardDatabaseUpdateRequested();
+
private slots:
void deckPathButtonClicked();
void filtersPathButtonClicked();
@@ -33,6 +37,7 @@ private slots:
void tokenDatabasePathButtonClicked();
void resetAllPathsClicked();
void languageBoxChanged(int index);
+ void cardLanguageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private:
@@ -46,6 +51,10 @@ private:
QComboBox languageBox;
QLabel advertiseTranslationPageLabel;
+ QLabel cardLanguageLabel;
+ QComboBox cardLanguageBox;
+ QLabel cardLanguageNoteLabel;
+
QLabel updateReleaseChannelLabel;
QComboBox updateReleaseChannelBox;
QCheckBox startupUpdateCheckCheckBox;
diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
index e4f24ab73..a3b89f8c4 100644
--- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
@@ -1,6 +1,7 @@
#include "messages_settings_page.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h"
#include
@@ -59,6 +60,10 @@ MessagesSettingsPage::MessagesSettingsPage()
connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setRoomHistory);
+ ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages());
+ connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
+ &ChatSettings::setIgnoreAllPrivateMessages);
+
customAlertString = new QLineEdit();
customAlertString->setText(SettingsCache::instance().chat().getHighlightWords());
connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(),
@@ -76,6 +81,7 @@ MessagesSettingsPage::MessagesSettingsPage()
chatGrid->addWidget(&messagePopups, 5, 0);
chatGrid->addWidget(&mentionPopups, 6, 0);
chatGrid->addWidget(&roomHistory, 7, 0);
+ chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0);
chatGroupBox = new QGroupBox;
chatGroupBox->setLayout(chatGrid);
@@ -102,15 +108,15 @@ MessagesSettingsPage::MessagesSettingsPage()
}
aAdd = new QAction(this);
- aAdd->setIcon(QPixmap("theme:icons/increment"));
+ aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
aEdit = new QAction(this);
- aEdit->setIcon(QPixmap("theme:icons/pencil"));
+ aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
aRemove = new QAction(this);
- aRemove->setIcon(QPixmap("theme:icons/decrement"));
+ aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
auto *messageToolBar = new QToolBar;
@@ -256,6 +262,7 @@ void MessagesSettingsPage::retranslateUi()
messagePopups.setText(tr("Enable desktop notifications for private messages"));
mentionPopups.setText(tr("Enable desktop notification for mentions"));
roomHistory.setText(tr("Enable room message history on join"));
+ ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages"));
hexLabel.setText(tr("(Color is hexadecimal)"));
hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));
diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
index e98ae0592..436ebbad9 100644
--- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
@@ -40,6 +40,7 @@ private:
QCheckBox messagePopups;
QCheckBox mentionPopups;
QCheckBox roomHistory;
+ QCheckBox ignoreAllPrivateMessagesCheckBox;
QGroupBox *chatGroupBox;
QGroupBox *highlightGroupBox;
QGroupBox *messageGroupBox;
diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
index 1f1867f7c..1277d0e49 100644
--- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
@@ -3,6 +3,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcut_treeview.h"
#include "../../../client/settings/shortcuts_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include "../interface/widgets/utility/sequence_edit.h"
@@ -47,8 +48,8 @@ ShortcutSettingsPage::ShortcutSettingsPage()
btnResetAll = new QPushButton(this);
btnClearAll = new QPushButton(this);
- btnResetAll->setIcon(QPixmap("theme:icons/update"));
- btnClearAll->setIcon(QPixmap("theme:icons/clearsearch"));
+ btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update")));
+ btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
// layout
auto *_editLayout = new QGridLayout;
diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp
index 66b68d823..657ef3dbe 100644
--- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp
@@ -1,5 +1,6 @@
#include "archidekt_api_response_deck_display_widget.h"
+#include "../../../../../../client/settings/cache_settings.h"
#include "../../../../../deck_loader/card_node_function.h"
#include "../../../../../deck_loader/deck_loader.h"
#include "../../../../cards/card_size_widget.h"
@@ -10,6 +11,7 @@
#include
#include
+#include
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
ArchidektApiResponseDeck _response,
@@ -120,6 +122,9 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
}
model = new DeckListModel(this);
+ model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model,
+ [this](const QString &lang) { model->setDisplayLanguage(lang); });
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
auto decklist = QSharedPointer(new DeckList);
diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
index 98b21d0f1..888197f53 100644
--- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
+++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
@@ -1,6 +1,7 @@
#include "tab_archidekt.h"
#include "../../../../../client/settings/cache_settings.h"
+#include "../../../../pixel_map_generator.h"
#include "../../../cards/additional_info/mana_symbol_widget.h"
#include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h"
@@ -213,7 +214,7 @@ void TabArchidekt::setupFilterWidgets()
// Format filter (collapsible)
formatButton = new SettingsButtonWidget(secondaryToolbar);
formatButton->setButtonText(tr("Formats"));
- formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced"));
+ formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
QWidget *formatContainer = new QWidget(secondaryToolbar);
QGridLayout *formatLayout = new QGridLayout(formatContainer);
diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
index 4f50e38a6..2cbbedfa4 100644
--- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
+++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
@@ -1,6 +1,7 @@
#include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h"
+#include "../../../../pixel_map_generator.h"
#include "commander_bracket_service.h"
#include
@@ -30,7 +31,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent
bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this);
- bracketRefreshButton->setIcon(QPixmap("theme:icons/reload"));
+ bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate);
diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp
index 410a48d40..dbcf50966 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp
+++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp
@@ -137,6 +137,11 @@ void TabAccount::retranslateUi()
buddyList->retranslateUi();
ignoreList->retranslateUi();
userInfoBox->retranslateUi();
+
+ buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the "
+ "server, and they can be invited to buddy-only games."));
+ ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private "
+ "messages or join your games."));
}
void TabAccount::processListUsersResponse(const Response &response)
diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
index 49e42e4cf..f3535d850 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
+++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
@@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
#include "../interface/widgets/utility/get_text_with_max.h"
@@ -105,19 +106,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Left side actions
aOpenLocalDeck = new QAction(this);
- aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
+ aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck);
aRenameLocal = new QAction(this);
- aRenameLocal->setIcon(QPixmap("theme:icons/rename"));
+ aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this);
- aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
+ aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload);
aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this);
- aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
+ aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
aOpenDecksFolder = new QAction(this);
@@ -126,16 +127,16 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Right side actions
aOpenRemoteDeck = new QAction(this);
- aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
+ aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck);
aDownload = new QAction(this);
- aDownload->setIcon(QPixmap("theme:icons/arrow_left_green"));
+ aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload);
aNewFolder = new QAction(this);
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
aDeleteRemoteDeck = new QAction(this);
- aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
+ aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
// Add actions to toolbars
diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp
new file mode 100644
index 000000000..39652ef7d
--- /dev/null
+++ b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp
@@ -0,0 +1,261 @@
+/**
+ * @file tab_developer.cpp
+ * @ingroup ServerTabs
+ */
+//! \todo Document this file.
+
+#include "tab_developer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static constexpr int DEFAULT_AUTO_REFRESH_INTERVAL_SECS = 30;
+
+TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client)
+ : Tab(_tabSupervisor), client(_client)
+{
+ statsTable = new QTableWidget(0, 2);
+ statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ statsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ statsTable->setSelectionMode(QAbstractItemView::SingleSelection);
+ statsTable->verticalHeader()->setVisible(false);
+ statsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
+ statsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
+ statsTable->horizontalHeader()->setStretchLastSection(true);
+
+ commandTable = new QTableWidget(0, 4);
+ commandTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ commandTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ commandTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ commandTable->setSelectionMode(QAbstractItemView::SingleSelection);
+ commandTable->verticalHeader()->setVisible(false);
+ commandTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
+ commandTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
+ commandTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Interactive);
+ commandTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive);
+
+ statusLabel = new QLabel;
+
+ autoRefreshCheckBox = new QCheckBox;
+ autoRefreshCheckBox->setChecked(false);
+
+ refreshIntervalSpinBox = new QSpinBox;
+ refreshIntervalSpinBox->setRange(5, 3600);
+ refreshIntervalSpinBox->setValue(DEFAULT_AUTO_REFRESH_INTERVAL_SECS);
+ refreshIntervalSpinBox->setEnabled(false);
+
+ autoRefreshTimer = new QTimer(this);
+ connect(autoRefreshTimer, &QTimer::timeout, this, &TabDeveloper::refreshClicked);
+ connect(autoRefreshCheckBox, &QCheckBox::toggled, this, &TabDeveloper::autoRefreshToggled);
+ connect(refreshIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this,
+ &TabDeveloper::refreshIntervalChanged);
+
+ refreshButton = new QPushButton;
+ refreshButton->setAutoDefault(true);
+ connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked);
+
+ auto *buttonLayout = new QHBoxLayout;
+ buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft);
+ buttonLayout->addWidget(autoRefreshCheckBox, 0, Qt::AlignRight);
+ buttonLayout->addWidget(refreshIntervalSpinBox, 0, Qt::AlignRight);
+ buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight);
+
+ auto *tableLayout = new QHBoxLayout;
+ tableLayout->addWidget(statsTable, 1);
+ tableLayout->addWidget(commandTable, 2);
+
+ auto *mainLayout = new QVBoxLayout;
+ mainLayout->addLayout(tableLayout, 1);
+ mainLayout->addLayout(buttonLayout);
+
+ auto *central = new QWidget;
+ central->setLayout(mainLayout);
+ setCentralWidget(central);
+
+ retranslateUi();
+}
+
+void TabDeveloper::retranslateUi()
+{
+ autoRefreshCheckBox->setText(tr("Auto-refresh"));
+ autoRefreshCheckBox->setToolTip(tr("Automatically request fresh server statistics at a fixed interval."));
+ refreshIntervalSpinBox->setSuffix(tr(" s"));
+ refreshIntervalSpinBox->setToolTip(tr("Seconds between automatic refreshes."));
+ refreshButton->setText(tr("Refresh server stats"));
+ statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";"));
+ commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";"));
+ if (statsTable->rowCount() == 0) {
+ statusLabel->clear();
+ }
+}
+
+QString TabDeveloper::formatBytes(quint64 bytes)
+{
+ const quint64 kib = 1024;
+ const quint64 mib = 1024 * kib;
+ const quint64 gib = 1024 * mib;
+ if (bytes >= gib) {
+ return tr("%1 GiB").arg(QString::number(bytes / static_cast(gib), 'f', 2));
+ }
+ if (bytes >= mib) {
+ return tr("%1 MiB").arg(QString::number(bytes / static_cast(mib), 'f', 2));
+ }
+ if (bytes >= kib) {
+ return tr("%1 KiB").arg(QString::number(bytes / static_cast(kib), 'f', 2));
+ }
+ return tr("%1 bytes").arg(bytes);
+}
+
+QString TabDeveloper::formatDurationMs(qint64 ms)
+{
+ if (ms >= 1000) {
+ return tr("%1 s").arg(QString::number(ms / 1000.0, 'f', 2));
+ }
+ return tr("%1 ms").arg(ms);
+}
+
+void TabDeveloper::appendStatRow(const QString &name, const QString &value)
+{
+ const int row = statsTable->rowCount();
+ statsTable->insertRow(row);
+ statsTable->setItem(row, 0, new QTableWidgetItem(name));
+ statsTable->setItem(row, 1, new QTableWidgetItem(value));
+}
+
+void TabDeveloper::appendSeparatorRow(const QString §ionTitle)
+{
+ const int row = statsTable->rowCount();
+ statsTable->insertRow(row);
+ auto *labelItem = new QTableWidgetItem(sectionTitle);
+ auto font = labelItem->font();
+ font.setBold(true);
+ labelItem->setFont(font);
+ labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable);
+ statsTable->setItem(row, 0, labelItem);
+ statsTable->setItem(row, 1, new QTableWidgetItem(QString()));
+}
+
+void TabDeveloper::refreshClicked()
+{
+ if (requestPending) {
+ return;
+ }
+ requestPending = true;
+ Command_GetServerStats cmd;
+ PendingCommand *pend = client->prepareDeveloperCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse);
+ client->sendCommand(pend);
+}
+
+void TabDeveloper::autoRefreshToggled(bool checked)
+{
+ refreshIntervalSpinBox->setEnabled(checked);
+ if (checked) {
+ refreshIntervalChanged();
+ refreshClicked();
+ } else {
+ autoRefreshTimer->stop();
+ }
+}
+
+void TabDeveloper::refreshIntervalChanged()
+{
+ if (autoRefreshCheckBox->isChecked()) {
+ autoRefreshTimer->start(refreshIntervalSpinBox->value() * 1000);
+ }
+}
+
+void TabDeveloper::serverStatsResponse(const Response &resp)
+{
+ requestPending = false;
+ if (resp.response_code() != Response::RespOk) {
+ statusLabel->setText(tr("No server statistics available yet."));
+ return;
+ }
+
+ const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext);
+
+ statsTable->setRowCount(0);
+
+ // Overview section
+ appendStatRow(tr("Registered users online"), QString::number(response.users_count()));
+ appendStatRow(tr("Moderators online"), QString::number(response.mods_count()));
+ appendStatRow(tr("Games running"), QString::number(response.games_count()));
+ appendStatRow(tr("Traffic sent (last tick)"), formatBytes(response.tx_bytes()));
+ appendStatRow(tr("Traffic received (last tick)"), formatBytes(response.rx_bytes()));
+
+ const qint64 uptime = static_cast(response.uptime_secs());
+ const int days = static_cast(uptime / 86400);
+ const int hours = static_cast((uptime % 86400) / 3600);
+ const int minutes = static_cast((uptime % 3600) / 60);
+ appendStatRow(tr("Server uptime"), days > 0 ? tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes)
+ : tr("%1h %2m").arg(hours).arg(minutes));
+
+ const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast(response.timest()));
+ appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm"));
+
+ // Live metrics section
+ appendSeparatorRow(tr("Live Metrics"));
+ appendStatRow(tr("Cards in live games"), QString::number(response.cards_in_games()));
+ appendStatRow(tr("Total commands processed"), QString::number(response.total_commands()));
+
+ if (response.total_commands() > 0) {
+ const double avgMs = static_cast(response.total_command_time_ms()) / response.total_commands();
+ appendStatRow(tr("Avg command time"), QString::number(avgMs, 'f', 2) + " ms");
+ }
+ appendStatRow(tr("Active command types"), QString::number(response.active_command_types()));
+
+ appendStatRow(tr("Event loop stalls"), QString::number(response.eventloop_stalls_total()));
+ appendStatRow(tr("Last stall overshoot"), formatDurationMs(response.eventloop_last_stall_ms()));
+ appendStatRow(tr("Worst stall overshoot"), formatDurationMs(response.eventloop_max_stall_ms()));
+
+ if (response.game_start_count() > 0) {
+ appendStatRow(tr("Game starts"), QString::number(response.game_start_count()));
+ const double avgStartMs = static_cast(response.game_start_total_ms()) / response.game_start_count();
+ appendStatRow(tr("Avg game start time"), QString::number(avgStartMs, 'f', 1) + " ms");
+ }
+
+ // Per-command breakdown table
+ QList sortedStats(response.command_stats().begin(), response.command_stats().end());
+ std::sort(sortedStats.begin(), sortedStats.end(),
+ [](const auto &a, const auto &b) { return a.total_ms() > b.total_ms(); });
+
+ commandTable->setRowCount(0);
+ for (const auto &cs : sortedStats) {
+ const int row = commandTable->rowCount();
+ commandTable->insertRow(row);
+ commandTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(cs.command_name())));
+
+ auto *countItem = new QTableWidgetItem(QString::number(cs.count()));
+ countItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ commandTable->setItem(row, 1, countItem);
+
+ auto *totalItem = new QTableWidgetItem(QString::number(cs.total_ms()));
+ totalItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ commandTable->setItem(row, 2, totalItem);
+
+ const double avg = cs.count() > 0 ? static_cast