From 0713cd755b3cd9c73f73d87395c5828ab2ca8cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 22 Feb 2026 10:44:34 +0100 Subject: [PATCH] [PictureLoader] Allow saving downloaded images to local storage and not just the QNetworkManager cache. Took 1 hour 11 minutes Took 4 seconds --- .../src/client/settings/cache_settings.cpp | 17 ++++ .../src/client/settings/cache_settings.h | 14 ++++ .../card_picture_loader.cpp | 82 ++++++++++++++++++- .../card_picture_loader/card_picture_loader.h | 1 + .../widgets/dialogs/dlg_settings.cpp | 25 +++++- .../interface/widgets/dialogs/dlg_settings.h | 4 + 6 files changed, 138 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 30093fd52..4576f0cfa 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -264,6 +264,9 @@ SettingsCache::SettingsCache() networkCacheSize = settings->value("personal/networkCacheSize", NETWORK_CACHE_SIZE_DEFAULT).toInt(); redirectCacheTtl = settings->value("personal/redirectCacheTtl", NETWORK_REDIRECT_CACHE_TTL_DEFAULT).toInt(); + saveCardImagesToLocalStorage = settings->value("personal/saveCardImagesToLocalStorage", true).toBool(); + localCardImageStorageNamingScheme = + settings->value("personal/localCardImageStorageNamingScheme", "{set}/{name}_{collector}_{uuid}.png").toString(); picDownload = settings->value("personal/picturedownload", true).toBool(); showStatusBar = settings->value("personal/showStatusBar", false).toBool(); @@ -1101,6 +1104,20 @@ void SettingsCache::setNetworkRedirectCacheTtl(const int _redirectCacheTtl) emit redirectCacheTtlChanged(redirectCacheTtl); } +void SettingsCache::setSaveCardImagesToLocalStorage(QT_STATE_CHANGED_T _saveCardImagesToLocalStorage) +{ + saveCardImagesToLocalStorage = _saveCardImagesToLocalStorage; + settings->setValue("personal/saveCardImagesToLocalStorage", saveCardImagesToLocalStorage); + emit saveCardImagesToLocalStorageChanged(saveCardImagesToLocalStorage); +} + +void SettingsCache::setLocalCardImageStorageNamingScheme(const QString _localCardImageStorageNamingScheme) +{ + localCardImageStorageNamingScheme = _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 5c5054105..b4436ad39 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -184,6 +184,8 @@ signals: void pixmapCacheSizeChanged(int newSizeInMBs); void networkCacheSizeChanged(int newSizeInMBs); void redirectCacheTtlChanged(int newTtl); + void saveCardImagesToLocalStorageChanged(bool saveCardImagesToLocalStorage); + void localCardImageStorageNamingSchemeChanged(QString localCardImageStorageNamingScheme); void masterVolumeChanged(int value); void chatMentionCompleterChanged(); void downloadSpoilerTimeIndexChanged(); @@ -302,6 +304,8 @@ private: int pixmapCacheSize; int networkCacheSize; int redirectCacheTtl; + bool saveCardImagesToLocalStorage; + QString localCardImageStorageNamingScheme; bool scaleCards; int verticalCardOverlapPercent; bool showMessagePopups; @@ -773,6 +777,14 @@ public: { return redirectCacheTtl; } + [[nodiscard]] bool getSaveCardImagesToLocalStorage() const + { + return saveCardImagesToLocalStorage; + } + [[nodiscard]] QString getLocalCardImageStorageNamingScheme() const + { + return localCardImageStorageNamingScheme; + } [[nodiscard]] bool getScaleCards() const { return scaleCards; @@ -1067,6 +1079,8 @@ public slots: void setPixmapCacheSize(const int _pixmapCacheSize); void setNetworkCacheSizeInMB(const int _networkCacheSize); void setNetworkRedirectCacheTtl(const int _redirectCacheTtl); + void setSaveCardImagesToLocalStorage(QT_STATE_CHANGED_T _saveCardImagesToLocalStorage); + void setLocalCardImageStorageNamingScheme(const QString _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 06a0476c9..ce04abd19 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -149,9 +149,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)) @@ -159,12 +160,18 @@ 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().getSaveCardImagesToLocalStorage()) { + 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. @@ -174,6 +181,75 @@ 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(); + const QString scheme = SettingsCache::instance().getLocalCardImageStorageNamingScheme(); + + if (picsRoot.isEmpty() || scheme.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 = scheme; + + relativePath.replace("{name}", cardName); + relativePath.replace("{set}", setName); + relativePath.replace("{collector}", collectorNumber); + relativePath.replace("{uuid}", uuid); + relativePath.replace("{ext}", "png"); + + // Normalize slashes + relativePath = QDir::cleanPath(relativePath); + + QFileInfo outInfo(baseDir.filePath(relativePath)); + 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; + } + } + + // Do not overwrite existing files + if (outInfo.exists()) { + 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/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index e7286f078..6b8f2bc63 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1063,6 +1063,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() networkRedirectCacheTtlEdit.setSingleStep(1); networkRedirectCacheTtlEdit.setValue(SettingsCache::instance().getRedirectCacheTtl()); + saveCardImagesToLocalStorageCheckBox.setChecked(SettingsCache::instance().getSaveCardImagesToLocalStorage()); + connect(&saveCardImagesToLocalStorageCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setSaveCardImagesToLocalStorage); + + localCardImageStorageNamingSchemeLineEdit = + new QLineEdit(SettingsCache::instance().getLocalCardImageStorageNamingScheme()); + connect(localCardImageStorageNamingSchemeLineEdit, &QLineEdit::textChanged, &SettingsCache::instance(), + &SettingsCache::setLocalCardImageStorageNamingScheme); + auto networkCacheLayout = new QHBoxLayout; networkCacheLayout->addStretch(); networkCacheLayout->addWidget(&networkCacheLabel); @@ -1078,6 +1087,14 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() pixmapCacheLayout->addWidget(&pixmapCacheLabel); pixmapCacheLayout->addWidget(&pixmapCacheEdit); + auto saveCardImagesToLocalStorageLayout = new QHBoxLayout; + saveCardImagesToLocalStorageLayout->addWidget(&saveCardImagesToLocalStorageLabel); + saveCardImagesToLocalStorageLayout->addWidget(&saveCardImagesToLocalStorageCheckBox); + + auto localCardImageStorageNamingSchemeLayout = new QHBoxLayout; + localCardImageStorageNamingSchemeLayout->addWidget(&localCardImageStorageNamingSchemeLabel); + localCardImageStorageNamingSchemeLayout->addWidget(localCardImageStorageNamingSchemeLineEdit); + // Top Layout lpGeneralGrid->addWidget(&picDownloadCheckBox, 0, 0); lpGeneralGrid->addWidget(&resetDownloadURLs, 0, 1); @@ -1085,8 +1102,10 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() 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->addLayout(saveCardImagesToLocalStorageLayout, 4, 0, 1, 2); + lpGeneralGrid->addLayout(localCardImageStorageNamingSchemeLayout, 5, 0, 1, 2); + lpGeneralGrid->addWidget(&urlLinkLabel, 7, 0); + lpGeneralGrid->addWidget(&clearDownloadedPicsButton, 7, 1); // Spoiler Layout lpSpoilerGrid->addWidget(&mcDownloadSpoilersCheckBox, 0, 0); @@ -1298,6 +1317,8 @@ void DeckEditorSettingsPage::retranslateUi() 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")); + saveCardImagesToLocalStorageLabel.setText(tr("Save downloaded images to local storage")); + localCardImageStorageNamingSchemeLabel.setText(tr("Save downloaded images using this naming scheme:")); updateNowButton->setText(tr("Update Spoilers")); aAdd->setText(tr("Add New URL")); aEdit->setText(tr("Edit URL")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index db107c6e2..9bbd700a4 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -237,6 +237,10 @@ private: QSpinBox networkRedirectCacheTtlEdit; QSpinBox pixmapCacheEdit; QLabel pixmapCacheLabel; + QCheckBox saveCardImagesToLocalStorageCheckBox; + QLabel saveCardImagesToLocalStorageLabel; + QLabel localCardImageStorageNamingSchemeLabel; + QLineEdit *localCardImageStorageNamingSchemeLineEdit; }; class MessagesSettingsPage : public AbstractSettingsPage