Deck loader is a gui class. (#6294)

* Deck loader is a gui class.

Took 31 minutes

Took 3 minutes

* Deck Loader is responsible for printing.

Took 8 minutes


Took 2 seconds

* Style proxy.

Took 14 minutes

Took 6 minutes

Took 1 minute

* Don't need to include QBrush anymore.

Took 3 minutes

Took 7 seconds

* Includes for printer.

Took 5 minutes

* Nuke getDeckList()

Took 9 minutes

* Adjust to rebase.

Took 35 seconds

* Lint.

Took 3 minutes

* Braces for one line return statements.

Took 13 minutes

Took 50 seconds

* Enum for model columns.

Took 9 minutes

* One more single line if.

Took 1 minute

* Another style lint on a sunday night

Took 5 minutes

* Move enum to namespace.

Took 3 minutes

* Fix a critical blocker.

Took 5 minutes

* Update docs.

Took 3 minutes

* Doxygen and namespace enums.

Took 2 minutes

Took 15 seconds

* Adjust to namespace.

Took 4 minutes

Took 1 minute

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2025-11-11 11:57:41 +01:00 committed by GitHub
parent c16267e60f
commit bfedc12fa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 384 additions and 255 deletions

View file

@ -2,7 +2,7 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h deck_loader.h)
set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
@ -12,7 +12,6 @@ endif()
add_library(
libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp
deck_loader.cpp
)
target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View file

@ -1,24 +1,12 @@
#include "deck_list_model.h"
#include "deck_loader.h"
#include <QBrush>
#include <QFont>
#include <QPrinter>
#include <QProgressDialog>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextStream>
#include <QTextTable>
#include <libcockatrice/card/database/card_database_manager.h>
DeckListModel::DeckListModel(QObject *parent)
: QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder)
{
deckList = new DeckLoader;
deckList = new DeckList;
deckList->setParent(this);
connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree);
connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged);
root = new InnerDecklistNode;
}
@ -107,83 +95,96 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
return {};
}
auto *temp = static_cast<AbstractDecklistNode *>(index.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(temp);
if (card == nullptr) {
const auto *node = dynamic_cast<InnerDecklistNode *>(temp);
auto *node = static_cast<AbstractDecklistNode *>(index.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(node);
// Group node
if (!card) {
const auto *group = dynamic_cast<InnerDecklistNode *>(node);
switch (role) {
case Qt::FontRole: {
QFont f;
f.setBold(true);
return f;
}
case Qt::DisplayRole:
case Qt::EditRole: {
switch (index.column()) {
case 0:
return node->recursiveCount(true);
case 1: {
if (role == Qt::DisplayRole)
return node->getVisibleName();
return node->getName();
}
case 2: {
return node->getCardSetShortName();
}
case 3: {
return node->getCardCollectorNumber();
}
case 4: {
return node->getCardProviderId();
}
case DeckListModelColumns::CARD_AMOUNT:
return group->recursiveCount(true);
case DeckListModelColumns::CARD_NAME:
if (role == Qt::DisplayRole) {
return group->getVisibleName();
}
return group->getName();
case DeckListModelColumns::CARD_SET:
return group->getCardSetShortName();
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return group->getCardCollectorNumber();
case DeckListModelColumns::CARD_PROVIDER_ID:
return group->getCardProviderId();
default:
return {};
}
}
case Qt::UserRole + 1:
case DeckRoles::IsCardRole:
return false;
case Qt::BackgroundRole: {
int color = 90 + 60 * node->depth();
return QBrush(QColor(color, 255, color));
}
case Qt::ForegroundRole: {
return QBrush(QColor(0, 0, 0));
}
default:
return {};
}
} else {
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole: {
switch (index.column()) {
case 0:
return card->getNumber();
case 1:
return card->getName();
case 2:
return card->getCardSetShortName();
case 3:
return card->getCardCollectorNumber();
case 4:
return card->getCardProviderId();
default:
return {};
}
}
case Qt::UserRole + 1:
case DeckRoles::DepthRole:
return group->depth();
// legality does not apply to group nodes
case DeckRoles::IsLegalRole:
return true;
case Qt::BackgroundRole: {
int color = 255 - (index.row() % 2) * 30;
return QBrush(QColor(color, color, color));
}
case Qt::ForegroundRole: {
return QBrush(QColor(0, 0, 0));
}
default:
return {};
}
}
// Card node
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
return card->getNumber();
case DeckListModelColumns::CARD_NAME:
return card->getName();
case DeckListModelColumns::CARD_SET:
return card->getCardSetShortName();
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return card->getCardCollectorNumber();
case DeckListModelColumns::CARD_PROVIDER_ID:
return card->getCardProviderId();
default:
return {};
}
case DeckRoles::IsCardRole: {
return true;
}
case DeckRoles::DepthRole: {
return card->depth();
}
default: {
return {};
}
}
}
void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent)
{
int rows = rowCount(parent);
if (rows == 0)
return;
QModelIndex topLeft = index(0, 0, parent);
QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent);
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole});
for (int r = 0; r < rows; ++r) {
QModelIndex child = index(r, 0, parent);
emitBackgroundUpdates(child);
}
}
QVariant DeckListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const
@ -197,15 +198,15 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie
}
switch (section) {
case 0:
case DeckListModelColumns::CARD_AMOUNT:
return tr("Count");
case 1:
case DeckListModelColumns::CARD_NAME:
return tr("Card");
case 2:
case DeckListModelColumns::CARD_SET:
return tr("Set");
case 3:
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return tr("Number");
case 4:
case DeckListModelColumns::CARD_PROVIDER_ID:
return tr("Provider ID");
default:
return {};
@ -262,19 +263,19 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
}
switch (index.column()) {
case 0:
case DeckListModelColumns::CARD_AMOUNT:
node->setNumber(value.toInt());
break;
case 1:
case DeckListModelColumns::CARD_NAME:
node->setName(value.toString());
break;
case 2:
case DeckListModelColumns::CARD_SET:
node->setCardSetShortName(value.toString());
break;
case 3:
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
node->setCardCollectorNumber(value.toString());
break;
case 4:
case DeckListModelColumns::CARD_PROVIDER_ID:
node->setCardProviderId(value.toString());
break;
default:
@ -520,7 +521,7 @@ void DeckListModel::sort(int column, Qt::SortOrder order)
emit layoutChanged();
}
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria)
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria)
{
activeGroupCriteria = newCriteria;
rebuildTree();
@ -528,19 +529,17 @@ void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteri
void DeckListModel::cleanList()
{
setDeckList(new DeckLoader);
setDeckList(new DeckList);
}
/**
* @param _deck The deck. Takes ownership of the object
*/
void DeckListModel::setDeckList(DeckLoader *_deck)
void DeckListModel::setDeckList(DeckList *_deck)
{
deckList->deleteLater();
deckList = _deck;
deckList->setParent(this);
connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree);
connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged);
rebuildTree();
}
@ -628,98 +627,4 @@ QList<QString> *DeckListModel::getZones() const
zones->append(currentZone->getName());
}
return zones;
}
void DeckListModel::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
{
const int totalColumns = 2;
if (node->height() == 1) {
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
charFormat.setFontPointSize(11);
charFormat.setFontWeight(QFont::Bold);
cursor->insertBlock(blockFormat, charFormat);
QTextTableFormat tableFormat;
tableFormat.setCellPadding(0);
tableFormat.setCellSpacing(0);
tableFormat.setBorder(0);
QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat);
for (int i = 0; i < node->size(); i++) {
auto *card = dynamic_cast<AbstractDecklistCardNode *>(node->at(i));
QTextCharFormat cellCharFormat;
cellCharFormat.setFontPointSize(9);
QTextTableCell cell = table->cellAt(i, 0);
cell.setFormat(cellCharFormat);
QTextCursor cellCursor = cell.firstCursorPosition();
cellCursor.insertText(QString("%1 ").arg(card->getNumber()));
cell = table->cellAt(i, 1);
cell.setFormat(cellCharFormat);
cellCursor = cell.firstCursorPosition();
cellCursor.insertText(card->getName());
}
} else if (node->height() == 2) {
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
charFormat.setFontPointSize(14);
charFormat.setFontWeight(QFont::Bold);
cursor->insertBlock(blockFormat, charFormat);
QTextTableFormat tableFormat;
tableFormat.setCellPadding(10);
tableFormat.setCellSpacing(0);
tableFormat.setBorder(0);
QVector<QTextLength> constraints;
for (int i = 0; i < totalColumns; i++) {
constraints << QTextLength(QTextLength::PercentageLength, 100.0 / totalColumns);
}
tableFormat.setColumnWidthConstraints(constraints);
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
for (int i = 0; i < node->size(); i++) {
QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition();
printDeckListNode(&cellCursor, dynamic_cast<InnerDecklistNode *>(node->at(i)));
}
}
cursor->movePosition(QTextCursor::End);
}
void DeckListModel::printDeckList(QPrinter *printer)
{
QTextDocument doc;
QFont font("Serif");
font.setStyleHint(QFont::Serif);
doc.setDefaultFont(font);
QTextCursor cursor(&doc);
QTextBlockFormat headerBlockFormat;
QTextCharFormat headerCharFormat;
headerCharFormat.setFontPointSize(16);
headerCharFormat.setFontWeight(QFont::Bold);
cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getName());
headerCharFormat.setFontPointSize(12);
cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getComments());
cursor.insertBlock(headerBlockFormat, headerCharFormat);
for (int i = 0; i < root->size(); i++) {
cursor.insertHtml("<br><img src=theme:hr.jpg>");
// cursor.insertHtml("<hr>");
cursor.insertBlock(headerBlockFormat, headerCharFormat);
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(root->at(i)));
}
doc.print(printer);
}
}

View file

@ -8,20 +8,73 @@
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/deck_list_card_node.h>
class DeckLoader;
class CardDatabase;
class QPrinter;
class QTextCursor;
/**
* @brief Specifies the criteria used to group cards in the DeckListModel.
* @namespace DeckRoles
* @brief Custom model roles used by the DeckListModel for data retrieval.
*
* These roles extend Qt's item data roles starting at Qt::UserRole.
*/
enum DeckListModelGroupCriteria
namespace DeckRoles
{
/**
* @enum DeckRoles
* @brief Custom data roles for deck-related model items.
*
* These roles are used to retrieve specialized data from the DeckListModel.
*/
enum
{
IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */
DepthRole, /**< Depth level within the deck's grouping hierarchy. */
IsLegalRole /**< Whether the card is legal in the current deck format. */
};
} // namespace DeckRoles
/**
* @namespace DeckListModelColumns
* @brief Column indices for the DeckListModel.
*
* These values map to the columns in the deck list table representation.
*/
namespace DeckListModelColumns
{
/**
* @enum DeckListModelColumns
* @brief Column identifiers for displaying card information in the deck list.
*/
enum
{
CARD_AMOUNT = 0, /**< The number of copies of the card. */
CARD_NAME = 1, /**< The card's name. */
CARD_SET = 2, /**< The set or expansion the card belongs to. */
CARD_COLLECTOR_NUMBER = 3, /**< Collector number of the card within the set. */
CARD_PROVIDER_ID = 4 /**< ID used by the external data provider (e.g., Scryfall). */
};
} // namespace DeckListModelColumns
/**
* @namespace DeckListModelGroupCriteria
* @brief Specifies criteria used to group cards in the DeckListModel.
*
* These values determine how cards are grouped in UI views such as the deck editor.
*/
namespace DeckListModelGroupCriteria
{
/**
* @enum DeckListModelGroupCriteria
* @brief Available grouping strategies for deck visualization.
*/
enum Type
{
MAIN_TYPE, /**< Group cards by their main type (e.g., creature, instant). */
MANA_COST, /**< Group cards by their mana cost. */
MANA_COST, /**< Group cards by their total mana cost. */
COLOR /**< Group cards by their color identity. */
};
} // namespace DeckListModelGroupCriteria
/**
* @class DecklistModelCardNode
@ -119,12 +172,12 @@ public:
*
* Slots:
* - rebuildTree(): rebuilds the model structure from the underlying DeckLoader.
* - printDeckList(): renders the decklist to a QPrinter.
*/
class DeckListModel : public QAbstractItemModel
{
Q_OBJECT
private slots:
public slots:
/**
* @brief Rebuilds the model tree from the underlying DeckLoader.
*
@ -133,13 +186,6 @@ private slots:
*/
void rebuildTree();
public slots:
/**
* @brief Prints the decklist to the provided QPrinter.
* @param printer The printer to render the decklist to.
*/
void printDeckList(QPrinter *printer);
signals:
/**
* @brief Emitted whenever the deck hash changes due to modifications in the model.
@ -170,6 +216,7 @@ public:
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
void emitBackgroundUpdates(const QModelIndex &parent);
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
QModelIndex parent(const QModelIndex &index) const override;
@ -222,11 +269,11 @@ public:
* @brief Removes all cards and resets the model.
*/
void cleanList();
DeckLoader *getDeckList() const
DeckList *getDeckList() const
{
return deckList;
}
void setDeckList(DeckLoader *_deck);
void setDeckList(DeckList *_deck);
QList<ExactCard> getCards() const;
QList<ExactCard> getCardsForZone(const QString &zoneName) const;
@ -236,12 +283,12 @@ public:
* @brief Sets the criteria used to group cards in the model.
* @param newCriteria The new grouping criteria.
*/
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria);
private:
DeckLoader *deckList; /**< Pointer to the deck loader providing the underlying data. */
DeckList *deckList; /**< Pointer to the deck loader providing the underlying data. */
InnerDecklistNode *root; /**< Root node of the model tree. */
DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
int lastKnownColumn; /**< Last column used for sorting. */
Qt::SortOrder lastKnownOrder; /**< Last known sort order. */
@ -254,8 +301,6 @@ private:
void emitRecursiveUpdates(const QModelIndex &index);
void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);
void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
template <typename T> T getNode(const QModelIndex &index) const
{
if (!index.isValid())

View file

@ -1,605 +0,0 @@
#include "deck_loader.h"
#include <QApplication>
#include <QClipboard>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QRegularExpression>
#include <QStringList>
#include <QtConcurrentRun>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/deck_list_card_node.h>
const QStringList DeckLoader::ACCEPTED_FILE_EXTENSIONS = {"*.cod", "*.dec", "*.dek", "*.txt", "*.mwDeck"};
const QStringList DeckLoader::FILE_NAME_FILTERS = {
tr("Common deck formats (%1)").arg(ACCEPTED_FILE_EXTENSIONS.join(" ")), tr("All files (*.*)")};
DeckLoader::DeckLoader() : DeckList(), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const QString &nativeString)
: DeckList(nativeString), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const DeckList &other)
: DeckList(other), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const DeckLoader &other)
: DeckList(other), lastFileName(other.lastFileName), lastFileFormat(other.lastFileFormat),
lastRemoteDeckId(other.lastRemoteDeckId)
{
}
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
bool result = false;
switch (fmt) {
case PlainTextFormat:
result = loadFromFile_Plain(&file);
break;
case CockatriceFormat: {
result = loadFromFile_Native(&file);
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
if (!result) {
qCInfo(DeckLoaderLog) << "Retrying as plain format";
file.seek(0);
result = loadFromFile_Plain(&file);
fmt = PlainTextFormat;
}
break;
}
default:
break;
}
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit deckLoaded();
}
qCInfo(DeckLoaderLog) << "Deck was loaded -" << result;
return result;
}
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
{
auto *watcher = new QFutureWatcher<bool>(this);
connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, fileName, fmt, userRequest]() {
const bool result = watcher->result();
watcher->deleteLater();
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit deckLoaded();
}
emit loadFinished(result);
});
QFuture<bool> future = QtConcurrent::run([=, this]() {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
switch (fmt) {
case PlainTextFormat:
return loadFromFile_Plain(&file);
case CockatriceFormat: {
bool result = false;
result = loadFromFile_Native(&file);
if (!result) {
file.seek(0);
return loadFromFile_Plain(&file);
}
return result;
}
default:
return false;
break;
}
});
watcher->setFuture(future);
return true; // Return immediately to indicate the async task was started
}
bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
{
bool result = loadFromString_Native(nativeString);
if (result) {
lastFileName = QString();
lastFileFormat = CockatriceFormat;
lastRemoteDeckId = remoteDeckId;
emit deckLoaded();
}
return result;
}
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
}
bool result = false;
switch (fmt) {
case PlainTextFormat:
result = saveToFile_Plain(&file);
break;
case CockatriceFormat:
result = saveToFile_Native(&file);
break;
}
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
}
file.flush();
file.close();
return result;
}
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
{
QFileInfo fileInfo(fileName);
if (!fileInfo.exists()) {
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
return false;
}
QDateTime originalTimestamp = fileInfo.lastModified();
// Open the file for writing
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName;
return false;
}
bool result = false;
// Perform file modifications
switch (fmt) {
case PlainTextFormat:
result = saveToFile_Plain(&file);
break;
case CockatriceFormat:
setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = saveToFile_Native(&file);
break;
}
file.close(); // Close the file to ensure changes are flushed
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
// Re-open the file and set the original timestamp
if (!file.open(QIODevice::ReadWrite)) {
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
return false;
}
if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) {
qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName;
file.close();
return false;
}
file.close();
}
return result;
}
static QString getDomainForWebsite(DeckLoader::DecklistWebsite website)
{
switch (website) {
case DeckLoader::DecklistOrg:
return "www.decklist.org";
case DeckLoader::DecklistXyz:
return "www.decklist.xyz";
default:
qCWarning(DeckLoaderLog) << "Invalid decklist website enum:" << website;
return "";
}
}
/**
* Converts the card to the String that represents it in the decklist export
*/
static QString toDecklistExportString(const DecklistCardNode *card)
{
QString cardString;
// Get the number of cards and add the card name
cardString += QString::number(card->getNumber());
// Add a space between card num and name
cardString += "%20";
// Add card name
cardString += card->getName();
if (!card->getCardSetShortName().isNull()) {
cardString += "%20";
cardString += "(" + card->getCardSetShortName() + ")";
}
if (!card->getCardCollectorNumber().isNull()) {
cardString += "%20";
cardString += card->getCardCollectorNumber();
}
// Add a return at the end of the card
cardString += "%0A";
return cardString;
}
/**
* Export deck to decklist function, called to format the deck in a way to be sent to a server
* @param website The website we're sending the deck to
*/
QString DeckLoader::exportDeckToDecklist(DecklistWebsite website)
{
// Add the base url
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
// Create two strings to pass to function
QString mainBoardCards, sideBoardCards;
// Set up the function to call
auto formatDeckListForExport = [&mainBoardCards, &sideBoardCards](const auto *node, const auto *card) {
// Get the card name
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
if (!dbCard || dbCard->getIsToken()) {
// If it's a token, we don't care about the card.
return;
}
// Check if it's a sideboard card.
if (node->getName() == DECK_ZONE_SIDE) {
sideBoardCards += toDecklistExportString(card);
} else {
// If it's a mainboard card, do the same thing, but for the mainboard card string
mainBoardCards += toDecklistExportString(card);
}
};
// call our struct function for each card in the deck
forEachCard(formatDeckListForExport);
// Remove the extra return at the end of the last cards
mainBoardCards.chop(3);
sideBoardCards.chop(3);
// if after we've called it for each card, and the strings are empty, we know that
// there were no non-token cards in the deck, so show an error message.
if ((QString::compare(mainBoardCards, "", Qt::CaseInsensitive) == 0) &&
(QString::compare(sideBoardCards, "", Qt::CaseInsensitive) == 0)) {
return "";
}
// return a string with the url for decklist export
deckString += "deckmain=" + mainBoardCards + "&deckside=" + sideBoardCards;
return deckString;
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId to the preferred printing.
struct SetProviderIdToPreferred
{
// Main operator for struct, allowing the foreachcard to work.
SetProviderIdToPreferred()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
QString providerId = preferredPrinting.getUuid();
QString setShortName = preferredPrinting.getSet()->getShortName();
QString collectorNumber = preferredPrinting.getProperty("num");
card->setCardProviderId(providerId);
card->setCardCollectorNumber(collectorNumber);
card->setCardSetShortName(setShortName);
}
};
/**
* This function iterates through each card in the decklist and sets the providerId
* on each card based on its set name and collector number.
*/
void DeckLoader::setProviderIdToPreferredPrinting()
{
// Set up the struct to call.
SetProviderIdToPreferred setProviderIdToPreferred;
// Call the forEachCard method for each card in the deck
forEachCard(setProviderIdToPreferred);
}
/**
* Sets the providerId on each card in the decklist based on its set name and collector number.
*/
void DeckLoader::resolveSetNameAndNumberToProviderID()
{
auto setProviderId = [](const auto node, const auto card) {
Q_UNUSED(node);
// Retrieve the providerId based on setName and collectorNumber
QString providerId =
CardDatabaseManager::getInstance()
->query()
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
.getUuid();
// Set the providerId on the card
card->setCardProviderId(providerId);
};
forEachCard(setProviderId);
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId.
struct ClearSetNameNumberAndProviderId
{
// Main operator for struct, allowing the foreachcard to work.
ClearSetNameNumberAndProviderId()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
}
};
/**
* Clears the set name and numbers on each card in the decklist.
*/
void DeckLoader::clearSetNamesAndNumbers()
{
auto clearSetNameAndNumber = [](const auto node, auto card) {
Q_UNUSED(node)
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
};
forEachCard(clearSetNameAndNumber);
}
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return CockatriceFormat;
}
return PlainTextFormat;
}
void DeckLoader::saveToClipboard(bool addComments, bool addSetNameAndNumber) const
{
QString buffer;
QTextStream stream(&buffer);
saveToStream_Plain(stream, addComments, addSetNameAndNumber);
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
bool DeckLoader::saveToStream_Plain(QTextStream &out, bool addComments, bool addSetNameAndNumber) const
{
if (addComments) {
saveToStream_DeckHeader(out);
}
// loop zones
for (int i = 0; i < getRoot()->size(); i++) {
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(getRoot()->at(i));
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
// end of zone
out << "\n";
}
return true;
}
void DeckLoader::saveToStream_DeckHeader(QTextStream &out) const
{
if (!getName().isEmpty()) {
out << "// " << getName() << "\n\n";
}
if (!getComments().isEmpty()) {
QStringList commentRows = getComments().split(QRegularExpression("\n|\r\n|\r"));
for (const QString &row : commentRows) {
out << "// " << row << "\n";
}
out << "\n";
}
}
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode,
bool addComments,
bool addSetNameAndNumber) const
{
// group cards by card type and count the subtotals
QMultiMap<QString, DecklistCardNode *> cardsByType;
QMap<QString, int> cardTotalByType;
int cardTotal = 0;
for (int j = 0; j < zoneNode->size(); j++) {
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
QString cardType = info ? info->getMainCardType() : "unknown";
cardsByType.insert(cardType, card);
if (cardTotalByType.contains(cardType)) {
cardTotalByType[cardType] += card->getNumber();
} else {
cardTotalByType[cardType] = card->getNumber();
}
cardTotal += card->getNumber();
}
if (addComments) {
out << "// " << cardTotal << " " << zoneNode->getVisibleName() << "\n";
}
// print cards to stream
for (const QString &cardType : cardsByType.uniqueKeys()) {
if (addComments) {
out << "// " << cardTotalByType[cardType] << " " << cardType << "\n";
}
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
if (addComments) {
out << "\n";
}
}
}
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
const InnerDecklistNode *zoneNode,
QList<DecklistCardNode *> cards,
bool addComments,
bool addSetNameAndNumber) const
{
// QMultiMap sorts values in reverse order
for (int i = cards.size() - 1; i >= 0; --i) {
DecklistCardNode *card = cards[i];
if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
out << "SB: ";
}
if (card->getNumber()) {
out << card->getNumber();
}
if (!card->getName().isNull() && !card->getName().isEmpty()) {
out << " " << card->getName();
}
if (addSetNameAndNumber) {
if (!card->getCardSetShortName().isNull() && !card->getCardSetShortName().isEmpty()) {
out << " "
<< "(" << card->getCardSetShortName() << ")";
}
if (!card->getCardCollectorNumber().isNull()) {
out << " " << card->getCardCollectorNumber();
}
}
out << "\n";
}
}
bool DeckLoader::convertToCockatriceFormat(QString fileName)
{
// Change the file extension to .cod
QFileInfo fileInfo(fileName);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
// Open the new file for writing
QFile file(newFileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName;
return false;
}
bool result = false;
// Perform file modifications based on the detected format
switch (getFormatFromName(fileName)) {
case PlainTextFormat:
// Save in Cockatrice's native format
result = saveToFile_Native(&file);
break;
case CockatriceFormat:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
result = true;
break;
default:
qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName;
result = false;
break;
}
file.close();
// Delete the old file if conversion was successful
if (result) {
if (!QFile::remove(fileName)) {
qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName;
} else {
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
}
lastFileName = newFileName;
lastFileFormat = CockatriceFormat;
}
return result;
}
QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName)
{
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(cardName);
if (card && card->getIsToken()) {
return DECK_ZONE_TOKENS;
}
return currentZoneName;
}
QString DeckLoader::getCompleteCardName(const QString &cardName) const
{
if (CardDatabaseManager::getInstance()) {
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
if (temp) {
return temp.getName();
}
}
return cardName;
}

View file

@ -1,111 +0,0 @@
/**
* @file deck_loader.h
* @ingroup ImportExport
* @brief TODO: Document this.
*/
#ifndef DECK_LOADER_H
#define DECK_LOADER_H
#include <QLoggingCategory>
#include <libcockatrice/deck_list/deck_list.h>
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader")
class DeckLoader : public DeckList
{
Q_OBJECT
signals:
void deckLoaded();
void loadFinished(bool success);
public:
enum FileFormat
{
PlainTextFormat,
CockatriceFormat
};
/**
* Supported file extensions for decklist files
*/
static const QStringList ACCEPTED_FILE_EXTENSIONS;
/**
* For use with `QFileDialog::setNameFilters`
*/
static const QStringList FILE_NAME_FILTERS;
enum DecklistWebsite
{
DecklistOrg,
DecklistXyz
};
private:
QString lastFileName;
FileFormat lastFileFormat;
int lastRemoteDeckId;
public:
DeckLoader();
explicit DeckLoader(const QString &nativeString);
explicit DeckLoader(const DeckList &other);
DeckLoader(const DeckLoader &other);
const QString &getLastFileName() const
{
return lastFileName;
}
void setLastFileName(const QString &_lastFileName)
{
lastFileName = _lastFileName;
}
FileFormat getLastFileFormat() const
{
return lastFileFormat;
}
int getLastRemoteDeckId() const
{
return lastRemoteDeckId;
}
bool hasNotBeenLoaded() const
{
return getLastFileName().isEmpty() && getLastRemoteDeckId() == -1;
}
void clearSetNamesAndNumbers();
static FileFormat getFormatFromName(const QString &fileName);
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
bool saveToFile(const QString &fileName, FileFormat fmt);
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
QString exportDeckToDecklist(DecklistWebsite website);
void setProviderIdToPreferredPrinting();
void resolveSetNameAndNumberToProviderID();
void saveToClipboard(bool addComments = true, bool addSetNameAndNumber = true) const;
// overload
bool saveToStream_Plain(QTextStream &out, bool addComments = true, bool addSetNameAndNumber = true) const;
bool convertToCockatriceFormat(QString fileName);
protected:
void saveToStream_DeckHeader(QTextStream &out) const;
void saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode,
bool addComments = true,
bool addSetNameAndNumber = true) const;
void saveToStream_DeckZoneCards(QTextStream &out,
const InnerDecklistNode *zoneNode,
QList<DecklistCardNode *> cards,
bool addComments = true,
bool addSetNameAndNumber = true) const;
[[nodiscard]] QString getCardZoneFromName(QString cardName, QString currentZoneName) override;
[[nodiscard]] QString getCompleteCardName(const QString &cardName) const override;
};
#endif