chore: pull changes from upstream

This commit is contained in:
OverMighty 2020-05-04 11:36:41 +02:00
commit 313a6574c1
24 changed files with 848 additions and 20 deletions

View File

@ -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 git clone https://github.com/MultiMC/MultiMC5.git
cd MultiMC5
git submodule init git submodule init
git submodule update git submodule update
cd MultiMC5
mkdir build mkdir build
cd build cd build
export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 export CMAKE_PREFIX_PATH=/usr/local/opt/qt5

View File

@ -46,7 +46,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc
######## Set version numbers ######## ######## Set version numbers ########
set(MultiMC_VERSION_MAJOR 0) set(MultiMC_VERSION_MAJOR 0)
set(MultiMC_VERSION_MINOR 6) set(MultiMC_VERSION_MINOR 6)
set(MultiMC_VERSION_HOTFIX 7) set(MultiMC_VERSION_HOTFIX 12)
# Build number # Build number
set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")

View File

@ -97,6 +97,7 @@ void Env::initHttpMetaCache()
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").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("skins", QDir("accounts/skins").absolutePath());
m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath());

View File

@ -78,7 +78,7 @@ void Version::parse()
// FIXME: this is bad. versions can contain a lot more separators... // FIXME: this is bad. versions can contain a lot more separators...
QStringList parts = m_string.split('.'); QStringList parts = m_string.split('.');
for (const auto part : parts) for (const auto &part : parts)
{ {
m_sections.append(Section(part)); m_sections.append(Section(part));
} }

View File

@ -11,6 +11,7 @@ void LaunchProfile::clear()
m_mainClass.clear(); m_mainClass.clear();
m_appletClass.clear(); m_appletClass.clear();
m_libraries.clear(); m_libraries.clear();
m_mavenFiles.clear();
m_traits.clear(); m_traits.clear();
m_jarMods.clear(); m_jarMods.clear();
m_mainJar.reset(); 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 const LibraryPtr LaunchProfile::getMainJar() const
{ {
return m_mainJar; return m_mainJar;
@ -253,6 +270,11 @@ const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const
return m_nativeLibraries; return m_nativeLibraries;
} }
const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const
{
return m_mavenFiles;
}
void LaunchProfile::getLibraryFiles( void LaunchProfile::getLibraryFiles(
const QString& architecture, const QString& architecture,
QStringList& jars, QStringList& jars,

View File

@ -20,6 +20,7 @@ public: /* application of profile variables from patches */
void applyJarMods(const QList<LibraryPtr> &jarMods); void applyJarMods(const QList<LibraryPtr> &jarMods);
void applyMods(const QList<LibraryPtr> &jarMods); void applyMods(const QList<LibraryPtr> &jarMods);
void applyLibrary(LibraryPtr library); void applyLibrary(LibraryPtr library);
void applyMavenFile(LibraryPtr library);
void applyMainJar(LibraryPtr jar); void applyMainJar(LibraryPtr jar);
void applyProblemSeverity(ProblemSeverity severity); void applyProblemSeverity(ProblemSeverity severity);
/// clear the profile /// clear the profile
@ -37,6 +38,7 @@ public: /* getters for profile variables */
const QList<LibraryPtr> & getJarMods() const; const QList<LibraryPtr> & getJarMods() const;
const QList<LibraryPtr> & getLibraries() const; const QList<LibraryPtr> & getLibraries() const;
const QList<LibraryPtr> & getNativeLibraries() const; const QList<LibraryPtr> & getNativeLibraries() const;
const QList<LibraryPtr> & getMavenFiles() const;
const LibraryPtr getMainJar() const; const LibraryPtr getMainJar() const;
void getLibraryFiles( void getLibraryFiles(
const QString & architecture, const QString & architecture,
@ -79,10 +81,13 @@ private:
/// the list of libraries /// the list of libraries
QList<LibraryPtr> m_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 /// the main jar
LibraryPtr m_mainJar; LibraryPtr m_mainJar;
/// the list of libraries /// the list of native libraries
QList<LibraryPtr> m_nativeLibraries; QList<LibraryPtr> m_nativeLibraries;
/// traits, collected from all the version files (version files can only add) /// traits, collected from all the version files (version files can only add)

View File

@ -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))) for (auto libVal : requireArray(root.value(which)))
{ {
QJsonObject libObj = requireObject(libVal); QJsonObject libObj = requireObject(libVal);
// parse the library // parse the library
auto lib = libraryFromJson(libObj, filename); auto lib = libraryFromJson(libObj, filename);
out->libraries.append(lib); out.append(lib);
} }
}; };
bool hasPlusLibs = root.contains("+libraries"); bool hasPlusLibs = root.contains("+libraries");
@ -160,16 +160,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
{ {
out->addProblem(ProblemSeverity::Warning, out->addProblem(ProblemSeverity::Warning,
QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported."));
readLibs("libraries"); readLibs("libraries", out->libraries);
readLibs("+libraries"); readLibs("+libraries", out->libraries);
} }
else if (hasLibs) else if (hasLibs)
{ {
readLibs("libraries"); readLibs("libraries", out->libraries);
} }
else if(hasPlusLibs) else if(hasPlusLibs)
{ {
readLibs("+libraries"); readLibs("+libraries", out->libraries);
}
if(root.contains("mavenFiles")) {
readLibs("mavenFiles", out->mavenFiles);
} }
// if we have mainJar, just use it // if we have mainJar, just use it
@ -276,6 +280,15 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
} }
root.insert("libraries", array); 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()) if (!patch->jarMods.isEmpty())
{ {
QJsonArray array; QJsonArray array;

View File

@ -41,6 +41,10 @@ void VersionFile::applyTo(LaunchProfile *profile)
{ {
profile->applyLibrary(library); profile->applyLibrary(library);
} }
for (auto mavenFile : mavenFiles)
{
profile->applyMavenFile(mavenFile);
}
profile->applyProblemSeverity(getProblemSeverity()); profile->applyProblemSeverity(getProblemSeverity());
} }
@ -53,4 +57,4 @@ void VersionFile::applyTo(LaunchProfile *profile)
throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion); throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion);
} }
} }
*/ */

View File

@ -75,6 +75,9 @@ public: /* data */
/// Mojang: list of libraries to add to the version /// Mojang: list of libraries to add to the version
QList<LibraryPtr> libraries; 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) /// The main jar (Minecraft version library, normally)
LibraryPtr mainJar; LibraryPtr mainJar;

View File

@ -45,6 +45,7 @@ void LibrariesTask::executeTask()
QList<LibraryPtr> libArtifactPool; QList<LibraryPtr> libArtifactPool;
libArtifactPool.append(profile->getLibraries()); libArtifactPool.append(profile->getLibraries());
libArtifactPool.append(profile->getNativeLibraries()); libArtifactPool.append(profile->getNativeLibraries());
libArtifactPool.append(profile->getMavenFiles());
libArtifactPool.append(profile->getMainJar()); libArtifactPool.append(profile->getMainJar());
processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath());

View File

@ -133,6 +133,11 @@ SET(MULTIMC_SOURCES
pages/modplatform/legacy_ftb/Page.h pages/modplatform/legacy_ftb/Page.h
pages/modplatform/legacy_ftb/ListModel.h pages/modplatform/legacy_ftb/ListModel.h
pages/modplatform/legacy_ftb/ListModel.cpp 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.cpp
pages/modplatform/ImportPage.h pages/modplatform/ImportPage.h
@ -251,6 +256,7 @@ SET(MULTIMC_UIS
# Platform pages # Platform pages
pages/modplatform/VanillaPage.ui pages/modplatform/VanillaPage.ui
pages/modplatform/legacy_ftb/Page.ui pages/modplatform/legacy_ftb/Page.ui
pages/modplatform/twitch/TwitchPage.ui
pages/modplatform/ImportPage.ui pages/modplatform/ImportPage.ui
# Dialogs # Dialogs

View File

@ -46,6 +46,7 @@ QString getCreditsHtml(QStringList patrons)
stream << "<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>\n"; stream << "<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>\n";
stream << "<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>\n"; stream << "<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>\n";
stream << "<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>\n"; stream << "<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>\n";
stream << "<p>Zeker Zhayard &lt;<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>&gt;</p>\n";
stream << "<br />\n"; stream << "<br />\n";
if(!patrons.isEmpty()) { if(!patrons.isEmpty()) {

View File

@ -35,6 +35,7 @@
#include "widgets/PageContainer.h" #include "widgets/PageContainer.h"
#include <pages/modplatform/VanillaPage.h> #include <pages/modplatform/VanillaPage.h>
#include <pages/modplatform/legacy_ftb/Page.h> #include <pages/modplatform/legacy_ftb/Page.h>
#include <pages/modplatform/twitch/TwitchPage.h>
#include <pages/modplatform/ImportPage.h> #include <pages/modplatform/ImportPage.h>
@ -119,11 +120,13 @@ void NewInstanceDialog::accept()
QList<BasePage *> NewInstanceDialog::getPages() QList<BasePage *> NewInstanceDialog::getPages()
{ {
importPage = new ImportPage(this); importPage = new ImportPage(this);
twitchPage = new TwitchPage(this);
return return
{ {
new VanillaPage(this), new VanillaPage(this),
importPage, importPage,
new LegacyFTB::Page(this), new LegacyFTB::Page(this),
twitchPage
}; };
} }

View File

@ -29,6 +29,7 @@ class NewInstanceDialog;
class PageContainer; class PageContainer;
class QDialogButtonBox; class QDialogButtonBox;
class ImportPage; class ImportPage;
class TwitchPage;
class NewInstanceDialog : public QDialog, public BasePageProvider class NewInstanceDialog : public QDialog, public BasePageProvider
{ {
@ -67,6 +68,7 @@ private:
QString InstIconKey; QString InstIconKey;
ImportPage *importPage = nullptr; ImportPage *importPage = nullptr;
TwitchPage *twitchPage = nullptr;
std::unique_ptr<InstanceTask> creationTask; std::unique_ptr<InstanceTask> creationTask;
bool importIcon = false; bool importIcon = false;

View File

@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=MultiMC Name=MultiMC
GenericName=MultiMC GenericName=Minecraft Launcher
Comment=Free, open source launcher and instance manager for Minecraft. Comment=Free, open source launcher and instance manager for Minecraft.
Type=Application Type=Application
Terminal=false Terminal=false

View File

@ -206,7 +206,7 @@ void VersionPage::updateVersionControls()
bool newCraft = controlsEnabled && (minecraftVersion >= Version("1.14")); bool newCraft = controlsEnabled && (minecraftVersion >= Version("1.14"));
bool oldCraft = controlsEnabled && (minecraftVersion <= Version("1.12.2")); bool oldCraft = controlsEnabled && (minecraftVersion <= Version("1.12.2"));
ui->actionInstall_Fabric->setEnabled(newCraft); ui->actionInstall_Fabric->setEnabled(newCraft);
ui->actionInstall_Forge->setEnabled(oldCraft); ui->actionInstall_Forge->setEnabled(true);
ui->actionInstall_LiteLoader->setEnabled(oldCraft); ui->actionInstall_LiteLoader->setEnabled(oldCraft);
ui->actionReload->setEnabled(true); ui->actionReload->setEnabled(true);
updateButtons(); updateButtons();

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

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

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

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

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

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

View File

@ -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. 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 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. by name and mods with the same name will be also sorted by version.
# Previous releases
## MultiMC 0.6.6 ## MultiMC 0.6.6
This release is mostly the smaller things that have accumulated over time, along with a big change in linux packaging. 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. 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. - 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! 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. - 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. 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 - UI for the language settings has been unified across the application

View File

@ -143,7 +143,7 @@ public class Launcher extends Applet implements AppletStub
public URL getDocumentBase() public URL getDocumentBase()
{ {
try { try {
return new URL("http://www.minecraft.net/game/"); return new URL("http", "www.minecraft.net", 80, "/game/", null);
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
e.printStackTrace(); e.printStackTrace();
} }