aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/CMakeLists.txt7
-rw-r--r--launcher/InstanceImportTask.cpp268
-rw-r--r--launcher/InstanceImportTask.h6
-rw-r--r--launcher/icons/IconList.cpp4
-rw-r--r--launcher/modplatform/ModAPI.h2
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp43
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp16
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h16
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp124
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h8
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp12
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h1
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp35
-rw-r--r--launcher/modplatform/flame/PackManifest.h10
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h8
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.cpp22
-rw-r--r--launcher/net/Upload.cpp199
-rw-r--r--launcher/net/Upload.h31
-rw-r--r--launcher/tasks/SequentialTask.cpp19
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp24
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h3
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp91
-rw-r--r--launcher/ui/dialogs/ProgressDialog.ui6
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp27
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.h12
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.ui81
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.cpp15
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.h20
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.ui84
-rw-r--r--launcher/ui/pages/global/APIPage.cpp3
-rw-r--r--launcher/ui/pages/global/APIPage.ui14
-rw-r--r--launcher/ui/pages/global/JavaPage.cpp6
-rw-r--r--launcher/ui/pages/global/JavaPage.ui69
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp5
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.cpp4
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp55
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h1
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.cpp2
39 files changed, 1014 insertions, 341 deletions
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 15534c71..bbf80185 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -128,6 +128,8 @@ set(NET_SOURCES
net/PasteUpload.h
net/Sink.h
net/Validator.h
+ net/Upload.cpp
+ net/Upload.h
)
# Game launch logic
@@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/SkinUploadDialog.h
ui/dialogs/ModDownloadDialog.cpp
ui/dialogs/ModDownloadDialog.h
+ ui/dialogs/ScrollMessageBox.cpp
+ ui/dialogs/ScrollMessageBox.h
# GUI - widgets
ui/widgets/Common.cpp
@@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
+ ui/dialogs/ScrollMessageBox.ui
)
qt5_add_resources(LAUNCHER_RESOURCES
@@ -958,7 +963,7 @@ qt5_add_resources(LAUNCHER_RESOURCES
######## Windows resource files ########
if(WIN32)
- set(LAUNCHER_RCS ../${Launcher_Branding_WindowsRC})
+ set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
endif()
# Add executable
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 4bad7251..09c2a333 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -60,9 +60,9 @@
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ScrollMessageBox.h"
#include <algorithm>
-#include <iterator>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
@@ -72,7 +72,8 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
bool InstanceImportTask::abort()
{
- m_filesNetJob->abort();
+ if (m_filesNetJob)
+ m_filesNetJob->abort();
m_extractFuture.cancel();
return false;
@@ -135,18 +136,20 @@ void InstanceImportTask::processZipPack()
return;
}
- QStringList blacklist = {"instance.cfg", "manifest.json"};
- QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
- bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json");
- QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
- QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json");
+ QuaZipDir packZipDir(m_packZip.get());
+
+ // https://docs.modrinth.com/docs/modpacks/format_definition/#storage
+ bool modrinthFound = packZipDir.exists("/modrinth.index.json");
+ bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json");
QString root;
- if(!mmcFound.isNull())
+
+ // NOTE: Prioritize modpack platforms that aren't searched for recursively.
+ // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
+ if(modrinthFound)
{
- // process as MultiMC instance/pack
- qDebug() << "MultiMC:" << mmcFound;
- root = mmcFound;
- m_modpackType = ModpackType::MultiMC;
+ // process as Modrinth pack
+ qDebug() << "Modrinth:" << modrinthFound;
+ m_modpackType = ModpackType::Modrinth;
}
else if (technicFound)
{
@@ -156,19 +159,25 @@ void InstanceImportTask::processZipPack()
extractDir.cd(".minecraft");
m_modpackType = ModpackType::Technic;
}
- else if(!flameFound.isNull())
- {
- // process as Flame pack
- qDebug() << "Flame:" << flameFound;
- root = flameFound;
- m_modpackType = ModpackType::Flame;
- }
- else if(!modrinthFound.isNull())
+ else
{
- // process as Modrinth pack
- qDebug() << "Modrinth:" << modrinthFound;
- root = modrinthFound;
- m_modpackType = ModpackType::Modrinth;
+ QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
+ QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
+
+ if (!mmcRoot.isNull())
+ {
+ // process as MultiMC instance/pack
+ qDebug() << "MultiMC:" << mmcRoot;
+ root = mmcRoot;
+ m_modpackType = ModpackType::MultiMC;
+ }
+ else if(!flameRoot.isNull())
+ {
+ // process as Flame pack
+ qDebug() << "Flame:" << flameRoot;
+ root = flameRoot;
+ m_modpackType = ModpackType::Flame;
+ }
}
if(m_modpackType == ModpackType::Unknown)
{
@@ -385,61 +394,136 @@ void InstanceImportTask::processFlame()
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
{
auto results = m_modIdResolver->getResults();
- m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
- for(auto result: results.files)
- {
- QString filename = result.fileName;
- if(!result.required)
- {
- filename += ".disabled";
+ //first check for blocked mods
+ QString text;
+ auto anyBlocked = false;
+ for(const auto& result: results.files.values()) {
+ if (!result.resolved || result.url.isEmpty()) {
+ text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
+ anyBlocked = true;
}
+ }
+ if(anyBlocked) {
+ qWarning() << "Blocked mods found, displaying mod list";
+
+ auto message_dialog = new ScrollMessageBox(m_parent,
+ 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);
+ 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.
+ 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;
+ }
}
- 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;
+ 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;
}
- 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();
}
- 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_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
@@ -501,6 +585,7 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
std::vector<Modrinth::File> files;
+ std::vector<Modrinth::File> non_whitelisted_files;
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
try {
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
@@ -515,11 +600,11 @@ void InstanceImportTask::processModrinth()
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
bool had_optional = false;
- for (auto& obj : jsonFiles) {
+ for (auto& modInfo : jsonFiles) {
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");
if (support == "unsupported") {
continue;
@@ -537,7 +622,7 @@ void InstanceImportTask::processModrinth()
file.path += ".disabled";
}
- QJsonObject hashes = Json::requireObject(obj, "hashes");
+ QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
@@ -557,13 +642,38 @@ void InstanceImportTask::processModrinth()
file.hashAlgorithm = hashAlgorithm;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
- file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path);
- if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
- throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
+
+ file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path);
+
+ if (!file.download.isValid()) {
+ qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(file.download.toString(), file.path);
+ throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
+ }
+ else if (!Modrinth::validateDownloadUrl(file.download)) {
+ qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path);
+ non_whitelisted_files.push_back(file);
}
+
files.push_back(file);
}
+ if (!non_whitelisted_files.empty()) {
+ QString text;
+ for (const auto& file : non_whitelisted_files) {
+ text += tr("Filepath: %1<br>URL: <a href='%2'>%2</a><br>").arg(file.path, file.download.toString());
+ }
+
+ auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"),
+ tr("The following mods have URLs that are not whitelisted by Modrinth.\n"
+ "Proceed with caution!"),
+ text);
+ message_dialog->setModal(true);
+ if (message_dialog->exec() == QDialog::Rejected) {
+ emitFailed("Aborted");
+ return;
+ }
+ }
+
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index 5e4d3235..b67d48f3 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -42,6 +42,7 @@
#include <QFutureWatcher>
#include "settings/SettingsObject.h"
#include "QObjectPtr.h"
+#include "modplatform/flame/PackManifest.h"
#include <nonstd/optional>
@@ -59,6 +60,10 @@ public:
bool canAbort() const override { return true; }
bool abort() override;
+ const QVector<Flame::File> &getBlockedFiles() const
+ {
+ return m_blockedMods;
+ }
protected:
//! Entry point for tasks.
@@ -87,6 +92,7 @@ private: /* data */
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QVector<Flame::File> m_blockedMods;
enum class ModpackType{
Unknown,
MultiMC,
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index c269d10a..0ddfae55 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -273,7 +273,7 @@ void IconList::installIcons(const QStringList &iconFiles)
QFileInfo fileinfo(file);
if (!fileinfo.isReadable() || !fileinfo.isFile())
continue;
- QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName());
+ QString target = FS::PathCombine(getDirectory(), fileinfo.fileName());
QString suffix = fileinfo.suffix();
if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif")
@@ -290,7 +290,7 @@ void IconList::installIcon(const QString &file, const QString &name)
if(!fileinfo.isReadable() || !fileinfo.isFile())
return;
- QString target = FS::PathCombine(m_dir.dirName(), name);
+ QString target = FS::PathCombine(getDirectory(), name);
QFile::copy(file, target);
}
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
index 4230df0b..eb0de3f0 100644
--- a/launcher/modplatform/ModAPI.h
+++ b/launcher/modplatform/ModAPI.h
@@ -68,7 +68,7 @@ class ModAPI {
{
QString s;
for(auto& ver : mcVersions){
- s += QString("%1,").arg(ver.toString());
+ s += QString("\"%1\",").arg(ver.toString());
}
s.remove(s.length() - 1, 1); //remove last comma
return s;
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 9b14f355..62c7bf6d 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -414,7 +414,31 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared
bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
{
- if(m_version.mainClass == QString() && m_version.extraArguments == QString()) {
+ if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) {
+ return true;
+ }
+
+ auto mainClass = m_version.mainClass.mainClass;
+ auto extraArguments = m_version.extraArguments.arguments;
+
+ auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty();
+ auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty();
+ if (hasMainClassDepends || hasExtraArgumentsDepends) {
+ QSet<QString> mods;
+ for (const auto& item : m_version.mods) {
+ mods.insert(item.name);
+ }
+
+ if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) {
+ mainClass = "";
+ }
+
+ if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) {
+ extraArguments = "";
+ }
+ }
+
+ if (mainClass.isEmpty() && extraArguments.isEmpty()) {
return true;
}
@@ -442,12 +466,12 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<
auto f = std::make_shared<VersionFile>();
f->name = m_pack + " " + m_version_name;
- if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) {
- f->mainClass = m_version.mainClass;
+ if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) {
+ f->mainClass = mainClass;
}
// Parse out tweakers
- auto args = m_version.extraArguments.split(" ");
+ auto args = extraArguments.split(" ");
QString previous;
for(auto arg : args) {
if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
@@ -757,6 +781,17 @@ bool PackInstallTask::extractMods(
for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
auto &from = iter.key();
auto &to = iter.value();
+
+ // If the file already exists, assume the mod is the correct copy - and remove
+ // the copy from the Configs.zip
+ QFileInfo fileInfo(to);
+ if (fileInfo.exists()) {
+ if (!QFile::remove(to)) {
+ qWarning() << "Failed to delete" << to;
+ return false;
+ }
+ }
+
FS::copy fileCopyOperation(from, to);
if(!fileCopyOperation()) {
qWarning() << "Failed to copy" << from << "to" << to;
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
index d01ec32c..3af02a09 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -212,6 +212,18 @@ static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj
m.update = Json::ensureString(obj, "update", "");
}
+static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj)
+{
+ m.mainClass = Json::ensureString(obj, "mainClass", "");
+ m.depends = Json::ensureString(obj, "depends", "");
+}
+
+static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj)
+{
+ a.arguments = Json::ensureString(obj, "arguments", "");
+ a.depends = Json::ensureString(obj, "depends", "");
+}
+
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@@ -220,12 +232,12 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
if(obj.contains("mainClass")) {
auto main = Json::requireObject(obj, "mainClass");
- v.mainClass = Json::ensureString(main, "mainClass", "");
+ loadVersionMainClass(v.mainClass, main);
}
if(obj.contains("extraArguments")) {
auto arguments = Json::requireObject(obj, "extraArguments");
- v.extraArguments = Json::ensureString(arguments, "arguments", "");
+ loadVersionExtraArguments(v.extraArguments, arguments);
}
if(obj.contains("loader")) {
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
index 23e162e3..43510c50 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.h
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -150,13 +150,25 @@ struct VersionMessages
QString update;
};
+struct PackVersionMainClass
+{
+ QString mainClass;
+ QString depends;
+};
+
+struct PackVersionExtraArguments
+{
+ QString arguments;
+ QString depends;
+};
+
struct PackVersion
{
QString version;
QString minecraft;
bool noConfigs;
- QString mainClass;
- QString extraArguments;
+ PackVersionMainClass mainClass;
+ PackVersionExtraArguments extraArguments;
VersionLoader loader;
QVector<VersionLibrary> libraries;
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 95924a68..a790ab9c 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -1,7 +1,9 @@
#include "FileResolvingTask.h"
+
#include "Json.h"
+#include "net/Upload.h"
-Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
+Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
{}
@@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
setStatus(tr("Resolving mod IDs..."));
setProgress(0, m_toProcess.files.size());
m_dljob = new NetJob("Mod id resolver", m_network);
- results.resize(m_toProcess.files.size());
- int index = 0;
- for (auto& file : m_toProcess.files) {
- auto projectIdStr = QString::number(file.projectId);
- auto fileIdStr = QString::number(file.fileId);
- QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
- auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
- m_dljob->addNetAction(dl);
- index++;
- }
+ result.reset(new QByteArray());
+ //build json data to send
+ QJsonObject object;
+
+ object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
+ l.push_back(s.fileId);
+ return l;
+ }));
+ 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);
m_dljob->start();
}
void Flame::FileResolvingTask::netJobFinished()
{
- bool failed = false;
int index = 0;
- for (auto& bytes : results) {
- auto& out = m_toProcess.files[index];
+ // job to check modrinth for blocked projects
+ 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 {
- failed &= (!out.parseFromBytes(bytes));
+ out.parseFromObject(Json::requireObject(file));
} catch (const JSONValidationError& e) {
- qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
- qCritical() << e.cause();
- qCritical() << "JSON:";
- qCritical() << bytes;
- failed = true;
+ qDebug() << "Blocked mod on curseforge" << out.fileName;
+ auto hash = out.hash;
+ if(!hash.isEmpty()) {
+ auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
+ 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++;
}
- if (!failed) {
- emitSucceeded();
+ connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
+
+ 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 {
- emitFailed(tr("Some mod ID resolving tasks failed."));
+ emitSucceeded();
}
}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 5e5adcd7..87981f0a 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -10,7 +10,7 @@ class FileResolvingTask : public Task
{
Q_OBJECT
public:
- explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
+ explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
const Flame::Manifest &getResults() const
@@ -27,7 +27,11 @@ protected slots:
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
- QVector<QByteArray> results;
+ std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
+
+ void modrinthCheckFinished();
+
+ QMap<File *, QByteArray *> blockedProjects;
};
}
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
index 6d48a3bf..bece7843 100644
--- a/launcher/modplatform/flame/FlamePackIndex.cpp
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -65,16 +65,12 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
// pick the latest version supported
file.mcVersion = versionArray[0].toString();
file.version = Json::requireString(version, "displayName");
- file.fileName = Json::requireString(version, "fileName");
file.downloadUrl = Json::ensureString(version, "downloadUrl");
- if(file.downloadUrl.isEmpty()){
- //FIXME : HACK, MAY NOT WORK FOR LONG
- file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3")
- .arg(QString::number(QString::number(file.fileId).leftRef(4).toInt())
- ,QString::number(QString::number(file.fileId).rightRef(3).toInt())
- ,QUrl::toPercentEncoding(file.fileName));
+
+ // only add if we have a download URL (third party distribution is enabled)
+ if (!file.downloadUrl.isEmpty()) {
+ unsortedVersions.append(file);
}
- unsortedVersions.append(file);
}
auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; };
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
index a8bb15be..7ffa29c3 100644
--- a/launcher/modplatform/flame/FlamePackIndex.h
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -18,7 +18,6 @@ struct IndexedVersion {
QString version;
QString mcVersion;
QString downloadUrl;
- QString fileName;
};
struct IndexedPack
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index e4f90c1a..12a4b990 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
auto obj = Json::requireObject(item);
Flame::File file;
loadFileV1(file, obj);
- m.files.append(file);
+ m.files.insert(file.fileId,file);
}
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
}
@@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
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");
-
- 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
// It is also optional
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
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;
return true;
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index 02f39f0e..26a48d1c 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -2,19 +2,24 @@
#include <QString>
#include <QVector>
+#include <QMap>
#include <QUrl>
+#include <QJsonObject>
namespace Flame
{
struct File
{
// NOTE: throws JSONValidationError
- bool parseFromBytes(const QByteArray &bytes);
+ bool parseFromObject(const QJsonObject& object);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
bool required = true;
+ QString hash;
+ // NOTE: only set on blocked files ! Empty otherwise.
+ QString websiteUrl;
// our
bool resolved = false;
@@ -54,7 +59,8 @@ struct Manifest
QString name;
QString version;
QString author;
- QVector<Flame::File> files;
+ //File id -> File
+ QMap<int,Flame::File> files;
QString overrides;
};
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 79bc5175..6119a4df 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -79,11 +79,11 @@ class ModrinthAPI : public NetworkModAPI {
{
return QString(BuildConfig.MODRINTH_PROD_URL +
"/project/%1/version?"
- "game_versions=[%2]"
+ "game_versions=[%2]&"
"loaders=[\"%3\"]")
- .arg(args.addonId)
- .arg(getGameVersionsString(args.mcVersions))
- .arg(getModLoaderStrings(args.loaders).join("\",\""));
+ .arg(args.addonId,
+ getGameVersionsString(args.mcVersions),
+ getModLoaderStrings(args.loaders).join("\",\""));
};
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
index f1ad39ce..33116231 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -42,6 +42,8 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include <QSet>
+
static ModrinthAPI api;
namespace Modrinth {
@@ -95,19 +97,15 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
auto validateDownloadUrl(QUrl url) -> bool
{
+ static QSet<QString> domainWhitelist{
+ "cdn.modrinth.com",
+ "github.com",
+ "raw.githubusercontent.com",
+ "gitlab.com"
+ };
+
auto domain = url.host();
- if(domain == "cdn.modrinth.com")
- return true;
- if(domain == "edge.forgecdn.net")
- return true;
- if(domain == "media.forgecdn.net")
- return true;
- if(domain == "github.com")
- return true;
- if(domain == "raw.githubusercontent.com")
- return true;
-
- return false;
+ return domainWhitelist.contains(domain);
}
auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
new file mode 100644
index 00000000..bbd27390
--- /dev/null
+++ b/launcher/net/Upload.cpp
@@ -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
diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h
new file mode 100644
index 00000000..ee784c6e
--- /dev/null
+++ b/launcher/net/Upload.h
@@ -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
+
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index 1573e476..ee57cac1 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -33,11 +33,22 @@ void SequentialTask::executeTask()
bool SequentialTask::abort()
{
- bool succeeded = true;
- for (auto& task : m_queue) {
- if (!task->abort()) succeeded = false;
+ if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) {
+ if(m_currentIndex == -1) {
+ // Don't call emitAborted() here, we want to bypass the 'is the task running' check
+ emit aborted();
+ emit finished();
+ }
+ m_queue.clear();
+ return true;
}
+ bool succeeded = m_queue[m_currentIndex]->abort();
+ m_queue.clear();
+
+ if(succeeded)
+ emitAborted();
+
return succeeded;
}
@@ -76,7 +87,7 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total)
setProgress(0, 100);
return;
}
- setProgress(m_currentIndex, m_queue.count());
+ setProgress(m_currentIndex + 1, m_queue.count());
m_stepProgress = current;
m_stepTotalProgress = total;
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
index 305e85c0..f01c9c07 100644
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ModDownloadDialog.cpp
@@ -77,18 +77,20 @@ void ModDownloadDialog::confirm()
auto keys = modTask.keys();
keys.sort(Qt::CaseInsensitive);
- auto confirm_dialog = ReviewMessageBox::create(
- this,
- tr("Confirm mods to download")
- );
+ auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download"));
- for(auto& task : keys){
- confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename());
+ for (auto& task : keys) {
+ confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() });
}
- connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept);
+ if (confirm_dialog->exec()) {
+ auto deselected = confirm_dialog->deselectedMods();
+ for (auto name : deselected) {
+ modTask.remove(name);
+ }
- confirm_dialog->open();
+ this->accept();
+ }
}
void ModDownloadDialog::accept()
@@ -132,6 +134,12 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena
return iter != modTask.end() && (iter.value()->getFilename() == filename);
}
+bool ModDownloadDialog::isModSelected(const QString &name) const
+{
+ auto iter = modTask.find(name);
+ return iter != modTask.end();
+}
+
ModDownloadDialog::~ModDownloadDialog()
{
}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h
index 782dc361..5c565ad3 100644
--- a/launcher/ui/dialogs/ModDownloadDialog.h
+++ b/launcher/ui/dialogs/ModDownloadDialog.h
@@ -32,6 +32,7 @@ public:
void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr);
void removeSelectedMod(const QString & name = QString());
bool isModSelected(const QString & name, const QString & filename) const;
+ bool isModSelected(const QString & name) const;
const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods;
@@ -41,8 +42,6 @@ public slots:
void accept() override;
void reject() override;
-//private slots:
-
private:
Ui::ModDownloadDialog *ui = nullptr;
PageContainer * m_container = nullptr;
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index 648bd88b..e5226016 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -16,12 +16,12 @@
#include "ProgressDialog.h"
#include "ui_ProgressDialog.h"
-#include <QKeyEvent>
#include <QDebug>
+#include <QKeyEvent>
#include "tasks/Task.h"
-ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog)
+ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog)
{
ui->setupUi(this);
this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
@@ -44,6 +44,7 @@ void ProgressDialog::on_skipButton_clicked(bool checked)
{
Q_UNUSED(checked);
task->abort();
+ QDialog::reject();
}
ProgressDialog::~ProgressDialog()
@@ -53,24 +54,22 @@ ProgressDialog::~ProgressDialog()
void ProgressDialog::updateSize()
{
- QSize qSize = QSize(480, minimumSizeHint().height());
+ QSize qSize = QSize(480, minimumSizeHint().height());
resize(qSize);
- setFixedSize(qSize);
+ setFixedSize(qSize);
}
-int ProgressDialog::execWithTask(Task *task)
+int ProgressDialog::execWithTask(Task* task)
{
this->task = task;
QDialog::DialogCode result;
- if(!task)
- {
+ if (!task) {
qDebug() << "Programmer error: progress dialog created with null task.";
return Accepted;
}
- if(handleImmediateResult(result))
- {
+ if (handleImmediateResult(result)) {
return result;
}
@@ -78,58 +77,51 @@ int ProgressDialog::execWithTask(Task *task)
connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
- connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &)));
+ connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&)));
+ connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&)));
connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
+ connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); });
+
m_is_multi_step = task->isMultiStep();
- if(!m_is_multi_step){
+ if (!m_is_multi_step) {
ui->globalStatusLabel->setHidden(true);
ui->globalProgressBar->setHidden(true);
}
// if this didn't connect to an already running task, invoke start
- if(!task->isRunning())
- {
+ if (!task->isRunning()) {
task->start();
}
- if(task->isRunning())
- {
+ if (task->isRunning()) {
changeProgress(task->getProgress(), task->getTotalProgress());
changeStatus(task->getStatus());
return QDialog::exec();
- }
- else if(handleImmediateResult(result))
- {
+ } else if (handleImmediateResult(result)) {
return result;
- }
- else
- {
+ } else {
return QDialog::Rejected;
}
}
// TODO: only provide the unique_ptr overloads
-int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task)
+int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task)
{
connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
return execWithTask(task.release());
}
-int ProgressDialog::execWithTask(std::unique_ptr<Task> &task)
+int ProgressDialog::execWithTask(std::unique_ptr<Task>& task)
{
connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
return execWithTask(task.release());
}
-bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result)
+bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result)
{
- if(task->isFinished())
- {
- if(task->wasSuccessful())
- {
+ if (task->isFinished()) {
+ if (task->wasSuccessful()) {
result = QDialog::Accepted;
- }
- else
- {
+ } else {
result = QDialog::Rejected;
}
return true;
@@ -137,14 +129,12 @@ bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result)
return false;
}
-Task *ProgressDialog::getTask()
+Task* ProgressDialog::getTask()
{
return task;
}
-void ProgressDialog::onTaskStarted()
-{
-}
+void ProgressDialog::onTaskStarted() {}
void ProgressDialog::onTaskFailed(QString failure)
{
@@ -156,10 +146,11 @@ void ProgressDialog::onTaskSucceeded()
accept();
}
-void ProgressDialog::changeStatus(const QString &status)
+void ProgressDialog::changeStatus(const QString& status)
{
+ ui->globalStatusLabel->setText(task->getStatus());
ui->statusLabel->setText(task->getStepStatus());
- ui->globalStatusLabel->setText(status);
+
updateSize();
}
@@ -168,27 +159,22 @@ void ProgressDialog::changeProgress(qint64 current, qint64 total)
ui->globalProgressBar->setMaximum(total);
ui->globalProgressBar->setValue(current);
- if(!m_is_multi_step){
+ if (!m_is_multi_step) {
ui->taskProgressBar->setMaximum(total);
ui->taskProgressBar->setValue(current);
- }
- else{
+ } else {
ui->taskProgressBar->setMaximum(task->getStepProgress());
ui->taskProgressBar->setValue(task->getStepTotalProgress());
}
}
-void ProgressDialog::keyPressEvent(QKeyEvent *e)
+void ProgressDialog::keyPressEvent(QKeyEvent* e)
{
- if(ui->skipButton->isVisible())
- {
- if (e->key() == Qt::Key_Escape)
- {
+ if (ui->skipButton->isVisible()) {
+ if (e->key() == Qt::Key_Escape) {
on_skipButton_clicked(true);
return;
- }
- else if(e->key() == Qt::Key_Tab)
- {
+ } else if (e->key() == Qt::Key_Tab) {
ui->skipButton->setFocusPolicy(Qt::StrongFocus);
ui->skipButton->setFocus();
ui->skipButton->setAutoDefault(true);
@@ -199,14 +185,11 @@ void ProgressDialog::keyPressEvent(QKeyEvent *e)
QDialog::keyPressEvent(e);
}
-void ProgressDialog::closeEvent(QCloseEvent *e)
+void ProgressDialog::closeEvent(QCloseEvent* e)
{
- if (task && task->isRunning())
- {
+ if (task && task->isRunning()) {
e->ignore();
- }
- else
- {
+ } else {
QDialog::closeEvent(e);
}
}
diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui
index bf119a78..34ab71e3 100644
--- a/launcher/ui/dialogs/ProgressDialog.ui
+++ b/launcher/ui/dialogs/ProgressDialog.ui
@@ -40,6 +40,12 @@
</item>
<item row="2" column="0">
<widget class="QLabel" name="statusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
<property name="text">
<string>Task Status...</string>
</property>
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index 2bfd02e0..c92234a4 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -5,6 +5,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin
: QDialog(parent), ui(new Ui::ReviewMessageBox)
{
ui->setupUi(this);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
}
ReviewMessageBox::~ReviewMessageBox()
@@ -17,15 +20,33 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon)
return new ReviewMessageBox(parent, title, icon);
}
-void ReviewMessageBox::appendMod(const QString& name, const QString& filename)
+void ReviewMessageBox::appendMod(ModInformation&& info)
{
auto itemTop = new QTreeWidgetItem(ui->modTreeWidget);
- itemTop->setText(0, name);
+ itemTop->setCheckState(0, Qt::CheckState::Checked);
+ itemTop->setText(0, info.name);
auto filenameItem = new QTreeWidgetItem(itemTop);
- filenameItem->setText(0, tr("Filename: %1").arg(filename));
+ filenameItem->setText(0, tr("Filename: %1").arg(info.filename));
itemTop->insertChildren(0, { filenameItem });
ui->modTreeWidget->addTopLevelItem(itemTop);
}
+
+auto ReviewMessageBox::deselectedMods() -> QStringList
+{
+ QStringList list;
+
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+
+ for (int i = 0; item != nullptr; ++i) {
+ if (item->checkState(0) == Qt::CheckState::Unchecked) {
+ list.append(item->text(0));
+ }
+
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+
+ return list;
+}
diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h
index 48742cd9..9cfa679a 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.h
+++ b/launcher/ui/dialogs/ReviewMessageBox.h
@@ -6,17 +6,23 @@ namespace Ui {
class ReviewMessageBox;
}
-class ReviewMessageBox final : public QDialog {
+class ReviewMessageBox : public QDialog {
Q_OBJECT
public:
static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*;
- void appendMod(const QString& name, const QString& filename);
+ using ModInformation = struct {
+ QString name;
+ QString filename;
+ };
+
+ void appendMod(ModInformation&& info);
+ auto deselectedMods() -> QStringList;
~ReviewMessageBox();
- private:
+ protected:
ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon);
Ui::ReviewMessageBox* ui;
diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui
index d04f3b3f..ab3bcc2f 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.ui
+++ b/launcher/ui/dialogs/ReviewMessageBox.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>400</width>
- <height>300</height>
+ <width>500</width>
+ <height>350</height>
</rect>
</property>
<property name="windowTitle">
@@ -20,24 +20,7 @@
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
- <item row="0" column="0">
- <widget class="QLabel" name="label">
- <property name="text">
- <string>You're about to download the following mods:</string>
- </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="QTreeWidget" name="modTreeWidget">
<property name="alternatingRowColors">
<bool>true</bool>
@@ -58,41 +41,33 @@
</column>
</widget>
</item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="explainLabel">
+ <property name="text">
+ <string>You're about to download the following mods:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" rowspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="onlyCheckedLabel">
+ <property name="text">
+ <string>Only mods with a check will be downloaded!</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
</layout>
</widget>
<resources/>
- <connections>
- <connection>
- <sender>buttonBox</sender>
- <signal>accepted()</signal>
- <receiver>ReviewMessageBox</receiver>
- <slot>accept()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>200</x>
- <y>265</y>
- </hint>
- <hint type="destinationlabel">
- <x>199</x>
- <y>149</y>
- </hint>
- </hints>
- </connection>
- <connection>
- <sender>buttonBox</sender>
- <signal>rejected()</signal>
- <receiver>ReviewMessageBox</receiver>
- <slot>reject()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>200</x>
- <y>265</y>
- </hint>
- <hint type="destinationlabel">
- <x>199</x>
- <y>149</y>
- </hint>
- </hints>
- </connection>
- </connections>
+ <connections/>
</ui>
diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp
new file mode 100644
index 00000000..afdc4bae
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.cpp
@@ -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;
+}
diff --git a/launcher/ui/dialogs/ScrollMessageBox.h b/launcher/ui/dialogs/ScrollMessageBox.h
new file mode 100644
index 00000000..84aa253a
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.h
@@ -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;
+};
diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui
new file mode 100644
index 00000000..299d2ecc
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.ui
@@ -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 notr="true">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>
diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp
index 6ad243dd..5d812d07 100644
--- a/launcher/ui/pages/global/APIPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -48,6 +48,7 @@
#include "tools/BaseProfiler.h"
#include "Application.h"
#include "net/PasteUpload.h"
+#include "BuildConfig.h"
APIPage::APIPage(QWidget *parent) :
QWidget(parent),
@@ -76,6 +77,8 @@ APIPage::APIPage(QWidget *parent) :
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
ui->tabWidget->tabBar()->hide();
+ ui->metaURL->setPlaceholderText(BuildConfig.META_URL);
+
loadSettings();
resetBaseURLNote();
diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui
index 24189c5c..5c927391 100644
--- a/launcher/ui/pages/global/APIPage.ui
+++ b/launcher/ui/pages/global/APIPage.ui
@@ -36,13 +36,16 @@
<item>
<widget class="QGroupBox" name="groupBox_paste">
<property name="title">
- <string>Pastebin Service</string>
+ <string>&amp;Pastebin Service</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="pasteServiceTypeLabel">
<property name="text">
- <string>Paste Service Type</string>
+ <string>Paste Service &amp;Type</string>
+ </property>
+ <property name="buddy">
+ <cstring>pasteTypeComboBox</cstring>
</property>
</widget>
</item>
@@ -52,7 +55,10 @@
<item>
<widget class="QLabel" name="baseURLLabel">
<property name="text">
- <string>Base URL</string>
+ <string>Base &amp;URL</string>
+ </property>
+ <property name="buddy">
+ <cstring>baseURLEntry</cstring>
</property>
</widget>
</item>
@@ -146,7 +152,7 @@
<item>
<widget class="QLineEdit" name="metaURL">
<property name="placeholderText">
- <string>(Default)</string>
+ <string/>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp
index b5e8de6c..025771e8 100644
--- a/launcher/ui/pages/global/JavaPage.cpp
+++ b/launcher/ui/pages/global/JavaPage.cpp
@@ -95,7 +95,7 @@ void JavaPage::applySettings()
// Java Settings
s->set("JavaPath", ui->javaPathTextBox->text());
- s->set("JvmArgs", ui->jvmArgsTextBox->text());
+ s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked());
s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked());
JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget());
@@ -120,7 +120,7 @@ void JavaPage::loadSettings()
// Java Settings
ui->javaPathTextBox->setText(s->get("JavaPath").toString());
- ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString());
+ ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString());
ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool());
ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool());
}
@@ -166,7 +166,7 @@ void JavaPage::on_javaTestBtn_clicked()
return;
}
checker.reset(new JavaCommon::TestCheck(
- this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(),
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "),
ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
checker->run();
diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui
index 3e4b12a1..6ccffed4 100644
--- a/launcher/ui/pages/global/JavaPage.ui
+++ b/launcher/ui/pages/global/JavaPage.ui
@@ -150,19 +150,16 @@
<string>Java Runtime</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QLabel" name="labelJavaPath">
+ <item row="3" column="1">
+ <widget class="QPushButton" name="javaDetectBtn">
<property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Java path:</string>
- </property>
- <property name="buddy">
- <cstring>javaPathTextBox</cstring>
+ <string>&amp;Auto-detect...</string>
</property>
</widget>
</item>
@@ -175,31 +172,31 @@
</sizepolicy>
</property>
<property name="text">
- <string>J&amp;VM arguments:</string>
+ <string>JVM arguments:</string>
</property>
- <property name="buddy">
- <cstring>jvmArgsTextBox</cstring>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="4" column="1">
- <widget class="QCheckBox" name="skipCompatibilityCheckbox">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJavaPath">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="toolTip">
- <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
- </property>
<property name="text">
- <string>&amp;Skip Java compatibility checks</string>
+ <string>&amp;Java path:</string>
+ </property>
+ <property name="buddy">
+ <cstring>javaPathTextBox</cstring>
</property>
</widget>
</item>
- <item row="3" column="1">
- <widget class="QPushButton" name="javaDetectBtn">
+ <item row="3" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@@ -207,7 +204,7 @@
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Auto-detect...</string>
+ <string>&amp;Test</string>
</property>
</widget>
</item>
@@ -237,22 +234,22 @@
</item>
</layout>
</item>
- <item row="3" column="2">
- <widget class="QPushButton" name="javaTestBtn">
+ <item row="4" column="1">
+ <widget class="QCheckBox" name="skipCompatibilityCheckbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
+ <property name="toolTip">
+ <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
+ </property>
<property name="text">
- <string>&amp;Test</string>
+ <string>&amp;Skip Java compatibility checks</string>
</property>
</widget>
</item>
- <item row="2" column="1" colspan="2">
- <widget class="QLineEdit" name="jvmArgsTextBox"/>
- </item>
<item row="5" column="1">
<widget class="QCheckBox" name="skipJavaWizardCheckbox">
<property name="toolTip">
@@ -263,6 +260,25 @@
</property>
</widget>
</item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -291,7 +307,6 @@
<tabstop>permGenSpinBox</tabstop>
<tabstop>javaBrowseBtn</tabstop>
<tabstop>javaPathTextBox</tabstop>
- <tabstop>jvmArgsTextBox</tabstop>
<tabstop>javaDetectBtn</tabstop>
<tabstop>javaTestBtn</tabstop>
<tabstop>tabWidget</tabstop>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 5574f9d2..b0cd405f 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -402,6 +402,10 @@ void ModFolderPage::on_actionInstall_mods_triggered()
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
tasks->deleteLater();
});
+ connect(tasks, &Task::aborted, [this, tasks]() {
+ CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
+ tasks->deleteLater();
+ });
connect(tasks, &Task::succeeded, [this, tasks]() {
QStringList warnings = tasks->warnings();
if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); }
@@ -411,6 +415,7 @@ void ModFolderPage::on_actionInstall_mods_triggered()
for (auto task : mdownload.getTasks()) {
tasks->addTask(task);
}
+
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp
index c7bc13d8..b3ed1b73 100644
--- a/launcher/ui/pages/modplatform/ImportPage.cpp
+++ b/launcher/ui/pages/modplatform/ImportPage.cpp
@@ -117,7 +117,7 @@ void ImportPage::updateState()
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
{
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
@@ -130,7 +130,7 @@ void ImportPage::updateState()
}
// hook, line and sinker.
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 9dd8f737..1eb5837b 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -38,27 +38,44 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
}
ModPlatform::IndexedPack pack = modpacks.at(pos);
- if (role == Qt::DisplayRole) {
- return pack.name;
- } else if (role == Qt::ToolTipRole) {
- if (pack.description.length() > 100) {
- // some magic to prevent to long tooltips and replace html linebreaks
- QString edit = pack.description.left(97);
- edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
- return edit;
+ switch (role) {
+ case Qt::DisplayRole: {
+ return pack.name;
}
- return pack.description;
- } else if (role == Qt::DecorationRole) {
- if (m_logoMap.contains(pack.logoName)) {
- return (m_logoMap.value(pack.logoName));
+ case Qt::ToolTipRole: {
+ if (pack.description.length() > 100) {
+ // some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+ }
+ return pack.description;
}
- QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
- ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
- return icon;
- } else if (role == Qt::UserRole) {
- QVariant v;
- v.setValue(pack);
- return v;
+ case Qt::DecorationRole: {
+ if (m_logoMap.contains(pack.logoName)) {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ // un-const-ify this
+ ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ case Qt::UserRole: {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ case Qt::FontRole: {
+ QFont font;
+ if (m_parent->getDialog()->isModSelected(pack.name)) {
+ font.setBold(true);
+ font.setUnderline(true);
+ }
+
+ return font;
+ }
+ default:
+ break;
}
return {};
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index 0e658a8d..32affd20 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -41,6 +41,7 @@ class ModPage : public QWidget, public BasePage {
auto apiProvider() const -> const ModAPI* { return api.get(); };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
+ auto getDialog() const -> const ModDownloadDialog* { return dialog; }
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1);
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index ec774621..7e90af47 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -201,7 +201,7 @@ void FlamePage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl,
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
index 27a12cda..7667d169 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -175,7 +175,7 @@ void Page::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName;
if(selected.logo.toLower().startsWith("ftb"))
{