Cockatrice/oracle/src/pages.cpp
BruebachL 675e1f7447
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
[Translation] Name languages without reading the translation files (#7662)
* [Translation] Name languages without reading the translation files

The language selector derived every label from the language's own .qm file:
it loaded the file and translated the literal "English" in the "i18n"
context. lupdate could never extract that literal from the callers, which
pass the DEFAULT_LANG_NAME macro, so a dummy QT_TRANSLATE_NOOP in
cache_settings.cpp existed to give it something to extract. #7050 deleted
that dummy, the next lupdate push dropped the string from Transifex, and
the following pull rewrote the .ts files without it. lrelease only emits
translated messages, so 22 of 25 .qm files returned an empty name.

The names are data, not translations: a selector has to be readable before
any translation is loaded, and a label must not depend on the translation
of the language it names. TranslationLoader::languageDisplayName now holds
them next to availableLanguages, falling back to Qt's native language name
and then to the code, so a language added later still gets a label.

Also select the configured language by item data instead of by matching the
translated "English" string, which pointed at the wrong row whenever the
lookup came back empty, and restore that English entry's own name: en_US is
"English (United States)" so it can no longer be confused with the
untranslated default.

Oracle shared the code path, so its wizard language page is fixed too.

* [Translation] Name the default language selection once

Review follow-up: log the unresolvable language through the page's own
category, and move the en_US entry both selectors fall back to for an unset
setting into TranslationLoader::defaultLanguageCode.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-26 02:39:26 +01:00

996 lines
31 KiB
C++

#include "pages.h"
#include "client/settings/cache_settings.h"
#include "main.h"
#include "oracleimporter.h"
#include "oraclewizard.h"
#include "pagetemplates.h"
#include "version_string.h"
#include <QBuffer>
#include <QCheckBox>
#include <QComboBox>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QIODevice>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QMap>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QPointer>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QScrollBar>
#include <QSettings>
#include <QSharedPointer>
#include <QTextEdit>
#include <QTextStream>
#include <QTimer>
#include <QUrl>
#include <QVariant>
#include <QWizard>
#include <QtConcurrentRun>
#include <QtGlobal>
#include <cstdio>
#include <functional>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/translation_loader.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qnumeric.h>
#include <stdlib.h>
#include <tuple>
#include <utility>
class QWidget;
#ifdef HAS_LZMA
#include "lzma/decompress.h"
#endif
#ifdef HAS_ZLIB
#include "zip/unzip.h"
#endif
#define ZIP_SIGNATURE "PK"
// Xz stream header: 0xFD + "7zXZ"
#define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A"
#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/"
#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json"
#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json"
#ifdef HAS_LZMA
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz"
#elif defined(HAS_ZLIB)
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.zip"
#else
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json"
#endif
/**
* @brief Emits one machine-readable progress line to stdout.
*
* The hosting Cockatrice client parses these lines to drive a determinate
* progress bar. stderr stays reserved for human-readable log output.
*/
static void emitProgress(const char *stage, qint64 done, qint64 total)
{
QTextStream out(stdout);
out << "PROGRESS " << stage << ' ' << done << ' ' << total << '\n';
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 SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent)
{
label = new QLabel(this);
label->setWordWrap(true);
languageLabel = new QLabel(this);
versionLabel = new QLabel(this);
languageBox = new QComboBox(this);
QStringList languageCodes = findQmFiles();
for (const QString &code : languageCodes) {
languageBox->addItem(TranslationLoader::languageDisplayName(code), code);
}
// Select the language the wizard runs with: its own entry, the base language for a variant without a
// file of its own (de_CH -> de), and the default language for an unset setting, which names no file.
const QString configuredLanguage = SettingsCache::instance().personal().getLang();
int index = languageBox->findData(configuredLanguage);
if (index < 0) {
index = languageBox->findData(configuredLanguage.section('_', 0, 0));
}
if (index < 0) {
index = languageBox->findData(TranslationLoader::defaultLanguageCode());
}
if (index >= 0) {
languageBox->setCurrentIndex(index);
} else {
qWarning() << "could not find language" << configuredLanguage;
}
connect(languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this, &IntroPage::languageBoxChanged);
auto *layout = new QGridLayout(this);
layout->addWidget(label, 0, 0, 1, 2);
layout->addWidget(languageLabel, 1, 0);
layout->addWidget(languageBox, 1, 1);
layout->addWidget(versionLabel, 2, 0, 1, 2);
setLayout(layout);
}
void IntroPage::initializePage()
{
if (wizard()->backgroundMode) {
emit readyToContinue();
}
}
QStringList IntroPage::findQmFiles()
{
return TranslationLoader::availableLanguages(translationPrefix,
TranslationLoader::applicationTranslationPaths(translationPath));
}
void IntroPage::languageBoxChanged(int index)
{
SettingsCache::instance().personal().setLang(languageBox->itemData(index).toString());
}
void IntroPage::retranslateUi()
{
setTitle(tr("Introduction"));
label->setText(tr("This wizard will import the list of sets, cards, and tokens "
"that will be used by Cockatrice."));
languageLabel->setText(tr("Interface language:"));
versionLabel->setText(tr("Version:") + QString(" %1").arg(VERSION_STRING));
}
void OutroPage::retranslateUi()
{
setTitle(tr("Finished"));
setSubTitle(tr("The wizard has finished.") + "<br>" +
tr("You can now start using Cockatrice with the newly updated cards.") + "<br><br>" +
tr("If the card databases don't reload automatically, restart the Cockatrice client."));
}
void OutroPage::initializePage()
{
if (wizard()->backgroundMode) {
wizard()->accept();
exit(0);
}
}
LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent)
{
urlRadioButton = new QRadioButton(this);
fileRadioButton = new QRadioButton(this);
urlLineEdit = new QLineEdit(this);
fileLineEdit = new QLineEdit(this);
progressLabel = new QLabel(this);
progressBar = new QProgressBar(this);
urlRadioButton->setChecked(true);
urlButton = new QPushButton(this);
connect(urlButton, &QPushButton::clicked, this, &LoadSetsPage::actRestoreDefaultUrl);
fileButton = new QPushButton(this);
connect(fileButton, &QPushButton::clicked, this, &LoadSetsPage::actLoadSetsFile);
auto *layout = new QGridLayout(this);
layout->addWidget(urlRadioButton, 0, 0);
layout->addWidget(urlLineEdit, 0, 1);
layout->addWidget(urlButton, 1, 1, Qt::AlignRight);
layout->addWidget(fileRadioButton, 2, 0);
layout->addWidget(fileLineEdit, 2, 1);
layout->addWidget(fileButton, 3, 1, Qt::AlignRight);
layout->addWidget(progressLabel, 4, 0);
layout->addWidget(progressBar, 4, 1);
connect(&watcher, &QFutureWatcher<bool>::finished, this, &LoadSetsPage::importFinished);
setLayout(layout);
}
bool LoadSetsPage::isComplete() const
{
return !loadActive;
}
void LoadSetsPage::initializePage()
{
urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString());
progressLabel->hide();
progressBar->hide();
if (wizard()->backgroundMode) {
if (isEnabled()) {
validatePage();
}
}
}
void LoadSetsPage::retranslateUi()
{
setTitle(tr("Source selection"));
setSubTitle(tr("Please specify a compatible source for the list of sets and cards. "
"You can specify a URL address that will be downloaded or "
"use an existing file from your computer."));
urlRadioButton->setText(tr("Download URL:"));
fileRadioButton->setText(tr("Local file:"));
urlButton->setText(tr("Restore default URL"));
fileButton->setText(tr("Choose file..."));
}
void LoadSetsPage::actRestoreDefaultUrl()
{
urlLineEdit->setText(ALLSETS_URL);
}
void LoadSetsPage::actLoadSetsFile()
{
QFileDialog dialog(this, tr("Load sets file"));
dialog.setFileMode(QFileDialog::ExistingFile);
QString extensions = "*.json *.xml";
#ifdef HAS_ZLIB
extensions += " *.zip";
#endif
#ifdef HAS_LZMA
extensions += " *.xz";
#endif
dialog.setNameFilter(tr("Sets file (%1)").arg(extensions));
if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) {
dialog.selectFile(fileLineEdit->text());
}
if (!dialog.exec()) {
return;
}
fileLineEdit->setText(dialog.selectedFiles().at(0));
}
bool LoadSetsPage::validatePage()
{
// once the import is finished, we call next(); skip validation
if (wizard()->downloadedPlainXml || wizard()->importer->getSets().count() > 0) {
return true;
}
// else, try to import sets
if (urlRadioButton->isChecked()) {
// If a user attempts to download from V4, redirect them to V5
if (urlLineEdit->text().contains(MTGJSON_V4_URL_COMPONENT)) {
actRestoreDefaultUrl();
}
const auto url = QUrl::fromUserInput(urlLineEdit->text());
if (!url.isValid()) {
QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid."));
return false;
}
progressLabel->setText(tr("Downloading (0 MB)"));
// show an infinite progressbar
progressBar->setMaximum(0);
progressBar->setMinimum(0);
progressBar->setValue(0);
progressLabel->show();
progressBar->show();
wizard()->disableButtons();
setEnabled(false);
downloadSetsFile(url);
} else if (fileRadioButton->isChecked()) {
QFile setsFile(fileLineEdit->text());
if (!setsFile.exists()) {
QMessageBox::critical(this, tr("Error"), tr("Please choose a file."));
return false;
}
wizard()->disableButtons();
setEnabled(false);
wizard()->setCardSourceUrl(setsFile.fileName());
wizard()->setCardSourceVersion("unknown");
readSetsFromFile(setsFile.fileName());
}
return false;
}
void LoadSetsPage::downloadSetsFile(const QUrl &url)
{
wizard()->setCardSourceVersion("unknown");
const auto urlString = url.toString();
if (urlString == ALLSETS_URL || urlString == ALLSETS_URL_FALLBACK) {
const auto versionUrl = QUrl::fromUserInput(MTGJSON_VERSION_URL);
QNetworkRequest request = QNetworkRequest(versionUrl);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
auto *versionReply = wizard()->nam->get(request);
connect(versionReply, &QNetworkReply::finished, [this, versionReply]() {
if (versionReply->error() == QNetworkReply::NoError) {
auto data = versionReply->readAll();
QJsonParseError jsonError{};
auto jsonResponse = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error == QJsonParseError::NoError) {
const auto jsonMap = jsonResponse.toVariant().toMap();
auto versionString = jsonMap.value("meta").toMap().value("version").toString();
if (versionString.isEmpty()) {
versionString = "unknown";
}
wizard()->setCardSourceVersion(versionString);
}
}
versionReply->deleteLater();
});
}
wizard()->setCardSourceUrl(url.toString());
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
auto *reply = wizard()->nam->get(request);
connect(reply, &QNetworkReply::finished, this, &LoadSetsPage::actDownloadFinishedSetsFile);
connect(reply, &QNetworkReply::downloadProgress, this, &LoadSetsPage::actDownloadProgressSetsFile);
}
void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total)
{
emitProgress("download", received, total);
if (total > 0) {
progressBar->setMaximum(static_cast<int>(total));
progressBar->setValue(static_cast<int>(received));
}
progressLabel->setText(tr("Downloading (%1 MB)").arg((int)received / (1024 * 1024)));
}
void LoadSetsPage::actDownloadFinishedSetsFile()
{
// check for a reply
auto *reply = dynamic_cast<QNetworkReply *>(sender());
if (wizard()->wasCancelled()) {
reply->deleteLater();
return;
}
auto errorCode = reply->error();
if (errorCode != QNetworkReply::NoError) {
QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString()));
wizard()->enableButtons();
setEnabled(true);
reply->deleteLater();
return;
}
auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302) {
const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
qDebug() << "following redirect url:" << redirectUrl.toString();
downloadSetsFile(redirectUrl);
reply->deleteLater();
return;
}
progressLabel->hide();
progressBar->hide();
// save AllPrintings.json url, but only if the user customized it and download was successful
if (urlLineEdit->text() != QString(ALLSETS_URL)) {
wizard()->settings->setValue("allsetsurl", urlLineEdit->text());
} else {
wizard()->settings->remove("allsetsurl");
}
readSetsFromByteArray(reply->readAll());
reply->deleteLater();
}
void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes)
{
if (totalBytes <= 0) {
return;
}
progressBar->setRange(0, totalBytes);
progressBar->setValue(bytesRead);
const int percent = static_cast<int>((100.0 * bytesRead) / totalBytes);
progressLabel->setText(tr("Parsing file (%1%)").arg(percent));
}
void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes)
{
emitProgress("scan", bytesRead, totalBytes);
}
void LoadSetsPage::beginLoadSets(bool compressedFile)
{
// Show an infinite progressbar while the worker decompresses; the scan
// steals the label via dataReadProgress as soon as it starts.
progressBar->setMaximum(0);
progressBar->setMinimum(0);
progressBar->setValue(0);
progressLabel->setText(compressedFile ? tr("Extracting file...") : tr("Parsing file"));
progressLabel->show();
progressBar->show();
// Keep Next disabled (via completeChanged) until the worker reports in;
// updateButtonStates() re-evaluates button state whenever we re-enable.
loadActive = true;
emit completeChanged();
wizard()->downloadedPlainXml = false;
wizard()->xmlData.clear();
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::readSetsFromByteArray(QByteArray _data)
{
const bool compressed = _data.startsWith(XZ_SIGNATURE) || _data.startsWith(ZIP_SIGNATURE);
beginLoadSets(compressed);
// Decompress and scan off the UI thread so a large download can't freeze the window.
const QPointer<OracleImporter> importer = wizard()->importer;
future = QtConcurrent::run(
[importer, data = std::move(_data)]() mutable { return loadSetsData(importer, std::move(data)); });
watcher.setFuture(future);
}
void LoadSetsPage::readSetsFromFile(const QString &fileName)
{
// 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;
}
return loadSetsData(importer, file.readAll());
});
watcher.setFuture(future);
}
void LoadSetsPage::cancelWork()
{
// The scan is short-lived; just wait it out before the wizard (and its
// importer) can be torn down underneath the worker thread.
if (future.isRunning()) {
future.cancel();
watcher.cancel();
future.waitForFinished();
}
}
void LoadSetsPage::zipDownloadFailed(const QString &message)
{
wizard()->enableButtons();
setEnabled(true);
progressLabel->hide();
progressBar->hide();
QMessageBox::StandardButton reply;
reply = static_cast<QMessageBox::StandardButton>(QMessageBox::question(
this, tr("Error"), message + "<br>" + tr("Do you want to download the uncompressed file instead?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes));
if (reply == QMessageBox::Yes) {
urlRadioButton->setChecked(true);
urlLineEdit->setText(ALLSETS_URL_FALLBACK);
wizard()->next();
}
}
void LoadSetsPage::importFinished()
{
loadActive = false;
emit completeChanged();
wizard()->enableButtons();
setEnabled(true);
const LoadSetsResult result = watcher.result();
if (result.plainXml) {
wizard()->downloadedPlainXml = true;
wizard()->xmlData = result.xmlData;
}
if (wizard()->backgroundMode) {
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.");
}
emit readyToContinue();
return;
}
const auto fail = [this](const QString &message) {
progressLabel->hide();
progressBar->hide();
QMessageBox::critical(this, tr("Error"), message);
};
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% and hold it there for a moment so the completed state
// is actually visible before the next page's own import progress takes over
// (a zero-length deferral can still fire before the repaint is delivered).
progressBar->setMaximum(1);
progressBar->setValue(1);
progressLabel->setText(tr("Parsing file (100%)"));
QTimer::singleShot(500, this, [this] {
if (wizard()->currentPage() == this) {
// Leave the page pristine: hide the completed load bar so a later
// Back from the save page doesn't show stale progress.
progressLabel->hide();
progressBar->hide();
wizard()->next();
}
});
}
SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
{
pathLabel = new QLabel(this);
saveLabel = new QLabel(this);
progressBar = new QProgressBar(this);
progressBar->hide();
defaultPathCheckBox = new QCheckBox(this);
messageLog = new QTextEdit(this);
messageLog->setReadOnly(true);
auto *layout = new QGridLayout(this);
layout->addWidget(progressBar, 0, 0);
layout->addWidget(messageLog, 1, 0);
layout->addWidget(saveLabel, 2, 0);
layout->addWidget(pathLabel, 3, 0);
layout->addWidget(defaultPathCheckBox, 4, 0);
setLayout(layout);
}
bool SaveSetsPage::isComplete() const
{
return !importActive;
}
void SaveSetsPage::cleanupPage()
{
cancelWork();
disconnect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress);
disconnect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished);
wizard()->importer->clear();
}
void SaveSetsPage::initializePage()
{
retranslateUi();
if (wizard()->downloadedPlainXml) {
messageLog->hide();
progressBar->hide();
if (wizard()->backgroundMode) {
emit readyToContinue();
}
return;
}
messageLog->clear();
messageLog->show();
progressBar->show();
totalSets = wizard()->importer->getSets().size();
progressBar->setRange(0, totalSets);
progressBar->setValue(0);
connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress,
Qt::UniqueConnection);
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection);
wizard()->disableButtons();
importActive = true;
emit completeChanged();
const QPointer<OracleImporter> importer = wizard()->importer;
importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; });
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;
emit completeChanged();
wizard()->importer->cancelImport();
importFuture.cancel();
importWatcher.cancel();
importFuture.waitForFinished();
}
void SaveSetsPage::importFinished()
{
if (!importActive) {
return;
}
importActive = false;
emit completeChanged();
wizard()->enableButtons();
const int setsImported = importWatcher.result();
const QPointer<OracleImporter> importer = wizard()->importer;
if (importer) {
importer->releaseSetData();
}
if (wizard()->backgroundMode) {
if (setsImported == 0) {
qWarning() << tr("No set has been imported.");
}
emit readyToContinue();
return;
}
progressBar->setValue(progressBar->maximum());
if (setsImported == 0) {
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
}
}
void SaveSetsPage::retranslateUi()
{
setTitle(tr("Sets imported"));
if (wizard()->downloadedPlainXml) {
setSubTitle(tr("A Cockatrice card database file of %1 MB has been downloaded.")
.arg(qRound(wizard()->xmlData.size() / 1000000.0)));
} else {
setSubTitle(tr("The following sets have been found:"));
}
saveLabel->setText(tr("Press \"Save\" to store the imported cards in the Cockatrice database."));
pathLabel->setText(tr("The card database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getCardDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));
setButtonText(QWizard::NextButton, tr("&Save"));
}
void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName)
{
if (!importActive) {
return;
}
if (setName.isEmpty()) {
progressBar->setValue(progressBar->maximum());
const int cardCount = wizard()->importer->getCardList().size();
if (wizard()->backgroundMode) {
qInfo() << tr("Import finished: %1 cards.").arg(cardCount);
} else {
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(cardCount) + "</b>");
}
emitProgress("import", totalSets, totalSets);
} else {
progressBar->setValue(setIndex);
if (wizard()->backgroundMode) {
qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported);
} else {
messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported));
}
emitProgress("import", setIndex, totalSets);
}
messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum());
}
bool SaveSetsPage::validatePage()
{
QString defaultPath = SettingsCache::instance().getCardDatabasePath();
QString windowName = tr("Save card database");
QString fileType = tr("XML; card database (*.xml)");
QString fileName;
if (defaultPathCheckBox->isChecked()) {
fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType);
} else {
fileName = defaultPath;
}
if (fileName.isEmpty()) {
return false;
}
QFileInfo fi(fileName);
QDir fileDir(fi.path());
if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
return false;
}
if (wizard()->downloadedPlainXml) {
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
qDebug() << "File write (w) failed for" << fileName;
return false;
}
if (file.write(wizard()->xmlData) < 1) {
qDebug() << "File write (w) failed for" << fileName;
return false;
}
wizard()->xmlData.clear();
} else if (!wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(),
wizard()->getCardSourceVersion())) {
QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName));
return false;
}
wizard()->markDatabaseSaved();
return true;
}
void LoadTokensPage::initializePage()
{
SimpleDownloadFilePage::initializePage();
if (wizard()->backgroundMode) {
emit readyToContinue();
}
}
QString LoadTokensPage::getDefaultUrl()
{
return TOKENS_URL;
}
QString LoadTokensPage::getCustomUrlSettingsKey()
{
return "tokensurl";
}
QString LoadTokensPage::getDefaultSavePath()
{
return SettingsCache::instance().getTokenDatabasePath();
}
QString LoadTokensPage::getWindowTitle()
{
return tr("Save token database");
}
QString LoadTokensPage::getFileType()
{
return tr("XML; token database (*.xml)");
}
QString LoadTokensPage::getFilePromptName()
{
return tr("tokens");
}
void LoadTokensPage::retranslateUi()
{
setTitle(tr("Tokens import"));
setSubTitle(tr("Please specify a compatible source for token data."));
urlRadioButton->setText(tr("Download URL:"));
fileRadioButton->setText(tr("Local file:"));
urlButton->setText(tr("Restore default URL"));
fileButton->setText(tr("Choose file..."));
pathLabel->setText(tr("The token database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getTokenDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));
}
QString LoadSpoilersPage::getDefaultUrl()
{
return SPOILERS_URL;
}
QString LoadSpoilersPage::getCustomUrlSettingsKey()
{
return "spoilersurl";
}
QString LoadSpoilersPage::getDefaultSavePath()
{
return SettingsCache::instance().getTokenDatabasePath();
}
QString LoadSpoilersPage::getWindowTitle()
{
return tr("Save spoiler database");
}
QString LoadSpoilersPage::getFileType()
{
return tr("XML; spoiler database (*.xml)");
}
QString LoadSpoilersPage::getFilePromptName()
{
return tr("spoiler");
}
void LoadSpoilersPage::retranslateUi()
{
setTitle(tr("Spoilers import"));
setSubTitle(tr("Please specify a compatible source for spoiler data."));
urlRadioButton->setText(tr("Download URL:"));
fileRadioButton->setText(tr("Local file:"));
urlButton->setText(tr("Restore default URL"));
fileButton->setText(tr("Choose file..."));
pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getSpoilerCardDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));
}