[Feature] TabArchidekt and Archidekt API integration (#6348)

* TabArchidekt and Archidekt API integration.


Took 37 seconds

Took 4 minutes

Took 40 seconds

Took 4 minutes

* Lint.

* Lont.

* Search bar, fancier display, resolve providerId

* Delegate click to base.

* Be explicit for pedantic compilers.

* Liiint.

* Leave them default I guess

* Leave them default I guess

* Small fixes.

* New utility display widgets.

* New style for deck listing.

* Lint.

* Lont.

* Scale things.

* Delegate paint to base.

* Use default Archidekt preview image for decks without featured.

* Consistent sizes.

* Increase font size, qt version guard.

* More version guards.

* Clean up filter layout, use mana symbols.

* Set content margins.

* Refresh on filter change.

* Lint.

* Better elision.

* Query actual new endpoints, new query parameters.

* Doxygen, reorder fields in constructor, readability.

* Update page size doc to min size.

* Update initial min deck size value.

* Add label to page selection.

* Okay, so, people upload a lot of 1 card decks frequently.

* Whoops.

* Add a selection combobox for sorting logic.

* Debounce and limit searches.

* Include.

* Lint.

* Don't imply that Archidekt supports multiple cards/commander names.

* Let's not lambda it and slot it instead.

* Overload.

* Add button to home tab.

Took 8 minutes

* Adjust to selection model change.

Took 5 minutes

* Cleanup auto-generated comments.

Took 8 minutes

* Remember card sizes.

Took 1 minute

* Initialize with correct size.

Took 3 minutes

* Use correct placeholders.

Took 2 minutes

* Style lint.

Took 16 minutes

* Parse double-faced cards correctly.

* Parse double-faced cards correctly.

* Allow TabArchidekt to use VDE group/sort/display buttons

* Lint.

* Indicate that things are clickable.

* Min treshold for nicer display.

* Lint.

* We have good labels at home.

* We do a little linting.

* Qt version guards.

* Qt5 is the devil.

* Update comments.

* Lint comments.

* More doxys.

* One more doxy.

* Lint.

* Update.

* Small fixes.

Took 7 minutes

Took 13 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2025-11-30 08:41:01 +01:00
committed by GitHub
parent de13c22552
commit eab4d435f8
43 changed files with 3285 additions and 141 deletions

View File

@@ -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

View File

@@ -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,8 +182,10 @@ void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSort
void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
if (selectionModel) {
selectionModel->clearSelection();
}
}
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
{

View File

@@ -38,8 +38,10 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
displayCards();
connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::onCategoryAddition);
if (selectionModel) {
connect(selectionModel, &QItemSelectionModel::selectionChanged, this,
&DeckCardZoneDisplayWidget::onSelectionChanged);
}
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::onCategoryRemoval);
}

View File

@@ -0,0 +1,34 @@
#include "background_plate_widget.h"
#include <QBrush>
#include <QColor>
#include <QPainter>
#include <QPen>
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();
}

View File

@@ -0,0 +1,22 @@
#ifndef COCKATRICE_BACKGROUND_PLATE_WIDGET_H
#define COCKATRICE_BACKGROUND_PLATE_WIDGET_H
#include <QWidget>
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

View File

@@ -0,0 +1,163 @@
#include "color_bar.h"
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent) : QWidget(parent), colors(_colors)
{
setMouseTracking(true);
}
void ColorBar::setColors(const QMap<QString, int> &_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<QString> 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<QString, QColor> 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;
}

View File

@@ -0,0 +1,128 @@
#ifndef COCKATRICE_COLOR_BAR_H
#define COCKATRICE_COLOR_BAR_H
#include <QColor>
#include <QMap>
#include <QString>
#include <QWidget>
/**
* @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<QString, int>`. 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 segments 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<QString, int> &colors, QWidget *parent = nullptr);
/**
* @brief Updates the color distribution map.
* @param colors New color → count mapping.
*
* Triggers an immediate repaint.
*/
void setColors(const QMap<QString, int> &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<QString, int> 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

View File

@@ -13,11 +13,17 @@
ShadowBackgroundLabel::ShadowBackgroundLabel(QWidget *parent, const QString &text) : QLabel(parent)
{
setAttribute(Qt::WA_TranslucentBackground); // Allows transparency.
setText("<font color='white'>" + text + "</font>"); ///< Ensures the text is rendered in white.
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("<font color='white'>" + text + "</font>"); ///< Ensures the text is rendered in white.
update();
}
/**
* @brief Handles resizing of the label.
*

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,29 @@
#include "archidekt_deck_listing_api_response.h"
#include <QDebug>
#include <QJsonArray>
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();
}
}

View File

@@ -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 <QJsonObject>
#include <QString>
#include <QUrl>
class ArchidektDeckListingApiResponse
{
public:
int count;
QUrl next;
QVector<ArchidektApiResponseDeckListingContainer> results;
void fromJson(const QJsonObject &json);
void debugPrint() const;
};
#endif // COCKATRICE_DECK_LISTING_API_RESPONSE_H

View File

@@ -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;
}

View File

@@ -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 <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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

View File

@@ -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();
}

View File

@@ -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 <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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

View File

@@ -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;
}

View File

@@ -0,0 +1,51 @@
#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_EDITION_H
#define COCKATRICE_ARCHIDEKT_API_RESPONSE_EDITION_H
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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

View File

@@ -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();
}
}

View File

@@ -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 <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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<ArchidektApiResponseCardEntry> getCards() const
{
return cards;
};
QVector<ArchidektApiResponseDeckCategory> 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<ArchidektApiResponseDeckCategory> categories;
QStringList deckTags;
QString playgroupDeckUrl;
QString cardPackage;
QVector<ArchidektApiResponseCardEntry> cards;
QStringList customCards;
};
#endif // COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_H

View File

@@ -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;
}

View File

@@ -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 <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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

View File

@@ -0,0 +1,64 @@
#include "archidekt_api_response_deck_listing_container.h"
#include "archidekt_api_response_deck_owner.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
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;
}

View File

@@ -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 <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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<QString, int> 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<QString, int> colors;
QString cardPackage;
QString contest;
};
#endif // COCKATRICE_ARCHIDEKT_DECK_LISTING_API_RESPONSE_CONTAINER_H

View File

@@ -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;
}

View File

@@ -0,0 +1,36 @@
#ifndef COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_OWNER_H
#define COCKATRICE_ARCHIDEKT_API_RESPONSE_DECK_OWNER_H
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
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

View File

@@ -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 <QSortFilterProxyModel>
#include <libcockatrice/card/database/card_database_manager.h>
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();
}

View File

@@ -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 <QPushButton>
#include <QResizeEvent>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
/**
* @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<QPersistentModelIndex, QWidget *> 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

View File

@@ -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 <QHBoxLayout>
#include <QLabel>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPixmap>
#include <QWidget>
#define ARCHIDEKT_DEFAULT_IMAGE "https://storage.googleapis.com/topdekt-user/images/archidekt_deck_card_shadow.jpg"
QString timeAgo(const QString &timestamp)
{
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<void *>(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<void *>();
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<int>(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()));
}

View File

@@ -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 <QLabel>
#include <QNetworkAccessManager>
#include <QResizeEvent>
#include <QVBoxLayout>
#include <QWidget>
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

View File

@@ -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();
}

View File

@@ -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 <QNetworkAccessManager>
#include <QResizeEvent>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
/**
* @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

View File

@@ -0,0 +1,80 @@
#include "archidekt_deck_preview_image_display_widget.h"
#include <QFontMetrics>
#include <QPainter>
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();
}

View File

@@ -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 <QLabel>
#include <QWidget>
/**
* @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

View File

@@ -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 <QCompleter>
#include <QDebug>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPushButton>
#include <QRegularExpression>
#include <QResizeEvent>
#include <QUrlQuery>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
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<int>::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<int>::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";
}
}

View File

@@ -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 <QCheckBox>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QNetworkAccessManager>
#include <QPushButton>
#include <QSpinBox>
#include <QString>
#include <libcockatrice/card/database/card_database.h>
/** 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<QChar> 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<QCheckBox *> formatChecks; ///< Individual checkboxes for each format
// ---------------------------------------------------------------------
// EDH Bracket / Package Toggle
// ---------------------------------------------------------------------
QComboBox *edhBracketCombo; ///< Dropdown for EDH bracket selection
QCheckBox *packagesCheck; ///< Toggle for searching card packages instead of full decks
// ---------------------------------------------------------------------
// Basic Search Fields
// ---------------------------------------------------------------------
QLineEdit *nameField; ///< Input for deck name filter
QLineEdit *ownerField; ///< Input for owner name filter
// ---------------------------------------------------------------------
// Card Filters
// ---------------------------------------------------------------------
QLineEdit *cardsField; ///< Input for cards included in the deck (comma-separated)
QLineEdit *commandersField; ///< Input for commander cards (comma-separated)
// ---------------------------------------------------------------------
// Deck Tag
// ---------------------------------------------------------------------
QLineEdit *deckTagNameField; ///< Input for deck tag filtering
// ---------------------------------------------------------------------
// Search Trigger
// ---------------------------------------------------------------------
QPushButton *searchPushButton; ///< Button to trigger the search manually
// ---------------------------------------------------------------------
// UI Settings (Card Size)
// ---------------------------------------------------------------------
SettingsButtonWidget *settingsButton; ///< Container for additional UI settings
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

View File

@@ -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);

View File

@@ -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<AbstractTabDeckEditor *> 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();

View File

@@ -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<VisualDeckEditorWidget *>(parent)) {
if (auto tabWidget = qobject_cast<TabDeckEditorVisualTabWidget *>(visualDeckEditorWidget)) {
// Inside a central widget QWidget container inside TabDeckEditorVisual
if (auto tab = qobject_cast<TabDeckEditorVisual *>(tabWidget->parent()->parent())) {
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
groupByComboBox->setModel(originalBox->model());
groupByComboBox->setModelColumn(originalBox->modelColumn());
// Original -> clone
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { groupByComboBox->setCurrentIndex(index); });
// Clone -> original
connect(groupByComboBox, QOverload<int>::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<const QString &>::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);
}

View File

@@ -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 <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QWidget>
/**
* @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 QListWidgets 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

View File

@@ -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 <QCheckBox>
#include <QCompleter>
@@ -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<TabDeckEditorVisualTabWidget *>(parent)) {
// Inside a central widget QWidget container inside TabDeckEditorVisual
if (auto tab = qobject_cast<TabDeckEditorVisual *>(tabWidget->parent()->parent())) {
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
groupByComboBox->setModel(originalBox->model());
groupByComboBox->setModelColumn(originalBox->modelColumn());
// Original -> clone
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { groupByComboBox->setCurrentIndex(index); });
// Clone -> original
connect(groupByComboBox, QOverload<int>::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<const QString &>::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();

View File

@@ -23,6 +23,7 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <qscrollarea.h>
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;

View File

@@ -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
/**