diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 522300346..eacbc8825 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -153,8 +153,10 @@ set(cockatrice_SOURCES src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp src/interface/widgets/general/background_sources.cpp + src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp src/interface/widgets/general/display/bar_widget.cpp + src/interface/widgets/general/display/color_bar.cpp src/interface/widgets/general/display/dynamic_font_size_label.cpp src/interface/widgets/general/display/dynamic_font_size_push_button.cpp src/interface/widgets/general/display/labeled_input.cpp @@ -202,6 +204,7 @@ set(cockatrice_SOURCES src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_widget.cpp src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp + src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -220,6 +223,19 @@ set(cockatrice_SOURCES src/interface/window_main.cpp src/main.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp + src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp + src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp + src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.cpp + src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp + src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.cpp + src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.cpp + src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.cpp + src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.cpp + src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.cpp + src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp + src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp + src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp + src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp index 75e075de6..cb83768f4 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -32,7 +32,10 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent, CardGroupDisplayWidget::updateCardDisplays(); connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition); - connect(selectionModel, &QItemSelectionModel::selectionChanged, this, &CardGroupDisplayWidget::onSelectionChanged); + if (selectionModel) { + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, + &CardGroupDisplayWidget::onSelectionChanged); + } connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval); } @@ -179,7 +182,9 @@ void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSort void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event) { QWidget::mousePressEvent(event); - selectionModel->clearSelection(); + if (selectionModel) { + selectionModel->clearSelection(); + } } void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index 618d7e565..132964f13 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -38,8 +38,10 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, displayCards(); connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::onCategoryAddition); - connect(selectionModel, &QItemSelectionModel::selectionChanged, this, - &DeckCardZoneDisplayWidget::onSelectionChanged); + if (selectionModel) { + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, + &DeckCardZoneDisplayWidget::onSelectionChanged); + } connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::onCategoryRemoval); } diff --git a/cockatrice/src/interface/widgets/general/display/background_plate_widget.cpp b/cockatrice/src/interface/widgets/general/display/background_plate_widget.cpp new file mode 100644 index 000000000..aad259900 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/background_plate_widget.cpp @@ -0,0 +1,34 @@ +#include "background_plate_widget.h" + +#include +#include +#include +#include + +BackgroundPlateWidget::BackgroundPlateWidget(QWidget *parent) : QWidget(parent) +{ + setAutoFillBackground(true); // For automatic background filling +} + +void BackgroundPlateWidget::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + // Set the background color to semi-transparent black with rounded corners + QRect rect = this->rect(); + painter.setPen(Qt::NoPen); // No border + if (focused) { + painter.setBrush(QColor(85, 190, 75, 140)); + } else { + painter.setBrush(QColor(0, 0, 0, 140)); // semi-transparent black + } + painter.drawRoundedRect(rect, 6, 6); // rounded corners +} + +void BackgroundPlateWidget::setFocused(bool _focused) +{ + focused = _focused; + update(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/background_plate_widget.h b/cockatrice/src/interface/widgets/general/display/background_plate_widget.h new file mode 100644 index 000000000..d529cb6fa --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/background_plate_widget.h @@ -0,0 +1,22 @@ +#ifndef COCKATRICE_BACKGROUND_PLATE_WIDGET_H +#define COCKATRICE_BACKGROUND_PLATE_WIDGET_H + +#include + +class BackgroundPlateWidget : public QWidget +{ + Q_OBJECT + +public: + explicit BackgroundPlateWidget(QWidget *parent = nullptr); + + void setFocused(bool focused); + +private: + bool focused = false; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // COCKATRICE_BACKGROUND_PLATE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.cpp b/cockatrice/src/interface/widgets/general/display/color_bar.cpp new file mode 100644 index 000000000..d1eb7ef4c --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/color_bar.cpp @@ -0,0 +1,163 @@ + +#include "color_bar.h" + +#include +#include +#include +#include + +ColorBar::ColorBar(const QMap &_colors, QWidget *parent) : QWidget(parent), colors(_colors) +{ + setMouseTracking(true); +} + +void ColorBar::setColors(const QMap &_colors) +{ + colors = _colors; + update(); +} + +QSize ColorBar::minimumSizeHint() const +{ + return QSize(200, 22); +} + +void ColorBar::paintEvent(QPaintEvent *) +{ + if (colors.isEmpty()) + return; + + int total = 0; + for (int v : colors.values()) + total += v; + + // Prevent divide-by-zero + if (total == 0) + return; + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + const int w = width(); + const int h = height(); + int x = 0; + + // Draw rounded border background + QRectF bounds(0.5, 0.5, w - 1, h - 1); + p.setPen(QPen(Qt::black, 1)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(bounds, 6, 6); + + // Clip to inside the border + p.setClipRect(bounds.adjusted(2, 2, -2, -2)); + + // Ensure predictable order + QList sortedKeys = colors.keys(); + std::sort(sortedKeys.begin(), sortedKeys.end()); // Sort alphabetically + + // Draw each color segment in the sorted order + for (const QString &key : sortedKeys) { + int value = colors[key]; + double ratio = double(value) / total; + + if (ratio <= minRatioThreshold) { + continue; + } + + int segmentWidth = int(ratio * w); + + // Ensure the segment width is at least 1 to avoid degenerate rectangles + if (segmentWidth < 1) + segmentWidth = 1; + + QColor base = colorFromName(key); + + // Slight gradient for nicer look + QLinearGradient grad(x, 0, x, h); + grad.setColorAt(0, base.lighter(120)); + grad.setColorAt(1, base.darker(120)); + + p.fillRect(QRect(x, 0, segmentWidth, h), grad); + + x += segmentWidth; + } +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ColorBar::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#else +void ColorBar::enterEvent(QEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#endif + +void ColorBar::leaveEvent(QEvent *) +{ + isHovered = false; +} + +void ColorBar::mouseMoveEvent(QMouseEvent *event) +{ + if (!isHovered || colors.isEmpty()) + return; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + int x = int(event->position().x()); + QPoint gp = event->globalPosition().toPoint(); +#else + int x = event->pos().x(); + QPoint gp = event->globalPos(); +#endif + + QString text = tooltipForPosition(x); + if (!text.isEmpty()) + QToolTip::showText(gp, text, this); +} + +QString ColorBar::tooltipForPosition(int x) const +{ + int total = 0; + for (int v : colors.values()) + total += v; + + if (total == 0) + return {}; + + int pos = 0; + for (auto it = colors.cbegin(); it != colors.cend(); ++it) { + const double ratio = double(it.value()) / total; + const int segmentWidth = int(ratio * width()); + + if (x >= pos && x < pos + segmentWidth) { + const double percent = (100.0 * it.value()) / total; + return QString("%1: %2 cards (%3%)").arg(it.key()).arg(it.value()).arg(QString::number(percent, 'f', 1)); + } + + pos += segmentWidth; + } + + return {}; +} + +QColor ColorBar::colorFromName(const QString &name) const +{ + static QMap map = { + {"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)}, + {"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)}, + }; + + if (map.contains(name)) + return map[name]; + + QColor c(name); + if (!c.isValid()) + c = Qt::gray; + + return c; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.h b/cockatrice/src/interface/widgets/general/display/color_bar.h new file mode 100644 index 000000000..f61ab6942 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/color_bar.h @@ -0,0 +1,128 @@ +#ifndef COCKATRICE_COLOR_BAR_H +#define COCKATRICE_COLOR_BAR_H + +#include +#include +#include +#include + +/** + * @class ColorBar + * @brief A widget for visualizing proportional color distributions as a horizontal bar. + * + * This widget renders a horizontal bar divided into colored segments whose widths reflect + * the relative values associated with each color key in a `QMap`. The class + * is designed as a small, lightweight, and self-contained visualization component suitable + * for representing distributions such as color counts, mana statistics, categorical frequencies, and similar data sets. + * + * Key features: + * - Filled segments for better visual clarity. + * - Deterministic alphabetical ordering of color keys. + * - Optional minimum percentage threshold for filtering out insignificant segments. + * - Mouse-hover tooltips showing each segment’s key, count, and percentage of total. + * + * Default color mappings exist for `"R"`, `"G"`, `"U"`, `"W"`, and `"B"`, using named + * colors, but any string recognized by `QColor` may be used. If an unknown name is provided, + * the segment will fall back to gray. + * + * This component is display-only and does not interpret or mutate domain-level data. + */ +class ColorBar : public QWidget +{ + Q_OBJECT + +public: + /** + * @brief Constructs a ColorBar widget. + * + * @param colors Map of color identifiers to integer counts. + * @param parent Optional parent widget. + */ + explicit ColorBar(const QMap &colors, QWidget *parent = nullptr); + + /** + * @brief Updates the color distribution map. + * @param colors New color → count mapping. + * + * Triggers an immediate repaint. + */ + void setColors(const QMap &colors); + + /** + * @brief Sets a minimum percentage threshold below which segments are not drawn. + * + * @param treshold Percentage from 0 to 100. + * + * Internally converted into a ratio (0.05 = 5%). + */ + void setMinPercentThreshold(double treshold) + { + minRatioThreshold = treshold / 100.0; + } + + /** + * @brief Returns the recommended minimum size. + */ + QSize minimumSizeHint() const override; + +protected: + /** + * @brief Paints the color distribution bar. + * + * Draws: + * - A rounded border + * - Filled segments for each color + * - Only segments above the minimum ratio threshold + */ + void paintEvent(QPaintEvent *event) override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + /** + * @brief Handles mouse hover entering (Qt6 version). + */ + void enterEvent(QEnterEvent *event) override; +#else + /** + * @brief Handles mouse hover entering (Qt5 version). + */ + void enterEvent(QEvent *event) override; +#endif + + /** + * @brief Handles mouse hover leaving. + */ + void leaveEvent(QEvent *event) override; + + /** + * @brief Handles mouse movement to update contextual tooltips. + */ + void mouseMoveEvent(QMouseEvent *event) override; + +private: + /// Map of color keys to counts used for rendering. + QMap colors; + + /// True if the mouse is currently inside the widget. + bool isHovered = false; + + /// Minimum ratio a segment must exceed to be drawn. + double minRatioThreshold = 0.0; + + /** + * @brief Converts a color name into a display QColor. + * + * Recognized special keys: `"R", "G", "U", "W", "B"`. + * Other strings are treated as QColor names or fall back to gray. + */ + QColor colorFromName(const QString &name) const; + + /** + * @brief Returns tooltip text for a given x-coordinate in the bar. + * + * @param x Horizontal coordinate relative to widget. + * @return Tooltip text or empty string if no segment applies. + */ + QString tooltipForPosition(int x) const; +}; + +#endif // COCKATRICE_COLOR_BAR_H diff --git a/cockatrice/src/interface/widgets/general/display/shadow_background_label.cpp b/cockatrice/src/interface/widgets/general/display/shadow_background_label.cpp index fbca8df8d..7db18c57d 100644 --- a/cockatrice/src/interface/widgets/general/display/shadow_background_label.cpp +++ b/cockatrice/src/interface/widgets/general/display/shadow_background_label.cpp @@ -12,12 +12,18 @@ */ ShadowBackgroundLabel::ShadowBackgroundLabel(QWidget *parent, const QString &text) : QLabel(parent) { - setAttribute(Qt::WA_TranslucentBackground); // Allows transparency. - setText("" + text + ""); ///< Ensures the text is rendered in white. + setAttribute(Qt::WA_TranslucentBackground); // Allows transparency. + setLabelText(text); setAlignment(Qt::AlignCenter); ///< Centers the text within the label. setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); ///< Ensures minimum size constraints. } +void ShadowBackgroundLabel::setLabelText(const QString &text) +{ + setText("" + text + ""); ///< Ensures the text is rendered in white. + update(); +} + /** * @brief Handles resizing of the label. * diff --git a/cockatrice/src/interface/widgets/general/display/shadow_background_label.h b/cockatrice/src/interface/widgets/general/display/shadow_background_label.h index 3483452f5..b2344b7d0 100644 --- a/cockatrice/src/interface/widgets/general/display/shadow_background_label.h +++ b/cockatrice/src/interface/widgets/general/display/shadow_background_label.h @@ -15,6 +15,7 @@ class ShadowBackgroundLabel : public QLabel public: explicit ShadowBackgroundLabel(QWidget *parent, const QString &text); + void setLabelText(const QString &text); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 2dfd129d5..88eec230c 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -196,6 +196,9 @@ QGroupBox *HomeWidget::createButtons() auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors); connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab); boxLayout->addWidget(edhrecButton); + auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors); + connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab); + boxLayout->addWidget(archidektButton); auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors); connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); boxLayout->addWidget(replaybutton); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp new file mode 100644 index 000000000..40882db04 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp @@ -0,0 +1,29 @@ +#include "archidekt_deck_listing_api_response.h" + +#include +#include + +void ArchidektDeckListingApiResponse::fromJson(const QJsonObject &json) +{ + count = json.value("count").toInt(); + next = QUrl(json.value("next").toString()); + + QJsonArray containerJson = json.value("results").toArray(); + + for (const QJsonValue &deckListingValue : containerJson) { + ArchidektApiResponseDeckListingContainer listingResult; + listingResult.fromJson(deckListingValue.toObject()); + results.append(listingResult); + } +} + +void ArchidektDeckListingApiResponse::debugPrint() const +{ + qDebug() << "Count:" << count; + qDebug() << "Next:" << next; + + qDebug() << "Results:"; + for (const auto &deckListing : results) { + deckListing.debugPrint(); + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.h new file mode 100644 index 000000000..723c65ce2 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.h @@ -0,0 +1,22 @@ +#ifndef COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_H +#define COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_H + +#include "deck_listings/archidekt_api_response_deck_listing_container.h" + +#include +#include +#include + +class ArchidektDeckListingApiResponse +{ + +public: + int count; + QUrl next; + QVector results; + + void fromJson(const QJsonObject &json); + void debugPrint() const; +}; + +#endif // COCKATRICE_DECK_LISTING_API_RESPONSE_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.cpp new file mode 100644 index 000000000..909c4d9eb --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.cpp @@ -0,0 +1,61 @@ +#include "archidekt_api_response_card.h" + +void ArchidektApiResponseCard::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + artist = json.value("artist").toString(); + tcgProductId = json.value("tcgProductId").toInt(); + ckFoilId = json.value("ckFoilId").toInt(); + ckNormalId = json.value("ckNormalId").toInt(); + cmEd = json.value("cmEd").toString(); + scgSku = json.value("scgSku").toString(); + scgFoilSku = json.value("scgFoilSku").toString(); + collectorNumber = json.value("collectorNumber").toString(); + multiverseId = json.value("multiverseId").toInt(); + mtgoFoilId = json.value("mtgoFoilId").toInt(); + mtgoNormalId = json.value("mtgoNormalId").toInt(); + uid = json.value("uid").toString(); + displayName = json.value("displayName").toString(); + releasedAt = json.value("releasedAt").toString(); + + edition.fromJson(json.value("edition").toObject()); + + flavor = json.value("flavor").toString(); + // TODO but not really important + // games = {""}; + // options = {""}; + scryfallImageHash = json.value("scryfallImageHash").toString(); + oracleCard = json.value("oracleCard").toObject(); + owned = json.value("owned").toInt(); + pinnedStatus = json.value("pinnedStatus").toInt(); + rarity = json.value("rarity").toString(); + // TODO but not really important + // globalCategories = {""}; +} + +void ArchidektApiResponseCard::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "id:" << artist; + qDebug() << "artist:" << tcgProductId; + qDebug() << "tcgProductId:" << ckFoilId; + qDebug() << "ckFoilId:" << ckNormalId; + qDebug() << "ckNormalId:" << cmEd; + qDebug() << "cmEd:" << scgSku; + qDebug() << "scgSku:" << scgFoilSku; + qDebug() << "scgFoilSku:" << collectorNumber; + qDebug() << "collectorNumber:" << multiverseId; + qDebug() << "multiverseId:" << mtgoFoilId; + qDebug() << "mtgoFoilId:" << mtgoNormalId; + qDebug() << "mtgoNormalId:" << uid; + qDebug() << "uid:" << displayName; + qDebug() << "displayName:" << releasedAt; + qDebug() << "releasedAt:" << flavor; + qDebug() << "flavor:" << games; + qDebug() << "games:" << options; + qDebug() << "options:" << scryfallImageHash; + qDebug() << "scryfallImageHash:" << owned; + qDebug() << "owned:" << pinnedStatus; + qDebug() << "pinnedStatus:" << rarity; + qDebug() << "rarity:" << globalCategories; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.h new file mode 100644 index 000000000..265498228 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.h @@ -0,0 +1,68 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_H + +#include "archidekt_api_response_edition.h" + +#include +#include +#include +#include +#include + +class ArchidektApiResponseCard +{ +public: + // Constructor + ArchidektApiResponseCard() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + QJsonObject getOracleCard() const + { + return oracleCard; + }; + + QString getCollectorNumber() const + { + return collectorNumber; + } + + ArchidektApiResponseEdition getEdition() const + { + return edition; + } + +private: + int id; + QString artist; + int tcgProductId; + int ckFoilId; + int ckNormalId; + QString cmEd; + QString scgSku; + QString scgFoilSku; + QString collectorNumber; + int multiverseId; + int mtgoFoilId; + int mtgoNormalId; + QString uid; + QString displayName; + QString releasedAt; + ArchidektApiResponseEdition edition; + QString flavor; + QStringList games; + QStringList options; + QString scryfallImageHash; + QJsonObject oracleCard; + int owned; + int pinnedStatus; + // ArchidektApiResponsePrices prices; + QString rarity; + QStringList globalCategories; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp new file mode 100644 index 000000000..7a424de8b --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp @@ -0,0 +1,43 @@ +#include "archidekt_api_response_card_entry.h" + +void ArchidektApiResponseCardEntry::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + + auto categoriesJson = json.value("categories").toArray(); + + for (auto category : categoriesJson) { + categories.append(category.toString()); + } + + companion = json.value("companion").toBool(); + flippedDefault = json.value("flippedDefault").toBool(); + label = json.value("label").toString(); + modifier = json.value("modifier").toString(); + quantity = json.value("quantity").toInt(); + customCmc = json.value("customCmc").toInt(); + // removedCategories = {""}; + createdAt = json.value("createdAt").toString(); + updatedAt = json.value("updatedAt").toString(); + deletedAt = json.value("deletedAt").toString(); + notes = json.value("notes").toString(); + card.fromJson(json.value("card").toObject()); +} + +void ArchidektApiResponseCardEntry::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "Categories:" << categories; + qDebug() << "Companion:" << companion; + qDebug() << "FlippedDefault:" << flippedDefault; + qDebug() << "Label:" << label; + qDebug() << "Modifier:" << modifier; + qDebug() << "Quantity:" << quantity; + qDebug() << "CustomCmc:" << customCmc; + qDebug() << "RemovedCategories:" << removedCategories; + qDebug() << "CreatedAt:" << createdAt; + qDebug() << "UpdatedAt:" << updatedAt; + qDebug() << "DeletedAt:" << deletedAt; + qDebug() << "Notes:" << notes; + card.debugPrint(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h new file mode 100644 index 000000000..f7f86e9ed --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.h @@ -0,0 +1,56 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_ENTRY_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_ENTRY_H + +#include "archidekt_api_response_card.h" + +#include +#include +#include +#include +#include + +class ArchidektApiResponseCardEntry +{ +public: + // Constructor + ArchidektApiResponseCardEntry() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + ArchidektApiResponseCard getCard() const + { + return card; + }; + + QStringList getCategories() const + { + return categories; + } + + int getQuantity() const + { + return quantity; + } + +private: + int id; + QStringList categories; + bool companion; + bool flippedDefault; + QString label; + QString modifier; + int quantity; + int customCmc; + QStringList removedCategories; + QString createdAt; + QString updatedAt; + QString deletedAt; + QString notes; + ArchidektApiResponseCard card; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_CARD_ENTRY_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.cpp new file mode 100644 index 000000000..122617a22 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.cpp @@ -0,0 +1,19 @@ +#include "archidekt_api_response_edition.h" + +void ArchidektApiResponseEdition::fromJson(const QJsonObject &json) +{ + editionCode = json.value("editioncode").toString(); + editionName = json.value("editionname").toString(); + editionDate = json.value("editiondate").toString(); + editionType = json.value("editiontype").toString(); + mtgoCode = json.value("mtgocode").toString(); +} + +void ArchidektApiResponseEdition::debugPrint() const +{ + qDebug() << "Edition Code: " << editionCode; + qDebug() << "Edition Name: " << editionName; + qDebug() << "Edition Date: " << editionDate; + qDebug() << "Edition Type: " << editionType; + qDebug() << "Mtgo Code: " << mtgoCode; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.h new file mode 100644 index 000000000..b898cd816 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.h @@ -0,0 +1,51 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_EDITION_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_EDITION_H + +#include +#include +#include +#include +#include + +class ArchidektApiResponseEdition +{ +public: + // Constructor + ArchidektApiResponseEdition() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + [[nodiscard]] QString getEditionCode() const + { + return editionCode; + } + [[nodiscard]] QString getEditionName() const + { + return editionName; + } + [[nodiscard]] QString getEditionDate() const + { + return editionDate; + } + [[nodiscard]] QString getEditionType() const + { + return editionType; + } + [[nodiscard]] QString getMtgoCode() const + { + return mtgoCode; + } + +private: + QString editionCode; + QString editionName; + QString editionDate; + QString editionType; + QString mtgoCode; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_EDITION_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.cpp new file mode 100644 index 000000000..98d8c6c8c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.cpp @@ -0,0 +1,79 @@ +#include "archidekt_api_response_deck.h" + +#include "../card/archidekt_api_response_card_entry.h" + +void ArchidektApiResponseDeck::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + name = json.value("name").toString(); + size = json.value("size").toInt(); + updatedAt = json.value("updatedAt").toString(); + createdAt = json.value("createdAt").toString(); + deckFormat = json.value("deckFormat").toInt(); + edhBracket = json.value("edhBracket").toInt(); + featured = json.value("featured").toString(); + customFeatured = json.value("customFeatured").toString(); + viewCount = json.value("viewCount").toInt(); + privateDeck = json.value("private").toBool(); + unlisted = json.value("unlisted").toBool(); + theoryCrafted = json.value("theoryCrafted").toBool(); + points = json.value("points").toInt(); + userInput = json.value("userInput").toInt(); + owner.fromJson(json.value("owner").toObject()); + commentRoot = json.value("commentRoot").toInt(); + editors = json.value("editors").toString(); + parentFolderId = json.value("parentFolderId").toInt(); + bookmarked = json.value("bookmarked").toBool(); + + auto categoriesJson = json.value("categories").toArray(); + for (auto category : categoriesJson) { + ArchidektApiResponseDeckCategory categoryEntry; + categoryEntry.fromJson(category.toObject()); + categories.append(categoryEntry); + } + + // deckTags = {""}; + playgroupDeckUrl = json.value("playgroupDeckUrl").toString(); + cardPackage = json.value("cardPackage").toString(); + + auto cardsObject = json.value("cards").toArray(); + + for (auto card : cardsObject) { + ArchidektApiResponseCardEntry entry; + entry.fromJson(card.toObject()); + cards.append(entry); + } + + // TODO but not really important + // customCards = {""}; +} + +void ArchidektApiResponseDeck::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "Name:" << name; + qDebug() << "Size:" << size; + qDebug() << "UpdatedAt:" << updatedAt; + qDebug() << "CreatedAt:" << createdAt; + qDebug() << "DeckFormat:" << deckFormat; + qDebug() << "EdhBracket:" << edhBracket; + qDebug() << "Featured:" << featured; + qDebug() << "CustomFeatured:" << customFeatured; + qDebug() << "ViewCount:" << viewCount; + qDebug() << "Private:" << privateDeck; + qDebug() << "Unlisted:" << unlisted; + qDebug() << "TheoryCrafted:" << theoryCrafted; + qDebug() << "Points:" << points; + qDebug() << "UserInput:" << userInput; + owner.debugPrint(); + qDebug() << "CommentRoot:" << commentRoot; + qDebug() << "Editors:" << editors; + qDebug() << "ParentFolderId:" << parentFolderId; + qDebug() << "Bookmarked:" << bookmarked; + qDebug() << "DeckTags:" << deckTags; + qDebug() << "PlaygroupDeckUrl:" << playgroupDeckUrl; + qDebug() << "CardPackage:" << cardPackage; + for (auto card : cards) { + card.debugPrint(); + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.h new file mode 100644 index 000000000..8cb81cac3 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck.h @@ -0,0 +1,71 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_H + +#include "../card/archidekt_api_response_card.h" +#include "../card/archidekt_api_response_card_entry.h" +#include "../deck_listings/archidekt_api_response_deck_owner.h" +#include "archidekt_api_response_deck_category.h" + +#include +#include +#include +#include +#include + +class ArchidektApiResponseDeck +{ +public: + // Constructor + ArchidektApiResponseDeck() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + QVector getCards() const + { + return cards; + }; + + QVector getCategories() const + { + return categories; + } + + QString getDeckName() const + { + return name; + }; + +private: + int id; + QString name; + int size; + QString updatedAt; + QString createdAt; + int deckFormat; + int edhBracket; + QString featured; + QString customFeatured; + int viewCount; + bool privateDeck; + bool unlisted; + bool theoryCrafted; + int points; + int userInput; + ArchidektApiResponseDeckOwner owner; + int commentRoot; + QString editors; + int parentFolderId; + bool bookmarked; + QVector categories; + QStringList deckTags; + QString playgroupDeckUrl; + QString cardPackage; + QVector cards; + QStringList customCards; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.cpp new file mode 100644 index 000000000..990293bc6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.cpp @@ -0,0 +1,19 @@ +#include "archidekt_api_response_deck_category.h" + +void ArchidektApiResponseDeckCategory::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + name = json.value("name").toString(); + isPremier = json.value("isPremier").toBool(); + includedInDeck = json.value("includedInDeck").toBool(); + includedInPrice = json.value("includedInPrice").toBool(); +} + +void ArchidektApiResponseDeckCategory::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "Name:" << name; + qDebug() << "isPremier:" << isPremier; + qDebug() << "IncludedInDeck:" << includedInDeck; + qDebug() << "IncludedInPrice:" << includedInPrice; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.h new file mode 100644 index 000000000..5e218b6a4 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck/archidekt_api_response_deck_category.h @@ -0,0 +1,55 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_CATEGORY_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_CATEGORY_H + +#include "../card/archidekt_api_response_card.h" +#include "../card/archidekt_api_response_card_entry.h" +#include "../deck_listings/archidekt_api_response_deck_owner.h" + +#include +#include +#include +#include +#include + +class ArchidektApiResponseDeckCategory +{ +public: + // Constructor + ArchidektApiResponseDeckCategory() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + [[nodiscard]] int getId() const + { + return id; + } + [[nodiscard]] QString getName() const + { + return name; + } + [[nodiscard]] bool getIsPremier() const + { + return isPremier; + } + [[nodiscard]] bool getIncludedInDeck() const + { + return includedInDeck; + } + [[nodiscard]] bool getIncludedInPrice() const + { + return includedInPrice; + } + +private: + int id; + QString name; + bool isPremier; + bool includedInDeck; + bool includedInPrice; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_CATEGORY_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.cpp new file mode 100644 index 000000000..16ea60487 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.cpp @@ -0,0 +1,64 @@ +#include "archidekt_api_response_deck_listing_container.h" + +#include "archidekt_api_response_deck_owner.h" + +#include +#include +#include + +void ArchidektApiResponseDeckListingContainer::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + name = json.value("name").toString(); + size = json.value("size").toInt(); + updatedAt = json.value("updatedAt").toString(); + createdAt = json.value("createdAt").toString(); + deckFormat = json.value("deckFormat").toInt(); + edhBracket = json.value("edhBracket").toInt(); + featured = json.value("featured").toString(); + customFeatured = json.value("customFeatured").toString(); + viewCount = json.value("viewCount").toInt(); + privateDeck = json.value("private").toBool(); + unlisted = json.value("unlisted").toBool(); + theoryCrafted = json.value("theoryCrafted").toBool(); + game = json.value("game").toString(); + hasDescription = json.value("hasDescription").toBool(); + // TODO + // tags = {""}; + parentFolderId = json.value("parentFolderId").toInt(); + owner.fromJson(json.value("owner").toObject()); + + auto colorsJson = json.value("colors").toObject(); + + for (auto color : colorsJson.keys()) { + colors[color] = colorsJson[color].toInt(); + } + + cardPackage = json.value("cardPackage").toString(); + contest = json.value("contest").toString(); +} + +void ArchidektApiResponseDeckListingContainer::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "Name:" << name; + qDebug() << "Size:" << size; + qDebug() << "UpdatedAt:" << updatedAt; + qDebug() << "CreatedAt:" << createdAt; + qDebug() << "DeckFormat:" << deckFormat; + qDebug() << "EdhBracket:" << edhBracket; + qDebug() << "Featured:" << featured; + qDebug() << "CustomFeatured:" << customFeatured; + qDebug() << "ViewCount:" << viewCount; + qDebug() << "Private:" << privateDeck; + qDebug() << "Unlisted:" << unlisted; + qDebug() << "TheoryCrafted:" << theoryCrafted; + qDebug() << "Game:" << game; + qDebug() << "HasDescription:" << hasDescription; + qDebug() << "Tags:" << tags; + qDebug() << "ParentFolderId:" << parentFolderId; + owner.debugPrint(); + qDebug() << "Colors:" << colors; + qDebug() << "CardPackage" << cardPackage; + qDebug() << "Contest:" << contest; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.h new file mode 100644 index 000000000..8924bb3f5 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_listing_container.h @@ -0,0 +1,133 @@ +#ifndef COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_CONTAINER_H +#define COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_CONTAINER_H + +#include "archidekt_api_response_deck_owner.h" + +#include +#include +#include +#include +#include + +class ArchidektApiResponseDeckListingContainer +{ +public: + // Constructor + ArchidektApiResponseDeckListingContainer() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + [[nodiscard]] int getId() const + { + return id; + } + [[nodiscard]] QString getName() const + { + return name; + } + [[nodiscard]] int getSize() const + { + return size; + } + [[nodiscard]] QString getUpdatedAt() const + { + return updatedAt; + } + [[nodiscard]] QString getCreatedAt() const + { + return createdAt; + } + [[nodiscard]] int getDeckFormat() const + { + return deckFormat; + } + [[nodiscard]] int getEDHBracket() const + { + return edhBracket; + } + [[nodiscard]] QString getFeatured() const + { + return featured; + } + [[nodiscard]] QString getCustomFeatured() const + { + return customFeatured; + } + [[nodiscard]] int getViewCount() const + { + return viewCount; + } + [[nodiscard]] bool getPrivateDeck() const + { + return privateDeck; + } + [[nodiscard]] bool getUnlisted() const + { + return unlisted; + } + [[nodiscard]] bool getTheoryCrafted() const + { + return theoryCrafted; + } + [[nodiscard]] QString getGame() const + { + return game; + } + [[nodiscard]] bool getHasDescription() const + { + return hasDescription; + } + [[nodiscard]] QStringList getTags() const + { + return tags; + } + [[nodiscard]] int getParentFolderId() const + { + return parentFolderId; + } + [[nodiscard]] ArchidektApiResponseDeckOwner getOwner() const + { + return owner; + } + [[nodiscard]] QMap getColors() const + { + return colors; + } + [[nodiscard]] QString getCardPackage() const + { + return cardPackage; + } + [[nodiscard]] QString getContest() const + { + return contest; + } + +private: + int id; + QString name; + int size; + QString updatedAt; + QString createdAt; + int deckFormat; + int edhBracket; + QString featured; + QString customFeatured; + int viewCount; + bool privateDeck; + bool unlisted; + bool theoryCrafted; + QString game; + bool hasDescription; + QStringList tags; + int parentFolderId; + ArchidektApiResponseDeckOwner owner; + QMap colors; + QString cardPackage; + QString contest; +}; + +#endif // COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_CONTAINER_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.cpp new file mode 100644 index 000000000..2ba926f8b --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.cpp @@ -0,0 +1,22 @@ +#include "archidekt_api_response_deck_owner.h" + +void ArchidektApiResponseDeckOwner::fromJson(const QJsonObject &json) +{ + id = json.value("id").toInt(); + userName = json.value("username").toString(); + avatar = QUrl(json.value("avatar").toString()); + moderator = json.value("moderator").toBool(); + pledgeLevel = json.value("pledgeLevel").toInt(); + // TODO but not really important + // roles = {""}; +} + +void ArchidektApiResponseDeckOwner::debugPrint() const +{ + qDebug() << "Id:" << id; + qDebug() << "UserName:" << userName; + qDebug() << "Avatar:" << avatar; + qDebug() << "Moderator:" << moderator; + qDebug() << "PledgeLevel:" << pledgeLevel; + qDebug() << "Roles:" << roles; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.h new file mode 100644 index 000000000..df3f48fd2 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/api_response/deck_listings/archidekt_api_response_deck_owner.h @@ -0,0 +1,36 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_OWNER_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_OWNER_H + +#include +#include +#include +#include +#include + +class ArchidektApiResponseDeckOwner +{ +public: + // Constructor + ArchidektApiResponseDeckOwner() = default; + + // Parse deck-related data from JSON + void fromJson(const QJsonObject &json); + + // Debug method for logging + void debugPrint() const; + + [[nodiscard]] QString getName() const + { + return userName; + } + +private: + int id; + QString userName; + QUrl avatar; + bool moderator; + int pledgeLevel; + QStringList roles; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_OWNER_H 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 new file mode 100644 index 000000000..33ee67a1f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -0,0 +1,157 @@ +#include "archidekt_api_response_deck_display_widget.h" + +#include "../../../../../deck_loader/deck_loader.h" +#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" +#include "../../../../cards/card_size_widget.h" +#include "../../../../cards/deck_card_zone_display_widget.h" +#include "../../../../visual_deck_editor/visual_deck_display_options_widget.h" +#include "../api_response/deck/archidekt_api_response_deck.h" + +#include +#include + +ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, + ArchidektApiResponseDeck _response, + CardSizeWidget *_cardSizeSlider) + : QWidget(parent), response(_response), cardSizeSlider(_cardSizeSlider) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + + openInEditorButton = new QPushButton(this); + layout->addWidget(openInEditorButton); + + connect(openInEditorButton, &QPushButton::clicked, this, + &ArchidektApiResponseDeckDisplayWidget::actOpenInDeckEditor); + + displayOptionsWidget = new VisualDeckDisplayOptionsWidget(this); + layout->addWidget(displayOptionsWidget); + + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::groupCriteriaChanged, this, + &ArchidektApiResponseDeckDisplayWidget::onGroupCriteriaChange); + + scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + layout->addWidget(scrollArea); + + container = new QWidget(scrollArea); + + scrollArea->setWidget(container); + + containerLayout = new QVBoxLayout(container); + container->setLayout(containerLayout); + + zoneContainer = new QWidget(container); + containerLayout->addWidget(zoneContainer); + zoneContainerLayout = new QVBoxLayout(zoneContainer); + zoneContainer->setLayout(zoneContainerLayout); + + QString tempDeck; + QTextStream deckStream(&tempDeck); + + for (auto card : response.getCards()) { + QString fullName = card.getCard().getOracleCard().value("name").toString(); + // We don't really care about the second card, the card database already has it as a relation + QString cleanName = fullName.split("//").first().trimmed(); + + tempDeck += QString("%1 %2 (%3) %4\n") + .arg(card.getQuantity()) + .arg(cleanName) + .arg(card.getCard().getEdition().getEditionCode().toUpper()) + .arg(card.getCard().getCollectorNumber()); + } + + model = new DeckListModel(this); + connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); + model->getDeckList()->loadFromStream_Plain(deckStream, false); + + DeckLoader::resolveSetNameAndNumberToProviderID(model->getDeckList()); + + model->rebuildTree(); + + retranslateUi(); +} + +void ArchidektApiResponseDeckDisplayWidget::retranslateUi() +{ + openInEditorButton->setText(tr("Open Deck in Deck Editor")); +} + +void ArchidektApiResponseDeckDisplayWidget::onGroupCriteriaChange(const QString &activeGroupCriteria) +{ + model->setActiveGroupCriteria(DeckListModelGroupCriteria::fromString(activeGroupCriteria)); + model->sort(1, Qt::AscendingOrder); +} + +void ArchidektApiResponseDeckDisplayWidget::actOpenInDeckEditor() +{ + auto loader = new DeckLoader(this); + loader->getDeckList()->loadFromString_Native(model->getDeckList()->writeToString_Native()); + + loader->getDeckList()->setName(response.getDeckName()); + + emit openInDeckEditor(loader); +} + +void ArchidektApiResponseDeckDisplayWidget::clearAllDisplayWidgets() +{ + for (auto idx : indexToWidgetMap.keys()) { + auto displayWidget = indexToWidgetMap.value(idx); + zoneContainerLayout->removeWidget(displayWidget); + indexToWidgetMap.remove(idx); + delete displayWidget; + } +} + +void ArchidektApiResponseDeckDisplayWidget::decklistModelReset() +{ + clearAllDisplayWidgets(); + constructZoneWidgetsFromDeckListModel(); +} + +void ArchidektApiResponseDeckDisplayWidget::constructZoneWidgetsFromDeckListModel() +{ + qDebug() << model->rowCount(model->getRoot()); + QSortFilterProxyModel proxy; + proxy.setSourceModel(model); + proxy.setSortRole(Qt::EditRole); + proxy.sort(1, Qt::AscendingOrder); + + for (int i = 0; i < proxy.rowCount(); ++i) { + QModelIndex proxyIndex = proxy.index(i, 0); + QModelIndex sourceIndex = proxy.mapToSource(proxyIndex); + + // Make a persistent index from the *source* model + QPersistentModelIndex persistent(sourceIndex); + + if (indexToWidgetMap.contains(persistent)) { + continue; + } + + DeckCardZoneDisplayWidget *zoneDisplayWidget = + new DeckCardZoneDisplayWidget(zoneContainer, model, nullptr, persistent, + model->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), + "maintype", {"name"}, DisplayType::Overlap, 20, 10, cardSizeSlider); + + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::sortCriteriaChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::onActiveSortCriteriaChanged); + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::groupCriteriaChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged); + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::displayTypeChanged, zoneDisplayWidget, + &DeckCardZoneDisplayWidget::refreshDisplayType); + zoneContainerLayout->addWidget(zoneDisplayWidget); + + indexToWidgetMap.insert(persistent, zoneDisplayWidget); + } +} + +void ArchidektApiResponseDeckDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + layout->invalidate(); + layout->activate(); + layout->update(); +} 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 new file mode 100644 index 000000000..3d624d293 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.h @@ -0,0 +1,125 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_DISPLAY_WIDGET_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_DISPLAY_WIDGET_H + +#include "../../../../../deck_loader/deck_loader.h" +#include "../../../../cards/card_size_widget.h" +#include "../../../../general/layout_containers/flow_widget.h" +#include "../../../../visual_deck_editor/visual_deck_display_options_widget.h" +#include "../api_response/deck/archidekt_api_response_deck.h" +#include "deck_list_model.h" + +#include +#include +#include +#include +#include + +/** + * @class ArchidektApiResponseDeckDisplayWidget + * @brief Displays a full deck fetched from an Archidekt API response. + * + * This widget visualizes all cards in a deck retrieved from the Archidekt API. + * It supports: + * - Interactive display options via a VisualDeckDisplayOptionsWidget. + * - Scrollable display of deck zones/cards with DeckCardZoneDisplayWidget. + * - Integration with a CardSizeWidget slider for card scaling. + * - Opening the deck in a deck editor. + * + * The widget internally constructs a DeckListModel from the Archidekt API response, + * then builds zone widgets for each group of cards according to the active group + * criteria. It also responds dynamically to model resets or sorting/grouping changes. + * + * ### Signals + * - `requestNavigation(QString url)` — triggered when navigation to a deck URL is requested. + * - `openInDeckEditor(DeckLoader *loader)` — emitted when the user chooses to open the deck + * in the deck editor. + * + * ### Features + * - Automatically generates DeckCardZoneDisplayWidget instances for each card group. + * - Provides a scrollable layout for decks of arbitrary size. + * - Updates layouts dynamically when resized or when display/group/sort criteria change. + */ +class ArchidektApiResponseDeckDisplayWidget : public QWidget +{ + Q_OBJECT + +signals: + /** + * @brief Emitted when navigation to a deck URL is requested. + * @param url URL of the deck on Archidekt. + */ + void requestNavigation(QString url); + + /** + * @brief Emitted when the deck should be opened in the deck editor. + * @param loader Initialized DeckLoader containing the deck data. + */ + void openInDeckEditor(DeckLoader *loader); + +public: + /** + * @brief Constructs a display widget for an Archidekt deck. + * @param parent Parent widget. + * @param response API deck data container. + * @param cardSizeSlider Slider controlling card scaling. + */ + explicit ArchidektApiResponseDeckDisplayWidget(QWidget *parent, + ArchidektApiResponseDeck response, + CardSizeWidget *cardSizeSlider); + + /** + * @brief Updates all UI text for retranslation/localization. + * + * Called when the application language changes. + */ + void retranslateUi(); + + /** + * @brief Opens the deck in the deck editor via DeckLoader. + */ + void actOpenInDeckEditor(); + + /** + * @brief Clears all dynamically generated card zone display widgets. + */ + void clearAllDisplayWidgets(); + + /** + * @brief Handles model reset by clearing and reconstructing display widgets. + */ + void decklistModelReset(); + + /** + * @brief Builds DeckCardZoneDisplayWidget instances from the current DeckListModel. + */ + void constructZoneWidgetsFromDeckListModel(); + +private slots: + /** + * @brief Slot triggered when the active group criteria change. + * @param activeGroupCriteria Name of the new grouping criteria. + */ + void onGroupCriteriaChange(const QString &activeGroupCriteria); + +private: + ArchidektApiResponseDeck response; ///< API deck data container + CardSizeWidget *cardSizeSlider; ///< Slider for adjusting card sizes + QVBoxLayout *layout; ///< Main vertical layout + QPushButton *openInEditorButton; ///< Button to open deck in editor + VisualDeckDisplayOptionsWidget *displayOptionsWidget; ///< Controls grouping/sorting/display + QScrollArea *scrollArea; ///< Scrollable area for deck zones + QWidget *zoneContainer; ///< Container for deck zones + QVBoxLayout *zoneContainerLayout; ///< Layout for deck zones + QWidget *container; ///< Outer container for scroll area + QHash indexToWidgetMap; ///< Maps model indices to widgets + QVBoxLayout *containerLayout; ///< Layout for container + DeckListModel *model; ///< Deck list model +protected slots: + /** + * @brief Updates layout and display on resize. + * @param event Resize event. + */ + void resizeEvent(QResizeEvent *event) override; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_DISPLAY_WIDGET_H 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 new file mode 100644 index 000000000..1b633c4d3 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -0,0 +1,235 @@ +#include "archidekt_api_response_deck_entry_display_widget.h" + +#include "../../../../../card_picture_loader/card_picture_loader.h" +#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" +#include "../../../../general/display/background_plate_widget.h" +#include "../../../../general/display/color_bar.h" +#include "archidekt_deck_preview_image_display_widget.h" + +#include +#include +#include +#include +#include +#include + +#define ARCHIDEKT_DEFAULT_IMAGE "https://storage.googleapis.com/topdekt-user/images/archidekt_deck_card_shadow.jpg" + +QString timeAgo(const QString ×tamp) +{ + QDateTime dt = QDateTime::fromString(timestamp, Qt::ISODate); + + if (!dt.isValid()) + return timestamp; // fallback if parsing fails + + qint64 secs = dt.secsTo(QDateTime::currentDateTimeUtc()); + + if (secs < 60) + return QString("%1 seconds ago").arg(secs); + if (secs < 3600) + return QString("%1 minutes ago").arg(secs / 60); + if (secs < 86400) + return QString("%1 hours ago").arg(secs / 3600); + if (secs < 30 * 86400) + return QString("%1 days ago").arg(secs / 86400); + if (secs < 365 * 86400) + return QString("%1 months ago").arg(secs / (30 * 86400)); + + return QString("%1 years ago").arg(secs / (365 * 86400)); +} + +ArchidektApiResponseDeckEntryDisplayWidget::ArchidektApiResponseDeckEntryDisplayWidget( + QWidget *parent, + ArchidektApiResponseDeckListingContainer _response, + QNetworkAccessManager *_imageNetworkManager) + : QWidget(parent), response(_response), imageNetworkManager(_imageNetworkManager) +{ + layout = new QVBoxLayout(this); + setLayout(layout); + + this->setMaximumWidth(400); + this->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + auto headerLayout = new QVBoxLayout(); + + previewWidget = new ArchidektDeckPreviewImageDisplayWidget(this); + + previewWidget->setMaximumWidth(400); + previewWidget->setMinimumHeight(300); // consistent height + + // Set deck name (ellided) + QFontMetrics fm(previewWidget->topLeftLabel->font()); + QString elided = fm.elidedText(response.getName(), Qt::ElideRight, 280); + previewWidget->topLeftLabel->setLabelText(elided); + previewWidget->topLeftLabel->setToolTip(response.getName()); + + // Set count + previewWidget->topRightLabel->setLabelText(QString::number(response.getSize())); + + // EDH bracket (skip if 0) + if (response.getEDHBracket() != 0) { + previewWidget->bottomLeftLabel->setLabelText(QString("EDH: %1").arg(response.getEDHBracket())); + } else { + previewWidget->bottomLeftLabel->hide(); + } + + // Views + previewWidget->bottomRightLabel->setLabelText(QString("Views: %1").arg(response.getViewCount())); + + // Use preview->imageLabel for image loading + picture = previewWidget->imageLabel; + + imageUrl = response.getFeatured().isEmpty() ? QUrl(ARCHIDEKT_DEFAULT_IMAGE) : QUrl(response.getFeatured()); + + QNetworkRequest req(imageUrl); + QNetworkReply *reply = imageNetworkManager->get(req); + + // tag the reply with "this" so we know it belongs to us later + reply->setProperty("deckWidget", QVariant::fromValue(this)); + reply->setProperty("requestedUrl", imageUrl); + + connect(imageNetworkManager, &QNetworkAccessManager::finished, this, + &ArchidektApiResponseDeckEntryDisplayWidget::onPreviewImageLoadFinished); + + headerLayout->addWidget(previewWidget); + + auto colors = response.getColors(); + + ColorBar *colorBar = new ColorBar(colors, this); + colorBar->setMinPercentThreshold(3); + colorBar->setFixedHeight(22); + + headerLayout->addWidget(colorBar); + + // Create a shared plate for the labels + backgroundPlateWidget = new BackgroundPlateWidget(this); + backgroundPlateWidget->setFixedHeight(120); // Adjust height to fit all labels + + QVBoxLayout *plateLayout = new QVBoxLayout(backgroundPlateWidget); + + // Add labels to the plate layout + QLabel *ownerLabel = new QLabel(QString("Owner: %1").arg(response.getOwner().getName())); + plateLayout->addWidget(ownerLabel); + + QLabel *createdAtLabel = new QLabel(QString("Created: %1").arg(timeAgo(response.getCreatedAt()))); + plateLayout->addWidget(createdAtLabel); + + QLabel *updatedAtLabel = new QLabel(QString("Updated: %1").arg(timeAgo(response.getUpdatedAt()))); + plateLayout->addWidget(updatedAtLabel); + + // Add the shared plate to the header layout + headerLayout->addWidget(backgroundPlateWidget); + + layout->addLayout(headerLayout); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::mousePressEvent(QMouseEvent *event) +{ + QWidget::mousePressEvent(event); + actRequestNavigationToDeck(); +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ArchidektApiResponseDeckEntryDisplayWidget::enterEvent(QEnterEvent *event) +#else +void ArchidektApiResponseDeckEntryDisplayWidget::enterEvent(QEvent *event) +#endif +{ + QWidget::enterEvent(event); + backgroundPlateWidget->setFocused(true); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::leaveEvent(QEvent *event) +{ + QWidget::leaveEvent(event); + backgroundPlateWidget->setFocused(false); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::setScaleFactor(int scale) +{ + scaleFactor = scale; + updateScaledPreview(); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::onPreviewImageLoadFinished(QNetworkReply *reply) +{ + // Check if this is our reply + void *owner = reply->property("deckWidget").value(); + if (owner != this) { + return; // not our reply + } + + // Check that the requested URL matches what we asked + QUrl requestedUrl = reply->property("requestedUrl").toUrl(); + if (requestedUrl != imageUrl) { + reply->deleteLater(); + return; + } + + QPixmap loaded; + + if (reply->error() != QNetworkReply::NoError || !loaded.loadFromData(reply->readAll())) { + CardPictureLoader::getCardBackLoadingFailedPixmap(loaded, QSize(400, 400)); + } + + originalPixmap = loaded; + + // Always scale preview widget to this ratio + previewWidget->setAspectRatio(DESIGN_RATIO); + previewWidget->setPreviewWidth(400); + + // Initial scaling + updateScaledPreview(); + + reply->deleteLater(); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::updateScaledPreview() +{ + if (originalPixmap.isNull()) { + return; + } + + int baseWidth = 400; + int newWidth = baseWidth * scaleFactor / 100; + int newHeight = static_cast(newWidth * DESIGN_RATIO); + + previewWidget->setFixedSize(newWidth, newHeight); + + // Scale image to fill the preview area (crop edges) + QPixmap scaled = + originalPixmap.scaled(newWidth, newHeight, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + // Crop to exact target size + QRect cropRect((scaled.width() - newWidth) / 2, (scaled.height() - newHeight) / 2, newWidth, newHeight); + QPixmap cropped = scaled.copy(cropRect); + + picture->setPixmap(cropped); + picture->setFixedSize(newWidth, newHeight); + + // Update the elided deck name based on new width + 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->setToolTip(response.getName()); + + setFixedWidth(newWidth); + + layout->invalidate(); + layout->activate(); + updateGeometry(); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + layout->invalidate(); + layout->activate(); + layout->update(); +} + +void ArchidektApiResponseDeckEntryDisplayWidget::actRequestNavigationToDeck() +{ + emit requestNavigation(QString("https://archidekt.com/api/decks/%1/").arg(response.getId())); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h new file mode 100644 index 000000000..365a99c9c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h @@ -0,0 +1,121 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_ENTRY_DISPLAY_WIDGET_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_ENTRY_DISPLAY_WIDGET_H + +#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" +#include "../api_response/deck_listings/archidekt_api_response_deck_listing_container.h" +#include "archidekt_deck_preview_image_display_widget.h" + +#include +#include +#include +#include +#include + +class BackgroundPlateWidget; + +/** + * @class ArchidektApiResponseDeckEntryDisplayWidget + * @brief Displays a single Archidekt deck listing as a preview card with metadata. + * + * This widget renders a deck entry received from an Archidekt API response. It includes: + * - A scaled deck preview image loaded asynchronously via QNetworkAccessManager. + * - Elided deck name in the top-left corner. + * - Deck size, EDH bracket, and view count labels. + * - A color distribution bar summarizing deck colors. + * - Metadata labels including owner, creation date, and last update date. + * + * The widget dynamically scales the preview image and labels according to a linked + * CardSizeWidget slider. Hovering over the widget highlights the background plate, + * and clicking emits a `requestNavigation` signal pointing to the deck URL. + * + * ### Features + * - Asynchronous image loading with fallback to a default placeholder image. + * - Maintains a fixed aspect ratio for the preview image (150:267). + * - Updates text elision dynamically when resized or scaled. + * - Integrates with FlowWidget containers for scrollable deck galleries. + * + * ### Signals + * - `requestNavigation(QString url)` — emitted when the widget is clicked to request + * navigation to the deck's Archidekt page. + * + * ### Slots + * - `actRequestNavigationToDeck()` — triggers navigation. + * - `setScaleFactor(int scale)` — adjusts preview image scaling. + */ +class ArchidektApiResponseDeckEntryDisplayWidget : public QWidget +{ + Q_OBJECT + +signals: + /** + * @brief Emitted when the user requests navigation. + * @param url Full URL to the Archidekt page. + */ + void requestNavigation(QString url); + +public: + /** + * @brief Constructs a deck entry display widget. + * @param parent Parent widget. + * @param response API container holding deck listing data. + * @param imageNetworkManager Shared network manager for fetching preview images. + */ + explicit ArchidektApiResponseDeckEntryDisplayWidget(QWidget *parent, + ArchidektApiResponseDeckListingContainer response, + QNetworkAccessManager *imageNetworkManager); + + /** + * @brief Handles finished network replies for preview images. + * @param reply QNetworkReply containing image data. + * + * Validates that the reply corresponds to this widget and updates the preview image. + */ + void onPreviewImageLoadFinished(QNetworkReply *reply); + + /** + * @brief Updates the scaled preview image and adjusts layout accordingly. + */ + void updateScaledPreview(); + + /** + * @brief Ensures layout responds correctly on resize events. + * @param event Resize event. + */ + void resizeEvent(QResizeEvent *event) override; + +public slots: + /** + * @brief Emits `requestNavigation` for the deck's URL. + */ + void actRequestNavigationToDeck(); + + /** + * @brief Sets a scaling factor (percentage) for the preview image. + * @param scale Scale percentage (100 = normal size). + */ + void setScaleFactor(int scale); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *event) override; ///< Qt6 hover enter +#else + void enterEvent(QEvent *event) override; ///< Qt5 hover enter +#endif + void leaveEvent(QEvent *event) override; + +private: + QVBoxLayout *layout; ///< Main vertical layout + ArchidektApiResponseDeckListingContainer response; ///< Deck data + QUrl imageUrl; ///< URL of the deck's preview image + QNetworkAccessManager *imageNetworkManager; ///< Shared network manager + ArchidektDeckPreviewImageDisplayWidget *previewWidget; ///< Widget showing the deck preview + QLabel *picture; ///< QLabel displaying the scaled pixmap + QPixmap originalPixmap; ///< Original image for scaling (avoids degradation) + int scaleFactor = 100; ///< Current scaling percentage + BackgroundPlateWidget *backgroundPlateWidget; ///< Plate for metadata labels + static constexpr float DESIGN_RATIO = 150.0f / 267.0f; ///< Design aspect ratio +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_ENTRY_DISPLAY_WIDGET_H 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 new file mode 100644 index 000000000..5747ce90d --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp @@ -0,0 +1,47 @@ +#include "archidekt_api_response_deck_listings_display_widget.h" + +#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" +#include "archidekt_api_response_deck_entry_display_widget.h" + +ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsDisplayWidget( + QWidget *parent, + ArchidektDeckListingApiResponse response, + CardSizeWidget *_cardSizeSlider) + : QWidget(parent), cardSizeSlider(_cardSizeSlider) +{ + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + + imageNetworkManager = new QNetworkAccessManager(this); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + imageNetworkManager->setTransferTimeout(); // Use Qt's default timeout +#endif + + imageNetworkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); + + // Add widgets for deck listings + auto deckListings = response.results; + for (const auto &deckListing : deckListings) { + 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); + } + + layout->addWidget(flowWidget); +} + +void ArchidektApiResponseDeckListingsDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + layout->invalidate(); + layout->activate(); + layout->update(); +} 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 new file mode 100644 index 000000000..f00941239 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.h @@ -0,0 +1,97 @@ +#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_LISTINGS_DISPLAY_WIDGET_H +#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_LISTINGS_DISPLAY_WIDGET_H + +#include "../../../../cards/card_size_widget.h" +#include "../../../../general/layout_containers/flow_widget.h" +#include "../api_response/archidekt_deck_listing_api_response.h" + +#include +#include +#include +#include +#include + +/** + * @class ArchidektApiResponseDeckListingsDisplayWidget + * @brief Displays a scrollable horizontal list of Archidekt deck listings with dynamic card sizing. + * + * This widget serves as a container for multiple + * ArchidektApiResponseDeckEntryDisplayWidget instances, each representing one deck listing + * returned from an Archidekt API call. + * + * ### Responsibilities + * - Creates a **FlowWidget** that arranges deck entries horizontally with wrapping. + * - Creates a shared **QNetworkAccessManager** for entry widgets to fetch card thumbnails. + * - Connects a **CardSizeWidget** slider to all deck entries to dynamically rescale their preview cards. + * - Propagates deck-navigation requests (`requestNavigation`) from children to the parent. + * + * ### Layout + * The widget uses a single `QHBoxLayout` containing the `FlowWidget`. + * The FlowWidget automatically manages child flow and scrollbar behavior (horizontal = off, + * vertical = auto), providing an efficient scrollable gallery of deck entries. + * + * ### API Integration + * The constructor consumes an `ArchidektDeckListingApiResponse`, iterates through its + * `results`, and instantiates a child entry widget for each deck. + * + * ### Signals + * - `requestNavigation(QString url)` — emitted whenever a child entry widget requests + * navigation to a deck or related Archidekt page. + * + * ### Performance Notes + * - All entry widgets share a single QNetworkAccessManager instance to reuse connections + * and avoid redundant session creation. + * - `resizeEvent()` forces layout invalidation to ensure the flow layout responds properly + * to container resizing. + */ +class ArchidektApiResponseDeckListingsDisplayWidget : public QWidget +{ + Q_OBJECT + +signals: + /** + * @brief Emitted when a child deck entry requests that the UI navigate to a particular URL. + * @param url The destination URL (typically an Archidekt deck page). + */ + void requestNavigation(QString url); + +public: + /** + * @brief Constructs a widget that displays multiple deck listing previews. + * + * @param parent Parent widget. + * @param response The Archidekt API response containing deck listings. + * @param cardSizeSlider A slider widget used to dynamically resize card previews. + * + * Each deck in `response.results` becomes its own + * ArchidektApiResponseDeckEntryDisplayWidget, added to the FlowWidget. + */ + explicit ArchidektApiResponseDeckListingsDisplayWidget(QWidget *parent, + ArchidektDeckListingApiResponse response, + CardSizeWidget *cardSizeSlider); + + /** + * @brief Ensures FlowWidget layout properly recomputes on resize. + * + * Forces the layout to invalidate and activate itself so that the + * FlowWidget recalculates wrapping and child placement. + * + * @param event Resize event. + */ + void resizeEvent(QResizeEvent *event) override; + +private: + /// Slider controlling the scale of card thumbnails in all deck entry widgets. + CardSizeWidget *cardSizeSlider; + + /// Main horizontal layout containing the FlowWidget. + QHBoxLayout *layout; + + /// Container providing scrollable multi-row flow layout of deck entries. + FlowWidget *flowWidget; + + /// Shared network manager used to download card images for all child entry widgets. + QNetworkAccessManager *imageNetworkManager; +}; + +#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_LISTINGS_DISPLAY_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp new file mode 100644 index 000000000..ced08491e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp @@ -0,0 +1,80 @@ +#include "archidekt_deck_preview_image_display_widget.h" + +#include +#include + +ArchidektDeckPreviewImageDisplayWidget::ArchidektDeckPreviewImageDisplayWidget(QWidget *parent) : QWidget(parent) +{ + imageLabel = new QLabel(this); + imageLabel->setAlignment(Qt::AlignCenter); + + topLeftLabel = new ShadowBackgroundLabel(this, ""); + topRightLabel = new ShadowBackgroundLabel(this, ""); + bottomLeftLabel = new ShadowBackgroundLabel(this, ""); + bottomRightLabel = new ShadowBackgroundLabel(this, ""); + + QFont f; + f.setBold(true); + f.setPointSize(16); + topLeftLabel->setFont(f); + topRightLabel->setFont(f); + bottomLeftLabel->setFont(f); + bottomRightLabel->setFont(f); + + // Raise so labels appear above image + topLeftLabel->raise(); + topRightLabel->raise(); + bottomLeftLabel->raise(); + bottomRightLabel->raise(); +} + +void ArchidektDeckPreviewImageDisplayWidget::setAspectRatio(float ratio) +{ + aspectRatio = ratio; +} + +void ArchidektDeckPreviewImageDisplayWidget::setPreviewWidth(int width) +{ + int height = int(width * aspectRatio); + + setFixedSize(width, height); + updateGeometry(); + update(); +} + +void ArchidektDeckPreviewImageDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + // Full size for the image + imageLabel->setGeometry(rect()); + + topLeftLabel->adjustSize(); + topRightLabel->adjustSize(); + bottomLeftLabel->adjustSize(); + bottomRightLabel->adjustSize(); + + // Padding settings + const int horizontalPadding = 8; + const int topPadding = 6; + const int bottomPadding = 6; + + // Left-aligned, top-aligned (Deck Name) + topLeftLabel->move(horizontalPadding, topPadding); + + // Right-aligned, top-aligned (Card Count) + topRightLabel->move(width() - topRightLabel->width() - horizontalPadding, topPadding); + + // Bottom-left, bottom-aligned (EDH bracket) + bottomLeftLabel->move(horizontalPadding, height() - bottomLeftLabel->height() - bottomPadding); + + // Bottom-right, bottom-aligned (views) + bottomRightLabel->move(width() - bottomRightLabel->width() - horizontalPadding, + height() - bottomRightLabel->height() - bottomPadding); + + // Ensure labels stay above image + topLeftLabel->raise(); + topRightLabel->raise(); + bottomLeftLabel->raise(); + bottomRightLabel->raise(); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h new file mode 100644 index 000000000..053823c45 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h @@ -0,0 +1,66 @@ +#ifndef COCKATRICE_ARCHIDEKT_DECK_PREVIEW_IMAGE_DISPLAY_WIDGET_H +#define COCKATRICE_ARCHIDEKT_DECK_PREVIEW_IMAGE_DISPLAY_WIDGET_H + +#include "../../../../general/display/shadow_background_label.h" + +#include +#include + +/** + * @class ArchidektDeckPreviewImageDisplayWidget + * @brief Widget for displaying a deck preview image with overlaid metadata labels. + * + * This widget shows a deck's preview image along with several overlay labels: + * - Top-left: Deck name. + * - Top-right: Card count. + * - Bottom-left: EDH bracket (if applicable). + * - Bottom-right: View count. + * + * Labels automatically scale and position themselves relative to the widget's size. + * The image can be scaled while maintaining a specified aspect ratio. + * + * ### Features + * - Adjustable preview width and aspect ratio. + * - Labels automatically repositioned on resize. + * - Supports overlaying multiple pieces of metadata with shadowed labels for readability. + */ +class ArchidektDeckPreviewImageDisplayWidget : public QWidget +{ + Q_OBJECT +public: + /** + * @brief Constructs the deck preview display widget. + * @param parent Optional parent widget. + */ + explicit ArchidektDeckPreviewImageDisplayWidget(QWidget *parent = nullptr); + + /** + * @brief Sets the aspect ratio for the preview image (height / width). + * @param ratio Aspect ratio to maintain. + */ + void setAspectRatio(float ratio); + + /** + * @brief Sets the width of the preview image; height is adjusted according to the aspect ratio. + * @param width Desired width in pixels. + */ + void setPreviewWidth(int width); + + QLabel *imageLabel; ///< QLabel to display the deck image + ShadowBackgroundLabel *topLeftLabel; ///< Overlay label at top-left (deck name) + ShadowBackgroundLabel *topRightLabel; ///< Overlay label at top-right (card count) + ShadowBackgroundLabel *bottomLeftLabel; ///< Overlay label at bottom-left (EDH bracket) + ShadowBackgroundLabel *bottomRightLabel; ///< Overlay label at bottom-right (views) + +protected: + /** + * @brief Handles resize events to reposition the image and overlay labels. + * @param event Resize event. + */ + void resizeEvent(QResizeEvent *event) override; + +private: + float aspectRatio = 1.0f; ///< Aspect ratio to maintain for the preview image +}; + +#endif // COCKATRICE_ARCHIDEKT_DECK_PREVIEW_IMAGE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp new file mode 100644 index 000000000..7f04987c1 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -0,0 +1,562 @@ +#include "tab_archidekt.h" + +#include "../../../../../client/settings/cache_settings.h" +#include "../../../cards/additional_info/mana_symbol_widget.h" +#include "../../tab_supervisor.h" +#include "api_response/archidekt_deck_listing_api_response.h" +#include "display/archidekt_api_response_deck_display_widget.h" +#include "display/archidekt_api_response_deck_listings_display_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) +{ + networkManager = new QNetworkAccessManager(this); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + networkManager->setTransferTimeout(); // Use Qt's default timeout +#endif + + networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); + connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *))); + + searchDebounceTimer = new QTimer(this); + searchDebounceTimer->setSingleShot(true); // We only want it to fire once after inactivity + searchDebounceTimer->setInterval(300); // 300ms debounce + + connect(searchDebounceTimer, &QTimer::timeout, this, [this]() { doSearchImmediate(); }); + + container = new QWidget(this); + mainLayout = new QVBoxLayout(container); + mainLayout->setContentsMargins(0, 0, 0, 0); + container->setLayout(mainLayout); + + navigationContainer = new QWidget(container); + navigationContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + navigationLayout = new QHBoxLayout(navigationContainer); + navigationLayout->setSpacing(3); + navigationContainer->setLayout(navigationLayout); + + // Sort by + + orderByCombo = new QComboBox(navigationContainer); + orderByCombo->addItems({"name", "updatedAt", "createdAt", "viewCount", "size", "edhBracket"}); + orderByCombo->setCurrentText("updatedAt"); // Pre-select updatedAt + + // Asc/Desc toggle + orderDirButton = new QPushButton(tr("Desc."), navigationContainer); + orderDirButton->setCheckable(true); // checked = DESC, unchecked = ASC + orderDirButton->setChecked(true); + + connect(orderByCombo, &QComboBox::currentTextChanged, this, &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 + + for (const QChar &color : colorIdentity) { + auto *manaSymbol = new ManaSymbolWidget(navigationContainer, color, false, true); + manaSymbol->setFixedWidth(25); + colorLayout->addWidget(manaSymbol); + + connect(manaSymbol, &ManaSymbolWidget::colorToggled, this, [this](QChar c, bool active) { + if (active) { + activeColors.insert(c); + } else { + activeColors.remove(c); + } + doSearch(); + }); + } + + logicalAndCheck = new QCheckBox("Require ALL colors", navigationContainer); + + // Formats + + formatLabel = new QLabel(this); + + formatSettingsWidget = new SettingsButtonWidget(this); + + QStringList formatNames = {"Standard", "Modern", "Commander", "Legacy", "Vintage", + "Pauper", "Custom", "Frontier", "Future Std", "Penny Dreadful", + "1v1 Commander", "Dual Commander", "Brawl"}; + + for (int i = 0; i < formatNames.size(); ++i) { + QCheckBox *formatCheckBox = new QCheckBox(formatNames[i], navigationContainer); + connect(formatCheckBox, &QCheckBox::clicked, this, &TabArchidekt::doSearch); + formatChecks << formatCheckBox; + formatSettingsWidget->addSettingsWidget(formatCheckBox); + } + + // EDH Bracket + edhBracketCombo = new QComboBox(navigationContainer); + edhBracketCombo->addItem(tr("Any Bracket")); + edhBracketCombo->addItems({"1", "2", "3", "4", "5"}); + + connect(edhBracketCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch); + + // Search for Card Packages instead of Decks + packagesCheck = new QCheckBox("Packages", navigationContainer); + + connect(packagesCheck, &QCheckBox::clicked, this, [this]() { + bool disable = packagesCheck->isChecked(); + for (auto *cb : formatChecks) + cb->setEnabled(!disable); + commandersField->setEnabled(!disable); + deckTagNameField->setEnabled(!disable); + edhBracketCombo->setCurrentIndex(0); + edhBracketCombo->setEnabled(!disable); + doSearch(); + }); + + // Deck Name + nameField = new QLineEdit(navigationContainer); + nameField->setPlaceholderText(tr("Deck name contains...")); + + // Owner Name + ownerField = new QLineEdit(navigationContainer); + ownerField->setPlaceholderText(tr("Owner name contains...")); + + // Contained cards + cardsField = new QLineEdit(navigationContainer); + cardsField->setPlaceholderText("Deck contains card..."); + + // Commanders + commandersField = new QLineEdit(navigationContainer); + commandersField->setPlaceholderText("Deck has commander..."); + + // DB supplemented card search + auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + auto displayModel = new CardDatabaseDisplayModel(this); + displayModel->setSourceModel(cardDatabaseModel); + auto *searchModel = new CardSearchModel(displayModel, this); + + auto *proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + auto *completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(10); + + cardsField->setCompleter(completer); + commandersField->setCompleter(completer); + + connect(cardsField, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); + + connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) + completer->complete(); + }); + + connect(commandersField, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); + + connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) { + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) + completer->complete(); + }); + + // Tag Name + deckTagNameField = new QLineEdit(navigationContainer); + deckTagNameField->setPlaceholderText("Deck tag"); + + connect(deckTagNameField, &QLineEdit::textChanged, this, &TabArchidekt::doSearch); + + // Search button + searchPushButton = new QPushButton(navigationContainer); + searchPushButton->setText("Search"); + + 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); + + 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 + + connect(minDeckSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); + connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch); + + // Page number + pageLabel = new QLabel(navigationContainer); + + pageSpin = new QSpinBox(navigationContainer); + pageSpin->setRange(1, 9999); + pageSpin->setValue(1); + + connect(pageSpin, QOverload::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); + + // Page display + currentPageDisplay = new QWidget(container); + currentPageLayout = new QVBoxLayout(currentPageDisplay); + currentPageLayout->setContentsMargins(0, 0, 0, 0); + currentPageDisplay->setLayout(currentPageLayout); + + // Layout composition + + // Sort section + navigationLayout->addWidget(orderByCombo); + navigationLayout->addWidget(orderDirButton); + + // Colors section + navigationLayout->addLayout(colorLayout); + navigationLayout->addWidget(logicalAndCheck); + + // Formats section + navigationLayout->addWidget(formatSettingsWidget); + navigationLayout->addWidget(formatLabel); + + // EDH Bracket + navigationLayout->addWidget(edhBracketCombo); + + // Packages toggle + navigationLayout->addWidget(packagesCheck); + + // Deck name + navigationLayout->addWidget(nameField); + + // Owner name + navigationLayout->addWidget(ownerField); + + // Contained cards + navigationLayout->addWidget(cardsField); + + // Commanders + navigationLayout->addWidget(commandersField); + + // Deck tag + navigationLayout->addWidget(deckTagNameField); + + // Search button + navigationLayout->addWidget(searchPushButton); + + // Card size settings + navigationLayout->addWidget(settingsButton); + + // 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(); +} + +void TabArchidekt::retranslateUi() +{ + searchPushButton->setText(tr("Search")); + formatLabel->setText(tr("Formats")); + minDeckSizeLabel->setText(tr("Min. # of Cards:")); + pageLabel->setText(tr("Page:")); +} + +QString TabArchidekt::buildSearchUrl() +{ + QUrlQuery query; + + // orderBy (field + direction) + { + QString field = orderByCombo->currentText(); + if (!field.isEmpty()) { + bool desc = orderDirButton->isChecked(); + QString final = desc ? "-" + field : field; + query.addQueryItem("orderBy", final); + } + } + + // Colors + QStringList selectedColors; + for (QChar c : activeColors) { + selectedColors.append(c); + } + if (!selectedColors.isEmpty()) { + query.addQueryItem("colors", selectedColors.join(",")); + } + + // logicalAnd + if (logicalAndCheck->isChecked()) { + query.addQueryItem("logicalAnd", "true"); + } + + // Formats + if (!packagesCheck->isChecked()) { + QStringList formatIds; + for (int i = 0; i < formatChecks.size(); ++i) + if (formatChecks[i]->isChecked()) { + formatIds << QString::number(i + 1); + } + + if (!formatIds.isEmpty()) { + query.addQueryItem("deckFormat", formatIds.join(",")); + } + } + + // edhBracket + if (!packagesCheck->isChecked()) { + if (!edhBracketCombo->currentText().isEmpty()) { + if (edhBracketCombo->currentText() != tr("Any Bracket")) { + query.addQueryItem("edhBracket", edhBracketCombo->currentText()); + } + } + } + + // Search for card packages instead of decks + if (packagesCheck->isChecked()) { + query.addQueryItem("packages", "true"); + } + + // Name + if (!nameField->text().isEmpty()) { + query.addQueryItem("name", nameField->text()); + } + + // owner + if (!ownerField->text().isEmpty()) { + query.addQueryItem("ownerUsername", ownerField->text()); + } + + // cards + if (!cardsField->text().isEmpty()) { + query.addQueryItem("cardName", cardsField->text()); + } + + // Commander Name + if (!packagesCheck->isChecked()) { + if (!commandersField->text().isEmpty()) { + query.addQueryItem("commanderName", commandersField->text()); + } + } + + // deckTagName + if (!packagesCheck->isChecked()) { + if (!deckTagNameField->text().isEmpty()) { + query.addQueryItem("deckTagName", deckTagNameField->text()); + } + } + + // page number + if (pageSpin->value() <= 1) { + query.addQueryItem("page", QString::number(pageSpin->value())); + } + + // Min deck size + if (minDeckSizeSpin->value() != 0) { + query.addQueryItem("size", QString::number(minDeckSizeSpin->value())); + + QString logic = "GTE"; // default + QString selected = minDeckSizeLogicCombo->currentText(); + if (selected == "≥") + logic = "GTE"; + else if (selected == "≤") + logic = "LTE"; + else + logic = ""; // Exact = unset + + if (!logic.isEmpty()) { + query.addQueryItem("sizeLogic", logic); + } + } + + // build final URL + QUrl url("https://archidekt.com/api/decks/v3/"); + url.setQuery(query); + + return url.toString(); +} + +void TabArchidekt::doSearch() +{ + searchDebounceTimer->start(); +} + +void TabArchidekt::doSearchImmediate() +{ + QString url = buildSearchUrl(); + QNetworkRequest req{QUrl(url)}; + networkManager->get(req); +} + +void TabArchidekt::actNavigatePage(QString url) +{ + QNetworkRequest request{QUrl(url)}; + networkManager->get(request); +} + +void TabArchidekt::getTopDecks() +{ + QNetworkRequest request{QUrl(buildSearchUrl())}; + networkManager->get(request); +} + +void TabArchidekt::processApiJson(QNetworkReply *reply) +{ + if (reply->error() != QNetworkReply::NoError) { + qDebug() << "Network error occurred:" << reply->errorString(); + reply->deleteLater(); + return; + } + + QByteArray responseData = reply->readAll(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + + if (!jsonDoc.isObject()) { + qDebug() << "Invalid JSON response received."; + reply->deleteLater(); + return; + } + + QJsonObject jsonObj = jsonDoc.object(); + + // Get the actual URL from the reply + QString responseUrl = reply->url().toString(); + + // Check if the response URL matches a commander request + if (responseUrl.startsWith("https://archidekt.com/api/decks/v3/")) { + processTopDecksResponse(jsonObj); + } else if (responseUrl.startsWith("https://archidekt.com/api/decks/")) { + processDeckResponse(jsonObj); + } else { + prettyPrintJson(jsonObj, 4); + } + + reply->deleteLater(); +} + +void TabArchidekt::processTopDecksResponse(QJsonObject reply) +{ + ArchidektDeckListingApiResponse deckData; + deckData.fromJson(reply); + + // **Remove previous page display to prevent stacking** + if (currentPageDisplay) { + mainLayout->removeWidget(currentPageDisplay); + delete currentPageDisplay; + currentPageDisplay = nullptr; + } + + // **Create new currentPageDisplay** + currentPageDisplay = new QWidget(container); + currentPageLayout = new QVBoxLayout(currentPageDisplay); + currentPageDisplay->setLayout(currentPageLayout); + + auto display = new ArchidektApiResponseDeckListingsDisplayWidget(currentPageDisplay, deckData, cardSizeSlider); + connect(display, &ArchidektApiResponseDeckListingsDisplayWidget::requestNavigation, this, + &TabArchidekt::actNavigatePage); + currentPageLayout->addWidget(display); + + mainLayout->addWidget(currentPageDisplay); + + // **Ensure layout stays correct** + mainLayout->setStretch(0, 0); // Keep navigationContainer at the top + mainLayout->setStretch(1, 1); // Make sure currentPageDisplay takes remaining space +} + +void TabArchidekt::processDeckResponse(QJsonObject reply) +{ + ArchidektApiResponseDeck deckData; + deckData.fromJson(reply); + + // **Remove previous page display to prevent stacking** + if (currentPageDisplay) { + mainLayout->removeWidget(currentPageDisplay); + delete currentPageDisplay; + currentPageDisplay = nullptr; + } + + // **Create new currentPageDisplay** + currentPageDisplay = new QWidget(container); + currentPageLayout = new QVBoxLayout(currentPageDisplay); + currentPageDisplay->setLayout(currentPageLayout); + + auto display = new ArchidektApiResponseDeckDisplayWidget(currentPageDisplay, deckData, cardSizeSlider); + connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage); + connect(display, &ArchidektApiResponseDeckDisplayWidget::openInDeckEditor, tabSupervisor, + &TabSupervisor::openDeckInNewTab); + currentPageLayout->addWidget(display); + + mainLayout->addWidget(currentPageDisplay); + + // **Ensure layout stays correct** + mainLayout->setStretch(0, 0); // Keep navigationContainer at the top + mainLayout->setStretch(1, 1); // Make sure currentPageDisplay takes remaining space +} + +void TabArchidekt::prettyPrintJson(const QJsonValue &value, int indentLevel) +{ + const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing + + if (value.isObject()) { + QJsonObject obj = value.toObject(); + for (auto it = obj.begin(); it != obj.end(); ++it) { + qDebug().noquote() << indent + it.key() + ":"; + prettyPrintJson(it.value(), indentLevel + 1); + } + } else if (value.isArray()) { + QJsonArray array = value.toArray(); + for (int i = 0; i < array.size(); ++i) { + qDebug().noquote() << indent + QString("[%1]:").arg(i); + prettyPrintJson(array[i], indentLevel + 1); + } + } else if (value.isString()) { + qDebug().noquote() << indent + "\"" + value.toString() + "\""; + } else if (value.isDouble()) { + qDebug().noquote() << indent + QString::number(value.toDouble()); + } else if (value.isBool()) { + qDebug().noquote() << indent + (value.toBool() ? "true" : "false"); + } else if (value.isNull()) { + qDebug().noquote() << indent + "null"; + } +} diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h new file mode 100644 index 000000000..68a5b78c9 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h @@ -0,0 +1,250 @@ +#ifndef COCKATRICE_TAB_ARCHIDEKT_H +#define COCKATRICE_TAB_ARCHIDEKT_H + +#include "../../interface/widgets/cards/card_size_widget.h" +#include "../../interface/widgets/quick_settings/settings_button_widget.h" +#include "../../tab.h" + +#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="; + +/** + * @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. + */ +class TabArchidekt : public Tab +{ + Q_OBJECT +public: + /** + * @brief Construct a new TabArchidekt object + * @param _tabSupervisor Parent tab supervisor responsible for tab management and callbacks + * + * Initializes the network manager, creates all UI components, sets up layouts, + * connects signals and slots, and triggers an initial fetch of top decks. + */ + explicit TabArchidekt(TabSupervisor *_tabSupervisor); + + /** + * @brief Update all UI text to reflect the current language or translation + * + * This function re-applies translations to all labels, buttons, and placeholders. + * It should be called after a language change. + */ + void retranslateUi() override; + + /** + * @brief Construct the search URL from all current filters + * @return QString Fully constructed URL including all query parameters + * + * The search URL is dynamically built using the state of all filter widgets. + * Parameters included: + * - Deck name + * - Owner + * - Included cards + * - Commander cards + * - Deck tag + * - Colors and logical AND requirement + * - Formats + * - EDH bracket + * - Packages toggle + * - Sorting field and direction + * - Minimum amount of cards in the deck + * - Pagination (page) + */ + QString buildSearchUrl(); + + /** + * @brief Retrieve the tab display text + * @return QString Human-readable title for the tab + * + * If a card is pre-selected (cardToQuery), its name is appended to the tab title. + */ + QString getTabText() const override + { + auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName(); + return tr("Archidekt: ") + cardName; + } + + /** + * @brief Get the card size slider widget + * @return CardSizeWidget* Pointer to the card size adjustment slider + * + * Allows external code to read or manipulate the current card size or hook up the sliders signals. + */ + CardSizeWidget *getCardSizeSlider() + { + return cardSizeSlider; + } + + /** @brief Network manager for handling API requests */ + QNetworkAccessManager *networkManager; + +public slots: + /** + * @brief Trigger a 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. + */ + void doSearch(); + void doSearchImmediate(); + /** + * @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, + * 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 + * + * Clears the previous page display and creates a new display widget for the results. + */ + void processTopDecksResponse(QJsonObject reply); + + /** + * @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. + */ + void processDeckResponse(QJsonObject reply); + + /** + * @brief Pretty-print a QJsonValue for debugging + * @param value The JSON value to print + * @param indentLevel The indentation depth (number of levels) + */ + void prettyPrintJson(const QJsonValue &value, int indentLevel); + + /** + * @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. + */ + void actNavigatePage(QString url); + + /** + * @brief Fetch top decks from the Archidekt API + * + * Called on initialization to populate the initial page display. + */ + void getTopDecks(); + +private: + QTimer *searchDebounceTimer; ///< Timer to debounce search requests by spin-boxes etc. + + // --------------------------------------------------------------------- + // 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 + + // --------------------------------------------------------------------- + // Sorting Controls + // --------------------------------------------------------------------- + + QComboBox *orderByCombo; ///< Dropdown for selecting the sort field + QPushButton *orderDirButton; ///< Toggle button for ascending/descending sort + + // --------------------------------------------------------------------- + // Color Filters + // --------------------------------------------------------------------- + + QSet activeColors; ///< Set of currently active mana colors + QCheckBox *logicalAndCheck; ///< Require ALL selected colors instead of ANY + + // --------------------------------------------------------------------- + // 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 + CardSizeWidget *cardSizeSlider; ///< Slider to adjust card size in results + + // --------------------------------------------------------------------- + // Minimum Cards in Deck + // --------------------------------------------------------------------- + + 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 + + // --------------------------------------------------------------------- + // Pagination + // --------------------------------------------------------------------- + + QLabel *pageLabel; ///< Label for current page selection + QSpinBox *pageSpin; ///< Spinner to select the page number for results + + // --------------------------------------------------------------------- + // Optional Context + // --------------------------------------------------------------------- + + CardInfoPtr cardToQuery; ///< Optional pre-selected card for initial filtering +}; + +#endif // COCKATRICE_TAB_ARCHIDEKT_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 18a98f5ec..df9387a6c 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -5,6 +5,7 @@ #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" +#include "api/archidekt/tab_archidekt.h" #include "api/edhrec/tab_edhrec_main.h" #include "tab_account.h" #include "tab_admin.h" @@ -140,6 +141,9 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabEdhRec = new QAction(this); connect(aTabEdhRec, &QAction::triggered, this, [this] { addEdhrecMainTab(); }); + aTabArchidekt = new QAction(this); + connect(aTabArchidekt, &QAction::triggered, this, [this] { addArchidektTab(); }); + aTabHome = new QAction(this); aTabHome->setCheckable(true); connect(aTabHome, &QAction::triggered, this, &TabSupervisor::actTabHome); @@ -204,6 +208,7 @@ void TabSupervisor::retranslateUi() aTabDeckEditor->setText(tr("Deck Editor")); aTabVisualDeckEditor->setText(tr("Visual Deck Editor")); aTabEdhRec->setText(tr("EDHRec")); + aTabArchidekt->setText(tr("Archidekt")); aTabHome->setText(tr("Home")); aTabVisualDeckStorage->setText(tr("&Visual Deck Storage")); aTabVisualDatabaseDisplay->setText(tr("Visual Database Display")); @@ -386,6 +391,7 @@ void TabSupervisor::resetTabsMenu() tabsMenu->addAction(aTabDeckEditor); tabsMenu->addAction(aTabVisualDeckEditor); tabsMenu->addAction(aTabEdhRec); + tabsMenu->addAction(aTabArchidekt); tabsMenu->addSeparator(); tabsMenu->addAction(aTabHome); tabsMenu->addAction(aTabVisualDeckStorage); @@ -899,6 +905,15 @@ TabEdhRecMain *TabSupervisor::addEdhrecMainTab() return tab; } +TabArchidekt *TabSupervisor::addArchidektTab() +{ + auto *tab = new TabArchidekt(this); + + myAddTab(tab); + setCurrentWidget(tab); + return tab; +} + TabVisualDatabaseDisplay *TabSupervisor::addVisualDatabaseDisplayTab() { auto *tab = new TabVisualDatabaseDisplay(this); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 9fd00f584..948ac6d7e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -11,6 +11,7 @@ #include "../../deck_loader/deck_loader.h" #include "../interface/widgets/server/user/user_list_proxy.h" #include "abstract_tab_deck_editor.h" +#include "api/archidekt/tab_archidekt.h" #include "api/edhrec/tab_edhrec.h" #include "api/edhrec/tab_edhrec_main.h" #include "tab_visual_database_display.h" @@ -110,7 +111,7 @@ private: QList deckEditorTabs; bool isLocalGame; - QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabVisualDeckStorage, + QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); @@ -172,6 +173,7 @@ public slots: TabDeckEditorVisual *addVisualDeckEditorTab(DeckLoader *deckToOpen); TabVisualDatabaseDisplay *addVisualDatabaseDisplayTab(); TabEdhRecMain *addEdhrecMainTab(); + TabArchidekt *addArchidektTab(); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); void switchToFirstAvailableNetworkTab(); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp new file mode 100644 index 000000000..c190038a5 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp @@ -0,0 +1,124 @@ +#include "visual_deck_display_options_widget.h" + +#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" + +VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) +{ + groupAndSortLayout = new QHBoxLayout(this); + groupAndSortLayout->setAlignment(Qt::AlignLeft); + this->setLayout(groupAndSortLayout); + + groupByLabel = new QLabel(this); + + groupByComboBox = new QComboBox(this); + if (auto visualDeckEditorWidget = qobject_cast(parent)) { + if (auto tabWidget = qobject_cast(visualDeckEditorWidget)) { + // Inside a central widget QWidget container inside TabDeckEditorVisual + if (auto tab = qobject_cast(tabWidget->parent()->parent())) { + auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox(); + groupByComboBox->setModel(originalBox->model()); + groupByComboBox->setModelColumn(originalBox->modelColumn()); + + // Original -> clone + connect(originalBox, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { groupByComboBox->setCurrentIndex(index); }); + + // Clone -> original + connect(groupByComboBox, QOverload::of(&QComboBox::currentIndexChanged), + [originalBox](int index) { originalBox->setCurrentIndex(index); }); + } + } + } else { + groupByComboBox->addItem( + tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::MAIN_TYPE))), + DeckListModelGroupCriteria::MAIN_TYPE); + groupByComboBox->addItem( + tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::MANA_COST))), + DeckListModelGroupCriteria::MANA_COST); + groupByComboBox->addItem( + tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::COLOR))), + DeckListModelGroupCriteria::COLOR); + groupByComboBox->setMinimumWidth(300); + connect(groupByComboBox, QOverload::of(&QComboBox::currentTextChanged), this, + &VisualDeckDisplayOptionsWidget::groupCriteriaChanged); + emit groupCriteriaChanged(groupByComboBox->currentText()); + } + + sortByLabel = new QLabel(this); + + sortCriteriaButton = new SettingsButtonWidget(this); + + sortLabel = new QLabel(sortCriteriaButton); + sortLabel->setWordWrap(true); + + QStringList sortProperties = {"colors", "cmc", "name", "maintype"}; + sortByListWidget = new QListWidget(); + sortByListWidget->setSelectionMode(QAbstractItemView::SingleSelection); + sortByListWidget->setDragDropMode(QAbstractItemView::InternalMove); + sortByListWidget->setDefaultDropAction(Qt::MoveAction); + + for (const QString &property : sortProperties) { + QListWidgetItem *item = new QListWidgetItem(property, sortByListWidget); + item->setFlags(item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + + connect(sortByListWidget->model(), &QAbstractItemModel::rowsMoved, this, + &VisualDeckDisplayOptionsWidget::onSortCriteriaChange); + onSortCriteriaChange(); + + sortByListWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + sortCriteriaButton->addSettingsWidget(sortLabel); + sortCriteriaButton->addSettingsWidget(sortByListWidget); + + displayTypeButton = new QPushButton(this); + connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckDisplayOptionsWidget::updateDisplayType); + + groupAndSortLayout->addWidget(groupByLabel); + groupAndSortLayout->addWidget(groupByComboBox); + groupAndSortLayout->addWidget(sortByLabel); + groupAndSortLayout->addWidget(sortCriteriaButton); + groupAndSortLayout->addWidget(displayTypeButton); + + retranslateUi(); +} + +void VisualDeckDisplayOptionsWidget::retranslateUi() +{ + groupByLabel->setText(tr("Group by:")); + groupByComboBox->setToolTip(tr("Change how cards are divided into categories/groups.")); + sortByLabel->setText(tr("Sort by:")); + sortLabel->setText(tr("Click and drag to change the sort order within the groups")); + sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); + displayTypeButton->setText(tr("Toggle Layout: Overlap")); + displayTypeButton->setToolTip( + tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); +} + +void VisualDeckDisplayOptionsWidget::onSortCriteriaChange() +{ + QStringList selectedCriteria; + for (int i = 0; i < sortByListWidget->count(); ++i) { + QListWidgetItem *item = sortByListWidget->item(i); + selectedCriteria.append(item->text()); // Collect user-defined sort order + } + + emit sortCriteriaChanged(selectedCriteria); +} + +void VisualDeckDisplayOptionsWidget::updateDisplayType() +{ + // Toggle the display type + currentDisplayType = (currentDisplayType == DisplayType::Overlap) ? DisplayType::Flat : DisplayType::Overlap; + + // Update UI and emit signal + switch (currentDisplayType) { + case DisplayType::Flat: + displayTypeButton->setText(tr("Toggle Layout: Flat")); + break; + case DisplayType::Overlap: + displayTypeButton->setText(tr("Toggle Layout: Overlap")); + break; + } + emit displayTypeChanged(currentDisplayType); +} diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h new file mode 100644 index 000000000..7a447753f --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h @@ -0,0 +1,138 @@ +#ifndef COCKATRICE_VISUAL_DECK_DISPLAY_OPTIONS_WIDGET_H +#define COCKATRICE_VISUAL_DECK_DISPLAY_OPTIONS_WIDGET_H + +#include "visual_deck_editor_widget.h" + +#include +#include +#include +#include +#include +#include + +/** + * @class VisualDeckDisplayOptionsWidget + * @brief A widget that controls how deck cards are displayed in the visual deck editor. + * + * This widget provides: + * - A **group-by** selector (QComboBox) + * - A **sort-by** multi-criteria, draggable list (QListWidget within a SettingsButtonWidget) + * - A **display-type toggler** (flat vs. overlap layout) + * + * Depending on whether the parent is a VisualDeckEditorWidget, this widget can mirror the + * original group by checkbox from the main deck editor UI to maintain synchronization. + * + * It emits signals whenever the grouping criterion, sorting criteria, or display mode changes. + */ +class VisualDeckDisplayOptionsWidget : public QWidget +{ + Q_OBJECT +signals: + /** + * @brief Emitted when the display type (flat or overlapping layout) changes. + * @param displayType The newly selected display layout. + */ + void displayTypeChanged(const DisplayType &displayType); + + /** + * @brief Emitted when a new grouping criterion is selected. + * @param activeGroupCriteria Name of the selected group-by criterion. + */ + void groupCriteriaChanged(const QString &activeGroupCriteria); + + /** + * @brief Emitted when the order of sort criteria changes. + * @param activeSortCriteria Ordered list of sorting keys. + */ + void sortCriteriaChanged(const QStringList &activeSortCriteria); + +public slots: + /** + * @brief Updates all UI text for retranslation/localization. + * + * Called when the application language changes. + */ + void retranslateUi(); + +public: + /** + * @brief Constructs a new VisualDeckDisplayOptionsWidget. + * @param parent The parent QWidget—may trigger cloning of models if the parent is a visual deck editor. + */ + explicit VisualDeckDisplayOptionsWidget(QWidget *parent); + + /** + * @brief Gets the current display type (Overlap or Flat). + */ + DisplayType getDisplayType() const + { + return currentDisplayType; + } + + /** + * @brief Gets the currently active group-by criterion. + */ + QString getActiveGroupCriteria() const + { + return activeGroupCriteria; + } + + /** + * @brief Gets the currently active ordered sort criteria. + */ + QStringList getActiveSortCriteria() const + { + return activeSortCriteria; + } + +private slots: + /** + * @brief Slot triggered whenever the sort list is reordered. + * + * Reads the QListWidget’s order and emits `sortCriteriaChanged()`. + */ + void onSortCriteriaChange(); + + /** + * @brief Toggles the display layout between flat and overlapping modes. + * + * Emits `displayTypeChanged()`. + */ + void updateDisplayType(); + +private: + /// Layout for grouping and sorting UI elements. + QHBoxLayout *groupAndSortLayout; + + /// Current deck display type. + DisplayType currentDisplayType = DisplayType::Overlap; + + /// Button used to toggle the display layout. + QPushButton *displayTypeButton; + + /// Label for the group-by selector. + QLabel *groupByLabel; + + /// Combo box listing group-by criteria. + QComboBox *groupByComboBox; + + /// Currently active group-by criterion. + QString activeGroupCriteria = "maintype"; + + /// Encapsulates the sort settings widgets (label + list). + SettingsButtonWidget *sortCriteriaButton; + + /// Label for “Sort by”. + QLabel *sortByLabel; + + /// Descriptive label inside the sort criteria button. + QLabel *sortLabel; + + /// Draggable list of sort criteria. + QListWidget *sortByListWidget; + + /// Ordered list of current sort criteria. + QStringList activeSortCriteria = {"name", "cmc", "colors", "maintype"}; +}; + +#endif // COCKATRICE_VISUAL_DECK_DISPLAY_OPTIONS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 5acad9df1..76cfe8c8e 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -9,6 +9,7 @@ #include "../general/layout_containers/flow_widget.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h" +#include "visual_deck_display_options_widget.h" #include #include @@ -109,75 +110,22 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, } }); - groupAndSortContainer = new QWidget(this); - groupAndSortLayout = new QHBoxLayout(groupAndSortContainer); - groupAndSortLayout->setAlignment(Qt::AlignLeft); - groupAndSortContainer->setLayout(groupAndSortLayout); + displayOptionsAndSearch = new QWidget(this); + displayOptionsAndSearchLayout = new QHBoxLayout(displayOptionsAndSearch); + displayOptionsAndSearchLayout->setAlignment(Qt::AlignLeft); + displayOptionsAndSearch->setLayout(displayOptionsAndSearchLayout); - groupByLabel = new QLabel(groupAndSortContainer); + displayOptionsWidget = new VisualDeckDisplayOptionsWidget(this); + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::displayTypeChanged, this, + &VisualDeckEditorWidget::displayTypeChanged); + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::groupCriteriaChanged, this, + &VisualDeckEditorWidget::activeGroupCriteriaChanged); + connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::sortCriteriaChanged, this, + &VisualDeckEditorWidget::activeSortCriteriaChanged); - groupByComboBox = new QComboBox(this); - if (auto tabWidget = qobject_cast(parent)) { - // Inside a central widget QWidget container inside TabDeckEditorVisual - if (auto tab = qobject_cast(tabWidget->parent()->parent())) { - auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox(); - groupByComboBox->setModel(originalBox->model()); - groupByComboBox->setModelColumn(originalBox->modelColumn()); - - // Original -> clone - connect(originalBox, QOverload::of(&QComboBox::currentIndexChanged), - [this](int index) { groupByComboBox->setCurrentIndex(index); }); - - // Clone -> original - connect(groupByComboBox, QOverload::of(&QComboBox::currentIndexChanged), - [originalBox](int index) { originalBox->setCurrentIndex(index); }); - } - } else { - QStringList groupProperties = {"maintype", "colors", "cmc", "name"}; - groupByComboBox->addItems(groupProperties); - groupByComboBox->setMinimumWidth(300); - connect(groupByComboBox, QOverload::of(&QComboBox::currentTextChanged), this, - &VisualDeckEditorWidget::actChangeActiveGroupCriteria); - actChangeActiveGroupCriteria(); - } - - sortByLabel = new QLabel(groupAndSortContainer); - - sortCriteriaButton = new SettingsButtonWidget(this); - - sortLabel = new QLabel(sortCriteriaButton); - sortLabel->setWordWrap(true); - - QStringList sortProperties = {"colors", "cmc", "name", "maintype"}; - sortByListWidget = new QListWidget(); - sortByListWidget->setSelectionMode(QAbstractItemView::SingleSelection); - sortByListWidget->setDragDropMode(QAbstractItemView::InternalMove); - sortByListWidget->setDefaultDropAction(Qt::MoveAction); - - for (const QString &property : sortProperties) { - QListWidgetItem *item = new QListWidgetItem(property, sortByListWidget); - item->setFlags(item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled); - } - - connect(sortByListWidget->model(), &QAbstractItemModel::rowsMoved, this, - &VisualDeckEditorWidget::actChangeActiveSortCriteria); - actChangeActiveSortCriteria(); - - sortByListWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - - sortCriteriaButton->addSettingsWidget(sortLabel); - sortCriteriaButton->addSettingsWidget(sortByListWidget); - - displayTypeButton = new QPushButton(this); - connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckEditorWidget::updateDisplayType); - - groupAndSortLayout->addWidget(groupByLabel); - groupAndSortLayout->addWidget(groupByComboBox); - groupAndSortLayout->addWidget(sortByLabel); - groupAndSortLayout->addWidget(sortCriteriaButton); - groupAndSortLayout->addWidget(displayTypeButton); - groupAndSortLayout->addWidget(searchBar); - groupAndSortLayout->addWidget(searchPushButton); + displayOptionsAndSearchLayout->addWidget(displayOptionsWidget); + displayOptionsAndSearchLayout->addWidget(searchBar); + displayOptionsAndSearchLayout->addWidget(searchPushButton); scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); @@ -197,7 +145,7 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(), &SettingsCache::setVisualDeckEditorCardSize); - mainLayout->addWidget(groupAndSortContainer); + mainLayout->addWidget(displayOptionsAndSearch); mainLayout->addWidget(scrollArea); mainLayout->addWidget(cardSizeWidget); @@ -218,17 +166,9 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, void VisualDeckEditorWidget::retranslateUi() { searchBar->setPlaceholderText(tr("Type a card name here for suggestions from the database...")); - groupByLabel->setText(tr("Group by:")); - groupByComboBox->setToolTip(tr("Change how cards are divided into categories/groups.")); - sortByLabel->setText(tr("Sort by:")); - sortLabel->setText(tr("Click and drag to change the sort order within the groups")); searchPushButton->setText(tr("Quick search and add card")); searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add " "preferred printing to the deck on pressing enter")); - sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); - displayTypeButton->setText(tr("Toggle Layout: Overlap")); - displayTypeButton->setToolTip( - tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); } void VisualDeckEditorWidget::setSelectionModel(QItemSelectionModel *model) @@ -326,8 +266,9 @@ void VisualDeckEditorWidget::constructZoneWidgetForIndex(QPersistentModelIndex p { DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget( zoneContainer, deckListModel, selectionModel, persistent, - deckListModel->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), activeGroupCriteria, - activeSortCriteria, currentDisplayType, 20, 10, cardSizeWidget); + deckListModel->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), + displayOptionsWidget->getActiveGroupCriteria(), displayOptionsWidget->getActiveSortCriteria(), + displayOptionsWidget->getDisplayType(), 20, 10, cardSizeWidget); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::requestCleanup, this, @@ -338,7 +279,7 @@ void VisualDeckEditorWidget::constructZoneWidgetForIndex(QPersistentModelIndex p &DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged); connect(this, &VisualDeckEditorWidget::displayTypeChanged, zoneDisplayWidget, &DeckCardZoneDisplayWidget::refreshDisplayType); - zoneDisplayWidget->refreshDisplayType(currentDisplayType); + zoneDisplayWidget->refreshDisplayType(displayOptionsWidget->getDisplayType()); zoneContainerLayout->addWidget(zoneDisplayWidget); indexToWidgetMap.insert(persistent, zoneDisplayWidget); @@ -370,48 +311,12 @@ void VisualDeckEditorWidget::updateZoneWidgets() { } -void VisualDeckEditorWidget::updateDisplayType() -{ - // Toggle the display type - currentDisplayType = (currentDisplayType == DisplayType::Overlap) ? DisplayType::Flat : DisplayType::Overlap; - - // Update UI and emit signal - switch (currentDisplayType) { - case DisplayType::Flat: - displayTypeButton->setText(tr("Toggle Layout: Flat")); - break; - case DisplayType::Overlap: - displayTypeButton->setText(tr("Toggle Layout: Overlap")); - break; - } - emit displayTypeChanged(currentDisplayType); -} - void VisualDeckEditorWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); zoneContainer->setMaximumWidth(scrollArea->viewport()->width()); } -void VisualDeckEditorWidget::actChangeActiveGroupCriteria() -{ - activeGroupCriteria = groupByComboBox->currentText(); - emit activeGroupCriteriaChanged(activeGroupCriteria); -} - -void VisualDeckEditorWidget::actChangeActiveSortCriteria() -{ - QStringList selectedCriteria; - for (int i = 0; i < sortByListWidget->count(); ++i) { - QListWidgetItem *item = sortByListWidget->item(i); - selectedCriteria.append(item->text()); // Collect user-defined sort order - } - - activeSortCriteria = selectedCriteria; - - emit activeSortCriteriaChanged(selectedCriteria); -} - void VisualDeckEditorWidget::decklistModelReset() { clearAllDisplayWidgets(); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h index 524762d89..3a60d09a7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -23,6 +23,7 @@ #include #include +class VisualDeckDisplayOptionsWidget; class DeckCardZoneDisplayWidget; enum class DisplayType { @@ -55,7 +56,6 @@ public: public slots: void decklistDataChanged(QModelIndex topLeft, QModelIndex bottomRight); void updateZoneWidgets(); - void updateDisplayType(); void cleanupInvalidZones(DeckCardZoneDisplayWidget *displayWidget); void onCardAddition(const QModelIndex &parent, int first, int last); void onCardRemoval(const QModelIndex &parent, int first, int last); @@ -73,8 +73,6 @@ signals: protected slots: void onHover(const ExactCard &hoveredCard); void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); - void actChangeActiveGroupCriteria(); - void actChangeActiveSortCriteria(); void decklistModelReset(); private: @@ -85,19 +83,10 @@ private: CardDatabaseDisplayModel *cardDatabaseDisplayModel; CardCompleterProxyModel *proxyModel; QCompleter *completer; + QWidget *displayOptionsAndSearch; + QHBoxLayout *displayOptionsAndSearchLayout; + VisualDeckDisplayOptionsWidget *displayOptionsWidget; QPushButton *searchPushButton; - DisplayType currentDisplayType = DisplayType::Overlap; - QPushButton *displayTypeButton; - QWidget *groupAndSortContainer; - QHBoxLayout *groupAndSortLayout; - QLabel *groupByLabel; - QComboBox *groupByComboBox; - QString activeGroupCriteria = "maintype"; - SettingsButtonWidget *sortCriteriaButton; - QLabel *sortByLabel; - QLabel *sortLabel; - QListWidget *sortByListWidget; - QStringList activeSortCriteria = {"name", "cmc", "colors", "maintype"}; QScrollArea *scrollArea; QWidget *zoneContainer; QVBoxLayout *zoneContainerLayout; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 9b96c0ce6..1e4a53553 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -74,6 +74,29 @@ enum Type MANA_COST, /**< Group cards by their total mana cost. */ COLOR /**< Group cards by their color identity. */ }; +static inline QString toString(Type t) +{ + switch (t) { + case MAIN_TYPE: + return "Main Type"; + case MANA_COST: + return "Mana Cost"; + case COLOR: + return "Colors"; + } + return {}; +} + +static inline Type fromString(const QString &s) +{ + if (s == "Main Type") + return MAIN_TYPE; + if (s == "Mana Cost") + return MANA_COST; + if (s == "Colors") + return COLOR; + return MAIN_TYPE; // default +} } // namespace DeckListModelGroupCriteria /** @@ -234,8 +257,8 @@ public: */ [[nodiscard]] QModelIndex findCard(const QString &cardName, const QString &zoneName, - const QString &providerId = "", - const QString &cardNumber = "") const; + const QString &providerId = "", + const QString &cardNumber = "") const; /** * @brief Adds a card using the preferred printing if available. @@ -296,8 +319,8 @@ private: QModelIndex nodeToIndex(AbstractDecklistNode *node) const; [[nodiscard]] DecklistModelCardNode *findCardNode(const QString &cardName, const QString &zoneName, - const QString &providerId = "", - const QString &cardNumber = "") const; + const QString &providerId = "", + const QString &cardNumber = "") const; void emitRecursiveUpdates(const QModelIndex &index); void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);