[Spoilers] Preserve spoiler files through update and reload failures (#7707)

* Preserve spoiler files and serialize update reloads

* Surface spoiler removal reload failures and allow disabled-state retry

* Address review feedback on spoiler update lifecycle

Only remove the spoiler file when the user unchecks the setting; opening
Settings no longer queues an unrequested reload for a leftover file.
Remove the spoilersUpdatedSuccessfully signal, which had no receiver.

* Fix spoiler settings state after reopen and while disabled

Reopen Settings while an application-owned update is running: the new page
now observes that updater, so the button reads Updating... and re-enables
on completion instead of offering a second update that cancels the first.
Observation does not take ownership: only updaters the page creates are
retired with deleteLater, because the launch check is stack-allocated.

When spoilers are off, the hint now describes what Reload Card Database
actually does, stays readable, and wraps instead of clipping.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
Bryce! 2026-11-04 12:24:19 -05:00 • committed by GitHub
parent 3a656c9c4a
commit 0de92c06f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 648 additions and 229 deletions

View file

@ -8,6 +8,7 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QAction>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
@ -61,7 +62,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
connect(mpSpoilerPathButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::spoilerPathButtonClicked);
updateNowButton = new QPushButton;
updateNowButton->setFixedWidth(150);
updateNowButton->setMinimumWidth(150);
connect(updateNowButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::updateSpoilers);
// Update the GUI depending on if the box is ticked or not
@ -120,12 +121,19 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
lpSpoilerGrid->addWidget(&lastUpdatedLabel, 2, 0);
lpSpoilerGrid->addWidget(updateNowButton, 2, 1);
lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop);
infoOnSpoilersLabel.setWordWrap(true);
// On a change to the checkbox, hide/un-hide the other fields
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(),
&DownloadSettings::setDownloadSpoilerStatus);
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled);
// The updater is application-owned and outlives this page, so a Settings page
// reopened mid-update must observe the operation that is still running.
if (auto *runningUpdater = SpoilerBackgroundUpdater::current()) {
observeSpoilerUpdater(runningUpdater);
}
mpGeneralGroupBox = new QGroupBox;
mpGeneralGroupBox->setLayout(lpGeneralGrid);
@ -328,20 +336,60 @@ void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const
void DeckEditorSettingsPage::updateSpoilers()
{
// Disable the button so the user can only press it once at a time
updateNowButton->setDisabled(true);
updateNowButton->setText(tr("Updating..."));
if (spoilerUpdater) {
return;
}
// Application ownership lets either operation finish after settings closes.
// When disabled, the same button retries file removal and the local reload.
SpoilerBackgroundUpdater *sbu;
if (SettingsCache::instance().downloads().getDownloadSpoilersStatus()) {
sbu = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true);
// Only an updater this page creates is retired by deleteLater: observation
// may attach to the stack-allocated launch updater, which must not be deleted.
connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, sbu, &QObject::deleteLater);
} else {
// deleteSpoilerFile() wires its own deleteLater.
sbu = SpoilerBackgroundUpdater::deleteSpoilerFile();
}
observeSpoilerUpdater(sbu);
}
// Create a new SBU that will act as if the client was just reloaded
auto *sbu = new SpoilerBackgroundUpdater();
connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, &DeckEditorSettingsPage::unlockSettings);
connect(sbu, &SpoilerBackgroundUpdater::spoilersUpdatedSuccessfully, this, &DeckEditorSettingsPage::unlockSettings);
void DeckEditorSettingsPage::observeSpoilerUpdater(SpoilerBackgroundUpdater *sbu)
{
spoilerUpdater = sbu;
unlockSettings();
connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, [this, sbu] {
if (spoilerUpdater == sbu) {
spoilerUpdater.clear();
unlockSettings();
}
});
connect(sbu, &SpoilerBackgroundUpdater::spoilerUpdateFailed, this, [this, sbu](const QString &message) {
if (spoilerUpdater != sbu) {
return;
}
auto *notice = new QMessageBox(QMessageBox::Warning, tr("Spoiler update"), message, QMessageBox::Ok, this);
notice->setTextFormat(Qt::PlainText);
notice->setAttribute(Qt::WA_DeleteOnClose);
notice->open();
});
}
void DeckEditorSettingsPage::unlockSettings()
{
updateNowButton->setDisabled(false);
updateNowButton->setText(tr("Update Spoilers"));
updateNowButton->setEnabled(!spoilerUpdater);
updateNowButton->setText(spoilerUpdater ? tr("Updating...")
: SettingsCache::instance().downloads().getDownloadSpoilersStatus()
? tr("Update Spoilers")
: tr("Reload Card Database"));
lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime());
infoOnSpoilersLabel.setText(
SettingsCache::instance().downloads().getDownloadSpoilersStatus()
? tr("Spoilers download automatically on launch") + "\n" +
tr("Press the button to manually update without relaunching") + "\n\n" +
tr("Updates continue if you close settings")
: tr("Spoilers are off. Reload Card Database removes any downloaded spoiler file and reloads your "
"card data."));
}
QString DeckEditorSettingsPage::getLastUpdateTime()
@ -379,12 +427,17 @@ void DeckEditorSettingsPage::setSpoilersEnabled(bool anInput)
mpSpoilerSavePathLineEdit->setEnabled(anInput);
mpSpoilerPathButton->setEnabled(anInput);
lastUpdatedLabel.setEnabled(anInput);
updateNowButton->setEnabled(anInput);
infoOnSpoilersLabel.setEnabled(anInput);
// The hint explains the button in both states, especially the disabled-state
// Reload Card Database action, so keep it readable while spoilers are off.
infoOnSpoilersLabel.setEnabled(true);
if (!anInput) {
SpoilerBackgroundUpdater::deleteSpoilerFile();
// Only react to the user disabling the setting. The constructor calls this to
// initialize the controls, and a leftover spoiler file is recovered through the
// Reload Card Database button rather than an unrequested reload on page open.
if (!anInput && sender() == &mcDownloadSpoilersCheckBox) {
observeSpoilerUpdater(SpoilerBackgroundUpdater::deleteSpoilerFile());
}
unlockSettings();
}
void DeckEditorSettingsPage::retranslateUi()
@ -394,13 +447,10 @@ void DeckEditorSettingsPage::retranslateUi()
mcDownloadSpoilersCheckBox.setText(tr("Download Spoilers Automatically"));
mcSpoilerSaveLabel.setText(tr("Spoiler Location:"));
lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime());
infoOnSpoilersLabel.setText(tr("Spoilers download automatically on launch") + "\n" +
tr("Press the button to manually update without relaunching") + "\n\n" +
tr("Do not close settings until manual update is complete"));
picDownloadCheckBox.setText(tr("Download card pictures on the fly"));
urlLinkLabel.setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL")));
resetDownloadURLs.setText(tr("Reset Download URLs"));
updateNowButton->setText(tr("Update Spoilers"));
unlockSettings();
aAdd->setText(tr("Add New URL"));
aEdit->setText(tr("Edit URL"));
aRemove->setText(tr("Remove URL"));

View file

@ -5,6 +5,7 @@
#include <QCheckBox>
#include <QLabel>
#include <QPointer>
#include <QPushButton>
#include <QString>
#include <qtmetamacros.h>
@ -15,6 +16,7 @@ class QLineEdit;
class QListWidget;
class QListWidgetItem;
class QModelIndex;
class SpoilerBackgroundUpdater;
class DeckEditorSettingsPage : public AbstractSettingsPage
{
@ -54,6 +56,7 @@ private:
QLabel infoOnSpoilersLabel;
QPushButton *mpSpoilerPathButton;
QPushButton *updateNowButton;
QPointer<SpoilerBackgroundUpdater> spoilerUpdater;
/** @brief Adds a list item for the given URL, storing the raw URL alongside its displayed label. */
QListWidgetItem *addUrlItem(const QString &url);
@ -66,6 +69,8 @@ private:
/** @brief Refreshes the displayed label of every URL item after limits or settings change. */
void refreshUrlItems();
void observeSpoilerUpdater(SpoilerBackgroundUpdater *updater);
};
#endif // COCKATRICE_DECK_EDITOR_SETTINGS_PAGE_H