Add the option to load decklists from Archidekt, Deckstats, Moxfield, TappedOut in deck editor and lobby (#6030)

* Add the option to load decklists from Archidekt, Deckstats, Moxfield, TappedOut in deck editor and lobby.

Took 3 hours 34 minutes

Took 9 seconds


Took 12 seconds

* Properly set quantities.

Took 11 minutes

* Warnings.

Took 5 minutes

* Static regexes.

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

* Category loggings and better warnings.

Took 18 minutes


Took 42 seconds

* use loadFromStream_Plain instead of manually adding CardNodes to the DeckList.

Took 30 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
This commit is contained in:
BruebachL 2025-07-15 05:12:25 +02:00 committed by GitHub
parent 83b90d472f
commit e05dad4267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 444 additions and 3 deletions

View file

@ -54,6 +54,9 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
aPrintDeck = new QAction(QString(), this);
connect(aPrintDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actPrintDeck);
aLoadDeckFromWebsite = new QAction(QString(), this);
connect(aLoadDeckFromWebsite, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromWebsite);
aExportDeckDecklist = new QAction(QString(), this);
connect(aExportDeckDecklist, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actExportDeckDecklist);
@ -97,6 +100,7 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
addMenu(saveDeckToClipboardMenu);
addSeparator();
addAction(aPrintDeck);
addAction(aLoadDeckFromWebsite);
addMenu(analyzeDeckMenu);
addSeparator();
addAction(deckEditor->filterDockWidget->aClearFilterOne);
@ -166,6 +170,7 @@ void DeckEditorMenu::retranslateUi()
aPrintDeck->setText(tr("&Print deck..."));
aLoadDeckFromWebsite->setText(tr("Load deck from online service..."));
analyzeDeckMenu->setTitle(tr("&Send deck to online service"));
aExportDeckDecklist->setText(tr("Create decklist (decklist.org)"));
aExportDeckDecklistXyz->setText(tr("Create decklist (decklist.xyz)"));

View file

@ -16,8 +16,8 @@ public:
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aExportDeckDecklist,
*aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
void setSaveStatus(bool newStatus);

View file

@ -0,0 +1,61 @@
#include "deck_link_to_api_transformer.h"
#include <QRegularExpression>
namespace DeckLinkToApiTransformer
{
static const QString TAPPEDOUT_BASE = "https://tappedout.net/mtg-decks/";
static const QString TAPPEDOUT_SUFFIX = "/?fmt=txt";
static const QString ARCHIDEKT_BASE = "https://archidekt.com/api/decks/";
static const QString ARCHIDEKT_SUFFIX = "/?format=json";
static const QString MOXFIELD_BASE = "https://api.moxfield.com/v2/decks/all/";
static const QString MOXFIELD_SUFFIX = "/";
static const QString DECKSTATS_SUFFIX = "?include_comments=1&export_mtgarena=1";
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
{
static QRegularExpression rxTappedOut("tappedout\\.net/(?:mtg-decks/)?([^/?#]+)");
static QRegularExpression rxArchidekt("archidekt\\.com/decks/(\\d+)");
static QRegularExpression rxMoxfield("moxfield\\.com/decks/([a-zA-Z0-9_-]+)");
static QRegularExpression rxDeckstats("deckstats\\.net/decks/(\\d+/[a-zA-Z0-9_-]+)");
QRegularExpressionMatch match;
if ((match = rxTappedOut.match(url)).hasMatch()) {
QString slug = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = TAPPEDOUT_BASE,
.deckID = slug,
.fullUrl = TAPPEDOUT_BASE + slug + TAPPEDOUT_SUFFIX,
.provider = DeckProvider::TappedOut};
return true;
} else if ((match = rxArchidekt.match(url)).hasMatch()) {
QString deckID = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = ARCHIDEKT_BASE,
.deckID = deckID,
.fullUrl = ARCHIDEKT_BASE + deckID + ARCHIDEKT_SUFFIX,
.provider = DeckProvider::Archidekt};
return true;
} else if ((match = rxMoxfield.match(url)).hasMatch()) {
QString deckID = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = MOXFIELD_BASE,
.deckID = deckID,
.fullUrl = MOXFIELD_BASE + deckID + MOXFIELD_SUFFIX,
.provider = DeckProvider::Moxfield};
return true;
} else if ((match = rxDeckstats.match(url)).hasMatch()) {
QString deckPath = match.captured(1);
outInfo = ParsedDeckInfo{.baseUrl = "https://deckstats.net/decks/",
.deckID = deckPath,
.fullUrl = "https://deckstats.net/decks/" + deckPath + DECKSTATS_SUFFIX,
.provider = DeckProvider::Deckstats};
return true;
}
return false;
}
} // namespace DeckLinkToApiTransformer

View file

@ -0,0 +1,31 @@
#ifndef DECK_LINK_TO_API_TRANSFORMER_H
#define DECK_LINK_TO_API_TRANSFORMER_H
#include <QString>
enum class DeckProvider
{
TappedOut,
Archidekt,
Moxfield,
Deckstats,
Unknown
};
struct ParsedDeckInfo
{
QString baseUrl;
QString deckID;
QString fullUrl;
DeckProvider provider;
};
namespace DeckLinkToApiTransformer
{
// Returns true if the input URL is recognized and fills outInfo.
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo);
} // namespace DeckLinkToApiTransformer
#endif // DECK_LINK_TO_API_TRANSFORMER_H

View file

@ -0,0 +1,112 @@
#ifndef INTERFACE_JSON_DECK_PARSER_H
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../deck/deck_loader.h"
#include <QJsonArray>
#include <QJsonObject>
class IJsonDeckParser
{
public:
virtual ~IJsonDeckParser() = default;
virtual DeckLoader *parse(const QJsonObject &obj) = 0;
};
class ArchidektJsonParser : public IJsonDeckParser
{
public:
DeckLoader *parse(const QJsonObject &obj) override
{
DeckLoader *list = new DeckLoader();
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
list->setName(deckName);
list->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
for (auto entry : obj.value("cards").toArray()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
auto oracleCard = card.value("oracleCard").toObject();
QString cardName = oracleCard.value("name").toString();
QString setName = card.value("edition").toObject().value("editioncode").toString().toUpper();
QString collectorNumber = card.value("collectorNumber").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
list->loadFromStream_Plain(outStream, false);
list->resolveSetNameAndNumberToProviderID();
return list;
}
};
class MoxfieldJsonParser : public IJsonDeckParser
{
public:
DeckLoader *parse(const QJsonObject &obj) override
{
DeckLoader *list = new DeckLoader();
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
list->setName(deckName);
list->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
for (auto entry : obj.value("mainboard").toObject()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
QString cardName = card.value("name").toString();
QString setName = card.value("set").toString().toUpper();
QString collectorNumber = card.value("cn").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
outStream << '\n';
for (auto entry : obj.value("sideboard").toObject()) {
auto quantity = entry.toObject().value("quantity").toInt();
auto card = entry.toObject().value("card").toObject();
QString cardName = card.value("name").toString();
QString setName = card.value("set").toString().toUpper();
QString collectorNumber = card.value("cn").toString();
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
list->loadFromStream_Plain(outStream, false);
list->resolveSetNameAndNumberToProviderID();
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {
for (auto it = commandersObj.begin(); it != commandersObj.end(); ++it) {
QJsonObject cardData = it.value().toObject().value("card").toObject();
QString commanderName = cardData.value("name").toString();
QString setName = cardData.value("set").toString().toUpper();
QString collectorNumber = cardData.value("cn").toString();
QString providerId = cardData.value("scryfall_id").toString();
list->setBannerCard(QPair<QString, QString>(commanderName, providerId));
list->addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
}
}
return list;
}
};
#endif // INTERFACE_JSON_DECK_PARSER_H

View file

@ -6,6 +6,7 @@
#include "../../deck/deck_stats_interface.h"
#include "../../dialogs/dlg_load_deck.h"
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../dialogs/dlg_load_deck_from_website.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_database_model.h"
#include "../../server/pending_command.h"
@ -467,6 +468,28 @@ void AbstractTabDeckEditor::actPrintDeck()
dlg->exec();
}
void AbstractTabDeckEditor::actLoadDeckFromWebsite()
{
auto deckOpenLocation = confirmOpen();
if (deckOpenLocation == CANCELLED) {
return;
}
DlgLoadDeckFromWebsite dlg(this);
if (!dlg.exec())
return;
if (deckOpenLocation == NEW_TAB) {
emit openDeckEditor(dlg.getDeck());
} else {
setDeck(dlg.getDeck());
setModified(true);
}
deckMenu->setSaveStatus(true);
}
void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website)
{
// check if deck is not null

View file

@ -102,6 +102,7 @@ protected slots:
void actSaveDeckToClipboardRaw();
void actSaveDeckToClipboardRawNoSetInfo();
void actPrintDeck();
void actLoadDeckFromWebsite();
void actExportDeckDecklist();
void actExportDeckDecklistXyz();
void actAnalyzeDeckDeckstats();