// NOTE: You must add the following members to tab_archidekt.h: // QComboBox* edhBracketCombo; // QLineEdit* deckTagNameField; // QComboBox* orderByCombo; // QCheckBox* packagesCheck; // QSpinBox* pageSpin; #include "tab_archidekt.h" #include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../tab_supervisor.h" #include "api_response/archidekt_deck_listing_api_response.h" #include "display/archidekt_api_response_deck_display_widget.h" #include "display/archidekt_api_response_deck_listings_display_widget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { networkManager = new QNetworkAccessManager(this); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) networkManager->setTransferTimeout(); // Use Qt's default timeout #endif networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *))); container = new QWidget(this); mainLayout = new QVBoxLayout(container); mainLayout->setContentsMargins(0, 0, 0, 0); container->setLayout(mainLayout); navigationContainer = new QWidget(container); navigationContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); navigationLayout = new QHBoxLayout(navigationContainer); navigationLayout->setSpacing(3); navigationContainer->setLayout(navigationLayout); // Sort by orderByCombo = new QComboBox(navigationContainer); orderByCombo->addItems({"name", "updatedAt", "createdAt", "viewCount", "size", "edhBracket"}); orderByCombo->setCurrentText("updatedAt"); // Pre-select updatedAt // Asc/Desc toggle orderDirButton = new QPushButton(tr("Desc."), navigationContainer); orderDirButton->setCheckable(true); // checked = DESC, unchecked = ASC orderDirButton->setChecked(true); connect(orderByCombo, &QComboBox::currentTextChanged, this, [this]() { doSearch(); }); connect(orderDirButton, &QPushButton::clicked, this, [this](bool checked) { orderDirButton->setText(checked ? tr("Desc.") : tr("Asc.")); doSearch(); }); // Colors QHBoxLayout *colorLayout = new QHBoxLayout(); QString colorIdentity = "WUBRG"; // Optionally include "C" for colorless once we have a symbol for it for (const QChar &color : colorIdentity) { auto *manaSymbol = new ManaSymbolWidget(navigationContainer, color, false, true); manaSymbol->setFixedWidth(25); 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("Require ALL colors", navigationContainer); // Formats formatLabel = new QLabel(this); formatSettingsWidget = new SettingsButtonWidget(this); QStringList formatNames = {"Standard", "Modern", "Commander", "Legacy", "Vintage", "Pauper", "Custom", "Frontier", "Future Std", "Penny Dreadful", "1v1 Commander", "Dual Commander", "Brawl"}; for (int i = 0; i < formatNames.size(); ++i) { QCheckBox *formatCheckBox = new QCheckBox(formatNames[i], navigationContainer); connect(formatCheckBox, &QCheckBox::clicked, this, [this]() { doSearch(); }); formatChecks << formatCheckBox; formatSettingsWidget->addSettingsWidget(formatCheckBox); } // EDH Bracket edhBracketCombo = new QComboBox(navigationContainer); edhBracketCombo->addItem(tr("Any Bracket")); edhBracketCombo->addItems({"1", "2", "3", "4", "5"}); connect(edhBracketCombo, &QComboBox::currentTextChanged, this, [this]() { doSearch(); }); // Search for Card Packages instead of Decks packagesCheck = new QCheckBox("Packages", navigationContainer); connect(packagesCheck, &QCheckBox::clicked, this, [this]() { bool disable = packagesCheck->isChecked(); for (auto *cb : formatChecks) cb->setEnabled(!disable); commandersField->setEnabled(!disable); deckTagNameField->setEnabled(!disable); edhBracketCombo->setCurrentIndex(0); edhBracketCombo->setEnabled(!disable); doSearch(); }); // Deck Name nameField = new QLineEdit(navigationContainer); nameField->setPlaceholderText(tr("Deck name contains...")); // Owner Name ownerField = new QLineEdit(navigationContainer); ownerField->setPlaceholderText(tr("Owner name contains...")); // Contained cards cardsField = new QLineEdit(navigationContainer); cardsField->setPlaceholderText("Cards (comma separated)"); // Commanders commandersField = new QLineEdit(navigationContainer); commandersField->setPlaceholderText("Commanders (comma separated)"); // DB supplemented card search auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); auto *searchModel = new CardSearchModel(displayModel, this); auto *proxyModel = new CardCompleterProxyModel(this); proxyModel->setSourceModel(searchModel); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel->setFilterRole(Qt::DisplayRole); auto *completer = new QCompleter(proxyModel, this); completer->setCompletionRole(Qt::DisplayRole); completer->setCompletionMode(QCompleter::PopupCompletion); completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setFilterMode(Qt::MatchContains); completer->setMaxVisibleItems(10); cardsField->setCompleter(completer); commandersField->setCompleter(completer); connect(cardsField, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); if (!text.isEmpty()) completer->complete(); }); connect(commandersField, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) { QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); if (!text.isEmpty()) completer->complete(); }); // Tag Name deckTagNameField = new QLineEdit(navigationContainer); deckTagNameField->setPlaceholderText("Deck tag"); connect(deckTagNameField, &QLineEdit::textChanged, this, [this]() { doSearch(); }); // Search button searchPushButton = new QPushButton(navigationContainer); searchPushButton->setText("Search"); connect(searchPushButton, &QPushButton::clicked, this, [this]() { doSearch(); }); // Card Size settings settingsButton = new SettingsButtonWidget(this); cardSizeSlider = new CardSizeWidget(this); settingsButton->addSettingsWidget(cardSizeSlider); // Min deck size minDeckSizeLabel = new QLabel(navigationContainer); minDeckSizeSpin = new QSpinBox(navigationContainer); minDeckSizeSpin->setRange(1, 200); minDeckSizeSpin->setValue(60); // Page number pageLabel = new QLabel(navigationContainer); pageSpin = new QSpinBox(navigationContainer); pageSpin->setRange(1, 9999); pageSpin->setValue(1); connect(pageSpin, &QSpinBox::valueChanged, this, [this]() { doSearch(); }); // Page display currentPageDisplay = new QWidget(container); currentPageLayout = new QVBoxLayout(currentPageDisplay); currentPageLayout->setContentsMargins(0, 0, 0, 0); currentPageDisplay->setLayout(currentPageLayout); // Layout composition // Sort section navigationLayout->addWidget(orderByCombo); navigationLayout->addWidget(orderDirButton); // Colors section navigationLayout->addLayout(colorLayout); navigationLayout->addWidget(logicalAndCheck); // Formats section navigationLayout->addWidget(formatSettingsWidget); navigationLayout->addWidget(formatLabel); // EDH Bracket navigationLayout->addWidget(edhBracketCombo); // Packages toggle navigationLayout->addWidget(packagesCheck); // Deck name navigationLayout->addWidget(nameField); // Owner name navigationLayout->addWidget(ownerField); // Contained cards navigationLayout->addWidget(cardsField); // Commanders navigationLayout->addWidget(commandersField); // Deck tag navigationLayout->addWidget(deckTagNameField); // Search button navigationLayout->addWidget(searchPushButton); // Card size settings navigationLayout->addWidget(settingsButton); // Page size navigationLayout->addWidget(minDeckSizeLabel); navigationLayout->addWidget(minDeckSizeSpin); // Page number navigationLayout->addWidget(pageLabel); navigationLayout->addWidget(pageSpin); mainLayout->addWidget(navigationContainer); mainLayout->addWidget(currentPageDisplay); // Ensure navigation stays at the top and currentPageDisplay takes remaining space mainLayout->setStretch(0, 0); // navigationContainer gets minimum space mainLayout->setStretch(1, 1); // currentPageDisplay expands as much as possible setCentralWidget(container); TabArchidekt::retranslateUi(); getTopDecks(); } void TabArchidekt::retranslateUi() { searchPushButton->setText(tr("Search")); formatLabel->setText(tr("Formats")); minDeckSizeLabel->setText(tr("Min. # of Cards:")); pageLabel->setText(tr("Page:")); } 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 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 (!packagesCheck->isChecked()) { if (!edhBracketCombo->currentText().isEmpty()) { if (edhBracketCombo->currentText() != tr("Any Bracket")) { query.addQueryItem("edhBracket", edhBracketCombo->currentText()); } } } // Search for card packages instead of decks 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("cardName", cardsField->text()); } // Commander Name if (!packagesCheck->isChecked()) { if (!commandersField->text().isEmpty()) { query.addQueryItem("commanderName", commandersField->text()); } } // deckTagName if (!packagesCheck->isChecked()) { if (!deckTagNameField->text().isEmpty()) { query.addQueryItem("deckTagName", deckTagNameField->text()); } } // page number if (pageSpin->value() <= 1) { query.addQueryItem("page", QString::number(pageSpin->value())); } // Min deck size if (minDeckSizeSpin->value() != 50) { query.addQueryItem("size", QString::number(minDeckSizeSpin->value())); } // build final URL QUrl url("https://archidekt.com/api/decks/v3/"); url.setQuery(query); return url.toString(); } void TabArchidekt::doSearch() { QString url = buildSearchUrl(); QNetworkRequest req{QUrl(url)}; networkManager->get(req); } void TabArchidekt::actNavigatePage(QString url) { QNetworkRequest request{QUrl(url)}; networkManager->get(request); } void TabArchidekt::getTopDecks() { QNetworkRequest request{QUrl(buildSearchUrl())}; networkManager->get(request); } void TabArchidekt::processApiJson(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { qDebug() << "Network error occurred:" << reply->errorString(); reply->deleteLater(); return; } QByteArray responseData = reply->readAll(); QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); if (!jsonDoc.isObject()) { qDebug() << "Invalid JSON response received."; reply->deleteLater(); return; } QJsonObject jsonObj = jsonDoc.object(); // Get the actual URL from the reply QString responseUrl = reply->url().toString(); // Check if the response URL matches a commander request if (responseUrl.startsWith("https://archidekt.com/api/decks/v3/")) { processTopDecksResponse(jsonObj); } else if (responseUrl.startsWith("https://archidekt.com/api/decks/")) { processDeckResponse(jsonObj); } else { prettyPrintJson(jsonObj, 4); } reply->deleteLater(); } void TabArchidekt::processTopDecksResponse(QJsonObject reply) { ArchidektDeckListingApiResponse deckData; deckData.fromJson(reply); // **Remove previous page display to prevent stacking** if (currentPageDisplay) { mainLayout->removeWidget(currentPageDisplay); delete currentPageDisplay; currentPageDisplay = nullptr; } // **Create new currentPageDisplay** currentPageDisplay = new QWidget(container); currentPageLayout = new QVBoxLayout(currentPageDisplay); currentPageDisplay->setLayout(currentPageLayout); auto display = new ArchidektApiResponseDeckListingsDisplayWidget(currentPageDisplay, deckData, cardSizeSlider); connect(display, &ArchidektApiResponseDeckListingsDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage); currentPageLayout->addWidget(display); mainLayout->addWidget(currentPageDisplay); // **Ensure layout stays correct** mainLayout->setStretch(0, 0); // Keep navigationContainer at the top mainLayout->setStretch(1, 1); // Make sure currentPageDisplay takes remaining space } void TabArchidekt::processDeckResponse(QJsonObject reply) { ArchidektApiResponseDeck deckData; deckData.fromJson(reply); // **Remove previous page display to prevent stacking** if (currentPageDisplay) { mainLayout->removeWidget(currentPageDisplay); delete currentPageDisplay; currentPageDisplay = nullptr; } // **Create new currentPageDisplay** currentPageDisplay = new QWidget(container); currentPageLayout = new QVBoxLayout(currentPageDisplay); currentPageDisplay->setLayout(currentPageLayout); auto display = new ArchidektApiResponseDeckDisplayWidget(currentPageDisplay, deckData, cardSizeSlider); connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage); connect(display, &ArchidektApiResponseDeckDisplayWidget::openInDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab); currentPageLayout->addWidget(display); mainLayout->addWidget(currentPageDisplay); // **Ensure layout stays correct** mainLayout->setStretch(0, 0); // Keep navigationContainer at the top mainLayout->setStretch(1, 1); // Make sure currentPageDisplay takes remaining space } void TabArchidekt::prettyPrintJson(const QJsonValue &value, int indentLevel) { const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing if (value.isObject()) { QJsonObject obj = value.toObject(); for (auto it = obj.begin(); it != obj.end(); ++it) { qDebug().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) { qDebug().noquote() << indent + QString("[%1]:").arg(i); prettyPrintJson(array[i], indentLevel + 1); } } else if (value.isString()) { qDebug().noquote() << indent + "\"" + value.toString() + "\""; } else if (value.isDouble()) { qDebug().noquote() << indent + QString::number(value.toDouble()); } else if (value.isBool()) { qDebug().noquote() << indent + (value.toBool() ? "true" : "false"); } else if (value.isNull()) { qDebug().noquote() << indent + "null"; } }