diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 6e9aec083..15916bb58 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -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}) diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp index 2043f9826..e9eb67cbd 100644 --- a/api/logic/Env.cpp +++ b/api/logic/Env.cpp @@ -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()); diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index e21874160..772149c41 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -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 +#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 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!!! diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h index d326391b9..1e19354b6 100644 --- a/api/logic/InstanceImportTask.h +++ b/api/logic/InstanceImportTask.h @@ -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; }; diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp index 3afdbf5e7..876d73287 100644 --- a/api/logic/MMCZip.cpp +++ b/api/logic/MMCZip.cpp @@ -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. diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index 85ac7802c..56d20fbe7 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -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); - } diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp new file mode 100644 index 000000000..833ac0a22 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp @@ -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 + +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::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::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 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.")); +} diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/api/logic/modplatform/technic/SingleZipPackInstallTask.h new file mode 100644 index 000000000..929476bb2 --- /dev/null +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.h @@ -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 +#include +#include + +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 m_packZip; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; +}; + +} // namespace Technic + +#endif // TECHNIC_SINGLEZIPPACKINSTALLTASK_H diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/api/logic/modplatform/technic/SolderPackInstallTask.cpp new file mode 100644 index 000000000..cb440e843 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.cpp @@ -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 +#include +#include +#include +#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::finished, this, &Technic::SolderPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::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 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; +} + diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.h b/api/logic/modplatform/technic/SolderPackInstallTask.h new file mode 100644 index 000000000..d3a1d0fd1 --- /dev/null +++ b/api/logic/modplatform/technic/SolderPackInstallTask.h @@ -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 +#include +#include + +#include + + +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 m_extractFuture; + QFutureWatcher m_extractFutureWatcher; + }; +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.cpp b/api/logic/modplatform/technic/TechnicPackProcessor.cpp new file mode 100644 index 000000000..f986a5295 --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include + + +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(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(); +} diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.h b/api/logic/modplatform/technic/TechnicPackProcessor.h new file mode 100644 index 000000000..49d046a5f --- /dev/null +++ b/api/logic/modplatform/technic/TechnicPackProcessor.h @@ -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 +#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); + }; +} diff --git a/api/logic/net/NetJob.cpp b/api/logic/net/NetJob.cpp index 71e317369..7dfa16cae 100644 --- a/api/logic/net/NetJob.cpp +++ b/api/logic/net/NetJob.cpp @@ -214,3 +214,5 @@ bool NetJob::addNetAction(NetActionPtr action) } return true; } + +NetJob::~NetJob() = default; diff --git a/api/logic/net/NetJob.h b/api/logic/net/NetJob.h index 0b56bdaa6..daca419ed 100644 --- a/api/logic/net/NetJob.h +++ b/api/logic/net/NetJob.h @@ -34,7 +34,7 @@ public: { setObjectName(job_name); } - virtual ~NetJob() {} + virtual ~NetJob(); bool addNetAction(NetActionPtr action); diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 802789a28..38bd586b4 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -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 diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index d8abdbd4a..c2887b017 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -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 #include #include +#include + NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent) @@ -122,12 +124,14 @@ QList 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 }; } diff --git a/application/pages/modplatform/technic/TechnicData.h b/application/pages/modplatform/technic/TechnicData.h new file mode 100644 index 000000000..5c7466194 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicData.h @@ -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 +#include + + +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) diff --git a/application/pages/modplatform/technic/TechnicModel.cpp b/application/pages/modplatform/technic/TechnicModel.cpp new file mode 100644 index 000000000..b3d36bac1 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.cpp @@ -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 + + +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 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); +} diff --git a/application/pages/modplatform/technic/TechnicModel.h b/application/pages/modplatform/technic/TechnicModel.h new file mode 100644 index 000000000..bd0aec69f --- /dev/null +++ b/application/pages/modplatform/technic/TechnicModel.h @@ -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 + +#include "TechnicData.h" +#include "net/NetJob.h" + +namespace Technic { + +typedef std::function 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 modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + QMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + enum SearchState { + None, + ResetRequested, + Finished + } searchState = None; + NetJobPtr jobPtr; + QByteArray response; +}; + +} diff --git a/application/pages/modplatform/technic/TechnicPage.cpp b/application/pages/modplatform/technic/TechnicPage.cpp new file mode 100644 index 000000000..75efd3ed0 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.cpp @@ -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 +#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(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(); + 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 response = std::make_shared(); + 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 = "" + name + ""; + if (!current.authors.empty()) { + auto authorToStr = [](Technic::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("%2").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)); + } +} diff --git a/application/pages/modplatform/technic/TechnicPage.h b/application/pages/modplatform/technic/TechnicPage.h new file mode 100644 index 000000000..1a10af716 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.h @@ -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 + +#include "pages/BasePage.h" +#include +#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; +}; diff --git a/application/pages/modplatform/technic/TechnicPage.ui b/application/pages/modplatform/technic/TechnicPage.ui new file mode 100644 index 000000000..be56fa827 --- /dev/null +++ b/application/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,62 @@ + + + TechnicPage + + + + 0 + 0 + 546 + 405 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Search + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + + diff --git a/application/pages/modplatform/twitch/TwitchModel.cpp b/application/pages/modplatform/twitch/TwitchModel.cpp index 9e3c3ad22..5c6c7858c 100644 --- a/application/pages/modplatform/twitch/TwitchModel.cpp +++ b/application/pages/modplatform/twitch/TwitchModel.cpp @@ -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)) diff --git a/application/resources/assets/underconstruction.png b/application/resources/assets/underconstruction.png new file mode 100644 index 000000000..6ae06476e Binary files /dev/null and b/application/resources/assets/underconstruction.png differ