From 12b5525a2d578fd356e26fd6f4eeb64550f43f1e Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:21:12 +0100 Subject: [PATCH] [TabArchidekt] Cleaner filters, infinite scrolling, and a "go back button" (#6545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- ...idekt_api_response_deck_display_widget.cpp | 15 +- ...chidekt_api_response_deck_display_widget.h | 10 +- ...api_response_deck_entry_display_widget.cpp | 2 +- ..._response_deck_listings_display_widget.cpp | 14 + ...pi_response_deck_listings_display_widget.h | 1 + .../tabs/api/archidekt/tab_archidekt.cpp | 636 ++++++++++-------- .../tabs/api/archidekt/tab_archidekt.h | 210 +++--- 7 files changed, 534 insertions(+), 354 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 3a2468368..f40b9094f 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -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")); } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.h index 58aca429e..aaf272718 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.h @@ -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 diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index 736b69ea2..c26220869 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -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); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp index 8746093c7..4d3dfd660 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp @@ -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); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.h index f00941239..479de5fc8 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.h @@ -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. diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index 3769cd9a2..352d55c79 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -9,6 +9,9 @@ #include #include +#include +#include +#include #include #include #include @@ -18,129 +21,258 @@ #include #include #include +#include +#include #include #include #include #include #include -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(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(&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(&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"; } -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h index 68a5b78c9..e837e672b 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h @@ -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 #include #include +#include #include #include #include +#include +#include #include #include +#include +#include +#include #include /** 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 colorSymbols; ///< Mana symbol toggle buttons + QSet activeColors; ///< Set of currently active mana colors + QCheckBox *logicalAndCheck; ///< Require ALL selected colors instead of ANY - QSet 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 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 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 \ No newline at end of file