Rework curseforge download (#611)
* Use the bulk endpoint on mod resolution for faster download * Search on modrinth for api blocked mods * Display a dialog for manually downloading blocked mods
This commit is contained in:
parent
fcbe233fdb
commit
699ad316f0
@ -128,6 +128,8 @@ set(NET_SOURCES
|
|||||||
net/PasteUpload.h
|
net/PasteUpload.h
|
||||||
net/Sink.h
|
net/Sink.h
|
||||||
net/Validator.h
|
net/Validator.h
|
||||||
|
net/Upload.cpp
|
||||||
|
net/Upload.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# Game launch logic
|
# Game launch logic
|
||||||
@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/dialogs/SkinUploadDialog.h
|
ui/dialogs/SkinUploadDialog.h
|
||||||
ui/dialogs/ModDownloadDialog.cpp
|
ui/dialogs/ModDownloadDialog.cpp
|
||||||
ui/dialogs/ModDownloadDialog.h
|
ui/dialogs/ModDownloadDialog.h
|
||||||
|
ui/dialogs/ScrollMessageBox.cpp
|
||||||
|
ui/dialogs/ScrollMessageBox.h
|
||||||
|
|
||||||
# GUI - widgets
|
# GUI - widgets
|
||||||
ui/widgets/Common.cpp
|
ui/widgets/Common.cpp
|
||||||
@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI
|
|||||||
ui/dialogs/LoginDialog.ui
|
ui/dialogs/LoginDialog.ui
|
||||||
ui/dialogs/EditAccountDialog.ui
|
ui/dialogs/EditAccountDialog.ui
|
||||||
ui/dialogs/ReviewMessageBox.ui
|
ui/dialogs/ReviewMessageBox.ui
|
||||||
|
ui/dialogs/ScrollMessageBox.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
qt5_add_resources(LAUNCHER_RESOURCES
|
qt5_add_resources(LAUNCHER_RESOURCES
|
||||||
|
@ -60,9 +60,9 @@
|
|||||||
#include "net/ChecksumValidator.h"
|
#include "net/ChecksumValidator.h"
|
||||||
|
|
||||||
#include "ui/dialogs/CustomMessageBox.h"
|
#include "ui/dialogs/CustomMessageBox.h"
|
||||||
|
#include "ui/dialogs/ScrollMessageBox.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iterator>
|
|
||||||
|
|
||||||
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
|
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
|
||||||
{
|
{
|
||||||
@ -394,61 +394,136 @@ void InstanceImportTask::processFlame()
|
|||||||
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
|
||||||
{
|
{
|
||||||
auto results = m_modIdResolver->getResults();
|
auto results = m_modIdResolver->getResults();
|
||||||
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
|
//first check for blocked mods
|
||||||
for(const auto& result: results.files)
|
QString text;
|
||||||
{
|
auto anyBlocked = false;
|
||||||
QString filename = result.fileName;
|
for(const auto& result: results.files.values()) {
|
||||||
if(!result.required)
|
if (!result.resolved || result.url.isEmpty()) {
|
||||||
{
|
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
|
||||||
filename += ".disabled";
|
anyBlocked = true;
|
||||||
}
|
|
||||||
|
|
||||||
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
|
|
||||||
auto path = FS::PathCombine(m_stagingPath , relpath);
|
|
||||||
|
|
||||||
switch(result.type)
|
|
||||||
{
|
|
||||||
case Flame::File::Type::Folder:
|
|
||||||
{
|
|
||||||
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
|
|
||||||
// fall-through intentional, we treat these as plain old mods and dump them wherever.
|
|
||||||
}
|
|
||||||
case Flame::File::Type::SingleFile:
|
|
||||||
case Flame::File::Type::Mod:
|
|
||||||
{
|
|
||||||
qDebug() << "Will download" << result.url << "to" << path;
|
|
||||||
auto dl = Net::Download::makeFile(result.url, path);
|
|
||||||
m_filesNetJob->addNetAction(dl);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Flame::File::Type::Modpack:
|
|
||||||
logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
|
|
||||||
break;
|
|
||||||
case Flame::File::Type::Cmod2:
|
|
||||||
case Flame::File::Type::Ctoc:
|
|
||||||
case Flame::File::Type::Unknown:
|
|
||||||
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_modIdResolver.reset();
|
if(anyBlocked) {
|
||||||
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
|
qWarning() << "Blocked mods found, displaying mod list";
|
||||||
{
|
|
||||||
m_filesNetJob.reset();
|
auto message_dialog = new ScrollMessageBox(m_parent,
|
||||||
emitSucceeded();
|
tr("Blocked mods found"),
|
||||||
|
tr("The following mods were blocked on third party launchers.<br/>"
|
||||||
|
"You will need to manually download them and add them to the modpack"),
|
||||||
|
text);
|
||||||
|
message_dialog->setModal(true);
|
||||||
|
message_dialog->show();
|
||||||
|
connect(message_dialog, &QDialog::rejected, [&]() {
|
||||||
|
m_modIdResolver.reset();
|
||||||
|
emitFailed("Canceled");
|
||||||
|
});
|
||||||
|
connect(message_dialog, &QDialog::accepted, [&]() {
|
||||||
|
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
|
||||||
|
for (const auto &result: m_modIdResolver->getResults().files) {
|
||||||
|
QString filename = result.fileName;
|
||||||
|
if (!result.required) {
|
||||||
|
filename += ".disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
|
||||||
|
auto path = FS::PathCombine(m_stagingPath, relpath);
|
||||||
|
|
||||||
|
switch (result.type) {
|
||||||
|
case Flame::File::Type::Folder: {
|
||||||
|
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
|
||||||
|
// fall-through intentional, we treat these as plain old mods and dump them wherever.
|
||||||
|
}
|
||||||
|
case Flame::File::Type::SingleFile:
|
||||||
|
case Flame::File::Type::Mod: {
|
||||||
|
if (!result.url.isEmpty()) {
|
||||||
|
qDebug() << "Will download" << result.url << "to" << path;
|
||||||
|
auto dl = Net::Download::makeFile(result.url, path);
|
||||||
|
m_filesNetJob->addNetAction(dl);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Flame::File::Type::Modpack:
|
||||||
|
logWarning(
|
||||||
|
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
|
||||||
|
relpath));
|
||||||
|
break;
|
||||||
|
case Flame::File::Type::Cmod2:
|
||||||
|
case Flame::File::Type::Ctoc:
|
||||||
|
case Flame::File::Type::Unknown:
|
||||||
|
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_modIdResolver.reset();
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
|
||||||
|
m_filesNetJob.reset();
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
|
||||||
|
m_filesNetJob.reset();
|
||||||
|
emitFailed(reason);
|
||||||
|
});
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
|
||||||
|
setProgress(current, total);
|
||||||
|
});
|
||||||
|
setStatus(tr("Downloading mods..."));
|
||||||
|
m_filesNetJob->start();
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
//TODO extract to function ?
|
||||||
|
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
|
||||||
|
for (const auto &result: m_modIdResolver->getResults().files) {
|
||||||
|
QString filename = result.fileName;
|
||||||
|
if (!result.required) {
|
||||||
|
filename += ".disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
|
||||||
|
auto path = FS::PathCombine(m_stagingPath, relpath);
|
||||||
|
|
||||||
|
switch (result.type) {
|
||||||
|
case Flame::File::Type::Folder: {
|
||||||
|
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
|
||||||
|
// fall-through intentional, we treat these as plain old mods and dump them wherever.
|
||||||
|
}
|
||||||
|
case Flame::File::Type::SingleFile:
|
||||||
|
case Flame::File::Type::Mod: {
|
||||||
|
if (!result.url.isEmpty()) {
|
||||||
|
qDebug() << "Will download" << result.url << "to" << path;
|
||||||
|
auto dl = Net::Download::makeFile(result.url, path);
|
||||||
|
m_filesNetJob->addNetAction(dl);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Flame::File::Type::Modpack:
|
||||||
|
logWarning(
|
||||||
|
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
|
||||||
|
relpath));
|
||||||
|
break;
|
||||||
|
case Flame::File::Type::Cmod2:
|
||||||
|
case Flame::File::Type::Ctoc:
|
||||||
|
case Flame::File::Type::Unknown:
|
||||||
|
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_modIdResolver.reset();
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
|
||||||
|
m_filesNetJob.reset();
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
|
||||||
|
m_filesNetJob.reset();
|
||||||
|
emitFailed(reason);
|
||||||
|
});
|
||||||
|
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
|
||||||
|
setProgress(current, total);
|
||||||
|
});
|
||||||
|
setStatus(tr("Downloading mods..."));
|
||||||
|
m_filesNetJob->start();
|
||||||
}
|
}
|
||||||
);
|
|
||||||
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
|
|
||||||
{
|
|
||||||
m_filesNetJob.reset();
|
|
||||||
emitFailed(reason);
|
|
||||||
});
|
|
||||||
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
|
|
||||||
{
|
|
||||||
setProgress(current, total);
|
|
||||||
});
|
|
||||||
setStatus(tr("Downloading mods..."));
|
|
||||||
m_filesNetJob->start();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
|
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
|
||||||
@ -524,11 +599,11 @@ void InstanceImportTask::processModrinth()
|
|||||||
|
|
||||||
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
|
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
|
||||||
bool had_optional = false;
|
bool had_optional = false;
|
||||||
for (auto& obj : jsonFiles) {
|
for (auto& modInfo : jsonFiles) {
|
||||||
Modrinth::File file;
|
Modrinth::File file;
|
||||||
file.path = Json::requireString(obj, "path");
|
file.path = Json::requireString(modInfo, "path");
|
||||||
|
|
||||||
auto env = Json::ensureObject(obj, "env");
|
auto env = Json::ensureObject(modInfo, "env");
|
||||||
QString support = Json::ensureString(env, "client", "unsupported");
|
QString support = Json::ensureString(env, "client", "unsupported");
|
||||||
if (support == "unsupported") {
|
if (support == "unsupported") {
|
||||||
continue;
|
continue;
|
||||||
@ -546,7 +621,7 @@ void InstanceImportTask::processModrinth()
|
|||||||
file.path += ".disabled";
|
file.path += ".disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject hashes = Json::requireObject(obj, "hashes");
|
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
|
||||||
QString hash;
|
QString hash;
|
||||||
QCryptographicHash::Algorithm hashAlgorithm;
|
QCryptographicHash::Algorithm hashAlgorithm;
|
||||||
hash = Json::ensureString(hashes, "sha1");
|
hash = Json::ensureString(hashes, "sha1");
|
||||||
@ -566,7 +641,7 @@ void InstanceImportTask::processModrinth()
|
|||||||
file.hashAlgorithm = hashAlgorithm;
|
file.hashAlgorithm = hashAlgorithm;
|
||||||
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
|
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
|
||||||
// (as Modrinth seems to incorrectly handle spaces)
|
// (as Modrinth seems to incorrectly handle spaces)
|
||||||
file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path);
|
file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path);
|
||||||
if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
|
if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
|
||||||
throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
|
throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include "settings/SettingsObject.h"
|
#include "settings/SettingsObject.h"
|
||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
|
#include "modplatform/flame/PackManifest.h"
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <nonstd/optional>
|
||||||
|
|
||||||
@ -59,6 +60,10 @@ public:
|
|||||||
|
|
||||||
bool canAbort() const override { return true; }
|
bool canAbort() const override { return true; }
|
||||||
bool abort() override;
|
bool abort() override;
|
||||||
|
const QVector<Flame::File> &getBlockedFiles() const
|
||||||
|
{
|
||||||
|
return m_blockedMods;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
//! Entry point for tasks.
|
//! Entry point for tasks.
|
||||||
@ -87,6 +92,7 @@ private: /* data */
|
|||||||
std::unique_ptr<QuaZip> m_packZip;
|
std::unique_ptr<QuaZip> m_packZip;
|
||||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
||||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
||||||
|
QVector<Flame::File> m_blockedMods;
|
||||||
enum class ModpackType{
|
enum class ModpackType{
|
||||||
Unknown,
|
Unknown,
|
||||||
MultiMC,
|
MultiMC,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
#include "FileResolvingTask.h"
|
#include "FileResolvingTask.h"
|
||||||
#include "Json.h"
|
|
||||||
|
|
||||||
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
|
#include "Json.h"
|
||||||
|
#include "net/Upload.h"
|
||||||
|
|
||||||
|
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
|
||||||
: m_network(network), m_toProcess(toProcess)
|
: m_network(network), m_toProcess(toProcess)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
|
|||||||
setStatus(tr("Resolving mod IDs..."));
|
setStatus(tr("Resolving mod IDs..."));
|
||||||
setProgress(0, m_toProcess.files.size());
|
setProgress(0, m_toProcess.files.size());
|
||||||
m_dljob = new NetJob("Mod id resolver", m_network);
|
m_dljob = new NetJob("Mod id resolver", m_network);
|
||||||
results.resize(m_toProcess.files.size());
|
result.reset(new QByteArray());
|
||||||
int index = 0;
|
//build json data to send
|
||||||
for (auto& file : m_toProcess.files) {
|
QJsonObject object;
|
||||||
auto projectIdStr = QString::number(file.projectId);
|
|
||||||
auto fileIdStr = QString::number(file.fileId);
|
object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
|
||||||
QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
|
l.push_back(s.fileId);
|
||||||
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
|
return l;
|
||||||
m_dljob->addNetAction(dl);
|
}));
|
||||||
index++;
|
QByteArray data = Json::toText(object);
|
||||||
}
|
auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
|
||||||
|
m_dljob->addNetAction(dl);
|
||||||
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
|
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
|
||||||
m_dljob->start();
|
m_dljob->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Flame::FileResolvingTask::netJobFinished()
|
void Flame::FileResolvingTask::netJobFinished()
|
||||||
{
|
{
|
||||||
bool failed = false;
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (auto& bytes : results) {
|
// job to check modrinth for blocked projects
|
||||||
auto& out = m_toProcess.files[index];
|
auto job = new NetJob("Modrinth check", m_network);
|
||||||
|
blockedProjects = QMap<File *,QByteArray *>();
|
||||||
|
auto doc = Json::requireDocument(*result);
|
||||||
|
auto array = Json::requireArray(doc.object()["data"]);
|
||||||
|
for (QJsonValueRef file : array) {
|
||||||
|
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
|
||||||
|
auto& out = m_toProcess.files[fileid];
|
||||||
try {
|
try {
|
||||||
failed &= (!out.parseFromBytes(bytes));
|
out.parseFromObject(Json::requireObject(file));
|
||||||
} catch (const JSONValidationError& e) {
|
} catch (const JSONValidationError& e) {
|
||||||
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
|
qDebug() << "Blocked mod on curseforge" << out.fileName;
|
||||||
qCritical() << e.cause();
|
auto hash = out.hash;
|
||||||
qCritical() << "JSON:";
|
if(!hash.isEmpty()) {
|
||||||
qCritical() << bytes;
|
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
|
||||||
failed = true;
|
auto output = new QByteArray();
|
||||||
|
auto dl = Net::Download::makeByteArray(QUrl(url), output);
|
||||||
|
QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
|
||||||
|
out.resolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
job->addNetAction(dl);
|
||||||
|
blockedProjects.insert(&out, output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
if (!failed) {
|
connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
|
||||||
emitSucceeded();
|
|
||||||
|
job->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Flame::FileResolvingTask::modrinthCheckFinished() {
|
||||||
|
qDebug() << "Finished with blocked mods : " << blockedProjects.size();
|
||||||
|
|
||||||
|
for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
|
||||||
|
auto &out = *it;
|
||||||
|
auto bytes = blockedProjects[out];
|
||||||
|
if (!out->resolved) {
|
||||||
|
delete bytes;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(*bytes);
|
||||||
|
auto obj = doc.object();
|
||||||
|
auto array = Json::requireArray(obj,"files");
|
||||||
|
for (auto file: array) {
|
||||||
|
auto fileObj = Json::requireObject(file);
|
||||||
|
auto primary = Json::requireBoolean(fileObj,"primary");
|
||||||
|
if (primary) {
|
||||||
|
out->url = Json::requireUrl(fileObj,"url");
|
||||||
|
qDebug() << "Found alternative on modrinth " << out->fileName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete bytes;
|
||||||
|
}
|
||||||
|
//copy to an output list and filter out projects found on modrinth
|
||||||
|
auto block = new QList<File *>();
|
||||||
|
auto it = blockedProjects.keys();
|
||||||
|
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
|
||||||
|
return !f->resolved;
|
||||||
|
});
|
||||||
|
//Display not found mods early
|
||||||
|
if (!block->empty()) {
|
||||||
|
//blocked mods found, we need the slug for displaying.... we need another job :D !
|
||||||
|
auto slugJob = new NetJob("Slug Job", m_network);
|
||||||
|
auto slugs = QVector<QByteArray>(block->size());
|
||||||
|
auto index = 0;
|
||||||
|
for (auto fileInfo: *block) {
|
||||||
|
auto projectId = fileInfo->projectId;
|
||||||
|
slugs[index] = QByteArray();
|
||||||
|
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
|
||||||
|
auto dl = Net::Download::makeByteArray(url, &slugs[index]);
|
||||||
|
slugJob->addNetAction(dl);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
|
||||||
|
slugJob->deleteLater();
|
||||||
|
auto index = 0;
|
||||||
|
for (const auto &slugResult: slugs) {
|
||||||
|
auto json = QJsonDocument::fromJson(slugResult);
|
||||||
|
auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
|
||||||
|
"websiteUrl");
|
||||||
|
auto mod = block->at(index);
|
||||||
|
auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
|
||||||
|
mod->websiteUrl = link;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
emitSucceeded();
|
||||||
|
});
|
||||||
|
slugJob->start();
|
||||||
} else {
|
} else {
|
||||||
emitFailed(tr("Some mod ID resolving tasks failed."));
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ class FileResolvingTask : public Task
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
|
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
|
||||||
virtual ~FileResolvingTask() {};
|
virtual ~FileResolvingTask() {};
|
||||||
|
|
||||||
const Flame::Manifest &getResults() const
|
const Flame::Manifest &getResults() const
|
||||||
@ -27,7 +27,11 @@ protected slots:
|
|||||||
private: /* data */
|
private: /* data */
|
||||||
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
||||||
Flame::Manifest m_toProcess;
|
Flame::Manifest m_toProcess;
|
||||||
QVector<QByteArray> results;
|
std::shared_ptr<QByteArray> result;
|
||||||
NetJob::Ptr m_dljob;
|
NetJob::Ptr m_dljob;
|
||||||
|
|
||||||
|
void modrinthCheckFinished();
|
||||||
|
|
||||||
|
QMap<File *, QByteArray *> blockedProjects;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
|
|||||||
auto obj = Json::requireObject(item);
|
auto obj = Json::requireObject(item);
|
||||||
Flame::File file;
|
Flame::File file;
|
||||||
loadFileV1(file, obj);
|
loadFileV1(file, obj);
|
||||||
m.files.append(file);
|
m.files.insert(file.fileId,file);
|
||||||
}
|
}
|
||||||
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
|
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
|
||||||
}
|
}
|
||||||
@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
|
|||||||
loadManifestV1(m, obj);
|
loadManifestV1(m, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Flame::File::parseFromBytes(const QByteArray& bytes)
|
bool Flame::File::parseFromObject(const QJsonObject& obj)
|
||||||
{
|
{
|
||||||
auto doc = Json::requireDocument(bytes);
|
|
||||||
if (!doc.isObject()) {
|
|
||||||
throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
|
|
||||||
}
|
|
||||||
auto obj = Json::ensureObject(doc.object(), "data");
|
|
||||||
|
|
||||||
fileName = Json::requireString(obj, "fileName");
|
fileName = Json::requireString(obj, "fileName");
|
||||||
|
|
||||||
QString rawUrl = Json::requireString(obj, "downloadUrl");
|
|
||||||
url = QUrl(rawUrl, QUrl::TolerantMode);
|
|
||||||
if (!url.isValid()) {
|
|
||||||
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
|
|
||||||
}
|
|
||||||
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
|
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
|
||||||
// It is also optional
|
// It is also optional
|
||||||
type = File::Type::SingleFile;
|
type = File::Type::SingleFile;
|
||||||
@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes)
|
|||||||
// this is probably a mod, dunno what else could modpacks download
|
// this is probably a mod, dunno what else could modpacks download
|
||||||
targetFolder = "mods";
|
targetFolder = "mods";
|
||||||
}
|
}
|
||||||
|
// get the hash
|
||||||
|
hash = QString();
|
||||||
|
auto hashes = Json::ensureArray(obj, "hashes");
|
||||||
|
for(QJsonValueRef item : hashes) {
|
||||||
|
auto hobj = Json::requireObject(item);
|
||||||
|
auto algo = Json::requireInteger(hobj, "algo");
|
||||||
|
auto value = Json::requireString(hobj, "value");
|
||||||
|
if (algo == 1) {
|
||||||
|
hash = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// may throw, if the project is blocked
|
||||||
|
QString rawUrl = Json::ensureString(obj, "downloadUrl");
|
||||||
|
url = QUrl(rawUrl, QUrl::TolerantMode);
|
||||||
|
if (!url.isValid()) {
|
||||||
|
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
|
||||||
|
}
|
||||||
|
|
||||||
resolved = true;
|
resolved = true;
|
||||||
return true;
|
return true;
|
||||||
|
@ -2,19 +2,24 @@
|
|||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
#include <QMap>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
namespace Flame
|
namespace Flame
|
||||||
{
|
{
|
||||||
struct File
|
struct File
|
||||||
{
|
{
|
||||||
// NOTE: throws JSONValidationError
|
// NOTE: throws JSONValidationError
|
||||||
bool parseFromBytes(const QByteArray &bytes);
|
bool parseFromObject(const QJsonObject& object);
|
||||||
|
|
||||||
int projectId = 0;
|
int projectId = 0;
|
||||||
int fileId = 0;
|
int fileId = 0;
|
||||||
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
|
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
|
||||||
bool required = true;
|
bool required = true;
|
||||||
|
QString hash;
|
||||||
|
// NOTE: only set on blocked files ! Empty otherwise.
|
||||||
|
QString websiteUrl;
|
||||||
|
|
||||||
// our
|
// our
|
||||||
bool resolved = false;
|
bool resolved = false;
|
||||||
@ -54,7 +59,8 @@ struct Manifest
|
|||||||
QString name;
|
QString name;
|
||||||
QString version;
|
QString version;
|
||||||
QString author;
|
QString author;
|
||||||
QVector<Flame::File> files;
|
//File id -> File
|
||||||
|
QMap<int,Flame::File> files;
|
||||||
QString overrides;
|
QString overrides;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
199
launcher/net/Upload.cpp
Normal file
199
launcher/net/Upload.cpp
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// Created by timoreo on 20/05/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "Upload.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include "ByteArraySink.h"
|
||||||
|
#include "BuildConfig.h"
|
||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
namespace Net {
|
||||||
|
|
||||||
|
void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
|
||||||
|
setProgress(bytesReceived, bytesTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Upload::downloadError(QNetworkReply::NetworkError error) {
|
||||||
|
if (error == QNetworkReply::OperationCanceledError) {
|
||||||
|
qCritical() << "Aborted " << m_url.toString();
|
||||||
|
m_state = State::AbortedByUser;
|
||||||
|
} else {
|
||||||
|
// error happened during download.
|
||||||
|
qCritical() << "Failed " << m_url.toString() << " with reason " << error;
|
||||||
|
m_state = State::Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Upload::sslErrors(const QList<QSslError> &errors) {
|
||||||
|
int i = 1;
|
||||||
|
for (const auto& error : errors) {
|
||||||
|
qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
|
||||||
|
auto cert = error.certificate();
|
||||||
|
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Upload::handleRedirect()
|
||||||
|
{
|
||||||
|
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||||
|
if (!redirect.isValid()) {
|
||||||
|
if (!m_reply->hasRawHeader("Location")) {
|
||||||
|
// no redirect -> it's fine to continue
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// there is a Location header, but it's not correct. we need to apply some workarounds...
|
||||||
|
QByteArray redirectBA = m_reply->rawHeader("Location");
|
||||||
|
if (redirectBA.size() == 0) {
|
||||||
|
// empty, yet present redirect header? WTF?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QString redirectStr = QString::fromUtf8(redirectBA);
|
||||||
|
|
||||||
|
if (redirectStr.startsWith("//")) {
|
||||||
|
/*
|
||||||
|
* IF the URL begins with //, we need to insert the URL scheme.
|
||||||
|
* See: https://bugreports.qt.io/browse/QTBUG-41061
|
||||||
|
* See: http://tools.ietf.org/html/rfc3986#section-4.2
|
||||||
|
*/
|
||||||
|
redirectStr = m_reply->url().scheme() + ":" + redirectStr;
|
||||||
|
} else if (redirectStr.startsWith("/")) {
|
||||||
|
/*
|
||||||
|
* IF the URL begins with /, we need to process it as a relative URL
|
||||||
|
*/
|
||||||
|
auto url = m_reply->url();
|
||||||
|
url.setPath(redirectStr, QUrl::TolerantMode);
|
||||||
|
redirectStr = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
|
||||||
|
* FIXME: report Qt bug for this
|
||||||
|
*/
|
||||||
|
redirect = QUrl(redirectStr, QUrl::TolerantMode);
|
||||||
|
if (!redirect.isValid()) {
|
||||||
|
qWarning() << "Failed to parse redirect URL:" << redirectStr;
|
||||||
|
downloadError(QNetworkReply::ProtocolFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
qDebug() << "Fixed location header:" << redirect;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Location header:" << redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_url = QUrl(redirect.toString());
|
||||||
|
qDebug() << "Following redirect to " << m_url.toString();
|
||||||
|
startAction(m_network);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Upload::downloadFinished() {
|
||||||
|
// handle HTTP redirection first
|
||||||
|
// very unlikely for post requests, still can happen
|
||||||
|
if (handleRedirect()) {
|
||||||
|
qDebug() << "Upload redirected:" << m_url.toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the download failed before this point ...
|
||||||
|
if (m_state == State::Succeeded) {
|
||||||
|
qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString();
|
||||||
|
m_sink->abort();
|
||||||
|
m_reply.reset();
|
||||||
|
emit succeeded();
|
||||||
|
return;
|
||||||
|
} else if (m_state == State::Failed) {
|
||||||
|
qDebug() << "Upload failed in previous step:" << m_url.toString();
|
||||||
|
m_sink->abort();
|
||||||
|
m_reply.reset();
|
||||||
|
emit failed("");
|
||||||
|
return;
|
||||||
|
} else if (m_state == State::AbortedByUser) {
|
||||||
|
qDebug() << "Upload aborted in previous step:" << m_url.toString();
|
||||||
|
m_sink->abort();
|
||||||
|
m_reply.reset();
|
||||||
|
emit aborted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we got all the remaining data, if any
|
||||||
|
auto data = m_reply->readAll();
|
||||||
|
if (data.size()) {
|
||||||
|
qDebug() << "Writing extra" << data.size() << "bytes";
|
||||||
|
m_state = m_sink->write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, finalize the whole graph
|
||||||
|
m_state = m_sink->finalize(*m_reply.get());
|
||||||
|
if (m_state != State::Succeeded) {
|
||||||
|
qDebug() << "Upload failed to finalize:" << m_url.toString();
|
||||||
|
m_sink->abort();
|
||||||
|
m_reply.reset();
|
||||||
|
emit failed("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_reply.reset();
|
||||||
|
qDebug() << "Upload succeeded:" << m_url.toString();
|
||||||
|
emit succeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Upload::downloadReadyRead() {
|
||||||
|
if (m_state == State::Running) {
|
||||||
|
auto data = m_reply->readAll();
|
||||||
|
m_state = m_sink->write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Upload::executeTask() {
|
||||||
|
setStatus(tr("Uploading %1").arg(m_url.toString()));
|
||||||
|
|
||||||
|
if (m_state == State::AbortedByUser) {
|
||||||
|
qWarning() << "Attempt to start an aborted Upload:" << m_url.toString();
|
||||||
|
emit aborted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QNetworkRequest request(m_url);
|
||||||
|
m_state = m_sink->init(request);
|
||||||
|
switch (m_state) {
|
||||||
|
case State::Succeeded:
|
||||||
|
emitSucceeded();
|
||||||
|
qDebug() << "Upload cache hit " << m_url.toString();
|
||||||
|
return;
|
||||||
|
case State::Running:
|
||||||
|
qDebug() << "Uploading " << m_url.toString();
|
||||||
|
break;
|
||||||
|
case State::Inactive:
|
||||||
|
case State::Failed:
|
||||||
|
emitFailed("");
|
||||||
|
return;
|
||||||
|
case State::AbortedByUser:
|
||||||
|
emitAborted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
|
||||||
|
if (request.url().host().contains("api.curseforge.com")) {
|
||||||
|
request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8());
|
||||||
|
}
|
||||||
|
//TODO other types of post requests ?
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QNetworkReply* rep = m_network->post(request, m_post_data);
|
||||||
|
|
||||||
|
m_reply.reset(rep);
|
||||||
|
connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
|
||||||
|
connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
|
||||||
|
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||||
|
connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors);
|
||||||
|
connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
|
||||||
|
auto* up = new Upload();
|
||||||
|
up->m_url = std::move(url);
|
||||||
|
up->m_sink.reset(new ByteArraySink(output));
|
||||||
|
up->m_post_data = std::move(m_post_data);
|
||||||
|
return up;
|
||||||
|
}
|
||||||
|
} // Net
|
31
launcher/net/Upload.h
Normal file
31
launcher/net/Upload.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "NetAction.h"
|
||||||
|
#include "Sink.h"
|
||||||
|
|
||||||
|
namespace Net {
|
||||||
|
|
||||||
|
class Upload : public NetAction {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
|
||||||
|
void downloadError(QNetworkReply::NetworkError error) override;
|
||||||
|
void sslErrors(const QList<QSslError> & errors);
|
||||||
|
void downloadFinished() override;
|
||||||
|
void downloadReadyRead() override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void executeTask() override;
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Sink> m_sink;
|
||||||
|
QByteArray m_post_data;
|
||||||
|
|
||||||
|
bool handleRedirect();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // Net
|
||||||
|
|
15
launcher/ui/dialogs/ScrollMessageBox.cpp
Normal file
15
launcher/ui/dialogs/ScrollMessageBox.cpp
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#include "ScrollMessageBox.h"
|
||||||
|
#include "ui_ScrollMessageBox.h"
|
||||||
|
|
||||||
|
|
||||||
|
ScrollMessageBox::ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body) :
|
||||||
|
QDialog(parent), ui(new Ui::ScrollMessageBox) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
this->setWindowTitle(title);
|
||||||
|
ui->label->setText(text);
|
||||||
|
ui->textBrowser->setText(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollMessageBox::~ScrollMessageBox() {
|
||||||
|
delete ui;
|
||||||
|
}
|
20
launcher/ui/dialogs/ScrollMessageBox.h
Normal file
20
launcher/ui/dialogs/ScrollMessageBox.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
namespace Ui { class ScrollMessageBox; }
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
class ScrollMessageBox : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body);
|
||||||
|
|
||||||
|
~ScrollMessageBox() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::ScrollMessageBox *ui;
|
||||||
|
};
|
84
launcher/ui/dialogs/ScrollMessageBox.ui
Normal file
84
launcher/ui/dialogs/ScrollMessageBox.ui
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ScrollMessageBox</class>
|
||||||
|
<widget class="QDialog" name="ScrollMessageBox">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>455</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>ScrollMessageBox</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
<property name="textFormat">
|
||||||
|
<enum>Qt::RichText</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QTextBrowser" name="textBrowser">
|
||||||
|
<property name="acceptRichText">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>ScrollMessageBox</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>425</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>227</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>ScrollMessageBox</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>425</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>227</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -117,7 +117,7 @@ void ImportPage::updateState()
|
|||||||
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
|
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
|
||||||
{
|
{
|
||||||
QFileInfo fi(url.fileName());
|
QFileInfo fi(url.fileName());
|
||||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
|
||||||
dialog->setSuggestedIcon("default");
|
dialog->setSuggestedIcon("default");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ void ImportPage::updateState()
|
|||||||
}
|
}
|
||||||
// hook, line and sinker.
|
// hook, line and sinker.
|
||||||
QFileInfo fi(url.fileName());
|
QFileInfo fi(url.fileName());
|
||||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
|
||||||
dialog->setSuggestedIcon("default");
|
dialog->setSuggestedIcon("default");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ void FlamePage::suggestCurrent()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
|
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
|
||||||
QString editedLogoName;
|
QString editedLogoName;
|
||||||
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
|
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
|
||||||
listModel->getLogo(current.logoName, current.logoUrl,
|
listModel->getLogo(current.logoName, current.logoUrl,
|
||||||
|
Loading…
Reference in New Issue
Block a user