mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-30 18:43:55 -07:00
Parallel picture loader (#5977)
* Parallelize picture loader. * Queue requests instead. * Include a status bar for the picture loader. * Save redirect cache on destruction. * Address comments. * Let's not overwrite an ambigious variable name. * Lint. * Oracle needs the status bar too. * We actually get a free request if we hit a cached image. * Fix cmake list. * toString() the url. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
9af3fbc35f
commit
fe57efb1a8
16 changed files with 596 additions and 350 deletions
|
|
@ -67,7 +67,10 @@ set(cockatrice_SOURCES
|
||||||
src/client/ui/line_edit_completer.cpp
|
src/client/ui/line_edit_completer.cpp
|
||||||
src/client/ui/phases_toolbar.cpp
|
src/client/ui/phases_toolbar.cpp
|
||||||
src/client/ui/picture_loader/picture_loader.cpp
|
src/client/ui/picture_loader/picture_loader.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_status_bar.cpp
|
||||||
src/client/ui/picture_loader/picture_loader_worker.cpp
|
src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_worker_work.cpp
|
||||||
src/client/ui/picture_loader/picture_to_load.cpp
|
src/client/ui/picture_loader/picture_to_load.cpp
|
||||||
src/client/ui/pixel_map_generator.cpp
|
src/client/ui/pixel_map_generator.cpp
|
||||||
src/client/ui/theme_manager.cpp
|
src/client/ui/theme_manager.cpp
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QMainWindow>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QNetworkDiskCache>
|
#include <QNetworkDiskCache>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmapCache>
|
#include <QPixmapCache>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QStatusBar>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
||||||
|
|
||||||
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
|
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
|
||||||
|
|
||||||
|
statusBar = new PictureLoaderStatusBar(nullptr);
|
||||||
|
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow->statusBar()->addPermanentWidget(statusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(worker, &PictureLoaderWorker::imageLoadQueued, statusBar, &PictureLoaderStatusBar::addQueuedImageLoad);
|
||||||
|
connect(worker, &PictureLoaderWorker::imageLoadSuccessful, statusBar,
|
||||||
|
&PictureLoaderStatusBar::addSuccessfulImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
PictureLoader::~PictureLoader()
|
PictureLoader::~PictureLoader()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define PICTURELOADER_H
|
#define PICTURELOADER_H
|
||||||
|
|
||||||
#include "../../../game/cards/card_info.h"
|
#include "../../../game/cards/card_info.h"
|
||||||
|
#include "picture_loader_status_bar.h"
|
||||||
#include "picture_loader_worker.h"
|
#include "picture_loader_worker.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
@ -27,6 +28,7 @@ private:
|
||||||
void operator=(PictureLoader const &);
|
void operator=(PictureLoader const &);
|
||||||
|
|
||||||
PictureLoaderWorker *worker;
|
PictureLoaderWorker *worker;
|
||||||
|
PictureLoaderStatusBar *statusBar;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#include "picture_loader_request_status_display_widget.h"
|
||||||
|
|
||||||
|
PictureLoaderRequestStatusDisplayWidget::PictureLoaderRequestStatusDisplayWidget(QWidget *parent,
|
||||||
|
const QUrl &_url,
|
||||||
|
PictureLoaderWorkerWork *worker)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
layout = new QHBoxLayout(this);
|
||||||
|
|
||||||
|
if (worker->cardToDownload.getCard()) {
|
||||||
|
name = new QLabel(this);
|
||||||
|
name->setText(worker->cardToDownload.getCard()->getName());
|
||||||
|
setShortname = new QLabel(this);
|
||||||
|
setShortname->setText(worker->cardToDownload.getSetName());
|
||||||
|
providerId = new QLabel(this);
|
||||||
|
providerId->setText(worker->cardToDownload.getCard()->getProperty("uuid"));
|
||||||
|
|
||||||
|
layout->addWidget(name);
|
||||||
|
layout->addWidget(setShortname);
|
||||||
|
layout->addWidget(providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = new QLabel(QDateTime::currentDateTime().toString(), this);
|
||||||
|
elapsedTime = new QLabel("0", this);
|
||||||
|
finished = new QLabel("False", this);
|
||||||
|
url = new QLabel(_url.toString(), this);
|
||||||
|
|
||||||
|
layout->addWidget(startTime);
|
||||||
|
layout->addWidget(elapsedTime);
|
||||||
|
layout->addWidget(finished);
|
||||||
|
layout->addWidget(url);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#ifndef PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||||
|
#define PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class PictureLoaderRequestStatusDisplayWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
PictureLoaderRequestStatusDisplayWidget(QWidget *parent, const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
PictureLoaderWorkerWork *worker;
|
||||||
|
|
||||||
|
void setFinished()
|
||||||
|
{
|
||||||
|
finished->setText("True");
|
||||||
|
update();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getFinished() const
|
||||||
|
{
|
||||||
|
return finished->text() == "True";
|
||||||
|
}
|
||||||
|
|
||||||
|
void setElapsedTime(const QString &_elapsedTime) const
|
||||||
|
{
|
||||||
|
elapsedTime->setText(_elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
int queryElapsedSeconds()
|
||||||
|
{
|
||||||
|
if (!finished) {
|
||||||
|
int elapsedSeconds = QDateTime::fromString(startTime->text()).secsTo(QDateTime::currentDateTime());
|
||||||
|
elapsedTime->setText(QString::number(elapsedSeconds));
|
||||||
|
update();
|
||||||
|
repaint();
|
||||||
|
return elapsedSeconds;
|
||||||
|
}
|
||||||
|
return elapsedTime->text().toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getStartTime() const
|
||||||
|
{
|
||||||
|
return startTime->text();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getUrl() const
|
||||||
|
{
|
||||||
|
return url->text();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHBoxLayout *layout;
|
||||||
|
QLabel *name;
|
||||||
|
QLabel *setShortname;
|
||||||
|
QLabel *providerId;
|
||||||
|
QLabel *startTime;
|
||||||
|
QLabel *elapsedTime;
|
||||||
|
QLabel *finished;
|
||||||
|
QLabel *url;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
#include "picture_loader_status_bar.h"
|
||||||
|
|
||||||
|
#include "picture_loader_request_status_display_widget.h"
|
||||||
|
|
||||||
|
PictureLoaderStatusBar::PictureLoaderStatusBar(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
layout = new QHBoxLayout(this);
|
||||||
|
progressBar = new QProgressBar(this);
|
||||||
|
progressBar->setMaximum(0);
|
||||||
|
progressBar->setFormat("%v/%m");
|
||||||
|
layout->addWidget(progressBar);
|
||||||
|
|
||||||
|
loadLog = new SettingsButtonWidget(this);
|
||||||
|
layout->addWidget(loadLog);
|
||||||
|
|
||||||
|
cleaner = new QTimer(this);
|
||||||
|
cleaner->setInterval(1000);
|
||||||
|
connect(cleaner, &QTimer::timeout, this, &PictureLoaderStatusBar::cleanOldEntries);
|
||||||
|
cleaner->start();
|
||||||
|
|
||||||
|
setLayout(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderStatusBar::cleanOldEntries()
|
||||||
|
{
|
||||||
|
if (!loadLog || !loadLog->popup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
|
||||||
|
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
|
||||||
|
statusDisplayWidget->queryElapsedSeconds();
|
||||||
|
if (statusDisplayWidget->getFinished() &&
|
||||||
|
QDateTime::fromString(statusDisplayWidget->getStartTime()).secsTo(QDateTime::currentDateTime()) > 10) {
|
||||||
|
loadLog->removeSettingsWidget(statusDisplayWidget);
|
||||||
|
progressBar->setMaximum(progressBar->maximum() - 1);
|
||||||
|
progressBar->setValue(progressBar->value() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderStatusBar::addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
|
{
|
||||||
|
loadLog->addSettingsWidget(new PictureLoaderRequestStatusDisplayWidget(loadLog, url, worker));
|
||||||
|
progressBar->setMaximum(progressBar->maximum() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderStatusBar::addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
|
{
|
||||||
|
Q_UNUSED(worker)
|
||||||
|
progressBar->setValue(progressBar->value() + 1);
|
||||||
|
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
|
||||||
|
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
|
||||||
|
if (statusDisplayWidget->getUrl() == url.toString()) {
|
||||||
|
statusDisplayWidget->queryElapsedSeconds();
|
||||||
|
statusDisplayWidget->setFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef PICTURE_LOADER_STATUS_BAR_H
|
||||||
|
#define PICTURE_LOADER_STATUS_BAR_H
|
||||||
|
|
||||||
|
#include "../widgets/quick_settings/settings_button_widget.h"
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class PictureLoaderStatusBar : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit PictureLoaderStatusBar(QWidget *parent);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
void addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
void cleanOldEntries();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHBoxLayout *layout;
|
||||||
|
QProgressBar *progressBar;
|
||||||
|
SettingsButtonWidget *loadLog;
|
||||||
|
QTimer *cleaner;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PICTURE_LOADER_STATUS_BAR_H
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
#include "../../../game/cards/card_database_manager.h"
|
#include "../../../game/cards/card_database_manager.h"
|
||||||
#include "../../../settings/cache_settings.h"
|
#include "../../../settings/cache_settings.h"
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
|
|
||||||
#include <QBuffer>
|
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QNetworkDiskCache>
|
#include <QNetworkDiskCache>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
// Card back returned by gatherer when card is not found
|
// Card back returned by gatherer when card is not found
|
||||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||||
|
|
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
||||||
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
||||||
{
|
{
|
||||||
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue,
|
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||||
Qt::QueuedConnection);
|
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
|
||||||
&PictureLoaderWorker::picDownloadChanged);
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
||||||
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
||||||
|
|
||||||
|
|
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||||
networkManager->setTransferTimeout();
|
networkManager->setTransferTimeout();
|
||||||
#endif
|
#endif
|
||||||
auto cache = new QNetworkDiskCache(this);
|
cache = new QNetworkDiskCache(this);
|
||||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||||
cache->setMaximumCacheSize(1024L * 1024L *
|
cache->setMaximumCacheSize(1024L * 1024L *
|
||||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
|
||||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
[this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||||
networkManager->setCache(cache);
|
networkManager->setCache(cache);
|
||||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||||
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
|
|
||||||
|
|
||||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||||
loadRedirectCache();
|
loadRedirectCache();
|
||||||
|
|
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
pictureLoaderThread = new QThread;
|
pictureLoaderThread = new QThread;
|
||||||
pictureLoaderThread->start(QThread::LowPriority);
|
pictureLoaderThread->start(QThread::LowPriority);
|
||||||
moveToThread(pictureLoaderThread);
|
moveToThread(pictureLoaderThread);
|
||||||
|
|
||||||
|
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
|
||||||
|
requestTimer.setInterval(1000);
|
||||||
|
requestTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
PictureLoaderWorker::~PictureLoaderWorker()
|
PictureLoaderWorker::~PictureLoaderWorker()
|
||||||
{
|
{
|
||||||
|
saveRedirectCache();
|
||||||
pictureLoaderThread->deleteLater();
|
pictureLoaderThread->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::processLoadQueue()
|
void PictureLoaderWorker::queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
{
|
{
|
||||||
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();
|
|
||||||
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
|
|
||||||
|
|
||||||
// FIXME: This is a hack so that to keep old Cockatrice behavior
|
|
||||||
// (ignoring provider ID) when the "override all card art with personal
|
|
||||||
// preference" is set.
|
|
||||||
//
|
|
||||||
// Figure out a proper way to integrate the two systems at some point.
|
|
||||||
//
|
|
||||||
// Note: need to go through a member for
|
|
||||||
// overrideAllCardArtWithPersonalPreference as reading from the
|
|
||||||
// SettingsCache instance from the PictureLoaderWorker thread could
|
|
||||||
// cause race conditions.
|
|
||||||
//
|
|
||||||
// XXX: Reading from the CardDatabaseManager instance from the
|
|
||||||
// PictureLoaderWorker thread might not be safe either
|
|
||||||
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
|
|
||||||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
|
||||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
|
|
||||||
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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, bool searchCustomPics)
|
|
||||||
{
|
|
||||||
QImage image;
|
|
||||||
QImageReader imgReader;
|
|
||||||
imgReader.setDecideFormatFromContent(true);
|
|
||||||
QList<QString> picsPaths = QList<QString>();
|
|
||||||
|
|
||||||
if (searchCustomPics) {
|
|
||||||
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)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
imgReader.setFileName(_picsPath + ".full");
|
|
||||||
if (imgReader.read(&image)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
imgReader.setFileName(_picsPath + ".xlhq");
|
|
||||||
if (imgReader.read(&image)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace() << "[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 {
|
|
||||||
qCWarning(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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)
|
|
||||||
{
|
|
||||||
// Check if the redirect is cached
|
|
||||||
QUrl cachedRedirect = getCachedRedirect(url);
|
QUrl cachedRedirect = getCachedRedirect(url);
|
||||||
if (!cachedRedirect.isEmpty()) {
|
if (!cachedRedirect.isEmpty()) {
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
queueRequest(cachedRedirect, worker);
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
}
|
||||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
|
if (cache->metaData(url).isValid()) {
|
||||||
<< " to " << cachedRedirect.toDisplayString();
|
makeRequest(url, worker);
|
||||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
} else {
|
||||||
|
requestLoadQueue.append(qMakePair(url, worker));
|
||||||
|
emit imageLoadQueued(url, worker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
|
{
|
||||||
|
// Check for cached redirects
|
||||||
|
QUrl cachedRedirect = getCachedRedirect(url);
|
||||||
|
if (!cachedRedirect.isEmpty()) {
|
||||||
|
emit imageLoadSuccessful(url, worker);
|
||||||
|
return makeRequest(cachedRedirect, worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest req(url);
|
QNetworkRequest req(url);
|
||||||
|
|
||||||
// QNetworkDiskCache leaks file descriptors when downloading compressed
|
|
||||||
// files, see https://bugreports.qt.io/browse/QTBUG-135641
|
|
||||||
//
|
|
||||||
// We can set the Accept-Encoding header manually to disable Qt's automatic
|
|
||||||
// response decompression, but then we would have to deal with decompression
|
|
||||||
// ourselves.
|
|
||||||
//
|
|
||||||
// Since we are dowloading images that are usually stored in a
|
|
||||||
// compressed format (e.g. jpeg or webp), it's not clear compression at the
|
|
||||||
// HTTP level helps; in fact, images are typically returned without
|
|
||||||
// compression. Redirects, on the other hand, are compressed and cause file
|
|
||||||
// descriptor leaks -- but since redirects have no payload, we don't really
|
|
||||||
// care either.
|
|
||||||
//
|
|
||||||
// In the end, just do the simple thing and disable HTTP compression.
|
|
||||||
req.setRawHeader("accept-encoding", "identity");
|
|
||||||
|
|
||||||
if (!picDownload) {
|
if (!picDownload) {
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = networkManager->get(req);
|
QNetworkReply *reply = networkManager->get(req);
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
// Connect reply handling
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, url, worker]() {
|
||||||
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||||
|
|
||||||
if (redirectTarget.isValid()) {
|
if (redirectTarget.isValid()) {
|
||||||
QUrl redirectUrl = redirectTarget.toUrl();
|
QUrl redirectUrl = redirectTarget.toUrl();
|
||||||
if (redirectUrl.isRelative()) {
|
if (redirectUrl.isRelative()) {
|
||||||
redirectUrl = url.resolved(redirectUrl);
|
redirectUrl = url.resolved(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheRedirect(url, redirectUrl);
|
cacheRedirect(url, redirectUrl);
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
||||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
|
|
||||||
<< " to " << redirectUrl.toDisplayString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
worker->picDownloadFinished(reply);
|
||||||
|
emit imageLoadSuccessful(url, worker);
|
||||||
|
|
||||||
|
// If we hit a cached image, we get to make another request for free.
|
||||||
|
if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
|
||||||
|
if (!requestLoadQueue.isEmpty()) {
|
||||||
|
auto request = requestLoadQueue.takeFirst();
|
||||||
|
makeRequest(request.first, request.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||||
|
qInfo() << "Too many requests.";
|
||||||
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::processQueuedRequests()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (!requestLoadQueue.isEmpty()) {
|
||||||
|
auto request = requestLoadQueue.takeFirst();
|
||||||
|
makeRequest(request.first, request.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::enqueueImageLoad(const CardInfoPtr &card)
|
||||||
|
{
|
||||||
|
auto worker = new PictureLoaderWorkerWork(this, card);
|
||||||
|
Q_UNUSED(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::imageLoadedSuccessfully(CardInfoPtr card, const QImage &image)
|
||||||
|
{
|
||||||
|
emit imageLoaded(std::move(card), image);
|
||||||
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
||||||
{
|
{
|
||||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||||
saveRedirectCache();
|
// saveRedirectCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
||||||
|
|
@ -353,129 +211,6 @@ void PictureLoaderWorker::cleanStaleEntries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
|
||||||
{
|
|
||||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
|
||||||
|
|
||||||
if (reply->error()) {
|
|
||||||
if (isFromCache) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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 {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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();
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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 {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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) {
|
|
||||||
qCInfo(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[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()
|
void PictureLoaderWorker::picDownloadChanged()
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
#ifndef PICTURE_LOADER_WORKER_H
|
#ifndef PICTURE_LOADER_WORKER_H
|
||||||
#define PICTURE_LOADER_WORKER_H
|
#define PICTURE_LOADER_WORKER_H
|
||||||
|
|
||||||
|
#include "../../../game/cards/card_database.h"
|
||||||
#include "../../../game/cards/card_info.h"
|
#include "../../../game/cards/card_info.h"
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
#include "picture_to_load.h"
|
#include "picture_to_load.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkDiskCache>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#define REDIRECT_HEADER_NAME "redirects"
|
#define REDIRECT_HEADER_NAME "redirects"
|
||||||
#define REDIRECT_ORIGINAL_URL "original"
|
#define REDIRECT_ORIGINAL_URL "original"
|
||||||
|
|
@ -17,16 +22,23 @@
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||||
|
|
||||||
|
class PictureLoaderWorkerWork;
|
||||||
class PictureLoaderWorker : public QObject
|
class PictureLoaderWorker : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PictureLoaderWorker();
|
explicit PictureLoaderWorker();
|
||||||
~PictureLoaderWorker() override;
|
~PictureLoaderWorker() override;
|
||||||
|
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
|
||||||
void enqueueImageLoad(CardInfoPtr card);
|
void enqueueImageLoad(const CardInfoPtr &card);
|
||||||
void clearNetworkCache();
|
void clearNetworkCache();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
|
||||||
|
void processQueuedRequests();
|
||||||
|
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QStringList md5Blacklist;
|
static QStringList md5Blacklist;
|
||||||
|
|
||||||
|
|
@ -35,6 +47,7 @@ private:
|
||||||
QList<PictureToLoad> loadQueue;
|
QList<PictureToLoad> loadQueue;
|
||||||
QMutex mutex;
|
QMutex mutex;
|
||||||
QNetworkAccessManager *networkManager;
|
QNetworkAccessManager *networkManager;
|
||||||
|
QNetworkDiskCache *cache;
|
||||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||||
QString cacheFilePath; // Path to persistent storage
|
QString cacheFilePath; // Path to persistent storage
|
||||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||||
|
|
@ -42,18 +55,11 @@ private:
|
||||||
PictureToLoad cardBeingLoaded;
|
PictureToLoad cardBeingLoaded;
|
||||||
PictureToLoad cardBeingDownloaded;
|
PictureToLoad cardBeingDownloaded;
|
||||||
bool picDownload, downloadRunning, loadQueueRunning;
|
bool picDownload, downloadRunning, loadQueueRunning;
|
||||||
|
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
|
||||||
|
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
|
||||||
|
QTimer requestTimer; // Timer for processing delayed requests
|
||||||
bool overrideAllCardArtWithPersonalPreference;
|
bool overrideAllCardArtWithPersonalPreference;
|
||||||
void startNextPicDownload();
|
|
||||||
|
|
||||||
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
|
|
||||||
disk, return `false` otherwise.
|
|
||||||
|
|
||||||
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
|
|
||||||
matching image first; otherwise, only the set-based folders are used. */
|
|
||||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
|
|
||||||
|
|
||||||
bool imageIsBlackListed(const QByteArray &);
|
|
||||||
QNetworkReply *makeRequest(const QUrl &url);
|
|
||||||
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||||
void loadRedirectCache();
|
void loadRedirectCache();
|
||||||
|
|
@ -61,18 +67,15 @@ private:
|
||||||
void cleanStaleEntries();
|
void cleanStaleEntries();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void picDownloadFinished(QNetworkReply *reply);
|
|
||||||
void picDownloadFailed();
|
|
||||||
|
|
||||||
void picDownloadChanged();
|
void picDownloadChanged();
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
||||||
public slots:
|
|
||||||
void processLoadQueue();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void startLoadQueue();
|
void startLoadQueue();
|
||||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||||
|
void imageLoadQueued(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
void imageLoadSuccessful(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PICTURE_LOADER_WORKER_H
|
#endif // PICTURE_LOADER_WORKER_H
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
|
|
||||||
|
#include "../../../game/cards/card_database_manager.h"
|
||||||
|
#include "../../../settings/cache_settings.h"
|
||||||
|
#include "picture_loader_worker.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QMovie>
|
||||||
|
#include <QNetworkDiskCache>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
// Card back returned by gatherer when card is not found
|
||||||
|
QStringList PictureLoaderWorkerWork::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||||
|
|
||||||
|
PictureLoaderWorkerWork::PictureLoaderWorkerWork(PictureLoaderWorker *_worker, const CardInfoPtr &toLoad)
|
||||||
|
: QThread(nullptr), worker(_worker), cardToDownload(toLoad)
|
||||||
|
{
|
||||||
|
connect(this, &PictureLoaderWorkerWork::requestImageDownload, worker, &PictureLoaderWorker::queueRequest,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
connect(this, &PictureLoaderWorkerWork::imageLoaded, worker, &PictureLoaderWorker::imageLoadedSuccessfully,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
pictureLoaderThread = new QThread;
|
||||||
|
pictureLoaderThread->start(QThread::LowPriority);
|
||||||
|
moveToThread(pictureLoaderThread);
|
||||||
|
startNextPicDownload();
|
||||||
|
}
|
||||||
|
|
||||||
|
PictureLoaderWorkerWork::~PictureLoaderWorkerWork()
|
||||||
|
{
|
||||||
|
pictureLoaderThread->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PictureLoaderWorkerWork::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
|
||||||
|
{
|
||||||
|
QImage image;
|
||||||
|
QImageReader imgReader;
|
||||||
|
imgReader.setDecideFormatFromContent(true);
|
||||||
|
QList<QString> picsPaths = QList<QString>();
|
||||||
|
QDirIterator it(SettingsCache::instance().getCustomPicsPath(),
|
||||||
|
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 << SettingsCache::instance().getPicsPath() + "/" + setName + "/" + correctedCardname
|
||||||
|
// We no longer store downloaded images there, but don't just ignore
|
||||||
|
// stuff that old versions have put there.
|
||||||
|
<< SettingsCache::instance().getPicsPath() + "/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)) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
||||||
|
imageLoaded(cardToDownload.getCard(), image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
imgReader.setFileName(_picsPath + ".full");
|
||||||
|
if (imgReader.read(&image)) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
|
||||||
|
<< " set: " << setName << "]: Picture.full found on disk.";
|
||||||
|
imageLoaded(cardToDownload.getCard(), image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
imgReader.setFileName(_picsPath + ".xlhq");
|
||||||
|
if (imgReader.read(&image)) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
|
||||||
|
<< " set: " << setName << "]: Picture.xlhq found on disk.";
|
||||||
|
imageLoaded(cardToDownload.getCard(), image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorkerWork::startNextPicDownload()
|
||||||
|
{
|
||||||
|
QString picUrl = cardToDownload.getCurrentUrl();
|
||||||
|
|
||||||
|
if (picUrl.isEmpty()) {
|
||||||
|
downloadRunning = false;
|
||||||
|
picDownloadFailed();
|
||||||
|
} else {
|
||||||
|
QUrl url(picUrl);
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Trying to fetch picture from url "
|
||||||
|
<< url.toDisplayString();
|
||||||
|
emit requestImageDownload(url, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorkerWork::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 (cardToDownload.nextUrl() || cardToDownload.nextSet()) {
|
||||||
|
startNextPicDownload();
|
||||||
|
} else {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Picture NOT found, "
|
||||||
|
<< (picDownload ? "download failed" : "downloads disabled")
|
||||||
|
<< ", no more url combinations to try: BAILING OUT";
|
||||||
|
imageLoaded(cardToDownload.getCard(), QImage());
|
||||||
|
}
|
||||||
|
emit startLoadQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorkerWork::picDownloadFinished(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||||
|
|
||||||
|
if (reply->error()) {
|
||||||
|
if (isFromCache) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Removing corrupted cache file for url "
|
||||||
|
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
|
||||||
|
|
||||||
|
networkManager->cache()->remove(reply->url());
|
||||||
|
|
||||||
|
requestImageDownload(reply->url(), this);
|
||||||
|
} else {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.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();
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: following "
|
||||||
|
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
|
||||||
|
requestImageDownload(redirectUrl, this);
|
||||||
|
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)) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.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(cardToDownload.getCard(), movie.currentImage());
|
||||||
|
logSuccessMessage = true;
|
||||||
|
} else if (imgReader.read(&testImage)) {
|
||||||
|
imageLoaded(cardToDownload.getCard(), testImage);
|
||||||
|
logSuccessMessage = true;
|
||||||
|
} else {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Possible " << (isFromCache ? "cached" : "downloaded")
|
||||||
|
<< " picture at " << reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
||||||
|
|
||||||
|
picDownloadFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logSuccessMessage) {
|
||||||
|
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||||
|
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||||
|
<< " set: " << cardToDownload.getSetName() << "]: Image successfully "
|
||||||
|
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
|
||||||
|
} else {
|
||||||
|
startNextPicDownload();
|
||||||
|
}
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PictureLoaderWorkerWork::imageIsBlackListed(const QByteArray &picData)
|
||||||
|
{
|
||||||
|
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||||
|
return md5Blacklist.contains(md5sum);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
#ifndef PICTURE_LOADER_WORKER_WORK_H
|
||||||
|
#define PICTURE_LOADER_WORKER_WORK_H
|
||||||
|
|
||||||
|
#include "../../../game/cards/card_database.h"
|
||||||
|
#include "picture_loader_worker.h"
|
||||||
|
#include "picture_to_load.h"
|
||||||
|
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#define REDIRECT_HEADER_NAME "redirects"
|
||||||
|
#define REDIRECT_ORIGINAL_URL "original"
|
||||||
|
#define REDIRECT_URL "redirect"
|
||||||
|
#define REDIRECT_TIMESTAMP "timestamp"
|
||||||
|
#define REDIRECT_CACHE_FILENAME "cache.ini"
|
||||||
|
|
||||||
|
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerWorkLog, "picture_loader.worker");
|
||||||
|
|
||||||
|
class PictureLoaderWorker;
|
||||||
|
class PictureLoaderWorkerWork : public QThread
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit PictureLoaderWorkerWork(PictureLoaderWorker *worker, const CardInfoPtr &toLoad);
|
||||||
|
~PictureLoaderWorkerWork() override;
|
||||||
|
PictureLoaderWorker *worker;
|
||||||
|
PictureToLoad cardToDownload;
|
||||||
|
public slots:
|
||||||
|
void picDownloadFinished(QNetworkReply *reply);
|
||||||
|
void picDownloadFailed();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QStringList md5Blacklist;
|
||||||
|
QThread *pictureLoaderThread;
|
||||||
|
QNetworkAccessManager *networkManager;
|
||||||
|
bool picDownload, downloadRunning, loadQueueRunning;
|
||||||
|
void startNextPicDownload();
|
||||||
|
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
|
||||||
|
bool imageIsBlackListed(const QByteArray &);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void startLoadQueue();
|
||||||
|
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||||
|
void requestImageDownload(const QUrl &url, PictureLoaderWorkerWork *instance);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PICTURE_LOADER_WORKER_WORK_H
|
||||||
|
|
@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
|
||||||
popup->addSettingsWidget(toAdd);
|
popup->addSettingsWidget(toAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||||
|
{
|
||||||
|
popup->removeSettingsWidget(toRemove);
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
||||||
{
|
{
|
||||||
button->setIcon(iconMap);
|
button->setIcon(iconMap);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
||||||
void addSettingsWidget(QWidget *toAdd) const;
|
void addSettingsWidget(QWidget *toAdd) const;
|
||||||
|
void removeSettingsWidget(QWidget *toRemove) const;
|
||||||
void setButtonIcon(QPixmap iconMap);
|
void setButtonIcon(QPixmap iconMap);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
@ -25,6 +26,8 @@ private slots:
|
||||||
private:
|
private:
|
||||||
QHBoxLayout *layout;
|
QHBoxLayout *layout;
|
||||||
QToolButton *button;
|
QToolButton *button;
|
||||||
|
|
||||||
|
public:
|
||||||
SettingsPopupWidget *popup;
|
SettingsPopupWidget *popup;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
|
||||||
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||||
|
{
|
||||||
|
containerLayout->removeWidget(toRemove);
|
||||||
|
toRemove->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsPopupWidget::adjustSizeToFitScreen()
|
void SettingsPopupWidget::adjustSizeToFitScreen()
|
||||||
{
|
{
|
||||||
QScreen *screen = QApplication::screenAt(this->pos());
|
QScreen *screen = QApplication::screenAt(this->pos());
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
||||||
void addSettingsWidget(QWidget *toAdd) const;
|
void addSettingsWidget(QWidget *toAdd) const;
|
||||||
|
void removeSettingsWidget(QWidget *toRemove) const;
|
||||||
void adjustSizeToFitScreen();
|
void adjustSizeToFitScreen();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,13 @@ set(oracle_SOURCES
|
||||||
../cockatrice/src/game/cards/card_database_manager.cpp
|
../cockatrice/src/game/cards/card_database_manager.cpp
|
||||||
../cockatrice/src/game/cards/card_info.cpp
|
../cockatrice/src/game/cards/card_info.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_status_bar.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_worker_work.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
|
||||||
|
../cockatrice/src/client/ui/widgets/quick_settings/settings_button_widget.cpp
|
||||||
|
../cockatrice/src/client/ui/widgets/quick_settings/settings_popup_widget.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
|
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
|
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp
|
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue