From a62ba91a5d2f09f33286762d0c4fea2954c283e6 Mon Sep 17 00:00:00 2001 From: Buce Date: Tue, 18 Feb 2014 18:26:09 -0600 Subject: [PATCH 01/41] Add different compare methods for sorting Give DeckList nodes the ability to sort based on name, price, or number. --- common/decklist.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++--- common/decklist.h | 9 +++++ 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/common/decklist.cpp b/common/decklist.cpp index d9a3d7f3..11119945 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -104,6 +104,13 @@ QString InnerDecklistNode::visibleNameFromName(const QString &_name) return _name; } +void InnerDecklistNode::setSortMethod(int method) +{ + sortMethod = method; + for (int i = 0; i < size(); i++) + at(i)->setSortMethod(method); +} + QString InnerDecklistNode::getVisibleName() const { return visibleNameFromName(name); @@ -163,21 +170,95 @@ float InnerDecklistNode::recursivePrice(bool countTotalCards) const } bool InnerDecklistNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case 0: + return compareNumber(other); + case 1: + return compareName(other); + case 2: + return comparePrice(other); + } +} + +bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const { InnerDecklistNode *other2 = dynamic_cast(other); - if (other2) - return (getName() > other->getName()); - else + if (other2) { + int n1 = recursiveCount(true); + int n2 = other2->recursiveCount(true); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { return false; + } +} + +bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const +{ + InnerDecklistNode *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return false; + } +} + +bool InnerDecklistNode::comparePrice(AbstractDecklistNode *other) const +{ + InnerDecklistNode *other2 = dynamic_cast(other); + if (other2) { + int p1 = 100*recursivePrice(true); + int p2 = 100*other2->recursivePrice(true); + return (p1 != p2) ? (p1 > p2) : compareName(other); + } else { + return false; + } } bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case 0: + return compareNumber(other); + case 1: + return compareName(other); + case 2: + return compareTotalPrice(other); + } +} + +bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const { AbstractDecklistCardNode *other2 = dynamic_cast(other); - if (other2) - return (getName() > other->getName()); - else + if (other2) { + int n1 = getNumber(); + int n2 = other2->getNumber(); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { return true; + } +} + +bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const +{ + AbstractDecklistCardNode *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return true; + } +} + +bool AbstractDecklistCardNode::compareTotalPrice(AbstractDecklistNode *other) const +{ + AbstractDecklistCardNode *other2 = dynamic_cast(other); + if (other2) { + int p1 = 100*getTotalPrice(); + int p2 = 100*other2->getTotalPrice(); + return (p1 != p2) ? (p1 > p2) : compareName(other); + } else { + return true; + } } class InnerDecklistNode::compareFunctor { diff --git a/common/decklist.h b/common/decklist.h index c5535c2d..6d1579fc 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -38,9 +38,11 @@ public: class AbstractDecklistNode { protected: InnerDecklistNode *parent; + int sortMethod; public: AbstractDecklistNode(InnerDecklistNode *_parent = 0); virtual ~AbstractDecklistNode() { } + virtual void setSortMethod(int method) { sortMethod = method; } virtual QString getName() const = 0; InnerDecklistNode *getParent() const { return parent; } int depth() const; @@ -59,6 +61,7 @@ public: InnerDecklistNode(const QString &_name = QString(), InnerDecklistNode *_parent = 0) : AbstractDecklistNode(_parent), name(_name) { } InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent = 0); virtual ~InnerDecklistNode(); + void setSortMethod(int method); QString getName() const { return name; } void setName(const QString &_name) { name = _name; } static QString visibleNameFromName(const QString &_name); @@ -69,6 +72,9 @@ public: int recursiveCount(bool countTotalCards = false) const; float recursivePrice(bool countTotalCards = false) const; bool compare(AbstractDecklistNode *other) const; + bool compareNumber(AbstractDecklistNode *other) const; + bool compareName(AbstractDecklistNode *other) const; + bool comparePrice(AbstractDecklistNode *other) const; QVector > sort(Qt::SortOrder order = Qt::AscendingOrder); bool readElement(QXmlStreamReader *xml); @@ -87,6 +93,9 @@ public: float getTotalPrice() const { return getNumber() * getPrice(); } int height() const { return 0; } bool compare(AbstractDecklistNode *other) const; + bool compareNumber(AbstractDecklistNode *other) const; + bool compareName(AbstractDecklistNode *other) const; + bool compareTotalPrice(AbstractDecklistNode *other) const; bool readElement(QXmlStreamReader *xml); void writeElement(QXmlStreamWriter *xml); From 6a5f2d24769ea6dfd85fe460bfdc2e888556c82c Mon Sep 17 00:00:00 2001 From: Buce Date: Tue, 18 Feb 2014 18:58:16 -0600 Subject: [PATCH 02/41] Allow sorting on different columns --- cockatrice/src/decklistmodel.cpp | 3 ++- cockatrice/src/tab_deck_editor.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index 3e55ec15..f26acfe1 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -296,9 +296,10 @@ void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) } } -void DeckListModel::sort(int /*column*/, Qt::SortOrder order) +void DeckListModel::sort(int column, Qt::SortOrder order) { emit layoutAboutToBeChanged(); + root->setSortMethod(column); sortHelper(root, order); emit layoutChanged(); } diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index 80709a84..7545c32b 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -110,6 +110,8 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent) deckView = new QTreeView(); deckView->setModel(deckModel); deckView->setUniformRowHeights(true); + deckView->setSortingEnabled(true); + deckView->sortByColumn(1, Qt::AscendingOrder); deckView->header()->setResizeMode(QHeaderView::ResizeToContents); connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(updateCardInfoRight(const QModelIndex &, const QModelIndex &))); @@ -606,10 +608,10 @@ void TabDeckEditor::setDeck(DeckLoader *_deck) nameEdit->setText(deckModel->getDeckList()->getName()); commentsEdit->setText(deckModel->getDeckList()->getComments()); updateHash(); - deckModel->sort(1); + deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); deckView->expandAll(); setModified(false); - + db->cacheCardPixmaps(deckModel->getDeckList()->getCardList()); deckView->expandAll(); setModified(false); From b32374b453330ad2bd52181b2fe701b3d484c0ad Mon Sep 17 00:00:00 2001 From: Buce Date: Tue, 18 Feb 2014 19:16:20 -0600 Subject: [PATCH 03/41] Sort correctly when adding cards --- cockatrice/src/decklistmodel.cpp | 8 ++++++-- cockatrice/src/decklistmodel.h | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index f26acfe1..fdf222aa 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -14,7 +14,7 @@ #include "deck_loader.h" DeckListModel::DeckListModel(QObject *parent) - : QAbstractItemModel(parent) + : QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder) { deckList = new DeckLoader; connect(deckList, SIGNAL(deckLoaded()), this, SLOT(rebuildTree())); @@ -251,12 +251,13 @@ QModelIndex DeckListModel::addCard(const QString &cardName, const QString &zoneN beginInsertRows(parentIndex, cardTypeNode->size(), cardTypeNode->size()); cardNode = new DecklistModelCardNode(decklistCard, cardTypeNode); endInsertRows(); - sort(1); + sort(lastKnownColumn, lastKnownOrder); emitRecursiveUpdates(parentIndex); return nodeToIndex(cardNode); } else { cardNode->setNumber(cardNode->getNumber() + 1); QModelIndex ind = nodeToIndex(cardNode); + sort(lastKnownColumn, lastKnownOrder); emitRecursiveUpdates(ind); deckList->updateDeckHash(); return ind; @@ -298,6 +299,9 @@ void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) void DeckListModel::sort(int column, Qt::SortOrder order) { + lastKnownColumn = column; + lastKnownOrder = order; + emit layoutAboutToBeChanged(); root->setSortMethod(column); sortHelper(root, order); diff --git a/cockatrice/src/decklistmodel.h b/cockatrice/src/decklistmodel.h index 2e2c570a..867f090f 100644 --- a/cockatrice/src/decklistmodel.h +++ b/cockatrice/src/decklistmodel.h @@ -54,6 +54,8 @@ public: private: DeckLoader *deckList; InnerDecklistNode *root; + int lastKnownColumn; + Qt::SortOrder lastKnownOrder; InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); QModelIndex nodeToIndex(AbstractDecklistNode *node) const; void emitRecursiveUpdates(const QModelIndex &index); From 875df01424ee681aceecc6ea904fabda32ca2816 Mon Sep 17 00:00:00 2001 From: Buce Date: Tue, 18 Feb 2014 19:52:23 -0600 Subject: [PATCH 04/41] Fix GUI glitches when adding cards to a deck Since we no longer sort on just card names, we need to emit updates for the parent of the card we add. --- cockatrice/src/decklistmodel.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index fdf222aa..758e540b 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -256,11 +256,11 @@ QModelIndex DeckListModel::addCard(const QString &cardName, const QString &zoneN return nodeToIndex(cardNode); } else { cardNode->setNumber(cardNode->getNumber() + 1); - QModelIndex ind = nodeToIndex(cardNode); + QModelIndex parentIndex = nodeToIndex(cardTypeNode); sort(lastKnownColumn, lastKnownOrder); - emitRecursiveUpdates(ind); + emitRecursiveUpdates(parentIndex); deckList->updateDeckHash(); - return ind; + return nodeToIndex(cardNode); } } From 401b34d22c7dff6cd0a8c6953623bcb1529fb856 Mon Sep 17 00:00:00 2001 From: Buce Date: Tue, 18 Feb 2014 20:05:22 -0600 Subject: [PATCH 05/41] Clean up DecklistModel::addCard() Split common functionality out of the if-else block. --- cockatrice/src/decklistmodel.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index 758e540b..829a1e5a 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -244,24 +244,20 @@ QModelIndex DeckListModel::addCard(const QString &cardName, const QString &zoneN QString cardType = info->getMainCardType(); InnerDecklistNode *cardTypeNode = createNodeIfNeeded(cardType, zoneNode); + QModelIndex parentIndex = nodeToIndex(cardTypeNode); DecklistModelCardNode *cardNode = dynamic_cast(cardTypeNode->findChild(cardName)); if (!cardNode) { DecklistCardNode *decklistCard = deckList->addCard(cardName, zoneName); - QModelIndex parentIndex = nodeToIndex(cardTypeNode); beginInsertRows(parentIndex, cardTypeNode->size(), cardTypeNode->size()); cardNode = new DecklistModelCardNode(decklistCard, cardTypeNode); endInsertRows(); - sort(lastKnownColumn, lastKnownOrder); - emitRecursiveUpdates(parentIndex); - return nodeToIndex(cardNode); } else { cardNode->setNumber(cardNode->getNumber() + 1); - QModelIndex parentIndex = nodeToIndex(cardTypeNode); - sort(lastKnownColumn, lastKnownOrder); - emitRecursiveUpdates(parentIndex); deckList->updateDeckHash(); - return nodeToIndex(cardNode); } + sort(lastKnownColumn, lastKnownOrder); + emitRecursiveUpdates(parentIndex); + return nodeToIndex(cardNode); } QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const From 94419b1e38d0d999c030ee19d35fcdadc335dfab Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:29:00 +0200 Subject: [PATCH 06/41] Oracle: build with qtjson --- oracle/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 6f4e18c9..c23dfc4e 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -4,7 +4,7 @@ PROJECT(oracle) # paths set(DESKTOPDIR share/applications CACHE STRING "path to .desktop files") -SET(oracle_SOURCES src/main.cpp src/oracleimporter.cpp src/window_main.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/settingscache.cpp) +SET(oracle_SOURCES src/main.cpp src/oracleimporter.cpp src/window_main.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/settingscache.cpp ../cockatrice/src/qt-json/json.cpp) SET(oracle_HEADERS src/oracleimporter.h src/window_main.h ../cockatrice/src/carddatabase.h ../cockatrice/src/settingscache.h) SET(QT_USE_QTNETWORK TRUE) From dbac97ee8957a7b3144e3d6a03084143624a6000 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:34:06 +0200 Subject: [PATCH 07/41] OracleImporter: use json to parse cards in sets Problem: Split card are imported as 2 different cards; need to find some way to join them --- oracle/src/oracleimporter.cpp | 94 +++++++++++------------------------ 1 file changed, 29 insertions(+), 65 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index c244e56a..4d431e51 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -5,6 +5,8 @@ #include #include +#include "qt-json/json.h" + OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) : CardDatabase(parent), dataDir(_dataDir), setIndex(-1) { @@ -147,72 +149,34 @@ CardInfo *OracleImporter::addCard(const QString &setName, int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) { int cards = 0; - QString bufferContents(data); - - // Workaround for ampersand bug in text spoilers - int index = -1; - while ((index = bufferContents.indexOf('&', index + 1)) != -1) { - int semicolonIndex = bufferContents.indexOf(';', index); - if (semicolonIndex > 5) { - bufferContents.insert(index + 1, "amp;"); - index += 4; - } - } - - QDomDocument doc; - QString errorMsg; - int errorLine, errorColumn; - if (!doc.setContent(bufferContents, &errorMsg, &errorLine, &errorColumn)) - qDebug() << "error:" << errorMsg << "line:" << errorLine << "column:" << errorColumn; + bool ok; + QVariantMap resultMap = QtJson::Json::parse(QString(data), ok).toMap(); + if (!ok) { + qDebug() << "error: QtJson::Json::parse()"; + return 0; + } + + QListIterator it(resultMap.value("cards").toList()); + while (it.hasNext()) { + QVariantMap map = it.next().toMap(); + QString cardName = map.value("name").toString(); + QString cardCost = map.value("manaCost").toString(); + QString cardType = map.value("type").toString(); + QString cardPT = map.value("power").toString() + QString('/') + map.value("toughness").toString(); + QString cardText = map.value("text").toString(); + int cardId = map.value("multiverseid").toInt(); + int cardLoyalty = map.value("loyalty").toInt(); + QStringList cardTextSplit = cardText.split("\n"); - QDomNodeList divs = doc.elementsByTagName("div"); - for (int i = 0; i < divs.size(); ++i) { - QDomElement div = divs.at(i).toElement(); - QDomNode divClass = div.attributes().namedItem("class"); - if (divClass.nodeValue() == "textspoiler") { - QString cardName, cardCost, cardType, cardPT, cardText; - int cardId = 0; - int cardLoyalty = 0; - - QDomNodeList trs = div.elementsByTagName("tr"); - for (int j = 0; j < trs.size(); ++j) { - QDomElement tr = trs.at(j).toElement(); - QDomNodeList tds = tr.elementsByTagName("td"); - if (tds.size() != 2) { - QStringList cardTextSplit = cardText.split("\n"); - for (int i = 0; i < cardTextSplit.size(); ++i) - cardTextSplit[i] = cardTextSplit[i].trimmed(); - - CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); - if (!set->contains(card)) { - card->addToSet(set); - cards++; - } - cardName = cardCost = cardType = cardPT = cardText = QString(); - } else { - QString v1 = tds.at(0).toElement().text().simplified(); - QString v2 = tds.at(1).toElement().text().replace(trUtf8("—"), "-"); - - if (v1 == "Name") { - QDomElement a = tds.at(1).toElement().elementsByTagName("a").at(0).toElement(); - QString href = a.attributes().namedItem("href").nodeValue(); - cardId = href.mid(href.indexOf("multiverseid=") + 13).toInt(); - cardName = v2.simplified(); - } else if (v1 == "Cost:") - cardCost = v2.simplified(); - else if (v1 == "Type:") - cardType = v2.simplified(); - else if (v1 == "Pow/Tgh:") - cardPT = v2.simplified().remove('(').remove(')'); - else if (v1 == "Rules Text:") - cardText = v2.trimmed(); - else if (v1 == "Loyalty:") - cardLoyalty = v2.trimmed().remove('(').remove(')').toInt(); - } - } - break; + CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); + + if (!set->contains(card)) { + card->addToSet(set); + cards++; } - } + cardName = cardCost = cardType = cardPT = cardText = QString(); + } + return cards; } @@ -257,7 +221,7 @@ void OracleImporter::downloadNextFile() QString urlString = setsToDownload[setIndex].getUrl(); if (urlString.isEmpty()) urlString = setUrl; - urlString = urlString.replace("!longname!", setsToDownload[setIndex].getLongName()); + urlString = urlString.replace("!name!", setsToDownload[setIndex].getShortName()); if (urlString.startsWith("http://")) { QUrl url(urlString); From 6f8b2baad8f3b62130bc4c738df0f8a6e46aa603 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:34:52 +0200 Subject: [PATCH 08/41] Corrections to sets.xml for mtgjson.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Used http://www.woogerworks.com/files/sets.xml as a base * update version * use mtgjson.com as source * adapted some sets name (es: 6E => 6ED) Caveats: * Not all previous sets are present (e.g.: WOTC, WMCQ, WRL); they were not present in woogerworks’s sets.xml anyway --- doc/sets.xml | 688 ++++++++++++++++++++++----------------------------- 1 file changed, 290 insertions(+), 398 deletions(-) diff --git a/doc/sets.xml b/doc/sets.xml index 1afa3958..92bf1b21 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,333 +1,253 @@ - + http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card -http://gatherer.wizards.com/Pages/Search/Default.aspx?output=spoiler&method=text&set=["!longname!"]&special=true - - 10E - Tenth Edition - - - 15ANN - 15th Anniversary - - - 4E - Fourth Edition - - - 5DN - Fifth Dawn - - - 5E - Fifth Edition - - - 6E - Classic Sixth Edition - - - 7E - Seventh Edition - - - 8EB - Eighth Edition Box Set - - - 8E - Eighth Edition - - - 9EB - Ninth Edition Box Set - - - 9E - Ninth Edition - - - AI - Alliances - - - ALA - Shards of Alara - - - AL - Limited Edition Alpha - - - AN - Arabian Nights - - - APAC - Asia Pacific Land Program - - - AP - Apocalypse - - - AQ - Antiquities - +http://mtgjson.com/json/!name!.json ARB Alara Reborn + ALL + Alliances + + + ATQ + Antiquities + + + APC + Apocalypse + + + ARN + Arabian Nights + + ARC Archenemy - - ARENA - Arena League - - - AT - Anthologies - AVR Avacyn Restored - - BD - Beatdown Box Set + + BRB + Battle Royale Box Set - - BE - Limited Edition Beta + + BTD + Beatdown Box Set BOK Betrayers of Kamigawa - BR - Battle Royale Box Set - - - CED - Collector's Edition - - - CEDI - International Collectors' Edition - - - CFX - Conflux - - - CH - Chronicles + BNG + Born of the Gods CHK Champions of Kamigawa - CMA - Commander's Arsenal + CHR + Chronicles - CMD - Commander + 6ED + Classic Sixth Edition - CP - Champs - - - CS + CSP Coldsnap - CSTD - Coldsnap Theme Decks + CON + Conflux + + + CMA + Commander's Arsenal + + ^M + C13^M + Commander 2013 Edition^M - DCILM - Legend Membership - - - DDF - Duel Decks: Elspeth vs. Tezzeret - - - DDG - Duel Decks: Knights vs. Dragons - - - DDH - Duel Decks: Ajani vs. Nicol Bolas - - - DDI - Duel Decks: Venser vs. Koth - - - DDJ - Duel Decks: Izzet vs. Golgari - - - DDK - Duel Decks: Sorin vs. Tibalt - - - DGM - Dragon's Maze - - - DI - Dissension + DST + Darksteel DKA Dark Ascension - DK - The Dark + DIS + Dissension - DM - Deckmasters + DGM + Dragon's Maze - - DPA - Duels of the Planeswalkers + + DDH + Duel Decks: Ajani vs. Nicol Bolas - - DRC - Dragon Con - - - DS - Darksteel - - - DVD + + DDC Duel Decks: Divine vs. Demonic + + DDF + Duel Decks: Elspeth vs. Tezzeret + + + EVG + Duel Decks: Elves vs. Goblins + + + DDD + Duel Decks: Garruk vs. Liliana + + + DDL + Duel Decks: Heroes vs. Monsters + + + DDJ + Duel Decks: Izzet vs. Golgari + + + DD2 + Duel Decks: Jace vs. Chandra + + + DDM + Duel Decks: Jace vs. Vraska + + + DDG + Duel Decks: Knights vs. Dragons + + + DDE + Duel Decks: Phyrexia vs. the Coalition + + + DDK + Duel Decks: Sorin vs. Tibalt + + + DDI + Duel Decks: Venser vs. Koth + - EURO - European Land Program + 8ED + Eighth Edition EVE Eventide - EVG - Duel Decks: Elves vs. Goblins - - - EX + EXO Exodus - FE + FEM Fallen Empires - FNMP - Friday Night Magic + 5DN + Fifth Dawn + + + 5ED + Fifth Edition + + + 4ED + Fourth Edition + + + DRB + From the Vault: Dragons + + + V09 + From the Vault: Exiled + + + V11 + From the Vault: Legends + + + V12 + From the Vault: Realms + + + V10 + From the Vault: Relics + + + V13 + From the Vault: Twenty FUT Future Sight - FVD - From the Vault: Dragons - - - FVE - From the Vault: Exiled - - - FVL - From the Vault: Legends - - - FVR - From the Vault: Relics - - - GP + GPT Guildpact - - GPX - Grand Prix - - - GRC - WPN/Gateway - GTC Gatecrash - GURU - Guru - - - GVL - Duel Decks: Garruk vs. Liliana - - - HHO - Happy Holidays - - - HL + HML Homelands - IA + ICE Ice Age - - IN - Invasion - ISD Innistrad - ITP - Introductory Two-Player Set + INV + Invasion - JR - Judge Gift Program + JOU + Journey into Nyx - JU + JUD Judgment - JVC - Duel Decks: Jace vs. Chandra - - - LE - Legions - - - LG + LEG Legends - LW + LGN + Legions + + + LEA + Limited Edition Alpha + + + LEB + Limited Edition Beta + + + LRW Lorwyn @@ -347,63 +267,59 @@ Magic 2013 - MBP - Media Inserts + M14 + Magic 2014 Core Set + + + CMD + Magic: The Gathering-Commander + + + CNS + Magic: The Gathering—Conspiracy + + + MED + Masters Edition + + + ME2 + Masters Edition II + + + ME3 + Masters Edition III + + + ME4 + Masters Edition IV + + + MMQ + Mercadian Masques + + + MIR + Mirage + + + MRD + Mirrodin MBS Mirrodin Besieged + ^M + MMA^M + Modern Masters^M + ^ - ME2 - MTGO Masters Edition II - - - ME3 - MTGO Masters Edition III - - - ME4 - MTGO Masters Edition IV - - - MED - MTGO Masters Edition - - - MGBC - Multiverse Gift Box Cards - - - MGDC - Magic Game Day Cards - - - MI - Mirrodin - - - MLP - Magic: The Gathering Launch Parties - - - MM - Mercadian Masques - - - MPRP - Magic Player Rewards - - - MR - Mirage - - - MT + MOR Morningtide - NE + NMS Nemesis @@ -411,104 +327,80 @@ New Phyrexia - OD + 9ED + Ninth Edition + + + ODY Odyssey - ON + ONS Onslaught - P3K - Portal Three Kingdoms + PLC + Planar Chaos - + + HOP + Planechase + + PC2 Planechase 2012 Edition - PC - Planar Chaos + PLS + Planeshift - PCH - Planechase - - - PD2 - Premium Deck Series: Fire and Lightning - - - PD3 - Premium Deck Series: Graveborn - - - PDS - Premium Deck Series: Slivers + POR + Portal PO2 Portal Second Age - PO - Portal + PTK + Portal Three Kingdoms + + + PD2 + Premium Deck Series: Fire and Lightning + + + PD3 + Premium Deck Series: Graveborn + + + H09 + Premium Deck Series: Slivers + + + PPR + Promo set for Gatherer - POT - Portal Demogame - - - PR + PCY Prophecy - - PRO - Pro Tour - - - PS - Planeshift - - - PTC - Prerelease Events - - - PVC - Duel Decks: Phyrexia vs. The Coalition - RAV Ravnica: City of Guilds - - REP - Release Events - - - ROE - Rise of the Eldrazi - RTR Return to Ravnica - RV + 3ED Revised Edition - SC - Scourge - - - SH - Stronghold - - - SHM - Shadowmoor + ROE + Rise of the Eldrazi SOK @@ -519,97 +411,97 @@ Scars of Mirrodin - ST2K - Starter 2000 + SCG + Scourge - ST + 7ED + Seventh Edition + + + SHM + Shadowmoor + + + ALA + Shards of Alara + + + S99 Starter 1999 - SUM - Summer of Magic + S00 + Starter 2000 - SUS - Super Series + STH + Stronghold - THGT - Two-Headed Giant Tournament - - - TP + TMP Tempest - TR - Torment + 10E + Tenth Edition - TS + DRK + The Dark + + + THS + Theros + + + TSP Time Spiral - TSTS + TSB Time Spiral "Timeshifted" - UD - Urza's Destiny + TOR + Torment - - UG + + UGL Unglued - - UHAA - Unhinged Alternate Foils - - - UH + + UNH Unhinged - UL - Urza's Legacy - - - UN + 2ED Unlimited Edition - UQC - Celebration Cards + UDS + Urza's Destiny - US + ULG + Urza's Legacy + + + USG Urza's Saga - - V12 - From the Vault: Realms + + VAN + Vanguard - VI + VIS Visions - WL + WTH Weatherlight - - WMCQ - World Magic Cup Qualifiers - - - WOTC - WotC Online Store - - - WRL - Worlds - WWK Worldwake From d58615df1a2c2bd02b3599d3fa62eccbe66f50d6 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:52:55 +0200 Subject: [PATCH 09/41] Fix import of P/T; misc optimizations --- oracle/src/oracleimporter.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4d431e51..af310a46 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -157,15 +157,24 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) } QListIterator it(resultMap.value("cards").toList()); + QVariantMap map; + QString cardName; + QString cardCost; + QString cardType; + QString cardPT; + QString cardText; + int cardId; + int cardLoyalty; + while (it.hasNext()) { - QVariantMap map = it.next().toMap(); - QString cardName = map.value("name").toString(); - QString cardCost = map.value("manaCost").toString(); - QString cardType = map.value("type").toString(); - QString cardPT = map.value("power").toString() + QString('/') + map.value("toughness").toString(); - QString cardText = map.value("text").toString(); - int cardId = map.value("multiverseid").toInt(); - int cardLoyalty = map.value("loyalty").toInt(); + map = it.next().toMap(); + cardName = map.contains("name") ? map.value("name").toString() : QString(""); + cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); + cardType = map.contains("type") ? map.value("type").toString() : QString(""); + cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); + cardText = map.contains("text") ? map.value("text").toString() : QString(""); + cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toInt() : 0; QStringList cardTextSplit = cardText.split("\n"); CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); @@ -174,7 +183,6 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) card->addToSet(set); cards++; } - cardName = cardCost = cardType = cardPT = cardText = QString(); } return cards; From 2b1d7c4f740923a06f7880c3e25f3384d31733fc Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 12:02:34 +0200 Subject: [PATCH 10/41] Use mtgimage.com for card images --- doc/sets.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sets.xml b/doc/sets.xml index 92bf1b21..639cbdfc 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,6 +1,6 @@ -http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card +http://mtgimage.com/multiverseid/!cardid!.jpg http://mtgjson.com/json/!name!.json ARB From 827f448cde5c4fb584b969bf2cb8b1ed0842a0ec Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 12:11:03 +0200 Subject: [PATCH 11/41] Keep gatherer as image source; mtgimage as a hq alternative --- doc/sets.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sets.xml b/doc/sets.xml index 639cbdfc..bcc7c20e 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,6 +1,7 @@ -http://mtgimage.com/multiverseid/!cardid!.jpg +http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card + http://mtgjson.com/json/!name!.json ARB From 89ab257d0bdc32212b5390e39192d961ac38dc4f Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:29:00 +0200 Subject: [PATCH 12/41] Oracle: build with qtjson --- oracle/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 340ce16a..8e1f9b16 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -13,6 +13,7 @@ SET(oracle_SOURCES src/window_main.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/settingscache.cpp + ../cockatrice/src/qt-json/json.cpp ) SET(QT_USE_QTNETWORK TRUE) From dfbcace05b56074ffae5de3c575ce35722adb2aa Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:34:06 +0200 Subject: [PATCH 13/41] OracleImporter: use json to parse cards in sets Problem: Split card are imported as 2 different cards; need to find some way to join them --- oracle/src/oracleimporter.cpp | 94 +++++++++++------------------------ 1 file changed, 29 insertions(+), 65 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index c244e56a..4d431e51 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -5,6 +5,8 @@ #include #include +#include "qt-json/json.h" + OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) : CardDatabase(parent), dataDir(_dataDir), setIndex(-1) { @@ -147,72 +149,34 @@ CardInfo *OracleImporter::addCard(const QString &setName, int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) { int cards = 0; - QString bufferContents(data); - - // Workaround for ampersand bug in text spoilers - int index = -1; - while ((index = bufferContents.indexOf('&', index + 1)) != -1) { - int semicolonIndex = bufferContents.indexOf(';', index); - if (semicolonIndex > 5) { - bufferContents.insert(index + 1, "amp;"); - index += 4; - } - } - - QDomDocument doc; - QString errorMsg; - int errorLine, errorColumn; - if (!doc.setContent(bufferContents, &errorMsg, &errorLine, &errorColumn)) - qDebug() << "error:" << errorMsg << "line:" << errorLine << "column:" << errorColumn; + bool ok; + QVariantMap resultMap = QtJson::Json::parse(QString(data), ok).toMap(); + if (!ok) { + qDebug() << "error: QtJson::Json::parse()"; + return 0; + } + + QListIterator it(resultMap.value("cards").toList()); + while (it.hasNext()) { + QVariantMap map = it.next().toMap(); + QString cardName = map.value("name").toString(); + QString cardCost = map.value("manaCost").toString(); + QString cardType = map.value("type").toString(); + QString cardPT = map.value("power").toString() + QString('/') + map.value("toughness").toString(); + QString cardText = map.value("text").toString(); + int cardId = map.value("multiverseid").toInt(); + int cardLoyalty = map.value("loyalty").toInt(); + QStringList cardTextSplit = cardText.split("\n"); - QDomNodeList divs = doc.elementsByTagName("div"); - for (int i = 0; i < divs.size(); ++i) { - QDomElement div = divs.at(i).toElement(); - QDomNode divClass = div.attributes().namedItem("class"); - if (divClass.nodeValue() == "textspoiler") { - QString cardName, cardCost, cardType, cardPT, cardText; - int cardId = 0; - int cardLoyalty = 0; - - QDomNodeList trs = div.elementsByTagName("tr"); - for (int j = 0; j < trs.size(); ++j) { - QDomElement tr = trs.at(j).toElement(); - QDomNodeList tds = tr.elementsByTagName("td"); - if (tds.size() != 2) { - QStringList cardTextSplit = cardText.split("\n"); - for (int i = 0; i < cardTextSplit.size(); ++i) - cardTextSplit[i] = cardTextSplit[i].trimmed(); - - CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); - if (!set->contains(card)) { - card->addToSet(set); - cards++; - } - cardName = cardCost = cardType = cardPT = cardText = QString(); - } else { - QString v1 = tds.at(0).toElement().text().simplified(); - QString v2 = tds.at(1).toElement().text().replace(trUtf8("—"), "-"); - - if (v1 == "Name") { - QDomElement a = tds.at(1).toElement().elementsByTagName("a").at(0).toElement(); - QString href = a.attributes().namedItem("href").nodeValue(); - cardId = href.mid(href.indexOf("multiverseid=") + 13).toInt(); - cardName = v2.simplified(); - } else if (v1 == "Cost:") - cardCost = v2.simplified(); - else if (v1 == "Type:") - cardType = v2.simplified(); - else if (v1 == "Pow/Tgh:") - cardPT = v2.simplified().remove('(').remove(')'); - else if (v1 == "Rules Text:") - cardText = v2.trimmed(); - else if (v1 == "Loyalty:") - cardLoyalty = v2.trimmed().remove('(').remove(')').toInt(); - } - } - break; + CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); + + if (!set->contains(card)) { + card->addToSet(set); + cards++; } - } + cardName = cardCost = cardType = cardPT = cardText = QString(); + } + return cards; } @@ -257,7 +221,7 @@ void OracleImporter::downloadNextFile() QString urlString = setsToDownload[setIndex].getUrl(); if (urlString.isEmpty()) urlString = setUrl; - urlString = urlString.replace("!longname!", setsToDownload[setIndex].getLongName()); + urlString = urlString.replace("!name!", setsToDownload[setIndex].getShortName()); if (urlString.startsWith("http://")) { QUrl url(urlString); From f8a6f7421193d78534ccabc7206cafe44c7cb71a Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:34:52 +0200 Subject: [PATCH 14/41] Corrections to sets.xml for mtgjson.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Used http://www.woogerworks.com/files/sets.xml as a base * update version * use mtgjson.com as source * adapted some sets name (es: 6E => 6ED) Caveats: * Not all previous sets are present (e.g.: WOTC, WMCQ, WRL); they were not present in woogerworks’s sets.xml anyway --- doc/sets.xml | 688 ++++++++++++++++++++++----------------------------- 1 file changed, 290 insertions(+), 398 deletions(-) diff --git a/doc/sets.xml b/doc/sets.xml index 1afa3958..92bf1b21 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,333 +1,253 @@ - + http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card -http://gatherer.wizards.com/Pages/Search/Default.aspx?output=spoiler&method=text&set=["!longname!"]&special=true - - 10E - Tenth Edition - - - 15ANN - 15th Anniversary - - - 4E - Fourth Edition - - - 5DN - Fifth Dawn - - - 5E - Fifth Edition - - - 6E - Classic Sixth Edition - - - 7E - Seventh Edition - - - 8EB - Eighth Edition Box Set - - - 8E - Eighth Edition - - - 9EB - Ninth Edition Box Set - - - 9E - Ninth Edition - - - AI - Alliances - - - ALA - Shards of Alara - - - AL - Limited Edition Alpha - - - AN - Arabian Nights - - - APAC - Asia Pacific Land Program - - - AP - Apocalypse - - - AQ - Antiquities - +http://mtgjson.com/json/!name!.json ARB Alara Reborn + ALL + Alliances + + + ATQ + Antiquities + + + APC + Apocalypse + + + ARN + Arabian Nights + + ARC Archenemy - - ARENA - Arena League - - - AT - Anthologies - AVR Avacyn Restored - - BD - Beatdown Box Set + + BRB + Battle Royale Box Set - - BE - Limited Edition Beta + + BTD + Beatdown Box Set BOK Betrayers of Kamigawa - BR - Battle Royale Box Set - - - CED - Collector's Edition - - - CEDI - International Collectors' Edition - - - CFX - Conflux - - - CH - Chronicles + BNG + Born of the Gods CHK Champions of Kamigawa - CMA - Commander's Arsenal + CHR + Chronicles - CMD - Commander + 6ED + Classic Sixth Edition - CP - Champs - - - CS + CSP Coldsnap - CSTD - Coldsnap Theme Decks + CON + Conflux + + + CMA + Commander's Arsenal + + ^M + C13^M + Commander 2013 Edition^M - DCILM - Legend Membership - - - DDF - Duel Decks: Elspeth vs. Tezzeret - - - DDG - Duel Decks: Knights vs. Dragons - - - DDH - Duel Decks: Ajani vs. Nicol Bolas - - - DDI - Duel Decks: Venser vs. Koth - - - DDJ - Duel Decks: Izzet vs. Golgari - - - DDK - Duel Decks: Sorin vs. Tibalt - - - DGM - Dragon's Maze - - - DI - Dissension + DST + Darksteel DKA Dark Ascension - DK - The Dark + DIS + Dissension - DM - Deckmasters + DGM + Dragon's Maze - - DPA - Duels of the Planeswalkers + + DDH + Duel Decks: Ajani vs. Nicol Bolas - - DRC - Dragon Con - - - DS - Darksteel - - - DVD + + DDC Duel Decks: Divine vs. Demonic + + DDF + Duel Decks: Elspeth vs. Tezzeret + + + EVG + Duel Decks: Elves vs. Goblins + + + DDD + Duel Decks: Garruk vs. Liliana + + + DDL + Duel Decks: Heroes vs. Monsters + + + DDJ + Duel Decks: Izzet vs. Golgari + + + DD2 + Duel Decks: Jace vs. Chandra + + + DDM + Duel Decks: Jace vs. Vraska + + + DDG + Duel Decks: Knights vs. Dragons + + + DDE + Duel Decks: Phyrexia vs. the Coalition + + + DDK + Duel Decks: Sorin vs. Tibalt + + + DDI + Duel Decks: Venser vs. Koth + - EURO - European Land Program + 8ED + Eighth Edition EVE Eventide - EVG - Duel Decks: Elves vs. Goblins - - - EX + EXO Exodus - FE + FEM Fallen Empires - FNMP - Friday Night Magic + 5DN + Fifth Dawn + + + 5ED + Fifth Edition + + + 4ED + Fourth Edition + + + DRB + From the Vault: Dragons + + + V09 + From the Vault: Exiled + + + V11 + From the Vault: Legends + + + V12 + From the Vault: Realms + + + V10 + From the Vault: Relics + + + V13 + From the Vault: Twenty FUT Future Sight - FVD - From the Vault: Dragons - - - FVE - From the Vault: Exiled - - - FVL - From the Vault: Legends - - - FVR - From the Vault: Relics - - - GP + GPT Guildpact - - GPX - Grand Prix - - - GRC - WPN/Gateway - GTC Gatecrash - GURU - Guru - - - GVL - Duel Decks: Garruk vs. Liliana - - - HHO - Happy Holidays - - - HL + HML Homelands - IA + ICE Ice Age - - IN - Invasion - ISD Innistrad - ITP - Introductory Two-Player Set + INV + Invasion - JR - Judge Gift Program + JOU + Journey into Nyx - JU + JUD Judgment - JVC - Duel Decks: Jace vs. Chandra - - - LE - Legions - - - LG + LEG Legends - LW + LGN + Legions + + + LEA + Limited Edition Alpha + + + LEB + Limited Edition Beta + + + LRW Lorwyn @@ -347,63 +267,59 @@ Magic 2013 - MBP - Media Inserts + M14 + Magic 2014 Core Set + + + CMD + Magic: The Gathering-Commander + + + CNS + Magic: The Gathering—Conspiracy + + + MED + Masters Edition + + + ME2 + Masters Edition II + + + ME3 + Masters Edition III + + + ME4 + Masters Edition IV + + + MMQ + Mercadian Masques + + + MIR + Mirage + + + MRD + Mirrodin MBS Mirrodin Besieged + ^M + MMA^M + Modern Masters^M + ^ - ME2 - MTGO Masters Edition II - - - ME3 - MTGO Masters Edition III - - - ME4 - MTGO Masters Edition IV - - - MED - MTGO Masters Edition - - - MGBC - Multiverse Gift Box Cards - - - MGDC - Magic Game Day Cards - - - MI - Mirrodin - - - MLP - Magic: The Gathering Launch Parties - - - MM - Mercadian Masques - - - MPRP - Magic Player Rewards - - - MR - Mirage - - - MT + MOR Morningtide - NE + NMS Nemesis @@ -411,104 +327,80 @@ New Phyrexia - OD + 9ED + Ninth Edition + + + ODY Odyssey - ON + ONS Onslaught - P3K - Portal Three Kingdoms + PLC + Planar Chaos - + + HOP + Planechase + + PC2 Planechase 2012 Edition - PC - Planar Chaos + PLS + Planeshift - PCH - Planechase - - - PD2 - Premium Deck Series: Fire and Lightning - - - PD3 - Premium Deck Series: Graveborn - - - PDS - Premium Deck Series: Slivers + POR + Portal PO2 Portal Second Age - PO - Portal + PTK + Portal Three Kingdoms + + + PD2 + Premium Deck Series: Fire and Lightning + + + PD3 + Premium Deck Series: Graveborn + + + H09 + Premium Deck Series: Slivers + + + PPR + Promo set for Gatherer - POT - Portal Demogame - - - PR + PCY Prophecy - - PRO - Pro Tour - - - PS - Planeshift - - - PTC - Prerelease Events - - - PVC - Duel Decks: Phyrexia vs. The Coalition - RAV Ravnica: City of Guilds - - REP - Release Events - - - ROE - Rise of the Eldrazi - RTR Return to Ravnica - RV + 3ED Revised Edition - SC - Scourge - - - SH - Stronghold - - - SHM - Shadowmoor + ROE + Rise of the Eldrazi SOK @@ -519,97 +411,97 @@ Scars of Mirrodin - ST2K - Starter 2000 + SCG + Scourge - ST + 7ED + Seventh Edition + + + SHM + Shadowmoor + + + ALA + Shards of Alara + + + S99 Starter 1999 - SUM - Summer of Magic + S00 + Starter 2000 - SUS - Super Series + STH + Stronghold - THGT - Two-Headed Giant Tournament - - - TP + TMP Tempest - TR - Torment + 10E + Tenth Edition - TS + DRK + The Dark + + + THS + Theros + + + TSP Time Spiral - TSTS + TSB Time Spiral "Timeshifted" - UD - Urza's Destiny + TOR + Torment - - UG + + UGL Unglued - - UHAA - Unhinged Alternate Foils - - - UH + + UNH Unhinged - UL - Urza's Legacy - - - UN + 2ED Unlimited Edition - UQC - Celebration Cards + UDS + Urza's Destiny - US + ULG + Urza's Legacy + + + USG Urza's Saga - - V12 - From the Vault: Realms + + VAN + Vanguard - VI + VIS Visions - WL + WTH Weatherlight - - WMCQ - World Magic Cup Qualifiers - - - WOTC - WotC Online Store - - - WRL - Worlds - WWK Worldwake From 4ff225cc3b1e232597f354bb187ed13122178dcb Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 11:52:55 +0200 Subject: [PATCH 15/41] Fix import of P/T; misc optimizations --- oracle/src/oracleimporter.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4d431e51..af310a46 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -157,15 +157,24 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) } QListIterator it(resultMap.value("cards").toList()); + QVariantMap map; + QString cardName; + QString cardCost; + QString cardType; + QString cardPT; + QString cardText; + int cardId; + int cardLoyalty; + while (it.hasNext()) { - QVariantMap map = it.next().toMap(); - QString cardName = map.value("name").toString(); - QString cardCost = map.value("manaCost").toString(); - QString cardType = map.value("type").toString(); - QString cardPT = map.value("power").toString() + QString('/') + map.value("toughness").toString(); - QString cardText = map.value("text").toString(); - int cardId = map.value("multiverseid").toInt(); - int cardLoyalty = map.value("loyalty").toInt(); + map = it.next().toMap(); + cardName = map.contains("name") ? map.value("name").toString() : QString(""); + cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); + cardType = map.contains("type") ? map.value("type").toString() : QString(""); + cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); + cardText = map.contains("text") ? map.value("text").toString() : QString(""); + cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toInt() : 0; QStringList cardTextSplit = cardText.split("\n"); CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); @@ -174,7 +183,6 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) card->addToSet(set); cards++; } - cardName = cardCost = cardType = cardPT = cardText = QString(); } return cards; From f562f3ef06f6c7443a724cc62a66f10749019e0a Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 12:02:34 +0200 Subject: [PATCH 16/41] Use mtgimage.com for card images --- doc/sets.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sets.xml b/doc/sets.xml index 92bf1b21..639cbdfc 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,6 +1,6 @@ -http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card +http://mtgimage.com/multiverseid/!cardid!.jpg http://mtgjson.com/json/!name!.json ARB From 62cfa74d0899dca2d2a964d7f1b884ca8506f525 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 12:11:03 +0200 Subject: [PATCH 17/41] Keep gatherer as image source; mtgimage as a hq alternative --- doc/sets.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sets.xml b/doc/sets.xml index 639cbdfc..bcc7c20e 100644 --- a/doc/sets.xml +++ b/doc/sets.xml @@ -1,6 +1,7 @@ -http://mtgimage.com/multiverseid/!cardid!.jpg +http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card + http://mtgjson.com/json/!name!.json ARB From 0d3ec71e8fc57756be06fe14de3b3f29f5fb480d Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 14 Jun 2014 16:47:36 +0200 Subject: [PATCH 18/41] Merge splitter cards into a single card --- oracle/src/oracleimporter.cpp | 98 ++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index af310a46..4adfb1a8 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -148,44 +148,94 @@ CardInfo *OracleImporter::addCard(const QString &setName, int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) { - int cards = 0; + int cards = 0; bool ok; QVariantMap resultMap = QtJson::Json::parse(QString(data), ok).toMap(); if (!ok) { - qDebug() << "error: QtJson::Json::parse()"; + qDebug() << "error: QtJson::Json::parse()"; return 0; } QListIterator it(resultMap.value("cards").toList()); QVariantMap map; - QString cardName; - QString cardCost; - QString cardType; - QString cardPT; - QString cardText; - int cardId; - int cardLoyalty; + QString cardName; + QString cardCost; + QString cardType; + QString cardPT; + QString cardText; + int cardId; + int cardLoyalty; + QMap splitCards; while (it.hasNext()) { map = it.next().toMap(); - cardName = map.contains("name") ? map.value("name").toString() : QString(""); - cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); - cardType = map.contains("type") ? map.value("type").toString() : QString(""); - cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); - cardText = map.contains("text") ? map.value("text").toString() : QString(""); - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; - cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toInt() : 0; - QStringList cardTextSplit = cardText.split("\n"); + if(0 == QString::compare(map.value("layout").toString(), QString("split"), Qt::CaseInsensitive)) + { + // Split card handling + cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + if(splitCards.contains(cardId)) + { + // merge two split cards + QVariantMap tmpMap = splitCards.take(cardId); + QVariantMap * card1 = 0, * card2 = 0; + // same cardid + cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + // this is currently an integer; can't accept 2 values + cardLoyalty = 0; - CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardTextSplit); + // determine which subcard is the first one in the split + QStringList names=map.contains("names") ? map.value("names").toStringList() : QStringList(""); + if(names.count()>0 && + map.contains("name") && + 0 == QString::compare(map.value("name").toString(), names.at(0))) + { + // map is the left part of the split card, tmpMap is right part + card1 = ↦ + card2 = &tmpMap; + } else { + //tmpMap is the left part of the split card, map is right part + card1 = &tmpMap; + card2 = ↦ + } - if (!set->contains(card)) { - card->addToSet(set); - cards++; - } + // add first card's data + cardName = card1->contains("name") ? card1->value("name").toString() : QString(""); + cardCost = card1->contains("manaCost") ? card1->value("manaCost").toString() : QString(""); + cardType = card1->contains("type") ? card1->value("type").toString() : QString(""); + cardPT = card1->contains("power") || card1->contains("toughness") ? card1->value("power").toString() + QString('/') + card1->value("toughness").toString() : QString(""); + cardText = card1->contains("text") ? card1->value("text").toString() : QString(""); + + // add second card's data + cardName += card2->contains("name") ? QString(" // ") + card2->value("name").toString() : QString(""); + cardCost += card2->contains("manaCost") ? QString(" // ") + card2->value("manaCost").toString() : QString(""); + cardType += card2->contains("type") ? QString(" // ") + card2->value("type").toString() : QString(""); + cardPT += card2->contains("power") || card2->contains("toughness") ? QString(" // ") + card2->value("power").toString() + QString('/') + card2->value("toughness").toString() : QString(""); + cardText += card2->contains("text") ? QString("\n\n---\n\n") + card2->value("text").toString() : QString(""); + } else { + // first card od a pair; enqueue for later merging + splitCards.insert(cardId, map); + continue; + } + } else { + // normal cards handling + cardName = map.contains("name") ? map.value("name").toString() : QString(""); + cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); + cardType = map.contains("type") ? map.value("type").toString() : QString(""); + cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); + cardText = map.contains("text") ? map.value("text").toString() : QString(""); + cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toInt() : 0; + } + + CardInfo *card = addCard(set->getShortName(), cardName, false, cardId, cardCost, cardType, cardPT, cardLoyalty, cardText.split("\n")); + + if (!set->contains(card)) { + card->addToSet(set); + cards++; + } } - - return cards; + + return cards; } QString OracleImporter::getPictureUrl(QString url, int cardId, QString name, const QString &setName) const From f3a57d5506cf4ea9a680c4f2bd8a48e32348295b Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Mon, 16 Jun 2014 00:27:33 +0200 Subject: [PATCH 19/41] Import data from mtgjson's allsets.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add multiverse id to cards.xml * remove pictures urls from cards.xml TBD: * fix/clean oracle’s gui * add support in cockatrice --- cockatrice/src/carddatabase.cpp | 23 +- cockatrice/src/carddatabase.h | 3 + oracle/src/oracleimporter.cpp | 395 +++++++++++++------------------- oracle/src/oracleimporter.h | 32 +-- oracle/src/window_main.cpp | 8 +- 5 files changed, 193 insertions(+), 268 deletions(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index e5c60e52..748d8197 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -406,12 +406,27 @@ QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfo *info) xml.writeTextElement("name", info->getName()); const SetList &sets = info->getSets(); + QString tmpString; + QString tmpSet; for (int i = 0; i < sets.size(); i++) { xml.writeStartElement("set"); - xml.writeAttribute("picURL", info->getPicURL(sets[i]->getShortName())); - xml.writeAttribute("picURLHq", info->getPicURLHq(sets[i]->getShortName())); - xml.writeAttribute("picURLSt", info->getPicURLSt(sets[i]->getShortName())); - xml.writeCharacters(sets[i]->getShortName()); + + tmpSet=sets[i]->getShortName(); + xml.writeAttribute("muId", QString::number(info->getMuId(tmpSet))); + + tmpString = info->getPicURL(tmpSet); + if(!tmpString.isEmpty()) + xml.writeAttribute("picURL", tmpString); + + tmpString = info->getPicURLHq(tmpSet); + if(!tmpString.isEmpty()) + xml.writeAttribute("picURLHq", tmpString); + + tmpString = info->getPicURLSt(tmpSet); + if(!tmpString.isEmpty()) + xml.writeAttribute("picURLSt", tmpString); + + xml.writeCharacters(tmpSet); xml.writeEndElement(); } const QStringList &colors = info->getColors(); diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index 9d3f33f3..987848d3 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -100,6 +100,7 @@ private: QStringList colors; int loyalty; QMap picURLs, picURLsHq, picURLsSt; + QMap muIds; bool cipt; int tableRow; QPixmap *pixmap; @@ -139,6 +140,7 @@ public: QString getPicURL(const QString &set) const { return picURLs.value(set); } QString getPicURLHq(const QString &set) const { return picURLsHq.value(set); } QString getPicURLSt(const QString &set) const { return picURLsSt.value(set); } + int getMuId(const QString &set) const { return muIds.value(set); } QString getPicURL() const; const QMap &getPicURLs() const { return picURLs; } QString getMainCardType() const; @@ -149,6 +151,7 @@ public: void setPicURL(const QString &_set, const QString &_picURL) { picURLs.insert(_set, _picURL); } void setPicURLHq(const QString &_set, const QString &_picURL) { picURLsHq.insert(_set, _picURL); } void setPicURLSt(const QString &_set, const QString &_picURL) { picURLsSt.insert(_set, _picURL); } + void setMuId(const QString &_set, const int &_muId) { muIds.insert(_set, _muId); } void addToSet(CardSet *set); QPixmap *loadPixmap(); QPixmap *getPixmap(QSize size); diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4adfb1a8..b3aa5e44 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -1,162 +1,138 @@ #include "oracleimporter.h" #include -#include -#include -#include #include #include "qt-json/json.h" OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) - : CardDatabase(parent), dataDir(_dataDir), setIndex(-1) + : CardDatabase(parent), dataDir(_dataDir) { - buffer = new QBuffer(this); - http = new QHttp(this); - connect(http, SIGNAL(requestFinished(int, bool)), this, SLOT(httpRequestFinished(int, bool))); - connect(http, SIGNAL(responseHeaderReceived(const QHttpResponseHeader &)), this, SLOT(readResponseHeader(const QHttpResponseHeader &))); - connect(http, SIGNAL(dataReadProgress(int, int)), this, SIGNAL(dataReadProgress(int, int))); } bool OracleImporter::readSetsFromFile(const QString &fileName) { - QFile setsFile(fileName); - if (!setsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(0, tr("Error"), tr("Cannot open file '%1'.").arg(fileName)); - return false; - } + QFile setsFile(fileName); + if (!setsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, tr("Error"), tr("Cannot open file '%1'.").arg(fileName)); + return false; + } - QXmlStreamReader xml(&setsFile); - return readSetsFromXml(xml); + return readSetsFromByteArray(setsFile.readAll()); } bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { - QXmlStreamReader xml(data); - return readSetsFromXml(xml); -} - -bool OracleImporter::readSetsFromXml(QXmlStreamReader &xml) -{ - QList newSetList; - - QString edition; - QString editionLong; - QString editionURL; - while (!xml.atEnd()) { - if (xml.readNext() == QXmlStreamReader::EndElement) - break; - if (xml.name() == "set") { - bool import = xml.attributes().value("import").toString().toInt(); - while (!xml.atEnd()) { - if (xml.readNext() == QXmlStreamReader::EndElement) - break; - if (xml.name() == "name") - edition = xml.readElementText(); - else if (xml.name() == "longname") - editionLong = xml.readElementText(); - else if (xml.name() == "url") - editionURL = xml.readElementText(); - } - newSetList.append(SetToDownload(edition, editionLong, editionURL, import)); - edition = editionLong = editionURL = QString(); - } else if (xml.name() == "picture_url") - pictureUrl = xml.readElementText(); - else if (xml.name() == "picture_url_hq") - pictureUrlHq = xml.readElementText(); - else if (xml.name() == "picture_url_st") - pictureUrlSt = xml.readElementText(); - else if (xml.name() == "set_url") - setUrl = xml.readElementText(); - } - if (newSetList.isEmpty()) - return false; - allSets = newSetList; - return true; -} - -CardInfo *OracleImporter::addCard(const QString &setName, - QString cardName, - bool isToken, - int cardId, - const QString &cardCost, - const QString &cardType, - const QString &cardPT, - int cardLoyalty, - const QStringList &cardText) -{ - QString fullCardText = cardText.join("\n"); - bool splitCard = false; - if (cardName.contains('(')) { - cardName.remove(QRegExp(" \\(.*\\)")); - splitCard = true; - } - // Workaround for card name weirdness - if (cardName.contains("XX")) - cardName.remove("XX"); - cardName = cardName.replace("Æ", "AE"); - cardName = cardName.replace("’", "'"); - - CardInfo *card; - if (cardHash.contains(cardName)) { - card = cardHash.value(cardName); - if (splitCard && !card->getText().contains(fullCardText)) - card->setText(card->getText() + "\n---\n" + fullCardText); - } else { - bool mArtifact = false; - if (cardType.endsWith("Artifact")) - for (int i = 0; i < cardText.size(); ++i) - if (cardText[i].contains("{T}") && cardText[i].contains("to your mana pool")) - mArtifact = true; - - QStringList colors; - QStringList allColors = QStringList() << "W" << "U" << "B" << "R" << "G"; - for (int i = 0; i < allColors.size(); i++) - if (cardCost.contains(allColors[i])) - colors << allColors[i]; - - if (cardText.contains(cardName + " is white.")) - colors << "W"; - if (cardText.contains(cardName + " is blue.")) - colors << "U"; - if (cardText.contains(cardName + " is black.")) - colors << "B"; - if (cardText.contains(cardName + " is red.")) - colors << "R"; - if (cardText.contains(cardName + " is green.")) - colors << "G"; - - bool cipt = (cardText.contains(cardName + " enters the battlefield tapped.")); - - card = new CardInfo(this, cardName, isToken, cardCost, cardType, cardPT, fullCardText, colors, cardLoyalty, cipt); - int tableRow = 1; - QString mainCardType = card->getMainCardType(); - if ((mainCardType == "Land") || mArtifact) - tableRow = 0; - else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) - tableRow = 3; - else if (mainCardType == "Creature") - tableRow = 2; - card->setTableRow(tableRow); - - cardHash.insert(cardName, card); - } - card->setPicURL(setName, getPictureUrl(pictureUrl, cardId, cardName, setName)); - card->setPicURLHq(setName, getPictureUrl(pictureUrlHq, cardId, cardName, setName)); - card->setPicURLSt(setName, getPictureUrl(pictureUrlSt, cardId, cardName, setName)); - return card; -} - -int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) -{ - int cards = 0; + QList newSetList; + bool ok; - QVariantMap resultMap = QtJson::Json::parse(QString(data), ok).toMap(); + setsMap = QtJson::Json::parse(QString(data), ok).toMap(); if (!ok) { qDebug() << "error: QtJson::Json::parse()"; return 0; } - QListIterator it(resultMap.value("cards").toList()); + QListIterator it(setsMap.values()); + QVariantMap map; + + QString edition; + QString editionLong; + QVariant editionCards; + bool import; + + while (it.hasNext()) { + map = it.next().toMap(); + edition = map.value("code").toString(); + editionLong = map.value("name").toString(); + editionCards = map.value("cards"); + + // core and expansion sets are marked to be imported by default + import = (0 == QString::compare(map.value("type").toString(), QString("core"), Qt::CaseInsensitive) || + 0 == QString::compare(map.value("type").toString(), QString("expansion"), Qt::CaseInsensitive)); + + newSetList.append(SetToDownload(edition, editionLong, editionCards, import)); + } + if (newSetList.isEmpty()) + return false; + allSets = newSetList; + return true; +} + +CardInfo *OracleImporter::addCard(const QString &setName, + QString cardName, + bool isToken, + int cardId, + const QString &cardCost, + const QString &cardType, + const QString &cardPT, + int cardLoyalty, + const QStringList &cardText) +{ + QString fullCardText = cardText.join("\n"); + bool splitCard = false; + if (cardName.contains('(')) { + cardName.remove(QRegExp(" \\(.*\\)")); + splitCard = true; + } + // Workaround for card name weirdness + if (cardName.contains("XX")) + cardName.remove("XX"); + cardName = cardName.replace("Æ", "AE"); + cardName = cardName.replace("’", "'"); + + CardInfo *card; + if (cardHash.contains(cardName)) { + card = cardHash.value(cardName); + if (splitCard && !card->getText().contains(fullCardText)) + card->setText(card->getText() + "\n---\n" + fullCardText); + } else { + bool mArtifact = false; + if (cardType.endsWith("Artifact")) + for (int i = 0; i < cardText.size(); ++i) + if (cardText[i].contains("{T}") && cardText[i].contains("to your mana pool")) + mArtifact = true; + + QStringList colors; + QStringList allColors = QStringList() << "W" << "U" << "B" << "R" << "G"; + for (int i = 0; i < allColors.size(); i++) + if (cardCost.contains(allColors[i])) + colors << allColors[i]; + + if (cardText.contains(cardName + " is white.")) + colors << "W"; + if (cardText.contains(cardName + " is blue.")) + colors << "U"; + if (cardText.contains(cardName + " is black.")) + colors << "B"; + if (cardText.contains(cardName + " is red.")) + colors << "R"; + if (cardText.contains(cardName + " is green.")) + colors << "G"; + + bool cipt = (cardText.contains(cardName + " enters the battlefield tapped.")); + + card = new CardInfo(this, cardName, isToken, cardCost, cardType, cardPT, fullCardText, colors, cardLoyalty, cipt); + int tableRow = 1; + QString mainCardType = card->getMainCardType(); + if ((mainCardType == "Land") || mArtifact) + tableRow = 0; + else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) + tableRow = 3; + else if (mainCardType == "Creature") + tableRow = 2; + card->setTableRow(tableRow); + + cardHash.insert(cardName, card); + } + card->setMuId(setName, cardId); + + return card; +} + +int OracleImporter::importTextSpoiler(CardSet *set, const QVariant &data) +{ + int cards = 0; + + QListIterator it(data.toList()); QVariantMap map; QString cardName; QString cardCost; @@ -237,111 +213,52 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QByteArray &data) return cards; } - +/* QString OracleImporter::getPictureUrl(QString url, int cardId, QString name, const QString &setName) const { - if ((name == "Island") || (name == "Swamp") || (name == "Mountain") || (name == "Plains") || (name == "Forest")) - name.append("1"); - return url.replace("!cardid!", QString::number(cardId)).replace("!set!", setName).replace("!name!", name - .replace("ö", "o") -// .remove('\'') - .remove(" // ") -// .remove(',') -// .remove(':') -// .remove('.') - .remove(QRegExp("\\(.*\\)")) - .simplified() -// .replace(' ', '_') -// .replace('-', '_') - ); + if ((name == "Island") || (name == "Swamp") || (name == "Mountain") || (name == "Plains") || (name == "Forest")) + name.append("1"); + return url.replace("!cardid!", QString::number(cardId)).replace("!set!", setName).replace("!name!", name + .replace("ö", "o") +// .remove('\'') + .remove(" // ") +// .remove(',') +// .remove(':') +// .remove('.') + .remove(QRegExp("\\(.*\\)")) + .simplified() +// .replace(' ', '_') +// .replace('-', '_') + ); } - -int OracleImporter::startDownload() +*/ +int OracleImporter::startImport() { - clear(); - - setsToDownload.clear(); - for (int i = 0; i < allSets.size(); ++i) - if (allSets[i].getImport()) - setsToDownload.append(allSets[i]); - - if (setsToDownload.isEmpty()) - return 0; - setIndex = 0; - emit setIndexChanged(0, 0, setsToDownload[0].getLongName()); - - downloadNextFile(); - return setsToDownload.size(); -} - -void OracleImporter::downloadNextFile() -{ - QString urlString = setsToDownload[setIndex].getUrl(); - if (urlString.isEmpty()) - urlString = setUrl; - urlString = urlString.replace("!name!", setsToDownload[setIndex].getShortName()); - - if (urlString.startsWith("http://")) { - QUrl url(urlString); - http->setHost(url.host(), QHttp::ConnectionModeHttp, url.port() == -1 ? 0 : url.port()); - QString path = QUrl::toPercentEncoding(urlString.mid(url.host().size() + 7).replace(' ', '+'), "?!$&'()*+,;=:@/"); - - buffer->close(); - buffer->setData(QByteArray()); - buffer->open(QIODevice::ReadWrite | QIODevice::Text); - reqId = http->get(path, buffer); - } else { - QFile file(dataDir + "/" + urlString); - file.open(QIODevice::ReadOnly | QIODevice::Text); - - buffer->close(); - buffer->setData(file.readAll()); - buffer->open(QIODevice::ReadWrite | QIODevice::Text); - reqId = 0; - httpRequestFinished(reqId, false); - } -} - -void OracleImporter::httpRequestFinished(int requestId, bool error) -{ - if (error) { - QMessageBox::information(0, tr("HTTP"), tr("Error.")); - return; - } - if (requestId != reqId) - return; - - CardSet *set = new CardSet(setsToDownload[setIndex].getShortName(), setsToDownload[setIndex].getLongName()); - if (!setHash.contains(set->getShortName())) - setHash.insert(set->getShortName(), set); - - buffer->seek(0); - buffer->close(); - int cards = importTextSpoiler(set, buffer->data()); - if (cards > 0) - ++setIndex; - - if (setIndex == setsToDownload.size()) { - emit setIndexChanged(cards, setIndex, QString()); - setIndex = -1; - } else { - downloadNextFile(); - emit setIndexChanged(cards, setIndex, setsToDownload[setIndex].getLongName()); - } -} - -void OracleImporter::readResponseHeader(const QHttpResponseHeader &responseHeader) -{ - switch (responseHeader.statusCode()) { - case 200: - case 301: - case 302: - case 303: - case 307: - break; - default: - QMessageBox::information(0, tr("HTTP"), tr("Download failed: %1.").arg(responseHeader.reasonPhrase())); - http->abort(); - deleteLater(); - } + clear(); + + int setCards = 0, setIndex= 0; + QListIterator it(allSets); + const SetToDownload * curSet; + + while (it.hasNext()) + { + curSet = & it.next(); + + emit setIndexChanged(0, 0, curSet->getLongName()); + + CardSet *set = new CardSet(curSet->getShortName(), curSet->getLongName()); + if (!setHash.contains(set->getShortName())) + setHash.insert(set->getShortName(), set); + + int setCards = importTextSpoiler(set, curSet->getCards()); + + ++setIndex; + + emit setIndexChanged(setCards, setIndex, curSet->getLongName()); + } + + emit setIndexChanged(setCards, setIndex, QString()); + + // total number of sets + return setIndex; } diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 3e03586f..76989933 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -1,44 +1,34 @@ #ifndef ORACLEIMPORTER_H #define ORACLEIMPORTER_H -#include -#include +#include -class QBuffer; -class QXmlStreamReader; +#include class SetToDownload { private: - QString shortName, longName, url; + QString shortName, longName; bool import; + QVariant cards; public: const QString &getShortName() const { return shortName; } const QString &getLongName() const { return longName; } - const QString &getUrl() const { return url; } + const QVariant &getCards() const { return cards; } bool getImport() const { return import; } void setImport(bool _import) { import = _import; } - SetToDownload(const QString &_shortName, const QString &_longName, const QString &_url, bool _import) - : shortName(_shortName), longName(_longName), url(_url), import(_import) { } + SetToDownload(const QString &_shortName, const QString &_longName, const QVariant &_cards, bool _import) + : shortName(_shortName), longName(_longName), cards(_cards), import(_import) { } }; class OracleImporter : public CardDatabase { Q_OBJECT private: - QList allSets, setsToDownload; - QString pictureUrl, pictureUrlHq, pictureUrlSt, setUrl; + QList allSets; + QVariantMap setsMap; QString dataDir; - int setIndex; - int reqId; - QBuffer *buffer; - QHttp *http; - QString getPictureUrl(QString url, int cardId, QString name, const QString &setName) const; void downloadNextFile(); - bool readSetsFromXml(QXmlStreamReader &xml); CardInfo *addCard(const QString &setName, QString cardName, bool isToken, int cardId, const QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, const QStringList &cardText); -private slots: - void httpRequestFinished(int requestId, bool error); - void readResponseHeader(const QHttpResponseHeader &responseHeader); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &nextSetName); void dataReadProgress(int bytesRead, int totalBytes); @@ -46,8 +36,8 @@ public: OracleImporter(const QString &_dataDir, QObject *parent = 0); bool readSetsFromByteArray(const QByteArray &data); bool readSetsFromFile(const QString &fileName); - int startDownload(); - int importTextSpoiler(CardSet *set, const QByteArray &data); + int startImport(); + int importTextSpoiler(CardSet *set, const QVariant &data); QList &getSets() { return allSets; } const QString &getDataDir() const { return dataDir; } }; diff --git a/oracle/src/window_main.cpp b/oracle/src/window_main.cpp index 0283bded..24d79b4f 100644 --- a/oracle/src/window_main.cpp +++ b/oracle/src/window_main.cpp @@ -19,7 +19,7 @@ #include "window_main.h" #include "oracleimporter.h" -const QString WindowMain::defaultSetsUrl = QString("http://www.woogerworks.com/files/sets.xml"); +const QString WindowMain::defaultSetsUrl = QString("http://mtgjson.com/json/AllSets.json"); WindowMain::WindowMain(QWidget *parent) : QMainWindow(parent) @@ -45,7 +45,7 @@ WindowMain::WindowMain(QWidget *parent) checkAllButtonLayout->addWidget(checkAllButton); checkAllButtonLayout->addWidget(uncheckAllButton); - startButton = new QPushButton(tr("&Start download")); + startButton = new QPushButton(tr("&Start import")); connect(startButton, SIGNAL(clicked()), this, SLOT(actStart())); QVBoxLayout *settingsLayout = new QVBoxLayout; @@ -130,7 +130,7 @@ void WindowMain::actLoadSetsFile() { QFileDialog dialog(this, tr("Load sets file")); dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setNameFilter("Sets XML file (*.xml)"); + dialog.setNameFilter("Sets JSON file (*.json)"); if (!dialog.exec()) return; @@ -224,7 +224,7 @@ void WindowMain::actUncheckAll() void WindowMain::actStart() { - int setsCount = importer->startDownload(); + int setsCount = importer->startImport(); if (!setsCount) { QMessageBox::critical(this, tr("Error"), tr("No sets to download selected.")); return; From 40f9536224c26c5e66d2885f2b86ba2424d2f2a0 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Thu, 19 Jun 2014 16:49:38 +0200 Subject: [PATCH 20/41] Rewrite oracle as a wizard --- oracle/CMakeLists.txt | 2 +- oracle/src/main.cpp | 13 +- oracle/src/oracleimporter.cpp | 18 +- oracle/src/oracleimporter.h | 5 +- oracle/src/oraclewizard.cpp | 392 ++++++++++++++++++++++++++++++++++ oracle/src/oraclewizard.h | 111 ++++++++++ oracle/src/window_main.cpp | 248 --------------------- oracle/src/window_main.h | 52 ----- 8 files changed, 518 insertions(+), 323 deletions(-) create mode 100644 oracle/src/oraclewizard.cpp create mode 100644 oracle/src/oraclewizard.h delete mode 100644 oracle/src/window_main.cpp delete mode 100644 oracle/src/window_main.h diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 8e1f9b16..031f38e4 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -9,8 +9,8 @@ set(DESKTOPDIR share/applications CACHE STRING "path to .desktop files") SET(oracle_SOURCES src/main.cpp + src/oraclewizard.cpp src/oracleimporter.cpp - src/window_main.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/settingscache.cpp ../cockatrice/src/qt-json/json.cpp diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index a84f4975..44b900f3 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -1,6 +1,6 @@ #include #include -#include "window_main.h" +#include "oraclewizard.h" #include "settingscache.h" SettingsCache *settingsCache; @@ -12,13 +12,14 @@ int main(int argc, char *argv[]) QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); QCoreApplication::setOrganizationName("Cockatrice"); - QCoreApplication::setOrganizationDomain("cockatrice.de"); + QCoreApplication::setOrganizationDomain("cockatrice"); + // this can't be changed, as it influences the default savepath for cards.xml QCoreApplication::setApplicationName("Cockatrice"); settingsCache = new SettingsCache; - - WindowMain wnd; - wnd.show(); - + + OracleWizard wizard; + wizard.show(); + return app.exec(); } diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index b3aa5e44..a95481e2 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -9,17 +9,6 @@ OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) { } -bool OracleImporter::readSetsFromFile(const QString &fileName) -{ - QFile setsFile(fileName); - if (!setsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(0, tr("Error"), tr("Cannot open file '%1'.").arg(fileName)); - return false; - } - - return readSetsFromByteArray(setsFile.readAll()); -} - bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { QList newSetList; @@ -51,6 +40,9 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) newSetList.append(SetToDownload(edition, editionLong, editionCards, import)); } + + qSort(newSetList); + if (newSetList.isEmpty()) return false; allSets = newSetList; @@ -243,8 +235,8 @@ int OracleImporter::startImport() while (it.hasNext()) { curSet = & it.next(); - - emit setIndexChanged(0, 0, curSet->getLongName()); + if(!curSet->getImport()) + continue; CardSet *set = new CardSet(curSet->getShortName(), curSet->getLongName()); if (!setHash.contains(set->getShortName())) diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 76989933..a320fd5a 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -18,6 +18,7 @@ public: void setImport(bool _import) { import = _import; } SetToDownload(const QString &_shortName, const QString &_longName, const QVariant &_cards, bool _import) : shortName(_shortName), longName(_longName), cards(_cards), import(_import) { } + bool operator<(const SetToDownload &set) const { return longName < set.longName; } }; class OracleImporter : public CardDatabase { @@ -27,15 +28,13 @@ private: QVariantMap setsMap; QString dataDir; - void downloadNextFile(); CardInfo *addCard(const QString &setName, QString cardName, bool isToken, int cardId, const QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, const QStringList &cardText); signals: - void setIndexChanged(int cardsImported, int setIndex, const QString &nextSetName); + void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); public: OracleImporter(const QString &_dataDir, QObject *parent = 0); bool readSetsFromByteArray(const QByteArray &data); - bool readSetsFromFile(const QString &fileName); int startImport(); int importTextSpoiler(CardSet *set, const QVariant &data); QList &getSets() { return allSets; } diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp new file mode 100644 index 00000000..92bcaa4c --- /dev/null +++ b/oracle/src/oraclewizard.cpp @@ -0,0 +1,392 @@ +#include +#include +#include +#include +#include + +#include "oraclewizard.h" +#include "oracleimporter.h" + +#define ALLSETS_URL "http://mtgjson.com/json/AllSets.json" + +OracleWizard::OracleWizard(QWidget *parent) + : QWizard(parent) +{ + importer = new OracleImporter(QDesktopServices::storageLocation(QDesktopServices::DataLocation), this); + + addPage(new IntroPage); + addPage(new LoadSetsPage); + addPage(new ChooseSetsPage); + addPage(new SaveSetsPage); + +/* + setPixmap(QWizard::BannerPixmap, QPixmap(":/images/banner.png")); + setPixmap(QWizard::BackgroundPixmap, QPixmap(":/images/background.png")); +*/ + setWindowTitle(tr("Oracle Importer")); +} + +void OracleWizard::accept() +{ + QDialog::accept(); +} + +void OracleWizard::enableButtons() +{ + button(QWizard::NextButton)->setDisabled(false); + button(QWizard::BackButton)->setDisabled(false); +} + +void OracleWizard::disableButtons() +{ + button(QWizard::NextButton)->setDisabled(true); + button(QWizard::BackButton)->setDisabled(true); +} + +IntroPage::IntroPage(QWidget *parent) + : OracleWizardPage(parent) +{ + setTitle(tr("Introduction")); + //setPixmap(QWizard::WatermarkPixmap, QPixmap(":/images/watermark1.png")); + + label = new QLabel(tr("This wizard will import the list of sets and cards " + "that will be used by Cockatrice. You will need to " + "specify an url or a filename that will be used as a " + "source, and then choose the wanted sets from the list " + "of the available ones."), + this); + label->setWordWrap(true); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(label); + setLayout(layout); +} + +LoadSetsPage::LoadSetsPage(QWidget *parent) + : OracleWizardPage(parent), nam(0) +{ + setTitle(tr("Source selection")); + setSubTitle(tr("Please specify a source for the list of sets and cards. " + "You can specify an url address that will be download or " + "use an existing file from your computer.")); + //setPixmap(QWizard::LogoPixmap, QPixmap(":/images/logo1.png")); + + urlRadioButton = new QRadioButton(tr("Download url:"), this); + fileRadioButton = new QRadioButton(tr("Local file:"), this); + + urlLineEdit = new QLineEdit(this); + fileLineEdit = new QLineEdit(this); + + progressLabel = new QLabel(this); + progressBar = new QProgressBar(this); + + urlRadioButton->setChecked(true); + urlLineEdit->setText(ALLSETS_URL); + + fileButton = new QPushButton(tr("Choose file..."), this); + connect(fileButton, SIGNAL(clicked()), this, SLOT(actLoadSetsFile())); + + QGridLayout *layout = new QGridLayout(this); + layout->addWidget(urlRadioButton, 0, 0); + layout->addWidget(urlLineEdit, 0, 1); + layout->addWidget(fileRadioButton, 1, 0); + layout->addWidget(fileLineEdit, 1, 1); + layout->addWidget(fileButton, 2, 1, Qt::AlignRight); + layout->addWidget(progressLabel, 3, 0); + layout->addWidget(progressBar, 3, 1); + + connect(&watcher, SIGNAL(finished()), this, SLOT(importFinished())); + + setLayout(layout); +} + +void LoadSetsPage::initializePage() +{ + progressLabel->hide(); + progressBar->hide(); +} + +void LoadSetsPage::actLoadSetsFile() +{ + QFileDialog dialog(this, tr("Load sets file")); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilter("Sets JSON file (*.json)"); + + if(!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) + dialog.selectFile(fileLineEdit->text()); + + if (!dialog.exec()) + return; + + fileLineEdit->setText(dialog.selectedFiles().at(0)); +} + +bool LoadSetsPage::validatePage() +{ + // once the import is finished, we call next(); skip validation + if(wizard()->importer->getSets().count() > 0) + return true; + + // else, try to import sets + if(urlRadioButton->isChecked()) + { + QUrl url = QUrl::fromUserInput(urlLineEdit->text()); + if(!url.isValid()) + { + QMessageBox::critical(this, tr("Error"), tr("The provided url is not valid.")); + return false; + } + + progressLabel->setText(tr("Downloading (0MB)")); + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->show(); + progressBar->show(); + + wizard()->disableButtons(); + setEnabled(false); + + if(!nam) + nam = new QNetworkAccessManager(this); + QNetworkReply *reply = nam->get(QNetworkRequest(url)); + + connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSetsFile())); + connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(actDownloadProgressSetsFile(qint64, qint64))); + + } else if(fileRadioButton->isChecked()) { + QFile setsFile(fileLineEdit->text()); + if(!setsFile.exists()) + { + QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); + return false; + } + + if (!setsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + + wizard()->disableButtons(); + setEnabled(false); + + readSetsFromByteArray(setsFile.readAll()); + + } + return false; +} + +void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) +{ + if(total > 0 && progressBar->maximum()==0) + { + progressBar->setMaximum(total); + progressBar->setValue(received); + } + progressLabel->setText(tr("Downloading (%1MB)").arg((int) received / 1048576)); +} + +void LoadSetsPage::actDownloadFinishedSetsFile() +{ + progressLabel->hide(); + progressBar->hide(); + + // check for a reply + QNetworkReply *reply = static_cast(sender()); + QNetworkReply::NetworkError errorCode = reply->error(); + if (errorCode != QNetworkReply::NoError) { + QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); + + wizard()->enableButtons(); + setEnabled(true); + + reply->deleteLater(); + return; + } + + this->readSetsFromByteArray(reply->readAll()); + reply->deleteLater(); +} + +void LoadSetsPage::readSetsFromByteArray(QByteArray data) +{ + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->setText(tr("Parsing file")); + progressLabel->show(); + progressBar->show(); + + // Start the computation. + future = QtConcurrent::run(wizard()->importer, &OracleImporter::readSetsFromByteArray, data); + watcher.setFuture(future); +} + +void LoadSetsPage::importFinished() +{ + if(watcher.future().result()) + { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + + wizard()->next(); + } else { + QMessageBox::critical(this, tr("Error"), tr("The file was retrieved successfully, but it does not contain any sets data.")); + } +} + +ChooseSetsPage::ChooseSetsPage(QWidget *parent) + : OracleWizardPage(parent) +{ + setTitle(tr("Sets selection")); + setSubTitle(tr("The following sets has been found in the source file. " + "Please mark the sets that will be imported.")); + + checkBoxLayout = new QVBoxLayout; + + QWidget *checkboxFrame = new QWidget(this); + checkboxFrame->setLayout(checkBoxLayout); + + QScrollArea *checkboxArea = new QScrollArea(this); + checkboxArea->setWidget(checkboxFrame); + checkboxArea->setWidgetResizable(true); + + checkAllButton = new QPushButton(tr("&Check all")); + connect(checkAllButton, SIGNAL(clicked()), this, SLOT(actCheckAll())); + uncheckAllButton = new QPushButton(tr("&Uncheck all")); + connect(uncheckAllButton, SIGNAL(clicked()), this, SLOT(actUncheckAll())); + + QGridLayout *layout = new QGridLayout(this); + layout->addWidget(checkboxArea, 0, 0, 1, 2); + layout->addWidget(checkAllButton, 1, 0); + layout->addWidget(uncheckAllButton, 1, 1); + + setLayout(layout); +} + +void ChooseSetsPage::initializePage() +{ + // populate checkbox list + for (int i = 0; i < checkBoxList.size(); ++i) + delete checkBoxList[i]; + checkBoxList.clear(); + + QList &sets = wizard()->importer->getSets(); + for (int i = 0; i < sets.size(); ++i) { + QCheckBox *checkBox = new QCheckBox(sets[i].getLongName()); + checkBox->setChecked(sets[i].getImport()); + connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxChanged(int))); + checkBoxLayout->addWidget(checkBox); + checkBoxList << checkBox; + } +} + +void ChooseSetsPage::checkBoxChanged(int state) +{ + QCheckBox *checkBox = qobject_cast(sender()); + QList &sets = wizard()->importer->getSets(); + for (int i = 0; i < sets.size(); ++i) + if (sets[i].getLongName() == checkBox->text()) { + sets[i].setImport(state); + break; + } +} + +void ChooseSetsPage::actCheckAll() +{ + for (int i = 0; i < checkBoxList.size(); ++i) + checkBoxList[i]->setChecked(true); +} + +void ChooseSetsPage::actUncheckAll() +{ + for (int i = 0; i < checkBoxList.size(); ++i) + checkBoxList[i]->setChecked(false); +} + +bool ChooseSetsPage::validatePage() +{ + for (int i = 0; i < checkBoxList.size(); ++i) + { + if(checkBoxList[i]->isChecked()) + return true; + } + + QMessageBox::critical(this, tr("Error"), tr("Please mark at least one set.")); + return false; +} + +SaveSetsPage::SaveSetsPage(QWidget *parent) + : OracleWizardPage(parent) +{ + setTitle(tr("Sets imported")); + setSubTitle(tr("The following sets has been imported. " + "Press \"Done\" to save the imported cards to the Cockatrice database.")); + +// setPixmap(QWizard::LogoPixmap, QPixmap(":/images/logo3.png")); + + messageLog = new QTextEdit(this); + messageLog->setReadOnly(true); + + QGridLayout *layout = new QGridLayout(this); + layout->addWidget(messageLog, 0, 0); + + setLayout(layout); +} + +void SaveSetsPage::cleanupPage() +{ + disconnect(wizard()->importer, SIGNAL(setIndexChanged(int, int, const QString &)), 0, 0); +} + +void SaveSetsPage::initializePage() +{ + messageLog->clear(); + + connect(wizard()->importer, SIGNAL(setIndexChanged(int, int, const QString &)), this, SLOT(updateTotalProgress(int, int, const QString &))); + + if (!wizard()->importer->startImport()) + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); +} + +void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName) +{ + if (setName.isEmpty()) { + messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + ""); + } else { + messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); + } + messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); +} + +bool SaveSetsPage::validatePage() +{ + bool ok = false; + const QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); + QDir dir(dataDir); + if (!dir.exists()) + dir.mkpath(dataDir); + QString savePath = dataDir + "/cards.xml"; + do { + QString fileName; + if (savePath.isEmpty()) + fileName = QFileDialog::getSaveFileName(this, tr("Save card database"), dataDir + "/cards.xml", tr("XML card database (*.xml)")); + else { + fileName = savePath; + savePath.clear(); + } + if (fileName.isEmpty()) { + return false; + } + if (wizard()->importer->saveToFile(fileName)) + ok = true; + else + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to the desired location.")); + } while (!ok); + + return true; +} diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h new file mode 100644 index 00000000..5514b9da --- /dev/null +++ b/oracle/src/oraclewizard.h @@ -0,0 +1,111 @@ +#ifndef ORACLEWIZARD_H +#define ORACLEWIZARD_H + +#include +#include +#include + +class QCheckBox; +class QGroupBox; +class QLabel; +class QLineEdit; +class QRadioButton; +class QProgressBar; +class QNetworkAccessManager; +class QTextEdit; +class QVBoxLayout; +class OracleImporter; + +class OracleWizard : public QWizard +{ + Q_OBJECT +public: + OracleWizard(QWidget *parent = 0); + void accept(); + void enableButtons(); + void disableButtons(); +public: + OracleImporter *importer; +}; + + +class OracleWizardPage : public QWizardPage +{ + Q_OBJECT +public: + OracleWizardPage(QWidget *parent = 0): QWizardPage(parent) {}; +protected: + inline OracleWizard *wizard() { return (OracleWizard*) QWizardPage::wizard(); }; +}; + +class IntroPage : public OracleWizardPage +{ + Q_OBJECT +public: + IntroPage(QWidget *parent = 0); +private: + QLabel *label; +}; + +class LoadSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + LoadSetsPage(QWidget *parent = 0); +protected: + void initializePage(); + bool validatePage(); + void readSetsFromByteArray(QByteArray data); +private: + QRadioButton *urlRadioButton; + QRadioButton *fileRadioButton; + QLineEdit *urlLineEdit; + QLineEdit *fileLineEdit; + QPushButton *fileButton; + QLabel *progressLabel; + QProgressBar * progressBar; + + QNetworkAccessManager *nam; + QFutureWatcher watcher; + QFuture future; +private slots: + void actLoadSetsFile(); + void actDownloadProgressSetsFile(qint64 received, qint64 total); + void actDownloadFinishedSetsFile(); + void importFinished(); +}; + +class ChooseSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + ChooseSetsPage(QWidget *parent = 0); +protected: + void initializePage(); + bool validatePage(); +private: + QPushButton *checkAllButton, *uncheckAllButton; + QVBoxLayout *checkBoxLayout; + QList checkBoxList; +private slots: + void actCheckAll(); + void actUncheckAll(); + void checkBoxChanged(int state); +}; + +class SaveSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + SaveSetsPage(QWidget *parent = 0); +private: + QTextEdit *messageLog; +protected: + void initializePage(); + void cleanupPage(); + bool validatePage(); +private slots: + void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); +}; + +#endif \ No newline at end of file diff --git a/oracle/src/window_main.cpp b/oracle/src/window_main.cpp deleted file mode 100644 index 24d79b4f..00000000 --- a/oracle/src/window_main.cpp +++ /dev/null @@ -1,248 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "window_main.h" -#include "oracleimporter.h" - -const QString WindowMain::defaultSetsUrl = QString("http://mtgjson.com/json/AllSets.json"); - -WindowMain::WindowMain(QWidget *parent) - : QMainWindow(parent) -{ - importer = new OracleImporter(QDesktopServices::storageLocation(QDesktopServices::DataLocation), this); - nam = new QNetworkAccessManager(this); - - checkBoxLayout = new QVBoxLayout; - - QWidget *checkboxFrame = new QWidget; - checkboxFrame->setLayout(checkBoxLayout); - - QScrollArea *checkboxArea = new QScrollArea; - checkboxArea->setWidget(checkboxFrame); - checkboxArea->setWidgetResizable(true); - - checkAllButton = new QPushButton(tr("&Check all")); - connect(checkAllButton, SIGNAL(clicked()), this, SLOT(actCheckAll())); - uncheckAllButton = new QPushButton(tr("&Uncheck all")); - connect(uncheckAllButton, SIGNAL(clicked()), this, SLOT(actUncheckAll())); - - QHBoxLayout *checkAllButtonLayout = new QHBoxLayout; - checkAllButtonLayout->addWidget(checkAllButton); - checkAllButtonLayout->addWidget(uncheckAllButton); - - startButton = new QPushButton(tr("&Start import")); - connect(startButton, SIGNAL(clicked()), this, SLOT(actStart())); - - QVBoxLayout *settingsLayout = new QVBoxLayout; - settingsLayout->addWidget(checkboxArea); - settingsLayout->addLayout(checkAllButtonLayout); - settingsLayout->addWidget(startButton); - - totalLabel = new QLabel(tr("Total progress:")); - totalProgressBar = new QProgressBar; - nextSetLabel1 = new QLabel(tr("Current file:")); - nextSetLabel2 = new QLabel; - fileLabel = new QLabel(tr("Progress:")); - fileProgressBar = new QProgressBar; - - messageLog = new QTextEdit; - messageLog->setReadOnly(true); - - QGridLayout *grid = new QGridLayout; - grid->addWidget(totalLabel, 0, 0); - grid->addWidget(totalProgressBar, 0, 1); - grid->addWidget(nextSetLabel1, 1, 0); - grid->addWidget(nextSetLabel2, 1, 1); - grid->addWidget(fileLabel, 2, 0); - grid->addWidget(fileProgressBar, 2, 1); - grid->addWidget(messageLog, 3, 0, 1, 2); - - QHBoxLayout *mainLayout = new QHBoxLayout; - mainLayout->addLayout(settingsLayout, 6); - mainLayout->addSpacing(10); - mainLayout->addLayout(grid, 10); - - QWidget *centralWidget = new QWidget; - centralWidget->setLayout(mainLayout); - setCentralWidget(centralWidget); - - connect(importer, SIGNAL(setIndexChanged(int, int, const QString &)), this, SLOT(updateTotalProgress(int, int, const QString &))); - connect(importer, SIGNAL(dataReadProgress(int, int)), this, SLOT(updateFileProgress(int, int))); - - aLoadSetsFile = new QAction(tr("Load sets information from &file..."), this); - connect(aLoadSetsFile, SIGNAL(triggered()), this, SLOT(actLoadSetsFile())); - aDownloadSetsFile = new QAction(tr("&Download sets information..."), this); - connect(aDownloadSetsFile, SIGNAL(triggered()), this, SLOT(actDownloadSetsFile())); - aExit = new QAction(tr("E&xit"), this); - connect(aExit, SIGNAL(triggered()), this, SLOT(close())); - - fileMenu = menuBar()->addMenu(tr("&File")); - fileMenu->addAction(aLoadSetsFile); - fileMenu->addAction(aDownloadSetsFile); - fileMenu->addSeparator(); - fileMenu->addAction(aExit); - - setWindowTitle(tr("Oracle importer")); - setMinimumSize(750, 500); - - QStringList args = qApp->arguments(); - if (args.contains("-dlsets")) - downloadSetsFile(defaultSetsUrl); - - statusLabel = new QLabel; - statusBar()->addWidget(statusLabel); - statusLabel->setText(tr("Sets data not loaded.")); -} - -void WindowMain::updateSetList() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - delete checkBoxList[i]; - checkBoxList.clear(); - - QList &sets = importer->getSets(); - for (int i = 0; i < sets.size(); ++i) { - QCheckBox *checkBox = new QCheckBox(sets[i].getLongName()); - checkBox->setChecked(sets[i].getImport()); - connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxChanged(int))); - checkBoxLayout->addWidget(checkBox); - checkBoxList << checkBox; - } - statusLabel->setText(tr("Ready.")); -} - -void WindowMain::actLoadSetsFile() -{ - QFileDialog dialog(this, tr("Load sets file")); - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setNameFilter("Sets JSON file (*.json)"); - if (!dialog.exec()) - return; - - QString fileName = dialog.selectedFiles().at(0); - if (importer->readSetsFromFile(fileName)) - updateSetList(); - else - QMessageBox::critical(this, tr("Error"), tr("This file does not contain any sets data.")); -} - -void WindowMain::actDownloadSetsFile() -{ - QString url = QInputDialog::getText(this, tr("Load sets from URL"), tr("Please enter the URL of the sets file:"), QLineEdit::Normal, defaultSetsUrl); - if (!url.isEmpty()) - downloadSetsFile(url); -} - -void WindowMain::downloadSetsFile(const QString &url) -{ - QNetworkReply *reply = nam->get(QNetworkRequest(url)); - connect(reply, SIGNAL(finished()), this, SLOT(setsDownloadFinished())); -} - -void WindowMain::setsDownloadFinished() -{ - QNetworkReply *reply = static_cast(sender()); - QNetworkReply::NetworkError errorCode = reply->error(); - if (errorCode == QNetworkReply::NoError) { - if (importer->readSetsFromByteArray(reply->readAll())) - updateSetList(); - else - QMessageBox::critical(this, tr("Error"), tr("The file was retrieved successfully, but it does not contain any sets data.")); - } else - QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); - reply->deleteLater(); -} - -void WindowMain::updateTotalProgress(int cardsImported, int setIndex, const QString &nextSetName) -{ - if (setIndex != 0) - messageLog->append(QString("%1: %2 cards imported").arg(nextSetLabel2->text()).arg(cardsImported)); - totalProgressBar->setValue(setIndex); - if (nextSetName.isEmpty()) { - QMessageBox::information(this, tr("Oracle importer"), tr("Import finished: %1 cards.").arg(importer->getCardList().size())); - bool ok = false; - const QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); - QDir dir(dataDir); - if (!dir.exists()) - dir.mkpath(dataDir); - QString savePath = dataDir + "/cards.xml"; - do { - QString fileName; - if (savePath.isEmpty()) - fileName = QFileDialog::getSaveFileName(this, tr("Save card database"), dataDir + "/cards.xml", tr("XML card database (*.xml)")); - else { - fileName = savePath; - savePath.clear(); - } - if (fileName.isEmpty()) { - qApp->quit(); - return; - } - if (importer->saveToFile(fileName)) - ok = true; - else - QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to the desired location.")); - } while (!ok); - qApp->quit(); - } else { - nextSetLabel2->setText(nextSetName); - } -} - -void WindowMain::updateFileProgress(int bytesRead, int totalBytes) -{ - fileProgressBar->setMaximum(totalBytes); - fileProgressBar->setValue(bytesRead); -} - -void WindowMain::actCheckAll() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - checkBoxList[i]->setChecked(true); -} - -void WindowMain::actUncheckAll() -{ - for (int i = 0; i < checkBoxList.size(); ++i) - checkBoxList[i]->setChecked(false); -} - -void WindowMain::actStart() -{ - int setsCount = importer->startImport(); - if (!setsCount) { - QMessageBox::critical(this, tr("Error"), tr("No sets to download selected.")); - return; - } - for (int i = 0; i < checkBoxList.size(); ++i) - checkBoxList[i]->setEnabled(false); - startButton->setEnabled(false); - totalProgressBar->setMaximum(setsCount); - statusLabel->setText(tr("Downloading card data...")); -} - -void WindowMain::checkBoxChanged(int state) -{ - QCheckBox *checkBox = qobject_cast(sender()); - QList &sets = importer->getSets(); - for (int i = 0; i < sets.size(); ++i) - if (sets[i].getLongName() == checkBox->text()) { - sets[i].setImport(state); - break; - } -} diff --git a/oracle/src/window_main.h b/oracle/src/window_main.h deleted file mode 100644 index 1fdba235..00000000 --- a/oracle/src/window_main.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef WINDOW_MAIN_H -#define WINDOW_MAIN_H - -#include -#include - -class OracleImporter; -class QLabel; -class QProgressBar; -class QTextEdit; -class QPushButton; -class QCheckBox; -class QVBoxLayout; -class QMenu; -class QAction; -class QNetworkAccessManager; - -class WindowMain : public QMainWindow { - Q_OBJECT -private: - static const QString defaultSetsUrl; - - OracleImporter *importer; - QNetworkAccessManager *nam; - - QMenu *fileMenu; - QAction *aLoadSetsFile, *aDownloadSetsFile, *aExit; - QPushButton *checkAllButton, *uncheckAllButton, *startButton; - QLabel *totalLabel, *fileLabel, *nextSetLabel1, *nextSetLabel2; - QProgressBar *totalProgressBar, *fileProgressBar; - QTextEdit *messageLog; - QVBoxLayout *checkBoxLayout; - QList checkBoxList; - QLabel *statusLabel; - - void downloadSetsFile(const QString &url); -private slots: - void updateTotalProgress(int cardsImported, int setIndex, const QString &nextSetName); - void updateFileProgress(int bytesRead, int totalBytes); - void updateSetList(); - void actCheckAll(); - void actUncheckAll(); - void actStart(); - void actLoadSetsFile(); - void actDownloadSetsFile(); - void setsDownloadFinished(); - void checkBoxChanged(int state); -public: - WindowMain(QWidget *parent = 0); -}; - -#endif From 8281f631342d92d5b54b99cb3b1a6b8d88fb831f Mon Sep 17 00:00:00 2001 From: Daenyth Date: Fri, 20 Jun 2014 22:10:26 -0400 Subject: [PATCH 21/41] Add settings fields for pic urls --- cockatrice/src/settingscache.cpp | 33 +++++++++++++++++++++++--------- cockatrice/src/settingscache.h | 9 +++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index b73d6d86..3bcc4111 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -4,23 +4,26 @@ SettingsCache::SettingsCache() { settings = new QSettings(this); - + lang = settings->value("personal/lang").toString(); - + deckPath = settings->value("paths/decks").toString(); replaysPath = settings->value("paths/replays").toString(); picsPath = settings->value("paths/pics").toString(); cardDatabasePath = settings->value("paths/carddatabase").toString(); tokenDatabasePath = settings->value("paths/tokendatabase").toString(); - + handBgPath = settings->value("zonebg/hand").toString(); stackBgPath = settings->value("zonebg/stack").toString(); tableBgPath = settings->value("zonebg/table").toString(); playerBgPath = settings->value("zonebg/playerarea").toString(); cardBackPicturePath = settings->value("paths/cardbackpicture").toString(); - - mainWindowGeometry = settings->value("interface/main_window_geometry").toByteArray(); + picDownload = settings->value("personal/picturedownload", true).toBool(); + picUrl = settings->value("personal/picUrl", PIC_URL_DEFAULT).toString(); + picUrlHq = settings->value("personal/picUrlHq", PIC_URL_HQ_DEFAULT).toString(); + + mainWindowGeometry = settings->value("interface/main_window_geometry").toByteArray(); notificationsEnabled = settings->value("interface/notificationsenabled", true).toBool(); doubleClickToPlay = settings->value("interface/doubleclicktoplay", true).toBool(); playToStack = settings->value("interface/playtostack", false).toBool(); @@ -31,15 +34,15 @@ SettingsCache::SettingsCache() invertVerticalCoordinate = settings->value("table/invert_vertical", false).toBool(); minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 5).toInt(); tapAnimation = settings->value("cards/tapanimation", true).toBool(); - + zoneViewSortByName = settings->value("zoneview/sortbyname", true).toBool(); zoneViewSortByType = settings->value("zoneview/sortbytype", true).toBool(); - + soundEnabled = settings->value("sound/enabled", false).toBool(); soundPath = settings->value("sound/path").toString(); - + priceTagFeature = settings->value("deckeditor/pricetags", false).toBool(); - + ignoreUnregisteredUsers = settings->value("chat/ignore_unregistered", false).toBool(); } @@ -125,6 +128,18 @@ void SettingsCache::setPicDownload(int _picDownload) emit picDownloadChanged(); } +void SettingsCache::setPicUrl(const QString &_picUrl) +{ + picUrl = _picUrl; + settings->setValue("personal/picUrl", picUrl); +} + +void SettingsCache::setPicUrlHq(const QString &_picUrlHq) +{ + picUrlHq = _picUrlHq; + settings->setValue("personal/picUrlHq", picUrlHq); +} + void SettingsCache::setNotificationsEnabled(int _notificationsEnabled) { notificationsEnabled = _notificationsEnabled; diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index 6c28227a..ba3dbb00 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -3,6 +3,9 @@ #include +#define PIC_URL_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" +#define PIC_URL_HQ_DEFAULT "http://mtgimage.com/multiverseid/!cardid!.jpg" + class QSettings; class SettingsCache : public QObject { @@ -48,6 +51,8 @@ private: QString soundPath; bool priceTagFeature; bool ignoreUnregisteredUsers; + QString picUrl; + QString picUrlHq; public: SettingsCache(); const QByteArray &getMainWindowGeometry() const { return mainWindowGeometry; } @@ -79,6 +84,8 @@ public: QString getSoundPath() const { return soundPath; } bool getPriceTagFeature() const { return priceTagFeature; } bool getIgnoreUnregisteredUsers() const { return ignoreUnregisteredUsers; } + QString getPicUrl() const { return picUrl; } + QString getPicUrlHq() const { return picUrlHq; } public slots: void setMainWindowGeometry(const QByteArray &_mainWindowGeometry); void setLang(const QString &_lang); @@ -109,6 +116,8 @@ public slots: void setSoundPath(const QString &_soundPath); void setPriceTagFeature(int _priceTagFeature); void setIgnoreUnregisteredUsers(bool _ignoreUnregisteredUsers); + void setPicUrl(const QString &_picUrl); + void setPicUrlHq(const QString &_picUrlHq); }; extern SettingsCache *settingsCache; From 53d2b82e61471095b562f5251af3cc3b697f89af Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sat, 21 Jun 2014 01:12:47 -0400 Subject: [PATCH 22/41] Use picUrl templates to download cards. Add HQ card option --- cockatrice/src/carddatabase.cpp | 125 +++++++++++++++++-------------- cockatrice/src/carddatabase.h | 24 +++--- cockatrice/src/dlg_settings.cpp | 20 +++-- cockatrice/src/dlg_settings.h | 1 + cockatrice/src/settingscache.cpp | 8 ++ cockatrice/src/settingscache.h | 6 +- 6 files changed, 105 insertions(+), 79 deletions(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 748d8197..2045f95e 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -80,11 +80,13 @@ bool PictureToLoad::nextSet() return true; } -PictureLoader::PictureLoader(const QString &__picsPath, bool _picDownload, QObject *parent) - : QObject(parent), _picsPath(__picsPath), picDownload(_picDownload), downloadRunning(false), loadQueueRunning(false) +PictureLoader::PictureLoader(const QString &__picsPath, bool _picDownload, bool _picDownloadHq, QObject *parent) + : QObject(parent), + _picsPath(__picsPath), picDownload(_picDownload), picDownloadHq(_picDownloadHq), + downloadRunning(false), loadQueueRunning(false) { connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection); - + networkManager = new QNetworkAccessManager(this); connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *))); } @@ -99,7 +101,7 @@ void PictureLoader::processLoadQueue() { if (loadQueueRunning) return; - + loadQueueRunning = true; forever { mutex.lock(); @@ -113,7 +115,7 @@ void PictureLoader::processLoadQueue() QString correctedName = ptl.getCard()->getCorrectedName(); QString picsPath = _picsPath; QString setName = ptl.getSetName(); - + QImage image; if (!image.load(QString("%1/%2/%3.full.jpg").arg(picsPath).arg(setName).arg(correctedName))) if (!image.load(QString("%1/%2/%3%4.full.jpg").arg(picsPath).arg(setName).arg(correctedName).arg(1))) @@ -135,6 +137,20 @@ void PictureLoader::processLoadQueue() } } +QString PictureLoader::getPicUrl(CardInfo *card) +{ + if (!picDownload) return 0; + + QString picUrl = picDownloadHq ? settingsCache->getPicUrlHq() : settingsCache->getPicUrl(); + picUrl.replace("!name!", QUrl::toPercentEncoding(card->getCorrectedName())); + CardSet *set = card->getPreferredSet(); + picUrl.replace("!setcode!", QUrl::toPercentEncoding(set->getShortName())); + picUrl.replace("!setname!", QUrl::toPercentEncoding(set->getLongName())); + picUrl.replace("!cardid!", QUrl::toPercentEncoding(QString::number(card->getPreferredMuId()))); + + return picUrl; +} + void PictureLoader::startNextPicDownload() { if (cardsToDownload.isEmpty()) { @@ -142,23 +158,16 @@ void PictureLoader::startNextPicDownload() downloadRunning = false; return; } - + downloadRunning = true; - + cardBeingDownloaded = cardsToDownload.takeFirst(); - QString picUrl; - if (cardBeingDownloaded.getStripped()) - picUrl = cardBeingDownloaded.getCard()->getPicURLSt(cardBeingDownloaded.getSetName()); - else if (cardBeingDownloaded.getHq()) { - picUrl = cardBeingDownloaded.getCard()->getPicURLHq(cardBeingDownloaded.getSetName()); - if (picUrl.isEmpty()) { - picUrl = cardBeingDownloaded.getCard()->getPicURL(cardBeingDownloaded.getSetName()); - cardBeingDownloaded.setHq(false); - } - } else - picUrl = cardBeingDownloaded.getCard()->getPicURL(cardBeingDownloaded.getSetName()); + + // TODO: Do something useful when picUrl is 0 or empty, etc + QString picUrl = getPicUrl(cardBeingDownloaded.getCard()); + QUrl url(picUrl); - + QNetworkRequest req(url); qDebug() << "starting picture download:" << req.url(); networkManager->get(req); @@ -232,6 +241,12 @@ void PictureLoader::setPicDownload(bool _picDownload) picDownload = _picDownload; } +void PictureLoader::setPicDownloadHq(bool _picDownloadHq) +{ + QMutexLocker locker(&mutex); + picDownloadHq = _picDownloadHq; +} + CardInfo::CardInfo(CardDatabase *_db, const QString &_name, bool _isToken, @@ -244,22 +259,18 @@ CardInfo::CardInfo(CardDatabase *_db, bool _cipt, int _tableRow, const SetList &_sets, - const QMap &_picURLs, - const QMap &_picURLsHq, - const QMap &_picURLsSt) + QMap _muIds) : db(_db), name(_name), isToken(_isToken), sets(_sets), + muIds(_muIds), manacost(_manacost), cardtype(_cardtype), powtough(_powtough), text(_text), colors(_colors), loyalty(_loyalty), - picURLs(_picURLs), - picURLsHq(_picURLsHq), - picURLsSt(_picURLsSt), cipt(_cipt), tableRow(_tableRow), pixmap(NULL) @@ -315,19 +326,12 @@ void CardInfo::addToSet(CardSet *set) sets << set; } -QString CardInfo::getPicURL() const -{ - SetList sortedSets = sets; - sortedSets.sortByKey(); - return picURLs.value(sortedSets.first()->getShortName()); -} - QPixmap *CardInfo::loadPixmap() { if (pixmap) return pixmap; pixmap = new QPixmap(); - + if (getName().isEmpty()) { pixmap->load(settingsCache->getCardBackPicturePath()); return pixmap; @@ -400,6 +404,18 @@ void CardInfo::updatePixmapCache() emit pixmapUpdated(); } +CardSet* CardInfo::getPreferredSet() +{ + SetList sortedSets = sets; + sortedSets.sortByKey(); + return sortedSets.first(); +} + +int CardInfo::getPreferredMuId() +{ + return muIds[getPreferredSet()->getShortName()]; +} + QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfo *info) { xml.writeStartElement("card"); @@ -414,18 +430,6 @@ QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfo *info) tmpSet=sets[i]->getShortName(); xml.writeAttribute("muId", QString::number(info->getMuId(tmpSet))); - tmpString = info->getPicURL(tmpSet); - if(!tmpString.isEmpty()) - xml.writeAttribute("picURL", tmpString); - - tmpString = info->getPicURLHq(tmpSet); - if(!tmpString.isEmpty()) - xml.writeAttribute("picURLHq", tmpString); - - tmpString = info->getPicURLSt(tmpSet); - if(!tmpString.isEmpty()) - xml.writeAttribute("picURLSt", tmpString); - xml.writeCharacters(tmpSet); xml.writeEndElement(); } @@ -457,12 +461,13 @@ CardDatabase::CardDatabase(QObject *parent) connect(settingsCache, SIGNAL(cardDatabasePathChanged()), this, SLOT(loadCardDatabase())); connect(settingsCache, SIGNAL(tokenDatabasePathChanged()), this, SLOT(loadTokenDatabase())); connect(settingsCache, SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); - + connect(settingsCache, SIGNAL(picDownloadHqChanged()), this, SLOT(picDownloadHqChanged())); + loadCardDatabase(); loadTokenDatabase(); - + pictureLoaderThread = new QThread; - pictureLoader = new PictureLoader(settingsCache->getPicsPath(), settingsCache->getPicDownload()); + pictureLoader = new PictureLoader(settingsCache->getPicsPath(), settingsCache->getPicDownload(), settingsCache->getPicDownloadHq()); pictureLoader->moveToThread(pictureLoaderThread); connect(pictureLoader, SIGNAL(imageLoaded(CardInfo *, const QImage &)), this, SLOT(imageLoaded(CardInfo *, const QImage &))); pictureLoaderThread->start(QThread::LowPriority); @@ -587,7 +592,7 @@ void CardDatabase::loadCardsFromXml(QXmlStreamReader &xml) if (xml.name() == "card") { QString name, manacost, type, pt, text; QStringList colors; - QMap picURLs, picURLsHq, picURLsSt; + QMap muids; SetList sets; int tableRow = 0; int loyalty = 0; @@ -607,14 +612,12 @@ void CardDatabase::loadCardsFromXml(QXmlStreamReader &xml) else if (xml.name() == "text") text = xml.readElementText(); else if (xml.name() == "set") { - QString picURL = xml.attributes().value("picURL").toString(); - QString picURLHq = xml.attributes().value("picURLHq").toString(); - QString picURLSt = xml.attributes().value("picURLSt").toString(); + QXmlStreamAttributes attrs = xml.attributes(); QString setName = xml.readElementText(); sets.append(getSet(setName)); - picURLs.insert(setName, picURL); - picURLsHq.insert(setName, picURLHq); - picURLsSt.insert(setName, picURLSt); + if (attrs.hasAttribute("muId")) { + muids[setName] = attrs.value("muId").toString().toInt(); + } } else if (xml.name() == "color") colors << xml.readElementText(); else if (xml.name() == "tablerow") @@ -626,7 +629,7 @@ void CardDatabase::loadCardsFromXml(QXmlStreamReader &xml) else if (xml.name() == "token") isToken = xml.readElementText().toInt(); } - cardHash.insert(name, new CardInfo(this, name, isToken, manacost, type, pt, text, colors, loyalty, cipt, tableRow, sets, picURLs, picURLsHq, picURLsSt)); + cardHash.insert(name, new CardInfo(this, name, isToken, manacost, type, pt, text, colors, loyalty, cipt, tableRow, sets, muids)); } } } @@ -732,6 +735,16 @@ void CardDatabase::picDownloadChanged() } } +void CardDatabase::picDownloadHqChanged() +{ + pictureLoader->setPicDownloadHq(settingsCache->getPicDownloadHq()); + if (settingsCache->getPicDownloadHq()) { + QHashIterator cardIterator(cardHash); + while (cardIterator.hasNext()) + cardIterator.next().value()->clearPixmapCacheMiss(); + } +} + bool CardDatabase::loadCardDatabase(const QString &path, bool tokens) { bool tempLoadSuccess = false; diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index 987848d3..39d2faec 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -68,13 +68,15 @@ private: QNetworkAccessManager *networkManager; QList cardsToDownload; PictureToLoad cardBeingDownloaded; - bool picDownload, downloadRunning, loadQueueRunning; + bool picDownload, picDownloadHq, downloadRunning, loadQueueRunning; void startNextPicDownload(); + QString getPicUrl(CardInfo* card); public: - PictureLoader(const QString &__picsPath, bool _picDownload, QObject *parent = 0); + PictureLoader(const QString &__picsPath, bool _picDownload, bool _picDownloadHq, QObject *parent = 0); ~PictureLoader(); void setPicsPath(const QString &path); void setPicDownload(bool _picDownload); + void setPicDownloadHq(bool _picDownloadHq); void loadImage(CardInfo *card, bool stripped); private slots: void picDownloadFinished(QNetworkReply *reply); @@ -99,7 +101,6 @@ private: QString text; QStringList colors; int loyalty; - QMap picURLs, picURLsHq, picURLsSt; QMap muIds; bool cipt; int tableRow; @@ -118,9 +119,7 @@ public: bool _cipt = false, int _tableRow = 0, const SetList &_sets = SetList(), - const QStringMap &_picURLs = QStringMap(), - const QStringMap &_picURLsHq = QStringMap(), - const QStringMap &_picURLsSt = QStringMap()); + QMap muids = QMap()); ~CardInfo(); const QString &getName() const { return name; } bool getIsToken() const { return isToken; } @@ -137,20 +136,12 @@ public: void setText(const QString &_text) { text = _text; emit cardInfoChanged(this); } void setColors(const QStringList &_colors) { colors = _colors; emit cardInfoChanged(this); } const QStringList &getColors() const { return colors; } - QString getPicURL(const QString &set) const { return picURLs.value(set); } - QString getPicURLHq(const QString &set) const { return picURLsHq.value(set); } - QString getPicURLSt(const QString &set) const { return picURLsSt.value(set); } int getMuId(const QString &set) const { return muIds.value(set); } - QString getPicURL() const; - const QMap &getPicURLs() const { return picURLs; } QString getMainCardType() const; QString getCorrectedName() const; int getTableRow() const { return tableRow; } void setTableRow(int _tableRow) { tableRow = _tableRow; } void setLoyalty(int _loyalty) { loyalty = _loyalty; emit cardInfoChanged(this); } - void setPicURL(const QString &_set, const QString &_picURL) { picURLs.insert(_set, _picURL); } - void setPicURLHq(const QString &_set, const QString &_picURL) { picURLsHq.insert(_set, _picURL); } - void setPicURLSt(const QString &_set, const QString &_picURL) { picURLsSt.insert(_set, _picURL); } void setMuId(const QString &_set, const int &_muId) { muIds.insert(_set, _muId); } void addToSet(CardSet *set); QPixmap *loadPixmap(); @@ -158,6 +149,8 @@ public: void clearPixmapCache(); void clearPixmapCacheMiss(); void imageLoaded(const QImage &image); + CardSet *getPreferredSet(); + int getPreferredMuId(); public slots: void updatePixmapCache(); signals: @@ -172,7 +165,7 @@ protected: QHash setHash; bool loadSuccess; CardInfo *noCard; - + QThread *pictureLoaderThread; PictureLoader *pictureLoader; private: @@ -202,6 +195,7 @@ public slots: private slots: void imageLoaded(CardInfo *card, QImage image); void picDownloadChanged(); + void picDownloadHqChanged(); void picsPathChanged(); void loadCardDatabase(); diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index 32d4f1fb..f7293964 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -27,7 +27,7 @@ GeneralSettingsPage::GeneralSettingsPage() { languageLabel = new QLabel; languageBox = new QComboBox; - + QString setLanguage = settingsCache->getLang(); QStringList qmFiles = findQmFiles(); for (int i = 0; i < qmFiles.size(); i++) { @@ -36,27 +36,32 @@ GeneralSettingsPage::GeneralSettingsPage() if ((qmFiles[i] == setLanguage) || (setLanguage.isEmpty() && langName == tr("English"))) languageBox->setCurrentIndex(i); } - + picDownloadCheckBox = new QCheckBox; picDownloadCheckBox->setChecked(settingsCache->getPicDownload()); - + + picDownloadHqCheckBox = new QCheckBox; + picDownloadHqCheckBox->setChecked(settingsCache->getPicDownloadHq()); + connect(languageBox, SIGNAL(currentIndexChanged(int)), this, SLOT(languageBoxChanged(int))); connect(picDownloadCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setPicDownload(int))); - + connect(picDownloadHqCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setPicDownloadHq(int))); + QGridLayout *personalGrid = new QGridLayout; personalGrid->addWidget(languageLabel, 0, 0); personalGrid->addWidget(languageBox, 0, 1); personalGrid->addWidget(picDownloadCheckBox, 1, 0, 1, 2); - + personalGrid->addWidget(picDownloadHqCheckBox, 2, 0, 1, 2); + personalGroupBox = new QGroupBox; personalGroupBox->setLayout(personalGrid); - + deckPathLabel = new QLabel; deckPathEdit = new QLineEdit(settingsCache->getDeckPath()); deckPathEdit->setReadOnly(true); QPushButton *deckPathButton = new QPushButton("..."); connect(deckPathButton, SIGNAL(clicked()), this, SLOT(deckPathButtonClicked())); - + replaysPathLabel = new QLabel; replaysPathEdit = new QLineEdit(settingsCache->getReplaysPath()); replaysPathEdit->setReadOnly(true); @@ -183,6 +188,7 @@ void GeneralSettingsPage::retranslateUi() personalGroupBox->setTitle(tr("Personal settings")); languageLabel->setText(tr("Language:")); picDownloadCheckBox->setText(tr("Download card pictures on the fly")); + picDownloadHqCheckBox->setText(tr("Download high-quality card pictures")); pathsGroupBox->setTitle(tr("Paths")); deckPathLabel->setText(tr("Decks directory:")); replaysPathLabel->setText(tr("Replays directory:")); diff --git a/cockatrice/src/dlg_settings.h b/cockatrice/src/dlg_settings.h index 5f87887c..8462f612 100644 --- a/cockatrice/src/dlg_settings.h +++ b/cockatrice/src/dlg_settings.h @@ -40,6 +40,7 @@ private: QGroupBox *personalGroupBox, *pathsGroupBox; QComboBox *languageBox; QCheckBox *picDownloadCheckBox; + QCheckBox *picDownloadHqCheckBox; QLabel *languageLabel, *deckPathLabel, *replaysPathLabel, *picsPathLabel, *cardDatabasePathLabel, *tokenDatabasePathLabel; }; diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 3bcc4111..ac5c130f 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -20,6 +20,7 @@ SettingsCache::SettingsCache() cardBackPicturePath = settings->value("paths/cardbackpicture").toString(); picDownload = settings->value("personal/picturedownload", true).toBool(); + picDownloadHq = settings->value("personal/picturedownloadhq", false).toBool(); picUrl = settings->value("personal/picUrl", PIC_URL_DEFAULT).toString(); picUrlHq = settings->value("personal/picUrlHq", PIC_URL_HQ_DEFAULT).toString(); @@ -128,6 +129,13 @@ void SettingsCache::setPicDownload(int _picDownload) emit picDownloadChanged(); } +void SettingsCache::setPicDownloadHq(int _picDownloadHq) +{ + picDownloadHq = _picDownloadHq; + settings->setValue("personal/picturedownloadhq", picDownloadHq); + emit picDownloadHqChanged(); +} + void SettingsCache::setPicUrl(const QString &_picUrl) { picUrl = _picUrl; diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index ba3dbb00..5156772d 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -3,7 +3,7 @@ #include -#define PIC_URL_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" +#define PIC_URL_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" #define PIC_URL_HQ_DEFAULT "http://mtgimage.com/multiverseid/!cardid!.jpg" class QSettings; @@ -21,6 +21,7 @@ signals: void playerBgPathChanged(); void cardBackPicturePathChanged(); void picDownloadChanged(); + void picDownloadHqChanged(); void displayCardNamesChanged(); void horizontalHandChanged(); void invertVerticalCoordinateChanged(); @@ -36,6 +37,7 @@ private: QString deckPath, replaysPath, picsPath, cardDatabasePath, tokenDatabasePath; QString handBgPath, stackBgPath, tableBgPath, playerBgPath, cardBackPicturePath; bool picDownload; + bool picDownloadHq; bool notificationsEnabled; bool doubleClickToPlay; bool playToStack; @@ -68,6 +70,7 @@ public: QString getPlayerBgPath() const { return playerBgPath; } QString getCardBackPicturePath() const { return cardBackPicturePath; } bool getPicDownload() const { return picDownload; } + bool getPicDownloadHq() const { return picDownloadHq; } bool getNotificationsEnabled() const { return notificationsEnabled; } bool getDoubleClickToPlay() const { return doubleClickToPlay; } bool getPlayToStack() const { return playToStack; } @@ -100,6 +103,7 @@ public slots: void setPlayerBgPath(const QString &_playerBgPath); void setCardBackPicturePath(const QString &_cardBackPicturePath); void setPicDownload(int _picDownload); + void setPicDownloadHq(int _picDownloadHq); void setNotificationsEnabled(int _notificationsEnabled); void setDoubleClickToPlay(int _doubleClickToPlay); void setPlayToStack(int _playToStack); From 53de2db89c6da54895c3182a4001834263ac66fb Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:04:52 +0200 Subject: [PATCH 23/41] Clean up code leftovers --- oracle/src/oracleimporter.cpp | 20 +------------------- oracle/src/oraclewizard.cpp | 8 -------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index a95481e2..c531a036 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -205,25 +205,7 @@ int OracleImporter::importTextSpoiler(CardSet *set, const QVariant &data) return cards; } -/* -QString OracleImporter::getPictureUrl(QString url, int cardId, QString name, const QString &setName) const -{ - if ((name == "Island") || (name == "Swamp") || (name == "Mountain") || (name == "Plains") || (name == "Forest")) - name.append("1"); - return url.replace("!cardid!", QString::number(cardId)).replace("!set!", setName).replace("!name!", name - .replace("ö", "o") -// .remove('\'') - .remove(" // ") -// .remove(',') -// .remove(':') -// .remove('.') - .remove(QRegExp("\\(.*\\)")) - .simplified() -// .replace(' ', '_') -// .replace('-', '_') - ); -} -*/ + int OracleImporter::startImport() { clear(); diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 92bcaa4c..5869b873 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -19,10 +19,6 @@ OracleWizard::OracleWizard(QWidget *parent) addPage(new ChooseSetsPage); addPage(new SaveSetsPage); -/* - setPixmap(QWizard::BannerPixmap, QPixmap(":/images/banner.png")); - setPixmap(QWizard::BackgroundPixmap, QPixmap(":/images/background.png")); -*/ setWindowTitle(tr("Oracle Importer")); } @@ -47,7 +43,6 @@ IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent) { setTitle(tr("Introduction")); - //setPixmap(QWizard::WatermarkPixmap, QPixmap(":/images/watermark1.png")); label = new QLabel(tr("This wizard will import the list of sets and cards " "that will be used by Cockatrice. You will need to " @@ -69,7 +64,6 @@ LoadSetsPage::LoadSetsPage(QWidget *parent) setSubTitle(tr("Please specify a source for the list of sets and cards. " "You can specify an url address that will be download or " "use an existing file from your computer.")); - //setPixmap(QWizard::LogoPixmap, QPixmap(":/images/logo1.png")); urlRadioButton = new QRadioButton(tr("Download url:"), this); fileRadioButton = new QRadioButton(tr("Local file:"), this); @@ -327,8 +321,6 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) setSubTitle(tr("The following sets has been imported. " "Press \"Done\" to save the imported cards to the Cockatrice database.")); -// setPixmap(QWizard::LogoPixmap, QPixmap(":/images/logo3.png")); - messageLog = new QTextEdit(this); messageLog->setReadOnly(true); From 075c743a17f33e9d195abfae71902176dd7b12d3 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:11:49 +0200 Subject: [PATCH 24/41] Sets: case insensitive sorting --- oracle/src/oracleimporter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index a320fd5a..dc5b45fc 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -18,7 +18,7 @@ public: void setImport(bool _import) { import = _import; } SetToDownload(const QString &_shortName, const QString &_longName, const QVariant &_cards, bool _import) : shortName(_shortName), longName(_longName), cards(_cards), import(_import) { } - bool operator<(const SetToDownload &set) const { return longName < set.longName; } + bool operator<(const SetToDownload &set) const { return longName.compare(set.longName, Qt::CaseInsensitive) < 0; } }; class OracleImporter : public CardDatabase { From c6078c384c693eef15afaf9786e12489c02f75d5 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:18:41 +0200 Subject: [PATCH 25/41] Strip { } around mana cost --- oracle/src/oracleimporter.cpp | 8 ++++++-- oracle/src/oracleimporter.h | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index c531a036..70972d92 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -53,7 +53,7 @@ CardInfo *OracleImporter::addCard(const QString &setName, QString cardName, bool isToken, int cardId, - const QString &cardCost, + QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, @@ -69,7 +69,11 @@ CardInfo *OracleImporter::addCard(const QString &setName, if (cardName.contains("XX")) cardName.remove("XX"); cardName = cardName.replace("Æ", "AE"); - cardName = cardName.replace("’", "'"); + cardName = cardName.replace("’", "'"); + + // Remove {} around mana costs + cardCost.remove(QChar('{')); + cardCost.remove(QChar('}')); CardInfo *card; if (cardHash.contains(cardName)) { diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index dc5b45fc..1f705eeb 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -28,7 +28,7 @@ private: QVariantMap setsMap; QString dataDir; - CardInfo *addCard(const QString &setName, QString cardName, bool isToken, int cardId, const QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, const QStringList &cardText); + CardInfo *addCard(const QString &setName, QString cardName, bool isToken, int cardId, QString &cardCost, const QString &cardType, const QString &cardPT, int cardLoyalty, const QStringList &cardText); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); From bb9a79f83b5253df1b2e4465b3d874c2fbd941a9 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:25:24 +0200 Subject: [PATCH 26/41] Rename the finish/done button as "Save" --- oracle/src/oraclewizard.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 5869b873..7b48b341 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -20,6 +20,7 @@ OracleWizard::OracleWizard(QWidget *parent) addPage(new SaveSetsPage); setWindowTitle(tr("Oracle Importer")); + QWizard::setButtonText(QWizard::FinishButton, tr("Save")); } void OracleWizard::accept() @@ -319,7 +320,7 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) { setTitle(tr("Sets imported")); setSubTitle(tr("The following sets has been imported. " - "Press \"Done\" to save the imported cards to the Cockatrice database.")); + "Press \"Save\" to save the imported cards to the Cockatrice database.")); messageLog = new QTextEdit(this); messageLog->setReadOnly(true); From 1edc34f086a9e9bb99891800de7d27c3d03d1f6e Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:37:46 +0200 Subject: [PATCH 27/41] Add an option to force selection of save path By default oracle saves cards.xml to the correct path, where cockatrice will be able to pick it up; if oracle is not able to determine the path or the user unchecks the option, oracle will ask for a save path --- oracle/src/oraclewizard.cpp | 9 +++++++-- oracle/src/oraclewizard.h | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 7b48b341..66417764 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -322,11 +322,16 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) setSubTitle(tr("The following sets has been imported. " "Press \"Save\" to save the imported cards to the Cockatrice database.")); + defaultPathCheckBox = new QCheckBox(this); + defaultPathCheckBox->setText(tr("Save to the default path (recommended)")); + defaultPathCheckBox->setChecked(true); + messageLog = new QTextEdit(this); messageLog->setReadOnly(true); QGridLayout *layout = new QGridLayout(this); - layout->addWidget(messageLog, 0, 0); + layout->addWidget(defaultPathCheckBox, 0, 0); + layout->addWidget(messageLog, 1, 0); setLayout(layout); } @@ -366,7 +371,7 @@ bool SaveSetsPage::validatePage() QString savePath = dataDir + "/cards.xml"; do { QString fileName; - if (savePath.isEmpty()) + if (savePath.isEmpty() || !defaultPathCheckBox->isChecked()) fileName = QFileDialog::getSaveFileName(this, tr("Save card database"), dataDir + "/cards.xml", tr("XML card database (*.xml)")); else { fileName = savePath; diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 5514b9da..152ce3ad 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -100,6 +100,7 @@ public: SaveSetsPage(QWidget *parent = 0); private: QTextEdit *messageLog; + QCheckBox * defaultPathCheckBox; protected: void initializePage(); void cleanupPage(); From 17f7fe7ad918b32e6a9472b25f367afc549ff952 Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 11:41:31 +0200 Subject: [PATCH 28/41] Don't lock the UI if the loaded file was not readable --- oracle/src/oraclewizard.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 66417764..28a5c646 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -221,13 +221,13 @@ void LoadSetsPage::readSetsFromByteArray(QByteArray data) void LoadSetsPage::importFinished() { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + if(watcher.future().result()) { - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - wizard()->next(); } else { QMessageBox::critical(this, tr("Error"), tr("The file was retrieved successfully, but it does not contain any sets data.")); From 0ec5842c3f4d134f837730368e4a3c0d7e518fcc Mon Sep 17 00:00:00 2001 From: Fabio Bas Date: Sat, 21 Jun 2014 12:10:04 +0200 Subject: [PATCH 29/41] Add an unexposed config value for the AllSets.json file url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It will be saved only if: * it’s different than the default (the user used a custom url); * the download actually worked --- oracle/src/oraclewizard.cpp | 12 ++++++++++-- oracle/src/oraclewizard.h | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 28a5c646..75381b1d 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -12,6 +12,7 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) { + settings = new QSettings(this); importer = new OracleImporter(QDesktopServices::storageLocation(QDesktopServices::DataLocation), this); addPage(new IntroPage); @@ -76,7 +77,6 @@ LoadSetsPage::LoadSetsPage(QWidget *parent) progressBar = new QProgressBar(this); urlRadioButton->setChecked(true); - urlLineEdit->setText(ALLSETS_URL); fileButton = new QPushButton(tr("Choose file..."), this); connect(fileButton, SIGNAL(clicked()), this, SLOT(actLoadSetsFile())); @@ -97,6 +97,8 @@ LoadSetsPage::LoadSetsPage(QWidget *parent) void LoadSetsPage::initializePage() { + urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); + progressLabel->hide(); progressBar->hide(); } @@ -200,7 +202,13 @@ void LoadSetsPage::actDownloadFinishedSetsFile() return; } - this->readSetsFromByteArray(reply->readAll()); + // save allsets.json url, but only if the user customized it and download was successfull + if(urlLineEdit->text() != QString(ALLSETS_URL)) + wizard()->settings->setValue("allsetsurl", urlLineEdit->text()); + else + wizard()->settings->remove("allsetsurl"); + + readSetsFromByteArray(reply->readAll()); reply->deleteLater(); } diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 152ce3ad..6e6a6ea8 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -15,6 +15,7 @@ class QNetworkAccessManager; class QTextEdit; class QVBoxLayout; class OracleImporter; +class QSettings; class OracleWizard : public QWizard { @@ -26,6 +27,7 @@ public: void disableButtons(); public: OracleImporter *importer; + QSettings * settings; }; From 38b83acae057bcf310ebe920ab8661e97bf2b019 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sat, 21 Jun 2014 12:42:46 -0400 Subject: [PATCH 30/41] Fix #105 - remove emdash from card type when getting main type --- cockatrice/src/carddatabase.cpp | 2 ++ cockatrice/src/main.cpp | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 2045f95e..3b8d9e95 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -295,6 +295,8 @@ QString CardInfo::getMainCardType() const int pos; if ((pos = result.indexOf('-')) != -1) result.remove(pos, result.length()); + if ((pos = result.indexOf("—")) != -1) + result.remove(pos, result.length()); if ((pos = result.indexOf("//")) != -1) result.remove(pos, result.length()); result = result.simplified(); diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index ef57655a..692f8652 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -116,9 +116,9 @@ int main(int argc, char *argv[]) qtTranslator = new QTranslator; translator = new QTranslator; installNewTranslator(); - + qsrand(QDateTime::currentDateTime().toTime_t()); - + bool startMainProgram = true; const QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); if (!db->getLoadSuccess()) From 3a82669b61d2c1341436c480d7fa49bf95c04bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Morschh=C3=A4user?= Date: Sat, 21 Jun 2014 19:24:13 +0200 Subject: [PATCH 31/41] Added Mac OS X build instructions. --- doc/usermanual/Usermanual.pdf | Bin 2879926 -> 2880215 bytes doc/usermanual/Usermanual.tex | 6 ++---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/usermanual/Usermanual.pdf b/doc/usermanual/Usermanual.pdf index 2fdcdb3f771f848746382d67e8a62c5af5bf48dd..3d279df3822720f6834a29441e735641695dc866 100644 GIT binary patch delta 68439 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
FI{~J) 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 Hg$4c_86yM- diff --git a/doc/usermanual/Usermanual.tex b/doc/usermanual/Usermanual.tex index 634c0040..e87ffa2f 100644 --- a/doc/usermanual/Usermanual.tex +++ b/doc/usermanual/Usermanual.tex @@ -224,7 +224,7 @@ Start the oracle.exe (the installer does this automatically) and let it generate \end{enumerate} Congratulations, you may now use Cockatrice! -\subsubsection{Linux and BSD} +\subsubsection{Linux, BSD, OS X} The following procedures have been tested with Debian Wheezy, Fedora 18, XUbuntu 13.10, FreeBSD 9.1 and 10.0. If you use Gentoo with KDE you have the needed prerequisites and may continue with downloading the source. If you use Bodhi or Arch Linux (AUR) or another distribution that includes Cockatrice, you might install Cockatrice from the default packages -- though the package might be old, @@ -239,6 +239,7 @@ Before you install new software, you should update your system. The following in yum install qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake} \item[FreeBSD 9] \shellcmd{pkg\_add -r qt4 qt4-linguist qt4-moc qt4-qmake qt4-rcc qt4-uic git cmake protobuf} \item[FreeBSD 10] \shellcmd{pkg install qt4 qt4-linguist qt4-moc qt4-qmake qt4-rcc qt4-uic git cmake protobuf} + \item[OS X] \shellcmd{brew install qt cmake protobuf} \end{description} \item Download the sources from github via \\ \shellcmd{cd\\ git clone https://github.com/Daenyth/Cockatrice.git} \item To compile the sources, change into the newly created directory, create a build directory and invoke cmake:\\ @@ -253,9 +254,6 @@ make}\\ The default paths for decks, pics, cards and tokens are located in \\ \shellcmd{/home//.local/share/data/Cockatrice/Cockatrice}. \end{enumerate} -\subsubsection{MacOS X} -TODO, please contribute this section! See Linux section, then use the \shellcmd{prepareMacRelease.sh} script from Cockatrice. - \subsection{Building the Server} You don't need your own server if you plan to play only. But as Cockatrice is open source you are free to run your own. The compilation works like already written above, but instead of invoking \shellcmd{cmake ..}, you have to do it like this: From 27847e15519fed0a93747c8e3d3064cbdcee1efc Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sat, 21 Jun 2014 15:51:47 -0400 Subject: [PATCH 32/41] Don't be a jerk when card database isn't usable. Better error message and allow the user to still connect Ref: #102 --- cockatrice/src/carddatabase.cpp | 51 ++++++++++++++++++------------- cockatrice/src/carddatabase.h | 13 ++++---- cockatrice/src/dlg_settings.cpp | 53 +++++++++++++++++++++++++++++++-- cockatrice/src/main.cpp | 33 ++++++++++++-------- 4 files changed, 109 insertions(+), 41 deletions(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 3b8d9e95..c9c3756f 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -14,7 +14,7 @@ #include #include -const int CardDatabase::versionNeeded = 2; +const int CardDatabase::versionNeeded = 3; CardSet::CardSet(const QString &_shortName, const QString &_longName) : shortName(_shortName), longName(_longName) @@ -457,7 +457,7 @@ QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfo *info) } CardDatabase::CardDatabase(QObject *parent) - : QObject(parent), loadSuccess(false), noCard(0) + : QObject(parent), loadStatus(NotLoaded), noCard(0) { connect(settingsCache, SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged())); connect(settingsCache, SIGNAL(cardDatabasePathChanged()), this, SLOT(loadCardDatabase())); @@ -636,13 +636,13 @@ void CardDatabase::loadCardsFromXml(QXmlStreamReader &xml) } } -bool CardDatabase::loadFromFile(const QString &fileName, bool tokens) +LoadStatus CardDatabase::loadFromFile(const QString &fileName, bool tokens) { QFile file(fileName); file.open(QIODevice::ReadOnly); if (!file.isOpen()) - return false; - + return FileError; + if (tokens) { QMutableHashIterator i(cardHash); while (i.hasNext()) { @@ -659,7 +659,7 @@ bool CardDatabase::loadFromFile(const QString &fileName, bool tokens) delete setIt.value(); } setHash.clear(); - + QMutableHashIterator i(cardHash); while (i.hasNext()) { i.next(); @@ -675,9 +675,12 @@ bool CardDatabase::loadFromFile(const QString &fileName, bool tokens) while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::StartElement) { if (xml.name() != "cockatrice_carddatabase") - return false; - if (xml.attributes().value("version").toString().toInt() < versionNeeded) - return false; + return Invalid; + int version = xml.attributes().value("version").toString().toInt(); + if (version < versionNeeded) { + qDebug() << "loadFromFile(): Version too old: " << version; + return VersionTooOld; + } while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::EndElement) break; @@ -689,7 +692,10 @@ bool CardDatabase::loadFromFile(const QString &fileName, bool tokens) } } qDebug() << cardHash.size() << "cards in" << setHash.size() << "sets loaded"; - return !cardHash.isEmpty(); + + if (cardHash.isEmpty()) return NoCards; + + return Ok; } bool CardDatabase::saveToFile(const QString &fileName, bool tokens) @@ -747,13 +753,13 @@ void CardDatabase::picDownloadHqChanged() } } -bool CardDatabase::loadCardDatabase(const QString &path, bool tokens) +LoadStatus CardDatabase::loadCardDatabase(const QString &path, bool tokens) { - bool tempLoadSuccess = false; + LoadStatus tempLoadStatus = NotLoaded; if (!path.isEmpty()) - tempLoadSuccess = loadFromFile(path, tokens); - - if (tempLoadSuccess) { + tempLoadStatus = loadFromFile(path, tokens); + + if (tempLoadStatus == Ok) { SetList allSets; QHashIterator setsIterator(setHash); while (setsIterator.hasNext()) @@ -761,14 +767,17 @@ bool CardDatabase::loadCardDatabase(const QString &path, bool tokens) allSets.sortByKey(); for (int i = 0; i < allSets.size(); ++i) allSets[i]->setSortKey(i); - + emit cardListChanged(); } - - if (!tokens) - loadSuccess = tempLoadSuccess; - - return tempLoadSuccess; + + if (!tokens) { + loadStatus = tempLoadStatus; + qDebug() << "loadCardDatabase(): Status = " << loadStatus; + } + + + return tempLoadStatus; } void CardDatabase::loadCardDatabase() diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index 39d2faec..bd3fd419 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -158,16 +158,18 @@ signals: void cardInfoChanged(CardInfo *card); }; +enum LoadStatus { Ok, VersionTooOld, Invalid, NotLoaded, FileError, NoCards }; + class CardDatabase : public QObject { Q_OBJECT protected: QHash cardHash; QHash setHash; - bool loadSuccess; CardInfo *noCard; QThread *pictureLoaderThread; PictureLoader *pictureLoader; + LoadStatus loadStatus; private: static const int versionNeeded; void loadCardsFromXml(QXmlStreamReader &xml); @@ -182,22 +184,23 @@ public: CardSet *getSet(const QString &setName); QList getCardList() const { return cardHash.values(); } SetList getSetList() const; - bool loadFromFile(const QString &fileName, bool tokens = false); + LoadStatus loadFromFile(const QString &fileName, bool tokens = false); bool saveToFile(const QString &fileName, bool tokens = false); QStringList getAllColors() const; QStringList getAllMainCardTypes() const; - bool getLoadSuccess() const { return loadSuccess; } + LoadStatus getLoadStatus() const { return loadStatus; } + bool getLoadSuccess() const { return loadStatus == Ok; } void cacheCardPixmaps(const QStringList &cardNames); void loadImage(CardInfo *card); public slots: void clearPixmapCache(); - bool loadCardDatabase(const QString &path, bool tokens = false); + LoadStatus loadCardDatabase(const QString &path, bool tokens = false); private slots: void imageLoaded(CardInfo *card, QImage image); void picDownloadChanged(); void picDownloadHqChanged(); void picsPathChanged(); - + void loadCardDatabase(); void loadTokenDatabase(); signals: diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index f7293964..14115824 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "carddatabase.h" #include "dlg_settings.h" #include "main.h" @@ -704,17 +705,65 @@ void DlgSettings::changeEvent(QEvent *event) void DlgSettings::closeEvent(QCloseEvent *event) { - if (!db->getLoadSuccess()) - if (QMessageBox::critical(this, tr("Error"), tr("Your card database is invalid. Would you like to go back and set the correct path?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + bool showLoadError = true; + QString loadErrorMessage = tr("Unknown Error loading card database"); + LoadStatus loadStatus = db->getLoadStatus(); + qDebug() << "Card Database load status: " << loadStatus; + switch(loadStatus) { + case Ok: + showLoadError = false; + break; + case Invalid: + loadErrorMessage = + tr("Your card database is invalid.\n\n" + "Cockatrice may not function correctly with an invalid database\n\n" + "You may need to rerun oracle to update your card database.\n\n" + "Would you like to change your database location setting?"); + break; + case VersionTooOld: + loadErrorMessage = + tr("Your card database version is too old.\n\n" + "This can cause problems loading card information or images\n\n" + "Usually this can be fixed by rerunning oracle to to update your card database.\n\n" + "Would you like to change your database location setting?"); + break; + case NotLoaded: + loadErrorMessage = + tr("Your card database did not finish loading\n\n" + "Please file a ticket at http://github.com/Daenyth/Cockatrice/issues with your cards.xml attached\n\n" + "Would you like to change your database location setting?"); + break; + case FileError: + loadErrorMessage = + tr("File Error loading your card database.\n\n" + "Would you like to change your database location setting?"); + break; + case NoCards: + loadErrorMessage = + tr("Your card database was loaded but contains no cards.\n\n" + "Would you like to change your database location setting?"); + break; + default: + loadErrorMessage = + tr("Unknown card database load status\n\n" + "Please file a ticket at http://github.com/Daenyth/Cockatrice/issues\n\n" + "Would you like to change your database location setting?"); + + break; + } + if (showLoadError) + if (QMessageBox::critical(this, tr("Error"), loadErrorMessage, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { event->ignore(); return; } if (!QDir(settingsCache->getDeckPath()).exists() || settingsCache->getDeckPath().isEmpty()) + // TODO: Prompt to create it if (QMessageBox::critical(this, tr("Error"), tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { event->ignore(); return; } if (!QDir(settingsCache->getPicsPath()).exists() || settingsCache->getPicsPath().isEmpty()) + // TODO: Prompt to create it if (QMessageBox::critical(this, tr("Error"), tr("The path to your card pictures directory is invalid. Would you like to go back and set the correct path?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { event->ignore(); return; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 692f8652..d0664bf9 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -74,10 +74,18 @@ void installNewTranslator() qApp->installTranslator(translator); } +bool settingsValid() +{ + return QDir(settingsCache->getDeckPath()).exists() && + !settingsCache->getDeckPath().isEmpty() && + QDir(settingsCache->getPicsPath()).exists() && + !settingsCache->getPicsPath().isEmpty(); +} + int main(int argc, char *argv[]) { QApplication app(argc, argv); - + if (app.arguments().contains("--debug-output")) qInstallMsgHandler(myMessageOutput); #ifdef Q_OS_MAC @@ -97,7 +105,7 @@ int main(int argc, char *argv[]) QCoreApplication::setOrganizationName("Cockatrice"); QCoreApplication::setOrganizationDomain("cockatrice.de"); QCoreApplication::setApplicationName("Cockatrice"); - + if (translationPath.isEmpty()) { #ifdef Q_OS_MAC QDir translationsDir = baseDir; @@ -108,7 +116,7 @@ int main(int argc, char *argv[]) translationPath = app.applicationDirPath() + "/translations"; #endif } - + rng = new RNG_SFMT; settingsCache = new SettingsCache; db = new CardDatabase; @@ -119,7 +127,6 @@ int main(int argc, char *argv[]) qsrand(QDateTime::currentDateTime().toTime_t()); - bool startMainProgram = true; const QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); if (!db->getLoadSuccess()) if (db->loadCardDatabase(dataDir + "/cards.xml")) @@ -138,30 +145,30 @@ int main(int argc, char *argv[]) QDir().mkpath(dataDir + "/pics"); settingsCache->setPicsPath(dataDir + "/pics"); } - if (!db->getLoadSuccess() || !QDir(settingsCache->getDeckPath()).exists() || settingsCache->getDeckPath().isEmpty() || settingsCache->getPicsPath().isEmpty() || !QDir(settingsCache->getPicsPath()).exists()) { + if (!settingsValid() || db->getLoadStatus() != Ok) { + qDebug("main(): invalid settings or load status"); DlgSettings dlgSettings; dlgSettings.show(); app.exec(); - startMainProgram = (db->getLoadSuccess() && QDir(settingsCache->getDeckPath()).exists() && !settingsCache->getDeckPath().isEmpty() && QDir(settingsCache->getPicsPath()).exists() && !settingsCache->getPicsPath().isEmpty()); } - - if (startMainProgram) { + + if (settingsValid()) { qDebug("main(): starting main program"); soundEngine = new SoundEngine; qDebug("main(): SoundEngine constructor finished"); MainWindow ui; qDebug("main(): MainWindow constructor finished"); - + QIcon icon(":/resources/appicon.svg"); ui.setWindowIcon(icon); - + ui.show(); qDebug("main(): ui.show() finished"); - + app.exec(); } - + qDebug("Event loop finished, terminating..."); delete db; delete settingsCache; @@ -169,6 +176,6 @@ int main(int argc, char *argv[]) PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - + return 0; } From 68c0932a2fd03af5a96b65c47de1f8f45847887a Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sun, 22 Jun 2014 08:36:35 -0400 Subject: [PATCH 33/41] Don't have decklist sort behavior rely on column order --- cockatrice/src/decklistmodel.cpp | 15 +++++++++++++-- common/decklist.cpp | 8 ++++---- common/decklist.h | 16 +++++++++------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index 0929c5e1..00d92fae 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -315,7 +315,7 @@ void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) } } changePersistentIndexList(from, to); - + // Recursion for (int i = node->size() - 1; i >= 0; --i) { InnerDecklistNode *subNode = dynamic_cast(node->at(i)); @@ -330,7 +330,18 @@ void DeckListModel::sort(int column, Qt::SortOrder order) lastKnownOrder = order; emit layoutAboutToBeChanged(); - root->setSortMethod(column); + DeckSortMethod sortMethod; + switch(column) { + case 0: + sortMethod = ByNumber; + break; + case 1: + sortMethod = ByName; + break; + case 2: + sortMethod = ByPrice; + } + root->setSortMethod(sortMethod); sortHelper(root, order); emit layoutChanged(); } diff --git a/common/decklist.cpp b/common/decklist.cpp index 11119945..8ba9378e 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -104,7 +104,7 @@ QString InnerDecklistNode::visibleNameFromName(const QString &_name) return _name; } -void InnerDecklistNode::setSortMethod(int method) +void InnerDecklistNode::setSortMethod(DeckSortMethod method) { sortMethod = method; for (int i = 0; i < size(); i++) @@ -218,11 +218,11 @@ bool InnerDecklistNode::comparePrice(AbstractDecklistNode *other) const bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const { switch (sortMethod) { - case 0: + case ByNumber: return compareNumber(other); - case 1: + case ByName: return compareName(other); - case 2: + case ByPrice: return compareTotalPrice(other); } } diff --git a/common/decklist.h b/common/decklist.h index 1a74a7fb..94df407a 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -29,26 +29,28 @@ public: SideboardPlan(const QString &_name = QString(), const QList &_moveList = QList()); bool readElement(QXmlStreamReader *xml); void write(QXmlStreamWriter *xml); - + QString getName() const { return name; } const QList &getMoveList() const { return moveList; } void setMoveList(const QList &_moveList); }; +enum DeckSortMethod { ByNumber, ByName, ByPrice }; + class AbstractDecklistNode { protected: InnerDecklistNode *parent; - int sortMethod; + DeckSortMethod sortMethod; public: AbstractDecklistNode(InnerDecklistNode *_parent = 0); virtual ~AbstractDecklistNode() { } - virtual void setSortMethod(int method) { sortMethod = method; } + virtual void setSortMethod(DeckSortMethod method) { sortMethod = method; } virtual QString getName() const = 0; InnerDecklistNode *getParent() const { return parent; } int depth() const; virtual int height() const = 0; virtual bool compare(AbstractDecklistNode *other) const = 0; - + virtual bool readElement(QXmlStreamReader *xml) = 0; virtual void writeElement(QXmlStreamWriter *xml) = 0; }; @@ -61,7 +63,7 @@ public: InnerDecklistNode(const QString &_name = QString(), InnerDecklistNode *_parent = 0) : AbstractDecklistNode(_parent), name(_name) { } InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent = 0); virtual ~InnerDecklistNode(); - void setSortMethod(int method); + void setSortMethod(DeckSortMethod method); QString getName() const { return name; } void setName(const QString &_name) { name = _name; } static QString visibleNameFromName(const QString &_name); @@ -76,7 +78,7 @@ public: bool compareName(AbstractDecklistNode *other) const; bool comparePrice(AbstractDecklistNode *other) const; QVector > sort(Qt::SortOrder order = Qt::AscendingOrder); - + bool readElement(QXmlStreamReader *xml); void writeElement(QXmlStreamWriter *xml); }; @@ -96,7 +98,7 @@ public: bool compareNumber(AbstractDecklistNode *other) const; bool compareName(AbstractDecklistNode *other) const; bool compareTotalPrice(AbstractDecklistNode *other) const; - + bool readElement(QXmlStreamReader *xml); void writeElement(QXmlStreamWriter *xml); }; From 951f08dd110494381061c65fdd9add586c429ade Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sun, 22 Jun 2014 20:50:49 -0400 Subject: [PATCH 34/41] Update CentOS doc in user manual Ref #109 --- doc/usermanual/Usermanual.tex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/usermanual/Usermanual.tex b/doc/usermanual/Usermanual.tex index e87ffa2f..23fa5580 100644 --- a/doc/usermanual/Usermanual.tex +++ b/doc/usermanual/Usermanual.tex @@ -704,6 +704,8 @@ USE AT YOUR OWN RISK. \begin{enumerate} \item Open a command shell and install the prerequisites \begin{enumerate} + \item \shellcmd{cd /etc/yum.repos.d/} + \item \shellcmd{sudo wget http://kdeforge.unl.edu/apt/kde-redhat/epel/kde.repo} \item \shellcmd{yum -y groupinstall "development tools"} \item \shellcmd{rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86\_64/epel-release-6-8.noarch.rpm} \item \shellcmd{yum -y install qt-mysql qt-devel qt-mobility-devel protobuf-devel protobuf-compiler cmake28 libgcrypt-devel} From e5d15e8dbce8af089cfeb1d2951e1b4c7a917dde Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sun, 22 Jun 2014 21:19:53 -0400 Subject: [PATCH 35/41] Notify deck editor when price feature setting is changed Ref #112 --- cockatrice/src/priceupdater.cpp | 2 +- cockatrice/src/settingscache.cpp | 1 + cockatrice/src/settingscache.h | 3 ++- cockatrice/src/tab_deck_editor.cpp | 10 ++++++++-- cockatrice/src/tab_deck_editor.h | 3 ++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/priceupdater.cpp b/cockatrice/src/priceupdater.cpp index 07ded728..a0d32a32 100644 --- a/cockatrice/src/priceupdater.cpp +++ b/cockatrice/src/priceupdater.cpp @@ -27,7 +27,7 @@ void PriceUpdater::updatePrices() QString q = "http://blacklotusproject.com/json/?cards="; QStringList cards = deck->getCardList(); for (int i = 0; i < cards.size(); ++i) { - q += cards[i] + "|"; + q += cards[i].toLower() + "|"; } QUrl url(q.replace(' ', '+')); diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index ac5c130f..10dad8c9 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -242,6 +242,7 @@ void SettingsCache::setPriceTagFeature(int _priceTagFeature) { priceTagFeature = _priceTagFeature; settings->setValue("deckeditor/pricetags", priceTagFeature); + emit priceTagFeatureChanged(priceTagFeature); } void SettingsCache::setIgnoreUnregisteredUsers(bool _ignoreUnregisteredUsers) diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index 5156772d..a9b9ce49 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -28,10 +28,11 @@ signals: void minPlayersForMultiColumnLayoutChanged(); void soundEnabledChanged(); void soundPathChanged(); + void priceTagFeatureChanged(int enabled); void ignoreUnregisteredUsersChanged(); private: QSettings *settings; - + QByteArray mainWindowGeometry; QString lang; QString deckPath, replaysPath, picsPath, cardDatabasePath, tokenDatabasePath; diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index 11ec0d3d..0c1d44e1 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -172,6 +172,7 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent) connect(aUpdatePrices, SIGNAL(triggered()), this, SLOT(actUpdatePrices())); if (!settingsCache->getPriceTagFeature()) aUpdatePrices->setVisible(false); + connect(settingsCache, SIGNAL(priceTagFeatureChanged(int)), this, SLOT(setPriceTagFeatureEnabled(int))); QToolBar *deckToolBar = new QToolBar; deckToolBar->setOrientation(Qt::Vertical); @@ -182,7 +183,7 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent) deckToolbarLayout->addStretch(); deckToolbarLayout->addWidget(deckToolBar); deckToolbarLayout->addStretch(); - + QVBoxLayout *rightFrame = new QVBoxLayout; rightFrame->addLayout(grid); rightFrame->addWidget(deckView, 10); @@ -193,7 +194,7 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent) mainLayout->addLayout(middleFrame); mainLayout->addLayout(rightFrame); setLayout(mainLayout); - + aNewDeck = new QAction(QString(), this); aNewDeck->setShortcuts(QKeySequence::New); connect(aNewDeck, SIGNAL(triggered()), this, SLOT(actNewDeck())); @@ -635,6 +636,11 @@ void TabDeckEditor::actDecrement() offsetCountAtIndex(currentIndex, -1); } +void TabDeckEditor::setPriceTagFeatureEnabled(int enabled) +{ + aUpdatePrices->setVisible(enabled); +} + void TabDeckEditor::actUpdatePrices() { aUpdatePrices->setDisabled(true); diff --git a/cockatrice/src/tab_deck_editor.h b/cockatrice/src/tab_deck_editor.h index 3acf1d15..b3ba8dcf 100644 --- a/cockatrice/src/tab_deck_editor.h +++ b/cockatrice/src/tab_deck_editor.h @@ -50,7 +50,7 @@ private slots: void actEditSets(); void actEditTokens(); - + void actClearSearch(); void actAddCard(); @@ -67,6 +67,7 @@ private slots: void saveDeckRemoteFinished(const Response &r); void filterViewCustomContextMenu(const QPoint &point); void filterRemove(QAction *action); + void setPriceTagFeatureEnabled(int enabled); private: CardInfo *currentCardInfo() const; void addCardHelper(QString zoneName); From 4574df0a2e2bafc0610af2ba778a046a1bb6ab8b Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sun, 22 Jun 2014 21:25:57 -0400 Subject: [PATCH 36/41] Remove testclient cmake option. This has been obsolete a long time --- CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8756e90..e820c5ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,9 +99,3 @@ option(WITH_ORACLE "build oracle" ON) if(WITH_ORACLE) add_subdirectory(oracle) endif() - -# Compile testclient (default off) -option(WITH_TESTCLIENT "build testclient" OFF) -if (WITH_TESTCLIENT) - add_subdirectory(testclient) -endif() \ No newline at end of file From ea08fe168cbc56f89159d0d0b761c8a7fe0d0596 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Sun, 22 Jun 2014 22:32:53 -0400 Subject: [PATCH 37/41] Fix Q_INTERFACES warnings Fix #88 --- cockatrice/src/abstractcarddragitem.h | 1 + cockatrice/src/abstractcounter.h | 1 + cockatrice/src/arrowitem.h | 1 + cockatrice/src/phasestoolbar.h | 2 ++ cockatrice/src/player.h | 4 +++- cockatrice/src/zoneviewzone.h | 1 + 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/abstractcarddragitem.h b/cockatrice/src/abstractcarddragitem.h index e574f6c2..d7c3a40e 100644 --- a/cockatrice/src/abstractcarddragitem.h +++ b/cockatrice/src/abstractcarddragitem.h @@ -9,6 +9,7 @@ class CardInfo; class AbstractCardDragItem : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) protected: AbstractCardItem *item; QPointF hotSpot; diff --git a/cockatrice/src/abstractcounter.h b/cockatrice/src/abstractcounter.h index 713c11c5..174cbf84 100644 --- a/cockatrice/src/abstractcounter.h +++ b/cockatrice/src/abstractcounter.h @@ -9,6 +9,7 @@ class QAction; class AbstractCounter : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) protected: Player *player; int id; diff --git a/cockatrice/src/arrowitem.h b/cockatrice/src/arrowitem.h index 67517404..7e897d31 100644 --- a/cockatrice/src/arrowitem.h +++ b/cockatrice/src/arrowitem.h @@ -11,6 +11,7 @@ class ArrowTarget; class ArrowItem : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) private: QPainterPath path; QMenu *menu; diff --git a/cockatrice/src/phasestoolbar.h b/cockatrice/src/phasestoolbar.h index 852f4f58..a91b020c 100644 --- a/cockatrice/src/phasestoolbar.h +++ b/cockatrice/src/phasestoolbar.h @@ -11,6 +11,7 @@ class GameCommand; class PhaseButton : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) private: QString name; bool active, highlightable; @@ -39,6 +40,7 @@ protected: class PhasesToolbar : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) private: QList buttonList; PhaseButton *nextTurnButton; diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index 9eed7c07..d8396e4d 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -58,7 +58,8 @@ class PendingCommand; class PlayerArea : public QObject, public QGraphicsItem { Q_OBJECT -private: + Q_INTERFACES(QGraphicsItem) +private: QBrush bgPixmapBrush; QRectF bRect; private slots: @@ -76,6 +77,7 @@ public: class Player : public QObject, public QGraphicsItem { Q_OBJECT + Q_INTERFACES(QGraphicsItem) signals: void openDeckEditor(const DeckLoader *deck); void newCardAdded(AbstractCardItem *card); diff --git a/cockatrice/src/zoneviewzone.h b/cockatrice/src/zoneviewzone.h index 727148a8..43134c78 100644 --- a/cockatrice/src/zoneviewzone.h +++ b/cockatrice/src/zoneviewzone.h @@ -11,6 +11,7 @@ class QGraphicsSceneWheelEvent; class ZoneViewZone : public SelectZone, public QGraphicsLayoutItem { Q_OBJECT + Q_INTERFACES(QGraphicsLayoutItem) private: QRectF bRect, optimumRect; int minRows, numberCards; From e925403bc5c425463948a6991e0fdc3eee06a328 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Mon, 23 Jun 2014 22:18:40 -0400 Subject: [PATCH 38/41] Log cardname when downloading --- cockatrice/src/carddatabase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index c9c3756f..6760e068 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -169,7 +169,7 @@ void PictureLoader::startNextPicDownload() QUrl url(picUrl); QNetworkRequest req(url); - qDebug() << "starting picture download:" << req.url(); + qDebug() << "starting picture download:" << cardBeingDownloaded.getCard()->getName() << "Url:" << req.url(); networkManager->get(req); } From 632e7f487c3d5be276bbc0d8bd5da24d55084287 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Mon, 23 Jun 2014 23:15:53 -0400 Subject: [PATCH 39/41] Add debugging to file for windows --- nsis/cockatrice.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nsis/cockatrice.nsi b/nsis/cockatrice.nsi index 33cc496b..90ebfe50 100644 --- a/nsis/cockatrice.nsi +++ b/nsis/cockatrice.nsi @@ -91,7 +91,7 @@ SectionEnd Section "Start menu item" SecStartMenu createDirectory "$SMPROGRAMS\Cockatrice" - createShortCut "$SMPROGRAMS\Cockatrice\Cockatrice.lnk" "$INSTDIR\cockatrice.exe" + createShortCut "$SMPROGRAMS\Cockatrice\Cockatrice.lnk" "$INSTDIR\cockatrice.exe" '--debug-output' createShortCut "$SMPROGRAMS\Cockatrice\Oracle.lnk" "$INSTDIR\oracle.exe" createShortCut "$SMPROGRAMS\Cockatrice\Usermanual.lnk" "$INSTDIR\Usermanual.pdf" SectionEnd From 0420f4f7af85ffc1fc35b25b852fa73ce11c1c87 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Mon, 23 Jun 2014 23:44:13 -0400 Subject: [PATCH 40/41] Log download failures --- cockatrice/src/carddatabase.cpp | 16 ++++++++++------ cockatrice/src/carddatabase.h | 2 -- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 6760e068..2e5c0ec7 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -132,7 +132,7 @@ void PictureLoader::processLoadQueue() } continue; } - + emit imageLoaded(ptl.getCard(), image); } } @@ -176,6 +176,10 @@ void PictureLoader::startNextPicDownload() void PictureLoader::picDownloadFinished(QNetworkReply *reply) { QString picsPath = _picsPath; + if (reply->error()) { + qDebug() << "Download failed:" << reply->errorString(); + } + const QByteArray &picData = reply->readAll(); QImage testImage; if (testImage.loadFromData(picData)) { @@ -189,17 +193,17 @@ void PictureLoader::picDownloadFinished(QNetworkReply *reply) QDir dir(QString(picsPath + "/downloadedPics")); dir.mkdir(cardBeingDownloaded.getSetName()); } - + QString suffix; if (!cardBeingDownloaded.getStripped()) suffix = ".full"; - + QFile newPic(picsPath + "/downloadedPics/" + cardBeingDownloaded.getSetName() + "/" + cardBeingDownloaded.getCard()->getCorrectedName() + suffix + ".jpg"); if (!newPic.open(QIODevice::WriteOnly)) return; newPic.write(picData); newPic.close(); - + emit imageLoaded(cardBeingDownloaded.getCard(), testImage); } else if (cardBeingDownloaded.getHq()) { qDebug() << "HQ: received invalid picture. URL:" << reply->request().url(); @@ -216,7 +220,7 @@ void PictureLoader::picDownloadFinished(QNetworkReply *reply) } else emit imageLoaded(cardBeingDownloaded.getCard(), QImage()); } - + reply->deleteLater(); startNextPicDownload(); } @@ -224,7 +228,7 @@ void PictureLoader::picDownloadFinished(QNetworkReply *reply) void PictureLoader::loadImage(CardInfo *card, bool stripped) { QMutexLocker locker(&mutex); - + loadQueue.append(PictureToLoad(card, stripped)); emit startLoadQueue(); } diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index bd3fd419..5e83080c 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -53,10 +53,8 @@ public: bool getStripped() const { return stripped; } QString getSetName() const { return sortedSets[setIndex]->getShortName(); } bool nextSet(); - bool getHq() const { return hq; } void setHq(bool _hq) { hq = _hq; } - }; class PictureLoader : public QObject { From e5d9692d780d964eb99bebaca8178521ee86ee32 Mon Sep 17 00:00:00 2001 From: Daenyth Date: Tue, 24 Jun 2014 00:14:59 -0400 Subject: [PATCH 41/41] Fall back to plain text load when xml load fails --- cockatrice/src/deck_loader.cpp | 19 +++++++++++++++---- common/decklist.cpp | 23 ++++++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/deck_loader.cpp b/cockatrice/src/deck_loader.cpp index 6a907ed7..4443b43b 100644 --- a/cockatrice/src/deck_loader.cpp +++ b/cockatrice/src/deck_loader.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "deck_loader.h" #include "decklist.h" @@ -44,18 +45,28 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt) 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); break; + case CockatriceFormat: + result = loadFromFile_Native(&file); + qDebug() << "Loaded from" << fileName << "-" << result; + if (!result) { + qDebug() << "Retying as plain format"; + file.seek(0); + result = loadFromFile_Plain(&file); + fmt = PlainTextFormat; + } + break; } if (result) { lastFileName = fileName; lastFileFormat = fmt; - + emit deckLoaded(); } + qDebug() << "Deck was loaded -" << result; return result; } @@ -66,7 +77,7 @@ bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId) lastFileName = QString(); lastFileFormat = CockatriceFormat; lastRemoteDeckId = remoteDeckId; - + emit deckLoaded(); } return result; diff --git a/common/decklist.cpp b/common/decklist.cpp index 8ba9378e..e81a0629 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "decklist.h" SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) @@ -441,6 +442,11 @@ void DeckList::write(QXmlStreamWriter *xml) bool DeckList::loadFromXml(QXmlStreamReader *xml) { + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } + cleanList(); while (!xml->atEnd()) { xml->readNext(); @@ -455,6 +461,10 @@ bool DeckList::loadFromXml(QXmlStreamReader *xml) } } updateDeckHash(); + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } return true; } @@ -477,8 +487,7 @@ QString DeckList::writeToString_Native() bool DeckList::loadFromFile_Native(QIODevice *device) { QXmlStreamReader xml(device); - loadFromXml(&xml); - return true; + return loadFromXml(&xml); } bool DeckList::saveToFile_Native(QIODevice *device) @@ -496,7 +505,7 @@ bool DeckList::saveToFile_Native(QIODevice *device) bool DeckList::loadFromStream_Plain(QTextStream &in) { cleanList(); - + InnerDecklistNode *main = 0, *side = 0; bool inSideboard = false; @@ -530,7 +539,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) line.remove(rx); rx.setPattern("\\(.*\\)"); line.remove(rx); - //Filter out post card name editions + //Filter out post card name editions rx.setPattern("\\|.*$"); line.remove(rx); line = line.simplified(); @@ -543,10 +552,10 @@ bool DeckList::loadFromStream_Plain(QTextStream &in) continue; QString cardName = line.mid(i + 1); - // Common differences between cockatrice's card names - // and what's commonly used in decklists + // Common differences between cockatrice's card names + // and what's commonly used in decklists rx.setPattern("’"); - cardName.replace(rx, "'"); + cardName.replace(rx, "'"); rx.setPattern("Æ"); cardName.replace(rx, "AE"); rx.setPattern("^Aether");