GH-469 Implement support for importing and searching for Technic Platform and Solder modpacks

This does not support any custom modpack.jar for 1.6 or newer, it simply uses standard Forge then.
Supports Forge and Fabric, and JAR mods for 1.5 and older.
This commit is contained in:
kb1000 2020-06-07 17:46:12 +02:00
parent 762ddaea65
commit 8021fb25d0
24 changed files with 1441 additions and 10 deletions

View File

@ -477,6 +477,15 @@ set(MODPACKSCH_SOURCES
modplatform/modpacksch/FTBPackManifest.cpp
)
set(TECHNIC_SOURCES
modplatform/technic/SingleZipPackInstallTask.h
modplatform/technic/SingleZipPackInstallTask.cpp
modplatform/technic/SolderPackInstallTask.h
modplatform/technic/SolderPackInstallTask.cpp
modplatform/technic/TechnicPackProcessor.h
modplatform/technic/TechnicPackProcessor.cpp
)
add_unit_test(Index
SOURCES meta/Index_test.cpp
LIBS MultiMC_logic
@ -508,6 +517,7 @@ set(LOGIC_SOURCES
${FTB_SOURCES}
${FLAME_SOURCES}
${MODPACKSCH_SOURCES}
${TECHNIC_SOURCES}
)
add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})

View File

@ -98,6 +98,7 @@ void Env::initHttpMetaCache()
m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath());
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
m_metacache->addBase("root", QDir::currentPath());

View File

@ -1,3 +1,18 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "InstanceImportTask.h"
#include "BaseInstance.h"
#include "FileSystem.h"
@ -15,6 +30,8 @@
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/PackManifest.h"
#include "Json.h"
#include <quazipdir.h>
#include "modplatform/technic/TechnicPackProcessor.h"
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
{
@ -23,8 +40,6 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
void InstanceImportTask::executeTask()
{
InstancePtr newInstance;
if (m_sourceUrl.isLocalFile())
{
m_archivePath = m_sourceUrl.toLocalFile();
@ -82,6 +97,7 @@ void InstanceImportTask::processZipPack()
QStringList blacklist = {"instance.cfg", "manifest.json"};
QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json");
QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
QString root;
if(!mmcFound.isNull())
@ -91,6 +107,14 @@ void InstanceImportTask::processZipPack()
root = mmcFound;
m_modpackType = ModpackType::MultiMC;
}
else if (technicFound)
{
// process as Technic pack
qDebug() << "Technic:" << technicFound;
extractDir.mkpath(".minecraft");
extractDir.cd(".minecraft");
m_modpackType = ModpackType::Technic;
}
else if(!flameFound.isNull())
{
// process as Flame pack
@ -98,7 +122,6 @@ void InstanceImportTask::processZipPack()
root = flameFound;
m_modpackType = ModpackType::Flame;
}
if(m_modpackType == ModpackType::Unknown)
{
emitFailed(tr("Archive does not contain a recognized modpack type."));
@ -161,6 +184,9 @@ void InstanceImportTask::extractFinished()
case ModpackType::MultiMC:
processMultiMC();
return;
case ModpackType::Technic:
processTechnic();
return;
case ModpackType::Unknown:
emitFailed(tr("Archive does not contain a recognized modpack type."));
return;
@ -371,6 +397,14 @@ void InstanceImportTask::processFlame()
m_modIdResolver->start();
}
void InstanceImportTask::processTechnic()
{
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath);
}
void InstanceImportTask::processMultiMC()
{
// FIXME: copy from FolderInstanceProvider!!! FIX IT!!!

View File

@ -1,3 +1,18 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "InstanceTask.h"
@ -29,6 +44,7 @@ private:
void processZipPack();
void processMultiMC();
void processFlame();
void processTechnic();
private slots:
void downloadSucceeded();
@ -49,6 +65,7 @@ private: /* data */
enum class ModpackType{
Unknown,
MultiMC,
Flame
Flame,
Technic
} m_modpackType = ModpackType::Unknown;
};

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2019 MultiMC Contributors
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2019 MultiMC Contributors
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -67,5 +67,4 @@ namespace MMCZip
* \return The list of the full paths of the files extracted, empty on failure.
*/
QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir);
}

View File

@ -0,0 +1,129 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SingleZipPackInstallTask.h"
#include "Env.h"
#include "MMCZip.h"
#include "TechnicPackProcessor.h"
#include <QtConcurrent>
Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
{
m_sourceUrl = sourceUrl;
m_minecraftVersion = minecraftVersion;
}
void Technic::SingleZipPackInstallTask::executeTask()
{
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
auto entry = ENV.metacache()->resolveEntry("general", path);
entry->setStale(true);
m_filesNetJob.reset(new NetJob(tr("Modpack download")));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SingleZipPackInstallTask::downloadSucceeded()
{
setStatus(tr("Extracting modpack"));
QDir extractDir(m_stagingPath);
qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath));
if (!m_packZip->open(QuaZip::mdUnzip))
{
emitFailed(tr("Unable to open supplied modpack zip file."));
return;
}
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
m_filesNetJob.reset();
}
void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current / 2, total);
}
void Technic::SingleZipPackInstallTask::extractFinished()
{
m_packZip.reset();
if (m_extractFuture.result().isEmpty())
{
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
QDirIterator it(extractDir, QDirIterator::Subdirectories);
while (it.hasNext())
{
auto filepath = it.next();
QFileInfo file(filepath);
auto permissions = QFile::permissions(filepath);
auto origPermissions = permissions;
if (file.isDir())
{
// Folder +rwx for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
}
else
{
// File +rw for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
}
if (origPermissions != permissions)
{
if (!QFile::setPermissions(filepath, permissions))
{
logWarning(tr("Could not fix permissions for %1").arg(filepath));
}
else
{
qDebug() << "Fixed" << filepath;
}
}
}
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion);
}
void Technic::SingleZipPackInstallTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
}

View File

@ -0,0 +1,64 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#ifndef TECHNIC_SINGLEZIPPACKINSTALLTASK_H
#define TECHNIC_SINGLEZIPPACKINSTALLTASK_H
#include "InstanceTask.h"
#include "net/NetJob.h"
#include "multimc_logic_export.h"
#include "quazip.h"
#include <QFutureWatcher>
#include <QStringList>
#include <QUrl>
namespace Technic {
class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask
{
Q_OBJECT
public:
SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
protected:
void executeTask() override;
private slots:
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void extractFinished();
void extractAborted();
private:
QUrl m_sourceUrl;
QString m_minecraftVersion;
QString m_archivePath;
NetJobPtr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
QFuture<QStringList> m_extractFuture;
QFutureWatcher<QStringList> m_extractFutureWatcher;
};
} // namespace Technic
#endif // TECHNIC_SINGLEZIPPACKINSTALLTASK_H

View File

@ -0,0 +1,194 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SolderPackInstallTask.h"
#include <FileSystem.h>
#include <Json.h>
#include <QtConcurrentRun>
#include <MMCZip.h>
#include "TechnicPackProcessor.h"
Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
{
m_sourceUrl = sourceUrl;
m_minecraftVersion = minecraftVersion;
}
void Technic::SolderPackInstallTask::executeTask()
{
setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob.reset(new NetJob(tr("Finding recommended version")));
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::versionSucceeded()
{
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString version = Json::requireString(obj, "recommended", "__placeholder__");
m_sourceUrl = m_sourceUrl.toString() + '/' + version;
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
m_filesNetJob.reset();
return;
}
setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob.reset(new NetJob(tr("Resolving modpack files")));
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::fileListSucceeded()
{
setStatus(tr("Downloading modpack:"));
QStringList modUrls;
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
if (!minecraftVersion.isEmpty())
m_minecraftVersion = minecraftVersion;
QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
for (auto mod: mods)
{
QJsonObject modObject = Json::requireObject(mod);
modUrls.append(Json::requireString(modObject, "url", "'url'"));
}
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
m_filesNetJob.reset();
return;
}
m_filesNetJob.reset(new NetJob(tr("Downloading modpack")));
int i = 0;
for (auto &modUrl: modUrls)
{
m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, m_outputDir.filePath(QString("%1").arg(i))));
i++;
}
m_modCount = modUrls.size();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
void Technic::SolderPackInstallTask::downloadSucceeded()
{
setStatus(tr("Extracting modpack"));
m_filesNetJob.reset();
m_extractFuture = QtConcurrent::run([this]()
{
int i = 0;
QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
FS::ensureFolderPathExists(extractDir);
while (m_modCount > i)
{
if (MMCZip::extractDir(m_outputDir.filePath(QString("%1").arg(i)), extractDir).isEmpty())
{
return false;
}
i++;
}
return true;
});
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void Technic::SolderPackInstallTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current / 2, total);
}
void Technic::SolderPackInstallTask::extractFinished()
{
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
QDirIterator it(extractDir, QDirIterator::Subdirectories);
while (it.hasNext())
{
auto filepath = it.next();
QFileInfo file(filepath);
auto permissions = QFile::permissions(filepath);
auto origPermissions = permissions;
if(file.isDir())
{
// Folder +rwx for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
}
else
{
// File +rw for current user
permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
}
if(origPermissions != permissions)
{
if(!QFile::setPermissions(filepath, permissions))
{
logWarning(tr("Could not fix permissions for %1").arg(filepath));
}
else
{
qDebug() << "Fixed" << filepath;
}
}
}
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true); // TODO: pass the minecraft version down
}
void Technic::SolderPackInstallTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
return;
}

View File

@ -0,0 +1,57 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <InstanceTask.h>
#include <net/NetJob.h>
#include <tasks/Task.h>
#include <QUrl>
namespace Technic
{
class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask
{
Q_OBJECT
public:
explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private slots:
void versionSucceeded();
void fileListSucceeded();
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void extractFinished();
void extractAborted();
private:
NetJobPtr m_filesNetJob;
QUrl m_sourceUrl;
QString m_minecraftVersion;
QByteArray m_response;
QTemporaryDir m_outputDir;
int m_modCount;
QFuture<bool> m_extractFuture;
QFutureWatcher<bool> m_extractFutureWatcher;
};
}

View File

@ -0,0 +1,201 @@
/* Copyright 2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "TechnicPackProcessor.h"
#include <FileSystem.h>
#include <Json.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/PackProfile.h>
#include <quazip.h>
#include <quazipdir.h>
#include <quazipfile.h>
#include <settings/INISettingsObject.h>
#include <memory>
void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder)
{
QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
instanceSettings->registerSetting("InstanceType", "Legacy");
instanceSettings->set("InstanceType", "OneSix");
MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
instance.setName(instName);
if (instIcon != "default")
{
instance.setIconKey(instIcon);
}
auto components = instance.getPackProfile();
components->buildingFromScratch();
QByteArray data;
QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
QString fmlMinecraftVersion;
if (QFile::exists(modpackJar))
{
QuaZip zipFile(modpackJar);
if (!zipFile.open(QuaZip::mdUnzip))
{
emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
return;
}
QuaZipDir zipFileRoot(&zipFile, "/");
if (zipFileRoot.exists("/version.json"))
{
if (zipFileRoot.exists("/fmlversion.properties"))
{
zipFile.setCurrentFile("fmlversion.properties");
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly))
{
emit failed(tr("Unable to open \"fmlversion.properties\"!"));
return;
}
QByteArray fmlVersionData = file.readAll();
file.close();
INIFile iniFile;
iniFile.loadFile(fmlVersionData);
// If not present, this evaluates to a null string
fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
}
zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly))
{
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
data = file.readAll();
file.close();
}
else
{
if (minecraftVersion.isEmpty())
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown"));
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({modpackJar});
// Forge for 1.4.7 and for 1.5.2 require extra libraries.
// Figure out the forge version and add it as a component
// (the code still comes from the jar mod installed above)
if (zipFileRoot.exists("/forgeversion.properties"))
{
zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
QuaZipFile file(&zipFile);
if (!file.open(QIODevice::ReadOnly))
{
// Really shouldn't happen, but error handling shall not be forgotten
emit failed(tr("Unable to open \"forgeversion.properties\""));
return;
}
QByteArray forgeVersionData = file.readAll();
file.close();
INIFile iniFile;
iniFile.loadFile(forgeVersionData);
QString major, minor, revision, build;
major = iniFile["forge.major.number"].toString();
minor = iniFile["forge.minor.number"].toString();
revision = iniFile["forge.revision.number"].toString();
build = iniFile["forge.build.number"].toString();
if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty())
{
emit failed(tr("Invalid \"forgeversion.properties\"!"));
return;
}
components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build);
}
components->saveNow();
emit succeeded();
return;
}
}
else if (QFile::exists(versionJson))
{
QFile file(versionJson);
if (!file.open(QIODevice::ReadOnly))
{
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
data = file.readAll();
file.close();
}
else
{
// This is the "Vanilla" modpack, excluded by the search code
emit failed(tr("Unable to find a \"version.json\"!"));
return;
}
try
{
QJsonDocument doc = Json::requireDocument(data);
QJsonObject root = Json::requireObject(doc, "version.json");
QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
if (minecraftVersion.isEmpty())
{
if (fmlMinecraftVersion.isEmpty())
{
emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
return;
}
minecraftVersion = fmlMinecraftVersion;
}
components->setComponentVersion("net.minecraft", minecraftVersion, true);
for (auto library: Json::ensureArray(root, "libraries", {}))
{
if (!library.isObject())
{
continue;
}
auto libraryObject = Json::ensureObject(library, {}, "");
auto libraryName = Json::ensureString(libraryObject, "name", "", "");
if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
{
components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
}
else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
{
components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
}
else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
{
components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
}
}
}
catch (const JSONValidationError &e)
{
emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
return;
}
components->saveNow();
emit succeeded();
}

View File

@ -0,0 +1,37 @@
/* Copyright 2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QString>
#include "settings/SettingsObject.h"
namespace Technic
{
// not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask
class TechnicPackProcessor : public QObject
{
Q_OBJECT
signals:
void succeeded();
void failed(QString reason);
public:
void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false);
};
}

View File

@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action)
}
return true;
}
NetJob::~NetJob() = default;

View File

@ -34,7 +34,7 @@ public:
{
setObjectName(job_name);
}
virtual ~NetJob() {}
virtual ~NetJob();
bool addNetAction(NetActionPtr action);

View File

@ -137,6 +137,10 @@ SET(MULTIMC_SOURCES
pages/modplatform/twitch/TwitchModel.h
pages/modplatform/twitch/TwitchPage.cpp
pages/modplatform/twitch/TwitchPage.h
pages/modplatform/technic/TechnicModel.cpp
pages/modplatform/technic/TechnicModel.h
pages/modplatform/technic/TechnicPage.cpp
pages/modplatform/technic/TechnicPage.h
pages/modplatform/ImportPage.cpp
pages/modplatform/ImportPage.h
@ -257,6 +261,7 @@ SET(MULTIMC_UIS
pages/modplatform/ftb/FtbPage.ui
pages/modplatform/legacy_ftb/Page.ui
pages/modplatform/twitch/TwitchPage.ui
pages/modplatform/technic/TechnicPage.ui
pages/modplatform/ImportPage.ui
# Dialogs

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2019 MultiMC Contributors
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -38,6 +38,8 @@
#include <pages/modplatform/legacy_ftb/Page.h>
#include <pages/modplatform/twitch/TwitchPage.h>
#include <pages/modplatform/ImportPage.h>
#include <pages/modplatform/technic/TechnicPage.h>
NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
@ -122,12 +124,14 @@ QList<BasePage *> NewInstanceDialog::getPages()
{
importPage = new ImportPage(this);
twitchPage = new TwitchPage(this);
auto technicPage = new TechnicPage(this);
return
{
new VanillaPage(this),
importPage,
new FtbPage(this),
new LegacyFTB::Page(this),
technicPage,
twitchPage
};
}

View File

@ -0,0 +1,40 @@
/* Copyright 2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QList>
#include <QString>
namespace Technic {
struct Modpack {
QString slug;
QString name;
QString logoUrl;
QString logoName;
bool broken = true;
QString url;
bool isSolder = false;
QString minecraftVersion;
bool metadataLoaded = false;
};
}
Q_DECLARE_METATYPE(Technic::Modpack)

View File

@ -0,0 +1,223 @@
/* Copyright 2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "TechnicModel.h"
#include "Env.h"
#include "MultiMC.h"
#include <QIcon>
Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
Technic::ListModel::~ListModel()
{
}
QVariant Technic::ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
{
return QString("INVALID INDEX %1").arg(pos);
}
Modpack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
return pack.name;
}
else if(role == Qt::DecorationRole)
{
if(m_logoMap.contains(pack.logoName))
{
return (m_logoMap.value(pack.logoName));
}
QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
else if(role == Qt::UserRole)
{
QVariant v;
v.setValue(pack);
return v;
}
return QVariant();
}
int Technic::ListModel::columnCount(const QModelIndex&) const
{
return 1;
}
int Technic::ListModel::rowCount(const QModelIndex&) const
{
return modpacks.size();
}
void Technic::ListModel::searchWithTerm(const QString& term)
{
if(currentSearchTerm == term) {
return;
}
currentSearchTerm = term;
if(jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
}
else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
performSearch();
}
void Technic::ListModel::performSearch()
{
NetJob *netJob = new NetJob("Technic::Search");
auto searchUrl = QString(
"https://api.technicpack.net/search?build=multimc&q=%1"
).arg(currentSearchTerm);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void Technic::ListModel::searchRequestFinished()
{
jobPtr.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError)
{
qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
QList<Modpack> newList;
auto objs = doc["modpacks"].toArray();
for (auto technicPack: objs) {
Modpack pack;
auto technicPackObject = technicPack.toObject();
pack.name = technicPackObject["name"].toString();
pack.slug = technicPackObject["slug"].toString();
if (pack.slug == "vanilla")
continue;
if (technicPackObject["iconUrl"].isString())
{
pack.logoUrl = technicPackObject["iconUrl"].toString();
pack.logoName = pack.logoUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
}
else
{
pack.logoUrl = "null";
pack.logoName = "null";
}
pack.broken = false;
newList.append(pack);
}
searchState = Finished;
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
}
void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
}
else
{
requestLogo(logo, logoUrl);
}
}
void Technic::ListModel::searchRequestFailed()
{
jobPtr.reset();
if(searchState == ResetRequested)
{
beginResetModel();
modpacks.clear();
endResetModel();
performSearch();
}
else
{
searchState = Finished;
}
}
void Technic::ListModel::logoLoaded(QString logo, QString out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, QIcon(out));
for(int i = 0; i < modpacks.size(); i++)
{
if(modpacks[i].logoName == logo)
{
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
}
}
}
void Technic::ListModel::logoFailed(QString logo)
{
m_failedLogos.append(logo);
m_loadingLogos.removeAll(logo);
}
void Technic::ListModel::requestLogo(QString logo, QString url)
{
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
{
return;
}
MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo));
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
{
logoLoaded(logo, fullPath);
});
QObject::connect(job, &NetJob::failed, this, [this, logo]
{
logoFailed(logo);
});
job->start();
m_loadingLogos.append(logo);
}

View File

@ -0,0 +1,70 @@
/* Copyright 2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QModelIndex>
#include "TechnicData.h"
#include "net/NetJob.h"
namespace Technic {
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
Q_OBJECT
public:
ListModel(QObject *parent);
virtual ~ListModel();
virtual QVariant data(const QModelIndex& index, int role) const;
virtual int columnCount(const QModelIndex& parent) const;
virtual int rowCount(const QModelIndex& parent) const;
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
void searchWithTerm(const QString & term);
private slots:
void searchRequestFinished();
void searchRequestFailed();
void logoFailed(QString logo);
void logoLoaded(QString logo, QString out);
private:
void performSearch();
void requestLogo(QString logo, QString url);
private:
QList<Modpack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
QMap<QString, QIcon> m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QString currentSearchTerm;
enum SearchState {
None,
ResetRequested,
Finished
} searchState = None;
NetJobPtr jobPtr;
QByteArray response;
};
}

View File

@ -0,0 +1,204 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "TechnicPage.h"
#include "ui_TechnicPage.h"
#include "MultiMC.h"
#include "dialogs/NewInstanceDialog.h"
#include "TechnicModel.h"
#include <QKeyEvent>
#include "modplatform/technic/SingleZipPackInstallTask.h"
#include "modplatform/technic/SolderPackInstallTask.h"
#include "Json.h"
TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
{
ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
model = new Technic::ListModel(this);
ui->packView->setModel(model);
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
}
bool TechnicPage::eventFilter(QObject* watched, QEvent* event)
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
keyEvent->accept();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
TechnicPage::~TechnicPage()
{
delete ui;
}
bool TechnicPage::shouldDisplay() const
{
return true;
}
void TechnicPage::openedImpl()
{
dialog->setSuggestedPack();
}
void TechnicPage::triggerSearch() {
model->searchWithTerm(ui->searchEdit->text());
}
void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
{
if(!first.isValid())
{
if(isOpened)
{
dialog->setSuggestedPack();
}
//ui->frame->clear();
return;
}
current = model->data(first, Qt::UserRole).value<Technic::Modpack>();
suggestCurrent();
}
void TechnicPage::suggestCurrent()
{
if (!isOpened)
{
return;
}
if (current.broken)
{
dialog->setSuggestedPack();
return;
}
QString editedLogoName;
editedLogoName = "technic_" + current.logoName.section(".", 0, 0);
model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
if (current.metadataLoaded)
{
metadataLoaded();
}
else
{
NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name));
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
QString slug = current.slug;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
{
if (current.slug != slug)
{
return;
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
QJsonObject obj = doc.object();
if(parse_error.error != QJsonParseError::NoError)
{
qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
if (!obj.contains("url"))
{
qWarning() << "Json doesn't contain an url key";
return;
}
QJsonValueRef url = obj["url"];
if (url.isString())
{
current.url = url.toString();
}
else
{
if (!obj.contains("solder"))
{
qWarning() << "Json doesn't contain a valid url or solder key";
return;
}
QJsonValueRef solderUrl = obj["solder"];
if (solderUrl.isString())
{
current.url = solderUrl.toString();
current.isSolder = true;
}
else
{
qWarning() << "Json doesn't contain a valid url or solder key";
return;
}
}
current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
current.metadataLoaded = true;
metadataLoaded();
});
netJob->start();
}
}
// expects current.metadataLoaded to be true
void TechnicPage::metadataLoaded()
{
/*QString text = "";
QString name = current.name;
if (current.websiteUrl.isEmpty())
text = name;
else
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
if (!current.authors.empty()) {
auto authorToStr = [](Technic::ModpackAuthor & author) {
if(author.url.isEmpty()) {
return author.name;
}
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
for(auto & author: current.authors) {
authorStrs.push_back(authorToStr(author));
}
text += tr(" by ") + authorStrs.join(", ");
}
ui->frame->setModText(text);
ui->frame->setModDescription(current.description);*/
if (!current.isSolder)
{
dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
}
else
{
while (current.url.endsWith('/')) current.url.chop(1);
dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion));
}
}

View File

@ -0,0 +1,78 @@
/* Copyright 2013-2020 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QWidget>
#include "pages/BasePage.h"
#include <MultiMC.h>
#include "tasks/Task.h"
#include "TechnicData.h"
namespace Ui
{
class TechnicPage;
}
class NewInstanceDialog;
namespace Technic {
class ListModel;
}
class TechnicPage : public QWidget, public BasePage
{
Q_OBJECT
public:
explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0);
virtual ~TechnicPage();
virtual QString displayName() const override
{
return tr("Technic");
}
virtual QIcon icon() const override
{
return MMC->getThemedIcon("technic");
}
virtual QString id() const override
{
return "technic";
}
virtual QString helpPage() const override
{
return "Technic-platform";
}
virtual bool shouldDisplay() const override;
void openedImpl() override;
bool eventFilter(QObject* watched, QEvent* event) override;
private:
void suggestCurrent();
void metadataLoaded();
private slots:
void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
private:
Ui::TechnicPage *ui = nullptr;
NewInstanceDialog* dialog = nullptr;
Technic::ListModel* model = nullptr;
Technic::Modpack current;
};

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TechnicPage</class>
<widget class="QWidget" name="TechnicPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>546</width>
<height>405</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="searchEdit"/>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QListView" name="packView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -104,7 +104,7 @@ void ListModel::requestLogo(QString logo, QString url)
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath]
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
{
emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo))

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB