mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-12 13:00:01 -08:00
[Oracle] Add a tag data step to the import wizard
Insert a Tags page between set selection and the import. It downloads Scryfall's Oracle Tags by default (or a user-supplied file/URL), can include parent tags, and persists tagsenabled, tagssource and tagsparents in oracle.ini. The page loads asynchronously and soft-fails in background mode, so a tag download or parse error never aborts the card database import.
This commit is contained in:
parent
77179f913e
commit
5582284991
4 changed files with 533 additions and 1 deletions
|
|
@ -56,7 +56,8 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
|
||||||
QList<OracleWizardPage *> pages;
|
QList<OracleWizardPage *> pages;
|
||||||
|
|
||||||
if (!isSpoilersOnly) {
|
if (!isSpoilersOnly) {
|
||||||
pages << new IntroPage << new LoadSetsPage << new SaveSetsPage << new LoadTokensPage << new OutroPage;
|
pages << new IntroPage << new LoadSetsPage << new TagsPage << new SaveSetsPage << new LoadTokensPage
|
||||||
|
<< new OutroPage;
|
||||||
} else {
|
} else {
|
||||||
pages << new LoadSpoilersPage << new OutroPage;
|
pages << new LoadSpoilersPage << new OutroPage;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWizard>
|
#include <QWizard>
|
||||||
|
#include <memory>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ class QNetworkAccessManager;
|
||||||
class OracleImporter;
|
class OracleImporter;
|
||||||
class QLocalServer;
|
class QLocalServer;
|
||||||
class QSettings;
|
class QSettings;
|
||||||
|
class TagDatabase;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
||||||
class OracleWizard : public QWizard
|
class OracleWizard : public QWizard
|
||||||
|
|
@ -68,6 +70,8 @@ public:
|
||||||
OracleImporter *importer;
|
OracleImporter *importer;
|
||||||
QSettings *settings;
|
QSettings *settings;
|
||||||
QNetworkAccessManager *nam;
|
QNetworkAccessManager *nam;
|
||||||
|
/** @brief Tag data loaded by the Tags page, consumed by the importer. */
|
||||||
|
std::shared_ptr<TagDatabase> tagDatabase;
|
||||||
bool downloadedPlainXml = false;
|
bool downloadedPlainXml = false;
|
||||||
QByteArray xmlData;
|
QByteArray xmlData;
|
||||||
bool backgroundMode = false;
|
bool backgroundMode = false;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "oracleimporter.h"
|
#include "oracleimporter.h"
|
||||||
#include "oraclewizard.h"
|
#include "oraclewizard.h"
|
||||||
#include "pagetemplates.h"
|
#include "pagetemplates.h"
|
||||||
|
#include "tag_database.h"
|
||||||
#include "version_string.h"
|
#include "version_string.h"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
|
@ -18,7 +19,9 @@
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QJsonParseError>
|
#include <QJsonParseError>
|
||||||
|
#include <QJsonValue>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
|
@ -70,6 +73,7 @@ class QWidget;
|
||||||
#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/"
|
#define MTGJSON_V4_URL_COMPONENT "mtgjson.com/files/"
|
||||||
#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json"
|
#define ALLSETS_URL_FALLBACK "https://www.mtgjson.com/api/v5/AllPrintings.json"
|
||||||
#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json"
|
#define MTGJSON_VERSION_URL "https://www.mtgjson.com/api/v5/Meta.json"
|
||||||
|
#define SCRYFALL_TAGS_URL "https://api.scryfall.com/bulk-data/oracle-tags"
|
||||||
|
|
||||||
#ifdef HAS_LZMA
|
#ifdef HAS_LZMA
|
||||||
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz"
|
#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.xz"
|
||||||
|
|
@ -692,6 +696,456 @@ void LoadSetsPage::importFinished()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief True when a URL points at a tag file rather than Scryfall's bulk metadata.
|
||||||
|
*/
|
||||||
|
bool looksLikeTagFileUrl(const QUrl &url)
|
||||||
|
{
|
||||||
|
const QString path = url.path().toLower();
|
||||||
|
return path.endsWith(".json") || path.endsWith(".jsonl") || path.endsWith(".gz") || path.endsWith(".xz") ||
|
||||||
|
path.endsWith(".zip");
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TagsPage::TagsPage(QWidget *parent) : OracleWizardPage(parent)
|
||||||
|
{
|
||||||
|
enableCheckBox = new QCheckBox(this);
|
||||||
|
enableCheckBox->setChecked(true);
|
||||||
|
connect(enableCheckBox, &QCheckBox::toggled, this, &TagsPage::updateSourceEnabled);
|
||||||
|
|
||||||
|
urlRadioButton = new QRadioButton(this);
|
||||||
|
fileRadioButton = new QRadioButton(this);
|
||||||
|
urlRadioButton->setChecked(true);
|
||||||
|
connect(urlRadioButton, &QRadioButton::toggled, this, &TagsPage::updateSourceEnabled);
|
||||||
|
connect(fileRadioButton, &QRadioButton::toggled, this, &TagsPage::updateSourceEnabled);
|
||||||
|
|
||||||
|
urlLineEdit = new QLineEdit(this);
|
||||||
|
fileLineEdit = new QLineEdit(this);
|
||||||
|
|
||||||
|
urlButton = new QPushButton(this);
|
||||||
|
connect(urlButton, &QPushButton::clicked, this, &TagsPage::actRestoreDefaultSource);
|
||||||
|
|
||||||
|
fileButton = new QPushButton(this);
|
||||||
|
connect(fileButton, &QPushButton::clicked, this, &TagsPage::actChooseFile);
|
||||||
|
|
||||||
|
parentsCheckBox = new QCheckBox(this);
|
||||||
|
|
||||||
|
statusLabel = new QLabel(this);
|
||||||
|
statusLabel->setWordWrap(true);
|
||||||
|
statusLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
|
||||||
|
progressLabel = new QLabel(this);
|
||||||
|
progressBar = new QProgressBar(this);
|
||||||
|
progressLabel->hide();
|
||||||
|
progressBar->hide();
|
||||||
|
|
||||||
|
auto *layout = new QGridLayout(this);
|
||||||
|
layout->addWidget(enableCheckBox, 0, 0, 1, 2);
|
||||||
|
layout->addWidget(urlRadioButton, 1, 0);
|
||||||
|
layout->addWidget(urlLineEdit, 1, 1);
|
||||||
|
layout->addWidget(urlButton, 2, 1, Qt::AlignRight);
|
||||||
|
layout->addWidget(fileRadioButton, 3, 0);
|
||||||
|
layout->addWidget(fileLineEdit, 3, 1);
|
||||||
|
layout->addWidget(fileButton, 4, 1, Qt::AlignRight);
|
||||||
|
layout->addWidget(parentsCheckBox, 5, 0, 1, 2);
|
||||||
|
layout->addWidget(statusLabel, 6, 0, 1, 2);
|
||||||
|
layout->addWidget(progressLabel, 7, 0);
|
||||||
|
layout->addWidget(progressBar, 7, 1);
|
||||||
|
layout->setRowStretch(8, 1);
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
connect(&watcher, &QFutureWatcher<LoadResult>::finished, this, &TagsPage::actLoadFinished);
|
||||||
|
|
||||||
|
updateSourceEnabled();
|
||||||
|
retranslateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::retranslateUi()
|
||||||
|
{
|
||||||
|
setTitle(tr("Tag data"));
|
||||||
|
setSubTitle(tr("Card tags let you search and filter by tag. Oracle downloads Scryfall's Oracle Tags by "
|
||||||
|
"default; you can also point it at your own tag database."));
|
||||||
|
|
||||||
|
enableCheckBox->setText(tr("Download card tags"));
|
||||||
|
enableCheckBox->setToolTip(
|
||||||
|
tr("Apply tag slugs to the card database so cards can be searched and filtered by tag."));
|
||||||
|
urlRadioButton->setText(tr("Download URL:"));
|
||||||
|
urlButton->setText(tr("Restore default URL"));
|
||||||
|
fileRadioButton->setText(tr("Local file:"));
|
||||||
|
fileButton->setText(tr("Choose file..."));
|
||||||
|
parentsCheckBox->setText(tr("Include parent tags"));
|
||||||
|
parentsCheckBox->setToolTip(tr("Also add every parent tag. This makes broad tags like \"animal\" work, "
|
||||||
|
"but adds many tags to each card."));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TagsPage::isComplete() const
|
||||||
|
{
|
||||||
|
return !loadActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::initializePage()
|
||||||
|
{
|
||||||
|
enableCheckBox->setChecked(wizard()->settings->value("tagsenabled", true).toBool());
|
||||||
|
parentsCheckBox->setChecked(wizard()->settings->value("tagsparents", false).toBool());
|
||||||
|
|
||||||
|
const QString source = wizard()->settings->value("tagssource").toString();
|
||||||
|
if (source.isEmpty() || source.startsWith("http://") || source.startsWith("https://")) {
|
||||||
|
urlRadioButton->setChecked(true);
|
||||||
|
urlLineEdit->setText(source.isEmpty() ? QString(SCRYFALL_TAGS_URL) : source);
|
||||||
|
fileLineEdit->clear();
|
||||||
|
} else {
|
||||||
|
fileRadioButton->setChecked(true);
|
||||||
|
fileLineEdit->setText(source);
|
||||||
|
urlLineEdit->setText(SCRYFALL_TAGS_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel->clear();
|
||||||
|
progressLabel->hide();
|
||||||
|
progressBar->hide();
|
||||||
|
updateSourceEnabled();
|
||||||
|
|
||||||
|
if (wizard()->backgroundMode && isEnabled()) {
|
||||||
|
// In background mode runInBackground() calls initializePage() instead of
|
||||||
|
// next(), so a page that has nothing to do must advance itself.
|
||||||
|
if (validatePage()) {
|
||||||
|
emit readyToContinue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TagsPage::selectedSource() const
|
||||||
|
{
|
||||||
|
return urlRadioButton->isChecked() ? urlLineEdit->text().trimmed() : fileLineEdit->text().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::updateSourceEnabled()
|
||||||
|
{
|
||||||
|
const bool enabled = enableCheckBox->isChecked();
|
||||||
|
const bool usingUrl = urlRadioButton->isChecked();
|
||||||
|
const bool usingFile = fileRadioButton->isChecked();
|
||||||
|
|
||||||
|
urlRadioButton->setEnabled(enabled);
|
||||||
|
fileRadioButton->setEnabled(enabled);
|
||||||
|
urlLineEdit->setEnabled(enabled && usingUrl);
|
||||||
|
urlButton->setEnabled(enabled && usingUrl);
|
||||||
|
fileLineEdit->setEnabled(enabled && usingFile);
|
||||||
|
fileButton->setEnabled(enabled && usingFile);
|
||||||
|
parentsCheckBox->setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::saveSettings()
|
||||||
|
{
|
||||||
|
wizard()->settings->setValue("tagsenabled", enableCheckBox->isChecked());
|
||||||
|
wizard()->settings->setValue("tagsparents", parentsCheckBox->isChecked());
|
||||||
|
|
||||||
|
QString source = selectedSource();
|
||||||
|
if (urlRadioButton->isChecked() && source == SCRYFALL_TAGS_URL) {
|
||||||
|
source.clear();
|
||||||
|
}
|
||||||
|
if (source.isEmpty()) {
|
||||||
|
wizard()->settings->remove("tagssource");
|
||||||
|
} else {
|
||||||
|
wizard()->settings->setValue("tagssource", source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TagsPage::validatePage()
|
||||||
|
{
|
||||||
|
if (!enableCheckBox->isChecked()) {
|
||||||
|
wizard()->tagDatabase.reset();
|
||||||
|
saveSettings();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto rejectValidation = [this](const QString &message) {
|
||||||
|
if (wizard()->backgroundMode) {
|
||||||
|
qWarning() << message;
|
||||||
|
wizard()->tagDatabase.reset();
|
||||||
|
emit readyToContinue();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, tr("Error"), message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QString source = selectedSource();
|
||||||
|
const bool includeParents = parentsCheckBox->isChecked();
|
||||||
|
|
||||||
|
// Going back and forth through the wizard must not reload an unchanged source.
|
||||||
|
if (wizard()->tagDatabase && source == loadedSource && includeParents == loadedParents) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlRadioButton->isChecked()) {
|
||||||
|
const QUrl url = QUrl::fromUserInput(source);
|
||||||
|
if (!url.isValid() || url.isEmpty()) {
|
||||||
|
return rejectValidation(tr("The provided tag URL is not valid."));
|
||||||
|
}
|
||||||
|
} else if (source.isEmpty() || !QFile::exists(source)) {
|
||||||
|
return rejectValidation(tr("Please choose an existing tag database file."));
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
loadedSource = source;
|
||||||
|
loadedParents = includeParents;
|
||||||
|
|
||||||
|
wizard()->disableButtons();
|
||||||
|
setEnabled(false);
|
||||||
|
loadActive = true;
|
||||||
|
emit completeChanged();
|
||||||
|
|
||||||
|
progressBar->setRange(0, 0);
|
||||||
|
progressBar->setValue(0);
|
||||||
|
progressLabel->setText(tr("Fetching tag data..."));
|
||||||
|
progressLabel->show();
|
||||||
|
progressBar->show();
|
||||||
|
statusLabel->clear();
|
||||||
|
|
||||||
|
if (urlRadioButton->isChecked()) {
|
||||||
|
const QUrl url = QUrl::fromUserInput(source);
|
||||||
|
if (looksLikeTagFileUrl(url)) {
|
||||||
|
downloadTagFile(url);
|
||||||
|
} else {
|
||||||
|
downloadMetadata(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadFromFile(source);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actChooseFile()
|
||||||
|
{
|
||||||
|
QFileDialog dialog(this, tr("Load tag database"));
|
||||||
|
dialog.setFileMode(QFileDialog::ExistingFile);
|
||||||
|
|
||||||
|
QString extensions = "*.json *.jsonl";
|
||||||
|
#ifdef HAS_ZLIB
|
||||||
|
extensions += " *.gz *.zip";
|
||||||
|
#endif
|
||||||
|
#ifdef HAS_LZMA
|
||||||
|
extensions += " *.xz";
|
||||||
|
#endif
|
||||||
|
dialog.setNameFilter(tr("Tag database (%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));
|
||||||
|
fileRadioButton->setChecked(true);
|
||||||
|
updateSourceEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actRestoreDefaultSource()
|
||||||
|
{
|
||||||
|
urlLineEdit->setText(SCRYFALL_TAGS_URL);
|
||||||
|
urlRadioButton->setChecked(true);
|
||||||
|
updateSourceEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::downloadMetadata(const QUrl &url)
|
||||||
|
{
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
|
activeReply = wizard()->nam->get(request);
|
||||||
|
connect(activeReply, &QNetworkReply::finished, this, &TagsPage::actMetadataFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actMetadataFinished()
|
||||||
|
{
|
||||||
|
auto *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!reply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeReply = nullptr;
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (wizard()->wasCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
failLoad(tr("Network error while fetching tag metadata: %1.").arg(reply->errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError{};
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||||
|
failLoad(tr("The tag metadata could not be read."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject metadata = document.object();
|
||||||
|
QString downloadUrl = metadata.value("jsonl_download_uri").toString();
|
||||||
|
if (downloadUrl.isEmpty()) {
|
||||||
|
downloadUrl = metadata.value("download_uri").toString();
|
||||||
|
}
|
||||||
|
if (downloadUrl.isEmpty()) {
|
||||||
|
failLoad(tr("The tag data source did not provide a download URL."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTagFile(QUrl::fromUserInput(downloadUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::downloadTagFile(const QUrl &url)
|
||||||
|
{
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
|
activeReply = wizard()->nam->get(request);
|
||||||
|
connect(activeReply, &QNetworkReply::finished, this, &TagsPage::actTagFileFinished);
|
||||||
|
connect(activeReply, &QNetworkReply::downloadProgress, this, &TagsPage::actDownloadProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actDownloadProgress(qint64 received, qint64 total)
|
||||||
|
{
|
||||||
|
emitProgress("download", received, total);
|
||||||
|
if (total > 0) {
|
||||||
|
progressBar->setRange(0, static_cast<int>(total));
|
||||||
|
progressBar->setValue(static_cast<int>(received));
|
||||||
|
}
|
||||||
|
progressLabel->setText(tr("Downloading tag data (%1 MB)").arg(received / (1024 * 1024)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actTagFileFinished()
|
||||||
|
{
|
||||||
|
auto *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!reply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeReply = nullptr;
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (wizard()->wasCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
failLoad(tr("Network error while downloading tag data: %1.").arg(reply->errorString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = reply->readAll();
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
failLoad(tr("The tag download was empty."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beginLoad(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::loadFromFile(const QString &path)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
failLoad(tr("Cannot open file '%1'.").arg(path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beginLoad(file.readAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::beginLoad(const QByteArray &data)
|
||||||
|
{
|
||||||
|
progressBar->setRange(0, 0);
|
||||||
|
progressLabel->setText(tr("Reading tag data..."));
|
||||||
|
|
||||||
|
const bool includeParents = parentsCheckBox->isChecked();
|
||||||
|
future = QtConcurrent::run([data, includeParents]() {
|
||||||
|
LoadResult result;
|
||||||
|
result.database = TagDatabase::loadFromData(data, TagDatabase::LoadOptions{.includeParentTags = includeParents},
|
||||||
|
&result.errorMessage);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
watcher.setFuture(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::actLoadFinished()
|
||||||
|
{
|
||||||
|
if (wizard()->wasCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadResult result = watcher.result();
|
||||||
|
|
||||||
|
loadActive = false;
|
||||||
|
emit completeChanged();
|
||||||
|
wizard()->enableButtons();
|
||||||
|
setEnabled(true);
|
||||||
|
progressLabel->hide();
|
||||||
|
progressBar->hide();
|
||||||
|
|
||||||
|
if (!result.database) {
|
||||||
|
wizard()->tagDatabase.reset();
|
||||||
|
failLoad(result.errorMessage.isEmpty() ? tr("The tag data could not be read.") : result.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard()->tagDatabase = result.database;
|
||||||
|
statusLabel->setText(tr("Loaded %1 tags for %2 cards.")
|
||||||
|
.arg(wizard()->tagDatabase->tagCount())
|
||||||
|
.arg(wizard()->tagDatabase->taggedCardCount()));
|
||||||
|
qInfo() << "Loaded tag database:" << wizard()->tagDatabase->tagCount() << "tags for"
|
||||||
|
<< wizard()->tagDatabase->taggedCardCount() << "cards";
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::failLoad(const QString &message)
|
||||||
|
{
|
||||||
|
loadActive = false;
|
||||||
|
emit completeChanged();
|
||||||
|
wizard()->enableButtons();
|
||||||
|
setEnabled(true);
|
||||||
|
progressLabel->hide();
|
||||||
|
progressBar->hide();
|
||||||
|
statusLabel->setText(message);
|
||||||
|
qWarning() << message;
|
||||||
|
|
||||||
|
if (wizard()->backgroundMode) {
|
||||||
|
wizard()->tagDatabase.reset();
|
||||||
|
emit readyToContinue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox::warning(this, tr("Tags not loaded"),
|
||||||
|
message + "\n\n" + tr("The card database will be imported without tags."));
|
||||||
|
wizard()->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::advance()
|
||||||
|
{
|
||||||
|
if (wizard()->backgroundMode) {
|
||||||
|
emit readyToContinue();
|
||||||
|
} else {
|
||||||
|
wizard()->next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsPage::cancelWork()
|
||||||
|
{
|
||||||
|
if (activeReply) {
|
||||||
|
activeReply->abort();
|
||||||
|
activeReply = nullptr;
|
||||||
|
}
|
||||||
|
if (future.isRunning()) {
|
||||||
|
future.cancel();
|
||||||
|
watcher.cancel();
|
||||||
|
future.waitForFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent)
|
||||||
{
|
{
|
||||||
pathLabel = new QLabel(this);
|
pathLabel = new QLabel(this);
|
||||||
|
|
@ -752,6 +1206,10 @@ void SaveSetsPage::initializePage()
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection);
|
connect(&importWatcher, &QFutureWatcher<int>::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection);
|
||||||
|
|
||||||
|
// The Tags page has already loaded (or deliberately skipped) the tag data.
|
||||||
|
// The wizard keeps it alive past this page; the importer only borrows it.
|
||||||
|
wizard()->importer->setTagDatabase(wizard()->tagDatabase);
|
||||||
|
|
||||||
wizard()->disableButtons();
|
wizard()->disableButtons();
|
||||||
importActive = true;
|
importActive = true;
|
||||||
emit completeChanged();
|
emit completeChanged();
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,25 @@
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <memory>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class QCheckBox;
|
class QCheckBox;
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
class QNetworkReply;
|
||||||
class QRadioButton;
|
class QRadioButton;
|
||||||
class QProgressBar;
|
class QProgressBar;
|
||||||
class QTextEdit;
|
class QTextEdit;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QUrl;
|
class QUrl;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
class TagDatabase;
|
||||||
|
|
||||||
class IntroPage : public OracleWizardPage
|
class IntroPage : public OracleWizardPage
|
||||||
{
|
{
|
||||||
|
|
@ -109,6 +113,71 @@ private slots:
|
||||||
void zipDownloadFailed(const QString &message);
|
void zipDownloadFailed(const QString &message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Optional wizard step that loads the tag database used by the importer.
|
||||||
|
*
|
||||||
|
* Reloading is skipped when the source and parent-tag setting are unchanged, so
|
||||||
|
* going back and forth through the wizard does not re-download the tag data.
|
||||||
|
*/
|
||||||
|
class TagsPage : public OracleWizardPage
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TagsPage(QWidget *parent = nullptr);
|
||||||
|
void retranslateUi() override;
|
||||||
|
bool isComplete() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initializePage() override;
|
||||||
|
bool validatePage() override;
|
||||||
|
void cancelWork() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct LoadResult
|
||||||
|
{
|
||||||
|
std::shared_ptr<TagDatabase> database;
|
||||||
|
QString errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
QCheckBox *enableCheckBox;
|
||||||
|
QRadioButton *urlRadioButton;
|
||||||
|
QRadioButton *fileRadioButton;
|
||||||
|
QLineEdit *urlLineEdit;
|
||||||
|
QLineEdit *fileLineEdit;
|
||||||
|
QPushButton *urlButton;
|
||||||
|
QPushButton *fileButton;
|
||||||
|
QCheckBox *parentsCheckBox;
|
||||||
|
QLabel *statusLabel;
|
||||||
|
QLabel *progressLabel;
|
||||||
|
QProgressBar *progressBar;
|
||||||
|
|
||||||
|
QFutureWatcher<LoadResult> watcher;
|
||||||
|
QFuture<LoadResult> future;
|
||||||
|
QPointer<QNetworkReply> activeReply;
|
||||||
|
bool loadActive = false;
|
||||||
|
QString loadedSource;
|
||||||
|
bool loadedParents = false;
|
||||||
|
|
||||||
|
[[nodiscard]] QString selectedSource() const;
|
||||||
|
void updateSourceEnabled();
|
||||||
|
void saveSettings();
|
||||||
|
void beginLoad(const QByteArray &data);
|
||||||
|
void loadFromFile(const QString &path);
|
||||||
|
void downloadMetadata(const QUrl &url);
|
||||||
|
void downloadTagFile(const QUrl &url);
|
||||||
|
void finishLoad();
|
||||||
|
void failLoad(const QString &message);
|
||||||
|
void advance();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void actChooseFile();
|
||||||
|
void actRestoreDefaultSource();
|
||||||
|
void actDownloadProgress(qint64 received, qint64 total);
|
||||||
|
void actMetadataFinished();
|
||||||
|
void actTagFileFinished();
|
||||||
|
void actLoadFinished();
|
||||||
|
};
|
||||||
|
|
||||||
class SaveSetsPage : public OracleWizardPage
|
class SaveSetsPage : public OracleWizardPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue