mirror of
https://github.com/UltimMC/Launcher.git
synced 2025-12-13 12:12:14 +00:00
Compare commits
37 Commits
feature/so
...
feature/of
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
836fefa2d9 | ||
|
|
060992b448 | ||
|
|
62e1bf327d | ||
|
|
280e0e6e36 | ||
|
|
3a67990acd | ||
|
|
23eab74e6d | ||
|
|
b9d4293552 | ||
|
|
5110b58def | ||
|
|
791a8227b6 | ||
|
|
725ec35635 | ||
|
|
739a86f171 | ||
|
|
48b2f95129 | ||
|
|
497d9bec02 | ||
|
|
c01d020afc | ||
|
|
ee83d432f6 | ||
|
|
8ee11b1a8e | ||
|
|
0b86a7ebf3 | ||
|
|
63330bf111 | ||
|
|
f74e3db804 | ||
|
|
a55fa04353 | ||
|
|
fde43c993e | ||
|
|
917f148fc4 | ||
|
|
34611c00e3 | ||
|
|
44a7c5867b | ||
|
|
75ddbc8851 | ||
|
|
2f1d31cf43 | ||
|
|
e7c5b266c8 | ||
|
|
384979bf94 | ||
|
|
b5a16935b7 | ||
|
|
320637e8dc | ||
|
|
77f3f028fa | ||
|
|
2a96e16902 | ||
|
|
1ed84eddd5 | ||
|
|
7b52b8689b | ||
|
|
d21700ee91 | ||
|
|
f87c890912 | ||
|
|
306b98edac |
@@ -32,7 +32,7 @@ set(CMAKE_C_STANDARD_REQUIRED true)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
include(GenerateExportHeader)
|
||||
set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}")
|
||||
set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}")
|
||||
if(UNIX AND APPLE)
|
||||
set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
@@ -46,7 +46,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc
|
||||
######## Set version numbers ########
|
||||
set(MultiMC_VERSION_MAJOR 0)
|
||||
set(MultiMC_VERSION_MINOR 6)
|
||||
set(MultiMC_VERSION_HOTFIX 5)
|
||||
set(MultiMC_VERSION_HOTFIX 6)
|
||||
|
||||
# Build number
|
||||
set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
|
||||
|
||||
@@ -443,6 +443,8 @@ set(FLAME_SOURCES
|
||||
modplatform/flame/PackManifest.cpp
|
||||
modplatform/flame/FileResolvingTask.h
|
||||
modplatform/flame/FileResolvingTask.cpp
|
||||
modplatform/flame/UrlResolvingTask.h
|
||||
modplatform/flame/UrlResolvingTask.cpp
|
||||
)
|
||||
|
||||
add_unit_test(Index
|
||||
|
||||
@@ -234,7 +234,7 @@ void InstanceList::deleteInstance(const InstanceId& id)
|
||||
auto inst = getInstanceById(id);
|
||||
if(!inst)
|
||||
{
|
||||
qDebug() << "Cannot delete instance" << id << " No such instance is present.";
|
||||
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -819,6 +819,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst
|
||||
instanceSet.insert(instID);
|
||||
m_groups.insert(groupName);
|
||||
emit instancesChanged();
|
||||
emit instanceSelectRequest(instID);
|
||||
}
|
||||
saveGroupList();
|
||||
return true;
|
||||
|
||||
@@ -129,6 +129,7 @@ public:
|
||||
signals:
|
||||
void dataIsInvalid();
|
||||
void instancesChanged();
|
||||
void instanceSelectRequest(QString instanceId);
|
||||
void groupsChanged(QSet<QString> groups);
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -635,6 +635,9 @@ void ComponentList::componentDataChanged()
|
||||
qWarning() << "ComponentList got dataChenged signal from a non-Component!";
|
||||
return;
|
||||
}
|
||||
if(objPtr->getID() == "net.minecraft") {
|
||||
emit minecraftChanged();
|
||||
}
|
||||
// figure out which one is it... in a seriously dumb way.
|
||||
int index = 0;
|
||||
for (auto component: d->components)
|
||||
|
||||
@@ -104,6 +104,9 @@ public:
|
||||
/// if there is a save scheduled, do it now.
|
||||
void saveNow();
|
||||
|
||||
signals:
|
||||
void minecraftChanged();
|
||||
|
||||
public:
|
||||
/// get the profile component by id
|
||||
Component * getComponent(const QString &id);
|
||||
|
||||
@@ -648,8 +648,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
|
||||
auto i = sessionRef.u.properties.begin();
|
||||
while (i != sessionRef.u.properties.end())
|
||||
{
|
||||
if(i.key() == "preferredLanguage")
|
||||
{
|
||||
if(i.value().length() <= 3) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -113,9 +113,6 @@ public:
|
||||
|
||||
virtual JavaVersion getJavaVersion() const;
|
||||
|
||||
signals:
|
||||
void versionReloaded();
|
||||
|
||||
protected:
|
||||
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
|
||||
QStringList validLaunchMethods();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "FileResolvingTask.h"
|
||||
#include "Json.h"
|
||||
|
||||
const char * metabase = "https://cursemeta.dries007.net";
|
||||
namespace {
|
||||
const char * metabase = "https://cursemeta.dries007.net";
|
||||
}
|
||||
|
||||
Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess)
|
||||
: m_toProcess(toProcess)
|
||||
@@ -34,70 +36,14 @@ void Flame::FileResolvingTask::netJobFinished()
|
||||
int index = 0;
|
||||
for(auto & bytes: results)
|
||||
{
|
||||
auto & out = m_toProcess.files[index];
|
||||
try
|
||||
{
|
||||
auto doc = Json::requireDocument(bytes);
|
||||
auto obj = Json::requireObject(doc);
|
||||
auto & out = m_toProcess.files[index];
|
||||
// result code signifies true failure.
|
||||
if(obj.contains("code"))
|
||||
{
|
||||
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a negative result:";
|
||||
qCritical() << bytes;
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
out.fileName = Json::requireString(obj, "FileNameOnDisk");
|
||||
QString rawUrl = Json::requireString(obj, "DownloadURL");
|
||||
out.url = QUrl(rawUrl, QUrl::TolerantMode);
|
||||
if(!out.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
|
||||
QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
|
||||
if(!projObj.isEmpty())
|
||||
{
|
||||
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
|
||||
if(strType == "singlefile")
|
||||
{
|
||||
out.type = File::Type::SingleFile;
|
||||
}
|
||||
else if(strType == "ctoc")
|
||||
{
|
||||
out.type = File::Type::Ctoc;
|
||||
}
|
||||
else if(strType == "cmod2")
|
||||
{
|
||||
out.type = File::Type::Cmod2;
|
||||
}
|
||||
else if(strType == "mod")
|
||||
{
|
||||
out.type = File::Type::Mod;
|
||||
}
|
||||
else if(strType == "folder")
|
||||
{
|
||||
out.type = File::Type::Folder;
|
||||
}
|
||||
else if(strType == "modpack")
|
||||
{
|
||||
out.type = File::Type::Modpack;
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of unknown file type:" << strType;
|
||||
out.type = File::Type::Unknown;
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
out.targetFolder = Json::ensureString(projObj, "Path", "mods");
|
||||
}
|
||||
out.resolved = true;
|
||||
failed &= (!out.parseFromBytes(bytes));
|
||||
}
|
||||
catch (const JSONValidationError &e)
|
||||
{
|
||||
auto & out = m_toProcess.files[index];
|
||||
|
||||
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
|
||||
qCritical() << e.cause();
|
||||
qCritical() << "JSON:";
|
||||
|
||||
@@ -64,3 +64,63 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
|
||||
}
|
||||
loadManifestV1(m, obj);
|
||||
}
|
||||
|
||||
bool Flame::File::parseFromBytes(const QByteArray& bytes)
|
||||
{
|
||||
auto doc = Json::requireDocument(bytes);
|
||||
auto obj = Json::requireObject(doc);
|
||||
// result code signifies true failure.
|
||||
if(obj.contains("code"))
|
||||
{
|
||||
qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
|
||||
qCritical() << bytes;
|
||||
return false;
|
||||
}
|
||||
fileName = Json::requireString(obj, "FileNameOnDisk");
|
||||
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
|
||||
QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
|
||||
if(!projObj.isEmpty())
|
||||
{
|
||||
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
|
||||
if(strType == "singlefile")
|
||||
{
|
||||
type = File::Type::SingleFile;
|
||||
}
|
||||
else if(strType == "ctoc")
|
||||
{
|
||||
type = File::Type::Ctoc;
|
||||
}
|
||||
else if(strType == "cmod2")
|
||||
{
|
||||
type = File::Type::Cmod2;
|
||||
}
|
||||
else if(strType == "mod")
|
||||
{
|
||||
type = File::Type::Mod;
|
||||
}
|
||||
else if(strType == "folder")
|
||||
{
|
||||
type = File::Type::Folder;
|
||||
}
|
||||
else if(strType == "modpack")
|
||||
{
|
||||
type = File::Type::Modpack;
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
|
||||
type = File::Type::Unknown;
|
||||
return false;
|
||||
}
|
||||
targetFolder = Json::ensureString(projObj, "Path", "mods");
|
||||
}
|
||||
resolved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace Flame
|
||||
{
|
||||
struct File
|
||||
{
|
||||
// NOTE: throws JSONValidationError
|
||||
bool parseFromBytes(const QByteArray &bytes);
|
||||
|
||||
int projectId = 0;
|
||||
int fileId = 0;
|
||||
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
|
||||
|
||||
175
api/logic/modplatform/flame/UrlResolvingTask.cpp
Normal file
175
api/logic/modplatform/flame/UrlResolvingTask.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "UrlResolvingTask.h"
|
||||
#include <QtXml>
|
||||
#include <Json.h>
|
||||
|
||||
|
||||
namespace {
|
||||
const char * metabase = "https://cursemeta.dries007.net";
|
||||
}
|
||||
|
||||
Flame::UrlResolvingTask::UrlResolvingTask(const QString& toProcess)
|
||||
: m_url(toProcess)
|
||||
{
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::executeTask()
|
||||
{
|
||||
resolveUrl();
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::resolveUrl()
|
||||
{
|
||||
setStatus(tr("Resolving URL..."));
|
||||
setProgress(0, 1);
|
||||
QUrl actualUrl(m_url);
|
||||
if(actualUrl.host() != "www.curseforge.com") {
|
||||
emitFailed(tr("Not a Twitch URL."));
|
||||
return;
|
||||
}
|
||||
m_dljob.reset(new NetJob("URL resolver"));
|
||||
|
||||
bool weAreDigging = false;
|
||||
needle = QString();
|
||||
|
||||
if(m_url.startsWith("https://")) {
|
||||
if(m_url.endsWith("?client=y")) {
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download?client=y
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088?client=y
|
||||
m_url.chop(9);
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088
|
||||
}
|
||||
if(m_url.endsWith("/download")) {
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download -> need to dig inside html...
|
||||
weAreDigging = true;
|
||||
needle = m_url;
|
||||
needle.replace("https://", "twitch://");
|
||||
needle.replace("/download", "/download-client/");
|
||||
m_url.append("?client=y");
|
||||
} else if (m_url.contains("/download/")) {
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download/2697088
|
||||
m_url.replace("/download/", "/download-client/");
|
||||
}
|
||||
}
|
||||
else if(m_url.startsWith("twitch://")) {
|
||||
// twitch://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
|
||||
m_url.replace(0, 9, "https://");
|
||||
// https://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
|
||||
}
|
||||
auto dl = Net::Download::makeByteArray(QUrl(m_url), &results);
|
||||
m_dljob->addNetAction(dl);
|
||||
if(weAreDigging) {
|
||||
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processHTML);
|
||||
} else {
|
||||
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processCCIP);
|
||||
}
|
||||
m_dljob->start();
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::processHTML()
|
||||
{
|
||||
QString htmlDoc = QString::fromUtf8(results);
|
||||
auto index = htmlDoc.indexOf(needle);
|
||||
if(index < 0) {
|
||||
emitFailed(tr("Couldn't find the needle in the haystack..."));
|
||||
return;
|
||||
}
|
||||
auto indexStart = index;
|
||||
int indexEnd = -1;
|
||||
while((index + 1) < htmlDoc.size() && htmlDoc[index] != '"') {
|
||||
index ++;
|
||||
if(htmlDoc[index] == '"') {
|
||||
indexEnd = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(indexEnd > 0) {
|
||||
QString found = htmlDoc.mid(indexStart, indexEnd - indexStart);
|
||||
qDebug() << "Found needle: " << found;
|
||||
// twitch://www.curseforge.com/minecraft/modpacks/ftb-sky-odyssey/download-client/2697088
|
||||
m_url = found;
|
||||
resolveUrl();
|
||||
return;
|
||||
}
|
||||
emitFailed(tr("Couldn't find the end of the needle in the haystack..."));
|
||||
return;
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::processCCIP()
|
||||
{
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(results)) {
|
||||
qDebug() << results;
|
||||
emitFailed(tr("Resolving failed."));
|
||||
return;
|
||||
}
|
||||
auto packageNode = doc.namedItem("package");
|
||||
if(!packageNode.isElement()) {
|
||||
emitFailed(tr("Resolving failed: missing package root element."));
|
||||
return;
|
||||
}
|
||||
auto projectNode = packageNode.namedItem("project");
|
||||
if(!projectNode.isElement()) {
|
||||
emitFailed(tr("Resolving failed: missing project element."));
|
||||
return;
|
||||
}
|
||||
auto attribs = projectNode.attributes();
|
||||
|
||||
auto projectIdNode = attribs.namedItem("id");
|
||||
if(!projectIdNode.isAttr()) {
|
||||
emitFailed(tr("Resolving failed: missing id attribute."));
|
||||
return;
|
||||
}
|
||||
auto fileIdNode = attribs.namedItem("file");
|
||||
if(!fileIdNode.isAttr()) {
|
||||
emitFailed(tr("Resolving failed: missing file attribute."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto projectId = projectIdNode.nodeValue();
|
||||
auto fileId = fileIdNode.nodeValue();
|
||||
bool success = true;
|
||||
m_result.projectId = projectId.toInt(&success);
|
||||
if(!success) {
|
||||
emitFailed(tr("Failed to resove projectId as a number."));
|
||||
return;
|
||||
}
|
||||
m_result.fileId = fileId.toInt(&success);
|
||||
if(!success) {
|
||||
emitFailed(tr("Failed to resove fileId as a number."));
|
||||
return;
|
||||
}
|
||||
qDebug() << "Resolved" << m_url << "as" << m_result.projectId << "/" << m_result.fileId;
|
||||
resolveIDs();
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::resolveIDs()
|
||||
{
|
||||
setStatus(tr("Resolving mod IDs..."));
|
||||
m_dljob.reset(new NetJob("Mod id resolver"));
|
||||
auto projectIdStr = QString::number(m_result.projectId);
|
||||
auto fileIdStr = QString::number(m_result.fileId);
|
||||
QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
|
||||
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results);
|
||||
m_dljob->addNetAction(dl);
|
||||
connect(m_dljob.get(), &NetJob::finished, this, &Flame::UrlResolvingTask::processCursemeta);
|
||||
m_dljob->start();
|
||||
}
|
||||
|
||||
void Flame::UrlResolvingTask::processCursemeta()
|
||||
{
|
||||
try {
|
||||
if(m_result.parseFromBytes(results)) {
|
||||
emitSucceeded();
|
||||
qDebug() << results;
|
||||
return;
|
||||
}
|
||||
} catch (const JSONValidationError &e) {
|
||||
|
||||
qCritical() << "Resolving of" << m_result.projectId << m_result.fileId << "failed because of a parsing error:";
|
||||
qCritical() << e.cause();
|
||||
qCritical() << "JSON:";
|
||||
qCritical() << results;
|
||||
}
|
||||
emitFailed(tr("Failed to resolve the modpack file."));
|
||||
}
|
||||
43
api/logic/modplatform/flame/UrlResolvingTask.h
Normal file
43
api/logic/modplatform/flame/UrlResolvingTask.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "tasks/Task.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "PackManifest.h"
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
namespace Flame
|
||||
{
|
||||
class MULTIMC_LOGIC_EXPORT UrlResolvingTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit UrlResolvingTask(const QString &toProcess);
|
||||
virtual ~UrlResolvingTask() {};
|
||||
|
||||
const Flame::File &getResults() const
|
||||
{
|
||||
return m_result;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void executeTask() override;
|
||||
|
||||
protected slots:
|
||||
void processCCIP();
|
||||
void processHTML();
|
||||
void processCursemeta();
|
||||
|
||||
private:
|
||||
void resolveUrl();
|
||||
void resolveIDs();
|
||||
|
||||
private: /* data */
|
||||
QString m_url;
|
||||
QString needle;
|
||||
Flame::File m_result;
|
||||
QByteArray results;
|
||||
NetJobPtr m_dljob;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,8 +36,10 @@ QString INIFile::unescape(QString orig)
|
||||
{
|
||||
if(c == 'n')
|
||||
out += '\n';
|
||||
else if (c == 't')
|
||||
else if(c == 't')
|
||||
out += '\t';
|
||||
else if(c == '#')
|
||||
out += '#';
|
||||
else
|
||||
out += c;
|
||||
prev = 0;
|
||||
@@ -67,6 +69,8 @@ QString INIFile::escape(QString orig)
|
||||
out += "\\t";
|
||||
else if(c == '\\')
|
||||
out += "\\\\";
|
||||
else if(c == '#')
|
||||
out += "\\#";
|
||||
else
|
||||
out += c;
|
||||
}
|
||||
@@ -120,7 +124,15 @@ bool INIFile::loadFile(QByteArray file)
|
||||
{
|
||||
QString &lineRaw = lines[i];
|
||||
// Ignore comments.
|
||||
QString line = lineRaw.left(lineRaw.indexOf('#')).trimmed();
|
||||
int commentIndex = 0;
|
||||
QString line = lineRaw;
|
||||
// Search for comments until no more escaped # are available
|
||||
while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
|
||||
if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
|
||||
continue;
|
||||
}
|
||||
line = line.left(lineRaw.indexOf('#')).trimmed();
|
||||
}
|
||||
|
||||
int eqPos = line.indexOf('=');
|
||||
if (eqPos == -1)
|
||||
|
||||
@@ -26,6 +26,7 @@ slots:
|
||||
QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet.";
|
||||
QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet.";
|
||||
QTest::newRow("Escape sequences 2") << "\"\n\n\"";
|
||||
QTest::newRow("Hashtags") << "some data#something";
|
||||
}
|
||||
void test_Escape()
|
||||
{
|
||||
@@ -40,7 +41,7 @@ slots:
|
||||
void test_SaveLoad()
|
||||
{
|
||||
QString a = "a";
|
||||
QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\";
|
||||
QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment";
|
||||
QString filename = "test_SaveLoad.ini";
|
||||
|
||||
// save
|
||||
|
||||
@@ -131,7 +131,14 @@ void DownloadTask::processDownloadedVersionInfo()
|
||||
QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
|
||||
QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed);
|
||||
|
||||
setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
|
||||
if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/MultiMC5/issues/1701
|
||||
{
|
||||
setStatus(tr("Downloading one update file."));
|
||||
}
|
||||
else
|
||||
{
|
||||
setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
|
||||
}
|
||||
qDebug() << "Begin downloading update files to" << m_updateFilesDir.path();
|
||||
m_filesNetJob = netJob;
|
||||
m_filesNetJob->start();
|
||||
|
||||
@@ -661,7 +661,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
|
||||
// Create the instance list widget
|
||||
{
|
||||
view = new GroupView(ui->centralWidget);
|
||||
view->setTextElideMode(Qt::TextElideMode::ElideRight);
|
||||
|
||||
view->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
// FIXME: leaks ListViewDelegate
|
||||
@@ -703,6 +702,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
|
||||
// model reset -> selection is invalid. All the instance pointers are wrong.
|
||||
connect(MMC->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad);
|
||||
|
||||
// handle newly added instances
|
||||
connect(MMC->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest);
|
||||
|
||||
// When the global settings page closes, we want to know about it and update our state
|
||||
connect(MMC, &MultiMC::globalSettingsClosed, this, &MainWindow::globalSettingsClosed);
|
||||
|
||||
@@ -1665,6 +1667,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto id = m_selectedInstance->id();
|
||||
auto response = CustomMessageBox::selectable(
|
||||
this,
|
||||
tr("CAREFUL!"),
|
||||
@@ -1675,7 +1678,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
|
||||
)->exec();
|
||||
if (response == QMessageBox::Yes)
|
||||
{
|
||||
MMC->instances()->deleteInstance(m_selectedInstance->id());
|
||||
MMC->instances()->deleteInstance(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1837,6 +1840,11 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex &
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::instanceSelectRequest(QString id)
|
||||
{
|
||||
setSelectedInstanceById(id);
|
||||
}
|
||||
|
||||
void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
|
||||
{
|
||||
auto current = view->selectionModel()->currentIndex();
|
||||
|
||||
@@ -152,6 +152,8 @@ private slots:
|
||||
|
||||
void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
|
||||
void instanceSelectRequest(QString id);
|
||||
|
||||
void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
|
||||
|
||||
void selectionBad();
|
||||
|
||||
@@ -488,6 +488,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
||||
m_settings->registerSetting("InstSortMode", "Name");
|
||||
m_settings->registerSetting("SelectedInstance", QString());
|
||||
|
||||
// Offline mode stuff
|
||||
m_settings->registerSetting("OfflineModeNameMode", "UseAccountName");
|
||||
m_settings->registerSetting("OfflineModeName", "Player");
|
||||
|
||||
// Window state and geometry
|
||||
m_settings->registerSetting("MainWindowState", "");
|
||||
m_settings->registerSetting("MainWindowGeometry", "");
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
#include <pages/modplatform/ImportPage.h>
|
||||
#include <pages/modplatform/TechnicPage.h>
|
||||
|
||||
|
||||
|
||||
NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
|
||||
: QDialog(parent), ui(new Ui::NewInstanceDialog)
|
||||
{
|
||||
@@ -94,8 +96,15 @@ NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString
|
||||
|
||||
if(!url.isEmpty())
|
||||
{
|
||||
m_container->selectPage("import");
|
||||
importPage->setUrl(url);
|
||||
QUrl actualUrl(url);
|
||||
if(actualUrl.host() == "www.curseforge.com") {
|
||||
m_container->selectPage("twitch");
|
||||
twitchPage->setUrl(url);
|
||||
}
|
||||
else {
|
||||
m_container->selectPage("import");
|
||||
importPage->setUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
updateDialogState();
|
||||
@@ -119,13 +128,13 @@ void NewInstanceDialog::accept()
|
||||
QList<BasePage *> NewInstanceDialog::getPages()
|
||||
{
|
||||
importPage = new ImportPage(this);
|
||||
twitchPage = new TwitchPage(this);
|
||||
return
|
||||
{
|
||||
new VanillaPage(this),
|
||||
new FTBPage(this),
|
||||
importPage,
|
||||
new TwitchPage(this),
|
||||
new TechnicPage(this)
|
||||
twitchPage,
|
||||
new FTBPage(this)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class NewInstanceDialog;
|
||||
class PageContainer;
|
||||
class QDialogButtonBox;
|
||||
class ImportPage;
|
||||
class TwitchPage;
|
||||
|
||||
class NewInstanceDialog : public QDialog, public BasePageProvider
|
||||
{
|
||||
@@ -67,6 +68,7 @@ private:
|
||||
|
||||
QString InstIconKey;
|
||||
ImportPage *importPage = nullptr;
|
||||
TwitchPage *twitchPage = nullptr;
|
||||
std::unique_ptr<InstanceTask> creationTask;
|
||||
|
||||
bool importIcon = false;
|
||||
|
||||
@@ -220,6 +220,8 @@ VisualGroup *GroupView::categoryAt(const QPoint &pos, VisualGroup::HitResults &
|
||||
|
||||
QString GroupView::groupNameAt(const QPoint &point)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
VisualGroup::HitResults hitresult;
|
||||
auto group = categoryAt(point + offset(), hitresult);
|
||||
if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit)))
|
||||
@@ -246,7 +248,7 @@ int GroupView::itemWidth() const
|
||||
|
||||
void GroupView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
// endCategoryEditor();
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
QPoint visualPos = event->pos();
|
||||
QPoint geometryPos = event->pos() + offset();
|
||||
@@ -295,6 +297,8 @@ void GroupView::mousePressEvent(QMouseEvent *event)
|
||||
|
||||
void GroupView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
QPoint topLeft;
|
||||
QPoint visualPos = event->pos();
|
||||
QPoint geometryPos = event->pos() + offset();
|
||||
@@ -351,6 +355,8 @@ void GroupView::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void GroupView::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
QPoint visualPos = event->pos();
|
||||
QPoint geometryPos = event->pos() + offset();
|
||||
QPersistentModelIndex index = indexAt(visualPos);
|
||||
@@ -405,6 +411,8 @@ void GroupView::mouseReleaseEvent(QMouseEvent *event)
|
||||
|
||||
void GroupView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
QModelIndex index = indexAt(event->pos());
|
||||
if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
|
||||
{
|
||||
@@ -528,6 +536,8 @@ void GroupView::resizeEvent(QResizeEvent *event)
|
||||
|
||||
void GroupView::dragEnterEvent(QDragEnterEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
if (!isDragEventAccepted(event))
|
||||
{
|
||||
return;
|
||||
@@ -539,6 +549,8 @@ void GroupView::dragEnterEvent(QDragEnterEvent *event)
|
||||
|
||||
void GroupView::dragMoveEvent(QDragMoveEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
if (!isDragEventAccepted(event))
|
||||
{
|
||||
return;
|
||||
@@ -550,12 +562,16 @@ void GroupView::dragMoveEvent(QDragMoveEvent *event)
|
||||
|
||||
void GroupView::dragLeaveEvent(QDragLeaveEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
m_lastDragPosition = QPoint();
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void GroupView::dropEvent(QDropEvent *event)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
m_lastDragPosition = QPoint();
|
||||
|
||||
stopAutoScroll();
|
||||
@@ -606,6 +622,8 @@ void GroupView::dropEvent(QDropEvent *event)
|
||||
|
||||
void GroupView::startDrag(Qt::DropActions supportedActions)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
QModelIndexList indexes = selectionModel()->selectedIndexes();
|
||||
if(indexes.count() == 0)
|
||||
return;
|
||||
@@ -651,11 +669,15 @@ void GroupView::startDrag(Qt::DropActions supportedActions)
|
||||
|
||||
QRect GroupView::visualRect(const QModelIndex &index) const
|
||||
{
|
||||
const_cast<GroupView*>(this)->executeDelayedItemsLayout();
|
||||
|
||||
return geometryRect(index).translated(-offset());
|
||||
}
|
||||
|
||||
QRect GroupView::geometryRect(const QModelIndex &index) const
|
||||
{
|
||||
const_cast<GroupView*>(this)->executeDelayedItemsLayout();
|
||||
|
||||
if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
|
||||
{
|
||||
return QRect();
|
||||
@@ -695,9 +717,10 @@ QModelIndex GroupView::indexAt(const QPoint &point) const
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
void GroupView::setSelection(const QRect &rect,
|
||||
const QItemSelectionModel::SelectionFlags commands)
|
||||
void GroupView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands)
|
||||
{
|
||||
executeDelayedItemsLayout();
|
||||
|
||||
for (int i = 0; i < model()->rowCount(); ++i)
|
||||
{
|
||||
QModelIndex index = model()->index(i, 0);
|
||||
@@ -732,8 +755,7 @@ QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) cons
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices,
|
||||
QRect *r) const
|
||||
QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const
|
||||
{
|
||||
Q_ASSERT(r);
|
||||
QRect &rect = *r;
|
||||
|
||||
@@ -34,6 +34,7 @@ static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &he
|
||||
height = 0;
|
||||
widthUsed = 0;
|
||||
textLayout.beginLayout();
|
||||
QString str = textLayout.text();
|
||||
while (true)
|
||||
{
|
||||
QTextLine line = textLayout.createLine();
|
||||
@@ -189,13 +190,11 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
|
||||
|
||||
QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
|
||||
|
||||
// FIXME: Things go really weird with long instance names
|
||||
// const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize);
|
||||
const int iconSize = 48;
|
||||
QRect iconbox = opt.rect;
|
||||
const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, opt.widget) + 1;
|
||||
const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, opt.widget) + 1;
|
||||
QRect textRect = opt.rect;
|
||||
textRect.setWidth(qMin(textRect.width(), iconbox.width()));
|
||||
QRect textHighlightRect = textRect;
|
||||
// clip the decoration on top, remove width padding
|
||||
textRect.adjust(textMargin, iconSize + textMargin + 5, -textMargin, 0);
|
||||
@@ -300,7 +299,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti
|
||||
const int lineCount = textLayout.lineCount();
|
||||
|
||||
const QRect layoutRect = QStyle::alignedRect(
|
||||
opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect);
|
||||
opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect);
|
||||
const QPointF position = layoutRect.topLeft();
|
||||
for (int i = 0; i < lineCount; ++i)
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
#endif
|
||||
|
||||
// initialize Qt
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
Package: multimc
|
||||
Version: 1.2-1
|
||||
Version: 1.3-1
|
||||
Architecture: all
|
||||
Maintainer: Petr Mrázek <peterix@gmail.com>
|
||||
Section: games
|
||||
Priority: optional
|
||||
Installed-Size: 75
|
||||
Depends: zenity, desktop-file-utils
|
||||
Depends: zenity, desktop-file-utils, qt5-default
|
||||
Recommends: openjdk-8-jre
|
||||
Homepage: http://multimc.org
|
||||
Description: A local install wrapper for MultiMC
|
||||
|
||||
@@ -4,9 +4,9 @@ A simple ubuntu package for MultiMC that wraps the contains a script that downlo
|
||||
It contains a `.desktop` file, an icon, and a simple script that does the heavy lifting.
|
||||
|
||||
# How to build this?
|
||||
You need dpkg utils. Rename the `multimc` folder to `multimc_1.2-1` and then run:
|
||||
You need dpkg utils. Rename the `multimc` folder to `multimc_1.3-1` and then run:
|
||||
```
|
||||
fakeroot dpkg-deb --build multimc_1.2-1
|
||||
fakeroot dpkg-deb --build multimc_1.3-1
|
||||
```
|
||||
|
||||
Replace the version with whatever is appropriate.
|
||||
|
||||
@@ -33,11 +33,18 @@
|
||||
|
||||
#include "MultiMC.h"
|
||||
|
||||
enum class OfflineModeNameMode
|
||||
{
|
||||
UseAccountName = 1,
|
||||
RememberPerAccount = 2,
|
||||
RememberPerInstance = 3,
|
||||
UseFixedName = 4
|
||||
};
|
||||
|
||||
AccountListPage::AccountListPage(QWidget *parent)
|
||||
: QWidget(parent), ui(new Ui::AccountListPage)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->tabWidget->tabBar()->hide();
|
||||
|
||||
m_accounts = MMC->accounts();
|
||||
|
||||
@@ -56,7 +63,15 @@ AccountListPage::AccountListPage(QWidget *parent)
|
||||
connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged()));
|
||||
connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged()));
|
||||
|
||||
ui->offlineButtonGroup->setId(ui->useSelectedNameBtn, int(OfflineModeNameMode::UseAccountName));
|
||||
ui->offlineButtonGroup->setId(ui->rememberNamesForAccountsBtn, int(OfflineModeNameMode::RememberPerAccount));
|
||||
ui->offlineButtonGroup->setId(ui->rememberNamesForInstancesBtn, int(OfflineModeNameMode::RememberPerInstance));
|
||||
ui->offlineButtonGroup->setId(ui->useFixedNameBtn, int(OfflineModeNameMode::UseFixedName));
|
||||
|
||||
connect(ui->offlineButtonGroup, SIGNAL(buttonToggled(int,bool)), this, SLOT(groupSelectionChanged(int,bool)));
|
||||
|
||||
updateButtonStates();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
AccountListPage::~AccountListPage()
|
||||
@@ -151,3 +166,68 @@ void AccountListPage::on_uploadSkinBtn_clicked()
|
||||
dialog.exec();
|
||||
}
|
||||
}
|
||||
|
||||
bool AccountListPage::apply()
|
||||
{
|
||||
applySettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AccountListPage::applySettings()
|
||||
{
|
||||
auto s = MMC->settings();
|
||||
auto sortMode = (OfflineModeNameMode)ui->offlineButtonGroup->checkedId();
|
||||
switch (sortMode)
|
||||
{
|
||||
default:
|
||||
case OfflineModeNameMode::UseAccountName:
|
||||
s->set("OfflineModeNameMode", "UseAccountName");
|
||||
break;
|
||||
case OfflineModeNameMode::RememberPerAccount:
|
||||
s->set("OfflineModeNameMode", "RememberPerAccount");
|
||||
break;
|
||||
case OfflineModeNameMode::RememberPerInstance:
|
||||
s->set("OfflineModeNameMode", "RememberPerInstance");
|
||||
break;
|
||||
case OfflineModeNameMode::UseFixedName:
|
||||
s->set("OfflineModeNameMode", "UseFixedName");
|
||||
break;
|
||||
}
|
||||
s->set("OfflineModeName", ui->mainOfflineNameEdit->text());
|
||||
}
|
||||
|
||||
void AccountListPage::loadSettings()
|
||||
{
|
||||
auto s = MMC->settings();
|
||||
auto value = s->get("OfflineModeNameMode").toString();
|
||||
if(value == "UseAccountName")
|
||||
{
|
||||
ui->useSelectedNameBtn->setChecked(true);
|
||||
}
|
||||
else if(value == "RememberPerAccount")
|
||||
{
|
||||
ui->rememberNamesForAccountsBtn->setChecked(true);
|
||||
}
|
||||
else if(value == "RememberPerInstance")
|
||||
{
|
||||
ui->rememberNamesForInstancesBtn->setChecked(true);
|
||||
}
|
||||
else if(value == "UseFixedName")
|
||||
{
|
||||
ui->useFixedNameBtn->setChecked(true);
|
||||
}
|
||||
ui->mainOfflineNameEdit->setText(s->get("OfflineModeName").toString());
|
||||
}
|
||||
|
||||
void AccountListPage::groupSelectionChanged(int, bool)
|
||||
{
|
||||
auto sortMode = (OfflineModeNameMode)ui->offlineButtonGroup->checkedId();
|
||||
if(sortMode == OfflineModeNameMode::UseFixedName)
|
||||
{
|
||||
ui->mainOfflineNameEdit->setEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->mainOfflineNameEdit->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
{
|
||||
return "Getting-Started#adding-an-account";
|
||||
}
|
||||
bool apply() override;
|
||||
|
||||
public
|
||||
slots:
|
||||
@@ -73,6 +74,8 @@ slots:
|
||||
|
||||
void listChanged();
|
||||
|
||||
void groupSelectionChanged(int, bool);
|
||||
|
||||
//! Updates the states of the dialog's buttons.
|
||||
void updateButtonStates();
|
||||
|
||||
@@ -83,6 +86,10 @@ protected
|
||||
slots:
|
||||
void addAccount(const QString& errMsg="");
|
||||
|
||||
private:
|
||||
void applySettings();
|
||||
void loadSettings();
|
||||
|
||||
private:
|
||||
Ui::AccountListPage *ui;
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>694</width>
|
||||
<height>609</height>
|
||||
<width>333</width>
|
||||
<height>302</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
@@ -28,9 +28,9 @@
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<widget class="QWidget" name="onlineTab">
|
||||
<attribute name="title">
|
||||
<string notr="true">Tab 1</string>
|
||||
<string>Online</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
@@ -113,10 +113,89 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="offlineTab">
|
||||
<attribute name="title">
|
||||
<string>Offline</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout22">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="offlineBox">
|
||||
<property name="title">
|
||||
<string>Offline mode name</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="foldersBoxLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="useSelectedNameBtn">
|
||||
<property name="text">
|
||||
<string>&Use selected account name</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">offlineButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QRadioButton" name="useFixedNameBtn">
|
||||
<property name="text">
|
||||
<string>Always use &this offline name:</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">offlineButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLineEdit" name="mainOfflineNameEdit">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="rememberNamesForAccountsBtn">
|
||||
<property name="text">
|
||||
<string>Remember offline &names per account</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">offlineButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QRadioButton" name="rememberNamesForInstancesBtn">
|
||||
<property name="text">
|
||||
<string>Remember offline &names per instance</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">offlineButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<buttongroups>
|
||||
<buttongroup name="offlineButtonGroup"/>
|
||||
</buttongroups>
|
||||
</ui>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "settings/SettingsObject.h"
|
||||
#include "MultiMC.h"
|
||||
#include "Env.h"
|
||||
|
||||
ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
|
||||
{
|
||||
@@ -75,6 +76,9 @@ void ProxyPage::applySettings()
|
||||
s->set("ProxyPort", ui->proxyPortEdit->value());
|
||||
s->set("ProxyUser", ui->proxyUserEdit->text());
|
||||
s->set("ProxyPass", ui->proxyPassEdit->text());
|
||||
|
||||
ENV.updateProxySettings(proxyType, ui->proxyAddrEdit->text(), ui->proxyPortEdit->value(),
|
||||
ui->proxyUserEdit->text(), ui->proxyPassEdit->text());
|
||||
}
|
||||
void ProxyPage::loadSettings()
|
||||
{
|
||||
|
||||
@@ -286,6 +286,38 @@ void ScreenshotsPage::on_uploadBtn_clicked()
|
||||
|
||||
QList<ScreenshotPtr> uploaded;
|
||||
auto job = NetJobPtr(new NetJob("Screenshot Upload"));
|
||||
if(selection.size() < 2)
|
||||
{
|
||||
auto item = selection.at(0);
|
||||
auto info = m_model->fileInfo(item);
|
||||
auto screenshot = std::make_shared<ScreenShot>(info);
|
||||
job->addNetAction(ImgurUpload::make(screenshot));
|
||||
|
||||
m_uploadActive = true;
|
||||
ProgressDialog dialog(this);
|
||||
if(dialog.execWithTask(job.get()) != QDialog::Accepted)
|
||||
{
|
||||
CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
|
||||
tr("Unknown error"), QMessageBox::Warning)->exec();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto link = screenshot->m_url;
|
||||
QClipboard *clipboard = QApplication::clipboard();
|
||||
clipboard->setText(link);
|
||||
CustomMessageBox::selectable(
|
||||
this,
|
||||
tr("Upload finished"),
|
||||
tr("The <a href=\"%1\">link to the uploaded screenshot</a> has been placed in your clipboard.")
|
||||
.arg(link),
|
||||
QMessageBox::Information
|
||||
)->exec();
|
||||
}
|
||||
|
||||
m_uploadActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto item : selection)
|
||||
{
|
||||
auto info = m_model->fileInfo(item);
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "minecraft/Mod.h"
|
||||
#include "icons/IconList.h"
|
||||
#include "Exception.h"
|
||||
#include "Version.h"
|
||||
|
||||
#include "MultiMC.h"
|
||||
|
||||
@@ -108,26 +109,18 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
|
||||
|
||||
reloadComponentList();
|
||||
|
||||
if (m_profile)
|
||||
{
|
||||
auto proxy = new IconProxy(ui->packageView);
|
||||
proxy->setSourceModel(m_profile.get());
|
||||
ui->packageView->setModel(proxy);
|
||||
ui->packageView->installEventFilter(this);
|
||||
ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent);
|
||||
auto smodel = ui->packageView->selectionModel();
|
||||
connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
|
||||
updateVersionControls();
|
||||
// select first item.
|
||||
preselect(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
disableVersionControls();
|
||||
}
|
||||
connect(m_inst, &MinecraftInstance::versionReloaded, this,
|
||||
&VersionPage::updateVersionControls);
|
||||
auto proxy = new IconProxy(ui->packageView);
|
||||
proxy->setSourceModel(m_profile.get());
|
||||
ui->packageView->setModel(proxy);
|
||||
ui->packageView->installEventFilter(this);
|
||||
ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent);
|
||||
auto smodel = ui->packageView->selectionModel();
|
||||
connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
|
||||
|
||||
updateVersionControls();
|
||||
preselect(0);
|
||||
connect(m_profile.get(), &ComponentList::minecraftChanged, this, &VersionPage::updateVersionControls);
|
||||
}
|
||||
|
||||
VersionPage::~VersionPage()
|
||||
@@ -180,18 +173,21 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &
|
||||
|
||||
void VersionPage::updateVersionControls()
|
||||
{
|
||||
ui->fabricBtn->setEnabled(true);
|
||||
ui->forgeBtn->setEnabled(true);
|
||||
ui->liteloaderBtn->setEnabled(true);
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void VersionPage::disableVersionControls()
|
||||
{
|
||||
ui->fabricBtn->setEnabled(false);
|
||||
ui->forgeBtn->setEnabled(false);
|
||||
ui->liteloaderBtn->setEnabled(false);
|
||||
ui->reloadBtn->setEnabled(false);
|
||||
// FIXME: this is a dirty hack
|
||||
if(m_profile) {
|
||||
auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
|
||||
bool newCraft = minecraftVersion >= Version("1.14");
|
||||
bool oldCraft = minecraftVersion <= Version("1.12.2");
|
||||
ui->fabricBtn->setEnabled(newCraft);
|
||||
ui->forgeBtn->setEnabled(oldCraft);
|
||||
ui->liteloaderBtn->setEnabled(oldCraft);
|
||||
}
|
||||
else {
|
||||
ui->fabricBtn->setEnabled(false);
|
||||
ui->forgeBtn->setEnabled(false);
|
||||
ui->liteloaderBtn->setEnabled(false);
|
||||
ui->reloadBtn->setEnabled(false);
|
||||
}
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ private slots:
|
||||
void on_downloadBtn_clicked();
|
||||
|
||||
void updateVersionControls();
|
||||
void disableVersionControls();
|
||||
void on_changeVersionBtn_clicked();
|
||||
|
||||
private:
|
||||
|
||||
@@ -75,6 +75,11 @@ void ImportPage::updateState()
|
||||
}
|
||||
else
|
||||
{
|
||||
if(input.endsWith("?client=y")) {
|
||||
input.chop(9);
|
||||
input.append("/file");
|
||||
url = QUrl::fromUserInput(input);
|
||||
}
|
||||
// hook, line and sinker.
|
||||
QFileInfo fi(url.fileName());
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
#include "MultiMC.h"
|
||||
#include "dialogs/NewInstanceDialog.h"
|
||||
#include <InstanceImportTask.h>
|
||||
|
||||
TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
|
||||
: QWidget(parent), ui(new Ui::TwitchPage), dialog(dialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
connect(ui->checkButton, &QPushButton::clicked, this, &TwitchPage::triggerCheck);
|
||||
}
|
||||
|
||||
TwitchPage::~TwitchPage()
|
||||
@@ -17,10 +19,42 @@ TwitchPage::~TwitchPage()
|
||||
|
||||
bool TwitchPage::shouldDisplay() const
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TwitchPage::openedImpl()
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
|
||||
void TwitchPage::triggerCheck(bool)
|
||||
{
|
||||
if(m_modIdResolver) {
|
||||
return;
|
||||
}
|
||||
auto task = new Flame::UrlResolvingTask(ui->lineEdit->text());
|
||||
connect(task, &Task::finished, this, &TwitchPage::checkDone);
|
||||
m_modIdResolver.reset(task);
|
||||
task->start();
|
||||
}
|
||||
|
||||
void TwitchPage::setUrl(const QString& url)
|
||||
{
|
||||
ui->lineEdit->setText(url);
|
||||
triggerCheck(true);
|
||||
}
|
||||
|
||||
void TwitchPage::checkDone()
|
||||
{
|
||||
auto result = m_modIdResolver->getResults();
|
||||
auto formatted = QString("Project %1, File %2").arg(result.projectId).arg(result.fileId);
|
||||
if(result.resolved && result.type == Flame::File::Type::Modpack) {
|
||||
ui->twitchLabel->setText(formatted);
|
||||
QFileInfo fi(result.fileName);
|
||||
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(result.url));
|
||||
} else {
|
||||
ui->twitchLabel->setPixmap(QPixmap(QString::fromUtf8(":/assets/deadglitch")));
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
m_modIdResolver.reset();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "pages/BasePage.h"
|
||||
#include <MultiMC.h>
|
||||
#include "tasks/Task.h"
|
||||
#include "modplatform/flame/UrlResolvingTask.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
@@ -37,7 +38,7 @@ public:
|
||||
virtual ~TwitchPage();
|
||||
virtual QString displayName() const override
|
||||
{
|
||||
return tr("Twitch");
|
||||
return tr("Twitch URL");
|
||||
}
|
||||
virtual QIcon icon() const override
|
||||
{
|
||||
@@ -55,7 +56,14 @@ public:
|
||||
|
||||
void openedImpl() override;
|
||||
|
||||
void setUrl(const QString & url);
|
||||
|
||||
private slots:
|
||||
void triggerCheck(bool checked);
|
||||
void checkDone();
|
||||
|
||||
private:
|
||||
Ui::TwitchPage *ui = nullptr;
|
||||
NewInstanceDialog* dialog = nullptr;
|
||||
shared_qobject_ptr<Flame::UrlResolvingTask> m_modIdResolver;
|
||||
};
|
||||
|
||||
@@ -10,9 +10,25 @@
|
||||
<height>405</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Twitch URL:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="3">
|
||||
<widget class="QLabel" name="twitchLabel">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>40</pointsize>
|
||||
@@ -26,8 +42,19 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="checkButton">
|
||||
<property name="text">
|
||||
<string>Check</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>lineEdit</tabstop>
|
||||
<tabstop>checkButton</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../resources/assets/assets.qrc"/>
|
||||
</resources>
|
||||
|
||||
@@ -23,6 +23,7 @@ VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
|
||||
connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
|
||||
connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
|
||||
connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
|
||||
connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
|
||||
connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
|
||||
}
|
||||
|
||||
@@ -58,6 +59,8 @@ void VanillaPage::filterChanged()
|
||||
out << "(old_snapshot)";
|
||||
if(ui->releaseFilter->isChecked())
|
||||
out << "(release)";
|
||||
if(ui->experimentsFilter->isChecked())
|
||||
out << "(experiment)";
|
||||
auto regexp = out.join('|');
|
||||
ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
|
||||
}
|
||||
|
||||
@@ -98,6 +98,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="experimentsFilter">
|
||||
<property name="text">
|
||||
<string>Experiments</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
@@ -144,6 +154,16 @@
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>releaseFilter</tabstop>
|
||||
<tabstop>snapshotFilter</tabstop>
|
||||
<tabstop>oldSnapshotFilter</tabstop>
|
||||
<tabstop>betaFilter</tabstop>
|
||||
<tabstop>alphaFilter</tabstop>
|
||||
<tabstop>experimentsFilter</tabstop>
|
||||
<tabstop>refreshBtn</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
90
changelog.md
90
changelog.md
@@ -1,8 +1,91 @@
|
||||
# MultiMC 0.6.5
|
||||
# MultiMC 0.6.6
|
||||
|
||||
This release is mostly the smaller things that have accumulated over time, along with a big change in linux packaging.
|
||||
|
||||
No 1.13+ Forge news yet. That's going to be a major overhaul of many of the internals of MultiMC.
|
||||
|
||||
## **IMPORTANT**
|
||||
|
||||
On linux, MultiMC no longer bundles the Qt libraries. This fixes many issues, but it might not run after the update unless you have the required libraries installed.
|
||||
|
||||
Make sure you have the following packages before you update:
|
||||
|
||||
- Arch: `qt5-base`
|
||||
- Debian/Ubuntu: `qt5-default`
|
||||
- CentOS/RHEL/Fedora: `qt5-qtbase-gui`
|
||||
- Suse: `libqt5-qtbase`
|
||||
|
||||
MultiMC on linux is built with Qt 5.4 and older versions of Qt will not work.
|
||||
|
||||
This should be a massive improvement to system integration on linux and resolves GH-1784, GH-2605, GH-1979, GH-2271, GH-1992, GH-1816 and their many duplicates.
|
||||
|
||||
### New or changed features
|
||||
|
||||
- GH-2487: No is now the default button when deleting instances.
|
||||
|
||||
- It is now possible to launch with profilers in offline mode.
|
||||
|
||||
- Massively improved support for icon formats when importing and exporting instances.
|
||||
|
||||
All of the formats MultiMC supports are now supported in exported instances too, instead of just PNG.
|
||||
|
||||
- Added the pocket fox icon.
|
||||
|
||||
We still have the big one under the staircase. It's cute. Just hide your chickens.
|
||||
|
||||
- Global settings can be opened from instance settings where appropriate.
|
||||
|
||||
Many people use the instance overrides where using the global settings would be more appropriate. Hopefully this makes it clearer that the instance settings are overrides for the global settings.
|
||||
|
||||
- Added direct Fabric loader support.
|
||||
|
||||
Much overdue. It's good. Fabric mod metadata is also supported in the mod pages.
|
||||
|
||||
- MultiMC now recognizes the new `experimental` Minecraft versions.
|
||||
|
||||
Go mess with the combat experiment. It's interesting.
|
||||
|
||||
- Added Twitch URL as an option to the Add Instance dialog.
|
||||
|
||||
You can now drag the purple download buttons from CurseForge into MultiMC and get a modpack out of it. Much easier!
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Translation folder is now created sooner, making first launch translation fetch work again.
|
||||
|
||||
- GH-2716: MultiMC will no longer try to censor values shorter than 4 characters in logs.
|
||||
|
||||
It was actually leaking information and destroying the logs instead of helping.
|
||||
|
||||
- GH-2551: Trim server name and IP before saving them.
|
||||
|
||||
- GH-2591: Fix multiple potential memory leaks and crashes related to destroying objects with Qt memory lifecycle model.
|
||||
|
||||
- `run.sh` on linux now passes all arguments to MultiMC.
|
||||
|
||||
- Adding a disabled mod duplicate now replaces the existing mod.
|
||||
|
||||
- GH-2592: Newly created instances are now selected again. This was a very old regression.
|
||||
|
||||
- GH-689: MultiMC no longer creates an imgur album for single screenshot uploads.
|
||||
|
||||
- GH-1813: `#` is now saved properly when used in instance notes.
|
||||
|
||||
- GH-2515: Deleting an instance externally while the delete dialog is open no longer leads to some other instance being deleted when you click OK.
|
||||
|
||||
- GH-2499: Proxy settings are applied immediately and no longer need an application restart.
|
||||
|
||||
- GH-1701: When downloading updates, the text now reflects the number of downloaded files better.
|
||||
|
||||
- Icon scaling issues on macOS should now be fixed.
|
||||
|
||||
# Previous releases
|
||||
|
||||
## MultiMC 0.6.5
|
||||
|
||||
Finalizing the translation workflow improvements and adding fixes for sounds missing in old game versions.
|
||||
|
||||
### New or changed features
|
||||
#### New or changed features
|
||||
|
||||
- UI for the language settings has been unified across the application
|
||||
|
||||
@@ -12,7 +95,6 @@ Finalizing the translation workflow improvements and adding fixes for sounds mis
|
||||
|
||||
Also, a minor issue with the reconstruction being done twice per launch has been fixed.
|
||||
|
||||
# Previous releases
|
||||
|
||||
## MultiMC 0.6.4
|
||||
|
||||
@@ -1088,4 +1170,4 @@ Long time coming, this release brought a lot of incremental improvements and fix
|
||||
- Added additional information to the about dialog.
|
||||
|
||||
## MultiMC 0.0
|
||||
- Initial release.
|
||||
- Initial release.
|
||||
|
||||
Reference in New Issue
Block a user