Merge pull request #3195 from kb-1000/technic-import

Technic pack import
This commit is contained in:
Petr Mrázek 2020-10-13 21:44:20 +02:00 committed by GitHub
commit 4c62776044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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