mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-03-21 17:55:21 -05:00
[TabArchidekt] Cleaner filters, infinite scrolling, and a "go back button" (#6545)
* [TabArchidekt] Cleaner filters, infinite scrolling, and a "go back button" Took 46 minutes Took 5 seconds * Fix infinite scroll triggering in detail view. Took 25 minutes Took 3 seconds * Use setLabelText() so it's white Took 2 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
3c48d92663
commit
12b5525a2d
|
|
@ -20,12 +20,22 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
|
|||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
openInEditorButton = new QPushButton(this);
|
||||
layout->addWidget(openInEditorButton);
|
||||
navigationContainer = new QWidget(this);
|
||||
navigationContainerLayout = new QHBoxLayout(navigationContainer);
|
||||
|
||||
homeButton = new QPushButton(navigationContainer);
|
||||
navigationContainerLayout->addWidget(homeButton);
|
||||
|
||||
connect(homeButton, &QPushButton::clicked, this, &ArchidektApiResponseDeckDisplayWidget::requestSearch);
|
||||
|
||||
openInEditorButton = new QPushButton(navigationContainer);
|
||||
navigationContainerLayout->addWidget(openInEditorButton);
|
||||
|
||||
connect(openInEditorButton, &QPushButton::clicked, this,
|
||||
&ArchidektApiResponseDeckDisplayWidget::actOpenInDeckEditor);
|
||||
|
||||
layout->addWidget(navigationContainer);
|
||||
|
||||
displayOptionsWidget = new VisualDeckDisplayOptionsWidget(this);
|
||||
layout->addWidget(displayOptionsWidget);
|
||||
|
||||
|
|
@ -80,6 +90,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
|
|||
|
||||
void ArchidektApiResponseDeckDisplayWidget::retranslateUi()
|
||||
{
|
||||
homeButton->setText(tr("Back to results"));
|
||||
openInEditorButton->setText(tr("Open Deck in Deck Editor"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ signals:
|
|||
* @param url URL of the deck on Archidekt.
|
||||
*/
|
||||
void requestNavigation(QString url);
|
||||
void requestSearch();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the deck should be opened in the deck editor.
|
||||
|
|
@ -102,9 +103,12 @@ private slots:
|
|||
void onGroupCriteriaChange(const QString &activeGroupCriteria);
|
||||
|
||||
private:
|
||||
ArchidektApiResponseDeck response; ///< API deck data container
|
||||
CardSizeWidget *cardSizeSlider; ///< Slider for adjusting card sizes
|
||||
QVBoxLayout *layout; ///< Main vertical layout
|
||||
ArchidektApiResponseDeck response; ///< API deck data container
|
||||
CardSizeWidget *cardSizeSlider; ///< Slider for adjusting card sizes
|
||||
QVBoxLayout *layout; ///< Main vertical layout
|
||||
QWidget *navigationContainer;
|
||||
QHBoxLayout *navigationContainerLayout;
|
||||
QPushButton *homeButton;
|
||||
QPushButton *openInEditorButton; ///< Button to open deck in editor
|
||||
VisualDeckDisplayOptionsWidget *displayOptionsWidget; ///< Controls grouping/sorting/display
|
||||
QScrollArea *scrollArea; ///< Scrollable area for deck zones
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ void ArchidektApiResponseDeckEntryDisplayWidget::updateScaledPreview()
|
|||
int textMaxWidth = int(newWidth * 0.7); // allow 70% of width for text
|
||||
QFontMetrics fm(previewWidget->topLeftLabel->font());
|
||||
QString elided = fm.elidedText(response.getName(), Qt::ElideRight, textMaxWidth);
|
||||
previewWidget->topLeftLabel->setText(elided);
|
||||
previewWidget->topLeftLabel->setLabelText(elided);
|
||||
previewWidget->topLeftLabel->setToolTip(response.getName());
|
||||
|
||||
setFixedWidth(newWidth);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,20 @@ ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsD
|
|||
layout->addWidget(flowWidget);
|
||||
}
|
||||
|
||||
void ArchidektApiResponseDeckListingsDisplayWidget::append(const ArchidektDeckListingApiResponse &data)
|
||||
{
|
||||
for (const auto &deckListing : data.results) {
|
||||
auto cardListDisplayWidget =
|
||||
new ArchidektApiResponseDeckEntryDisplayWidget(this, deckListing, imageNetworkManager);
|
||||
cardListDisplayWidget->setScaleFactor(cardSizeSlider->getSlider()->value());
|
||||
connect(cardListDisplayWidget, &ArchidektApiResponseDeckEntryDisplayWidget::requestNavigation, this,
|
||||
&ArchidektApiResponseDeckListingsDisplayWidget::requestNavigation);
|
||||
connect(cardSizeSlider->getSlider(), &QSlider::valueChanged, cardListDisplayWidget,
|
||||
&ArchidektApiResponseDeckEntryDisplayWidget::setScaleFactor);
|
||||
flowWidget->addWidget(cardListDisplayWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void ArchidektApiResponseDeckListingsDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public:
|
|||
explicit ArchidektApiResponseDeckListingsDisplayWidget(QWidget *parent,
|
||||
ArchidektDeckListingApiResponse response,
|
||||
CardSizeWidget *cardSizeSlider);
|
||||
void append(const ArchidektDeckListingApiResponse &data);
|
||||
|
||||
/**
|
||||
* @brief Ensures FlowWidget layout properly recomputes on resize.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
|
@ -18,129 +21,258 @@
|
|||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
||||
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||
#include <version_string.h>
|
||||
|
||||
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
||||
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
||||
: Tab(_tabSupervisor), currentPage(1), isLoadingMore(false), isListMode(true)
|
||||
{
|
||||
// Initialize network
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
networkManager->setTransferTimeout(); // Use Qt's default timeout
|
||||
networkManager->setTransferTimeout();
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *)));
|
||||
connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson);
|
||||
|
||||
// Initialize debounce timer
|
||||
searchDebounceTimer = new QTimer(this);
|
||||
searchDebounceTimer->setSingleShot(true); // We only want it to fire once after inactivity
|
||||
searchDebounceTimer->setInterval(300); // 300ms debounce
|
||||
searchDebounceTimer->setSingleShot(true);
|
||||
searchDebounceTimer->setInterval(300);
|
||||
connect(searchDebounceTimer, &QTimer::timeout, this, &TabArchidekt::doSearchImmediate);
|
||||
|
||||
connect(searchDebounceTimer, &QTimer::timeout, this, [this]() { doSearchImmediate(); });
|
||||
initializeUi();
|
||||
setupFilterWidgets();
|
||||
connectSignals();
|
||||
retranslateUi();
|
||||
|
||||
getTopDecks();
|
||||
}
|
||||
|
||||
void TabArchidekt::initializeUi()
|
||||
{
|
||||
// Main container
|
||||
container = new QWidget(this);
|
||||
mainLayout = new QVBoxLayout(container);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
container->setLayout(mainLayout);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
navigationContainer = new QWidget(container);
|
||||
navigationContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
navigationLayout = new QHBoxLayout(navigationContainer);
|
||||
navigationLayout->setSpacing(3);
|
||||
navigationContainer->setLayout(navigationLayout);
|
||||
// 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 by
|
||||
|
||||
orderByCombo = new QComboBox(navigationContainer);
|
||||
// Sort controls
|
||||
sortByLabel = new QLabel(primaryToolbar);
|
||||
orderByCombo = new QComboBox(primaryToolbar);
|
||||
orderByCombo->addItems({"name", "updatedAt", "createdAt", "viewCount", "size", "edhBracket"});
|
||||
orderByCombo->setCurrentText("updatedAt"); // Pre-select updatedAt
|
||||
orderByCombo->setCurrentText("updatedAt");
|
||||
orderByCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
|
||||
// Asc/Desc toggle
|
||||
orderDirButton = new QPushButton(tr("Desc."), navigationContainer);
|
||||
orderDirButton->setCheckable(true); // checked = DESC, unchecked = ASC
|
||||
orderDirButton = new QPushButton(tr("Desc."), primaryToolbar);
|
||||
orderDirButton->setCheckable(true);
|
||||
orderDirButton->setChecked(true);
|
||||
orderDirButton->setFixedWidth(60);
|
||||
|
||||
connect(orderByCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::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
|
||||
// Color filter (inline)
|
||||
QWidget *colorWidget = new QWidget(primaryToolbar);
|
||||
QHBoxLayout *colorLayout = new QHBoxLayout(colorWidget);
|
||||
colorLayout->setContentsMargins(0, 0, 0, 0);
|
||||
colorLayout->setSpacing(2);
|
||||
|
||||
QString colorIdentity = "WUBRG";
|
||||
for (const QChar &color : colorIdentity) {
|
||||
auto *manaSymbol = new ManaSymbolWidget(navigationContainer, color, false, true);
|
||||
manaSymbol->setFixedWidth(25);
|
||||
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) {
|
||||
if (active)
|
||||
activeColors.insert(c);
|
||||
} else {
|
||||
else
|
||||
activeColors.remove(c);
|
||||
}
|
||||
doSearch();
|
||||
});
|
||||
}
|
||||
|
||||
logicalAndCheck = new QCheckBox("Require ALL colors", navigationContainer);
|
||||
logicalAndCheck = new QCheckBox(tr("AND"), primaryToolbar);
|
||||
logicalAndCheck->setToolTip(tr("Require ALL selected colors"));
|
||||
|
||||
// Formats
|
||||
// Common search fields
|
||||
nameField = new QLineEdit(primaryToolbar);
|
||||
nameField->setPlaceholderText(tr("Deck name..."));
|
||||
nameField->setMinimumWidth(150);
|
||||
|
||||
formatLabel = new QLabel(this);
|
||||
ownerField = new QLineEdit(primaryToolbar);
|
||||
ownerField->setPlaceholderText(tr("Owner..."));
|
||||
ownerField->setMinimumWidth(120);
|
||||
|
||||
formatSettingsWidget = new SettingsButtonWidget(this);
|
||||
// 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().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 = new QScrollArea(container);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
|
||||
resultsContainer = new QWidget();
|
||||
resultsLayout = new QVBoxLayout(resultsContainer);
|
||||
resultsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
resultsLayout->setSpacing(0);
|
||||
|
||||
scrollArea->setWidget(resultsContainer);
|
||||
|
||||
scrollArea->viewport()->installEventFilter(this);
|
||||
|
||||
mainLayout->addWidget(primaryToolbar);
|
||||
mainLayout->addWidget(secondaryToolbar);
|
||||
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
|
||||
auto *bracketLabel = new QLabel(tr("Bracket:"), secondaryToolbar);
|
||||
edhBracketCombo = new QComboBox(secondaryToolbar);
|
||||
edhBracketCombo->addItem(tr("Any"));
|
||||
edhBracketCombo->addItems({"1", "2", "3", "4", "5"});
|
||||
edhBracketCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
|
||||
// Format filter (collapsible)
|
||||
formatButton = new SettingsButtonWidget(secondaryToolbar);
|
||||
formatButton->setButtonText(tr("Formats"));
|
||||
formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced"));
|
||||
|
||||
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"};
|
||||
|
||||
for (int i = 0; i < formatNames.size(); ++i) {
|
||||
QCheckBox *formatCheckBox = new QCheckBox(formatNames[i], navigationContainer);
|
||||
connect(formatCheckBox, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
||||
int row = 0, col = 0;
|
||||
for (const QString &formatName : formatNames) {
|
||||
auto *formatCheckBox = new QCheckBox(formatName, formatContainer);
|
||||
formatChecks << formatCheckBox;
|
||||
formatSettingsWidget->addSettingsWidget(formatCheckBox);
|
||||
formatLayout->addWidget(formatCheckBox, row, col);
|
||||
connect(formatCheckBox, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
||||
|
||||
col++;
|
||||
if (col >= 3) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
// EDH Bracket
|
||||
edhBracketCombo = new QComboBox(navigationContainer);
|
||||
edhBracketCombo->addItem(tr("Any Bracket"));
|
||||
edhBracketCombo->addItems({"1", "2", "3", "4", "5"});
|
||||
formatButton->addSettingsWidget(formatContainer);
|
||||
|
||||
connect(edhBracketCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
|
||||
cardsField = new QLineEdit(secondaryToolbar);
|
||||
cardsField->setPlaceholderText(tr("Contains card..."));
|
||||
cardsField->setMinimumWidth(140);
|
||||
|
||||
// Search for Card Packages instead of Decks
|
||||
packagesCheck = new QCheckBox("Packages", navigationContainer);
|
||||
commandersField = new QLineEdit(secondaryToolbar);
|
||||
commandersField->setPlaceholderText(tr("Commander..."));
|
||||
commandersField->setMinimumWidth(140);
|
||||
|
||||
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();
|
||||
});
|
||||
deckTagNameField = new QLineEdit(secondaryToolbar);
|
||||
deckTagNameField->setPlaceholderText(tr("Tag..."));
|
||||
deckTagNameField->setMinimumWidth(100);
|
||||
|
||||
// Deck Name
|
||||
nameField = new QLineEdit(navigationContainer);
|
||||
nameField->setPlaceholderText(tr("Deck name contains..."));
|
||||
// Deck size filter (collapsible)
|
||||
deckSizeButton = new SettingsButtonWidget(secondaryToolbar);
|
||||
deckSizeButton->setButtonText(tr("Deck Size"));
|
||||
|
||||
// Owner Name
|
||||
ownerField = new QLineEdit(navigationContainer);
|
||||
ownerField->setPlaceholderText(tr("Owner name contains..."));
|
||||
QWidget *sizeContainer = new QWidget(secondaryToolbar);
|
||||
QHBoxLayout *sizeLayout = new QHBoxLayout(sizeContainer);
|
||||
sizeLayout->setContentsMargins(4, 4, 4, 4);
|
||||
|
||||
// Contained cards
|
||||
cardsField = new QLineEdit(navigationContainer);
|
||||
cardsField->setPlaceholderText("Deck contains card...");
|
||||
minDeckSizeSpin = new QSpinBox(sizeContainer);
|
||||
minDeckSizeSpin->setSpecialValueText(tr("Any"));
|
||||
minDeckSizeSpin->setRange(0, 200);
|
||||
minDeckSizeSpin->setValue(0);
|
||||
|
||||
// Commanders
|
||||
commandersField = new QLineEdit(navigationContainer);
|
||||
commandersField->setPlaceholderText("Deck has commander...");
|
||||
minDeckSizeLogicCombo = new QComboBox(sizeContainer);
|
||||
minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"});
|
||||
minDeckSizeLogicCombo->setCurrentIndex(1);
|
||||
|
||||
// DB supplemented card search
|
||||
sizeLayout->addWidget(new QLabel(tr("Cards:"), sizeContainer));
|
||||
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);
|
||||
|
|
@ -161,144 +293,119 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
|||
cardsField->setCompleter(completer);
|
||||
commandersField->setCompleter(completer);
|
||||
|
||||
connect(cardsField, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults);
|
||||
|
||||
// Keep autocomplete working for both fields
|
||||
connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) {
|
||||
searchModel->updateSearchResults(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) {
|
||||
searchModel->updateSearchResults(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");
|
||||
// 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();
|
||||
}
|
||||
|
||||
connect(deckTagNameField, &QLineEdit::textChanged, this, &TabArchidekt::doSearch);
|
||||
void TabArchidekt::connectSignals()
|
||||
{
|
||||
// Advanced filters toggle
|
||||
connect(advancedFiltersButton, &QPushButton::clicked, this,
|
||||
[this](bool checked) { secondaryToolbar->setVisible(checked); });
|
||||
|
||||
// Search button
|
||||
searchPushButton = new QPushButton(navigationContainer);
|
||||
searchPushButton->setText("Search");
|
||||
// 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(searchPushButton, &QPushButton::clicked, this, &TabArchidekt::doSearch);
|
||||
|
||||
// Card Size settings
|
||||
settingsButton = new SettingsButtonWidget(this);
|
||||
cardSizeSlider = new CardSizeWidget(this, nullptr, SettingsCache::instance().getArchidektPreviewSize());
|
||||
connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(),
|
||||
&SettingsCache::setArchidektPreviewCardSize);
|
||||
settingsButton->addSettingsWidget(cardSizeSlider);
|
||||
|
||||
// Min deck size
|
||||
minDeckSizeLabel = new QLabel(navigationContainer);
|
||||
// Search button triggers immediate search
|
||||
connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate);
|
||||
|
||||
minDeckSizeSpin = new QSpinBox(navigationContainer);
|
||||
minDeckSizeSpin->setSpecialValueText(tr("Disabled"));
|
||||
minDeckSizeSpin->setRange(0, 200);
|
||||
minDeckSizeSpin->setValue(0);
|
||||
|
||||
// Size logic
|
||||
minDeckSizeLogicCombo = new QComboBox(navigationContainer);
|
||||
minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"}); // Exact = unset, ≥ = GTE, ≤ = LTE
|
||||
minDeckSizeLogicCombo->setCurrentIndex(1); // default GTE
|
||||
// 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::currentTextChanged, this, &TabArchidekt::doSearch);
|
||||
connect(minDeckSizeSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
||||
connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
|
||||
|
||||
// Page number
|
||||
pageLabel = new QLabel(navigationContainer);
|
||||
// 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);
|
||||
|
||||
pageSpin = new QSpinBox(navigationContainer);
|
||||
pageSpin->setRange(1, 9999);
|
||||
pageSpin->setValue(1);
|
||||
// Format checkboxes trigger search
|
||||
for (auto *formatCheck : formatChecks) {
|
||||
connect(formatCheck, &QCheckBox::clicked, this, &TabArchidekt::doSearch);
|
||||
}
|
||||
}
|
||||
|
||||
connect(pageSpin, qOverload<int>(&QSpinBox::valueChanged), 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);
|
||||
}
|
||||
|
||||
// Page display
|
||||
currentPageDisplay = new QWidget(container);
|
||||
currentPageLayout = new QVBoxLayout(currentPageDisplay);
|
||||
currentPageLayout->setContentsMargins(0, 0, 0, 0);
|
||||
currentPageDisplay->setLayout(currentPageLayout);
|
||||
edhBracketCombo->setEnabled(!isPackageMode);
|
||||
if (isPackageMode) {
|
||||
edhBracketCombo->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Min. # of cards in deck
|
||||
navigationLayout->addWidget(minDeckSizeLabel);
|
||||
navigationLayout->addWidget(minDeckSizeSpin);
|
||||
navigationLayout->addWidget(minDeckSizeLogicCombo);
|
||||
|
||||
// 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();
|
||||
commandersField->setEnabled(!isPackageMode);
|
||||
deckTagNameField->setEnabled(!isPackageMode);
|
||||
}
|
||||
|
||||
void TabArchidekt::retranslateUi()
|
||||
{
|
||||
searchPushButton->setText(tr("Search"));
|
||||
formatLabel->setText(tr("Formats"));
|
||||
minDeckSizeLabel->setText(tr("Min. # of Cards:"));
|
||||
pageLabel->setText(tr("Page:"));
|
||||
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..."));
|
||||
|
||||
formatButton->setButtonText(tr("Formats"));
|
||||
deckSizeButton->setButtonText(tr("Deck Size"));
|
||||
|
||||
searchButton->setText(tr("Search"));
|
||||
|
||||
settingsButton->setToolTip(tr("Display Settings"));
|
||||
}
|
||||
|
||||
QString TabArchidekt::buildSearchUrl()
|
||||
|
|
@ -306,13 +413,11 @@ 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);
|
||||
}
|
||||
QString field = orderByCombo->currentText();
|
||||
if (!field.isEmpty()) {
|
||||
bool desc = orderDirButton->isChecked();
|
||||
QString final = desc ? "-" + field : field;
|
||||
query.addQueryItem("orderBy", final);
|
||||
}
|
||||
|
||||
// Colors
|
||||
|
|
@ -329,29 +434,26 @@ QString TabArchidekt::buildSearchUrl()
|
|||
query.addQueryItem("logicalAnd", "true");
|
||||
}
|
||||
|
||||
// Formats
|
||||
// Formats (disabled in package mode)
|
||||
if (!packagesCheck->isChecked()) {
|
||||
QStringList formatIds;
|
||||
for (int i = 0; i < formatChecks.size(); ++i)
|
||||
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());
|
||||
}
|
||||
// edhBracket
|
||||
if (edhBracketCombo->currentIndex() > 0) {
|
||||
query.addQueryItem("edhBracket", edhBracketCombo->currentText());
|
||||
}
|
||||
}
|
||||
|
||||
// Search for card packages instead of decks
|
||||
// Package mode
|
||||
if (packagesCheck->isChecked()) {
|
||||
query.addQueryItem("packages", "true");
|
||||
}
|
||||
|
|
@ -361,54 +463,47 @@ QString TabArchidekt::buildSearchUrl()
|
|||
query.addQueryItem("name", nameField->text());
|
||||
}
|
||||
|
||||
// owner
|
||||
// Owner
|
||||
if (!ownerField->text().isEmpty()) {
|
||||
query.addQueryItem("ownerUsername", ownerField->text());
|
||||
}
|
||||
|
||||
// cards
|
||||
// Cards
|
||||
if (!cardsField->text().isEmpty()) {
|
||||
query.addQueryItem("cardName", cardsField->text());
|
||||
query.addQueryItem("cards", cardsField->text());
|
||||
}
|
||||
|
||||
// Commander Name
|
||||
if (!packagesCheck->isChecked()) {
|
||||
if (!commandersField->text().isEmpty()) {
|
||||
query.addQueryItem("commanderName", commandersField->text());
|
||||
}
|
||||
// Commander (disabled in package mode)
|
||||
if (!packagesCheck->isChecked() && !commandersField->text().isEmpty()) {
|
||||
query.addQueryItem("commanderName", commandersField->text());
|
||||
}
|
||||
|
||||
// deckTagName
|
||||
if (!packagesCheck->isChecked()) {
|
||||
if (!deckTagNameField->text().isEmpty()) {
|
||||
query.addQueryItem("deckTagName", deckTagNameField->text());
|
||||
}
|
||||
// Deck tag (disabled in package mode)
|
||||
if (!packagesCheck->isChecked() && !deckTagNameField->text().isEmpty()) {
|
||||
query.addQueryItem("deckTagName", deckTagNameField->text());
|
||||
}
|
||||
|
||||
// page number
|
||||
if (pageSpin->value() <= 1) {
|
||||
query.addQueryItem("page", QString::number(pageSpin->value()));
|
||||
}
|
||||
// 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"; // default
|
||||
QString logic = "GTE";
|
||||
QString selected = minDeckSizeLogicCombo->currentText();
|
||||
if (selected == "≥")
|
||||
logic = "GTE";
|
||||
else if (selected == "≤")
|
||||
logic = "LTE";
|
||||
else
|
||||
logic = ""; // Exact = unset
|
||||
logic = "";
|
||||
|
||||
if (!logic.isEmpty()) {
|
||||
query.addQueryItem("sizeLogic", logic);
|
||||
}
|
||||
}
|
||||
|
||||
// build final URL
|
||||
QUrl url("https://archidekt.com/api/decks/v3/");
|
||||
url.setQuery(query);
|
||||
|
||||
|
|
@ -417,7 +512,12 @@ QString TabArchidekt::buildSearchUrl()
|
|||
|
||||
void TabArchidekt::doSearch()
|
||||
{
|
||||
searchDebounceTimer->start();
|
||||
// Reset to first page on new search
|
||||
currentPage = 1;
|
||||
// We're searching, so we'll be in list mode
|
||||
isListMode = true;
|
||||
// Don't debounce - only called by explicit user actions now
|
||||
doSearchImmediate();
|
||||
}
|
||||
|
||||
void TabArchidekt::doSearchImmediate()
|
||||
|
|
@ -428,6 +528,21 @@ void TabArchidekt::doSearchImmediate()
|
|||
networkManager->get(req);
|
||||
}
|
||||
|
||||
void TabArchidekt::loadNextPage()
|
||||
{
|
||||
if (isLoadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingMore = true;
|
||||
currentPage++;
|
||||
|
||||
QString url = buildSearchUrl();
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(req);
|
||||
}
|
||||
|
||||
void TabArchidekt::actNavigatePage(QString url)
|
||||
{
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
|
|
@ -437,6 +552,7 @@ void TabArchidekt::actNavigatePage(QString url)
|
|||
|
||||
void TabArchidekt::getTopDecks()
|
||||
{
|
||||
currentPage = 1;
|
||||
QNetworkRequest request{QUrl(buildSearchUrl())};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
|
|
@ -445,7 +561,7 @@ void TabArchidekt::getTopDecks()
|
|||
void TabArchidekt::processApiJson(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qDebug() << "Network error occurred:" << reply->errorString();
|
||||
isLoadingMore = false;
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
|
@ -454,17 +570,14 @@ void TabArchidekt::processApiJson(QNetworkReply *reply)
|
|||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
|
||||
|
||||
if (!jsonDoc.isObject()) {
|
||||
qDebug() << "Invalid JSON response received.";
|
||||
isLoadingMore = false;
|
||||
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/")) {
|
||||
|
|
@ -473,6 +586,7 @@ void TabArchidekt::processApiJson(QNetworkReply *reply)
|
|||
prettyPrintJson(jsonObj, 4);
|
||||
}
|
||||
|
||||
isLoadingMore = false;
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
|
|
@ -481,28 +595,27 @@ 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;
|
||||
// New search → clear everything
|
||||
if (currentPage == 1) {
|
||||
QLayoutItem *item;
|
||||
while ((item = resultsLayout->takeAt(0)) != nullptr) {
|
||||
delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
|
||||
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||
|
||||
connect(listingsWidget, &ArchidektApiResponseDeckListingsDisplayWidget::requestNavigation, this,
|
||||
&TabArchidekt::actNavigatePage);
|
||||
|
||||
resultsLayout->addWidget(listingsWidget);
|
||||
return;
|
||||
}
|
||||
|
||||
// **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
|
||||
// Infinite scroll → append
|
||||
if (listingsWidget) {
|
||||
listingsWidget->append(deckData);
|
||||
}
|
||||
}
|
||||
|
||||
void TabArchidekt::processDeckResponse(QJsonObject reply)
|
||||
|
|
@ -510,54 +623,47 @@ 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;
|
||||
// We're in single deck mode - disable infinite scroll
|
||||
isListMode = false;
|
||||
|
||||
// Clear existing results for single deck view
|
||||
QLayoutItem *item;
|
||||
while ((item = resultsLayout->takeAt(0)) != nullptr) {
|
||||
delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
|
||||
// **Create new currentPageDisplay**
|
||||
currentPageDisplay = new QWidget(container);
|
||||
currentPageLayout = new QVBoxLayout(currentPageDisplay);
|
||||
currentPageDisplay->setLayout(currentPageLayout);
|
||||
|
||||
auto display = new ArchidektApiResponseDeckDisplayWidget(currentPageDisplay, deckData, cardSizeSlider);
|
||||
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||
connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage);
|
||||
connect(display, &ArchidektApiResponseDeckDisplayWidget::requestSearch, this, &TabArchidekt::doSearchImmediate);
|
||||
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
|
||||
resultsLayout->addWidget(display);
|
||||
}
|
||||
|
||||
void TabArchidekt::prettyPrintJson(const QJsonValue &value, int indentLevel)
|
||||
{
|
||||
const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing
|
||||
const QString indent(indentLevel * 2, ' ');
|
||||
|
||||
if (value.isObject()) {
|
||||
QJsonObject obj = value.toObject();
|
||||
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
||||
qDebug().noquote() << indent + it.key() + ":";
|
||||
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) {
|
||||
qDebug().noquote() << indent + QString("[%1]:").arg(i);
|
||||
qInfo().noquote() << indent + QString("[%1]:").arg(i);
|
||||
prettyPrintJson(array[i], indentLevel + 1);
|
||||
}
|
||||
} else if (value.isString()) {
|
||||
qDebug().noquote() << indent + "\"" + value.toString() + "\"";
|
||||
qInfo().noquote() << indent + "\"" + value.toString() + "\"";
|
||||
} else if (value.isDouble()) {
|
||||
qDebug().noquote() << indent + QString::number(value.toDouble());
|
||||
qInfo().noquote() << indent + QString::number(value.toDouble());
|
||||
} else if (value.isBool()) {
|
||||
qDebug().noquote() << indent + (value.toBool() ? "true" : "false");
|
||||
qInfo().noquote() << indent + (value.toBool() ? "true" : "false");
|
||||
} else if (value.isNull()) {
|
||||
qDebug().noquote() << indent + "null";
|
||||
qInfo().noquote() << indent + "null";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,26 +4,35 @@
|
|||
#include "../../interface/widgets/cards/card_size_widget.h"
|
||||
#include "../../interface/widgets/quick_settings/settings_button_widget.h"
|
||||
#include "../../tab.h"
|
||||
#include "display/archidekt_api_response_deck_listings_display_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QSpinBox>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
|
||||
/** Base API link for Archidekt deck search */
|
||||
inline QString archidektApiLink = "https://archidekt.com/api/decks/v3/?name=";
|
||||
|
||||
class ManaSymbolWidget;
|
||||
|
||||
/**
|
||||
* @brief Tab for browsing, searching, and filtering Archidekt decks.
|
||||
*
|
||||
* This class provides a comprehensive interface for querying decks from the Archidekt API.
|
||||
* Users can filter decks by name, owner, included cards, commanders, deck tags, colors, EDH bracket,
|
||||
* and formats. It also provides sorting and pagination, as well as a card size adjustment widget.
|
||||
* and formats. It supports infinite scroll pagination for seamless browsing.
|
||||
*/
|
||||
class TabArchidekt : public Tab
|
||||
{
|
||||
|
|
@ -63,7 +72,7 @@ public:
|
|||
* - Packages toggle
|
||||
* - Sorting field and direction
|
||||
* - Minimum amount of cards in the deck
|
||||
* - Pagination (page)
|
||||
* - Current page (for infinite scroll)
|
||||
*/
|
||||
QString buildSearchUrl();
|
||||
|
||||
|
|
@ -90,32 +99,45 @@ public:
|
|||
return cardSizeSlider;
|
||||
}
|
||||
|
||||
/** @brief Network manager for handling API requests */
|
||||
QNetworkAccessManager *networkManager;
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Trigger a search using the current filters
|
||||
* @brief Trigger a debounced search using the current filters
|
||||
*
|
||||
* Sends a network request to the Archidekt API using the URL generated by buildSearchUrl().
|
||||
* Updates the current page display with results asynchronously.
|
||||
* Resets to page 1 and starts the debounce timer. The actual search will execute
|
||||
* after 300ms of inactivity.
|
||||
*/
|
||||
void doSearch();
|
||||
|
||||
/**
|
||||
* @brief Immediately trigger a search using the current filters
|
||||
*
|
||||
* Sends a network request to the Archidekt API using the URL generated by buildSearchUrl().
|
||||
* Updates the results display asynchronously.
|
||||
*/
|
||||
void doSearchImmediate();
|
||||
|
||||
/**
|
||||
* @brief Load the next page of results for infinite scroll
|
||||
*
|
||||
* Increments the current page and fetches additional results, which are appended
|
||||
* to the existing results display.
|
||||
*/
|
||||
void loadNextPage();
|
||||
|
||||
/**
|
||||
* @brief Process a network reply containing JSON data
|
||||
* @param reply QNetworkReply object with the API response
|
||||
*
|
||||
* Determines whether the response corresponds to a top decks query or a single deck,
|
||||
* Determines whether the response corresponds to a deck listing or a single deck,
|
||||
* and dispatches it to the appropriate handler.
|
||||
*/
|
||||
void processApiJson(QNetworkReply *reply);
|
||||
|
||||
/**
|
||||
* @brief Handle a JSON response containing multiple decks
|
||||
* @param reply QJsonObject containing top deck listings
|
||||
* @param reply QJsonObject containing deck listings
|
||||
*
|
||||
* Clears the previous page display and creates a new display widget for the results.
|
||||
* If this is page 1, clears previous results. Appends new results to the display.
|
||||
*/
|
||||
void processTopDecksResponse(QJsonObject reply);
|
||||
|
||||
|
|
@ -123,7 +145,7 @@ public slots:
|
|||
* @brief Handle a JSON response for a single deck
|
||||
* @param reply QJsonObject containing deck data
|
||||
*
|
||||
* Clears the previous page display and creates a new display widget for the deck details.
|
||||
* Clears the results area and displays the single deck details.
|
||||
*/
|
||||
void processDeckResponse(QJsonObject reply);
|
||||
|
||||
|
|
@ -138,107 +160,129 @@ public slots:
|
|||
* @brief Navigate to a specified page URL
|
||||
* @param url The URL to request
|
||||
*
|
||||
* Typically called when a navigation button is clicked in a deck listing.
|
||||
* Typically called when a deck card is clicked in the listing.
|
||||
*/
|
||||
void actNavigatePage(QString url);
|
||||
|
||||
/**
|
||||
* @brief Fetch top decks from the Archidekt API
|
||||
*
|
||||
* Called on initialization to populate the initial page display.
|
||||
* Called on initialization to populate the initial results display.
|
||||
*/
|
||||
void getTopDecks();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Event filter to catch wheel events for infinite scroll
|
||||
* @param obj The object that received the event
|
||||
* @param event The event to filter
|
||||
* @return bool Whether the event was handled
|
||||
*/
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private:
|
||||
QTimer *searchDebounceTimer; ///< Timer to debounce search requests by spin-boxes etc.
|
||||
/**
|
||||
* @brief Initialize the main UI layout and toolbars
|
||||
*
|
||||
* Creates the container, main layout, primary toolbar (sort, colors, name, owner, packages),
|
||||
* secondary toolbar (advanced filters), and scrollable results area.
|
||||
*/
|
||||
void initializeUi();
|
||||
|
||||
/**
|
||||
* @brief Set up all filter widgets
|
||||
*
|
||||
* Creates filter widgets for:
|
||||
* - Card search with autocomplete
|
||||
* - Commander search with autocomplete
|
||||
* - Deck tags
|
||||
* - Format selection (collapsible)
|
||||
* - Deck size filter (collapsible)
|
||||
*/
|
||||
void setupFilterWidgets();
|
||||
|
||||
/**
|
||||
* @brief Connect all signals and slots for UI interactions
|
||||
*
|
||||
* Links all widget signals to their appropriate handlers, including
|
||||
* search triggers, filter changes, package mode toggling, and infinite scroll.
|
||||
*/
|
||||
void connectSignals();
|
||||
|
||||
/**
|
||||
* @brief Update UI state when package mode is toggled
|
||||
* @param isPackageMode Whether package mode is currently enabled
|
||||
*
|
||||
* Disables format-specific and commander-specific filters when searching
|
||||
* for card packages instead of full decks.
|
||||
*/
|
||||
void updatePackageModeState(bool isPackageMode);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Network & Timing
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QNetworkAccessManager *networkManager; ///< Network manager for handling API requests
|
||||
QTimer *searchDebounceTimer; ///< Timer to debounce search requests
|
||||
int currentPage; ///< Current page number for infinite scroll
|
||||
bool isLoadingMore; ///< Flag to prevent multiple simultaneous page loads
|
||||
bool isListMode;
|
||||
ArchidektApiResponseDeckListingsDisplayWidget *listingsWidget = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Layout Containers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QWidget *container; ///< Root container for the entire tab
|
||||
QVBoxLayout *mainLayout; ///< Outer vertical layout containing navigation and page display
|
||||
QWidget *navigationContainer; ///< Container for all navigation/filter controls
|
||||
QHBoxLayout *navigationLayout; ///< Layout for horizontal arrangement of filter widgets
|
||||
QWidget *currentPageDisplay; ///< Widget containing the currently displayed deck(s)
|
||||
QVBoxLayout *currentPageLayout; ///< Layout for deck display widgets
|
||||
QWidget *container; ///< Root container for the entire tab
|
||||
QVBoxLayout *mainLayout; ///< Outer vertical layout containing toolbars and results
|
||||
|
||||
QWidget *primaryToolbar; ///< Primary toolbar with most important filters
|
||||
QHBoxLayout *primaryToolbarLayout; ///< Layout for primary toolbar
|
||||
|
||||
QWidget *secondaryToolbar; ///< Secondary toolbar with advanced filters
|
||||
QHBoxLayout *secondaryToolbarLayout; ///< Layout for secondary toolbar
|
||||
|
||||
QScrollArea *scrollArea; ///< Scrollable area for results (enables infinite scroll)
|
||||
QWidget *resultsContainer; ///< Container widget inside scroll area
|
||||
QVBoxLayout *resultsLayout; ///< Layout for results (decks appended here)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Sorting Controls
|
||||
// Primary Toolbar Controls (Most Important)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLabel *sortByLabel; ///< Label for sort controls
|
||||
QComboBox *orderByCombo; ///< Dropdown for selecting the sort field
|
||||
QPushButton *orderDirButton; ///< Toggle button for ascending/descending sort
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Color Filters
|
||||
// ---------------------------------------------------------------------
|
||||
QLabel *filterByLabel; ///< Label for filter controls
|
||||
QList<ManaSymbolWidget *> colorSymbols; ///< Mana symbol toggle buttons
|
||||
QSet<QChar> activeColors; ///< Set of currently active mana colors
|
||||
QCheckBox *logicalAndCheck; ///< Require ALL selected colors instead of ANY
|
||||
|
||||
QSet<QChar> activeColors; ///< Set of currently active mana colors
|
||||
QCheckBox *logicalAndCheck; ///< Require ALL selected colors instead of ANY
|
||||
QLineEdit *nameField; ///< Input for deck name filter
|
||||
QLineEdit *ownerField; ///< Input for owner name filter
|
||||
QCheckBox *packagesCheck; ///< Toggle for searching card packages instead of full decks
|
||||
QPushButton *searchButton; ///< Button to trigger search
|
||||
QPushButton *advancedFiltersButton; ///< Button to show/hide advanced filters
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Format Filters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLabel *formatLabel; ///< Label displaying "Formats"
|
||||
SettingsButtonWidget *formatSettingsWidget; ///< Collapsible widget containing format checkboxes
|
||||
QVector<QCheckBox *> formatChecks; ///< Individual checkboxes for each format
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EDH Bracket / Package Toggle
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QComboBox *edhBracketCombo; ///< Dropdown for EDH bracket selection
|
||||
QCheckBox *packagesCheck; ///< Toggle for searching card packages instead of full decks
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Basic Search Fields
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLineEdit *nameField; ///< Input for deck name filter
|
||||
QLineEdit *ownerField; ///< Input for owner name filter
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Card Filters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLineEdit *cardsField; ///< Input for cards included in the deck (comma-separated)
|
||||
QLineEdit *commandersField; ///< Input for commander cards (comma-separated)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Deck Tag
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLineEdit *deckTagNameField; ///< Input for deck tag filtering
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Search Trigger
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QPushButton *searchPushButton; ///< Button to trigger the search manually
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// UI Settings (Card Size)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
SettingsButtonWidget *settingsButton; ///< Container for additional UI settings
|
||||
SettingsButtonWidget *settingsButton; ///< Container for card size settings
|
||||
CardSizeWidget *cardSizeSlider; ///< Slider to adjust card size in results
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Minimum Cards in Deck
|
||||
// Secondary Toolbar Controls (Advanced Filters)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
QLabel *minDeckSizeLabel; ///< Label for minimum number of cards per deck
|
||||
QSpinBox *minDeckSizeSpin; ///< Spinner to select minimum deck size
|
||||
QComboBox *minDeckSizeLogicCombo; ///< Combo box for the size logic to apply
|
||||
QLineEdit *cardsField; ///< Input for cards included in the deck
|
||||
QLineEdit *commandersField; ///< Input for commander cards
|
||||
QLineEdit *deckTagNameField; ///< Input for deck tag filtering
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Pagination
|
||||
// ---------------------------------------------------------------------
|
||||
SettingsButtonWidget *formatButton; ///< Collapsible button for format filters
|
||||
QVector<QCheckBox *> formatChecks; ///< Individual checkboxes for each format
|
||||
QComboBox *edhBracketCombo; ///< Dropdown for EDH bracket selection
|
||||
|
||||
QLabel *pageLabel; ///< Label for current page selection
|
||||
QSpinBox *pageSpin; ///< Spinner to select the page number for results
|
||||
SettingsButtonWidget *deckSizeButton; ///< Collapsible button for deck size filter
|
||||
QSpinBox *minDeckSizeSpin; ///< Spinner to select minimum deck size
|
||||
QComboBox *minDeckSizeLogicCombo; ///< Combo box for size comparison logic
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Optional Context
|
||||
|
|
@ -247,4 +291,4 @@ private:
|
|||
CardInfoPtr cardToQuery; ///< Optional pre-selected card for initial filtering
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TAB_ARCHIDEKT_H
|
||||
#endif // COCKATRICE_TAB_ARCHIDEKT_H
|
||||
Loading…
Reference in New Issue
Block a user