Merge branch 'PolyMC:develop' into macos-app-heuristic
This commit is contained in:
@ -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
|
||||
|
@ -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())
|
||||
else
|
||||
{
|
||||
// process as Flame pack
|
||||
qDebug() << "Flame:" << flameFound;
|
||||
root = flameFound;
|
||||
m_modpackType = ModpackType::Flame;
|
||||
}
|
||||
else if(!modrinthFound.isNull())
|
||||
{
|
||||
// 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";
|
||||
}
|
||||
|
||||
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;
|
||||
//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;
|
||||
}
|
||||
}
|
||||
m_modIdResolver.reset();
|
||||
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
|
||||
{
|
||||
m_filesNetJob.reset();
|
||||
emitSucceeded();
|
||||
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);
|
||||
|
||||
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)
|
||||
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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")) {
|
||||
|
@ -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;
|
||||
|
@ -1,7 +1,9 @@
|
||||
#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)
|
||||
{}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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; };
|
||||
|
@ -18,7 +18,6 @@ struct IndexedVersion {
|
||||
QString version;
|
||||
QString mcVersion;
|
||||
QString downloadUrl;
|
||||
QString fileName;
|
||||
};
|
||||
|
||||
struct IndexedPack
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
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;
|
||||
static QSet<QString> domainWhitelist{
|
||||
"cdn.modrinth.com",
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
"gitlab.com"
|
||||
};
|
||||
|
||||
return false;
|
||||
auto domain = url.host();
|
||||
return domainWhitelist.contains(domain);
|
||||
}
|
||||
|
||||
auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
|
||||
|
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
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
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 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>
|
@ -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();
|
||||
|
@ -36,13 +36,16 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_paste">
|
||||
<property name="title">
|
||||
<string>Pastebin Service</string>
|
||||
<string>&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 &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 &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>
|
||||
|
@ -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();
|
||||
|
@ -150,6 +150,35 @@
|
||||
<string>Java Runtime</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="javaDetectBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Auto-detect...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelJVMArgs">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>JVM arguments:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelJavaPath">
|
||||
<property name="sizePolicy">
|
||||
@ -166,40 +195,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelJVMArgs">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>J&VM arguments:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>jvmArgsTextBox</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<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>&Skip Java compatibility checks</string>
|
||||
</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>&Auto-detect...</string>
|
||||
<string>&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>&Test</string>
|
||||
<string>&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>
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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"))
|
||||
{
|
||||
|
Reference in New Issue
Block a user