diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index a66897b4a..fc3f7ce31 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -1,5 +1,7 @@ #include "cache_settings.h" +#include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" +#include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" #include "../network/update/client/release_channel.h" #include "card_counter_settings.h" #include "version_string.h" @@ -264,6 +266,16 @@ SettingsCache::SettingsCache() networkCacheSize = settings->value("personal/networkCacheSize", NETWORK_CACHE_SIZE_DEFAULT).toInt(); redirectCacheTtl = settings->value("personal/redirectCacheTtl", NETWORK_REDIRECT_CACHE_TTL_DEFAULT).toInt(); + cardPictureLoaderCacheMethod = + settings + ->value("personal/cardPictureLoaderCacheMethod", + static_cast(CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE)) + .toInt(); + localCardImageStorageNamingScheme = + settings + ->value("personal/localCardImageStorageNamingScheme", + static_cast(CardPictureLoaderLocalSchemes::NamingScheme::Set_Folder_Name_Set_Collector)) + .toInt(); picDownload = settings->value("personal/picturedownload", true).toBool(); showStatusBar = settings->value("personal/showStatusBar", false).toBool(); @@ -1097,6 +1109,13 @@ void SettingsCache::setPixmapCacheSize(const int _pixmapCacheSize) emit pixmapCacheSizeChanged(pixmapCacheSize); } +void SettingsCache::setCardImageCacheMethod(const CardPictureLoaderCacheMethod::CacheMethod _cardImageCachingMethod) +{ + cardPictureLoaderCacheMethod = static_cast(_cardImageCachingMethod); + settings->setValue("personal/cardPictureLoaderCacheMethod", cardPictureLoaderCacheMethod); + emit cardPictureLoaderCacheMethodChanged(cardPictureLoaderCacheMethod); +} + void SettingsCache::setNetworkCacheSizeInMB(const int _networkCacheSize) { networkCacheSize = _networkCacheSize; @@ -1111,6 +1130,14 @@ void SettingsCache::setNetworkRedirectCacheTtl(const int _redirectCacheTtl) emit redirectCacheTtlChanged(redirectCacheTtl); } +void SettingsCache::setLocalCardImageStorageNamingScheme( + const CardPictureLoaderLocalSchemes::NamingScheme _localCardImageStorageNamingScheme) +{ + localCardImageStorageNamingScheme = static_cast(_localCardImageStorageNamingScheme); + settings->setValue("personal/localCardImageStorageNamingScheme", localCardImageStorageNamingScheme); + emit localCardImageStorageNamingSchemeChanged(localCardImageStorageNamingScheme); +} + void SettingsCache::setClientID(const QString &_clientID) { clientID = _clientID; diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index ece61487f..6cb89bb1c 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -7,6 +7,8 @@ #ifndef SETTINGSCACHE_H #define SETTINGSCACHE_H +#include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" +#include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" #include "shortcuts_settings.h" #include @@ -184,6 +186,8 @@ signals: void pixmapCacheSizeChanged(int newSizeInMBs); void networkCacheSizeChanged(int newSizeInMBs); void redirectCacheTtlChanged(int newTtl); + void cardPictureLoaderCacheMethodChanged(int cardPictureLoaderCacheMethod); + void localCardImageStorageNamingSchemeChanged(int localCardImageStorageNamingScheme); void masterVolumeChanged(int value); void chatMentionCompleterChanged(); void downloadSpoilerTimeIndexChanged(); @@ -302,6 +306,8 @@ private: int pixmapCacheSize; int networkCacheSize; int redirectCacheTtl; + int cardPictureLoaderCacheMethod; + int localCardImageStorageNamingScheme; bool scaleCards; int verticalCardOverlapPercent; bool showMessagePopups; @@ -781,6 +787,10 @@ public: { return pixmapCacheSize; } + [[nodiscard]] CardPictureLoaderCacheMethod::CacheMethod getCardPictureLoaderCacheMethod() const + { + return static_cast(cardPictureLoaderCacheMethod); + } [[nodiscard]] int getNetworkCacheSizeInMB() const { return networkCacheSize; @@ -789,6 +799,10 @@ public: { return redirectCacheTtl; } + [[nodiscard]] CardPictureLoaderLocalSchemes::NamingScheme getLocalCardImageStorageNamingScheme() const + { + return static_cast(localCardImageStorageNamingScheme); + } [[nodiscard]] bool getScaleCards() const { return scaleCards; @@ -1093,8 +1107,11 @@ public slots: void setIgnoreUnregisteredUsers(QT_STATE_CHANGED_T _ignoreUnregisteredUsers); void setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignoreUnregisteredUserMessages); void setPixmapCacheSize(const int _pixmapCacheSize); + void setCardImageCacheMethod(CardPictureLoaderCacheMethod::CacheMethod _cardImageCachingMethod); void setNetworkCacheSizeInMB(const int _networkCacheSize); void setNetworkRedirectCacheTtl(const int _redirectCacheTtl); + void setLocalCardImageStorageNamingScheme( + const CardPictureLoaderLocalSchemes::NamingScheme _localCardImageStorageNamingScheme); void setCardScaling(const QT_STATE_CHANGED_T _scaleCards); void setStackCardOverlapPercent(const int _verticalCardOverlapPercent); void setShowMessagePopups(const QT_STATE_CHANGED_T _showMessagePopups); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index dbd51b973..0285b306e 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -150,9 +150,10 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) { + QPixmap finalPixmap; + if (image.isNull()) { qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName(); - QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap()); } else { if (card.getInfo().getUiAttributes().upsideDownArt) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) @@ -160,12 +161,19 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) #else QImage mirrorImage = image.mirrored(true, true); #endif - QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(mirrorImage)); + finalPixmap = QPixmap::fromImage(mirrorImage); } else { - QPixmapCache::insert(card.getPixmapCacheKey(), QPixmap::fromImage(image)); + finalPixmap = QPixmap::fromImage(image); } } + QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap); + + if (SettingsCache::instance().getCardPictureLoaderCacheMethod() == + CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) { + saveCardImageToLocalStorage(card, finalPixmap); + } + // imageLoaded should only be reached if the exactCard isn't already in cache. // (plus there's a deduplication mechanism in CardPictureLoaderWorker) // It should be safe to connect the CardInfo here without worrying about redundant connections. @@ -175,6 +183,88 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) card.emitPixmapUpdated(); } +void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap) +{ + if (pixmap.isNull() || !card) { + return; + } + + const QString picsRoot = SettingsCache::instance().getPicsPath(); + CardPictureLoaderLocalSchemes::NamingScheme scheme = + SettingsCache::instance().getLocalCardImageStorageNamingScheme(); + + QString pattern; + + for (const auto &s : CardPictureLoaderLocalSchemes::exportSchemes()) { + if (s.id == scheme) { + pattern = s.pattern; + break; + } + } + + if (picsRoot.isEmpty() || pattern.isEmpty()) { + return; + } + + // Base directory: /downloadedPics + QDir baseDir(picsRoot); + if (!baseDir.exists("downloadedPics")) { + baseDir.mkpath("downloadedPics"); + } + baseDir.cd("downloadedPics"); + + // Collect card metadata + const QString cardName = card.getInfo().getCorrectedName(); + + QString setName; + QString collectorNumber; + QString uuid; + + PrintingInfo printing = card.getPrinting(); + if (printing.getSet()) { + setName = printing.getSet()->getCorrectedShortName(); + collectorNumber = printing.getProperty("num"); + uuid = printing.getUuid(); + } + + // Build path from scheme + QString relativePath = + CardPictureLoaderLocalSchemes::expandPattern(pattern, cardName, setName, collectorNumber, uuid); + + if (relativePath.isEmpty()) { + return; + } + + // append extension + relativePath += ".png"; + + // Normalize slashes + relativePath = QDir::cleanPath(relativePath); + + QFileInfo outInfo(baseDir.filePath(relativePath)); + + // Do not overwrite existing files + if (outInfo.exists()) { + return; + } + + QDir outDir = outInfo.dir(); + + // Ensure directory exists + if (!outDir.exists()) { + if (!baseDir.mkpath(outDir.path())) { + qCWarning(CardPictureLoaderLog) << "Failed to create directory for downloaded card image:" << outDir.path(); + return; + } + } + + // Save image + QImage image = pixmap.toImage(); + if (!image.save(outInfo.absoluteFilePath(), "PNG")) { + qCWarning(CardPictureLoaderLog) << "Failed to save card image to" << outInfo.absoluteFilePath(); + } +} + void CardPictureLoader::clearPixmapCache() { QPixmapCache::clear(); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 4000fd99a..0c114ae92 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -117,6 +117,7 @@ public slots: * @param image Loaded QImage. */ void imageLoaded(const ExactCard &card, const QImage &image); + void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap); private slots: /** diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_cache_method.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_cache_method.h new file mode 100644 index 000000000..43f42ff12 --- /dev/null +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_cache_method.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H +#define COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H +#include +#include +#include + +namespace CardPictureLoaderCacheMethod +{ +enum class CacheMethod +{ + NETWORK_CACHE, + FILESYSTEM_CACHE +}; + +struct CacheMethodInfo +{ + CacheMethod id; + QString displayName; +}; + +static inline const QList methods() +{ + static QList all = { + {CacheMethod::NETWORK_CACHE, QCoreApplication::translate("CardPictureLoaderCacheMethod", "Network Cache")}, + {CacheMethod::FILESYSTEM_CACHE, + QCoreApplication::translate("CardPictureLoaderCacheMethod", "Filesystem Cache")}, + }; + return all; +} +} // namespace CardPictureLoaderCacheMethod + +#endif // COCKATRICE_CARD_PICTURE_LOADER_CACHE_METHOD_H diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp index dd00e6e1d..bb10d1c42 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp @@ -1,6 +1,7 @@ #include "card_picture_loader_local.h" #include "../../client/settings/cache_settings.h" +#include "card_picture_loader_local_schemes.h" #include "card_picture_to_load.h" #include @@ -77,26 +78,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName, imgReader.setDecideFormatFromContent(true); // Most-to-least specific, these will fall through in order. - QStringList nameVariants; - - // cardName_providerId - if (!providerId.isEmpty()) { - nameVariants << QString("%1-%2").arg(correctedCardName, providerId) - << QString("%1_%2").arg(correctedCardName, providerId); - } - // cardName_setName_collectorNumber & setName-collectorNumber-cardName - if (!setName.isEmpty() && !collectorNumber.isEmpty()) { - nameVariants << QString("%1_%2_%3").arg(correctedCardName, setName, collectorNumber) - << QString("%1-%2-%3").arg(setName, collectorNumber, correctedCardName); - } - // cardName_setName - if (!setName.isEmpty()) { - nameVariants << QString("%1_%2").arg(correctedCardName, setName) - << QString("%1-%2").arg(setName, correctedCardName); - } - - // cardName - nameVariants << correctedCardName; + QStringList nameVariants = + CardPictureLoaderLocalSchemes::generateImportVariants(correctedCardName, setName, collectorNumber, providerId); for (const QString &nameVariant : nameVariants) { if (nameVariant.isEmpty()) { diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local_schemes.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local_schemes.h new file mode 100644 index 000000000..cad7d2d5f --- /dev/null +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local_schemes.h @@ -0,0 +1,109 @@ +#ifndef COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H +#define COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H + +#include +#include +#include +#include + +namespace CardPictureLoaderLocalSchemes +{ + +enum class NamingScheme +{ + NameOnly, + Name_Set, + Name_Set_Collector, + Set_Collector_Name, + Name_ProviderId, + Set_Folder_Name_ProviderId, + Set_Folder_Name_Set_Collector +}; + +struct NamingSchemeInfo +{ + NamingScheme id; + QString displayName; + QString pattern; +}; + +inline const QList &importSchemes() +{ + static QList list = { + {NamingScheme::Name_ProviderId, "Card Name + Provider ID", "{name}_{providerId}"}, + {NamingScheme::Name_Set_Collector, "Card Name + Set + Collector", "{name}_{set}_{collector}"}, + {NamingScheme::Set_Collector_Name, "Set + Collector + Card Name", "{set}_{collector}_{name}"}, + {NamingScheme::Name_Set, "Card Name + Set", "{name}_{set}"}, + {NamingScheme::NameOnly, "Card Name", "{name}"}, + }; + return list; +} + +inline const QList &exportSchemes() +{ + static QList list = { + {NamingScheme::Set_Folder_Name_ProviderId, "Set Folder / Name + Provider ID", "{set}/{name}_{providerId}"}, + {NamingScheme::Set_Folder_Name_Set_Collector, "Set Folder / Name + Set Name + Collector", + "{set}/{name}_{set}_{collector}"}, + {NamingScheme::Name_ProviderId, "Card Name + Provider ID", "{name}_{providerId}"}, + {NamingScheme::Name_Set_Collector, "Card Name + Set + Collector", "{name}_{set}_{collector}"}, + {NamingScheme::Set_Collector_Name, "Set + Collector + Card Name", "{set}_{collector}_{name}"}, + }; + return list; +} + +inline QString expandPattern(const QString &pattern, + const QString &name, + const QString &set, + const QString &collector, + const QString &providerId) +{ + QString result = pattern; + + auto replaceIfPresent = [&](const QString &token, const QString &value) -> bool { + if (!result.contains(token)) + return true; + + if (value.isEmpty()) + return false; + + result.replace(token, value); + return true; + }; + + if (!replaceIfPresent("{name}", name)) + return {}; + if (!replaceIfPresent("{set}", set)) + return {}; + if (!replaceIfPresent("{collector}", collector)) + return {}; + if (!replaceIfPresent("{providerId}", providerId)) + return {}; + + return result; +} + +inline QStringList +generateImportVariants(const QString &name, const QString &set, const QString &collector, const QString &providerId) +{ + QStringList variants; + const QStringList separators = {"_", "-"}; + + for (const auto &scheme : importSchemes()) { + for (const QString &sep : separators) { + + QString pattern = scheme.pattern; + pattern.replace("_", sep); + + QString v = expandPattern(pattern, name, set, collector, providerId); + if (!v.isEmpty()) + variants << v; + } + } + + return variants; +} + +} // namespace CardPictureLoaderLocalSchemes + +#endif // COCKATRICE_CARD_PICTURE_LOADER_LOCAL_SCHEMES_H \ No newline at end of file diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index a246d74f2..a0f000139 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -26,10 +26,14 @@ CardPictureLoaderWorker::CardPictureLoaderWorker() cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setMaximumCacheSize(1024L * 1024L * static_cast(SettingsCache::instance().getNetworkCacheSizeInMB())); - // Note: the settings is in MB, but QNetworkDiskCache uses bytes - connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this, - [this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast(newSizeInMB)); }); + + connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, [this](int newSizeInMB) { + if (cache) + cache->setMaximumCacheSize(1024L * 1024L * static_cast(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); @@ -65,14 +69,19 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor QUrl cachedRedirect = getCachedRedirect(url); if (!cachedRedirect.isEmpty()) { queueRequest(cachedRedirect, worker); - } else if (cache->metaData(url).isValid()) { - // If we hit a cached url, we get to make the request for free, since it won't contribute towards the rate-limit - makeRequest(url, worker); - } else { - requestLoadQueue.append(qMakePair(url, worker)); - emit imageRequestQueued(url, worker->cardToDownload.getCard(), worker->cardToDownload.getSetName()); - processQueuedRequests(); + return; } + if (SettingsCache::instance().getCardPictureLoaderCacheMethod() == + CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE && + cache->metaData(url).isValid()) { + // If we hit a cached url, we get to make the request for free, since it won't contribute towards the + // rate-limit + makeRequest(url, worker); + return; + } + requestLoadQueue.append(qMakePair(url, worker)); + emit imageRequestQueued(url, worker->cardToDownload.getCard(), worker->cardToDownload.getSetName()); + processQueuedRequests(); } QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker) @@ -87,9 +96,12 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8"); - if (!picDownload) { - req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache); - } + + bool useNetworkCache = !picDownload && SettingsCache::instance().getCardPictureLoaderCacheMethod() == + CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE; + + req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, + useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork); QNetworkReply *reply = networkManager->get(req); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 6238bc80b..eed9acf39 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1003,8 +1003,6 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); urlLinkLabel.setOpenExternalLinks(true); - connect(&clearDownloadedPicsButton, &QPushButton::clicked, this, - &DeckEditorSettingsPage::clearDownloadedPicsButtonClicked); connect(&resetDownloadURLs, &QPushButton::clicked, this, &DeckEditorSettingsPage::resetDownloadedURLsButtonClicked); auto *lpGeneralGrid = new QGridLayout; @@ -1056,49 +1054,11 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() urlListLayout->addWidget(urlToolBar); urlListLayout->addWidget(urlList); - // pixmap cache - pixmapCacheEdit.setMinimum(PIXMAPCACHE_SIZE_MIN); - // 2047 is the max value to avoid overflowing of QPixmapCache::setCacheLimit(int size) - pixmapCacheEdit.setMaximum(PIXMAPCACHE_SIZE_MAX); - pixmapCacheEdit.setSingleStep(64); - pixmapCacheEdit.setValue(SettingsCache::instance().getPixmapCacheSize()); - pixmapCacheEdit.setSuffix(" MB"); - - networkCacheEdit.setMinimum(NETWORK_CACHE_SIZE_MIN); - networkCacheEdit.setMaximum(NETWORK_CACHE_SIZE_MAX); - networkCacheEdit.setSingleStep(1); - networkCacheEdit.setValue(SettingsCache::instance().getNetworkCacheSizeInMB()); - networkCacheEdit.setSuffix(" MB"); - - networkRedirectCacheTtlEdit.setMinimum(NETWORK_REDIRECT_CACHE_TTL_MIN); - networkRedirectCacheTtlEdit.setMaximum(NETWORK_REDIRECT_CACHE_TTL_MAX); - networkRedirectCacheTtlEdit.setSingleStep(1); - networkRedirectCacheTtlEdit.setValue(SettingsCache::instance().getRedirectCacheTtl()); - - auto networkCacheLayout = new QHBoxLayout; - networkCacheLayout->addStretch(); - networkCacheLayout->addWidget(&networkCacheLabel); - networkCacheLayout->addWidget(&networkCacheEdit); - - auto networkRedirectCacheLayout = new QHBoxLayout; - networkRedirectCacheLayout->addStretch(); - networkRedirectCacheLayout->addWidget(&networkRedirectCacheTtlLabel); - networkRedirectCacheLayout->addWidget(&networkRedirectCacheTtlEdit); - - auto pixmapCacheLayout = new QHBoxLayout; - pixmapCacheLayout->addStretch(); - pixmapCacheLayout->addWidget(&pixmapCacheLabel); - pixmapCacheLayout->addWidget(&pixmapCacheEdit); - // Top Layout lpGeneralGrid->addWidget(&picDownloadCheckBox, 0, 0); lpGeneralGrid->addWidget(&resetDownloadURLs, 0, 1); lpGeneralGrid->addLayout(urlListLayout, 1, 0, 1, 2); - lpGeneralGrid->addLayout(networkCacheLayout, 2, 1); - lpGeneralGrid->addLayout(networkRedirectCacheLayout, 3, 0); - lpGeneralGrid->addLayout(pixmapCacheLayout, 3, 1); - lpGeneralGrid->addWidget(&urlLinkLabel, 5, 0); - lpGeneralGrid->addWidget(&clearDownloadedPicsButton, 5, 1); + lpGeneralGrid->addWidget(&urlLinkLabel, 4, 0); // Spoiler Layout lpSpoilerGrid->addWidget(&mcDownloadSpoilersCheckBox, 0, 0); @@ -1113,12 +1073,6 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance(), &SettingsCache::setDownloadSpoilerStatus); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); - connect(&pixmapCacheEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), - &SettingsCache::setPixmapCacheSize); - connect(&networkCacheEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), - &SettingsCache::setNetworkCacheSizeInMB); - connect(&networkRedirectCacheTtlEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), - &SettingsCache::setNetworkRedirectCacheTtl); mpGeneralGroupBox = new QGroupBox; mpGeneralGroupBox->setLayout(lpGeneralGrid); @@ -1144,46 +1098,6 @@ void DeckEditorSettingsPage::resetDownloadedURLsButtonClicked() QMessageBox::information(this, tr("Success"), tr("Download URLs have been reset.")); } -void DeckEditorSettingsPage::clearDownloadedPicsButtonClicked() -{ - CardPictureLoader::clearNetworkCache(); - - // These are not used anymore, but we don't delete them automatically, so - // we should do it here lest we leave pictures hanging around on users' - // machines. - QString picsPath = SettingsCache::instance().getPicsPath() + "/downloadedPics/"; - QStringList dirs = QDir(picsPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot); - bool outerSuccessRemove = true; - for (const auto &dir : dirs) { - QString currentPath = picsPath + dir + "/"; - QStringList files = QDir(currentPath).entryList(QDir::Files); - bool innerSuccessRemove = true; - for (int j = 0; j < files.length(); j++) { - if (!QDir(currentPath).remove(files.at(j))) { - qInfo() << "Failed to remove " + currentPath.toUtf8() + files.at(j).toUtf8(); - outerSuccessRemove = false; - innerSuccessRemove = false; - } - qInfo() << "Removed " << currentPath << files.at(j); - } - - if (innerSuccessRemove) { - bool success = QDir(picsPath).rmdir(dir); - if (!success) { - qInfo() << "Failed to remove inner directory" << picsPath; - } else { - qInfo() << "Removed" << currentPath; - } - } - } - if (outerSuccessRemove) { - QMessageBox::information(this, tr("Success"), tr("Downloaded card pictures have been reset.")); - QDir(SettingsCache::instance().getPicsPath()).rmdir("downloadedPics"); - } else { - QMessageBox::critical(this, tr("Error"), tr("One or more downloaded card pictures could not be cleared.")); - } -} - void DeckEditorSettingsPage::actAddURL() { bool ok; @@ -1302,18 +1216,245 @@ void DeckEditorSettingsPage::retranslateUi() tr("Do not close settings until manual update is complete")); picDownloadCheckBox.setText(tr("Download card pictures on the fly")); urlLinkLabel.setText(QString("%2").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL"))); - clearDownloadedPicsButton.setText(tr("Delete Downloaded Images")); resetDownloadURLs.setText(tr("Reset Download URLs")); + updateNowButton->setText(tr("Update Spoilers")); + aAdd->setText(tr("Add New URL")); + aEdit->setText(tr("Edit URL")); + aRemove->setText(tr("Remove URL")); +} + +StorageSettingsPage::StorageSettingsPage() +{ + auto *lpNetworkCacheGrid = new QGridLayout; + auto *lpImageBackupGrid = new QGridLayout; + auto *lpPixmapCacheGrid = new QGridLayout; + + networkCacheExplainerLabel.setWordWrap(true); + imageBackupExplainerLabel.setWordWrap(true); + pixmapCacheExplainerLabel.setWordWrap(true); + + connect(&clearDownloadedPicsButton, &QPushButton::clicked, this, + &StorageSettingsPage::clearDownloadedPicsButtonClicked); + + connect(&clearPixmapCacheButton, &QPushButton::clicked, this, &StorageSettingsPage::clearPixmapCacheButtonClicked); + + // pixmap cache + pixmapCacheEdit.setMinimum(PIXMAPCACHE_SIZE_MIN); + // 2047 is the max value to avoid overflowing of QPixmapCache::setCacheLimit(int size) + pixmapCacheEdit.setMaximum(PIXMAPCACHE_SIZE_MAX); + pixmapCacheEdit.setSingleStep(64); + pixmapCacheEdit.setValue(SettingsCache::instance().getPixmapCacheSize()); + pixmapCacheEdit.setSuffix(" MB"); + + // Caching method + + cardPictureLoaderCacheMethodComboBox = new QComboBox; + for (auto method : CardPictureLoaderCacheMethod::methods()) { + cardPictureLoaderCacheMethodComboBox->addItem(method.displayName, static_cast(method.id)); + } + + int currentCacheMethod = static_cast(SettingsCache::instance().getCardPictureLoaderCacheMethod()); + + int currentIndex = cardPictureLoaderCacheMethodComboBox->findData(currentCacheMethod); + if (currentIndex >= 0) { + cardPictureLoaderCacheMethodComboBox->setCurrentIndex(currentIndex); + } + + connect(cardPictureLoaderCacheMethodComboBox, qOverload(&QComboBox::currentIndexChanged), this, + [this](int index) { + auto cacheMethod = static_cast( + cardPictureLoaderCacheMethodComboBox->itemData(index).toInt()); + + bool useNetworkCache = (cacheMethod == CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE); + + if (useNetworkCache) { + clearImageBackupsButtonClicked(); + } else { + clearDownloadedPicsButtonClicked(); + } + + mpNetworkCacheGroupBox->setEnabled(useNetworkCache); + mpImageBackupGroupBox->setEnabled(!useNetworkCache); + + SettingsCache::instance().setCardImageCacheMethod(cacheMethod); + }); + + // Network Cache + + networkCacheEdit.setMinimum(NETWORK_CACHE_SIZE_MIN); + networkCacheEdit.setMaximum(NETWORK_CACHE_SIZE_MAX); + networkCacheEdit.setSingleStep(1); + networkCacheEdit.setValue(SettingsCache::instance().getNetworkCacheSizeInMB()); + networkCacheEdit.setSuffix(" MB"); + + networkRedirectCacheTtlEdit.setMinimum(NETWORK_REDIRECT_CACHE_TTL_MIN); + networkRedirectCacheTtlEdit.setMaximum(NETWORK_REDIRECT_CACHE_TTL_MAX); + networkRedirectCacheTtlEdit.setSingleStep(1); + networkRedirectCacheTtlEdit.setValue(SettingsCache::instance().getRedirectCacheTtl()); + + // Image Backup + localCardImageStorageNamingSchemeComboBox = new QComboBox; + for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) { + localCardImageStorageNamingSchemeComboBox->addItem(scheme.displayName, static_cast(scheme.id)); + } + + int current = static_cast(SettingsCache::instance().getLocalCardImageStorageNamingScheme()); + + int index = localCardImageStorageNamingSchemeComboBox->findData(current); + if (index >= 0) { + localCardImageStorageNamingSchemeComboBox->setCurrentIndex(index); + } + + connect(localCardImageStorageNamingSchemeComboBox, qOverload(&QComboBox::currentIndexChanged), this, + [this](int index) { + auto scheme = static_cast( + localCardImageStorageNamingSchemeComboBox->itemData(index).toInt()); + SettingsCache::instance().setLocalCardImageStorageNamingScheme(scheme); + }); + + connect(&clearBackupsButton, &QPushButton::clicked, this, &StorageSettingsPage::clearImageBackupsButtonClicked); + + auto cacheMethodLayout = new QHBoxLayout; + cacheMethodLayout->addWidget(&cardPictureLoaderCacheMethodLabel); + cacheMethodLayout->addWidget(cardPictureLoaderCacheMethodComboBox); + + auto networkCacheLayout = new QHBoxLayout; + networkCacheLayout->addWidget(&clearDownloadedPicsButton); + networkCacheLayout->addStretch(); + networkCacheLayout->addWidget(&networkCacheLabel); + networkCacheLayout->addWidget(&networkCacheEdit); + + auto networkRedirectCacheLayout = new QHBoxLayout; + networkRedirectCacheLayout->addStretch(); + networkRedirectCacheLayout->addWidget(&networkRedirectCacheTtlLabel); + networkRedirectCacheLayout->addWidget(&networkRedirectCacheTtlEdit); + + auto pixmapCacheLayout = new QHBoxLayout; + pixmapCacheLayout->addWidget(&clearPixmapCacheButton); + pixmapCacheLayout->addStretch(); + pixmapCacheLayout->addWidget(&pixmapCacheLabel); + pixmapCacheLayout->addWidget(&pixmapCacheEdit); + + lpNetworkCacheGrid->addWidget(&networkCacheExplainerLabel, 0, 0); + lpNetworkCacheGrid->addLayout(networkCacheLayout, 1, 0); + lpNetworkCacheGrid->addLayout(networkRedirectCacheLayout, 2, 0); + + // Image Backup Layout + lpImageBackupGrid->addWidget(&imageBackupExplainerLabel, 0, 0, 1, 2); + lpImageBackupGrid->addWidget(&localCardImageStorageNamingSchemeLabel, 1, 0); + lpImageBackupGrid->addWidget(localCardImageStorageNamingSchemeComboBox, 1, 1); + lpImageBackupGrid->addWidget(&clearBackupsButton, 2, 0); + + lpPixmapCacheGrid->addWidget(&pixmapCacheExplainerLabel, 0, 0); + lpPixmapCacheGrid->addLayout(pixmapCacheLayout, 1, 0); + + connect(&pixmapCacheEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), + &SettingsCache::setPixmapCacheSize); + connect(&networkCacheEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), + &SettingsCache::setNetworkCacheSizeInMB); + connect(&networkRedirectCacheTtlEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), + &SettingsCache::setNetworkRedirectCacheTtl); + + mpCacheMethodGroupBox = new QGroupBox; + mpCacheMethodGroupBox->setLayout(cacheMethodLayout); + + mpNetworkCacheGroupBox = new QGroupBox; + mpNetworkCacheGroupBox->setLayout(lpNetworkCacheGrid); + + mpImageBackupGroupBox = new QGroupBox; + mpImageBackupGroupBox->setLayout(lpImageBackupGrid); + + mpPixmapCacheGroupBox = new QGroupBox; + mpPixmapCacheGroupBox->setLayout(lpPixmapCacheGrid); + + auto *lpMainLayout = new QVBoxLayout; + + lpMainLayout->addWidget(mpCacheMethodGroupBox); + lpMainLayout->addWidget(mpNetworkCacheGroupBox); + lpMainLayout->addWidget(mpImageBackupGroupBox); + lpMainLayout->addWidget(mpPixmapCacheGroupBox); + + setLayout(lpMainLayout); + + bool useNetworkCache = SettingsCache::instance().getCardPictureLoaderCacheMethod() == + CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE; + + mpNetworkCacheGroupBox->setEnabled(useNetworkCache); + mpImageBackupGroupBox->setEnabled(!useNetworkCache); + + connect(&SettingsCache::instance(), &SettingsCache::langChanged, this, &StorageSettingsPage::retranslateUi); + retranslateUi(); +} + +void StorageSettingsPage::clearDownloadedPicsButtonClicked() +{ + CardPictureLoader::clearNetworkCache(); + CardPictureLoader::clearPixmapCache(); + QMessageBox::information(this, tr("Success"), tr("Cached card pictures have been reset.")); +} + +void StorageSettingsPage::clearImageBackupsButtonClicked() +{ + QString picsPath = SettingsCache::instance().getPicsPath() + "/downloadedPics"; + + QDir dir(picsPath); + bool success = dir.removeRecursively(); + + CardPictureLoader::clearPixmapCache(); + + if (success) { + QMessageBox::information(this, tr("Success"), tr("Downloaded card pictures have been reset.")); + } else { + QMessageBox::critical(this, tr("Error"), tr("One or more downloaded card pictures could not be cleared.")); + } +} + +void StorageSettingsPage::clearPixmapCacheButtonClicked() +{ + CardPictureLoader::clearPixmapCache(); + QMessageBox::information(this, tr("Success"), tr("In-memory (currently loaded) card pictures have been reset.")); +} + +void StorageSettingsPage::retranslateUi() +{ + cardPictureLoaderCacheMethodLabel.setText(tr("Card Picture Loader Caching Method:")); + + networkCacheExplainerLabel.setText( + tr("The network cache is the preferred way of storing images. Downloaded images " + "are stored here until the size of the cache exceeds the configured size. Cockatrice automatically monitors " + "this cache and deletes the least recently seen card images to ensure the cache does not exceed the " + "configured size.")); + imageBackupExplainerLabel.setText( + tr("Writing card images directly to a folder on your hard drive is another way " + "of storing images. This does not change how Cockatrice accesses or downloads " + "images. Cockatrice will NOT automatically monitor and clear this folder, so if you enable this option, it " + "is up to you to ensure sufficient available space. It should also be noted that if a provider outage " + "causes you to download the wrong picture (i.e. wrong printing) you will be stuck with it until you " + "manually delete the file, as opposed to using the network cache, which automatically rotates and thus " + "correct errors after a while.")); + pixmapCacheExplainerLabel.setText( + tr("This is the in-memory picture cache used by the application at runtime. It determines how much memory " + "(RAM) Cockatrice can use before it has to fetch card images from the hard disk again. Increasing this will " + "allow more card images to be displayed at once but shouldn't be necessary. Clearing this will make " + "Cockatrice reload all images from the network cache or the disk.")); + + clearDownloadedPicsButton.setText(tr("Delete Cached Images")); + clearBackupsButton.setText(tr("Delete Saved Images")); + clearPixmapCacheButton.setText(tr("Clear In-Memory Images")); + + mpCacheMethodGroupBox->setTitle(tr("Card Picture Loader Cache Method")); + mpNetworkCacheGroupBox->setTitle(tr("Network Cache")); + mpImageBackupGroupBox->setTitle(tr("Image Backup")); + mpPixmapCacheGroupBox->setTitle(tr("In-Memory Picture Cache")); + networkCacheLabel.setText(tr("Network Cache Size:")); networkCacheEdit.setToolTip(tr("On-disk cache for downloaded pictures")); networkRedirectCacheTtlLabel.setText(tr("Redirect Cache TTL:")); networkRedirectCacheTtlEdit.setToolTip(tr("How long cached redirects for urls are valid for.")); pixmapCacheLabel.setText(tr("Picture Cache Size:")); pixmapCacheEdit.setToolTip(tr("In-memory cache for pictures not currently on screen")); - updateNowButton->setText(tr("Update Spoilers")); - aAdd->setText(tr("Add New URL")); - aEdit->setText(tr("Edit URL")); - aRemove->setText(tr("Remove URL")); + localCardImageStorageNamingSchemeLabel.setText(tr("Naming scheme:")); + networkRedirectCacheTtlEdit.setSuffix(" " + tr("Day(s)")); } @@ -1789,6 +1930,7 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage)); pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage)); pagesWidget->addWidget(new DeckEditorSettingsPage); + pagesWidget->addWidget(new StorageSettingsPage); pagesWidget->addWidget(new MessagesSettingsPage); pagesWidget->addWidget(new SoundSettingsPage); pagesWidget->addWidget(new ShortcutSettingsPage); @@ -1837,6 +1979,11 @@ void DlgSettings::createIcons() deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); deckEditorButton->setIcon(QPixmap("theme:config/deckeditor")); + storageButton = new QListWidgetItem(contentsWidget); + storageButton->setTextAlignment(Qt::AlignHCenter); + storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + storageButton->setIcon(QPixmap("theme:config/deckeditor")); + messagesButton = new QListWidgetItem(contentsWidget); messagesButton->setTextAlignment(Qt::AlignHCenter); messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index b655a30bc..42268e997 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -213,11 +213,9 @@ private slots: void actAddURL(); void actRemoveURL(); void actEditURL(); - void clearDownloadedPicsButtonClicked(); void resetDownloadedURLsButtonClicked(); private: - QPushButton clearDownloadedPicsButton; QPushButton resetDownloadURLs; QLabel urlLinkLabel; QCheckBox picDownloadCheckBox; @@ -227,18 +225,51 @@ private: QLabel msDownloadSpoilersLabel; QGroupBox *mpGeneralGroupBox; QGroupBox *mpSpoilerGroupBox; + QLineEdit *mpSpoilerSavePathLineEdit; QLabel mcSpoilerSaveLabel; QLabel lastUpdatedLabel; QLabel infoOnSpoilersLabel; QPushButton *mpSpoilerPathButton; QPushButton *updateNowButton; +}; + +class StorageSettingsPage : public AbstractSettingsPage +{ + Q_OBJECT +public: + StorageSettingsPage(); + void retranslateUi() override; + +private slots: + void clearDownloadedPicsButtonClicked(); + void clearImageBackupsButtonClicked(); + void clearPixmapCacheButtonClicked(); + +private: + QPushButton clearDownloadedPicsButton; + QPushButton clearBackupsButton; + QPushButton clearPixmapCacheButton; + + QGroupBox *mpCacheMethodGroupBox; + QGroupBox *mpNetworkCacheGroupBox; + QGroupBox *mpImageBackupGroupBox; + QGroupBox *mpPixmapCacheGroupBox; + + QLabel networkCacheExplainerLabel; + QLabel imageBackupExplainerLabel; + QLabel pixmapCacheExplainerLabel; + + QLabel cardPictureLoaderCacheMethodLabel; + QComboBox *cardPictureLoaderCacheMethodComboBox; QLabel networkCacheLabel; QSpinBox networkCacheEdit; QLabel networkRedirectCacheTtlLabel; QSpinBox networkRedirectCacheTtlEdit; QSpinBox pixmapCacheEdit; QLabel pixmapCacheLabel; + QLabel localCardImageStorageNamingSchemeLabel; + QComboBox *localCardImageStorageNamingSchemeComboBox; }; class MessagesSettingsPage : public AbstractSettingsPage @@ -353,8 +384,8 @@ private slots: private: QListWidget *contentsWidget; QStackedWidget *pagesWidget; - QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *messagesButton, - *soundButton, *shortcutsButton; + QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, + *messagesButton, *soundButton, *shortcutsButton; void createIcons(); void retranslateUi();