Compare commits

..

36 Commits

Author SHA1 Message Date
Petr Mrázek
7b0a812db7 WIP break stuff 2019-07-13 23:31:25 +02:00
Petr Mrázek
62e1bf327d Merge pull request #2740 from jturnism/patch-1
Update changelog.md
2019-07-12 08:19:20 +02:00
Joseph Turner
280e0e6e36 Update changelog.md
Just installing "qt5-qtbase" on Fedora 30 does not allow MultiMC to run. It still needs "libQt5Widgets.so.5" and by running "dnf whatprovides" it tells me "qt5-qtbase-gui" provides that file and also pulls in "qt5-qtbase" as a dependency if not already installed. I am assuming this is the same situation for CentOS/RHEL
2019-07-11 21:00:55 -05:00
Petr Mrázek
3a67990acd NOISSUE bump deb package version 2019-07-11 01:28:48 +02:00
Petr Mrázek
23eab74e6d NOISSUE make the deb package depend on Qt5 2019-07-11 01:26:33 +02:00
Petr Mrázek
b9d4293552 NOISSUE update component buttons some more when the versions change 2019-07-11 01:01:47 +02:00
Petr Mrázek
5110b58def NOISSUE update version and changelog 2019-07-11 00:35:44 +02:00
Petr Mrázek
791a8227b6 NOISSUE disable component install buttons in impossible cases 2019-07-10 22:30:42 +02:00
Petr Mrázek
725ec35635 NOISSUE recognize curseforge URLs dropped on top of MultiMC 2019-07-09 22:04:52 +02:00
Petr Mrázek
739a86f171 Revert "NOISSUE Import page is now a MultiMC pack page"
This reverts commit f74e3db804.
2019-07-09 21:51:19 +02:00
Petr Mrázek
48b2f95129 Revert "NOISSUE simple/stupid default game options, UI only"
This reverts commit 497d9bec02.
2019-07-09 21:43:12 +02:00
Petr Mrázek
497d9bec02 NOISSUE simple/stupid default game options, UI only 2019-07-09 02:37:04 +02:00
Petr Mrázek
c01d020afc GH-2723 disable deprecation warnings
We are targeting version 5.4 of the Qt ABI.
Deprecations from 2019 are irrelevant.
2019-07-03 01:11:18 +02:00
Petr Mrázek
ee83d432f6 GH-2724 update group view geometries in more cases
Fixes crashes when adding instances to groups that didn't exist before.
2019-07-02 02:09:41 +02:00
Petr Mrázek
8ee11b1a8e GH-2716 do not censor values shorter than 4 in logs 2019-07-01 00:00:34 +02:00
Petr Mrázek
0b86a7ebf3 Merge pull request #2718 from therealfarfetchd/hidpi-icon-fix
Enable HiDPI pixmaps to fix icon scaling for HiDPI displays
2019-06-30 15:39:29 +02:00
Petr Mrázek
63330bf111 NOISSUE connect twitch URL resolving to modpack resolving. works now. 2019-06-30 11:03:59 +02:00
Petr Mrázek
f74e3db804 NOISSUE Import page is now a MultiMC pack page 2019-06-29 01:13:39 +02:00
therealfarfetchd
a55fa04353 Enable HiDPI pixmaps to fix icon scaling for HiDPI displays 2019-06-27 23:04:53 +02:00
Petr Mrázek
fde43c993e NOISSUE add silly twitch URL and CCIP resolving page to 'add instance'
It needs a few more steps and it will handle all kinds of twitch packs.
2019-06-27 03:20:11 +02:00
Petr Mrázek
917f148fc4 NOISSUE add support for 'experiment' Minecraft versions 2019-06-26 20:51:04 +02:00
Petr Mrázek
34611c00e3 Merge branch 'feature/update_translation_fix' into develop 2019-06-25 23:41:16 +02:00
Petr Mrázek
44a7c5867b Merge pull request #2703 from Janrupf/feature/apply_proxy_settings
GH-2499 Apply proxy settings immediately
2019-06-23 21:38:30 +02:00
Petr Mrázek
75ddbc8851 Merge pull request #2705 from Janrupf/feature/fix_external_deletion_interaction
GH-2515 Save instance ID before display dialog
2019-06-23 21:31:56 +02:00
Petr Mrázek
2f1d31cf43 Merge pull request #2706 from Janrupf/feature/fix_hashtag_in_notes
Feature/fix hashtag in notes
2019-06-23 21:19:10 +02:00
Petr Mrázek
e7c5b266c8 Merge pull request #2708 from Janrupf/feature/single_imgur_uploads
GH-689 Don't create album for single screenshot
2019-06-23 21:15:06 +02:00
Petr Mrázek
384979bf94 Merge pull request #2704 from Janrupf/feature/autoselect_new_instances
GH-2592 Autoselect newly created instances
2019-06-23 19:58:21 +02:00
janrupf
b5a16935b7 NOISSUE Renaming for better understanding 2019-06-23 14:54:17 +02:00
janrupf
320637e8dc GH-1701 Check job size for translation 2019-06-22 01:54:08 +02:00
janrupf
77f3f028fa GH-2499 Apply proxy settings immediately 2019-06-22 01:48:37 +02:00
janrupf
2a96e16902 GH-689 Don't create album for single screenshot 2019-06-22 01:47:07 +02:00
janrupf
1ed84eddd5 GH-2515 Save instance ID before display dialog 2019-06-21 23:55:16 +02:00
janrupf
7b52b8689b NOISSUE Test comment escaping with unit tests 2019-06-21 23:46:54 +02:00
janrupf
d21700ee91 NOISSUE Revert INI parser back to single pass 2019-06-21 23:46:54 +02:00
janrupf
f87c890912 GH-1813 Escape # in INI (and better reader) 2019-06-21 23:46:54 +02:00
janrupf
306b98edac GH-2592 Autoselect newly created instances 2019-06-21 22:38:26 +02:00
50 changed files with 789 additions and 388 deletions

View File

@@ -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.")

View File

@@ -175,11 +175,6 @@ QString BaseInstance::instanceRoot() const
return m_rootDir;
}
void BaseInstance::setInstanceRoot(QString newRoot)
{
m_rootDir = std::move(newRoot);
}
SettingsObjectPtr BaseInstance::settings() const
{
return m_settings;
@@ -230,23 +225,11 @@ QString BaseInstance::iconKey() const
return m_settings->get("iconKey").toString();
}
void BaseInstance::setName(QString val, bool requestDirChange)
void BaseInstance::setName(QString val)
{
if(m_settings->get("name") == val)
{
return;
}
//FIXME: if no change, do not set. setting involves saving a file.
m_settings->set("name", val);
emit propertiesChanged(this);
if(requestDirChange && !isRunning())
{
emit instanceDirChangeRequest(this);
}
else if(requestDirChange && isRunning())
{
qWarning() << "Tried to rename running instance with folder";
}
}
QString BaseInstance::name() const

View File

@@ -93,8 +93,6 @@ public:
/// Path to the instance's root directory.
QString instanceRoot() const;
void setInstanceRoot(QString newRoot);
/// Path to the instance's game root directory.
virtual QString gameRoot() const
{
@@ -102,7 +100,7 @@ public:
}
QString name() const;
void setName(QString val, bool requestDirChange);
void setName(QString val);
/// Value used for instance window titles
QString windowTitle() const;
@@ -247,8 +245,6 @@ signals:
void statusChanged(Status from, Status to);
void instanceDirChangeRequest(BaseInstance *inst);
protected slots:
void iconUpdated(QString key);

View File

@@ -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

View File

@@ -44,7 +44,7 @@ void InstanceCopyTask::copyFinished()
instanceSettings->registerSetting("InstanceType", "Legacy");
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(m_instName, false);
inst->setName(m_instName);
inst->setIconKey(m_instIcon);
emitSucceeded();
}

View File

@@ -23,7 +23,7 @@ void InstanceCreationTask::executeTask()
auto components = inst.getComponentList();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
inst.setName(m_instName, false);
inst.setName(m_instName);
inst.setIconKey(m_instIcon);
instanceSettings->resumeSave();
}

View File

@@ -293,7 +293,7 @@ void InstanceImportTask::processFlame()
// nuke the original files
FS::deletePath(jarmodsPath);
}
instance.setName(m_instName, false);
instance.setName(m_instName);
m_modIdResolver.reset(new Flame::FileResolvingTask(pack));
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
{
@@ -384,7 +384,7 @@ void InstanceImportTask::processMultiMC()
instance.resetTimePlayed();
// set a new nice name
instance.setName(m_instName, false);
instance.setName(m_instName);
// if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon != "default")

View File

@@ -135,7 +135,7 @@ bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int
{
return true;
}
pdata->setName(newName, true);
pdata->setName(newName);
return true;
}
@@ -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;
}
@@ -396,7 +396,6 @@ void InstanceList::add(const QList<InstancePtr> &t)
for(auto & ptr : t)
{
connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
connect(ptr.get(), &BaseInstance::instanceDirChangeRequest, this, &InstanceList::instanceDirUpdateRequested);
}
endInsertRows();
}
@@ -470,27 +469,6 @@ void InstanceList::propertiesChanged(BaseInstance *inst)
}
}
void InstanceList::instanceDirUpdateRequested(BaseInstance *inst)
{
if(m_groupMap.remove(inst->id()))
{
saveGroupList();
}
QString oldRoot = inst->instanceRoot();
QString instID = FS::DirNameFromString(inst->name(), m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);
if(!QDir().rename(oldRoot, destination))
{
qWarning() << "Failed to move" << inst->instanceRoot() << "to" << destination;
}
FS::deletePath(oldRoot);
loadList();
}
InstancePtr InstanceList::loadInstance(const InstanceId& id)
{
if(!m_groupsLoaded)
@@ -841,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;

View File

@@ -129,6 +129,7 @@ public:
signals:
void dataIsInvalid();
void instancesChanged();
void instanceSelectRequest(QString instanceId);
void groupsChanged(QSet<QString> groups);
public slots:
@@ -136,7 +137,6 @@ public slots:
private slots:
void propertiesChanged(BaseInstance *inst);
void instanceDirUpdateRequested(BaseInstance *inst);
void providerUpdated();
void instanceDirContentsChanged(const QString &path);

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -113,9 +113,6 @@ public:
virtual JavaVersion getJavaVersion() const;
signals:
void versionReloaded();
protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
QStringList validLaunchMethods();

View File

@@ -4,7 +4,7 @@
#include <QSaveFile>
namespace {
bool load(const QString& path, std::vector<GameOptionItem> &contents, int & version)
bool load(const QString& path, RawGameOptions & contents)
{
contents.clear();
QFile file(path);
@@ -13,7 +13,6 @@ bool load(const QString& path, std::vector<GameOptionItem> &contents, int & vers
qWarning() << "Failed to read options file.";
return false;
}
version = 0;
while(!file.atEnd())
{
auto line = file.readLine();
@@ -31,32 +30,32 @@ bool load(const QString& path, std::vector<GameOptionItem> &contents, int & vers
qDebug() << "!!" << key << "!!";
if(key == "version")
{
version = value.toInt();
contents.version = value.toInt();
continue;
}
contents.emplace_back(GameOptionItem{key, value});
contents.mapping[key] = value;
}
qDebug() << "Loaded" << path << "with version:" << version;
qDebug() << "Loaded" << path << "with version:" << contents.version;
return true;
}
bool save(const QString& path, std::vector<GameOptionItem> &mapping, int version)
bool save(const QString& path, RawGameOptions& contents)
{
QSaveFile out(path);
if(!out.open(QIODevice::WriteOnly))
{
return false;
}
if(version != 0)
if(contents.version != 0)
{
QString versionLine = QString("version:%1\n").arg(version);
QString versionLine = QString("version:%1\n").arg(contents.version);
out.write(versionLine.toUtf8());
}
auto iter = mapping.begin();
while (iter != mapping.end())
auto iter = contents.mapping.begin();
while (iter != contents.mapping.end())
{
out.write(iter->key.toUtf8());
out.write(iter->first.toUtf8());
out.write(":");
out.write(iter->value.toUtf8());
out.write(iter->second.toUtf8());
out.write("\n");
iter++;
}
@@ -95,7 +94,7 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const
int row = index.row();
int column = index.column();
if (row < 0 || row >= int(contents.size()))
if (row < 0 || row >= rowCount())
return QVariant();
switch (role)
@@ -103,11 +102,11 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const
case Qt::DisplayRole:
if(column == 0)
{
return contents[row].key;
return cookedOptions.items[row].id;
}
else
{
return contents[row].value;
return cookedOptions.items[row].default_value;
}
default:
return QVariant();
@@ -117,7 +116,7 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const
int GameOptions::rowCount(const QModelIndex&) const
{
return contents.size();
return cookedOptions.items.size();
}
int GameOptions::columnCount(const QModelIndex&) const
@@ -133,12 +132,12 @@ bool GameOptions::isLoaded() const
bool GameOptions::reload()
{
beginResetModel();
loaded = load(path, contents, version);
loaded = load(path, rawOptions);
endResetModel();
return loaded;
}
bool GameOptions::save()
{
return ::save(path, contents, version);
return ::save(path, rawOptions);
}

View File

@@ -4,12 +4,46 @@
#include <QString>
#include <QAbstractListModel>
struct RawGameOptions
{
void clear()
{
version = 0;
mapping.clear();
}
std::map<QString, QString> mapping;
int version = 0;
};
struct GameOptionItem
{
QString id;
enum ValueType
{
INT,
FLOAT,
BOOL,
FOV_MADNESS,
FPS_MADNESS
} value_type;
enum VisualType
{
} visual_type;
QVariant null_value;
QVariant default_value;
QVariant min_value;
QVariant max_value;
QString key;
QString value;
};
struct CookedGameOptions
{
std::vector<GameOptionItem> items;
};
class GameOptions : public QAbstractListModel
{
Q_OBJECT
@@ -27,7 +61,8 @@ public:
bool save();
private:
std::vector<GameOptionItem> contents;
RawGameOptions rawOptions;
CookedGameOptions cookedOptions;
bool loaded = false;
QString path;
int version = 0;

View File

@@ -66,7 +66,7 @@ void LegacyUpgradeTask::copyFinished()
// NOTE: this scope ensures the instance is fully saved before we emitSucceeded
{
MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath);
inst.setName(m_instName, false);
inst.setName(m_instName);
QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId());
if(preferredVersionNumber.isNull())

View File

@@ -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:";

View File

@@ -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;
}

View File

@@ -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.

View 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."));
}

View 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;
};
}

View File

@@ -186,7 +186,7 @@ void FtbPackInstallTask::install()
progress(4, 4);
instance.setName(m_instName, false);
instance.setName(m_instName);
if(m_instIcon == "default")
{
m_instIcon = "ftb_logo";

View File

@@ -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)

View File

@@ -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

View File

@@ -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();

View File

@@ -145,8 +145,6 @@ SET(MULTIMC_SOURCES
# GUI - dialogs
dialogs/AboutDialog.cpp
dialogs/AboutDialog.h
dialogs/CheckableInputDialog.cpp
dialogs/CheckableInputDialog.h
dialogs/ProfileSelectDialog.cpp
dialogs/ProfileSelectDialog.h
dialogs/CopyInstanceDialog.cpp
@@ -260,7 +258,6 @@ SET(MULTIMC_UIS
pages/modplatform/ImportPage.ui
# Dialogs
dialogs/CheckableInputDialog.ui
dialogs/CopyInstanceDialog.ui
dialogs/NewComponentDialog.ui
dialogs/NewInstanceDialog.ui

View File

@@ -31,7 +31,6 @@
#include <QtWidgets/QAction>
#include <QtWidgets/QApplication>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QMainWindow>
@@ -75,7 +74,6 @@
#include "groupview/InstanceDelegate.h"
#include "widgets/LabeledToolButton.h"
#include "widgets/ServerStatus.h"
#include "dialogs/CheckableInputDialog.h"
#include "dialogs/NewInstanceDialog.h"
#include "dialogs/ProgressDialog.h"
#include "dialogs/AboutDialog.h"
@@ -704,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);
@@ -847,7 +848,7 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
actionSep->setSeparator(true);
bool onInstance = view->indexAt(pos).isValid();
if (onInstance && m_selectedInstance)
if (onInstance)
{
actions = ui->instanceToolBar->actions();
@@ -1366,7 +1367,6 @@ void MainWindow::finalizeInstance(InstancePtr inst)
});
if(update)
{
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(update.get());
}
@@ -1667,6 +1667,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
{
return;
}
auto id = m_selectedInstance->id();
auto response = CustomMessageBox::selectable(
this,
tr("CAREFUL!"),
@@ -1677,7 +1678,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
)->exec();
if (response == QMessageBox::Yes)
{
MMC->instances()->deleteInstance(m_selectedInstance->id());
MMC->instances()->deleteInstance(id);
}
}
@@ -1694,15 +1695,7 @@ void MainWindow::on_actionRenameInstance_triggered()
{
if (m_selectedInstance)
{
CheckableInputDialog dialog(this);
dialog.setWindowTitle(tr("Rename"));
dialog.setText(tr("Enter new name for the instance:"));
dialog.setCheckboxText(tr("Rename instance folder"));
dialog.setExtraText(tr("WARNING: Renaming the instance folder may break shortcuts and automation you made!"));
if(dialog.exec() == QDialog::Accepted && !dialog.getInput().isEmpty())
{
m_selectedInstance->setName(dialog.getInput(), dialog.checkboxChecked());
}
view->edit(view->currentIndex());
}
}
@@ -1831,7 +1824,6 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch());
ui->actionExportInstance->setEnabled(m_selectedInstance->canExport());
ui->renameButton->setText(m_selectedInstance->name());
ui->actionRenameInstance->setDisabled(m_selectedInstance->isRunning());
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
updateInstanceToolIcon(m_selectedInstance->iconKey());
@@ -1848,6 +1840,11 @@ void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &
}
}
void MainWindow::instanceSelectRequest(QString id)
{
setSelectedInstanceById(id);
}
void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
auto current = view->selectionModel()->currentIndex();

View File

@@ -152,6 +152,8 @@ private slots:
void instanceChanged(const QModelIndex &current, const QModelIndex &previous);
void instanceSelectRequest(QString id);
void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void selectionBad();

View File

@@ -1,46 +0,0 @@
#include "CheckableInputDialog.h"
#include <QPushButton>
#include <QDebug>
#include "ui_CheckableInputDialog.h"
CheckableInputDialog::CheckableInputDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CheckableInputDialog)
{
ui->setupUi(this);
connect(ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok), &QPushButton::clicked, this, &QDialog::accept);
connect(ui->buttonBox->button(QDialogButtonBox::StandardButton::Cancel), &QPushButton::clicked, this,
&QDialog::reject);
}
CheckableInputDialog::~CheckableInputDialog()
{
delete ui;
}
void CheckableInputDialog::setText(QString text)
{
ui->label->setText(text);
}
void CheckableInputDialog::setExtraText(QString text)
{
ui->extraText->setText(text);
}
void CheckableInputDialog::setCheckboxText(QString checkboxText)
{
ui->checkBox->setText(checkboxText);
}
bool CheckableInputDialog::checkboxChecked()
{
return ui->checkBox->checkState();
}
QString CheckableInputDialog::getInput()
{
return ui->lineEdit->text();
}

View File

@@ -1,28 +0,0 @@
#pragma once
#include <QDialog>
namespace Ui
{
class CheckableInputDialog;
}
class CheckableInputDialog : public QDialog
{
Q_OBJECT
public:
CheckableInputDialog(QWidget *parent);
~CheckableInputDialog();
void setText(QString text);
void setExtraText(QString text);
void setCheckboxText(QString checkboxText);
bool checkboxChecked();
QString getInput();
private:
Ui::CheckableInputDialog *ui;
};

View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheckableInputDialog</class>
<widget class="QDialog" name="CheckableInputDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>186</width>
<height>112</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>CheckBox</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="extraText">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<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>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CheckableInputDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CheckableInputDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -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)
};
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -341,4 +341,82 @@ QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem &option,
return sz;
}
class NoReturnTextEdit: public QTextEdit
{
Q_OBJECT
public:
explicit NoReturnTextEdit(QWidget *parent) : QTextEdit(parent)
{
setTextInteractionFlags(Qt::TextEditorInteraction);
setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
}
bool event(QEvent * event) override
{
auto eventType = event->type();
if(eventType == QEvent::KeyPress || eventType == QEvent::KeyRelease)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
auto key = keyEvent->key();
if (key == Qt::Key_Return || key == Qt::Key_Enter)
{
emit editingDone();
return true;
}
if(key == Qt::Key_Tab)
{
return true;
}
}
return QTextEdit::event(event);
}
signals:
void editingDone();
};
void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
const int iconSize = 48;
QRect textRect = option.rect;
// QStyle *style = option.widget ? option.widget->style() : QApplication::style();
textRect.adjust(0, iconSize + 5, 0, 0);
editor->setGeometry(textRect);
}
void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
auto text = index.data(Qt::EditRole).toString();
QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
realeditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
realeditor->append(text);
realeditor->selectAll();
realeditor->document()->clearUndoRedoStacks();
}
void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
QString text = realeditor->toPlainText();
text.replace(QChar('\n'), QChar(' '));
text = text.trimmed();
if(text.size() != 0)
{
model->setData(index, text);
}
}
QWidget * ListViewDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
auto editor = new NoReturnTextEdit(parent);
connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone);
return editor;
}
void ListViewDelegate::editingDone()
{
NoReturnTextEdit *editor = qobject_cast<NoReturnTextEdit *>(sender());
emit commitData(editor);
emit closeEditor(editor);
}
#include "InstanceDelegate.moc"

View File

@@ -28,4 +28,12 @@ public:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
void setEditorData(QWidget * editor, const QModelIndex & index) const override;
void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
private slots:
void editingDone();
};

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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()
{

View File

@@ -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);

View File

@@ -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 &current, 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();
}

View File

@@ -66,7 +66,6 @@ private slots:
void on_downloadBtn_clicked();
void updateVersionControls();
void disableVersionControls();
void on_changeVersionBtn_clicked();
private:

View File

@@ -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));

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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));
}

View File

@@ -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>

View File

@@ -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.