Merge pull request #565 from Scrumplex/modrinth-packs

Initial Modrinth modpack support
This commit is contained in:
Sefa Eyeoglu
2022-05-17 16:09:22 +02:00
committed by GitHub
29 changed files with 1701 additions and 150 deletions

View File

@ -13,7 +13,7 @@
#include <QDialogButtonBox>
#include "ui/widgets/PageContainer.h"
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ModDownloadTask.h"
@ -98,7 +98,7 @@ void ModDownloadDialog::accept()
QList<BasePage *> ModDownloadDialog::getPages()
{
modrinthPage = new ModrinthPage(this, m_instance);
modrinthPage = new ModrinthModPage(this, m_instance);
flameModPage = new FlameModPage(this, m_instance);
return
{

View File

@ -16,7 +16,7 @@ class ModDownloadDialog;
class PageContainer;
class QDialogButtonBox;
class ModrinthPage;
class ModrinthModPage;
class ModDownloadDialog : public QDialog, public BasePageProvider
{
@ -50,7 +50,7 @@ private:
QVBoxLayout *m_verticalLayout = nullptr;
ModrinthPage *modrinthPage = nullptr;
ModrinthModPage *modrinthPage = nullptr;
FlameModPage *flameModPage = nullptr;
QHash<QString, ModDownloadTask*> modTask;
BaseInstance *m_instance;

View File

@ -39,6 +39,7 @@
#include "ui/pages/modplatform/legacy_ftb/Page.h"
#include "ui/pages/modplatform/flame/FlamePage.h"
#include "ui/pages/modplatform/ImportPage.h"
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h"
@ -134,6 +135,7 @@ QList<BasePage *> NewInstanceDialog::getPages()
flamePage,
new FtbPage(this),
new LegacyFTB::Page(this),
new ModrinthPage(this),
technicPage
};
}

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -109,7 +110,11 @@ void ImportPage::updateState()
{
// FIXME: actually do some validation of what's inside here... this is fake AF
QFileInfo fi(input);
if(fi.exists() && fi.suffix() == "zip")
// mrpack is a modrinth pack
// Allow non-latin people to use ZIP files!
auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip");
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
{
QFileInfo fi(url.fileName());
dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
@ -144,6 +149,7 @@ void ImportPage::setUrl(const QString& url)
void ImportPage::on_modpackBtn_clicked()
{
auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString();
filter += ";;" + tr("Modrinth pack (*.mrpack)");
const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter);
if (url.isValid())
{

View File

@ -11,28 +11,75 @@
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QPushButton" name="modpackBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QLineEdit" name="modpackEdit">
<property name="placeholderText">
<string notr="true">http://</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="modpackLabel">
<item row="2" column="1">
<widget class="QPushButton" name="modpackBtn">
<property name="text">
<string>Local file or link to a direct download:</string>
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" rowspan="2" colspan="2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>The following file types are implemented (both for local files and URLs):</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>- Curseforge modpacks (ZIP)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>- Modrinth modpacks (ZIP and mrpack)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>- PolyMC / MultiMC exported instances (ZIP)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>- Technic modpacks (ZIP)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -45,6 +92,13 @@
</property>
</spacer>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="modpackLabel">
<property name="text">
<string>Local file or link to a direct download:</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ModrinthModModel.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
namespace Modrinth {
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
Modrinth::loadIndexedPack(m, obj);
}
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
} // namespace Modrinth

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "ModrinthModPage.h"
namespace Modrinth {
class ListModel : public ModPlatform::ListModel {
Q_OBJECT
public:
ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){};
~ListModel() override = default;
private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
static const char* sorts[5];
inline auto getSorts() const -> const char** override { return sorts; };
};
} // namespace Modrinth

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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 "ModrinthModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui_ModPage.h"
#include "ModrinthModModel.h"
#include "ui/dialogs/ModDownloadDialog.h"
ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance)
: ModPage(dialog, instance, new ModrinthAPI())
{
listModel = new Modrinth::ListModel(this);
ui->packView->setModel(listModel);
// index is used to set the sorting with the modrinth api
ui->sortByBox->addItem(tr("Sort by Relevance"));
ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Newest"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's constructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected);
}
auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
{
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader);
auto loaderCompatible = false;
for (auto remoteLoader : ver.loaders)
{
if (loaderStrings.contains(remoteLoader)) {
loaderCompatible = true;
break;
}
}
return ver.mcVersion.contains(mineVer) && loaderCompatible;
}
// I don't know why, but doing this on the parent class makes it so that
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }

View File

@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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 "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
class ModrinthModPage : public ModPage {
Q_OBJECT
public:
explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~ModrinthModPage() override = default;
inline auto displayName() const -> QString override { return "Modrinth"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,30 +14,301 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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 "ModrinthModel.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
#include "BuildConfig.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include <QMessageBox>
namespace Modrinth {
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" };
ModpackListModel::ModpackListModel(ModrinthPage* parent) : QAbstractListModel(parent), m_parent(parent) {}
void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
auto ModpackListModel::debugName() const -> QString
{
Modrinth::loadIndexedPack(m, obj);
return m_parent->debugName();
}
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
/******** Make data requests ********/
void ModpackListModel::fetchMore(const QModelIndex& parent)
{
Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
if (parent.isValid())
return;
if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
performPaginatedSearch();
}
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant
{
return obj.object().value("hits").toArray();
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
Modrinth::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.iconName)) {
auto icon = m_logoMap.value(pack.iconName);
auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48));
return icon_scaled;
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString());
return icon;
} else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
}
return {};
}
void ModpackListModel::performPaginatedSearch()
{
// TODO: Move to standalone API
NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network());
auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL +
"/search?"
"offset=%1&"
"limit=%2&"
"query=%3&"
"index=%4&"
"facets=[[\"project_type:modpack\"]]")
.arg(nextSearchOffset)
.arg(m_modpacks_per_page)
.arg(currentSearchTerm)
.arg(currentSort);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response));
QObject::connect(netJob, &NetJob::succeeded, this, [this] {
QJsonParseError parse_error_all{};
QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all);
if (parse_error_all.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset
<< " reason: " << parse_error_all.errorString();
qWarning() << m_all_response;
return;
}
searchRequestFinished(doc_all);
});
QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
jobPtr = netJob;
jobPtr->start();
}
void ModpackListModel::refresh()
{
if (jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
} else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
}
static auto sortFromIndex(int index) -> QString
{
switch(index){
default:
case 1:
return "relevance";
case 2:
return "downloads";
case 3:
return "follows";
case 4:
return "newest";
case 5:
return "updated";
}
return {};
}
void ModpackListModel::searchWithTerm(const QString& term, const int sort)
{
if(sort > 5 || sort < 0)
return;
auto sort_str = sortFromIndex(sort);
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) {
return;
}
currentSearchTerm = term;
currentSort = sort_str;
refresh();
}
void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
{
if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()
->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))
->getFullPath());
} else {
requestLogo(logo, logoUrl);
}
}
void ModpackListModel::requestLogo(QString logo, QString url)
{
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
return;
}
MetaEntryPtr entry =
APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
job->deleteLater();
emit logoFailed(logo);
});
job->start();
m_loadingLogos.append(logo);
}
/******** Request callbacks ********/
void ModpackListModel::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].iconName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
void ModpackListModel::logoFailed(QString logo)
{
m_failedLogos.append(logo);
m_loadingLogos.removeAll(logo);
}
void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
{
jobPtr.reset();
QList<Modrinth::Modpack> newList;
auto packs_all = doc_all.object().value("hits").toArray();
for (auto packRaw : packs_all) {
auto packObj = packRaw.toObject();
Modrinth::Modpack pack;
try {
Modrinth::loadIndexedPack(pack, packObj);
newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
continue;
}
}
if (packs_all.size() < m_modpacks_per_page) {
searchState = Finished;
} else {
nextSearchOffset += m_modpacks_per_page;
searchState = CanPossiblyFetchMore;
}
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
}
void ModpackListModel::searchRequestFailed(QString reason)
{
if (!jobPtr->first()->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
} else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
QString("%1 %2")
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
}
jobPtr.reset();
if (searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
endResetModel();
nextSearchOffset = 0;
performPaginatedSearch();
} else {
searchState = Finished;
}
}
} // namespace Modrinth
/******** Helpers ********/

View File

@ -1,25 +1,116 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 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 "ModrinthPage.h"
#include <QAbstractListModel>
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
class ModPage;
class Version;
namespace Modrinth {
class ListModel : public ModPlatform::ListModel {
using LogoMap = QMap<QString, QIcon>;
using LogoCallback = std::function<void (QString)>;
class ModpackListModel : public QAbstractListModel {
Q_OBJECT
public:
ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){};
~ListModel() override = default;
ModpackListModel(ModrinthPage* parent);
~ModpackListModel() override = default;
private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
static const char* sorts[5];
inline auto getSorts() const -> const char** override { return sorts; };
auto debugName() const -> QString;
/* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
/* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, const int sort);
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; };
public slots:
void searchRequestFinished(QJsonDocument& doc_all);
void searchRequestFailed(QString reason);
protected slots:
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
void performPaginatedSearch();
protected:
void requestLogo(QString file, QString url);
inline auto getMineVersions() const -> std::list<Version>;
protected:
ModrinthPage* m_parent;
QList<Modrinth::Modpack> modpacks;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
QString currentSearchTerm;
QString currentSort;
int nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
NetJob::Ptr jobPtr;
QByteArray m_all_response;
QByteArray m_specific_response;
int m_modpacks_per_page = 20;
};
} // namespace Modrinth
} // namespace ModPlatform

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,6 +19,7 @@
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2021-2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,49 +35,237 @@
*/
#include "ModrinthPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui_ModPage.h"
#include "ui_ModrinthPage.h"
#include "ModrinthModel.h"
#include "ui/dialogs/ModDownloadDialog.h"
ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance)
: ModPage(dialog, instance, new ModrinthAPI())
#include "BuildConfig.h"
#include "InstanceImportTask.h"
#include "Json.h"
#include <HoeDown.h>
#include <QComboBox>
#include <QKeyEvent>
#include <QPushButton>
ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog)
{
listModel = new Modrinth::ListModel(this);
ui->packView->setModel(listModel);
ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
m_model = new Modrinth::ModpackListModel(this);
ui->packView->setModel(m_model);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
// index is used to set the sorting with the modrinth api
ui->sortByBox->addItem(tr("Sort by Relevance"));
ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Total Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Newest"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's constructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected);
}
auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
ModrinthPage::~ModrinthPage()
{
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader);
delete ui;
}
void ModrinthPage::retranslate()
{
ui->retranslateUi(this);
}
void ModrinthPage::openedImpl()
{
BasePage::openedImpl();
triggerSearch();
}
bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
auto* keyEvent = reinterpret_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
this->triggerSearch();
keyEvent->accept();
return true;
}
}
return QObject::eventFilter(watched, event);
}
void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
{
ui->versionSelectionBox->clear();
if (!first.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
}
return;
}
current = m_model->data(first, Qt::UserRole).value<Modrinth::Modpack>();
auto name = current.name;
if (!current.extraInfoLoaded) {
qDebug() << "Loading modrinth modpack information";
auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network());
auto response = new QByteArray();
QString id = current.id;
netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] {
if (id != current.id) {
return; // wrong request?
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
auto obj = Json::requireObject(doc);
try {
Modrinth::loadIndexedInfo(current, obj);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading modrinth modpack version: " << e.cause();
}
updateUI();
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
netJob->deleteLater();
delete response;
});
netJob->start();
} else
updateUI();
if (!current.versionsLoaded) {
qDebug() << "Loading modrinth modpack versions";
auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = new QByteArray();
QString id = current.id;
netJob->addNetAction(
Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] {
if (id != current.id) {
return; // wrong request?
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
Modrinth::loadIndexedVersions(current, doc);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading modrinth modpack version: " << e.cause();
}
for (auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.id));
}
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
netJob->deleteLater();
delete response;
});
netJob->start();
} else {
for (auto version : current.versions) {
ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id));
}
suggestCurrent();
}
}
void ModrinthPage::updateUI()
{
QString text = "";
if (current.extra.projectUrl.isEmpty())
text = current.name;
else
text = "<a href=\"" + current.extra.projectUrl + "\">" + current.name + "</a>";
// TODO: Implement multiple authors with links
text += "<br>" + tr(" by ") + QString("<a href=%1>%2</a>").arg(std::get<1>(current.author).toString(), std::get<0>(current.author));
text += "<br>";
HoeDown h;
text += h.process(current.extra.body.toUtf8());
ui->packDescription->setHtml(text + current.description);
}
void ModrinthPage::suggestCurrent()
{
if (!isOpened) {
return;
}
if (selectedVersion.isEmpty()) {
dialog->setSuggestedPack();
return;
}
for (auto& ver : current.versions) {
if (ver.id == selectedVersion) {
dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url, this));
auto iconName = current.iconName;
m_model->getLogo(iconName, current.iconUrl.toString(),
[this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); });
auto loaderCompatible = false;
for (auto remoteLoader : ver.loaders)
{
if (loaderStrings.contains(remoteLoader)) {
loaderCompatible = true;
break;
}
}
return ver.mcVersion.contains(mineVer) && loaderCompatible;
}
// I don't know why, but doing this on the parent class makes it so that
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto ModrinthPage::shouldDisplay() const -> bool { return true; }
void ModrinthPage::triggerSearch()
{
m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
}
void ModrinthPage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = "";
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toString();
suggestCurrent();
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,6 +19,7 @@
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
* Copyright 2021-2022 kb1000
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -35,27 +36,56 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "Application.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "ui/pages/BasePage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
class ModrinthPage : public ModPage {
#include <QWidget>
namespace Ui {
class ModrinthPage;
}
namespace Modrinth {
class ModpackListModel;
}
class ModrinthPage : public QWidget, public BasePage {
Q_OBJECT
public:
explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance);
~ModrinthPage() override = default;
explicit ModrinthPage(NewInstanceDialog* dialog, QWidget* parent = nullptr);
~ModrinthPage() override;
inline auto displayName() const -> QString override { return "Modrinth"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
QString displayName() const override { return tr("Modrinth"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); }
QString id() const override { return "modrinth"; }
QString helpPage() const override { return "Modrinth-platform"; }
inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
inline auto debugName() const -> QString { return "Modrinth"; }
inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override;
auto getCurrent() -> Modrinth::Modpack& { return current; }
void suggestCurrent();
auto shouldDisplay() const -> bool override;
void updateUI();
void retranslate() override;
void openedImpl() override;
bool eventFilter(QObject* watched, QEvent* event) override;
private slots:
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void triggerSearch();
private:
Ui::ModrinthPage* ui;
NewInstanceDialog* dialog;
Modrinth::ModpackListModel* m_model;
Modrinth::Modpack current;
QString selectedVersion;
};

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModrinthPage</class>
<widget class="QWidget" name="ModrinthPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>837</width>
<height>685</height>
</rect>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Note: Modrinth modpacks are still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QListView" name="packView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QComboBox" name="sortByBox"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>searchButton</tabstop>
<tabstop>packView</tabstop>
<tabstop>packDescription</tabstop>
<tabstop>sortByBox</tabstop>
<tabstop>versionSelectionBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>