mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Runtime inversion is not a theming system: it rotates hue, breaks duotone art and mishandles alpha, and a QPixmap snapshot cannot follow a palette change. Redraw the top-level UI glyph set on a 24x24 grid with `currentColor` as the paint token, drop Inkscape/Sodipodi cruft, and delete the icons whose metaphor was duplicated (cogwheel/gear -> settings, scale_balanced -> scales, update.png -> reload). Load the glyphs through `loadThemedIcon` so the engine resolves the token from the palette on every request, and let `themePixmap` use the same resolution for one-shot pixmaps. `loadColorAdjustedPixmap` is gone; every caller now keeps a QIcon, which `SettingsButtonWidget::setButtonIcon` accepts instead of a QPixmap. Content art (mana, counters, countries) keeps its authored colors. Add a render test covering aspect ratio, palette flips, icon modes and high-DPI pixmaps.
739 lines
26 KiB
C++
739 lines
26 KiB
C++
#include "tab_archidekt.h"
|
|
|
|
#include "../../../../../client/settings/cache_settings.h"
|
|
#include "../../../../theme_icon.h"
|
|
#include "../../../cards/additional_info/mana_symbol_widget.h"
|
|
#include "../../../cards/card_size_widget.h"
|
|
#include "../../../quick_settings/settings_button_widget.h"
|
|
#include "../../../utility/completer_utils.h"
|
|
#include "../../tab.h"
|
|
#include "../../tab_supervisor.h"
|
|
#include "api_response/archidekt_deck_listing_api_response.h"
|
|
#include "api_response/deck/archidekt_api_response_deck.h"
|
|
#include "card_database_display_model.h"
|
|
#include "card_database_model.h"
|
|
#include "display/archidekt_api_response_deck_display_widget.h"
|
|
#include "display/archidekt_api_response_deck_listings_display_widget.h"
|
|
|
|
#include <QByteArray>
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QDebug>
|
|
#include <QEvent>
|
|
#include <QGridLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QJsonValueRef>
|
|
#include <QLabel>
|
|
#include <QLayoutItem>
|
|
#include <QLineEdit>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QPoint>
|
|
#include <QPushButton>
|
|
#include <QScrollArea>
|
|
#include <QSharedPointer>
|
|
#include <QSizePolicy>
|
|
#include <QSpinBox>
|
|
#include <QStringList>
|
|
#include <QStringLiteral>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
#include <QUrlQuery>
|
|
#include <QVBoxLayout>
|
|
#include <QWheelEvent>
|
|
#include <QWidget>
|
|
#include <QtGlobal>
|
|
#include <libcockatrice/card/database/card_database_manager.h>
|
|
#include <libcockatrice/network/network_manager.h>
|
|
#include <libcockatrice/settings/cards_display_settings.h>
|
|
#include <libcockatrice/utility/qt_utils.h>
|
|
#include <qlogging.h>
|
|
#include <qnamespace.h>
|
|
#include <version_string.h>
|
|
|
|
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
|
: Tab(_tabSupervisor), currentPage(1), isLoadingMore(false), isListMode(true)
|
|
{
|
|
// Initialize network
|
|
networkManager = NetworkManagerFactory::create(this);
|
|
|
|
// Initialize debounce timer
|
|
searchDebounceTimer = new QTimer(this);
|
|
searchDebounceTimer->setSingleShot(true);
|
|
searchDebounceTimer->setInterval(300);
|
|
connect(searchDebounceTimer, &QTimer::timeout, this, &TabArchidekt::doSearchImmediate);
|
|
|
|
initializeUi();
|
|
setupFilterWidgets();
|
|
connectSignals();
|
|
retranslateUi();
|
|
|
|
getTopDecks();
|
|
}
|
|
|
|
TabArchidekt::~TabArchidekt()
|
|
{
|
|
cancelRequest();
|
|
}
|
|
|
|
void TabArchidekt::cancelRequest()
|
|
{
|
|
const QPointer<QNetworkReply> previousReply = activeReply;
|
|
activeReply = nullptr;
|
|
isLoadingMore = false;
|
|
if (previousReply) {
|
|
disconnect(previousReply, nullptr, this, nullptr);
|
|
previousReply->abort();
|
|
previousReply->deleteLater();
|
|
}
|
|
}
|
|
|
|
void TabArchidekt::startRequest(QUrl url, RequestKind kind, int page)
|
|
{
|
|
cancelRequest();
|
|
requestUrl = url;
|
|
requestKind = kind;
|
|
requestedPage = page;
|
|
isLoadingMore = true;
|
|
retryButton->hide();
|
|
requestStatusLabel->setText(kind == RequestKind::Deck ? tr("Loading deck…") : tr("Loading decks…"));
|
|
requestStatusLabel->show();
|
|
|
|
QNetworkRequest request(url);
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
|
auto *reply = networkManager->get(request);
|
|
activeReply = reply;
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() { processApiJson(reply); });
|
|
}
|
|
|
|
void TabArchidekt::showRequestError(const QString &error)
|
|
{
|
|
requestStatusLabel->setText(tr("Unable to load decks: %1").arg(error));
|
|
requestStatusLabel->show();
|
|
retryButton->show();
|
|
}
|
|
|
|
void TabArchidekt::initializeUi()
|
|
{
|
|
// Main container
|
|
container = new QWidget(this);
|
|
mainLayout = QtUtils::makeLayout<QVBoxLayout>(container, 0);
|
|
|
|
// Primary toolbar (most important filters)
|
|
primaryToolbar = new QWidget(container);
|
|
primaryToolbar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
primaryToolbarLayout = new QHBoxLayout(primaryToolbar);
|
|
primaryToolbarLayout->setContentsMargins(6, 6, 6, 6);
|
|
primaryToolbarLayout->setSpacing(6);
|
|
|
|
// Sort controls
|
|
sortByLabel = new QLabel(primaryToolbar);
|
|
orderByCombo = new QComboBox(primaryToolbar);
|
|
orderByCombo->addItems({"name", "updatedAt", "createdAt", "viewCount", "size", "edhBracket"});
|
|
orderByCombo->setCurrentText("updatedAt");
|
|
orderByCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
|
|
|
orderDirButton = new QPushButton(tr("Desc."), primaryToolbar);
|
|
orderDirButton->setCheckable(true);
|
|
orderDirButton->setChecked(true);
|
|
orderDirButton->setFixedWidth(60);
|
|
|
|
// Color filter (inline)
|
|
QWidget *colorWidget = new QWidget(primaryToolbar);
|
|
QHBoxLayout *colorLayout = QtUtils::makeLayout<QHBoxLayout>(colorWidget, 2);
|
|
|
|
QString colorIdentity = "WUBRG";
|
|
for (const QChar &color : colorIdentity) {
|
|
auto *manaSymbol = new ManaSymbolWidget(colorWidget, color, false, true);
|
|
manaSymbol->setFixedSize(28, 28);
|
|
colorSymbols.append(manaSymbol);
|
|
colorLayout->addWidget(manaSymbol);
|
|
|
|
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this, [this](QChar c, bool active) {
|
|
if (active) {
|
|
activeColors.insert(c);
|
|
} else {
|
|
activeColors.remove(c);
|
|
}
|
|
doSearch();
|
|
});
|
|
}
|
|
|
|
logicalAndCheck = new QCheckBox(tr("AND"), primaryToolbar);
|
|
logicalAndCheck->setToolTip(tr("Require ALL selected colors"));
|
|
|
|
// Common search fields
|
|
nameField = new QLineEdit(primaryToolbar);
|
|
nameField->setPlaceholderText(tr("Deck name..."));
|
|
nameField->setMinimumWidth(150);
|
|
|
|
ownerField = new QLineEdit(primaryToolbar);
|
|
ownerField->setPlaceholderText(tr("Owner..."));
|
|
ownerField->setMinimumWidth(120);
|
|
|
|
// Filter by label
|
|
filterByLabel = new QLabel(primaryToolbar);
|
|
|
|
// Package toggle
|
|
packagesCheck = new QCheckBox(tr("Packages"), primaryToolbar);
|
|
|
|
// Search button
|
|
searchButton = new QPushButton(tr("Search"), primaryToolbar);
|
|
searchButton->setDefault(true);
|
|
|
|
// Advanced filters toggle button
|
|
advancedFiltersButton = new QPushButton(tr("Advanced Filters"), primaryToolbar);
|
|
advancedFiltersButton->setCheckable(true);
|
|
advancedFiltersButton->setChecked(false);
|
|
|
|
// Settings
|
|
settingsButton = new SettingsButtonWidget(primaryToolbar);
|
|
cardSizeSlider =
|
|
new CardSizeWidget(primaryToolbar, nullptr, SettingsCache::instance().cardsDisplay().getArchidektPreviewSize());
|
|
settingsButton->addSettingsWidget(cardSizeSlider);
|
|
|
|
// Assemble primary toolbar
|
|
primaryToolbarLayout->addWidget(sortByLabel);
|
|
primaryToolbarLayout->addWidget(orderByCombo);
|
|
primaryToolbarLayout->addWidget(orderDirButton);
|
|
|
|
// Add separator/spacing
|
|
primaryToolbarLayout->addSpacing(12);
|
|
|
|
primaryToolbarLayout->addWidget(filterByLabel);
|
|
primaryToolbarLayout->addWidget(colorWidget);
|
|
primaryToolbarLayout->addWidget(logicalAndCheck);
|
|
primaryToolbarLayout->addWidget(nameField, 1);
|
|
primaryToolbarLayout->addWidget(ownerField, 1);
|
|
primaryToolbarLayout->addWidget(packagesCheck);
|
|
primaryToolbarLayout->addWidget(searchButton, 1);
|
|
primaryToolbarLayout->addWidget(advancedFiltersButton);
|
|
primaryToolbarLayout->addWidget(settingsButton);
|
|
|
|
// Secondary toolbar (advanced filters - initially hidden)
|
|
secondaryToolbar = new QWidget(container);
|
|
secondaryToolbar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
secondaryToolbar->setVisible(false); // Start hidden
|
|
secondaryToolbarLayout = new QHBoxLayout(secondaryToolbar);
|
|
secondaryToolbarLayout->setContentsMargins(6, 3, 6, 6);
|
|
secondaryToolbarLayout->setSpacing(6);
|
|
|
|
// Scrollable results area
|
|
scrollArea = QtUtils::makeScrollArea(container);
|
|
|
|
resultsContainer = new QWidget();
|
|
resultsLayout = QtUtils::makeLayout<QVBoxLayout>(resultsContainer, 0);
|
|
|
|
scrollArea->setWidget(resultsContainer);
|
|
|
|
scrollArea->viewport()->installEventFilter(this);
|
|
|
|
mainLayout->addWidget(primaryToolbar);
|
|
mainLayout->addWidget(secondaryToolbar);
|
|
|
|
auto *requestStatusRow = new QHBoxLayout;
|
|
requestStatusLabel = new QLabel(container);
|
|
requestStatusLabel->setTextFormat(Qt::PlainText);
|
|
requestStatusLabel->setWordWrap(true);
|
|
retryButton = new QPushButton(tr("Retry"), container);
|
|
retryButton->hide();
|
|
connect(retryButton, &QPushButton::clicked, this,
|
|
[this]() { startRequest(requestUrl, requestKind, requestedPage); });
|
|
requestStatusRow->addWidget(requestStatusLabel, 1);
|
|
requestStatusRow->addWidget(retryButton);
|
|
mainLayout->addLayout(requestStatusRow);
|
|
mainLayout->addWidget(scrollArea);
|
|
|
|
setCentralWidget(container);
|
|
}
|
|
|
|
bool TabArchidekt::eventFilter(QObject *obj, QEvent *event)
|
|
{
|
|
if (obj == scrollArea->viewport() && event->type() == QEvent::Wheel) {
|
|
auto *wheelEvent = static_cast<QWheelEvent *>(event);
|
|
|
|
if (wheelEvent->angleDelta().y() < 0 && !isLoadingMore && isListMode) {
|
|
loadNextPage();
|
|
wheelEvent->accept();
|
|
return false; // allow scrolling
|
|
}
|
|
}
|
|
|
|
// Always pass the event to the parent to handle normal scrolling
|
|
return QWidget::eventFilter(obj, event);
|
|
}
|
|
|
|
void TabArchidekt::setupFilterWidgets()
|
|
{
|
|
// Advanced filters (in secondary toolbar)
|
|
|
|
// EDH Bracket
|
|
bracketLabel = new QLabel(secondaryToolbar);
|
|
edhBracketCombo = new QComboBox(secondaryToolbar);
|
|
edhBracketCombo->addItem(QString());
|
|
edhBracketCombo->addItems({"1", "2", "3", "4", "5"});
|
|
edhBracketCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
|
|
|
// Format filter (collapsible)
|
|
formatButton = new SettingsButtonWidget(secondaryToolbar);
|
|
formatButton->setButtonText(tr("Formats"));
|
|
formatButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales")));
|
|
|
|
QWidget *formatContainer = new QWidget(secondaryToolbar);
|
|
QGridLayout *formatLayout = new QGridLayout(formatContainer);
|
|
formatLayout->setContentsMargins(4, 4, 4, 4);
|
|
|
|
QStringList formatNames = {"Standard", "Modern", "Commander", "Legacy", "Vintage",
|
|
"Pauper", "Custom", "Frontier", "Future Std", "Penny Dreadful",
|
|
"1v1 Commander", "Dual Commander", "Brawl"};
|
|
|
|
int row = 0, col = 0;
|
|
for (const QString &formatName : formatNames) {
|
|
auto *formatCheckBox = new QCheckBox(formatName, formatContainer);
|
|
formatChecks << formatCheckBox;
|
|
formatLayout->addWidget(formatCheckBox, row, col);
|
|
connect(formatCheckBox, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
|
|
|
col++;
|
|
if (col >= 3) {
|
|
col = 0;
|
|
row++;
|
|
}
|
|
}
|
|
|
|
formatButton->addSettingsWidget(formatContainer);
|
|
|
|
cardsField = new QLineEdit(secondaryToolbar);
|
|
cardsField->setPlaceholderText(tr("Contains card..."));
|
|
cardsField->setMinimumWidth(140);
|
|
|
|
commandersField = new QLineEdit(secondaryToolbar);
|
|
commandersField->setPlaceholderText(tr("Commander..."));
|
|
commandersField->setMinimumWidth(140);
|
|
|
|
deckTagNameField = new QLineEdit(secondaryToolbar);
|
|
deckTagNameField->setPlaceholderText(tr("Tag..."));
|
|
deckTagNameField->setMinimumWidth(100);
|
|
|
|
// Deck size filter (collapsible)
|
|
deckSizeButton = new SettingsButtonWidget(secondaryToolbar);
|
|
deckSizeButton->setButtonText(tr("Deck Size"));
|
|
|
|
QWidget *sizeContainer = new QWidget(secondaryToolbar);
|
|
QHBoxLayout *sizeLayout = new QHBoxLayout(sizeContainer);
|
|
sizeLayout->setContentsMargins(4, 4, 4, 4);
|
|
|
|
minDeckSizeSpin = new QSpinBox(sizeContainer);
|
|
minDeckSizeSpin->setRange(0, 200);
|
|
minDeckSizeSpin->setValue(0);
|
|
|
|
minDeckSizeLogicCombo = new QComboBox(sizeContainer);
|
|
minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"});
|
|
minDeckSizeLogicCombo->setCurrentIndex(1);
|
|
|
|
cardsLabel = new QLabel(sizeContainer);
|
|
sizeLayout->addWidget(cardsLabel);
|
|
sizeLayout->addWidget(minDeckSizeSpin);
|
|
sizeLayout->addWidget(minDeckSizeLogicCombo);
|
|
|
|
deckSizeButton->addSettingsWidget(sizeContainer);
|
|
|
|
// Setup card name autocomplete
|
|
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
|
auto displayModel = new CardDatabaseDisplayModel(this);
|
|
displayModel->setSourceModel(cardDatabaseModel);
|
|
|
|
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
|
|
cardsField->setCompleter(cardSetup.completer);
|
|
commandersField->setCompleter(cardSetup.completer);
|
|
|
|
// Keep autocomplete working for both fields
|
|
connectCardCompleterSearch(cardsField, cardSetup);
|
|
connectCardCompleterSearch(commandersField, cardSetup);
|
|
|
|
// Assemble secondary toolbar
|
|
secondaryToolbarLayout->addWidget(bracketLabel);
|
|
secondaryToolbarLayout->addWidget(edhBracketCombo);
|
|
secondaryToolbarLayout->addWidget(formatButton);
|
|
secondaryToolbarLayout->addWidget(cardsField);
|
|
secondaryToolbarLayout->addWidget(commandersField);
|
|
secondaryToolbarLayout->addWidget(deckTagNameField);
|
|
secondaryToolbarLayout->addWidget(deckSizeButton);
|
|
secondaryToolbarLayout->addStretch();
|
|
}
|
|
|
|
void TabArchidekt::connectSignals()
|
|
{
|
|
// Advanced filters toggle
|
|
connect(advancedFiltersButton, &QPushButton::clicked, this,
|
|
[this](bool checked) { secondaryToolbar->setVisible(checked); });
|
|
|
|
// These trigger immediate search (no debounce needed)
|
|
connect(orderByCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
|
|
connect(orderDirButton, &QPushButton::clicked, [this](bool checked) {
|
|
orderDirButton->setText(checked ? tr("Desc.") : tr("Asc."));
|
|
doSearch();
|
|
});
|
|
|
|
connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
|
&CardsDisplaySettings::setArchidektPreviewCardSize);
|
|
|
|
// Search button triggers immediate search
|
|
connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate);
|
|
|
|
// These trigger search (but not text fields)
|
|
connect(logicalAndCheck, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
|
connect(packagesCheck, &QCheckBox::clicked, [this]() {
|
|
updatePackageModeState(packagesCheck->isChecked());
|
|
doSearch();
|
|
});
|
|
|
|
// Format filters trigger search
|
|
connect(edhBracketCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch);
|
|
connect(minDeckSizeSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
|
connect(minDeckSizeLogicCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch);
|
|
|
|
// Allow Enter key in text fields to trigger search
|
|
connect(nameField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
|
|
connect(ownerField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
|
|
connect(cardsField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
|
|
connect(commandersField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
|
|
connect(deckTagNameField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
|
|
|
|
// Format checkboxes trigger search
|
|
for (auto *formatCheck : formatChecks) {
|
|
connect(formatCheck, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
|
}
|
|
}
|
|
|
|
void TabArchidekt::updatePackageModeState(bool isPackageMode)
|
|
{
|
|
// Disable format-specific and commander-specific filters in package mode
|
|
for (auto *cb : formatChecks) {
|
|
cb->setEnabled(!isPackageMode);
|
|
}
|
|
|
|
edhBracketCombo->setEnabled(!isPackageMode);
|
|
if (isPackageMode) {
|
|
edhBracketCombo->setCurrentIndex(0);
|
|
}
|
|
|
|
commandersField->setEnabled(!isPackageMode);
|
|
deckTagNameField->setEnabled(!isPackageMode);
|
|
}
|
|
|
|
void TabArchidekt::retranslateUi()
|
|
{
|
|
sortByLabel->setText(tr("Sort by:"));
|
|
orderDirButton->setText(orderDirButton->isChecked() ? tr("Desc.") : tr("Asc."));
|
|
|
|
filterByLabel->setText(tr("Filter by:"));
|
|
|
|
logicalAndCheck->setText(tr("AND"));
|
|
logicalAndCheck->setToolTip(tr("Require ALL selected colors"));
|
|
|
|
nameField->setPlaceholderText(tr("Deck name..."));
|
|
ownerField->setPlaceholderText(tr("Owner..."));
|
|
packagesCheck->setText(tr("Packages"));
|
|
advancedFiltersButton->setText(tr("Advanced Filters"));
|
|
|
|
cardsField->setPlaceholderText(tr("Contains card..."));
|
|
commandersField->setPlaceholderText(tr("Commander..."));
|
|
deckTagNameField->setPlaceholderText(tr("Tag..."));
|
|
|
|
bracketLabel->setText(tr("Bracket:"));
|
|
// Index 0 is the "Any" placeholder added in setupFilterWidgets()
|
|
edhBracketCombo->setItemText(0, tr("Any"));
|
|
|
|
formatButton->setButtonText(tr("Formats"));
|
|
deckSizeButton->setButtonText(tr("Deck Size"));
|
|
cardsLabel->setText(tr("Cards:"));
|
|
minDeckSizeSpin->setSpecialValueText(tr("Any"));
|
|
minDeckSizeLogicCombo->setItemText(0, tr("Exact"));
|
|
|
|
searchButton->setText(tr("Search"));
|
|
retryButton->setText(tr("Retry"));
|
|
|
|
settingsButton->setToolTip(tr("Display Settings"));
|
|
}
|
|
|
|
QString TabArchidekt::buildSearchUrl()
|
|
{
|
|
QUrlQuery query;
|
|
|
|
// orderBy (field + direction)
|
|
QString field = orderByCombo->currentText();
|
|
if (!field.isEmpty()) {
|
|
bool desc = orderDirButton->isChecked();
|
|
QString final = desc ? "-" + field : field;
|
|
query.addQueryItem("orderBy", final);
|
|
}
|
|
|
|
// Colors
|
|
QStringList selectedColors;
|
|
for (QChar c : activeColors) {
|
|
selectedColors.append(c);
|
|
}
|
|
if (!selectedColors.isEmpty()) {
|
|
query.addQueryItem("colors", selectedColors.join(","));
|
|
}
|
|
|
|
// logicalAnd
|
|
if (logicalAndCheck->isChecked()) {
|
|
query.addQueryItem("logicalAnd", "true");
|
|
}
|
|
|
|
// Formats (disabled in package mode)
|
|
if (!packagesCheck->isChecked()) {
|
|
QStringList formatIds;
|
|
for (int i = 0; i < formatChecks.size(); ++i) {
|
|
if (formatChecks[i]->isChecked()) {
|
|
formatIds << QString::number(i + 1);
|
|
}
|
|
}
|
|
|
|
if (!formatIds.isEmpty()) {
|
|
query.addQueryItem("deckFormat", formatIds.join(","));
|
|
}
|
|
|
|
// edhBracket
|
|
if (edhBracketCombo->currentIndex() > 0) {
|
|
query.addQueryItem("edhBracket", edhBracketCombo->currentText());
|
|
}
|
|
}
|
|
|
|
// Package mode
|
|
if (packagesCheck->isChecked()) {
|
|
query.addQueryItem("packages", "true");
|
|
}
|
|
|
|
// Name
|
|
if (!nameField->text().isEmpty()) {
|
|
query.addQueryItem("name", nameField->text());
|
|
}
|
|
|
|
// Owner
|
|
if (!ownerField->text().isEmpty()) {
|
|
query.addQueryItem("ownerUsername", ownerField->text());
|
|
}
|
|
|
|
// Cards
|
|
if (!cardsField->text().isEmpty()) {
|
|
query.addQueryItem("cards", cardsField->text());
|
|
}
|
|
|
|
// Commander (disabled in package mode)
|
|
if (!packagesCheck->isChecked() && !commandersField->text().isEmpty()) {
|
|
query.addQueryItem("commanderName", commandersField->text());
|
|
}
|
|
|
|
// Deck tag (disabled in package mode)
|
|
if (!packagesCheck->isChecked() && !deckTagNameField->text().isEmpty()) {
|
|
query.addQueryItem("deckTagName", deckTagNameField->text());
|
|
}
|
|
|
|
// Page number (for infinite scroll)
|
|
query.addQueryItem("page", QString::number(currentPage));
|
|
|
|
// Min deck size
|
|
if (minDeckSizeSpin->value() != 0) {
|
|
query.addQueryItem("size", QString::number(minDeckSizeSpin->value()));
|
|
|
|
QString logic = "GTE";
|
|
// Index order matches setupFilterWidgets(): 0 = Exact, 1 = ≥, 2 = ≤
|
|
switch (minDeckSizeLogicCombo->currentIndex()) {
|
|
case 1:
|
|
logic = "GTE";
|
|
break;
|
|
case 2:
|
|
logic = "LTE";
|
|
break;
|
|
default:
|
|
logic = "";
|
|
break;
|
|
}
|
|
|
|
if (!logic.isEmpty()) {
|
|
query.addQueryItem("sizeLogic", logic);
|
|
}
|
|
}
|
|
|
|
QUrl url("https://archidekt.com/api/decks/v3/");
|
|
url.setQuery(query);
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
void TabArchidekt::doSearch()
|
|
{
|
|
// Don't debounce - only called by explicit user actions now
|
|
doSearchImmediate();
|
|
}
|
|
|
|
void TabArchidekt::doSearchImmediate()
|
|
{
|
|
searchDebounceTimer->stop();
|
|
currentPage = 1;
|
|
startRequest(QUrl(buildSearchUrl()), RequestKind::Search);
|
|
}
|
|
|
|
void TabArchidekt::loadNextPage()
|
|
{
|
|
if (activeReply || !isListMode || !listingsWidget || nextPageUrl.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Continue the accepted search, even if the user has edited filters without submitting them.
|
|
// Advance currentPage only after this response has been accepted, so failures can retry it.
|
|
startRequest(nextPageUrl, RequestKind::NextPage, currentPage + 1);
|
|
}
|
|
|
|
void TabArchidekt::actNavigatePage(QString url)
|
|
{
|
|
searchDebounceTimer->stop();
|
|
startRequest(QUrl(url), RequestKind::Deck);
|
|
}
|
|
|
|
void TabArchidekt::getTopDecks()
|
|
{
|
|
doSearchImmediate();
|
|
}
|
|
|
|
void TabArchidekt::processApiJson(QNetworkReply *reply)
|
|
{
|
|
if (reply != activeReply) {
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
activeReply = nullptr;
|
|
isLoadingMore = false;
|
|
reply->deleteLater();
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
showRequestError(reply->errorString());
|
|
return;
|
|
}
|
|
|
|
QByteArray responseData = reply->readAll();
|
|
QJsonParseError parseError;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &parseError);
|
|
|
|
if (parseError.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
|
|
showRequestError(tr("Invalid response from Archidekt."));
|
|
return;
|
|
}
|
|
|
|
QJsonObject jsonObj = jsonDoc.object();
|
|
if (requestKind != RequestKind::Deck) {
|
|
if (!jsonObj.value("results").isArray() ||
|
|
(!jsonObj.value("next").isNull() && !jsonObj.value("next").isString())) {
|
|
showRequestError(tr("Invalid deck listing from Archidekt."));
|
|
return;
|
|
}
|
|
for (const auto &value : jsonObj.value("results").toArray()) {
|
|
if (!value.isObject() || value.toObject().value("id").toInt() <= 0) {
|
|
showRequestError(tr("Invalid deck listing from Archidekt."));
|
|
return;
|
|
}
|
|
}
|
|
currentPage = requestedPage;
|
|
const QString next = jsonObj.value("next").toString();
|
|
nextPageUrl = next.isEmpty() ? QUrl() : reply->url().resolved(QUrl(next));
|
|
// An empty or self-repeating page cannot make forward progress.
|
|
if (jsonObj.value("results").toArray().isEmpty() || nextPageUrl == reply->url()) {
|
|
nextPageUrl.clear();
|
|
}
|
|
requestStatusLabel->setText(
|
|
currentPage == 1 && jsonObj.value("results").toArray().isEmpty() ? tr("No decks found.") : QString());
|
|
requestStatusLabel->setVisible(!requestStatusLabel->text().isEmpty());
|
|
processTopDecksResponse(jsonObj);
|
|
} else {
|
|
if (jsonObj.value("id").toInt() <= 0 || !jsonObj.value("cards").isArray()) {
|
|
showRequestError(tr("Invalid deck from Archidekt."));
|
|
return;
|
|
}
|
|
requestStatusLabel->hide();
|
|
processDeckResponse(jsonObj);
|
|
}
|
|
}
|
|
|
|
void TabArchidekt::processTopDecksResponse(QJsonObject reply)
|
|
{
|
|
ArchidektDeckListingApiResponse deckData;
|
|
deckData.fromJson(reply);
|
|
|
|
// An accepted listing restores list mode; a failed deck navigation leaves the listing usable.
|
|
isListMode = true;
|
|
|
|
// New search → clear everything
|
|
if (requestKind == RequestKind::Search) {
|
|
listingsWidget = nullptr;
|
|
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
|
|
|
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
|
|
|
connect(listingsWidget, &ArchidektApiResponseDeckListingsDisplayWidget::requestNavigation, this,
|
|
&TabArchidekt::actNavigatePage);
|
|
|
|
resultsLayout->addWidget(listingsWidget);
|
|
return;
|
|
}
|
|
|
|
// Infinite scroll → append
|
|
if (listingsWidget) {
|
|
listingsWidget->append(deckData);
|
|
}
|
|
}
|
|
|
|
void TabArchidekt::processDeckResponse(QJsonObject reply)
|
|
{
|
|
ArchidektApiResponseDeck deckData;
|
|
deckData.fromJson(reply);
|
|
|
|
// We're in single deck mode - disable infinite scroll
|
|
isListMode = false;
|
|
nextPageUrl.clear();
|
|
|
|
// Clear existing results for single deck view
|
|
listingsWidget = nullptr;
|
|
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
|
|
|
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
|
connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage);
|
|
connect(display, &ArchidektApiResponseDeckDisplayWidget::requestSearch, this, &TabArchidekt::doSearch);
|
|
connect(display, &ArchidektApiResponseDeckDisplayWidget::openInDeckEditor, tabSupervisor,
|
|
&TabSupervisor::openDeckInNewTab);
|
|
resultsLayout->addWidget(display);
|
|
}
|
|
|
|
void TabArchidekt::prettyPrintJson(const QJsonValue &value, int indentLevel)
|
|
{
|
|
const QString indent(indentLevel * 2, ' ');
|
|
|
|
if (value.isObject()) {
|
|
QJsonObject obj = value.toObject();
|
|
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
|
qInfo().noquote() << indent + it.key() + ":";
|
|
prettyPrintJson(it.value(), indentLevel + 1);
|
|
}
|
|
} else if (value.isArray()) {
|
|
QJsonArray array = value.toArray();
|
|
for (int i = 0; i < array.size(); ++i) {
|
|
qInfo().noquote() << indent + QString("[%1]:").arg(i);
|
|
prettyPrintJson(array[i], indentLevel + 1);
|
|
}
|
|
} else if (value.isString()) {
|
|
qInfo().noquote() << indent + "\"" + value.toString() + "\"";
|
|
} else if (value.isDouble()) {
|
|
qInfo().noquote() << indent + QString::number(value.toDouble());
|
|
} else if (value.isBool()) {
|
|
qInfo().noquote() << indent + (value.toBool() ? "true" : "false");
|
|
} else if (value.isNull()) {
|
|
qInfo().noquote() << indent + "null";
|
|
}
|
|
}
|