mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-23 10:05:10 -07:00
[Oracle/Client] Harden oracle progress workers and quit prompt
Address review feedback on the download-progress change: decompress and read sets files off the UI thread, cancel the load/import workers before the wizard can tear down the importer, and show an 'Extracting file...' status plus a clean 100% tail so the poll never looks stuck. Quitting Cockatrice while a card database update runs now asks for confirmation.
This commit is contained in:
parent
c47dc578e0
commit
ababf3d70b
8 changed files with 304 additions and 129 deletions
|
|
@ -844,6 +844,17 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||||
}
|
}
|
||||||
bClosingDown = true;
|
bClosingDown = true;
|
||||||
|
|
||||||
|
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
|
||||||
|
if (QMessageBox::question(this, tr("Are you sure?"),
|
||||||
|
tr("A card database update is still running. Quitting now will cancel it.\n"
|
||||||
|
"Are you sure you want to quit?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
|
||||||
|
event->ignore();
|
||||||
|
bClosingDown = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!tabSupervisor->close()) {
|
if (!tabSupervisor->close()) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
bClosingDown = false;
|
bClosingDown = false;
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,8 @@ int OracleImporter::startImport()
|
||||||
{
|
{
|
||||||
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
|
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
|
||||||
|
|
||||||
|
importCancelled.storeRelease(0);
|
||||||
|
|
||||||
// Pre-allocate the cards hash to avoid rehashing during import. Keys are
|
// Pre-allocate the cards hash to avoid rehashing during import. Keys are
|
||||||
// distinct card names while raw ranges only count printings (AllPrintings
|
// distinct card names while raw ranges only count printings (AllPrintings
|
||||||
// ~100k printings vs ~35k names), so this over-reserves somewhat; an exact
|
// ~100k printings vs ~35k names), so this over-reserves somewhat; an exact
|
||||||
|
|
@ -576,6 +578,12 @@ int OracleImporter::startImport()
|
||||||
int setIndex = 0;
|
int setIndex = 0;
|
||||||
|
|
||||||
for (const SetToDownload &curSetToParse : allSets) {
|
for (const SetToDownload &curSetToParse : allSets) {
|
||||||
|
if (importCancelled.loadAcquire()) {
|
||||||
|
// The wizard was closed mid-import: stop at the next set boundary so
|
||||||
|
// the caller can wait for this future without processing every set.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(),
|
CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(),
|
||||||
curSetToParse.getLongName(), curSetToParse.getSetType(),
|
curSetToParse.getLongName(), curSetToParse.getSetType(),
|
||||||
curSetToParse.getReleaseDate(), curSetToParse.getPriority());
|
curSetToParse.getReleaseDate(), curSetToParse.getPriority());
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "raw_json_scanner.h"
|
#include "raw_json_scanner.h"
|
||||||
|
|
||||||
|
#include <QAtomicInt>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
@ -163,6 +164,13 @@ private:
|
||||||
*/
|
*/
|
||||||
bool progressReporting = true;
|
bool progressReporting = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomic "please stop importing" flag. startImport() checks it between sets
|
||||||
|
* so a wizard being closed mid-import can be torn down without waiting for
|
||||||
|
* the whole import (or racing it).
|
||||||
|
*/
|
||||||
|
QAtomicInt importCancelled;
|
||||||
|
|
||||||
CardInfoPtr addCard(QString name,
|
CardInfoPtr addCard(QString name,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
bool isToken,
|
bool isToken,
|
||||||
|
|
@ -193,6 +201,17 @@ public:
|
||||||
*/
|
*/
|
||||||
bool readSetsFromByteArray(QByteArray data);
|
bool readSetsFromByteArray(QByteArray data);
|
||||||
int startImport();
|
int startImport();
|
||||||
|
/**
|
||||||
|
* @brief Requests an in-flight startImport() to stop at the next set boundary.
|
||||||
|
*
|
||||||
|
* Works by setting an atomic flag that startImport() polls between sets, so
|
||||||
|
* cancelImport() followed by a short waitForFinished() on the running future is
|
||||||
|
* safe the moment the wizard is about to be destroyed.
|
||||||
|
*/
|
||||||
|
void cancelImport()
|
||||||
|
{
|
||||||
|
importCancelled.storeRelease(1);
|
||||||
|
}
|
||||||
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
||||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,17 @@ void OracleWizard::accept()
|
||||||
QDialog::accept();
|
QDialog::accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OracleWizard::reject()
|
||||||
|
{
|
||||||
|
// The wizard is being closed while a page may still run a worker on the
|
||||||
|
// importer. Ask it to stop before the wizard (and the importer child) is
|
||||||
|
// destroyed, so the worker thread never touches freed memory.
|
||||||
|
if (auto *active = dynamic_cast<OracleWizardPage *>(currentPage())) {
|
||||||
|
active->cancelWork();
|
||||||
|
}
|
||||||
|
QWizard::reject();
|
||||||
|
}
|
||||||
|
|
||||||
void OracleWizard::runInBackground()
|
void OracleWizard::runInBackground()
|
||||||
{
|
{
|
||||||
backgroundMode = true;
|
backgroundMode = true;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class OracleWizard : public QWizard
|
||||||
public:
|
public:
|
||||||
explicit OracleWizard(QWidget *parent = nullptr);
|
explicit OracleWizard(QWidget *parent = nullptr);
|
||||||
void accept() override;
|
void accept() override;
|
||||||
|
void reject() override;
|
||||||
void enableButtons();
|
void enableButtons();
|
||||||
void disableButtons();
|
void disableButtons();
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,101 @@ static void emitBackgroundProgress(const char *stage, qint64 done, qint64 total)
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decompresses and dispatches a sets-file payload on a worker thread.
|
||||||
|
*
|
||||||
|
* Iteratively unwraps xz/zip compression, then either hands the JSON to the
|
||||||
|
* importer (which reports scan progress via dataReadProgress) or returns the raw
|
||||||
|
* XML for the plain-XML path. Must never touch the wizard or the page: the caller
|
||||||
|
* consumes the returned LoadSetsResult on the UI thread in importFinished().
|
||||||
|
*/
|
||||||
|
LoadSetsResult loadSetsData(const QPointer<OracleImporter> &importer, QByteArray data)
|
||||||
|
{
|
||||||
|
LoadSetsResult result;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (data.startsWith(XZ_SIGNATURE)) {
|
||||||
|
#ifdef HAS_LZMA
|
||||||
|
QBuffer inBuffer(&data);
|
||||||
|
QByteArray decompressed;
|
||||||
|
QBuffer outBuffer(&decompressed);
|
||||||
|
inBuffer.open(QBuffer::ReadOnly);
|
||||||
|
outBuffer.open(QBuffer::WriteOnly);
|
||||||
|
XzDecompressor xz;
|
||||||
|
if (!xz.decompress(&inBuffer, &outBuffer)) {
|
||||||
|
result.errorMessage = LoadSetsPage::tr("Xz extraction failed.");
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
data = decompressed;
|
||||||
|
continue;
|
||||||
|
#else
|
||||||
|
result.errorMessage =
|
||||||
|
LoadSetsPage::tr("Sorry, this version of Oracle does not support xz compressed files.");
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.startsWith(ZIP_SIGNATURE)) {
|
||||||
|
#ifdef HAS_ZLIB
|
||||||
|
QBuffer inBuffer(&data);
|
||||||
|
UnZip uz;
|
||||||
|
const UnZip::ErrorCode openEc = uz.openArchive(&inBuffer);
|
||||||
|
if (openEc != UnZip::Ok) {
|
||||||
|
result.errorMessage = LoadSetsPage::tr("Failed to open Zip archive: %1.").arg(uz.formatError(openEc));
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (uz.fileList().size() != 1) {
|
||||||
|
result.errorMessage =
|
||||||
|
LoadSetsPage::tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.");
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const QString fileName = uz.fileList().at(0);
|
||||||
|
QByteArray decompressed;
|
||||||
|
QBuffer outBuffer(&decompressed);
|
||||||
|
outBuffer.open(QBuffer::ReadWrite);
|
||||||
|
const UnZip::ErrorCode ec = uz.extractFile(fileName, &outBuffer);
|
||||||
|
uz.closeArchive();
|
||||||
|
if (ec != UnZip::Ok) {
|
||||||
|
result.errorMessage = LoadSetsPage::tr("Zip extraction failed: %1.").arg(uz.formatError(ec));
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
data = decompressed;
|
||||||
|
continue;
|
||||||
|
#else
|
||||||
|
result.errorMessage = LoadSetsPage::tr("Sorry, this version of Oracle does not support zipped files.");
|
||||||
|
result.offerUncompressedFallback = true;
|
||||||
|
return result;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.startsWith("<")) {
|
||||||
|
result.ok = true;
|
||||||
|
result.plainXml = true;
|
||||||
|
result.xmlData = std::move(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.startsWith("{")) {
|
||||||
|
result.ok = importer && importer->readSetsFromByteArray(std::move(data));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.errorMessage = LoadSetsPage::tr("Failed to interpret downloaded data.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
#define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml"
|
#define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml"
|
||||||
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
|
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
|
||||||
|
|
||||||
|
|
@ -297,18 +392,13 @@ bool LoadSetsPage::validatePage()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!setsFile.open(QIODevice::ReadOnly)) {
|
|
||||||
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
wizard()->disableButtons();
|
wizard()->disableButtons();
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
|
|
||||||
wizard()->setCardSourceUrl(setsFile.fileName());
|
wizard()->setCardSourceUrl(setsFile.fileName());
|
||||||
wizard()->setCardSourceVersion("unknown");
|
wizard()->setCardSourceVersion("unknown");
|
||||||
|
|
||||||
readSetsFromByteArray(setsFile.readAll());
|
readSetsFromFile(setsFile.fileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -410,6 +500,7 @@ void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes)
|
||||||
if (totalBytes <= 0) {
|
if (totalBytes <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
progressBar->setRange(0, totalBytes);
|
||||||
progressBar->setValue(bytesRead);
|
progressBar->setValue(bytesRead);
|
||||||
const int percent = static_cast<int>((100.0 * bytesRead) / totalBytes);
|
const int percent = static_cast<int>((100.0 * bytesRead) / totalBytes);
|
||||||
progressLabel->setText(tr("Parsing file (%1%)").arg(percent));
|
progressLabel->setText(tr("Parsing file (%1%)").arg(percent));
|
||||||
|
|
@ -420,120 +511,76 @@ void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes)
|
||||||
emitBackgroundProgress("scan", bytesRead, totalBytes);
|
emitBackgroundProgress("scan", bytesRead, totalBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadSetsPage::readSetsFromByteArray(QByteArray _data)
|
void LoadSetsPage::beginLoadSets(bool compressedFile)
|
||||||
{
|
{
|
||||||
// show an infinite progressbar
|
// Show an infinite progressbar while the worker decompresses; the scan
|
||||||
|
// steals the label via dataReadProgress as soon as it starts.
|
||||||
progressBar->setMaximum(0);
|
progressBar->setMaximum(0);
|
||||||
progressBar->setMinimum(0);
|
progressBar->setMinimum(0);
|
||||||
progressBar->setValue(0);
|
progressBar->setValue(0);
|
||||||
progressLabel->setText(tr("Parsing file"));
|
progressLabel->setText(compressedFile ? tr("Extracting file...") : tr("Parsing file"));
|
||||||
progressLabel->show();
|
progressLabel->show();
|
||||||
progressBar->show();
|
progressBar->show();
|
||||||
|
|
||||||
wizard()->downloadedPlainXml = false;
|
wizard()->downloadedPlainXml = false;
|
||||||
wizard()->xmlData.clear();
|
wizard()->xmlData.clear();
|
||||||
readSetsFromByteArrayRef(_data);
|
|
||||||
|
if (wizard()->backgroundMode) {
|
||||||
|
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::scanProgressToStdout,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
} else {
|
||||||
|
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::updateParsingProgress,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data)
|
void LoadSetsPage::readSetsFromByteArray(QByteArray _data)
|
||||||
{
|
{
|
||||||
// unzip the file if needed
|
const bool compressed = _data.startsWith(XZ_SIGNATURE) || _data.startsWith(ZIP_SIGNATURE);
|
||||||
if (_data.startsWith(XZ_SIGNATURE)) {
|
beginLoadSets(compressed);
|
||||||
#ifdef HAS_LZMA
|
|
||||||
// zipped file
|
// Decompress and scan off the UI thread so a large download can't freeze the window.
|
||||||
auto *inBuffer = new QBuffer(&_data);
|
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||||
auto newData = QByteArray();
|
future = QtConcurrent::run(
|
||||||
auto *outBuffer = new QBuffer(&newData);
|
[importer, data = std::move(_data)]() mutable { return loadSetsData(importer, std::move(data)); });
|
||||||
inBuffer->open(QBuffer::ReadOnly);
|
watcher.setFuture(future);
|
||||||
outBuffer->open(QBuffer::WriteOnly);
|
}
|
||||||
XzDecompressor xz;
|
|
||||||
if (!xz.decompress(inBuffer, outBuffer)) {
|
void LoadSetsPage::readSetsFromFile(const QString &fileName)
|
||||||
zipDownloadFailed(tr("Xz extraction failed."));
|
{
|
||||||
return;
|
// Peek at the header on the UI thread so the status text can distinguish
|
||||||
|
// "Extracting file..." from a plain JSON parse; the full read happens in the worker.
|
||||||
|
QFile headerFile(fileName);
|
||||||
|
bool compressed = false;
|
||||||
|
if (headerFile.open(QIODevice::ReadOnly)) {
|
||||||
|
const QByteArray header = headerFile.read(6);
|
||||||
|
compressed = header.startsWith(XZ_SIGNATURE) || header.startsWith(ZIP_SIGNATURE);
|
||||||
|
}
|
||||||
|
beginLoadSets(compressed);
|
||||||
|
|
||||||
|
// Read, decompress and scan off the UI thread (a plain JSON can be hundreds
|
||||||
|
// of MB, so even the read itself must not block the window).
|
||||||
|
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||||
|
future = QtConcurrent::run([importer, fileName]() mutable -> LoadSetsResult {
|
||||||
|
QFile file(fileName);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
LoadSetsResult readError;
|
||||||
|
readError.errorMessage = LoadSetsPage::tr("Cannot open file '%1'.").arg(fileName);
|
||||||
|
return readError;
|
||||||
}
|
}
|
||||||
_data.clear();
|
return loadSetsData(importer, file.readAll());
|
||||||
readSetsFromByteArrayRef(newData);
|
});
|
||||||
return;
|
watcher.setFuture(future);
|
||||||
#else
|
}
|
||||||
zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files."));
|
|
||||||
|
|
||||||
wizard()->enableButtons();
|
void LoadSetsPage::cancelWork()
|
||||||
setEnabled(true);
|
{
|
||||||
progressLabel->hide();
|
// The scan is short-lived; just wait it out before the wizard (and its
|
||||||
progressBar->hide();
|
// importer) can be torn down underneath the worker thread.
|
||||||
return;
|
if (future.isRunning()) {
|
||||||
#endif
|
future.cancel();
|
||||||
} else if (_data.startsWith(ZIP_SIGNATURE)) {
|
watcher.cancel();
|
||||||
#ifdef HAS_ZLIB
|
future.waitForFinished();
|
||||||
// zipped file
|
|
||||||
auto *inBuffer = new QBuffer(&_data);
|
|
||||||
auto newData = QByteArray();
|
|
||||||
auto *outBuffer = new QBuffer(&newData);
|
|
||||||
QString fileName;
|
|
||||||
UnZip::ErrorCode ec;
|
|
||||||
UnZip uz;
|
|
||||||
|
|
||||||
ec = uz.openArchive(inBuffer);
|
|
||||||
if (ec != UnZip::Ok) {
|
|
||||||
zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uz.fileList().size() != 1) {
|
|
||||||
zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileName = uz.fileList().at(0);
|
|
||||||
|
|
||||||
outBuffer->open(QBuffer::ReadWrite);
|
|
||||||
ec = uz.extractFile(fileName, outBuffer);
|
|
||||||
if (ec != UnZip::Ok) {
|
|
||||||
zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec)));
|
|
||||||
uz.closeArchive();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_data.clear();
|
|
||||||
readSetsFromByteArrayRef(newData);
|
|
||||||
return;
|
|
||||||
#else
|
|
||||||
zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files."));
|
|
||||||
|
|
||||||
wizard()->enableButtons();
|
|
||||||
setEnabled(true);
|
|
||||||
progressLabel->hide();
|
|
||||||
progressBar->hide();
|
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
} else if (_data.startsWith("{")) {
|
|
||||||
if (wizard()->backgroundMode) {
|
|
||||||
qInfo() << tr("Parsing file");
|
|
||||||
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::scanProgressToStdout,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
} else {
|
|
||||||
// Start the computation.
|
|
||||||
progressBar->setRange(0, static_cast<int>(_data.size()));
|
|
||||||
progressBar->setValue(0);
|
|
||||||
progressLabel->setText(tr("Parsing file (0%)"));
|
|
||||||
connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::updateParsingProgress,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPointer<OracleImporter> importer = wizard()->importer;
|
|
||||||
future = QtConcurrent::run([importer, data = std::move(_data)]() mutable {
|
|
||||||
return importer ? importer->readSetsFromByteArray(std::move(data)) : false;
|
|
||||||
});
|
|
||||||
watcher.setFuture(future);
|
|
||||||
} else if (_data.startsWith("<")) {
|
|
||||||
// save xml file and don't do any processing
|
|
||||||
wizard()->downloadedPlainXml = true;
|
|
||||||
wizard()->xmlData = std::move(_data);
|
|
||||||
importFinished();
|
|
||||||
} else {
|
|
||||||
wizard()->enableButtons();
|
|
||||||
setEnabled(true);
|
|
||||||
progressLabel->hide();
|
|
||||||
progressBar->hide();
|
|
||||||
QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -561,24 +608,52 @@ void LoadSetsPage::importFinished()
|
||||||
{
|
{
|
||||||
wizard()->enableButtons();
|
wizard()->enableButtons();
|
||||||
setEnabled(true);
|
setEnabled(true);
|
||||||
progressLabel->hide();
|
|
||||||
progressBar->hide();
|
|
||||||
|
|
||||||
const bool hasData = wizard()->downloadedPlainXml || watcher.future().result();
|
const LoadSetsResult result = watcher.result();
|
||||||
|
|
||||||
|
if (result.plainXml) {
|
||||||
|
wizard()->downloadedPlainXml = true;
|
||||||
|
wizard()->xmlData = result.xmlData;
|
||||||
|
}
|
||||||
|
|
||||||
if (wizard()->backgroundMode) {
|
if (wizard()->backgroundMode) {
|
||||||
if (!hasData) {
|
progressLabel->hide();
|
||||||
|
progressBar->hide();
|
||||||
|
if (!result.errorMessage.isEmpty()) {
|
||||||
|
qWarning() << result.errorMessage;
|
||||||
|
} else if (!result.ok && !result.plainXml) {
|
||||||
qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data.");
|
qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data.");
|
||||||
}
|
}
|
||||||
emit readyToContinue();
|
emit readyToContinue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasData) {
|
const auto fail = [this](const QString &message) {
|
||||||
wizard()->next();
|
progressLabel->hide();
|
||||||
} else {
|
progressBar->hide();
|
||||||
QMessageBox::critical(this, tr("Error"),
|
QMessageBox::critical(this, tr("Error"), message);
|
||||||
tr("The file was retrieved successfully, but it does not contain any sets data."));
|
};
|
||||||
|
|
||||||
|
if (!result.errorMessage.isEmpty()) {
|
||||||
|
if (result.offerUncompressedFallback) {
|
||||||
|
zipDownloadFailed(result.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail(result.errorMessage);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!result.ok && !result.plainXml) {
|
||||||
|
fail(tr("The file was retrieved successfully, but it does not contain any sets data."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the bar to 100% so the tail never looks stuck at 99% while the next
|
||||||
|
// page's own import progress is being set up.
|
||||||
|
progressBar->setMaximum(1);
|
||||||
|
progressBar->setValue(1);
|
||||||
|
progressLabel->setText(tr("Parsing file (100%)"));
|
||||||
|
wizard()->next();
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
||||||
|
|
@ -601,15 +676,15 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
||||||
layout->addWidget(pathLabel, 3, 0);
|
layout->addWidget(pathLabel, 3, 0);
|
||||||
layout->addWidget(defaultPathCheckBox, 4, 0);
|
layout->addWidget(defaultPathCheckBox, 4, 0);
|
||||||
|
|
||||||
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished);
|
|
||||||
|
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveSetsPage::cleanupPage()
|
void SaveSetsPage::cleanupPage()
|
||||||
{
|
{
|
||||||
|
cancelWork();
|
||||||
|
disconnect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress);
|
||||||
|
disconnect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished);
|
||||||
wizard()->importer->clear();
|
wizard()->importer->clear();
|
||||||
disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveSetsPage::initializePage()
|
void SaveSetsPage::initializePage()
|
||||||
|
|
@ -627,21 +702,44 @@ void SaveSetsPage::initializePage()
|
||||||
messageLog->clear();
|
messageLog->clear();
|
||||||
messageLog->show();
|
messageLog->show();
|
||||||
progressBar->show();
|
progressBar->show();
|
||||||
progressBar->setRange(0, wizard()->importer->getSets().size());
|
|
||||||
|
totalSets = wizard()->importer->getSets().size();
|
||||||
|
progressBar->setRange(0, totalSets);
|
||||||
progressBar->setValue(0);
|
progressBar->setValue(0);
|
||||||
|
|
||||||
connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress,
|
connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection);
|
||||||
|
|
||||||
wizard()->disableButtons();
|
wizard()->disableButtons();
|
||||||
|
importActive = true;
|
||||||
|
|
||||||
const QPointer<OracleImporter> importer = wizard()->importer;
|
const QPointer<OracleImporter> importer = wizard()->importer;
|
||||||
importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; });
|
importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; });
|
||||||
importWatcher.setFuture(importFuture);
|
importWatcher.setFuture(importFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SaveSetsPage::cancelWork()
|
||||||
|
{
|
||||||
|
if (!importActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ask the worker to stop at the next set boundary, then wait it out so the
|
||||||
|
// wizard (and the importer it owns) is never torn down under a running thread.
|
||||||
|
importActive = false;
|
||||||
|
wizard()->importer->cancelImport();
|
||||||
|
importFuture.cancel();
|
||||||
|
importWatcher.cancel();
|
||||||
|
importFuture.waitForFinished();
|
||||||
|
}
|
||||||
|
|
||||||
void SaveSetsPage::importFinished()
|
void SaveSetsPage::importFinished()
|
||||||
{
|
{
|
||||||
|
if (!importActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importActive = false;
|
||||||
|
|
||||||
wizard()->enableButtons();
|
wizard()->enableButtons();
|
||||||
|
|
||||||
const int setsImported = importWatcher.result();
|
const int setsImported = importWatcher.result();
|
||||||
|
|
@ -685,20 +783,21 @@ void SaveSetsPage::retranslateUi()
|
||||||
|
|
||||||
void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName)
|
void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName)
|
||||||
{
|
{
|
||||||
const bool background = wizard()->backgroundMode;
|
if (!importActive) {
|
||||||
const int totalSets = wizard()->importer->getSets().size();
|
return;
|
||||||
|
}
|
||||||
if (setName.isEmpty()) {
|
if (setName.isEmpty()) {
|
||||||
progressBar->setValue(progressBar->maximum());
|
progressBar->setValue(progressBar->maximum());
|
||||||
if (background) {
|
const int cardCount = wizard()->importer->getCardList().size();
|
||||||
qInfo() << tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size());
|
if (wizard()->backgroundMode) {
|
||||||
|
qInfo() << tr("Import finished: %1 cards.").arg(cardCount);
|
||||||
emitBackgroundProgress("import", totalSets, totalSets);
|
emitBackgroundProgress("import", totalSets, totalSets);
|
||||||
} else {
|
} else {
|
||||||
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) +
|
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(cardCount) + "</b>");
|
||||||
"</b>");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
progressBar->setValue(setIndex);
|
progressBar->setValue(setIndex);
|
||||||
if (background) {
|
if (wizard()->backgroundMode) {
|
||||||
qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported);
|
qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported);
|
||||||
emitBackgroundProgress("import", setIndex, totalSets);
|
emitBackgroundProgress("import", setIndex, totalSets);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
#include "pagetemplates.h"
|
#include "pagetemplates.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWizard>
|
#include <QWizard>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -57,6 +59,16 @@ protected:
|
||||||
void initializePage() override;
|
void initializePage() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @brief Result of a worker-thread sets-file load (read + decompress + dispatch). */
|
||||||
|
struct LoadSetsResult
|
||||||
|
{
|
||||||
|
bool ok = false; ///< JSON scan produced set data (or plain XML was handled)
|
||||||
|
bool plainXml = false; ///< input was a plain Cockatrice XML database
|
||||||
|
QByteArray xmlData; ///< raw XML for the plain-XML path
|
||||||
|
QString errorMessage; ///< set when the input could not be processed
|
||||||
|
bool offerUncompressedFallback = false; ///< decompression-only failure: offer the uncompressed URL
|
||||||
|
};
|
||||||
|
|
||||||
class LoadSetsPage : public OracleWizardPage
|
class LoadSetsPage : public OracleWizardPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -68,8 +80,9 @@ protected:
|
||||||
void initializePage() override;
|
void initializePage() override;
|
||||||
bool validatePage() override;
|
bool validatePage() override;
|
||||||
void readSetsFromByteArray(QByteArray _data);
|
void readSetsFromByteArray(QByteArray _data);
|
||||||
void readSetsFromByteArrayRef(QByteArray &_data);
|
void readSetsFromFile(const QString &fileName);
|
||||||
void downloadSetsFile(const QUrl &url);
|
void downloadSetsFile(const QUrl &url);
|
||||||
|
void cancelWork() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QRadioButton *urlRadioButton;
|
QRadioButton *urlRadioButton;
|
||||||
|
|
@ -81,8 +94,10 @@ private:
|
||||||
QLabel *progressLabel;
|
QLabel *progressLabel;
|
||||||
QProgressBar *progressBar;
|
QProgressBar *progressBar;
|
||||||
|
|
||||||
QFutureWatcher<bool> watcher;
|
QFutureWatcher<LoadSetsResult> watcher;
|
||||||
QFuture<bool> future;
|
QFuture<LoadSetsResult> future;
|
||||||
|
|
||||||
|
void beginLoadSets(bool compressedFile = false);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void actLoadSetsFile();
|
void actLoadSetsFile();
|
||||||
|
|
@ -111,11 +126,14 @@ private:
|
||||||
|
|
||||||
QFutureWatcher<int> importWatcher;
|
QFutureWatcher<int> importWatcher;
|
||||||
QFuture<int> importFuture;
|
QFuture<int> importFuture;
|
||||||
|
int totalSets = 0;
|
||||||
|
bool importActive = false;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void initializePage() override;
|
void initializePage() override;
|
||||||
void cleanupPage() override;
|
void cleanupPage() override;
|
||||||
bool validatePage() override;
|
bool validatePage() override;
|
||||||
|
void cancelWork() override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void importFinished();
|
void importFinished();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@ public:
|
||||||
}
|
}
|
||||||
virtual void retranslateUi() = 0;
|
virtual void retranslateUi() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Asks an active page to stop any background worker before the wizard
|
||||||
|
* (and its importer) can be torn down underneath it. Default is a no-op.
|
||||||
|
*/
|
||||||
|
virtual void cancelWork()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void readyToContinue();
|
void readyToContinue();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue