Major Directory Refactoring (#5118)

* refactored cardzone.cpp, added doc and changed if to switch case

* started moving every files into different folders

* remove undercase to match with other files naming convention

* refactored dialog files

* ran format.sh

* refactored client/tabs folder

* refactored client/tabs folder

* refactored client/tabs folder

* refactored client folder

* refactored carddbparser

* refactored dialogs

* Create sonar-project.properties

temporary file for lint

* Create build.yml

temporary file for lint

* removed all files from root directory

* removed all files from root directory

* added current branch to workflow

* fixed most broken import

* fixed issues while renaming files

* fixed oracle importer

* fixed dbconverter

* updated translations

* made sub-folders for client

* removed linter

* removed linter folder

* fixed oracle import

* revert card_zone documentation

* renamed db parser files name and deck_view imports

* fixed dlg file issue

* ran format file and fixed test file

* fixed carddb test files

* moved player folder in game

* updated translations and format files

* fixed peglib import

* format cmake files

* removing vcpkg to try to add it back later

* tried fixing vcpkg file

* renamed filter to filters and moved database parser to cards folder

* reverted translation files

* reverted oracle translated

* Update cockatrice/src/dialogs/dlg_register.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* Update cockatrice/src/client/ui/window_main.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* removed empty line at file start

* removed useless include from tab_supervisor.cpp

* refactored cardzone.cpp, added doc and changed if to switch case

* started moving every files into different folders

* remove undercase to match with other files naming convention

* refactored dialog files

* ran format.sh

* refactored client/tabs folder

* refactored client folder

* refactored carddbparser

* refactored dialogs

* removed all files from root directory

* Create sonar-project.properties

temporary file for lint

* Create build.yml

temporary file for lint

* added current branch to workflow

* fixed most broken import

* fixed issues while renaming files

* fixed oracle importer

* fixed dbconverter

* updated translations

* made sub-folders for client

* removed linter

* removed linter folder

* fixed oracle import

* revert card_zone documentation

* renamed db parser files name and deck_view imports

* fixed dlg file issue

* ran format file and fixed test file

* fixed carddb test files

* moved player folder in game

* updated translations and format files

* fixed peglib import

* reverted translation files

* format cmake files

* removing vcpkg to try to add it back later

* tried fixing vcpkg file

* pre-updating of cockatrice changes

* removed empty line at file start

* reverted oracle translated

* Update cockatrice/src/dialogs/dlg_register.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* Update cockatrice/src/client/ui/window_main.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* removed useless include from tab_supervisor.cpp

---------

Co-authored-by: tooomm <tooomm@users.noreply.github.com>
This commit is contained in:
LunaticCat 2024-10-20 16:11:35 +02:00 committed by GitHub
parent d1e0f9dfc5
commit fa999880ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 735 additions and 729 deletions

View file

@ -0,0 +1,133 @@
#include "line_edit_completer.h"
#include <QAbstractItemView>
#include <QCompleter>
#include <QFocusEvent>
#include <QKeyEvent>
#include <QScrollBar>
#include <QStringListModel>
#include <QTextCursor>
#include <QWidget>
LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent), c(nullptr)
{
}
void LineEditCompleter::focusOutEvent(QFocusEvent *e)
{
LineEditUnfocusable::focusOutEvent(e);
if (c->popup()->isVisible()) {
// Remove Popup
c->popup()->hide();
// Truncate the line to last space or whole string
QString textValue = text();
int lastIndex = textValue.length();
int lastWordStartIndex = textValue.lastIndexOf(" ") + 1;
int leftShift = qMin(lastIndex, lastWordStartIndex);
setText(textValue.left(leftShift));
// Insert highlighted line from popup
insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " ");
// Set focus back to the textbox since tab was pressed
setFocus();
}
}
void LineEditCompleter::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
case Qt::Key_Escape:
if (c->popup()->isVisible()) {
event->ignore();
// Remove Popup
c->popup()->hide();
// Truncate the line to last space or whole string
QString textValue = text();
int lastIndexof = qMax(0, textValue.lastIndexOf(" "));
QString finalString = textValue.left(lastIndexof);
// Add a space if there's a word
if (finalString != "")
finalString += " ";
setText(finalString);
return;
}
break;
case Qt::Key_Space:
if (c->popup()->isVisible()) {
event->ignore();
// Remove Popup
c->popup()->hide();
// Truncate the line to last space or whole string
QString textValue = text();
int lastIndex = textValue.length();
int lastWordStartIndex = textValue.lastIndexOf(" ") + 1;
int leftShift = qMin(lastIndex, lastWordStartIndex);
setText(textValue.left(leftShift));
// Insert highlighted line from popup
insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " ");
return;
}
break;
default:
break;
}
LineEditUnfocusable::keyPressEvent(event);
// return if the completer is null or if the most recently typed char was '@'.
// Only want the popup AFTER typing the first char of the mention.
if (!c || text().right(1).contains("@")) {
c->popup()->hide();
return;
}
// Set new completion prefix
c->setCompletionPrefix(cursorWord(text()));
if (c->completionPrefix().length() < 1) {
c->popup()->hide();
return;
}
// Draw completion box
QRect cr = cursorRect();
cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width());
c->complete(cr);
// Select first item in the completion popup
QItemSelectionModel *sm = new QItemSelectionModel(c->completionModel());
c->popup()->setSelectionModel(sm);
sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect);
sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate);
}
QString LineEditCompleter::cursorWord(const QString &line) const
{
return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1,
cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1);
}
void LineEditCompleter::insertCompletion(QString arg)
{
QString s_arg = arg + " ";
setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1,
cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg));
}
void LineEditCompleter::setCompleter(QCompleter *completer)
{
c = completer;
c->setWidget(this);
connect(c, SIGNAL(activated(QString)), this, SLOT(insertCompletion(QString)));
}
void LineEditCompleter::setCompletionList(QStringList completionList)
{
if (!c || c->popup()->isVisible())
return;
QStringListModel *model;
model = (QStringListModel *)(c->model());
if (model == NULL)
model = new QStringListModel();
model->setStringList(completionList);
}

View file

@ -0,0 +1,29 @@
#ifndef LINEEDITCOMPLETER_H
#define LINEEDITCOMPLETER_H
#include "../../deck/custom_line_edit.h"
#include <QFocusEvent>
#include <QKeyEvent>
#include <QStringList>
class LineEditCompleter : public LineEditUnfocusable
{
Q_OBJECT
private:
QString cursorWord(const QString &line) const;
QCompleter *c;
private slots:
void insertCompletion(QString);
protected:
void keyPressEvent(QKeyEvent *event);
void focusOutEvent(QFocusEvent *e);
public:
explicit LineEditCompleter(QWidget *parent = nullptr);
void setCompleter(QCompleter *);
void setCompletionList(QStringList);
};
#endif

View file

@ -0,0 +1,273 @@
#include "phases_toolbar.h"
#include "pb/command_draw_cards.pb.h"
#include "pb/command_next_turn.pb.h"
#include "pb/command_set_active_phase.pb.h"
#include "pb/command_set_card_attr.pb.h"
#include "pixel_map_generator.h"
#include <QAction>
#include <QDebug>
#include <QPainter>
#include <QPen>
#include <QTimer>
PhaseButton::PhaseButton(const QString &_name, QGraphicsItem *parent, QAction *_doubleClickAction, bool _highlightable)
: QObject(), QGraphicsItem(parent), name(_name), active(false), highlightable(_highlightable),
activeAnimationCounter(0), doubleClickAction(_doubleClickAction), width(50)
{
if (highlightable) {
activeAnimationTimer = new QTimer(this);
connect(activeAnimationTimer, SIGNAL(timeout()), this, SLOT(updateAnimation()));
activeAnimationTimer->setSingleShot(false);
} else
activeAnimationCounter = 9;
setCacheMode(DeviceCoordinateCache);
}
QRectF PhaseButton::boundingRect() const
{
return {0, 0, width, width};
}
void PhaseButton::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
QRectF iconRect = boundingRect().adjusted(3, 3, -3, -3);
QRectF translatedIconRect = painter->combinedTransform().mapRect(iconRect);
qreal scaleFactor = translatedIconRect.width() / iconRect.width();
QPixmap iconPixmap = PhasePixmapGenerator::generatePixmap(qRound(translatedIconRect.height()), name);
painter->setBrush(QColor(static_cast<int>(220 * (activeAnimationCounter / 10.0)),
static_cast<int>(220 * (activeAnimationCounter / 10.0)),
static_cast<int>(220 * (activeAnimationCounter / 10.0))));
painter->setPen(Qt::gray);
painter->drawRect(0, 0, static_cast<int>(width - 1), static_cast<int>(width - 1));
painter->save();
resetPainterTransform(painter);
painter->drawPixmap(iconPixmap.rect().translated(qRound(3 * scaleFactor), qRound(3 * scaleFactor)), iconPixmap,
iconPixmap.rect());
painter->restore();
painter->setBrush(QColor(0, 0, 0, static_cast<int>(255 * ((10 - activeAnimationCounter) / 15.0))));
painter->setPen(Qt::gray);
painter->drawRect(0, 0, static_cast<int>(width - 1), static_cast<int>(width - 1));
}
void PhaseButton::setWidth(double _width)
{
prepareGeometryChange();
width = _width;
}
void PhaseButton::setActive(bool _active)
{
if ((active == _active) || !highlightable)
return;
active = _active;
activeAnimationTimer->start(25);
}
void PhaseButton::updateAnimation()
{
if (!highlightable)
return;
if (active) {
if (++activeAnimationCounter >= 10)
activeAnimationTimer->stop();
} else {
if (--activeAnimationCounter <= 0)
activeAnimationTimer->stop();
}
update();
}
void PhaseButton::mousePressEvent(QGraphicsSceneMouseEvent * /*event*/)
{
emit clicked();
}
void PhaseButton::mouseDoubleClickEvent(QGraphicsSceneMouseEvent * /*event*/)
{
triggerDoubleClickAction();
}
void PhaseButton::triggerDoubleClickAction()
{
if (doubleClickAction)
doubleClickAction->trigger();
}
PhasesToolbar::PhasesToolbar(QGraphicsItem *parent)
: QGraphicsItem(parent), width(100), height(100), ySpacing(1), symbolSize(8)
{
auto *aUntapAll = new QAction(this);
connect(aUntapAll, SIGNAL(triggered()), this, SLOT(actUntapAll()));
auto *aDrawCard = new QAction(this);
connect(aDrawCard, SIGNAL(triggered()), this, SLOT(actDrawCard()));
PhaseButton *untapButton = new PhaseButton("untap", this, aUntapAll);
PhaseButton *upkeepButton = new PhaseButton("upkeep", this);
PhaseButton *drawButton = new PhaseButton("draw", this, aDrawCard);
PhaseButton *main1Button = new PhaseButton("main1", this);
PhaseButton *combatStartButton = new PhaseButton("combat_start", this);
PhaseButton *combatAttackersButton = new PhaseButton("combat_attackers", this);
PhaseButton *combatBlockersButton = new PhaseButton("combat_blockers", this);
PhaseButton *combatDamageButton = new PhaseButton("combat_damage", this);
PhaseButton *combatEndButton = new PhaseButton("combat_end", this);
PhaseButton *main2Button = new PhaseButton("main2", this);
PhaseButton *cleanupButton = new PhaseButton("cleanup", this);
buttonList << untapButton << upkeepButton << drawButton << main1Button << combatStartButton << combatAttackersButton
<< combatBlockersButton << combatDamageButton << combatEndButton << main2Button << cleanupButton;
for (auto &i : buttonList)
connect(i, SIGNAL(clicked()), this, SLOT(phaseButtonClicked()));
nextTurnButton = new PhaseButton("nextturn", this, nullptr, false);
connect(nextTurnButton, SIGNAL(clicked()), this, SLOT(actNextTurn()));
rearrangeButtons();
retranslateUi();
}
QRectF PhasesToolbar::boundingRect() const
{
return {0, 0, width, height};
}
void PhasesToolbar::retranslateUi()
{
for (int i = 0; i < buttonList.size(); ++i)
buttonList[i]->setToolTip(getLongPhaseName(i));
}
QString PhasesToolbar::getLongPhaseName(int phase) const
{
switch (phase) {
case 0:
return tr("Untap step");
case 1:
return tr("Upkeep step");
case 2:
return tr("Draw step");
case 3:
return tr("First main phase");
case 4:
return tr("Beginning of combat step");
case 5:
return tr("Declare attackers step");
case 6:
return tr("Declare blockers step");
case 7:
return tr("Combat damage step");
case 8:
return tr("End of combat step");
case 9:
return tr("Second main phase");
case 10:
return tr("End of turn step");
default:
return QString();
}
}
void PhasesToolbar::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{
painter->fillRect(boundingRect(), QColor(50, 50, 50));
}
const double PhasesToolbar::marginSize = 3;
void PhasesToolbar::rearrangeButtons()
{
for (auto &i : buttonList)
i->setWidth(symbolSize);
nextTurnButton->setWidth(symbolSize);
double y = marginSize;
buttonList[0]->setPos(marginSize, y);
buttonList[1]->setPos(marginSize, y += symbolSize);
buttonList[2]->setPos(marginSize, y += symbolSize);
y += ySpacing;
buttonList[3]->setPos(marginSize, y += symbolSize);
y += ySpacing;
buttonList[4]->setPos(marginSize, y += symbolSize);
buttonList[5]->setPos(marginSize, y += symbolSize);
buttonList[6]->setPos(marginSize, y += symbolSize);
buttonList[7]->setPos(marginSize, y += symbolSize);
buttonList[8]->setPos(marginSize, y += symbolSize);
y += ySpacing;
buttonList[9]->setPos(marginSize, y += symbolSize);
y += ySpacing;
buttonList[10]->setPos(marginSize, y += symbolSize);
y += ySpacing;
y += ySpacing;
nextTurnButton->setPos(marginSize, y + symbolSize);
}
void PhasesToolbar::setHeight(double _height)
{
prepareGeometryChange();
height = _height;
ySpacing = (height - 2 * marginSize) / (buttonCount * 5 + spaceCount);
symbolSize = ySpacing * 5;
width = symbolSize + 2 * marginSize;
rearrangeButtons();
}
void PhasesToolbar::setActivePhase(int phase)
{
if (phase >= buttonList.size())
return;
for (int i = 0; i < buttonList.size(); ++i)
buttonList[i]->setActive(i == phase);
}
void PhasesToolbar::triggerPhaseAction(int phase)
{
if (0 <= phase && phase < buttonList.size()) {
buttonList[phase]->triggerDoubleClickAction();
}
}
void PhasesToolbar::phaseButtonClicked()
{
auto *button = qobject_cast<PhaseButton *>(sender());
if (button->getActive())
button->triggerDoubleClickAction();
Command_SetActivePhase cmd;
cmd.set_phase(static_cast<google::protobuf::uint32>(buttonList.indexOf(button)));
emit sendGameCommand(cmd, -1);
}
void PhasesToolbar::actNextTurn()
{
emit sendGameCommand(Command_NextTurn(), -1);
}
void PhasesToolbar::actUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone("table");
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");
emit sendGameCommand(cmd, -1);
}
void PhasesToolbar::actDrawCard()
{
Command_DrawCards cmd;
cmd.set_number(1);
emit sendGameCommand(cmd, -1);
}

View file

@ -0,0 +1,100 @@
#ifndef PHASESTOOLBAR_H
#define PHASESTOOLBAR_H
#include "../../game/board/abstract_graphics_item.h"
#include <QFrame>
#include <QGraphicsObject>
#include <QList>
namespace google
{
namespace protobuf
{
class Message;
}
} // namespace google
class Player;
class GameCommand;
class PhaseButton : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
private:
QString name;
bool active, highlightable;
int activeAnimationCounter;
QTimer *activeAnimationTimer;
QAction *doubleClickAction;
double width;
// void updatePixmap(QPixmap &pixmap);
private slots:
void updateAnimation();
public:
explicit PhaseButton(const QString &_name,
QGraphicsItem *parent = nullptr,
QAction *_doubleClickAction = nullptr,
bool _highlightable = true);
QRectF boundingRect() const override;
void setWidth(double _width);
void setActive(bool _active);
bool getActive() const
{
return active;
}
void triggerDoubleClickAction();
signals:
void clicked();
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
};
class PhasesToolbar : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
private:
QList<PhaseButton *> buttonList;
PhaseButton *nextTurnButton;
double width, height, ySpacing, symbolSize;
static const int buttonCount = 12;
static const int spaceCount = 6;
static const double marginSize;
void rearrangeButtons();
public:
explicit PhasesToolbar(QGraphicsItem *parent = nullptr);
QRectF boundingRect() const override;
void retranslateUi();
void setHeight(double _height);
double getWidth() const
{
return width;
}
int phaseCount() const
{
return buttonList.size();
}
QString getLongPhaseName(int phase) const;
public slots:
void setActivePhase(int phase);
void triggerPhaseAction(int phase);
private slots:
void phaseButtonClicked();
void actNextTurn();
void actUntapAll();
void actDrawCard();
signals:
void sendGameCommand(const ::google::protobuf::Message &command, int playerId);
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override;
};
#endif

View file

@ -0,0 +1,697 @@
#include "picture_loader.h"
#include "../../game/cards/card_database.h"
#include "../../main.h"
#include "../../settings/cache_settings.h"
#include "theme_manager.h"
#include <QApplication>
#include <QBuffer>
#include <QCryptographicHash>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QImageReader>
#include <QMovie>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QScreen>
#include <QSet>
#include <QSvgRenderer>
#include <QThread>
#include <QUrl>
#include <algorithm>
#include <utility>
// never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300
PictureToLoad::PictureToLoad(CardInfoPtr _card)
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
{
if (card) {
for (const auto &set : card->getSets()) {
sortedSets << set.getPtr();
}
if (sortedSets.empty()) {
sortedSets << CardSet::newInstance("", "", "", QDate());
}
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
// The first time called, nextSet will also populate the Urls for the first set.
nextSet();
}
}
void PictureToLoad::populateSetUrls()
{
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
and Urls are removed from it as a download is attempted from each one. Custom Urls for
a set are given higher priority, so should be placed first in the list. */
currentSetUrls.clear();
if (card && currentSet) {
QString setCustomURL = card->getCustomPicURL(currentSet->getShortName());
if (!setCustomURL.isEmpty()) {
currentSetUrls.append(setCustomURL);
}
}
for (const QString &urlTemplate : urlTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {
currentSetUrls.append(transformedUrl);
}
}
/* Call nextUrl to make sure currentUrl is up-to-date
but we don't need the result here. */
(void)nextUrl();
}
bool PictureToLoad::nextSet()
{
if (!sortedSets.isEmpty()) {
currentSet = sortedSets.takeFirst();
populateSetUrls();
return true;
}
currentSet = {};
return false;
}
bool PictureToLoad::nextUrl()
{
if (!currentSetUrls.isEmpty()) {
currentUrl = currentSetUrls.takeFirst();
return true;
}
currentUrl = QString();
return false;
}
QString PictureToLoad::getSetName() const
{
if (currentSet) {
return currentSet->getCorrectedShortName();
} else {
return QString();
}
}
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
PictureLoaderWorker::PictureLoaderWorker()
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
{
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout();
#endif
auto cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
networkManager->setCache(cache);
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
}
PictureLoaderWorker::~PictureLoaderWorker()
{
pictureLoaderThread->deleteLater();
}
void PictureLoaderWorker::processLoadQueue()
{
if (loadQueueRunning) {
return;
}
loadQueueRunning = true;
while (true) {
mutex.lock();
if (loadQueue.isEmpty()) {
mutex.unlock();
loadQueueRunning = false;
return;
}
cardBeingLoaded = loadQueue.takeFirst();
mutex.unlock();
QString setName = cardBeingLoaded.getSetName();
QString cardName = cardBeingLoaded.getCard()->getName();
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: Trying to load picture";
if (cardImageExistsOnDisk(setName, correctedCardName)) {
continue;
}
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: No custom picture, trying to download";
cardsToDownload.append(cardBeingLoaded);
cardBeingLoaded.clear();
if (!downloadRunning) {
startNextPicDownload();
}
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
if (!setName.isEmpty()) {
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture.full found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture.xlhq found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
}
return false;
}
static int parse(const QString &urlTemplate,
const QString &propType,
const QString &cardName,
const QString &setName,
std::function<QString(const QString &)> getProperty,
QMap<QString, QString> &transformMap)
{
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
auto matches = rxCardProp.globalMatch(urlTemplate);
while (matches.hasNext()) {
auto match = matches.next();
QString templatePropertyName = match.captured(1);
auto fillMatch = rxFillWith.match(templatePropertyName);
QString cardPropertyName;
QString fillWith;
int subStrPos = 0;
int subStrLen = -1;
if (fillMatch.hasMatch()) {
cardPropertyName = fillMatch.captured(1);
fillWith = fillMatch.captured(2);
} else {
fillWith = QString();
auto subStrMatch = rxSubStr.match(templatePropertyName);
if (subStrMatch.hasMatch()) {
cardPropertyName = subStrMatch.captured(1);
subStrPos = subStrMatch.captured(2).toInt();
subStrLen = subStrMatch.captured(3).toInt();
} else {
cardPropertyName = templatePropertyName;
}
}
QString propertyValue = getProperty(cardPropertyName);
if (propertyValue.isEmpty()) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << "property (" << cardPropertyName << ") for Url template (" << urlTemplate
<< ") is not available";
return 1;
} else {
int propLength = propertyValue.length();
if (subStrLen > 0) {
if (subStrPos + subStrLen > propLength) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << " property (" << cardPropertyName << ") for Url template ("
<< urlTemplate << ") is smaller than substr specification (" << subStrPos
<< " + " << subStrLen << " > " << propLength << ")";
return 1;
} else {
propertyValue = propertyValue.mid(subStrPos, subStrLen);
propLength = subStrLen;
}
}
if (!fillWith.isEmpty()) {
int fillLength = fillWith.length();
if (fillLength < propLength) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << " property (" << cardPropertyName << ") for Url template ("
<< urlTemplate << ") is longer than fill specification (" << fillWith << ")";
return 1;
} else {
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
}
}
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
}
}
return 0;
}
QString PictureToLoad::transformUrl(const QString &urlTemplate) const
{
/* This function takes Url templates and substitutes actual card details
into the url. This is used for making Urls with follow a predictable format
for downloading images. If information is requested by the template that is
not populated for this specific card/set combination, an empty string is returned.*/
CardSetPtr set = getCurrentSet();
QMap<QString, QString> transformMap = QMap<QString, QString>();
QString setName = getSetName();
// name
QString cardName = card->getName();
transformMap["!name!"] = cardName;
transformMap["!name_lower!"] = card->getName().toLower();
transformMap["!corrected_name!"] = card->getCorrectedName();
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
// card properties
if (parse(
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->getProperty(name); },
transformMap)) {
return QString();
}
if (set) {
transformMap["!setcode!"] = set->getShortName();
transformMap["!setcode_lower!"] = set->getShortName().toLower();
transformMap["!setname!"] = set->getLongName();
transformMap["!setname_lower!"] = set->getLongName().toLower();
if (parse(
urlTemplate, "set", cardName, setName,
[&](const QString &name) { return card->getSetProperty(set->getShortName(), name); }, transformMap)) {
return QString();
}
}
// language setting
transformMap["!sflang!"] = QString(QCoreApplication::translate(
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
if (transformedUrl.contains(prop)) {
if (!transformMap[prop].isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
} else {
/* This means the template is requesting information that is not
* populated in this card, so it should return an empty string,
* indicating an invalid Url.
*/
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: Requested information (" << prop << ") for Url template (" << urlTemplate
<< ") is not available";
return QString();
}
}
}
return transformedUrl;
}
void PictureLoaderWorker::startNextPicDownload()
{
if (cardsToDownload.isEmpty()) {
cardBeingDownloaded.clear();
downloadRunning = false;
return;
}
downloadRunning = true;
cardBeingDownloaded = cardsToDownload.takeFirst();
QString picUrl = cardBeingDownloaded.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Trying to fetch picture from url "
<< url.toDisplayString();
makeRequest(url);
}
}
void PictureLoaderWorker::picDownloadFailed()
{
/* Take advantage of short circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
mutex.lock();
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardBeingDownloaded.getCard(), QImage());
cardBeingDownloaded.clear();
}
emit startLoadQueue();
}
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
QNetworkRequest req(url);
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
return networkManager->get(req);
}
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString()
<< " and retrying (" << reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
makeRequest(reply->url());
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: following "
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
makeRequest(redirectUrl);
reply->deleteLater();
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardBeingDownloaded.getCard(), testImage);
logSuccessMessage = true;
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Possible "
<< (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
}
reply->deleteLater();
startNextPicDownload();
}
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
{
QMutexLocker locker(&mutex);
// avoid queueing the same card more than once
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
return;
}
for (const PictureToLoad &pic : loadQueue) {
if (pic.getCard() == card)
return;
}
for (const PictureToLoad &pic : cardsToDownload) {
if (pic.getCard() == card)
return;
}
loadQueue.append(PictureToLoad(card));
emit startLoadQueue();
}
void PictureLoaderWorker::picDownloadChanged()
{
QMutexLocker locker(&mutex);
picDownload = SettingsCache::instance().getPicDownload();
}
void PictureLoaderWorker::picsPathChanged()
{
QMutexLocker locker(&mutex);
picsPath = SettingsCache::instance().getPicsPath();
customPicsPath = SettingsCache::instance().getCustomPicsPath();
}
void PictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
}
PictureLoader::PictureLoader() : QObject(nullptr)
{
worker = new PictureLoaderWorker;
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
}
PictureLoader::~PictureLoader()
{
worker->deleteLater();
}
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
{
if (card == nullptr) {
return;
}
// search for an exact size copy of the picture in cache
QString key = card->getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap))
return;
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen->devicePixelRatio();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
return;
}
// add the card to the load queue
getInstance().worker->enqueueImageLoad(card);
}
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
{
if (image.isNull()) {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
} else {
if (card->getUpsideDownArt()) {
QImage mirrorImage = image.mirrored(true, true);
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
} else {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
}
}
card->emitPixmapUpdated();
}
void PictureLoader::clearPixmapCache(CardInfoPtr card)
{
if (card) {
QPixmapCache::remove(card->getPixmapCacheKey());
}
}
void PictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
}
void PictureLoader::clearNetworkCache()
{
getInstance().worker->clearNetworkCache();
}
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
{
QPixmap tmp;
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
for (int i = 0; i < max; ++i) {
const CardInfoPtr &card = cards.at(i);
if (!card) {
continue;
}
QString key = card->getPixmapCacheKey();
if (QPixmapCache::find(key, &tmp)) {
continue;
}
getInstance().worker->enqueueImageLoad(card);
}
}
void PictureLoader::picDownloadChanged()
{
QPixmapCache::clear();
}
void PictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}

View file

@ -0,0 +1,144 @@
#ifndef PICTURELOADER_H
#define PICTURELOADER_H
#include "../../game/cards/card_database.h"
#include <QList>
#include <QMap>
#include <QMutex>
#include <QNetworkRequest>
class QNetworkAccessManager;
class QNetworkReply;
class QThread;
class PictureToLoad
{
private:
class SetDownloadPriorityComparator
{
public:
/*
* Returns true if a has higher download priority than b
* Enabled sets have priority over disabled sets
* Both groups follows the user-defined order
*/
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
{
if (a->getEnabled()) {
return !b->getEnabled() || a->getSortKey() < b->getSortKey();
} else {
return !b->getEnabled() && a->getSortKey() < b->getSortKey();
}
}
};
CardInfoPtr card;
QList<CardSetPtr> sortedSets;
QList<QString> urlTemplates;
QList<QString> currentSetUrls;
QString currentUrl;
CardSetPtr currentSet;
public:
explicit PictureToLoad(CardInfoPtr _card = CardInfoPtr());
CardInfoPtr getCard() const
{
return card;
}
void clear()
{
card.clear();
}
QString getCurrentUrl() const
{
return currentUrl;
}
CardSetPtr getCurrentSet() const
{
return currentSet;
}
QString getSetName() const;
QString transformUrl(const QString &urlTemplate) const;
bool nextSet();
bool nextUrl();
void populateSetUrls();
};
class PictureLoaderWorker : public QObject
{
Q_OBJECT
public:
explicit PictureLoaderWorker();
~PictureLoaderWorker() override;
void enqueueImageLoad(CardInfoPtr card);
void clearNetworkCache();
private:
static QStringList md5Blacklist;
QThread *pictureLoaderThread;
QString picsPath, customPicsPath;
QList<PictureToLoad> loadQueue;
QMutex mutex;
QNetworkAccessManager *networkManager;
QList<PictureToLoad> cardsToDownload;
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
void picDownloadChanged();
void picsPathChanged();
public slots:
void processLoadQueue();
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
};
class PictureLoader : public QObject
{
Q_OBJECT
public:
static PictureLoader &getInstance()
{
static PictureLoader instance;
return instance;
}
private:
explicit PictureLoader();
~PictureLoader() override;
// Singleton - Don't implement copy constructor and assign operator
PictureLoader(PictureLoader const &);
void operator=(PictureLoader const &);
PictureLoaderWorker *worker;
public:
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
static void clearPixmapCache(CardInfoPtr card);
static void clearPixmapCache();
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
public slots:
static void clearNetworkCache();
private slots:
void picDownloadChanged();
void picsPathChanged();
public slots:
void imageLoaded(CardInfoPtr card, const QImage &image);
};
#endif

View file

@ -0,0 +1,159 @@
#include "pixel_map_generator.h"
#include "pb/serverinfo_user.pb.h"
#include <QApplication>
#include <QDebug>
#include <QPainter>
#include <QPalette>
QMap<QString, QPixmap> PhasePixmapGenerator::pmCache;
QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
{
QString key = name + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap =
QPixmap("theme:phases/" + name).scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> CounterPixmapGenerator::pmCache;
QPixmap CounterPixmapGenerator::generatePixmap(int height, QString name, bool highlight)
{
if (highlight)
name.append("_highlight");
QString key = name + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap =
QPixmap("theme:counters/" + name).scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (pixmap.isNull()) {
name = "general";
if (highlight)
name.append("_highlight");
pixmap =
QPixmap("theme:counters/" + name).scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
pmCache.insert(key, pixmap);
return pixmap;
}
QPixmap PingPixmapGenerator::generatePixmap(int size, int value, int max)
{
int key = size * 1000000 + max * 1000 + value;
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
QColor color;
if ((max == -1) || (value == -1))
color = Qt::black;
else
color.setHsv(120 * (1.0 - ((double)value / max)), 255, 255);
QRadialGradient g(QPointF((double)pixmap.width() / 2, (double)pixmap.height() / 2),
qMin(pixmap.width(), pixmap.height()) / 2.0);
g.setColorAt(0, color);
g.setColorAt(1, Qt::transparent);
painter.fillRect(0, 0, pixmap.width(), pixmap.height(), QBrush(g));
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<int, QPixmap> PingPixmapGenerator::pmCache;
QPixmap CountryPixmapGenerator::generatePixmap(int height, const QString &countryCode)
{
if (countryCode.size() != 2)
return QPixmap();
QString key = countryCode + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
int width = height * 2;
QPixmap pixmap = QPixmap("theme:countries/" + countryCode.toLower())
.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPainter painter(&pixmap);
painter.setPen(Qt::black);
painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> CountryPixmapGenerator::pmCache;
QPixmap UserLevelPixmapGenerator::generatePixmap(int height, UserLevelFlags userLevel, bool isBuddy, QString privLevel)
{
QString key = QString::number(height * 10000) + ":" + (short)userLevel + ":" + (short)isBuddy + ":" + privLevel;
if (pmCache.contains(key))
return pmCache.value(key);
QString levelString;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
levelString = "admin";
if (privLevel.toLower() == "vip")
levelString.append("_" + privLevel.toLower());
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
levelString = "moderator";
if (privLevel.toLower() == "vip")
levelString.append("_" + privLevel.toLower());
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
levelString = "registered";
if (privLevel.toLower() != "none")
levelString.append("_" + privLevel.toLower());
} else
levelString = "normal";
if (isBuddy)
levelString.append("_buddy");
QPixmap pixmap = QPixmap("theme:userlevels/" + levelString)
.scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> UserLevelPixmapGenerator::pmCache;
QPixmap LockPixmapGenerator::generatePixmap(int height)
{
int key = height;
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap = QPixmap("theme:icons/lock").scaled(height, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<int, QPixmap> LockPixmapGenerator::pmCache;
const QPixmap loadColorAdjustedPixmap(QString name)
{
if (qApp->palette().windowText().color().lightness() > 200) {
QImage img(name);
img.invertPixels();
QPixmap result;
result.convertFromImage(img);
return result;
} else {
return QPixmap(name);
}
}

View file

@ -0,0 +1,89 @@
#ifndef PIXMAPGENERATOR_H
#define PIXMAPGENERATOR_H
#include "user_level.h"
#include <QMap>
#include <QPixmap>
class PhasePixmapGenerator
{
private:
static QMap<QString, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int size, QString name);
static void clear()
{
pmCache.clear();
}
};
class CounterPixmapGenerator
{
private:
static QMap<QString, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int size, QString name, bool highlight);
static void clear()
{
pmCache.clear();
}
};
class PingPixmapGenerator
{
private:
static QMap<int, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int size, int value, int max);
static void clear()
{
pmCache.clear();
}
};
class CountryPixmapGenerator
{
private:
static QMap<QString, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int height, const QString &countryCode);
static void clear()
{
pmCache.clear();
}
};
class UserLevelPixmapGenerator
{
private:
static QMap<QString, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int height, UserLevelFlags userLevel, bool isBuddy, QString privLevel = "NONE");
static void clear()
{
pmCache.clear();
}
};
class LockPixmapGenerator
{
private:
static QMap<int, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int height);
static void clear()
{
pmCache.clear();
}
};
const QPixmap loadColorAdjustedPixmap(QString name);
#endif

View file

@ -0,0 +1,204 @@
#include "theme_manager.h"
#include "../../settings/cache_settings.h"
#include <QApplication>
#include <QColor>
#include <QDebug>
#include <QLibraryInfo>
#include <QPixmapCache>
#include <QStandardPaths>
#define NONE_THEME_NAME "Default"
#define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone"
#define PLAYERZONE_BG_NAME "playerzone"
#define STACKZONE_BG_NAME "stackzone"
#define TABLEZONE_BG_NAME "tablezone"
static const QColor HANDZONE_BG_DEFAULT = QColor(80, 100, 50);
static const QColor TABLEZONE_BG_DEFAULT = QColor(70, 50, 100);
static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200);
static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43);
static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"};
ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
{
ensureThemeDirectoryExists();
connect(&SettingsCache::instance(), SIGNAL(themeChanged()), this, SLOT(themeChangedSlot()));
themeChangedSlot();
}
void ThemeManager::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
qDebug() << "Theme name not set, setting default value";
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
}
}
QStringMap &ThemeManager::getAvailableThemes()
{
QDir dir;
availableThemes.clear();
// add default value
availableThemes.insert(NONE_THEME_NAME, QString());
// load themes from user profile dir
dir.setPath(SettingsCache::instance().getThemesPath());
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
if (!availableThemes.contains(themeName)) {
availableThemes.insert(themeName, dir.absoluteFilePath(themeName));
}
}
// load themes from cockatrice system dir
dir.setPath(qApp->applicationDirPath() +
#ifdef Q_OS_MAC
"/../Resources/themes"
#elif defined(Q_OS_WIN)
"/themes"
#else // linux
"/../share/cockatrice/themes"
#endif
);
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
if (!availableThemes.contains(themeName)) {
availableThemes.insert(themeName, dir.absoluteFilePath(themeName));
}
}
return availableThemes;
}
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
{
QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName);
if (tmp.isNull()) {
brush.setColor(fallbackColor);
brush.setStyle(Qt::SolidPattern);
} else {
brush.setTexture(tmp);
}
return brush;
}
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
{
QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName);
if (tmp.isNull()) {
brush = fallbackBrush;
} else {
brush.setTexture(tmp);
}
return brush;
}
void ThemeManager::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getThemeName();
qDebug() << "Theme changed:" << themeName;
QString dirPath = getAvailableThemes().value(themeName);
QDir dir = dirPath;
// css
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
} else {
qApp->setStyleSheet("");
}
if (dirPath.isEmpty()) {
// set default values
QDir::setSearchPaths("theme", DEFAULT_RESOURCE_PATHS);
handBgBrush = HANDZONE_BG_DEFAULT;
tableBgBrush = TABLEZONE_BG_DEFAULT;
playerBgBrush = PLAYERZONE_BG_DEFAULT;
stackBgBrush = STACKZONE_BG_DEFAULT;
} else {
// resources
QStringList resources;
resources << dir.absolutePath() << DEFAULT_RESOURCE_PATHS;
QDir::setSearchPaths("theme", resources);
// zones bg
dir.cd("zones");
handBgBrush = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
tableBgBrush = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
playerBgBrush = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
stackBgBrush = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
}
tableBgBrushesCache.clear();
stackBgBrushesCache.clear();
playerBgBrushesCache.clear();
handBgBrushesCache.clear();
QPixmapCache::clear();
emit themeChanged();
}
QBrush ThemeManager::getExtraTableBgBrush(QString extraNumber, QBrush &fallbackBrush)
{
QBrush returnBrush;
if (!tableBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(TABLEZONE_BG_NAME + extraNumber, fallbackBrush);
tableBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = tableBgBrushesCache.value(extraNumber.toInt());
}
return returnBrush;
}
QBrush ThemeManager::getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush)
{
QBrush returnBrush;
if (!stackBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(STACKZONE_BG_NAME + extraNumber, fallbackBrush);
stackBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = stackBgBrushesCache.value(extraNumber.toInt());
}
return returnBrush;
}
QBrush ThemeManager::getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush)
{
QBrush returnBrush;
if (!playerBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(PLAYERZONE_BG_NAME + extraNumber, fallbackBrush);
playerBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = playerBgBrushesCache.value(extraNumber.toInt());
}
return returnBrush;
}
QBrush ThemeManager::getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush)
{
QBrush returnBrush;
if (!handBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(HANDZONE_BG_NAME + extraNumber, fallbackBrush);
handBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = handBgBrushesCache.value(extraNumber.toInt());
}
return returnBrush;
}

View file

@ -0,0 +1,65 @@
#ifndef THEMEMANAGER_H
#define THEMEMANAGER_H
#include <QBrush>
#include <QDir>
#include <QMap>
#include <QObject>
#include <QPixmap>
#include <QString>
typedef QMap<QString, QString> QStringMap;
typedef QMap<int, QBrush> QBrushMap;
class QApplication;
class ThemeManager : public QObject
{
Q_OBJECT
public:
ThemeManager(QObject *parent = nullptr);
private:
QBrush handBgBrush, stackBgBrush, tableBgBrush, playerBgBrush;
QStringMap availableThemes;
/*
Internal cache for multiple backgrounds
*/
QBrushMap tableBgBrushesCache, stackBgBrushesCache, playerBgBrushesCache, handBgBrushesCache;
protected:
void ensureThemeDirectoryExists();
QBrush loadBrush(QString fileName, QColor fallbackColor);
QBrush loadExtraBrush(QString fileName, QBrush &fallbackBrush);
public:
QBrush &getHandBgBrush()
{
return handBgBrush;
}
QBrush &getStackBgBrush()
{
return stackBgBrush;
}
QBrush &getTableBgBrush()
{
return tableBgBrush;
}
QBrush &getPlayerBgBrush()
{
return playerBgBrush;
}
QStringMap &getAvailableThemes();
QBrush getExtraTableBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush);
protected slots:
void themeChangedSlot();
signals:
void themeChanged();
};
extern ThemeManager *themeManager;
#endif

View file

@ -0,0 +1,103 @@
#include "tip_of_the_day.h"
#include <QDate>
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QXmlStreamReader>
#include <utility>
#define TIPDDBMODEL_COLUMNS 3
TipOfTheDay::TipOfTheDay(QString _title, QString _content, QString _imagePath, QDate _date)
: title(std::move(_title)), content(std::move(_content)), imagePath(std::move(_imagePath)), date(_date)
{
}
TipsOfTheDay::TipsOfTheDay(QString xmlPath, QObject *parent) : QAbstractListModel(parent)
{
tipList = new QList<TipOfTheDay>;
QFile xmlFile(xmlPath);
QTextStream errorStream(stderr);
if (!QFile::exists(xmlPath)) {
errorStream << tr("File does not exist.\n");
return;
} else if (!xmlFile.open(QIODevice::ReadOnly)) {
errorStream << tr("Failed to open file.\n");
return;
}
QXmlStreamReader reader(&xmlFile);
while (!reader.atEnd()) {
if (reader.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto readerName = reader.name().toString();
if (readerName == "tip") {
QString title, content, imagePath;
QDate date;
reader.readNext();
while (!reader.atEnd()) {
if (reader.readNext() == QXmlStreamReader::EndElement) {
break;
}
readerName = reader.name().toString();
if (readerName == "title") {
title = reader.readElementText();
} else if (readerName == "text") {
content = reader.readElementText();
} else if (readerName == "image") {
imagePath = "theme:tips/images/" + reader.readElementText();
} else if (readerName == "date") {
date = QDate::fromString(reader.readElementText(), Qt::ISODate);
} else {
// unknown element, do nothing
}
}
tipList->append(TipOfTheDay(title, content, imagePath, date));
}
}
}
TipsOfTheDay::~TipsOfTheDay()
{
delete tipList;
}
QVariant TipsOfTheDay::data(const QModelIndex &index, int /*role*/) const
{
if (!index.isValid() || index.row() >= tipList->size() || index.column() >= TIPDDBMODEL_COLUMNS)
return QVariant();
TipOfTheDay tip = tipList->at(index.row());
switch (index.column()) {
case TitleColumn:
return tip.getTitle();
case ContentColumn:
return tip.getContent();
case ImagePathColumn:
return tip.getImagePath();
case DateColumn:
return tip.getDate();
default:
return QVariant();
}
}
TipOfTheDay TipsOfTheDay::getTip(int tipId)
{
return tipList->at(tipId);
}
int TipsOfTheDay::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return tipList->size();
}

View file

@ -0,0 +1,55 @@
#ifndef TIP_OF_DAY_H
#define TIP_OF_DAY_H
#include <QAbstractListModel>
#include <QDate>
class TipOfTheDay
{
public:
explicit TipOfTheDay(QString _title, QString _content, QString _imagePath, QDate _date);
QString getTitle() const
{
return title;
}
QString getContent() const
{
return content;
}
QString getImagePath() const
{
return imagePath;
}
QDate getDate() const
{
return date;
}
private:
QString title, content, imagePath;
QDate date;
};
class TipsOfTheDay : public QAbstractListModel
{
Q_OBJECT
public:
enum Columns
{
TitleColumn,
ContentColumn,
ImagePathColumn,
DateColumn,
};
explicit TipsOfTheDay(QString xmlPath, QObject *parent = nullptr);
~TipsOfTheDay() override;
TipOfTheDay getTip(int tipId);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
private:
QList<TipOfTheDay> *tipList;
};
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,157 @@
/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@gmx.net *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifndef WINDOW_H
#define WINDOW_H
#include "../game_logic/abstract_client.h"
#include "pb/response.pb.h"
#include <QList>
#include <QMainWindow>
#include <QMessageBox>
#include <QProcess>
#include <QSystemTrayIcon>
#include <QtNetwork>
class DlgConnect;
class DlgViewLog;
class GameReplay;
class HandlePublicServers;
class LocalClient;
class LocalServer;
class QThread;
class RemoteClient;
class ServerInfo_User;
class TabSupervisor;
class WndSets;
class DlgTipOfTheDay;
class MainWindow : public QMainWindow
{
Q_OBJECT
public slots:
void actCheckCardUpdates();
void actCheckServerUpdates();
private slots:
void updateTabMenu(const QList<QMenu *> &newMenuList);
void statusChanged(ClientStatus _status);
void processConnectionClosedEvent(const Event_ConnectionClosed &event);
void processServerShutdownEvent(const Event_ServerShutdown &event);
void serverTimeout();
void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);
void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);
void activateError();
void socketError(const QString &errorStr);
void protocolVersionMismatch(int localVersion, int remoteVersion);
void userInfoReceived(const ServerInfo_User &userInfo);
void registerAccepted();
void registerAcceptedNeedsActivate();
void activateAccepted();
void localGameEnded();
void pixmapCacheSizeChanged(int newSizeInMBs);
void notifyUserAboutUpdate();
void actConnect();
void actDisconnect();
void actSinglePlayer();
void actWatchReplay();
void actDeckEditor();
void actFullScreen(bool checked);
void actRegister();
void actSettings();
void actExit();
void actForgotPasswordRequest();
void actAbout();
void actTips();
void actUpdate();
void actViewLog();
void forgotPasswordSuccess();
void forgotPasswordError();
void promptForgotPasswordReset();
void actShow();
void promptForgotPasswordChallenge();
void showWindowIfHidden();
void cardUpdateError(QProcess::ProcessError err);
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
void refreshShortcuts();
void cardDatabaseLoadingFailed();
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
void cardDatabaseAllNewSetsEnabled();
void actOpenCustomFolder();
void actOpenCustomsetsFolder();
void actAddCustomSet();
void actManageSets();
void actEditTokens();
void startupConfigCheck();
void alertForcedOracleRun(const QString &version, bool isUpdate);
private:
static const QString appName;
static const QStringList fileNameFilters;
void setClientStatusTitle();
void retranslateUi();
void createActions();
void createMenus();
void createTrayIcon();
int getNextCustomSetPrefix(QDir dataDir);
inline QString getCardUpdaterBinaryName()
{
return "oracle";
};
void exitCardDatabaseUpdate();
QList<QMenu *> tabMenus;
QMenu *cockatriceMenu, *dbMenu, *helpMenu, *trayIconMenu;
QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aFullScreen, *aSettings, *aExit,
*aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aForgotPassword, *aUpdate, *aViewLog, *aManageSets,
*aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet, *aShow;
TabSupervisor *tabSupervisor;
WndSets *wndSets;
RemoteClient *client;
QThread *clientThread;
LocalServer *localServer;
bool bHasActivated, askedForDbUpdater;
QMessageBox serverShutdownMessageBox;
QProcess *cardUpdateProcess;
DlgViewLog *logviewDialog;
DlgConnect *dlgConnect;
GameReplay *replay;
DlgTipOfTheDay *tip;
QUrl connectTo;
public:
explicit MainWindow(QWidget *parent = nullptr);
void setConnectTo(QString url)
{
connectTo = QUrl(QString("cockatrice://%1").arg(url));
}
~MainWindow() override;
protected:
void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override;
QString extractInvalidUsernameMessage(QString &in);
};
#endif