mirror of
https://github.com/MultiMC/MultiMC5.git
synced 2024-09-01 20:49:17 +00:00
chore: pull changes from upstream
This commit is contained in:
commit
313a6574c1
2
BUILD.md
2
BUILD.md
@ -169,9 +169,9 @@ Pick an installation path - this is where the final `.app` will be constructed w
|
||||
|
||||
```
|
||||
git clone https://github.com/MultiMC/MultiMC5.git
|
||||
cd MultiMC5
|
||||
git submodule init
|
||||
git submodule update
|
||||
cd MultiMC5
|
||||
mkdir build
|
||||
cd build
|
||||
export CMAKE_PREFIX_PATH=/usr/local/opt/qt5
|
||||
|
@ -46,7 +46,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc
|
||||
######## Set version numbers ########
|
||||
set(MultiMC_VERSION_MAJOR 0)
|
||||
set(MultiMC_VERSION_MINOR 6)
|
||||
set(MultiMC_VERSION_HOTFIX 7)
|
||||
set(MultiMC_VERSION_HOTFIX 12)
|
||||
|
||||
# Build number
|
||||
set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
|
||||
|
@ -97,6 +97,7 @@ void Env::initHttpMetaCache()
|
||||
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
|
||||
m_metacache->addBase("general", QDir("cache").absolutePath());
|
||||
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
|
||||
m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath());
|
||||
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
|
||||
m_metacache->addBase("root", QDir::currentPath());
|
||||
m_metacache->addBase("translations", QDir("translations").absolutePath());
|
||||
|
@ -78,7 +78,7 @@ void Version::parse()
|
||||
// FIXME: this is bad. versions can contain a lot more separators...
|
||||
QStringList parts = m_string.split('.');
|
||||
|
||||
for (const auto part : parts)
|
||||
for (const auto &part : parts)
|
||||
{
|
||||
m_sections.append(Section(part));
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ void LaunchProfile::clear()
|
||||
m_mainClass.clear();
|
||||
m_appletClass.clear();
|
||||
m_libraries.clear();
|
||||
m_mavenFiles.clear();
|
||||
m_traits.clear();
|
||||
m_jarMods.clear();
|
||||
m_mainJar.reset();
|
||||
@ -157,6 +158,22 @@ void LaunchProfile::applyLibrary(LibraryPtr library)
|
||||
}
|
||||
}
|
||||
|
||||
void LaunchProfile::applyMavenFile(LibraryPtr mavenFile)
|
||||
{
|
||||
if(!mavenFile->isActive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(mavenFile->isNative())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// unlike libraries, we do not keep only one version or try to dedupe them
|
||||
m_mavenFiles.append(Library::limitedCopy(mavenFile));
|
||||
}
|
||||
|
||||
const LibraryPtr LaunchProfile::getMainJar() const
|
||||
{
|
||||
return m_mainJar;
|
||||
@ -253,6 +270,11 @@ const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const
|
||||
return m_nativeLibraries;
|
||||
}
|
||||
|
||||
const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const
|
||||
{
|
||||
return m_mavenFiles;
|
||||
}
|
||||
|
||||
void LaunchProfile::getLibraryFiles(
|
||||
const QString& architecture,
|
||||
QStringList& jars,
|
||||
|
@ -20,6 +20,7 @@ public: /* application of profile variables from patches */
|
||||
void applyJarMods(const QList<LibraryPtr> &jarMods);
|
||||
void applyMods(const QList<LibraryPtr> &jarMods);
|
||||
void applyLibrary(LibraryPtr library);
|
||||
void applyMavenFile(LibraryPtr library);
|
||||
void applyMainJar(LibraryPtr jar);
|
||||
void applyProblemSeverity(ProblemSeverity severity);
|
||||
/// clear the profile
|
||||
@ -37,6 +38,7 @@ public: /* getters for profile variables */
|
||||
const QList<LibraryPtr> & getJarMods() const;
|
||||
const QList<LibraryPtr> & getLibraries() const;
|
||||
const QList<LibraryPtr> & getNativeLibraries() const;
|
||||
const QList<LibraryPtr> & getMavenFiles() const;
|
||||
const LibraryPtr getMainJar() const;
|
||||
void getLibraryFiles(
|
||||
const QString & architecture,
|
||||
@ -79,10 +81,13 @@ private:
|
||||
/// the list of libraries
|
||||
QList<LibraryPtr> m_libraries;
|
||||
|
||||
/// the list of maven files to be placed in the libraries folder, but not acted upon
|
||||
QList<LibraryPtr> m_mavenFiles;
|
||||
|
||||
/// the main jar
|
||||
LibraryPtr m_mainJar;
|
||||
|
||||
/// the list of libraries
|
||||
/// the list of native libraries
|
||||
QList<LibraryPtr> m_nativeLibraries;
|
||||
|
||||
/// traits, collected from all the version files (version files can only add)
|
||||
|
@ -144,14 +144,14 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
|
||||
}
|
||||
}
|
||||
|
||||
auto readLibs = [&](const char * which)
|
||||
auto readLibs = [&](const char * which, QList<LibraryPtr> & out)
|
||||
{
|
||||
for (auto libVal : requireArray(root.value(which)))
|
||||
{
|
||||
QJsonObject libObj = requireObject(libVal);
|
||||
// parse the library
|
||||
auto lib = libraryFromJson(libObj, filename);
|
||||
out->libraries.append(lib);
|
||||
out.append(lib);
|
||||
}
|
||||
};
|
||||
bool hasPlusLibs = root.contains("+libraries");
|
||||
@ -160,16 +160,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
|
||||
{
|
||||
out->addProblem(ProblemSeverity::Warning,
|
||||
QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported."));
|
||||
readLibs("libraries");
|
||||
readLibs("+libraries");
|
||||
readLibs("libraries", out->libraries);
|
||||
readLibs("+libraries", out->libraries);
|
||||
}
|
||||
else if (hasLibs)
|
||||
{
|
||||
readLibs("libraries");
|
||||
readLibs("libraries", out->libraries);
|
||||
}
|
||||
else if(hasPlusLibs)
|
||||
{
|
||||
readLibs("+libraries");
|
||||
readLibs("+libraries", out->libraries);
|
||||
}
|
||||
|
||||
if(root.contains("mavenFiles")) {
|
||||
readLibs("mavenFiles", out->mavenFiles);
|
||||
}
|
||||
|
||||
// if we have mainJar, just use it
|
||||
@ -276,6 +280,15 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
|
||||
}
|
||||
root.insert("libraries", array);
|
||||
}
|
||||
if (!patch->mavenFiles.isEmpty())
|
||||
{
|
||||
QJsonArray array;
|
||||
for (auto value: patch->mavenFiles)
|
||||
{
|
||||
array.append(OneSixVersionFormat::libraryToJson(value.get()));
|
||||
}
|
||||
root.insert("mavenFiles", array);
|
||||
}
|
||||
if (!patch->jarMods.isEmpty())
|
||||
{
|
||||
QJsonArray array;
|
||||
|
@ -41,6 +41,10 @@ void VersionFile::applyTo(LaunchProfile *profile)
|
||||
{
|
||||
profile->applyLibrary(library);
|
||||
}
|
||||
for (auto mavenFile : mavenFiles)
|
||||
{
|
||||
profile->applyMavenFile(mavenFile);
|
||||
}
|
||||
profile->applyProblemSeverity(getProblemSeverity());
|
||||
}
|
||||
|
||||
@ -53,4 +57,4 @@ void VersionFile::applyTo(LaunchProfile *profile)
|
||||
throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion);
|
||||
}
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
@ -75,6 +75,9 @@ public: /* data */
|
||||
/// Mojang: list of libraries to add to the version
|
||||
QList<LibraryPtr> libraries;
|
||||
|
||||
/// MultiMC: list of maven files to put in the libraries folder, but not in classpath
|
||||
QList<LibraryPtr> mavenFiles;
|
||||
|
||||
/// The main jar (Minecraft version library, normally)
|
||||
LibraryPtr mainJar;
|
||||
|
||||
|
@ -45,6 +45,7 @@ void LibrariesTask::executeTask()
|
||||
QList<LibraryPtr> libArtifactPool;
|
||||
libArtifactPool.append(profile->getLibraries());
|
||||
libArtifactPool.append(profile->getNativeLibraries());
|
||||
libArtifactPool.append(profile->getMavenFiles());
|
||||
libArtifactPool.append(profile->getMainJar());
|
||||
processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath());
|
||||
|
||||
|
@ -133,6 +133,11 @@ SET(MULTIMC_SOURCES
|
||||
pages/modplatform/legacy_ftb/Page.h
|
||||
pages/modplatform/legacy_ftb/ListModel.h
|
||||
pages/modplatform/legacy_ftb/ListModel.cpp
|
||||
pages/modplatform/twitch/TwitchData.h
|
||||
pages/modplatform/twitch/TwitchModel.cpp
|
||||
pages/modplatform/twitch/TwitchModel.h
|
||||
pages/modplatform/twitch/TwitchPage.cpp
|
||||
pages/modplatform/twitch/TwitchPage.h
|
||||
pages/modplatform/ImportPage.cpp
|
||||
pages/modplatform/ImportPage.h
|
||||
|
||||
@ -251,6 +256,7 @@ SET(MULTIMC_UIS
|
||||
# Platform pages
|
||||
pages/modplatform/VanillaPage.ui
|
||||
pages/modplatform/legacy_ftb/Page.ui
|
||||
pages/modplatform/twitch/TwitchPage.ui
|
||||
pages/modplatform/ImportPage.ui
|
||||
|
||||
# Dialogs
|
||||
|
@ -46,6 +46,7 @@ QString getCreditsHtml(QStringList patrons)
|
||||
stream << "<p>TakSuyu <<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>></p>\n";
|
||||
stream << "<p>Kilobyte <<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>></p>\n";
|
||||
stream << "<p>Rootbear75 <<a href='https://twitter.com/rootbear75'>@rootbear75</a>></p>\n";
|
||||
stream << "<p>Zeker Zhayard <<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>></p>\n";
|
||||
stream << "<br />\n";
|
||||
|
||||
if(!patrons.isEmpty()) {
|
||||
|
@ -35,6 +35,7 @@
|
||||
#include "widgets/PageContainer.h"
|
||||
#include <pages/modplatform/VanillaPage.h>
|
||||
#include <pages/modplatform/legacy_ftb/Page.h>
|
||||
#include <pages/modplatform/twitch/TwitchPage.h>
|
||||
#include <pages/modplatform/ImportPage.h>
|
||||
|
||||
|
||||
@ -119,11 +120,13 @@ void NewInstanceDialog::accept()
|
||||
QList<BasePage *> NewInstanceDialog::getPages()
|
||||
{
|
||||
importPage = new ImportPage(this);
|
||||
twitchPage = new TwitchPage(this);
|
||||
return
|
||||
{
|
||||
new VanillaPage(this),
|
||||
importPage,
|
||||
new LegacyFTB::Page(this),
|
||||
twitchPage
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ class NewInstanceDialog;
|
||||
class PageContainer;
|
||||
class QDialogButtonBox;
|
||||
class ImportPage;
|
||||
class TwitchPage;
|
||||
|
||||
class NewInstanceDialog : public QDialog, public BasePageProvider
|
||||
{
|
||||
@ -67,6 +68,7 @@ private:
|
||||
|
||||
QString InstIconKey;
|
||||
ImportPage *importPage = nullptr;
|
||||
TwitchPage *twitchPage = nullptr;
|
||||
std::unique_ptr<InstanceTask> creationTask;
|
||||
|
||||
bool importIcon = false;
|
||||
|
@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=MultiMC
|
||||
GenericName=MultiMC
|
||||
GenericName=Minecraft Launcher
|
||||
Comment=Free, open source launcher and instance manager for Minecraft.
|
||||
Type=Application
|
||||
Terminal=false
|
||||
|
@ -206,7 +206,7 @@ void VersionPage::updateVersionControls()
|
||||
bool newCraft = controlsEnabled && (minecraftVersion >= Version("1.14"));
|
||||
bool oldCraft = controlsEnabled && (minecraftVersion <= Version("1.12.2"));
|
||||
ui->actionInstall_Fabric->setEnabled(newCraft);
|
||||
ui->actionInstall_Forge->setEnabled(oldCraft);
|
||||
ui->actionInstall_Forge->setEnabled(true);
|
||||
ui->actionInstall_LiteLoader->setEnabled(oldCraft);
|
||||
ui->actionReload->setEnabled(true);
|
||||
updateButtons();
|
||||
|
38
application/pages/modplatform/twitch/TwitchData.h
Normal file
38
application/pages/modplatform/twitch/TwitchData.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
namespace Twitch {
|
||||
|
||||
struct ModpackAuthor {
|
||||
QString name;
|
||||
QString url;
|
||||
};
|
||||
|
||||
struct ModpackFile {
|
||||
int addonId;
|
||||
int fileId;
|
||||
QString version;
|
||||
QString mcVersion;
|
||||
QString downloadUrl;
|
||||
};
|
||||
|
||||
struct Modpack
|
||||
{
|
||||
bool broken = true;
|
||||
int addonId = 0;
|
||||
|
||||
QString name;
|
||||
QString description;
|
||||
QList<ModpackAuthor> authors;
|
||||
QString mcVersion;
|
||||
QString logoName;
|
||||
QString logoUrl;
|
||||
QString websiteUrl;
|
||||
|
||||
ModpackFile latestFile;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(Twitch::Modpack)
|
314
application/pages/modplatform/twitch/TwitchModel.cpp
Normal file
314
application/pages/modplatform/twitch/TwitchModel.cpp
Normal file
@ -0,0 +1,314 @@
|
||||
#include "TwitchModel.h"
|
||||
#include "MultiMC.h"
|
||||
|
||||
#include <MMCStrings.h>
|
||||
#include <Version.h>
|
||||
|
||||
#include <QtMath>
|
||||
#include <QLabel>
|
||||
|
||||
#include <RWStorage.h>
|
||||
#include <Env.h>
|
||||
|
||||
#include "net/URLConstants.h"
|
||||
|
||||
namespace Twitch {
|
||||
|
||||
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ListModel::~ListModel()
|
||||
{
|
||||
}
|
||||
|
||||
int ListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return modpacks.size();
|
||||
}
|
||||
|
||||
int ListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant 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::ToolTipRole)
|
||||
{
|
||||
if(pack.description.length() > 100)
|
||||
{
|
||||
//some magic to prevent to long tooltips and replace html linebreaks
|
||||
QString edit = pack.description.left(97);
|
||||
edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
|
||||
return edit;
|
||||
|
||||
}
|
||||
return pack.description;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
void ListModel::logoLoaded(QString logo, QIcon out)
|
||||
{
|
||||
m_loadingLogos.removeAll(logo);
|
||||
m_logoMap.insert(logo, 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 ListModel::logoFailed(QString logo)
|
||||
{
|
||||
m_failedLogos.append(logo);
|
||||
m_loadingLogos.removeAll(logo);
|
||||
}
|
||||
|
||||
void ListModel::requestLogo(QString logo, QString url)
|
||||
{
|
||||
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MetaEntryPtr entry = ENV.metacache()->resolveEntry("TwitchPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
|
||||
NetJob *job = new NetJob(QString("Twitch Icon Download %1").arg(logo));
|
||||
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
|
||||
|
||||
auto fullPath = entry->getFullPath();
|
||||
QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath]
|
||||
{
|
||||
emit logoLoaded(logo, QIcon(fullPath));
|
||||
if(waitingCallbacks.contains(logo))
|
||||
{
|
||||
waitingCallbacks.value(logo)(fullPath);
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(job, &NetJob::failed, this, [this, logo]
|
||||
{
|
||||
emit logoFailed(logo);
|
||||
});
|
||||
|
||||
job->start();
|
||||
|
||||
m_loadingLogos.append(logo);
|
||||
}
|
||||
|
||||
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
|
||||
{
|
||||
if(m_logoMap.contains(logo))
|
||||
{
|
||||
callback(ENV.metacache()->resolveEntry("TwitchPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
requestLogo(logo, logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
return QAbstractListModel::flags(index);
|
||||
}
|
||||
|
||||
bool ListModel::canFetchMore(const QModelIndex& parent) const
|
||||
{
|
||||
return searchState == CanPossiblyFetchMore;
|
||||
}
|
||||
|
||||
void ListModel::fetchMore(const QModelIndex& parent)
|
||||
{
|
||||
if (parent.isValid())
|
||||
return;
|
||||
if(nextSearchOffset == 0) {
|
||||
qWarning() << "fetchMore with 0 offset is wrong...";
|
||||
return;
|
||||
}
|
||||
performPaginatedSearch();
|
||||
}
|
||||
|
||||
void ListModel::performPaginatedSearch()
|
||||
{
|
||||
NetJob *netJob = new NetJob("Twitch::Search");
|
||||
auto searchUrl = QString(
|
||||
"https://addons-ecs.forgesvc.net/api/v2/addon/search?"
|
||||
"categoryId=0&"
|
||||
"gameId=432&"
|
||||
//"gameVersion=1.12.2&"
|
||||
"index=%1&"
|
||||
"pageSize=25&"
|
||||
"searchFilter=%2&"
|
||||
"sectionId=4471&"
|
||||
"sort=0"
|
||||
).arg(nextSearchOffset).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 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;
|
||||
}
|
||||
nextSearchOffset = 0;
|
||||
performPaginatedSearch();
|
||||
}
|
||||
|
||||
void Twitch::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 Twitch at " << parse_error.offset << " reason: " << parse_error.errorString();
|
||||
qWarning() << response;
|
||||
return;
|
||||
}
|
||||
|
||||
QList<Modpack> newList;
|
||||
auto objs = doc.array();
|
||||
for(auto projectIter: objs) {
|
||||
Modpack pack;
|
||||
auto project = projectIter.toObject();
|
||||
pack.addonId = project.value("id").toInt(0);
|
||||
if (pack.addonId == 0) {
|
||||
qWarning() << "Pack without an ID, skipping: " << pack.name;
|
||||
continue;
|
||||
}
|
||||
pack.name = project.value("name").toString();
|
||||
pack.websiteUrl = project.value("websiteUrl").toString();
|
||||
pack.description = project.value("summary").toString();
|
||||
bool thumbnailFound = false;
|
||||
auto attachments = project.value("attachments").toArray();
|
||||
for(auto attachmentIter: attachments) {
|
||||
auto attachment = attachmentIter.toObject();
|
||||
bool isDefault = attachment.value("isDefault").toBool(false);
|
||||
if(isDefault) {
|
||||
thumbnailFound = true;
|
||||
pack.logoName = attachment.value("title").toString();
|
||||
pack.logoUrl = attachment.value("thumbnailUrl").toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!thumbnailFound) {
|
||||
qWarning() << "Pack without an icon, skipping: " << pack.name;
|
||||
continue;
|
||||
}
|
||||
auto authors = project.value("authors").toArray();
|
||||
for(auto authorIter: authors) {
|
||||
auto author = authorIter.toObject();
|
||||
ModpackAuthor packAuthor;
|
||||
packAuthor.name = author.value("name").toString();
|
||||
packAuthor.url = author.value("url").toString();
|
||||
pack.authors.append(packAuthor);
|
||||
}
|
||||
int defaultFileId = project.value("defaultFileId").toInt(0);
|
||||
if(defaultFileId == 0) {
|
||||
qWarning() << "Pack without default file, skipping: " << pack.name;
|
||||
continue;
|
||||
}
|
||||
bool found = false;
|
||||
auto files = project.value("latestFiles").toArray();
|
||||
for(auto fileIter: files) {
|
||||
auto file = fileIter.toObject();
|
||||
int id = file.value("id").toInt(0);
|
||||
// NOTE: for now, ignore everything that's not the default...
|
||||
if(id != defaultFileId) {
|
||||
continue;
|
||||
}
|
||||
pack.latestFile.addonId = pack.addonId;
|
||||
pack.latestFile.fileId = id;
|
||||
// FIXME: what to do when there's more than one, or there's no version?
|
||||
auto versionArray = file.value("gameVersion").toArray();
|
||||
if(versionArray.size() != 1) {
|
||||
continue;
|
||||
}
|
||||
pack.latestFile.mcVersion = versionArray[0].toString();
|
||||
pack.latestFile.version = file.value("displayName").toString();
|
||||
pack.latestFile.downloadUrl = file.value("downloadUrl").toString();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if(!found) {
|
||||
qWarning() << "Pack with no good file, skipping: " << pack.name;
|
||||
continue;
|
||||
}
|
||||
pack.broken = false;
|
||||
newList.append(pack);
|
||||
}
|
||||
if(objs.size() < 25) {
|
||||
searchState = Finished;
|
||||
} else {
|
||||
nextSearchOffset += 25;
|
||||
searchState = CanPossiblyFetchMore;
|
||||
}
|
||||
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
|
||||
modpacks.append(newList);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void Twitch::ListModel::searchRequestFailed(QString reason)
|
||||
{
|
||||
jobPtr.reset();
|
||||
|
||||
if(searchState == ResetRequested) {
|
||||
beginResetModel();
|
||||
modpacks.clear();
|
||||
endResetModel();
|
||||
|
||||
nextSearchOffset = 0;
|
||||
performPaginatedSearch();
|
||||
} else {
|
||||
searchState = Finished;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
76
application/pages/modplatform/twitch/TwitchModel.h
Normal file
76
application/pages/modplatform/twitch/TwitchModel.h
Normal file
@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <modplatform/legacy_ftb/PackHelpers.h>
|
||||
#include <RWStorage.h>
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QThreadPool>
|
||||
#include <QIcon>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QMetaType>
|
||||
|
||||
#include <functional>
|
||||
#include <net/NetJob.h>
|
||||
|
||||
#include "TwitchData.h"
|
||||
|
||||
namespace Twitch {
|
||||
|
||||
|
||||
typedef QMap<QString, QIcon> LogoMap;
|
||||
typedef std::function<void(QString)> LogoCallback;
|
||||
|
||||
class ListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ListModel(QObject *parent);
|
||||
virtual ~ListModel();
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
int columnCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
bool canFetchMore(const QModelIndex & parent) const override;
|
||||
void fetchMore(const QModelIndex & parent) override;
|
||||
|
||||
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
|
||||
void searchWithTerm(const QString & term);
|
||||
|
||||
private slots:
|
||||
void performPaginatedSearch();
|
||||
|
||||
void logoFailed(QString logo);
|
||||
void logoLoaded(QString logo, QIcon out);
|
||||
|
||||
void searchRequestFinished();
|
||||
void searchRequestFailed(QString reason);
|
||||
|
||||
private:
|
||||
void requestLogo(QString file, QString url);
|
||||
|
||||
private:
|
||||
QList<Modpack> modpacks;
|
||||
QStringList m_failedLogos;
|
||||
QStringList m_loadingLogos;
|
||||
LogoMap m_logoMap;
|
||||
QMap<QString, LogoCallback> waitingCallbacks;
|
||||
|
||||
QString currentSearchTerm;
|
||||
int nextSearchOffset = 0;
|
||||
enum SearchState {
|
||||
None,
|
||||
CanPossiblyFetchMore,
|
||||
ResetRequested,
|
||||
Finished
|
||||
} searchState = None;
|
||||
NetJobPtr jobPtr;
|
||||
QByteArray response;
|
||||
};
|
||||
|
||||
}
|
111
application/pages/modplatform/twitch/TwitchPage.cpp
Normal file
111
application/pages/modplatform/twitch/TwitchPage.cpp
Normal file
@ -0,0 +1,111 @@
|
||||
#include "TwitchPage.h"
|
||||
#include "ui_TwitchPage.h"
|
||||
|
||||
#include "MultiMC.h"
|
||||
#include "dialogs/NewInstanceDialog.h"
|
||||
#include <InstanceImportTask.h>
|
||||
#include "TwitchModel.h"
|
||||
#include <QKeyEvent>
|
||||
|
||||
TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent)
|
||||
: QWidget(parent), ui(new Ui::TwitchPage), dialog(dialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
connect(ui->searchButton, &QPushButton::clicked, this, &TwitchPage::triggerSearch);
|
||||
ui->searchEdit->installEventFilter(this);
|
||||
model = new Twitch::ListModel(this);
|
||||
ui->packView->setModel(model);
|
||||
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TwitchPage::onSelectionChanged);
|
||||
}
|
||||
|
||||
TwitchPage::~TwitchPage()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
bool TwitchPage::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);
|
||||
}
|
||||
|
||||
bool TwitchPage::shouldDisplay() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void TwitchPage::openedImpl()
|
||||
{
|
||||
suggestCurrent();
|
||||
}
|
||||
|
||||
void TwitchPage::triggerSearch()
|
||||
{
|
||||
model->searchWithTerm(ui->searchEdit->text());
|
||||
}
|
||||
|
||||
void TwitchPage::onSelectionChanged(QModelIndex first, QModelIndex second)
|
||||
{
|
||||
if(!first.isValid())
|
||||
{
|
||||
if(isOpened)
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
ui->frame->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
current = model->data(first, Qt::UserRole).value<Twitch::Modpack>();
|
||||
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 = [](Twitch::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);
|
||||
suggestCurrent();
|
||||
}
|
||||
|
||||
void TwitchPage::suggestCurrent()
|
||||
{
|
||||
if(!isOpened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(current.broken)
|
||||
{
|
||||
dialog->setSuggestedPack();
|
||||
}
|
||||
|
||||
dialog->setSuggestedPack(current.name, new InstanceImportTask(current.latestFile.downloadUrl));
|
||||
QString editedLogoName;
|
||||
editedLogoName = "twitch_" + current.logoName.section(".", 0, 0);
|
||||
model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
|
||||
{
|
||||
dialog->setSuggestedIconFromFile(logo, editedLogoName);
|
||||
});
|
||||
}
|
77
application/pages/modplatform/twitch/TwitchPage.h
Normal file
77
application/pages/modplatform/twitch/TwitchPage.h
Normal file
@ -0,0 +1,77 @@
|
||||
/* Copyright 2013-2019 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 "TwitchData.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class TwitchPage;
|
||||
}
|
||||
|
||||
class NewInstanceDialog;
|
||||
|
||||
namespace Twitch {
|
||||
class ListModel;
|
||||
}
|
||||
|
||||
class TwitchPage : public QWidget, public BasePage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TwitchPage(NewInstanceDialog* dialog, QWidget *parent = 0);
|
||||
virtual ~TwitchPage();
|
||||
virtual QString displayName() const override
|
||||
{
|
||||
return tr("Twitch");
|
||||
}
|
||||
virtual QIcon icon() const override
|
||||
{
|
||||
return MMC->getThemedIcon("twitch");
|
||||
}
|
||||
virtual QString id() const override
|
||||
{
|
||||
return "twitch";
|
||||
}
|
||||
virtual QString helpPage() const override
|
||||
{
|
||||
return "Twitch-platform";
|
||||
}
|
||||
virtual bool shouldDisplay() const override;
|
||||
|
||||
void openedImpl() override;
|
||||
|
||||
bool eventFilter(QObject * watched, QEvent * event) override;
|
||||
|
||||
private:
|
||||
void suggestCurrent();
|
||||
|
||||
private slots:
|
||||
void triggerSearch();
|
||||
void onSelectionChanged(QModelIndex first, QModelIndex second);
|
||||
|
||||
private:
|
||||
Ui::TwitchPage *ui = nullptr;
|
||||
NewInstanceDialog* dialog = nullptr;
|
||||
Twitch::ListModel* model = nullptr;
|
||||
Twitch::Modpack current;
|
||||
};
|
88
application/pages/modplatform/twitch/TwitchPage.ui
Normal file
88
application/pages/modplatform/twitch/TwitchPage.ui
Normal file
@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TwitchPage</class>
|
||||
<widget class="QWidget" name="TwitchPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>875</width>
|
||||
<height>745</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLineEdit" name="searchEdit"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="searchButton">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QTreeView" 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>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="MCModInfoFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>MCModInfoFrame</class>
|
||||
<extends>QFrame</extends>
|
||||
<header>widgets/MCModInfoFrame.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>searchEdit</tabstop>
|
||||
<tabstop>searchButton</tabstop>
|
||||
<tabstop>packView</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
75
changelog.md
75
changelog.md
@ -1,4 +1,69 @@
|
||||
# MultiMC 0.6.7
|
||||
# MultiMC 0.6.11
|
||||
|
||||
This adds Forge 1.13+ support using [ForgeWrapper](https://github.com/ZekerZhayard/ForgeWrapper) by ZekerZhayard.
|
||||
|
||||
### New or changed features
|
||||
|
||||
- GH-2988: You can now import instances and curse modpacks from the command line with the `--import` option followed by either an URL or a local file path.
|
||||
|
||||
- GH-2544: MultiMC now supports downloading library files without including them on the Java classpath.
|
||||
|
||||
This is done by adding them to the `mavenFiles` list instead of the `libraries` list.
|
||||
|
||||
Such downloads are not deduplicated or version upgraded like libraries are.
|
||||
|
||||
This enables ForgeWrapper to work - MultiMC downloads all the files, and ForgeWrapper runs the Forge installer during instance start when needed.
|
||||
|
||||
# Previous releases
|
||||
|
||||
## MultiMC 0.6.8
|
||||
|
||||
This is mostly about removal of the 'curse URL' related features, because they were of low quality and generally unreliable.
|
||||
|
||||
There are some bug fixes included.
|
||||
|
||||
MultiMC also migrated to a new continuous deployment system, which makes everything that much smoother.
|
||||
|
||||
### New or changed features
|
||||
|
||||
- GH-852: Instance group expansion status now saves/loads as expected.
|
||||
|
||||
- The bees have invaded the launcher. We now have a bee icon.
|
||||
|
||||
- Translations have been overhauled, yet again...
|
||||
|
||||
- We now have a [crowdin site](https://translate.multimc.org/) for all the translation work.
|
||||
|
||||
- Translations are made based on the development version, and for the development version.
|
||||
|
||||
- Many strings have been tweaked to make translating the application easier.
|
||||
|
||||
- When selecting languages, European Portuguese is now displaying properly.
|
||||
|
||||
- Accessibility has been further improved - the main window reads as `MultiMC`, not a long string of nonsensical version numbers, when announced by a screen reader.
|
||||
|
||||
- Removed the unimplemented Technic page from instance creation dialog.
|
||||
|
||||
- GH-2859: Broken twitch URL import method was removed.
|
||||
|
||||
- GH-2819: Filter bar in mod lists now also works with descriptions and author lists.
|
||||
|
||||
- GH-2832: Version page now has buttons for opening the Minecraft and internal libraries folders of the instance.
|
||||
|
||||
- GH-2769: When copying an instance, there's now an option to keep or remove the total play time from the copy.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- GH-2880: Clicking the service status indicators now opens a valid site again, instead of going nowhere.
|
||||
|
||||
- GH-2853: When collapsing groups in instance view, the action no longer becomes 'sticky' and doesn't apply to items clicked afterwards.
|
||||
|
||||
- GH-2787: "Download All" button works again.
|
||||
|
||||
- When a component is customized, the launcher will not try to update it in an infinite loop when something else requires a different version.
|
||||
|
||||
|
||||
## MultiMC 0.6.7
|
||||
|
||||
The previous release introduced some extra buttons that made the instance window way too big for some displays. This release is aimed at fixing that, along with other UI and performance improvements.
|
||||
|
||||
@ -47,8 +112,6 @@ There are some accessibility fixes thrown in too.
|
||||
Sorting cascades from 'Enabled' to 'Name' and then 'Version'. This means that if you sort 'Enabled', the enabled and disabled mods are still sorted
|
||||
by name and mods with the same name will be also sorted by version.
|
||||
|
||||
# Previous releases
|
||||
|
||||
## MultiMC 0.6.6
|
||||
|
||||
This release is mostly the smaller things that have accumulated over time, along with a big change in linux packaging.
|
||||
@ -70,7 +133,7 @@ MultiMC on linux is built with Qt 5.4 and older versions of Qt will not work.
|
||||
|
||||
This should be a massive improvement to system integration on linux and resolves GH-1784, GH-2605, GH-1979, GH-2271, GH-1992, GH-1816 and their many duplicates.
|
||||
|
||||
#### New or changed features
|
||||
### New or changed features
|
||||
|
||||
- GH-2487: No is now the default button when deleting instances.
|
||||
|
||||
@ -100,7 +163,7 @@ This should be a massive improvement to system integration on linux and resolves
|
||||
|
||||
You can now drag the purple download buttons from CurseForge into MultiMC and get a modpack out of it. Much easier!
|
||||
|
||||
#### Bugfixes
|
||||
### Bugfixes
|
||||
|
||||
- Translation folder is now created sooner, making first launch translation fetch work again.
|
||||
|
||||
@ -134,7 +197,7 @@ This should be a massive improvement to system integration on linux and resolves
|
||||
|
||||
Finalizing the translation workflow improvements and adding fixes for sounds missing in old game versions.
|
||||
|
||||
#### New or changed features
|
||||
### New or changed features
|
||||
|
||||
- UI for the language settings has been unified across the application
|
||||
|
||||
|
@ -143,7 +143,7 @@ public class Launcher extends Applet implements AppletStub
|
||||
public URL getDocumentBase()
|
||||
{
|
||||
try {
|
||||
return new URL("http://www.minecraft.net/game/");
|
||||
return new URL("http", "www.minecraft.net", 80, "/game/", null);
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user