aboutsummaryrefslogtreecommitdiff
path: root/launcher/ui/pages
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/ui/pages')
-rw-r--r--launcher/ui/pages/global/APIPage.cpp (renamed from launcher/ui/pages/global/PasteEEPage.cpp)54
-rw-r--r--launcher/ui/pages/global/APIPage.h (renamed from launcher/ui/pages/global/PasteEEPage.h)22
-rw-r--r--launcher/ui/pages/global/APIPage.ui179
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp39
-rw-r--r--launcher/ui/pages/global/AccountListPage.h1
-rw-r--r--launcher/ui/pages/global/AccountListPage.ui6
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp39
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui10
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp5
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui20
-rw-r--r--launcher/ui/pages/global/PasteEEPage.ui128
-rw-r--r--launcher/ui/pages/instance/LegacyUpgradePage.ui9
-rw-r--r--launcher/ui/pages/instance/LogPage.ui2
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp45
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h1
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.ui2
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.cpp47
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.h2
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.ui18
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp3
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp273
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h79
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.cpp196
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.h67
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.ui90
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp11
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp276
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h79
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp180
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.h67
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui90
31 files changed, 1826 insertions, 214 deletions
diff --git a/launcher/ui/pages/global/PasteEEPage.cpp b/launcher/ui/pages/global/APIPage.cpp
index 4b375d9a..ad79e00c 100644
--- a/launcher/ui/pages/global/PasteEEPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -1,4 +1,4 @@
-/* Copyright 2013-2021 MultiMC Contributors
+/* Copyright 2013-2021 MultiMC & PolyMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,69 +13,55 @@
* limitations under the License.
*/
-#include "PasteEEPage.h"
-#include "ui_PasteEEPage.h"
+#include "APIPage.h"
+#include "ui_APIPage.h"
#include <QMessageBox>
#include <QFileDialog>
#include <QStandardPaths>
#include <QTabBar>
+#include <QVariant>
#include "settings/SettingsObject.h"
#include "tools/BaseProfiler.h"
#include "Application.h"
-PasteEEPage::PasteEEPage(QWidget *parent) :
+APIPage::APIPage(QWidget *parent) :
QWidget(parent),
- ui(new Ui::PasteEEPage)
+ ui(new Ui::APIPage)
{
+ static QRegularExpression validUrlRegExp("https?://.+");
ui->setupUi(this);
+ ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices));
ui->tabWidget->tabBar()->hide();\
- connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited);
loadSettings();
}
-PasteEEPage::~PasteEEPage()
+APIPage::~APIPage()
{
delete ui;
}
-void PasteEEPage::loadSettings()
+void APIPage::loadSettings()
{
auto s = APPLICATION->settings();
- QString keyToUse = s->get("PasteEEAPIKey").toString();
- if(keyToUse == "multimc")
- {
- ui->multimcButton->setChecked(true);
- }
- else
- {
- ui->customButton->setChecked(true);
- ui->customAPIkeyEdit->setText(keyToUse);
- }
+ QString pastebinURL = s->get("PastebinURL").toString();
+ ui->urlChoices->setCurrentText(pastebinURL);
+ QString msaClientID = s->get("MSAClientIDOverride").toString();
+ ui->msaClientID->setText(msaClientID);
}
-void PasteEEPage::applySettings()
+void APIPage::applySettings()
{
auto s = APPLICATION->settings();
-
- QString pasteKeyToUse;
- if (ui->customButton->isChecked())
- pasteKeyToUse = ui->customAPIkeyEdit->text();
- else
- {
- pasteKeyToUse = "multimc";
- }
- s->set("PasteEEAPIKey", pasteKeyToUse);
+ QString pastebinURL = ui->urlChoices->currentText();
+ s->set("PastebinURL", pastebinURL);
+ QString msaClientID = ui->msaClientID->text();
+ s->set("MSAClientIDOverride", msaClientID);
}
-bool PasteEEPage::apply()
+bool APIPage::apply()
{
applySettings();
return true;
}
-
-void PasteEEPage::textEdited(const QString& text)
-{
- ui->customButton->setChecked(true);
-}
diff --git a/launcher/ui/pages/global/PasteEEPage.h b/launcher/ui/pages/global/APIPage.h
index a1c7d434..9474ebbb 100644
--- a/launcher/ui/pages/global/PasteEEPage.h
+++ b/launcher/ui/pages/global/APIPage.h
@@ -21,32 +21,32 @@
#include <Application.h>
namespace Ui {
-class PasteEEPage;
+class APIPage;
}
-class PasteEEPage : public QWidget, public BasePage
+class APIPage : public QWidget, public BasePage
{
Q_OBJECT
public:
- explicit PasteEEPage(QWidget *parent = 0);
- ~PasteEEPage();
+ explicit APIPage(QWidget *parent = 0);
+ ~APIPage();
QString displayName() const override
{
- return tr("Log Upload");
+ return tr("APIs");
}
QIcon icon() const override
{
- return APPLICATION->getThemedIcon("log");
+ return APPLICATION->getThemedIcon("worlds");
}
QString id() const override
{
- return "log-upload";
+ return "apis";
}
QString helpPage() const override
{
- return "Log-Upload";
+ return "APIs";
}
virtual bool apply() override;
@@ -54,9 +54,7 @@ private:
void loadSettings();
void applySettings();
-private slots:
- void textEdited(const QString &text);
-
private:
- Ui::PasteEEPage *ui;
+ Ui::APIPage *ui;
};
+
diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui
new file mode 100644
index 00000000..28c53b79
--- /dev/null
+++ b/launcher/ui/pages/global/APIPage.ui
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>APIPage</class>
+ <widget class="QWidget" name="APIPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>491</width>
+ <height>474</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox_paste">
+ <property name="title">
+ <string>Pastebin URL</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: only input that starts with &lt;span style=&quot; font-weight:600;&quot;&gt;http://&lt;/span&gt; or &lt;span style=&quot; font-weight:600;&quot;&gt;https://&lt;/span&gt; will be accepted.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="scaledContents">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="urlChoices">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ <property name="insertPolicy">
+ <enum>QComboBox::NoInsert</enum>
+ </property>
+ <item>
+ <property name="text">
+ <string>https://0x0.st</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>https://paste.polymc.org</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Here you can choose from a predefined list of paste services, or input the URL of a different paste service of your choice, provided it supports the same protocol as 0x0.st, that is POST a file parameter to the URL and return a link in the response body.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_msa">
+ <property name="title">
+ <string>Microsoft Authentication</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Note: you probably don't need to set this if logging in via Microsoft Authentication already works.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="msaClientID">
+ <property name="placeholderText">
+ <string>(Default)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Enter a custom client ID for Microsoft Authentication here. </string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index 87fcac86..eb1ee8d3 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -24,6 +24,7 @@
#include "net/NetJob.h"
#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/OfflineLoginDialog.h"
#include "ui/dialogs/LoginDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -72,7 +73,10 @@ AccountListPage::AccountListPage(QWidget *parent)
updateButtonStates();
// Xbox authentication won't work without a client identifier, so disable the button if it is missing
- ui->actionAddMicrosoft->setVisible(BuildConfig.MSA_CLIENT_ID.size() != 0);
+ if (APPLICATION->getMSAClientID().isEmpty()) {
+ ui->actionAddMicrosoft->setVisible(false);
+ ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set."));
+ }
}
AccountListPage::~AccountListPage()
@@ -132,8 +136,8 @@ void AccountListPage::on_actionAddMicrosoft_triggered()
this,
tr("Microsoft Accounts not available"),
tr(
- "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n"
- "Please update both your operating system and MultiMC."
+ "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated PolyMC.\n\n"
+ "Please update both your operating system and PolyMC."
),
QMessageBox::Warning
)->exec();
@@ -153,6 +157,35 @@ void AccountListPage::on_actionAddMicrosoft_triggered()
}
}
+void AccountListPage::on_actionAddOffline_triggered()
+{
+ if (!m_accounts->anyAccountIsValid()) {
+ QMessageBox::warning(
+ this,
+ tr("Error"),
+ tr(
+ "You must add a Microsoft or Mojang account that owns Minecraft before you can add an offline account."
+ "<br><br>"
+ "If you have lost your account you can contact Microsoft for support."
+ )
+ );
+ return;
+ }
+
+ MinecraftAccountPtr account = OfflineLoginDialog::newAccount(
+ this,
+ tr("Please enter your desired username to add your offline account.")
+ );
+
+ if (account)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_accounts->setDefaultAccount(account);
+ }
+ }
+}
+
void AccountListPage::on_actionRemove_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h
index 1c65e708..841c3fd2 100644
--- a/launcher/ui/pages/global/AccountListPage.h
+++ b/launcher/ui/pages/global/AccountListPage.h
@@ -62,6 +62,7 @@ public:
public slots:
void on_actionAddMojang_triggered();
void on_actionAddMicrosoft_triggered();
+ void on_actionAddOffline_triggered();
void on_actionRemove_triggered();
void on_actionRefresh_triggered();
void on_actionSetDefault_triggered();
diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui
index 29738c02..d21a92e2 100644
--- a/launcher/ui/pages/global/AccountListPage.ui
+++ b/launcher/ui/pages/global/AccountListPage.ui
@@ -54,6 +54,7 @@
</attribute>
<addaction name="actionAddMicrosoft"/>
<addaction name="actionAddMojang"/>
+ <addaction name="actionAddOffline"/>
<addaction name="actionRefresh"/>
<addaction name="actionRemove"/>
<addaction name="actionSetDefault"/>
@@ -103,6 +104,11 @@
<string>Add Microsoft</string>
</property>
</action>
+ <action name="actionAddOffline">
+ <property name="text">
+ <string>Add Offline</string>
+ </property>
+ </action>
<action name="actionRefresh">
<property name="text">
<string>Refresh</string>
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index 4d4d4e89..0ffe8050 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -246,32 +246,31 @@ void LauncherPage::applySettings()
//FIXME: make generic
switch (ui->themeComboBox->currentIndex())
{
- case 1:
+ case 0:
s->set("IconTheme", "pe_dark");
break;
- case 2:
+ case 1:
s->set("IconTheme", "pe_light");
break;
- case 3:
+ case 2:
s->set("IconTheme", "pe_blue");
break;
- case 4:
+ case 3:
s->set("IconTheme", "pe_colored");
break;
- case 5:
+ case 4:
s->set("IconTheme", "OSX");
break;
- case 6:
+ case 5:
s->set("IconTheme", "iOS");
break;
- case 7:
+ case 6:
s->set("IconTheme", "flat");
break;
- case 8:
+ case 7:
s->set("IconTheme", "custom");
break;
- case 0:
- default:
+ case 8:
s->set("IconTheme", "multimc");
break;
}
@@ -327,40 +326,40 @@ void LauncherPage::loadSettings()
auto theme = s->get("IconTheme").toString();
if (theme == "pe_dark")
{
- ui->themeComboBox->setCurrentIndex(1);
+ ui->themeComboBox->setCurrentIndex(0);
}
else if (theme == "pe_light")
{
- ui->themeComboBox->setCurrentIndex(2);
+ ui->themeComboBox->setCurrentIndex(1);
}
else if (theme == "pe_blue")
{
- ui->themeComboBox->setCurrentIndex(3);
+ ui->themeComboBox->setCurrentIndex(2);
}
else if (theme == "pe_colored")
{
- ui->themeComboBox->setCurrentIndex(4);
+ ui->themeComboBox->setCurrentIndex(3);
}
else if (theme == "OSX")
{
- ui->themeComboBox->setCurrentIndex(5);
+ ui->themeComboBox->setCurrentIndex(4);
}
else if (theme == "iOS")
{
- ui->themeComboBox->setCurrentIndex(6);
+ ui->themeComboBox->setCurrentIndex(5);
}
else if (theme == "flat")
{
+ ui->themeComboBox->setCurrentIndex(6);
+ }
+ else if (theme == "multimc")
+ {
ui->themeComboBox->setCurrentIndex(7);
}
else if (theme == "custom")
{
ui->themeComboBox->setCurrentIndex(8);
}
- else
- {
- ui->themeComboBox->setCurrentIndex(0);
- }
{
auto currentTheme = s->get("ApplicationTheme").toString();
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index 2b3729bc..47fed873 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -264,11 +264,6 @@
</property>
<item>
<property name="text">
- <string>Default</string>
- </property>
- </item>
- <item>
- <property name="text">
<string>Simple (Dark Icons)</string>
</property>
</item>
@@ -307,6 +302,11 @@
<string>Custom</string>
</property>
</item>
+ <item>
+ <property name="text">
+ <string>MultiMC</string>
+ </property>
+ </item>
</widget>
</item>
<item row="1" column="1">
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index c763f8ac..5470a586 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -71,6 +71,9 @@ void MinecraftPage::applySettings()
s->set("ShowGameTime", ui->showGameTime->isChecked());
s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked());
s->set("RecordGameTime", ui->recordGameTime->isChecked());
+
+ // Miscellaneous
+ s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked());
}
void MinecraftPage::loadSettings()
@@ -88,4 +91,6 @@ void MinecraftPage::loadSettings()
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
+
+ ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool());
}
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
index 857b8cfb..a28b1f59 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -165,6 +165,25 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Miscellaneous</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QCheckBox" name="closeAfterLaunchCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;PolyMC will automatically reopen when the game crashes or exits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Close PolyMC after game window opens</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacerMinecraft">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -184,7 +203,6 @@
</layout>
</widget>
<tabstops>
- <tabstop>tabWidget</tabstop>
<tabstop>maximizedCheckBox</tabstop>
<tabstop>windowWidthSpinBox</tabstop>
<tabstop>windowHeightSpinBox</tabstop>
diff --git a/launcher/ui/pages/global/PasteEEPage.ui b/launcher/ui/pages/global/PasteEEPage.ui
deleted file mode 100644
index 10883781..00000000
--- a/launcher/ui/pages/global/PasteEEPage.ui
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>PasteEEPage</class>
- <widget class="QWidget" name="PasteEEPage">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>491</width>
- <height>474</height>
- </rect>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="tab">
- <attribute name="title">
- <string notr="true">Tab 1</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QGroupBox" name="groupBox_2">
- <property name="title">
- <string>paste.ee API key</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_10">
- <item>
- <widget class="QRadioButton" name="multimcButton">
- <property name="text">
- <string>MultiMC key - 12MB &amp;upload limit</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">pasteButtonGroup</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QRadioButton" name="customButton">
- <property name="text">
- <string>&amp;Your own key - 12MB upload limit:</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">pasteButtonGroup</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QLineEdit" name="customAPIkeyEdit">
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- <property name="placeholderText">
- <string>Paste your API key here!</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="Line" name="line">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label">
- <property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; is used by MultiMC for log uploads. If you have a &lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; account, you can add your API key here and have your uploaded logs paired with your account.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="textFormat">
- <enum>Qt::RichText</enum>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- <property name="openExternalLinks">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>216</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- </widget>
- </item>
- </layout>
- </widget>
- <tabstops>
- <tabstop>tabWidget</tabstop>
- <tabstop>multimcButton</tabstop>
- <tabstop>customButton</tabstop>
- <tabstop>customAPIkeyEdit</tabstop>
- </tabstops>
- <resources/>
- <connections/>
- <buttongroups>
- <buttongroup name="pasteButtonGroup"/>
- </buttongroups>
-</ui>
diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.ui b/launcher/ui/pages/instance/LegacyUpgradePage.ui
index 085919e3..b22c03e5 100644
--- a/launcher/ui/pages/instance/LegacyUpgradePage.ui
+++ b/launcher/ui/pages/instance/LegacyUpgradePage.ui
@@ -26,7 +26,14 @@
<item>
<widget class="QTextBrowser" name="textBrowser">
<property name="html">
- <string>&lt;html&gt;&lt;body&gt;&lt;h1&gt;Upgrade is required&lt;/h1&gt;&lt;p&gt;MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.&lt;/p&gt;&lt;p&gt;The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.&lt;/p&gt;&lt;p&gt;Please report any issues on our &lt;a href=&quot;https://github.com/MultiMC/Launcher/issues&quot;&gt;github issues page&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There is also a &lt;a href=&quot;https://discord.gg/GtPmv93&quot;&gt;discord channel for testing here&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;h1 style=&quot; margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:xx-large; font-weight:600;&quot;&gt;Upgrade is required&lt;/span&gt;&lt;/h1&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;PolyMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.&lt;/p&gt;
+&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Please report any issues on our &lt;a href=&quot;https://github.com/PolyMC/PolyMC/issues&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#3584e4;&quot;&gt;github issues page&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui
index ccfc1551..31bb368c 100644
--- a/launcher/ui/pages/instance/LogPage.ui
+++ b/launcher/ui/pages/instance/LogPage.ui
@@ -100,7 +100,7 @@
<item>
<widget class="QPushButton" name="btnPaste">
<property name="toolTip">
- <string>Upload the log to paste.ee - it will stay online for a month</string>
+ <string>Upload the log to the paste service configured in preferences</string>
</property>
<property name="text">
<string>Upload</string>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index e63b1434..494d32f0 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -26,6 +26,7 @@
#include "Application.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/GuiUtil.h"
#include "DesktopServices.h"
@@ -36,6 +37,7 @@
#include "minecraft/PackProfile.h"
#include "Version.h"
+#include "ui/dialogs/ProgressDialog.h"
namespace {
// FIXME: wasteful
@@ -141,6 +143,11 @@ ModFolderPage::ModFolderPage(
ui(new Ui::ModFolderPage)
{
ui->setupUi(this);
+ if(id == "mods") {
+ auto act = new QAction(tr("Install Mods"), this);
+ ui->actionsToolbar->insertActionBefore(ui->actionView_configs,act);
+ connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered);
+ }
ui->actionsToolbar->insertSpacer(ui->actionView_configs);
m_inst = inst;
@@ -342,6 +349,44 @@ void ModFolderPage::on_actionRemove_triggered()
m_mods->deleteMods(selection.indexes());
}
+void ModFolderPage::on_actionInstall_mods_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ if(m_inst->typeName() != "Minecraft"){
+ return; //this is a null instance or a legacy instance
+ }
+ bool hasFabric = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ bool hasForge = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.minecraftforge").isEmpty();
+ if (!hasFabric && !hasForge) {
+ QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!"));
+ return;
+ }
+ ModDownloadDialog mdownload(m_mods, this, m_inst);
+ if(mdownload.exec()) {
+ ModDownloadTask *task = mdownload.getTask();
+ if (task) {
+ connect(task, &Task::failed, [this, task](QString reason) {
+ task->deleteLater();
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
+ });
+ connect(task, &Task::succeeded, [this, task]() {
+ QStringList warnings = task->warnings();
+ if (warnings.count()) {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'),
+ QMessageBox::Warning)->show();
+ }
+ task->deleteLater();
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(task);
+ m_mods->update();
+ }
+ }
+}
+
void ModFolderPage::on_actionView_configs_triggered()
{
DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 8ef7559b..fbda3cd8 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -102,6 +102,7 @@ slots:
void on_actionRemove_triggered();
void on_actionEnable_triggered();
void on_actionDisable_triggered();
+ void on_actionInstall_mods_triggered();
void on_actionView_Folder_triggered();
void on_actionView_configs_triggered();
void ShowContextMenu(const QPoint &pos);
diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui
index 56ff3b62..77f3e647 100644
--- a/launcher/ui/pages/instance/OtherLogsPage.ui
+++ b/launcher/ui/pages/instance/OtherLogsPage.ui
@@ -84,7 +84,7 @@
<item row="3" column="2">
<widget class="QPushButton" name="btnPaste">
<property name="toolTip">
- <string>Upload the log to paste.ee - it will stay online for a month</string>
+ <string>Upload the log to the paste service configured in preferences.</string>
</property>
<property name="text">
<string>Upload</string>
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp
index f568ef0d..4011d88c 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.cpp
+++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp
@@ -250,6 +250,12 @@ bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt)
return QWidget::eventFilter(obj, evt);
}
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
+
+ if (keyEvent->matches(QKeySequence::Copy)) {
+ on_actionCopy_File_s_triggered();
+ return true;
+ }
+
switch (keyEvent->key())
{
case Qt::Key_Delete:
@@ -272,6 +278,11 @@ ScreenshotsPage::~ScreenshotsPage()
void ScreenshotsPage::ShowContextMenu(const QPoint& pos)
{
auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+
+ if (ui->listView->selectionModel()->selectedRows().size() > 1) {
+ menu->removeAction( ui->actionCopy_Image );
+ }
+
menu->exec(ui->listView->mapToGlobal(pos));
delete menu;
}
@@ -377,6 +388,42 @@ void ScreenshotsPage::on_actionUpload_triggered()
m_uploadActive = false;
}
+void ScreenshotsPage::on_actionCopy_Image_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedRows();
+ if(selection.size() < 1)
+ {
+ return;
+ }
+
+ // You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied.
+ auto item = selection[0];
+ auto info = m_model->fileInfo(item);
+ QImage image(info.absoluteFilePath());
+ Q_ASSERT(!image.isNull());
+ QApplication::clipboard()->setImage(image, QClipboard::Clipboard);
+}
+
+void ScreenshotsPage::on_actionCopy_File_s_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedRows();
+ if(selection.size() < 1)
+ {
+ // Don't do anything so we don't empty the users clipboard
+ return;
+ }
+
+ QString buf = "";
+ for (auto item : selection)
+ {
+ auto info = m_model->fileInfo(item);
+ buf += "file:///" + info.absoluteFilePath() + "\r\n";
+ }
+ QMimeData* mimeData = new QMimeData();
+ mimeData->setData("text/uri-list", buf.toLocal8Bit());
+ QApplication::clipboard()->setMimeData(mimeData);
+}
+
void ScreenshotsPage::on_actionDelete_triggered()
{
auto mbox = CustomMessageBox::selectable(
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h
index d2f44837..2a1fdeee 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.h
+++ b/launcher/ui/pages/instance/ScreenshotsPage.h
@@ -73,6 +73,8 @@ protected:
private slots:
void on_actionUpload_triggered();
+ void on_actionCopy_Image_triggered();
+ void on_actionCopy_File_s_triggered();
void on_actionDelete_triggered();
void on_actionRename_triggered();
void on_actionView_Folder_triggered();
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui
index ec461087..2e2227a2 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.ui
+++ b/launcher/ui/pages/instance/ScreenshotsPage.ui
@@ -50,6 +50,8 @@
<bool>false</bool>
</attribute>
<addaction name="actionUpload"/>
+ <addaction name="actionCopy_Image"/>
+ <addaction name="actionCopy_File_s"/>
<addaction name="actionDelete"/>
<addaction name="actionRename"/>
<addaction name="actionView_Folder"/>
@@ -74,6 +76,22 @@
<string>View Folder</string>
</property>
</action>
+ <action name="actionCopy_Image">
+ <property name="text">
+ <string>Copy Image</string>
+ </property>
+ <property name="toolTip">
+ <string>Copy Image</string>
+ </property>
+ </action>
+ <action name="actionCopy_File_s">
+ <property name="text">
+ <string>Copy File(s)</string>
+ </property>
+ <property name="toolTip">
+ <string>Copy File(s)</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 6e57909b..0fa5f68d 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -395,7 +395,7 @@ void VersionPage::on_actionDownload_All_triggered()
{
CustomMessageBox::selectable(
this, tr("Error"),
- tr("MultiMC cannot download Minecraft or update instances unless you have at least "
+ tr("PolyMC cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Mojang or Minecraft account."),
QMessageBox::Warning)->show();
return;
@@ -635,4 +635,3 @@ void VersionPage::onFilterTextChanged(const QString &newContents)
}
#include "VersionPage.moc"
-
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
new file mode 100644
index 00000000..2cf83261
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp
@@ -0,0 +1,273 @@
+#include "FlameModModel.h"
+#include "Application.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "FlameModPage.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+
+
+namespace FlameMod {
+
+ListModel::ListModel(FlameModPage *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);
+ }
+
+ IndexedPack 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 = APPLICATION->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 = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ auto job = new NetJob(QString("Flame Icon Download %1").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);
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("FlameMods", 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();
+}
+const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"};
+
+void ListModel::performPaginatedSearch()
+{
+
+ QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
+ bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ auto netJob = new NetJob("Flame::Search", APPLICATION->network());
+ auto searchUrl = QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "gameId=432&"
+ "categoryId=0&"
+ "sectionId=6&"
+
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sort=%3&"
+ "%4"
+ "gameVersion=%5"
+ )
+ .arg(nextSearchOffset)
+ .arg(currentSearchTerm)
+ .arg(sorts[currentSort])
+ .arg(hasFabric ? "modLoaderType=4&" : "")
+ .arg(mcVersion);
+
+ 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, const int sort)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
+ return;
+ }
+ currentSearchTerm = term;
+ currentSort = sort;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void 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 Flame at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<FlameMod::IndexedPack> newList;
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ FlameMod::IndexedPack pack;
+ try
+ {
+ FlameMod::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading mod from Flame: " << e.cause();
+ continue;
+ }
+ }
+ if(packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h
new file mode 100644
index 00000000..0c1cb95e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#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 <modplatform/flame/FlamePackIndex.h>
+#include "modplatform/flame/FlameModIndex.h"
+#include "BaseInstance.h"
+#include "FlameModPage.h"
+
+namespace FlameMod {
+
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(FlameModPage *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, const int sort);
+
+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<IndexedPack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
new file mode 100644
index 00000000..a816c681
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
@@ -0,0 +1,196 @@
+#include "FlameModPage.h"
+#include "ui_FlameModPage.h"
+
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "Json.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+#include "InstanceImportTask.h"
+#include "FlameModModel.h"
+#include "ModDownloadTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance)
+ : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &FlameModPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ listModel = new FlameMod::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ // index is used to set the sorting with the flame api
+ ui->sortByBox->addItem(tr("Sort by Featured"));
+ ui->sortByBox->addItem(tr("Sort by Popularity"));
+ ui->sortByBox->addItem(tr("Sort by last updated"));
+ ui->sortByBox->addItem(tr("Sort by Name"));
+ ui->sortByBox->addItem(tr("Sort by Author"));
+ ui->sortByBox->addItem(tr("Sort by Downloads"));
+
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
+}
+
+FlameModPage::~FlameModPage()
+{
+ delete ui;
+}
+
+bool FlameModPage::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 FlameModPage::shouldDisplay() const
+{
+ return true;
+}
+
+void FlameModPage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void FlameModPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedMod();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>();
+ 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 = [](FlameMod::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 += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+ text += "<br><br>";
+
+ ui->packDescription->setHtml(text + current.description);
+
+ if (!current.versionsLoaded)
+ {
+ qDebug() << "Loading flame mod versions";
+ auto netJob = new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network());
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ int addonId = current.addonId;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get()));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob]
+ {
+ netJob->deleteLater();
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ QJsonArray arr = doc.array();
+ try
+ {
+ FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading Flame mod version: " << e.cause();
+ }
+ auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
+ QString mcVersion = packProfile->getComponentVersion("net.minecraft");
+ QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge";
+ for(int i = 0; i < current.versions.size(); i++) {
+ auto version = current.versions[i];
+ if(!version.mcVersion.contains(mcVersion)){
+ continue;
+ }
+ ui->versionSelectionBox->addItem(version.version, QVariant(i));
+ }
+ if(ui->versionSelectionBox->count() == 0){
+ ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1));
+ }
+
+ suggestCurrent();
+ });
+ netJob->start();
+ }
+ else
+ {
+ for(int i = 0; i < current.versions.size(); i++) {
+ ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
+ }
+ if(ui->versionSelectionBox->count() == 0){
+ ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1));
+ }
+ suggestCurrent();
+ }
+}
+
+void FlameModPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion == -1)
+ {
+ dialog->setSuggestedMod();
+ return;
+ }
+
+ auto version = current.versions[selectedVersion];
+ dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods));
+}
+
+void FlameModPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = -1;
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toInt();
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h
new file mode 100644
index 00000000..8fa3248a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+#include "modplatform/flame/FlameModIndex.h"
+
+namespace Ui
+{
+class FlameModPage;
+}
+
+class ModDownloadDialog;
+
+namespace FlameMod {
+ class ListModel;
+}
+
+class FlameModPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance);
+ virtual ~FlameModPage();
+ virtual QString displayName() const override
+ {
+ return tr("CurseForge");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("flame");
+ }
+ virtual QString id() const override
+ {
+ return "curseforge";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Flame-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+ BaseInstance *m_instance;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FlameModPage *ui = nullptr;
+ ModDownloadDialog* dialog = nullptr;
+ FlameMod::ListModel* listModel = nullptr;
+ FlameMod::IndexedPack current;
+
+ int selectedVersion = -1;
+};
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui
new file mode 100644
index 00000000..7da0bb4a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FlameModPage</class>
+ <widget class="QWidget" name="FlameModPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>837</width>
+ <height>685</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QListView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <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 row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </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>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index 891676cf..fe163cae 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -6,9 +6,6 @@
#include <Version.h>
#include <QtMath>
-#include <QLabel>
-
-#include <RWStorage.h>
namespace Flame {
@@ -100,12 +97,13 @@ void ListModel::requestLogo(QString logo, QString url)
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
- NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network());
+ auto job = new NetJob(QString("Flame Icon Download %1").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]
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job]
{
+ job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo))
{
@@ -113,8 +111,9 @@ void ListModel::requestLogo(QString logo, QString url)
}
});
- QObject::connect(job, &NetJob::failed, this, [this, logo]
+ QObject::connect(job, &NetJob::failed, this, [this, logo, job]
{
+ job->deleteLater();
emit logoFailed(logo);
});
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
new file mode 100644
index 00000000..5a18830a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -0,0 +1,276 @@
+#include "ModrinthModel.h"
+#include "Application.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ModrinthPage.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QMessageBox>
+
+
+namespace Modrinth {
+
+ListModel::ListModel(ModrinthPage *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);
+ }
+
+ IndexedPack 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 = APPLICATION->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 = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ auto job = new NetJob(QString("Modrinth Icon Download %1").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);
+}
+
+void ListModel::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);
+ }
+}
+
+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();
+}
+const char* sorts[5]{"relevance","downloads","follows","updated","newest"};
+
+void ListModel::performPaginatedSearch()
+{
+
+ QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft");
+ bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty();
+ auto netJob = new NetJob("Modrinth::Search", APPLICATION->network());
+ auto searchUrl = QString(
+ "https://api.modrinth.com/v2/search?"
+ "offset=%1&"
+ "limit=25&"
+ "query=%2&"
+ "index=%3&"
+ "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]"
+ )
+ .arg(nextSearchOffset)
+ .arg(currentSearchTerm)
+ .arg(sorts[currentSort])
+ .arg(hasFabric ? "fabric" : "forge")
+ .arg(mcVersion);
+
+ 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, const int sort)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
+ return;
+ }
+ currentSearchTerm = term;
+ currentSort = sort;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void Modrinth::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 Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Modrinth::IndexedPack> newList;
+ auto packs = doc.object().value("hits").toArray();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ Modrinth::IndexedPack pack;
+ try
+ {
+ Modrinth::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading mod from Modrinth: " << e.cause();
+ continue;
+ }
+ }
+ if(packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Modrinth::ListModel::searchRequestFailed(QString reason)
+{
+ if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){
+ //409 Gone, notify user to update
+ QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!"));
+ //self-destruct
+ ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject();
+ }
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
new file mode 100644
index 00000000..53f1f134
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#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 <modplatform/flame/FlamePackIndex.h>
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+#include "BaseInstance.h"
+#include "ModrinthPage.h"
+
+namespace Modrinth {
+
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(ModrinthPage *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, const int sort);
+
+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<IndexedPack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
new file mode 100644
index 00000000..c5a54c29
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -0,0 +1,180 @@
+#include "ModrinthPage.h"
+#include "ui_ModrinthPage.h"
+
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "Json.h"
+#include "ui/dialogs/ModDownloadDialog.h"
+#include "InstanceImportTask.h"
+#include "ModrinthModel.h"
+#include "ModDownloadTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance)
+ : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ listModel = new Modrinth::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ 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 Relevence"));
+ 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"));
+
+ 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);
+}
+
+ModrinthPage::~ModrinthPage()
+{
+ delete ui;
+}
+
+bool ModrinthPage::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 ModrinthPage::shouldDisplay() const
+{
+ return true;
+}
+
+void ModrinthPage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void ModrinthPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedMod();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>();
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ text += "<br>"+ tr(" by ") + "<a href=\""+current.author.url+"\">"+current.author.name+"</a><br><br>";
+ ui->packDescription->setHtml(text + current.description);
+
+ if (!current.versionsLoaded)
+ {
+ qDebug() << "Loading Modrinth mod versions";
+ auto netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network());
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ QString addonId = current.addonId;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response.get()));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob]
+ {
+ netJob->deleteLater();
+ 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;
+ }
+ QJsonArray arr = doc.array();
+ try
+ {
+ Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading Modrinth mod version: " << e.cause();
+ }
+ auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile();
+ QString mcVersion = packProfile->getComponentVersion("net.minecraft");
+ QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge";
+ for(int i = 0; i < current.versions.size(); i++) {
+ auto version = current.versions[i];
+ if(!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)){
+ continue;
+ }
+ ui->versionSelectionBox->addItem(version.version, QVariant(i));
+ }
+ if(ui->versionSelectionBox->count() == 0){
+ ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1));
+ }
+
+ suggestCurrent();
+ });
+ netJob->start();
+ }
+ else
+ {
+ for(int i = 0; i < current.versions.size(); i++) {
+ ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
+ }
+ if(ui->versionSelectionBox->count() == 0){
+ ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1));
+ }
+ suggestCurrent();
+ }
+}
+
+void ModrinthPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion == -1)
+ {
+ dialog->setSuggestedMod();
+ return;
+ }
+ auto version = current.versions[selectedVersion];
+ dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods));
+}
+
+void ModrinthPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = -1;
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toInt();
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
new file mode 100644
index 00000000..3c517069
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+
+namespace Ui
+{
+class ModrinthPage;
+}
+
+class ModDownloadDialog;
+
+namespace Modrinth {
+ class ListModel;
+}
+
+class ModrinthPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance);
+ virtual ~ModrinthPage();
+ virtual QString displayName() const override
+ {
+ return tr("Modrinth");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("modrinth");
+ }
+ virtual QString id() const override
+ {
+ return "modrinth";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Modrinth-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+ BaseInstance *m_instance;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::ModrinthPage *ui = nullptr;
+ ModDownloadDialog* dialog = nullptr;
+ Modrinth::ListModel* listModel = nullptr;
+ Modrinth::IndexedPack current;
+
+ int selectedVersion = -1;
+};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
new file mode 100644
index 00000000..6d183de5
--- /dev/null
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
@@ -0,0 +1,90 @@
+<?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="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QListView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <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 row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </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>