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.cpp103
-rw-r--r--launcher/ui/pages/global/APIPage.h5
-rw-r--r--launcher/ui/pages/global/APIPage.ui207
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp18
-rw-r--r--launcher/ui/pages/global/CustomCommandsPage.cpp2
-rw-r--r--launcher/ui/pages/global/JavaPage.cpp11
-rw-r--r--launcher/ui/pages/global/JavaPage.ui69
-rw-r--r--launcher/ui/pages/global/LanguagePage.cpp1
-rw-r--r--launcher/ui/pages/global/LanguagePage.h1
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp12
-rw-r--r--launcher/ui/pages/global/LauncherPage.h1
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui75
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp13
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui60
-rw-r--r--launcher/ui/pages/global/ProxyPage.cpp16
-rw-r--r--launcher/ui/pages/global/ProxyPage.h9
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp297
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h73
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui (renamed from launcher/ui/pages/instance/ModFolderPage.ui)49
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp34
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui68
-rw-r--r--launcher/ui/pages/instance/LogPage.cpp3
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp387
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h112
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h20
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.cpp6
-rw-r--r--launcher/ui/pages/instance/ServersPage.cpp13
-rw-r--r--launcher/ui/pages/instance/ServersPage.h3
-rw-r--r--launcher/ui/pages/instance/ShaderPackPage.h16
-rw-r--r--launcher/ui/pages/instance/TexturePackPage.h18
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp2
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp1
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.cpp17
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp87
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h4
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp128
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h7
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp37
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h6
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp20
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.h3
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.h4
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp16
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.h1
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp95
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.h2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.ui192
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.cpp12
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp37
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.cpp5
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.ui6
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp5
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp6
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h4
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp27
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.h1
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp65
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui10
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.cpp5
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.ui6
62 files changed, 1694 insertions, 826 deletions
diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp
index 287eb74f..b889e6f7 100644
--- a/launcher/ui/pages/global/APIPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -3,6 +3,7 @@
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (c) 2022 Lenny McLennington <lenny@sneed.church>
*
* 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
@@ -46,16 +47,43 @@
#include "settings/SettingsObject.h"
#include "tools/BaseProfiler.h"
#include "Application.h"
+#include "net/PasteUpload.h"
+#include "BuildConfig.h"
APIPage::APIPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::APIPage)
{
+ // This is here so you can reorder the entries in the combobox without messing stuff up
+ int comboBoxEntries[] = {
+ PasteUpload::PasteType::Mclogs,
+ PasteUpload::PasteType::NullPointer,
+ PasteUpload::PasteType::PasteGG,
+ PasteUpload::PasteType::Hastebin
+ };
+
static QRegularExpression validUrlRegExp("https?://.+");
+
ui->setupUi(this);
- ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices));
- ui->tabWidget->tabBar()->hide();\
+
+ for (auto pasteType : comboBoxEntries) {
+ ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType);
+ }
+
+ void (QComboBox::*currentIndexChangedSignal)(int) (&QComboBox::currentIndexChanged);
+ connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder);
+ // This function needs to be called even when the ComboBox's index is still in its default state.
+ updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex());
+ ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
+
+ ui->metaURL->setPlaceholderText(BuildConfig.META_URL);
+ ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT);
+
loadSettings();
+
+ resetBaseURLNote();
+ connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote);
+ connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote);
}
APIPage::~APIPage()
@@ -63,22 +91,85 @@ APIPage::~APIPage()
delete ui;
}
+void APIPage::resetBaseURLNote()
+{
+ ui->baseURLNote->hide();
+ baseURLPasteType = ui->pasteTypeComboBox->currentIndex();
+}
+
+void APIPage::updateBaseURLNote(int index)
+{
+ if (baseURLPasteType == index)
+ {
+ ui->baseURLNote->hide();
+ }
+ else if (!ui->baseURLEntry->text().isEmpty())
+ {
+ ui->baseURLNote->show();
+ }
+}
+
+void APIPage::updateBaseURLPlaceholder(int index)
+{
+ int pasteType = ui->pasteTypeComboBox->itemData(index).toInt();
+ QString pasteDefaultURL = PasteUpload::PasteTypes.at(pasteType).defaultBase;
+ ui->baseURLEntry->setPlaceholderText(pasteDefaultURL);
+}
+
void APIPage::loadSettings()
{
auto s = APPLICATION->settings();
- QString pastebinURL = s->get("PastebinURL").toString();
- ui->urlChoices->setCurrentText(pastebinURL);
+
+ int pasteType = s->get("PastebinType").toInt();
+ QString pastebinURL = s->get("PastebinCustomAPIBase").toString();
+
+ ui->baseURLEntry->setText(pastebinURL);
+ int pasteTypeIndex = ui->pasteTypeComboBox->findData(pasteType);
+ if (pasteTypeIndex == -1)
+ {
+ pasteTypeIndex = ui->pasteTypeComboBox->findData(PasteUpload::PasteType::Mclogs);
+ ui->baseURLEntry->clear();
+ }
+
+ ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex);
+
QString msaClientID = s->get("MSAClientIDOverride").toString();
ui->msaClientID->setText(msaClientID);
+ QString metaURL = s->get("MetaURLOverride").toString();
+ ui->metaURL->setText(metaURL);
+ QString curseKey = s->get("CFKeyOverride").toString();
+ ui->curseKey->setText(curseKey);
+ QString customUserAgent = s->get("UserAgentOverride").toString();
+ ui->userAgentLineEdit->setText(customUserAgent);
}
void APIPage::applySettings()
{
auto s = APPLICATION->settings();
- QString pastebinURL = ui->urlChoices->currentText();
- s->set("PastebinURL", pastebinURL);
+
+ s->set("PastebinType", ui->pasteTypeComboBox->currentData().toInt());
+ s->set("PastebinCustomAPIBase", ui->baseURLEntry->text());
+
QString msaClientID = ui->msaClientID->text();
s->set("MSAClientIDOverride", msaClientID);
+ QUrl metaURL = ui->metaURL->text();
+ // Add required trailing slash
+ if (!metaURL.isEmpty() && !metaURL.path().endsWith('/'))
+ {
+ QString path = metaURL.path();
+ path.append('/');
+ metaURL.setPath(path);
+ }
+ // Don't allow HTTP, since meta is basically RCE with all the jar files.
+ if(!metaURL.isEmpty() && metaURL.scheme() == "http")
+ {
+ metaURL.setScheme("https");
+ }
+
+ s->set("MetaURLOverride", metaURL);
+ QString curseKey = ui->curseKey->text();
+ s->set("CFKeyOverride", curseKey);
+ s->set("UserAgentOverride", ui->userAgentLineEdit->text());
}
bool APIPage::apply()
diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h
index 20356009..17e62ae7 100644
--- a/launcher/ui/pages/global/APIPage.h
+++ b/launcher/ui/pages/global/APIPage.h
@@ -3,6 +3,7 @@
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (c) 2022 Lenny McLennington <lenny@sneed.church>
*
* 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
@@ -73,6 +74,10 @@ public:
void retranslate() override;
private:
+ int baseURLPasteType;
+ void resetBaseURLNote();
+ void updateBaseURLNote(int index);
+ void updateBaseURLPlaceholder(int index);
void loadSettings();
void applySettings();
diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui
index acde9aef..5327771c 100644
--- a/launcher/ui/pages/global/APIPage.ui
+++ b/launcher/ui/pages/global/APIPage.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>491</width>
- <height>474</height>
+ <width>800</width>
+ <height>600</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -30,56 +30,91 @@
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
- <string notr="true">Tab 1</string>
+ <string notr="true">Services</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_paste">
<property name="title">
- <string>&amp;Pastebin URL</string>
+ <string>&amp;Pastebin Service</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
- <widget class="Line" name="line">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <widget class="QLabel" name="pasteServiceTypeLabel">
+ <property name="text">
+ <string>Paste Service &amp;Type</string>
+ </property>
+ <property name="buddy">
+ <cstring>pasteTypeComboBox</cstring>
</property>
</widget>
</item>
<item>
- <widget class="QLabel" name="label_2">
- <property name="font">
- <font>
- <pointsize>10</pointsize>
- </font>
+ <widget class="QComboBox" name="pasteTypeComboBox"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="baseURLLabel">
+ <property name="text">
+ <string>Base &amp;URL</string>
+ </property>
+ <property name="buddy">
+ <cstring>baseURLEntry</cstring>
</property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="baseURLEntry">
+ <property name="placeholderText">
+ <string/>
+ </property>
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="baseURLNote">
<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>
+ <string>Note: you probably want to change or clear the Base URL after changing the paste service type.</string>
</property>
- <property name="scaledContents">
- <bool>false</bool>
+ <property name="wordWrap">
+ <bool>true</bool>
</property>
</widget>
</item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_meta">
+ <property name="title">
+ <string>Meta&amp;data Server</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
<item>
- <widget class="QComboBox" name="urlChoices">
- <property name="editable">
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>You can set this to a third-party metadata server to use patched libraries or other hacks.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
<bool>true</bool>
</property>
- <property name="insertPolicy">
- <enum>QComboBox::NoInsert</enum>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="metaURL">
+ <property name="placeholderText">
+ <string/>
</property>
- <item>
- <property name="text">
- <string notr="true">https://0x0.st</string>
- </property>
- </item>
</widget>
</item>
<item>
- <widget class="QLabel" name="label">
+ <widget class="QLabel" name="label_6">
<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>
+ <string>Enter a custom URL for meta here.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
@@ -96,19 +131,32 @@
</widget>
</item>
<item>
+ <spacer name="verticalSpacer_2">
+ <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 class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>API Keys</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
<widget class="QGroupBox" name="groupBox_msa">
<property name="title">
<string>&amp;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>
@@ -148,6 +196,51 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="groupBox_curse">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>&amp;CurseForge Core API</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_8">
+ <property name="text">
+ <string>Note: you probably don't need to set this if CurseForge already works.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>Enter a custom API Key for CurseForge 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>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="curseKey">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="placeholderText">
+ <string>(Default)</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -162,13 +255,55 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="tab_3">
+ <attribute name="title">
+ <string>Miscellaneous</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QGroupBox" name="groupBox_ua">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>User Agent</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QLineEdit" name="userAgentLineEdit"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="userAgentLabel">
+ <property name="text">
+ <string>Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher.</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <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 6e1e2183..a608771e 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -73,9 +73,11 @@ AccountListPage::AccountListPage(QWidget *parent)
m_accounts = APPLICATION->accounts();
ui->listView->setModel(m_accounts.get());
- ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
- ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
- ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::MigrationColumn, QHeaderView::ResizeToContents);
+ ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents);
+ ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents);
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
// Expand the account column
@@ -253,19 +255,21 @@ void AccountListPage::updateButtonStates()
{
// If there is no selection, disable buttons that require something selected.
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
- bool hasSelection = selection.size() > 0;
+ bool hasSelection = !selection.empty();
bool accountIsReady = false;
+ bool accountIsOnline = false;
if (hasSelection)
{
QModelIndex selected = selection.first();
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
accountIsReady = !account->isActive();
+ accountIsOnline = !account->isOffline();
}
ui->actionRemove->setEnabled(accountIsReady);
ui->actionSetDefault->setEnabled(accountIsReady);
- ui->actionUploadSkin->setEnabled(accountIsReady);
- ui->actionDeleteSkin->setEnabled(accountIsReady);
- ui->actionRefresh->setEnabled(accountIsReady);
+ ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline);
+ ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline);
+ ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline);
if(m_accounts->defaultAccount().get() == nullptr) {
ui->actionNoDefault->setEnabled(false);
diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp
index 436d766e..df1420ca 100644
--- a/launcher/ui/pages/global/CustomCommandsPage.cpp
+++ b/launcher/ui/pages/global/CustomCommandsPage.cpp
@@ -2,7 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * 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
diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp
index b5e8de6c..2cee15bf 100644
--- a/launcher/ui/pages/global/JavaPage.cpp
+++ b/launcher/ui/pages/global/JavaPage.cpp
@@ -95,7 +95,7 @@ void JavaPage::applySettings()
// Java Settings
s->set("JavaPath", ui->javaPathTextBox->text());
- s->set("JvmArgs", ui->jvmArgsTextBox->text());
+ s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked());
s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked());
JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget());
@@ -120,13 +120,18 @@ void JavaPage::loadSettings()
// Java Settings
ui->javaPathTextBox->setText(s->get("JavaPath").toString());
- ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString());
+ ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString());
ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool());
ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool());
}
void JavaPage::on_javaDetectBtn_clicked()
{
+ if (JavaUtils::getJavaCheckPath().isEmpty()) {
+ JavaCommon::javaCheckNotFound(this);
+ return;
+ }
+
JavaInstallPtr java;
VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true);
@@ -166,7 +171,7 @@ void JavaPage::on_javaTestBtn_clicked()
return;
}
checker.reset(new JavaCommon::TestCheck(
- this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(),
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "),
ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
checker->run();
diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui
index 3e4b12a1..6ccffed4 100644
--- a/launcher/ui/pages/global/JavaPage.ui
+++ b/launcher/ui/pages/global/JavaPage.ui
@@ -150,19 +150,16 @@
<string>Java Runtime</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QLabel" name="labelJavaPath">
+ <item row="3" column="1">
+ <widget class="QPushButton" name="javaDetectBtn">
<property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Java path:</string>
- </property>
- <property name="buddy">
- <cstring>javaPathTextBox</cstring>
+ <string>&amp;Auto-detect...</string>
</property>
</widget>
</item>
@@ -175,31 +172,31 @@
</sizepolicy>
</property>
<property name="text">
- <string>J&amp;VM arguments:</string>
+ <string>JVM arguments:</string>
</property>
- <property name="buddy">
- <cstring>jvmArgsTextBox</cstring>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="4" column="1">
- <widget class="QCheckBox" name="skipCompatibilityCheckbox">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJavaPath">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="toolTip">
- <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
- </property>
<property name="text">
- <string>&amp;Skip Java compatibility checks</string>
+ <string>&amp;Java path:</string>
+ </property>
+ <property name="buddy">
+ <cstring>javaPathTextBox</cstring>
</property>
</widget>
</item>
- <item row="3" column="1">
- <widget class="QPushButton" name="javaDetectBtn">
+ <item row="3" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@@ -207,7 +204,7 @@
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Auto-detect...</string>
+ <string>&amp;Test</string>
</property>
</widget>
</item>
@@ -237,22 +234,22 @@
</item>
</layout>
</item>
- <item row="3" column="2">
- <widget class="QPushButton" name="javaTestBtn">
+ <item row="4" column="1">
+ <widget class="QCheckBox" name="skipCompatibilityCheckbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
+ <property name="toolTip">
+ <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
+ </property>
<property name="text">
- <string>&amp;Test</string>
+ <string>&amp;Skip Java compatibility checks</string>
</property>
</widget>
</item>
- <item row="2" column="1" colspan="2">
- <widget class="QLineEdit" name="jvmArgsTextBox"/>
- </item>
<item row="5" column="1">
<widget class="QCheckBox" name="skipJavaWizardCheckbox">
<property name="toolTip">
@@ -263,6 +260,25 @@
</property>
</widget>
</item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -291,7 +307,6 @@
<tabstop>permGenSpinBox</tabstop>
<tabstop>javaBrowseBtn</tabstop>
<tabstop>javaPathTextBox</tabstop>
- <tabstop>jvmArgsTextBox</tabstop>
<tabstop>javaDetectBtn</tabstop>
<tabstop>javaTestBtn</tabstop>
<tabstop>tabWidget</tabstop>
diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp
index 485d7fd4..fcd174bd 100644
--- a/launcher/ui/pages/global/LanguagePage.cpp
+++ b/launcher/ui/pages/global/LanguagePage.cpp
@@ -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
diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h
index 9b321170..2fd4ab0d 100644
--- a/launcher/ui/pages/global/LanguagePage.h
+++ b/launcher/ui/pages/global/LanguagePage.h
@@ -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
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index b244b039..73ef0024 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -191,6 +191,11 @@ void LauncherPage::on_modsDirBrowseBtn_clicked()
}
}
+void LauncherPage::on_metadataDisableBtn_clicked()
+{
+ ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
+}
+
void LauncherPage::refreshUpdateChannelList()
{
// Stop listening for selection changes. It's going to change a lot while we update it and
@@ -354,6 +359,9 @@ void LauncherPage::applySettings()
s->set("InstSortMode", "Name");
break;
}
+
+ // Mods
+ s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked());
}
void LauncherPage::loadSettings()
{
@@ -465,6 +473,10 @@ void LauncherPage::loadSettings()
{
ui->sortByNameBtn->setChecked(true);
}
+
+ // Mods
+ ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool());
+ ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
}
void LauncherPage::refreshFontPreview()
diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h
index bbf5d2fe..f38c922e 100644
--- a/launcher/ui/pages/global/LauncherPage.h
+++ b/launcher/ui/pages/global/LauncherPage.h
@@ -88,6 +88,7 @@ slots:
void on_instDirBrowseBtn_clicked();
void on_modsDirBrowseBtn_clicked();
void on_iconsDirBrowseBtn_clicked();
+ void on_metadataDisableBtn_clicked();
/*!
* Updates the list of update channels in the combo box.
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index a306a91b..645f7ef6 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -94,19 +94,13 @@
<string>Folders</string>
</property>
<layout class="QGridLayout" name="foldersBoxLayout">
- <item row="0" column="0">
- <widget class="QLabel" name="labelInstDir">
+ <item row="1" column="2">
+ <widget class="QToolButton" name="modsDirBrowseBtn">
<property name="text">
- <string>I&amp;nstances:</string>
- </property>
- <property name="buddy">
- <cstring>instDirTextBox</cstring>
+ <string notr="true">...</string>
</property>
</widget>
</item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="instDirTextBox"/>
- </item>
<item row="0" column="2">
<widget class="QToolButton" name="instDirBrowseBtn">
<property name="text">
@@ -114,43 +108,78 @@
</property>
</widget>
</item>
- <item row="1" column="0">
- <widget class="QLabel" name="labelModsDir">
+ <item row="2" column="2">
+ <widget class="QToolButton" name="iconsDirBrowseBtn">
<property name="text">
- <string>&amp;Mods:</string>
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="instDirTextBox"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelIconsDir">
+ <property name="text">
+ <string>&amp;Icons:</string>
</property>
<property name="buddy">
- <cstring>modsDirTextBox</cstring>
+ <cstring>iconsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
- <item row="1" column="2">
- <widget class="QToolButton" name="modsDirBrowseBtn">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelInstDir">
<property name="text">
- <string notr="true">...</string>
+ <string>I&amp;nstances:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
- <item row="2" column="0">
- <widget class="QLabel" name="labelIconsDir">
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelModsDir">
<property name="text">
- <string>&amp;Icons:</string>
+ <string>&amp;Mods:</string>
</property>
<property name="buddy">
- <cstring>iconsDirTextBox</cstring>
+ <cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
- <item row="2" column="2">
- <widget class="QToolButton" name="iconsDirBrowseBtn">
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="modsBox">
+ <property name="title">
+ <string>Mods</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="metadataDisableBtn">
+ <property name="toolTip">
+ <string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string>
+ </property>
<property name="text">
- <string notr="true">...</string>
+ <string>Disable using metadata for mods?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="metadataWarningLabel">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; color:#f5c211;&quot;&gt;Warning&lt;/span&gt;&lt;span style=&quot; color:#f5c211;&quot;&gt;: Disabling mod metadata may also disable some upcoming QoL features, such as mod updating!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index f49f5a92..e3ac7e7c 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -87,6 +87,11 @@ void MinecraftPage::applySettings()
s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked());
s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked());
+ // Peformance related options
+ s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked());
+ s->set("EnableMangoHud", ui->enableMangoHud->isChecked());
+ s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked());
+
// Game time
s->set("ShowGameTime", ui->showGameTime->isChecked());
s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked());
@@ -109,6 +114,14 @@ void MinecraftPage::loadSettings()
ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool());
ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool());
+ ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool());
+ ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool());
+ ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool());
+
+#if !defined(Q_OS_LINUX)
+ ui->perfomanceGroupBox->setVisible(false);
+#endif
+
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
index 353390bd..640f436d 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -135,6 +135,45 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="perfomanceGroupBox">
+ <property name="title">
+ <string>Performance</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="enableFeralGamemodeCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable Feral GameMode</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="enableMangoHud">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable MangoHud</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useDiscreteGpuCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use discrete GPU</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<widget class="QGroupBox" name="gameTimeGroupBox">
<property name="title">
<string>Game time</string>
@@ -181,15 +220,15 @@
</widget>
</item>
<item>
- <widget class="QCheckBox" name="quitAfterGameStopCheck">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>&amp;Quit the launcher after game window closes</string>
- </property>
- </widget>
- </item>
+ <widget class="QCheckBox" name="quitAfterGameStopCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The launcher will automatically quit after the game exits or crashes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>&amp;Quit the launcher after game window closes</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -218,6 +257,9 @@
<tabstop>windowHeightSpinBox</tabstop>
<tabstop>useNativeGLFWCheck</tabstop>
<tabstop>useNativeOpenALCheck</tabstop>
+ <tabstop>enableFeralGamemodeCheck</tabstop>
+ <tabstop>enableMangoHud</tabstop>
+ <tabstop>useDiscreteGpuCheck</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp
index aefd1e74..ffff8456 100644
--- a/launcher/ui/pages/global/ProxyPage.cpp
+++ b/launcher/ui/pages/global/ProxyPage.cpp
@@ -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
@@ -36,11 +37,11 @@
#include "ProxyPage.h"
#include "ui_ProxyPage.h"
+#include <QButtonGroup>
#include <QTabBar>
#include "settings/SettingsObject.h"
#include "Application.h"
-#include "Application.h"
ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
{
@@ -49,7 +50,8 @@ ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
loadSettings();
updateCheckboxStuff();
- connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int)));
+ connect(ui->proxyGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
+ this, &ProxyPage::proxyGroupChanged);
}
ProxyPage::~ProxyPage()
@@ -65,13 +67,13 @@ bool ProxyPage::apply()
void ProxyPage::updateCheckboxStuff()
{
- ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
- !ui->proxyDefaultBtn->isChecked());
- ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
- !ui->proxyDefaultBtn->isChecked());
+ bool enableEditing = ui->proxyHTTPBtn->isChecked()
+ || ui->proxySOCKS5Btn->isChecked();
+ ui->proxyAddrBox->setEnabled(enableEditing);
+ ui->proxyAuthBox->setEnabled(enableEditing);
}
-void ProxyPage::proxyChanged(int)
+void ProxyPage::proxyGroupChanged(QAbstractButton *button)
{
updateCheckboxStuff();
}
diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h
index e3677774..279a9029 100644
--- a/launcher/ui/pages/global/ProxyPage.h
+++ b/launcher/ui/pages/global/ProxyPage.h
@@ -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
@@ -36,6 +37,7 @@
#pragma once
#include <memory>
+#include <QAbstractButton>
#include <QDialog>
#include "ui/pages/BasePage.h"
@@ -73,15 +75,14 @@ public:
bool apply() override;
void retranslate() override;
+private slots:
+ void proxyGroupChanged(QAbstractButton *button);
+
private:
void updateCheckboxStuff();
void applySettings();
void loadSettings();
-private
-slots:
- void proxyChanged(int);
-
private:
Ui::ProxyPage *ui;
};
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
new file mode 100644
index 00000000..d06f412b
--- /dev/null
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -0,0 +1,297 @@
+#include "ExternalResourcesPage.h"
+#include "ui_ExternalResourcesPage.h"
+
+#include "DesktopServices.h"
+#include "Version.h"
+#include "minecraft/mod/ModFolderModel.h"
+#include "ui/GuiUtil.h"
+
+#include <QKeyEvent>
+#include <QMenu>
+
+namespace {
+// FIXME: wasteful
+void RemoveThePrefix(QString& string)
+{
+ QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
+ string.remove(regex);
+ string = string.trimmed();
+}
+} // namespace
+
+class SortProxy : public QSortFilterProxyModel {
+ public:
+ explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
+
+ protected:
+ bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
+ {
+ ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
+ if (!model)
+ return false;
+
+ const auto& mod = model->at(source_row);
+
+ if (mod.name().contains(filterRegularExpression()))
+ return true;
+ if (mod.description().contains(filterRegularExpression()))
+ return true;
+
+ for (auto& author : mod.authors()) {
+ if (author.contains(filterRegularExpression())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
+ {
+ ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
+ if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
+ // proceed.
+
+ auto column = (ModFolderModel::Columns) source_left.column();
+ bool invert = false;
+ switch (column) {
+ // GH-2550 - sort by enabled/disabled
+ case ModFolderModel::ActiveColumn: {
+ auto dataL = source_left.data(Qt::CheckStateRole).toBool();
+ auto dataR = source_right.data(Qt::CheckStateRole).toBool();
+ if (dataL != dataR)
+ return dataL > dataR;
+
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2722 - sort mod names in a way that discards "The" prefixes
+ case ModFolderModel::NameColumn: {
+ auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataL);
+ auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataR);
+
+ auto less = dataL.compare(dataR, sortCaseSensitivity());
+ if (less != 0)
+ return invert ? (less > 0) : (less < 0);
+
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2762 - sort versions by parsing them as versions
+ case ModFolderModel::VersionColumn: {
+ auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
+ auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
+ return invert ? (dataL > dataR) : (dataL < dataR);
+ }
+ default: {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+ }
+ }
+};
+
+ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent)
+ : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
+{
+ ui->setupUi(this);
+
+ runningStateChanged(m_instance && m_instance->isRunning());
+
+ ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
+
+ m_filterModel = new SortProxy(this);
+ m_filterModel->setDynamicSortFilter(true);
+ m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSourceModel(m_model.get());
+ m_filterModel->setFilterKeyColumn(-1);
+ ui->treeView->setModel(m_filterModel);
+
+ ui->treeView->installEventFilter(this);
+ ui->treeView->sortByColumn(1, Qt::AscendingOrder);
+ ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ // The default function names by Qt are pretty ugly, so let's just connect the actions manually,
+ // to make it easier to read :)
+ connect(ui->actionAddItem, &QAction::triggered, this, &ExternalResourcesPage::addItem);
+ connect(ui->actionRemoveItem, &QAction::triggered, this, &ExternalResourcesPage::removeItem);
+ connect(ui->actionEnableItem, &QAction::triggered, this, &ExternalResourcesPage::enableItem);
+ connect(ui->actionDisableItem, &QAction::triggered, this, &ExternalResourcesPage::disableItem);
+ connect(ui->actionViewConfigs, &QAction::triggered, this, &ExternalResourcesPage::viewConfigs);
+ connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder);
+
+ connect(ui->treeView, &ModListView::customContextMenuRequested, this, &ExternalResourcesPage::ShowContextMenu);
+ connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated);
+
+ auto selection_model = ui->treeView->selectionModel();
+ connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current);
+ connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged);
+ connect(m_instance, &BaseInstance::runningStatusChanged, this, &ExternalResourcesPage::runningStateChanged);
+}
+
+ExternalResourcesPage::~ExternalResourcesPage()
+{
+ m_model->stopWatching();
+ delete ui;
+}
+
+void ExternalResourcesPage::itemActivated(const QModelIndex&)
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle);
+}
+
+QMenu* ExternalResourcesPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction());
+ return filteredMenu;
+}
+
+void ExternalResourcesPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->treeView->mapToGlobal(pos));
+ delete menu;
+}
+
+void ExternalResourcesPage::openedImpl()
+{
+ m_model->startWatching();
+}
+
+void ExternalResourcesPage::closedImpl()
+{
+ m_model->stopWatching();
+}
+
+void ExternalResourcesPage::retranslate()
+{
+ ui->retranslateUi(this);
+}
+
+void ExternalResourcesPage::filterTextChanged(const QString& newContents)
+{
+ m_viewFilter = newContents;
+ m_filterModel->setFilterFixedString(m_viewFilter);
+}
+
+void ExternalResourcesPage::runningStateChanged(bool running)
+{
+ if (m_controlsEnabled == !running)
+ return;
+
+ m_controlsEnabled = !running;
+ ui->actionAddItem->setEnabled(m_controlsEnabled);
+ ui->actionDisableItem->setEnabled(m_controlsEnabled);
+ ui->actionEnableItem->setEnabled(m_controlsEnabled);
+ ui->actionRemoveItem->setEnabled(m_controlsEnabled);
+}
+
+bool ExternalResourcesPage::shouldDisplay() const
+{
+ return true;
+}
+
+bool ExternalResourcesPage::listFilter(QKeyEvent* keyEvent)
+{
+ switch (keyEvent->key()) {
+ case Qt::Key_Delete:
+ removeItem();
+ return true;
+ case Qt::Key_Plus:
+ addItem();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(ui->treeView, keyEvent);
+}
+
+bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev)
+{
+ if (ev->type() != QEvent::KeyPress)
+ return QWidget::eventFilter(obj, ev);
+
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
+ if (obj == ui->treeView)
+ return listFilter(keyEvent);
+
+ return QWidget::eventFilter(obj, ev);
+}
+
+void ExternalResourcesPage::addItem()
+{
+ if (!m_controlsEnabled)
+ return;
+
+
+ auto list = GuiUtil::BrowseForFiles(
+ helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()),
+ m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
+
+ if (!list.isEmpty()) {
+ for (auto filename : list) {
+ m_model->installMod(filename);
+ }
+ }
+}
+
+void ExternalResourcesPage::removeItem()
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->deleteMods(selection.indexes());
+}
+
+void ExternalResourcesPage::enableItem()
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->setModStatus(selection.indexes(), ModFolderModel::Enable);
+}
+
+void ExternalResourcesPage::disableItem()
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->setModStatus(selection.indexes(), ModFolderModel::Disable);
+}
+
+void ExternalResourcesPage::viewConfigs()
+{
+ DesktopServices::openDirectory(m_instance->instanceConfigFolder(), true);
+}
+
+void ExternalResourcesPage::viewFolder()
+{
+ DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
+}
+
+void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
+{
+ if (!current.isValid()) {
+ ui->frame->clear();
+ return;
+ }
+
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ Mod& m = m_model->operator[](row);
+ ui->frame->updateWithMod(m);
+}
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
new file mode 100644
index 00000000..41237139
--- /dev/null
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <QMainWindow>
+#include <QSortFilterProxyModel>
+
+#include "Application.h"
+#include "minecraft/MinecraftInstance.h"
+#include "ui/pages/BasePage.h"
+
+class ModFolderModel;
+
+namespace Ui {
+class ExternalResourcesPage;
+}
+
+/* This page is used as a base for pages in which the user can manage external resources
+ * related to the game, such as mods, shaders or resource packs. */
+class ExternalResourcesPage : public QMainWindow, public BasePage {
+ Q_OBJECT
+
+ public:
+ // FIXME: Switch to different model (or change the name of this one)
+ explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
+ virtual ~ExternalResourcesPage();
+
+ virtual QString displayName() const override = 0;
+ virtual QIcon icon() const override = 0;
+ virtual QString id() const override = 0;
+ virtual QString helpPage() const override = 0;
+
+ virtual bool shouldDisplay() const override = 0;
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ void retranslate() override;
+
+ protected:
+ bool eventFilter(QObject* obj, QEvent* ev) override;
+ bool listFilter(QKeyEvent* ev);
+ QMenu* createPopupMenu() override;
+
+ public slots:
+ void current(const QModelIndex& current, const QModelIndex& previous);
+
+ protected slots:
+ void itemActivated(const QModelIndex& index);
+ void filterTextChanged(const QString& newContents);
+ void runningStateChanged(bool running);
+
+ virtual void addItem();
+ virtual void removeItem();
+
+ virtual void enableItem();
+ virtual void disableItem();
+
+ virtual void viewFolder();
+ virtual void viewConfigs();
+
+ void ShowContextMenu(const QPoint& pos);
+
+ protected:
+ BaseInstance* m_instance = nullptr;
+
+ Ui::ExternalResourcesPage* ui = nullptr;
+ std::shared_ptr<ModFolderModel> m_model;
+ QSortFilterProxyModel* m_filterModel = nullptr;
+
+ QString m_fileSelectionFilter;
+ QString m_viewFilter;
+
+ bool m_controlsEnabled = true;
+};
diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index ab59b0df..17bf455a 100644
--- a/launcher/ui/pages/instance/ModFolderPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>ModFolderPage</class>
- <widget class="QMainWindow" name="ModFolderPage">
+ <class>ExternalResourcesPage</class>
+ <widget class="QMainWindow" name="ExternalResourcesPage">
<property name="geometry">
<rect>
<x>0</x>
@@ -53,7 +53,7 @@
</widget>
</item>
<item row="1" column="1" colspan="3">
- <widget class="ModListView" name="modTreeView">
+ <widget class="ModListView" name="treeView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
@@ -83,15 +83,15 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
- <addaction name="actionAdd"/>
+ <addaction name="actionAddItem"/>
<addaction name="separator"/>
- <addaction name="actionRemove"/>
- <addaction name="actionEnable"/>
- <addaction name="actionDisable"/>
- <addaction name="actionView_configs"/>
- <addaction name="actionView_Folder"/>
+ <addaction name="actionRemoveItem"/>
+ <addaction name="actionEnableItem"/>
+ <addaction name="actionDisableItem"/>
+ <addaction name="actionViewConfigs"/>
+ <addaction name="actionViewFolder"/>
</widget>
- <action name="actionAdd">
+ <action name="actionAddItem">
<property name="text">
<string>&amp;Add</string>
</property>
@@ -99,31 +99,31 @@
<string>Add</string>
</property>
</action>
- <action name="actionRemove">
+ <action name="actionRemoveItem">
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="toolTip">
- <string>Remove selected mods</string>
+ <string>Remove selected item</string>
</property>
</action>
- <action name="actionEnable">
+ <action name="actionEnableItem">
<property name="text">
<string>&amp;Enable</string>
</property>
<property name="toolTip">
- <string>Enable selected mods</string>
+ <string>Enable selected item</string>
</property>
</action>
- <action name="actionDisable">
+ <action name="actionDisableItem">
<property name="text">
<string>&amp;Disable</string>
</property>
<property name="toolTip">
- <string>Disable selected mods</string>
+ <string>Disable selected item</string>
</property>
</action>
- <action name="actionView_configs">
+ <action name="actionViewConfigs">
<property name="text">
<string>View &amp;Configs</string>
</property>
@@ -131,11 +131,22 @@
<string>Open the 'config' folder in the system file manager.</string>
</property>
</action>
- <action name="actionView_Folder">
+ <action name="actionViewFolder">
<property name="text">
<string>View &amp;Folder</string>
</property>
</action>
+ <action name="actionDownloadItem">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>&amp;Download</string>
+ </property>
+ <property name="toolTip">
+ <string>Download a new resource</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
@@ -156,7 +167,7 @@
</customwidget>
</customwidgets>
<tabstops>
- <tabstop>modTreeView</tabstop>
+ <tabstop>treeView</tabstop>
<tabstop>filterEdit</tabstop>
</tabstops>
<resources/>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index b4562843..fcc110de 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -2,7 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * 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
@@ -50,6 +50,7 @@
#include "Application.h"
#include "java/JavaInstallList.h"
+#include "java/JavaUtils.h"
#include "FileSystem.h"
@@ -232,6 +233,22 @@ void InstanceSettingsPage::applySettings()
m_settings->reset("UseNativeGLFW");
}
+ // Performance
+ bool performance = ui->perfomanceGroupBox->isChecked();
+ m_settings->set("OverridePerformance", performance);
+ if(performance)
+ {
+ m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked());
+ m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked());
+ m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked());
+ }
+ else
+ {
+ m_settings->reset("EnableFeralGamemode");
+ m_settings->reset("EnableMangoHud");
+ m_settings->reset("UseDiscreteGpu");
+ }
+
// Game time
bool gameTime = ui->gameTimeGroupBox->isChecked();
m_settings->set("OverrideGameTime", gameTime);
@@ -325,6 +342,16 @@ void InstanceSettingsPage::loadSettings()
ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool());
ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool());
+ // Performance
+ ui->perfomanceGroupBox->setChecked(m_settings->get("OverridePerformance").toBool());
+ ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool());
+ ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool());
+ ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
+
+ #if !defined(Q_OS_LINUX)
+ ui->perfomanceGroupBox->setVisible(false);
+ #endif
+
// Miscellanous
ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool());
@@ -336,6 +363,11 @@ void InstanceSettingsPage::loadSettings()
void InstanceSettingsPage::on_javaDetectBtn_clicked()
{
+ if (JavaUtils::getJavaCheckPath().isEmpty()) {
+ JavaCommon::javaCheckNotFound(this);
+ return;
+ }
+
JavaInstallPtr java;
VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true);
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui
index cb66b3ce..8b3c3370 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.ui
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui
@@ -455,6 +455,74 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="performancePage">
+ <attribute name="title">
+ <string>Performance</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_14">
+ <item>
+ <widget class="QGroupBox" name="perfomanceGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Performance</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_13">
+ <item>
+ <widget class="QCheckBox" name="enableFeralGamemodeCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Feral Interactive's GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable Feral GameMode</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="enableMangoHud">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable MangoHud</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useDiscreteGpuCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use discrete GPU</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <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 class="QWidget" name="miscellaneousPage">
<attribute name="title">
<string>Miscellaneous</string>
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp
index 30a8735f..3d9fb025 100644
--- a/launcher/ui/pages/instance/LogPage.cpp
+++ b/launcher/ui/pages/instance/LogPage.cpp
@@ -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
@@ -62,7 +63,7 @@ public:
{
case Qt::FontRole:
return m_font;
- case Qt::TextColorRole:
+ case Qt::ForegroundRole:
{
MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt();
return m_colors->getFront(level);
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 8113fe85..4432ccc8 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -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
@@ -34,409 +35,123 @@
*/
#include "ModFolderPage.h"
-#include "ui_ModFolderPage.h"
+#include "ui_ExternalResourcesPage.h"
-#include <QMessageBox>
+#include <QAbstractItemModel>
#include <QEvent>
#include <QKeyEvent>
-#include <QAbstractItemModel>
#include <QMenu>
+#include <QMessageBox>
#include <QSortFilterProxyModel>
#include "Application.h"
+#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ModDownloadDialog.h"
-#include "ui/GuiUtil.h"
#include "DesktopServices.h"
-#include "minecraft/mod/ModFolderModel.h"
-#include "minecraft/mod/Mod.h"
-#include "minecraft/VersionFilterData.h"
#include "minecraft/PackProfile.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/mod/Mod.h"
+#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModAPI.h"
#include "Version.h"
+#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/ProgressDialog.h"
-#include "tasks/SequentialTask.h"
-
-namespace {
- // FIXME: wasteful
- void RemoveThePrefix(QString & string) {
- QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +"));
- string.remove(regex);
- string = string.trimmed();
- }
-}
-
-class ModSortProxy : public QSortFilterProxyModel
-{
-public:
- explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent)
- {
- }
-
-protected:
- bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override {
- ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
- if(!model) {
- return false;
- }
- const auto &mod = model->at(source_row);
- if(mod.name().contains(filterRegExp())) {
- return true;
- }
- if(mod.description().contains(filterRegExp())) {
- return true;
- }
- for(auto & author: mod.authors()) {
- if (author.contains(filterRegExp())) {
- return true;
- }
- }
- return false;
- }
-
- bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override
- {
- ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
- if(
- !model ||
- !source_left.isValid() ||
- !source_right.isValid() ||
- source_left.column() != source_right.column()
- ) {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
-
- // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed.
- auto column = (ModFolderModel::Columns) source_left.column();
- bool invert = false;
- switch(column) {
- // GH-2550 - sort by enabled/disabled
- case ModFolderModel::ActiveColumn: {
- auto dataL = source_left.data(Qt::CheckStateRole).toBool();
- auto dataR = source_right.data(Qt::CheckStateRole).toBool();
- if(dataL != dataR) {
- return dataL > dataR;
- }
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2722 - sort mod names in a way that discards "The" prefixes
- case ModFolderModel::NameColumn: {
- auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataL);
- auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataR);
-
- auto less = dataL.compare(dataR, sortCaseSensitivity());
- if(less != 0) {
- return invert ? (less > 0) : (less < 0);
- }
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2762 - sort versions by parsing them as versions
- case ModFolderModel::VersionColumn: {
- auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
- auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
- return invert ? (dataL > dataR) : (dataL < dataR);
- }
- default: {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
- }
- }
-};
-
-ModFolderPage::ModFolderPage(
- BaseInstance *inst,
- std::shared_ptr<ModFolderModel> mods,
- QString id,
- QString iconName,
- QString displayName,
- QString helpPage,
- QWidget *parent
-) :
- QMainWindow(parent),
- ui(new Ui::ModFolderPage)
+ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
+ : ExternalResourcesPage(inst, mods, parent)
{
- ui->setupUi(this);
-
// This is structured like that so that these changes
- // do not affect the Resouce pack and Shader pack tabs
- if(id == "mods") {
- auto act = new QAction(tr("Download mods"), this);
- act->setToolTip(tr("Download mods from online mod platforms"));
- ui->actionsToolbar->insertActionBefore(ui->actionAdd, act);
- connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered);
-
- ui->actionAdd->setText(tr("Add .jar"));
- ui->actionAdd->setToolTip(tr("Add mods via local file"));
- }
-
- ui->actionsToolbar->insertSpacer(ui->actionView_configs);
-
- m_inst = inst;
- on_RunningState_changed(m_inst && m_inst->isRunning());
- m_mods = mods;
- m_id = id;
- m_displayName = displayName;
- m_iconName = iconName;
- m_helpName = helpPage;
- m_fileSelectionFilter = "%1 (*.zip *.jar)";
- m_filterModel = new ModSortProxy(this);
- m_filterModel->setDynamicSortFilter(true);
- m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
- m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
- m_filterModel->setSourceModel(m_mods.get());
- m_filterModel->setFilterKeyColumn(-1);
- ui->modTreeView->setModel(m_filterModel);
- ui->modTreeView->installEventFilter(this);
- ui->modTreeView->sortByColumn(1, Qt::AscendingOrder);
- ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu);
- connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated);
+ // do not affect the Resource pack and Shader pack tabs
+ {
+ ui->actionDownloadItem->setText(tr("Download mods"));
+ ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms"));
+ ui->actionDownloadItem->setEnabled(true);
+ ui->actionAddItem->setText(tr("Add file"));
+ ui->actionAddItem->setToolTip(tr("Add a locally downloaded file"));
- auto smodel = ui->modTreeView->selectionModel();
- connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent);
- connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged);
- connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed);
-}
+ ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
-void ModFolderPage::modItemActivated(const QModelIndex&)
-{
- if(!m_controlsEnabled) {
- return;
+ connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods);
}
- auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
- m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle);
-}
-
-QMenu * ModFolderPage::createPopupMenu()
-{
- QMenu* filteredMenu = QMainWindow::createPopupMenu();
- filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() );
- return filteredMenu;
-}
-
-void ModFolderPage::ShowContextMenu(const QPoint& pos)
-{
- auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu"));
- menu->exec(ui->modTreeView->mapToGlobal(pos));
- delete menu;
-}
-
-void ModFolderPage::openedImpl()
-{
- m_mods->startWatching();
-}
-
-void ModFolderPage::closedImpl()
-{
- m_mods->stopWatching();
-}
-
-void ModFolderPage::on_filterTextChanged(const QString& newContents)
-{
- m_viewFilter = newContents;
- m_filterModel->setFilterFixedString(m_viewFilter);
-}
-
-
-CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods,
- QString id, QString iconName, QString displayName,
- QString helpPage, QWidget *parent)
- : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent)
-{
}
-ModFolderPage::~ModFolderPage()
-{
- m_mods->stopWatching();
- delete ui;
-}
-
-void ModFolderPage::on_RunningState_changed(bool running)
-{
- if(m_controlsEnabled == !running) {
- return;
- }
- m_controlsEnabled = !running;
- ui->actionsToolbar->setEnabled(m_controlsEnabled);
-}
+CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
+ : ModFolderPage(inst, mods, parent)
+{}
bool ModFolderPage::shouldDisplay() const
{
return true;
}
-void ModFolderPage::retranslate()
-{
- ui->retranslateUi(this);
-}
-
bool CoreModFolderPage::shouldDisplay() const
{
- if (ModFolderPage::shouldDisplay())
- {
- auto inst = dynamic_cast<MinecraftInstance *>(m_inst);
+ if (ModFolderPage::shouldDisplay()) {
+ auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
if (!inst)
return true;
+
auto version = inst->getPackProfile();
+
if (!version)
return true;
- if(!version->getComponent("net.minecraftforge"))
- {
+ if (!version->getComponent("net.minecraftforge"))
return false;
- }
- if(!version->getComponent("net.minecraft"))
- {
+ if (!version->getComponent("net.minecraft"))
return false;
- }
- if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
- {
+ if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
return true;
- }
+
}
return false;
}
-bool ModFolderPage::modListFilter(QKeyEvent *keyEvent)
-{
- switch (keyEvent->key())
- {
- case Qt::Key_Delete:
- on_actionRemove_triggered();
- return true;
- case Qt::Key_Plus:
- on_actionAdd_triggered();
- return true;
- default:
- break;
- }
- return QWidget::eventFilter(ui->modTreeView, keyEvent);
-}
-
-bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev)
-{
- if (ev->type() != QEvent::KeyPress)
- {
- return QWidget::eventFilter(obj, ev);
- }
- QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
- if (obj == ui->modTreeView)
- return modListFilter(keyEvent);
- return QWidget::eventFilter(obj, ev);
-}
-
-void ModFolderPage::on_actionAdd_triggered()
-{
- if(!m_controlsEnabled) {
- return;
- }
- auto list = GuiUtil::BrowseForFiles(
- m_helpName,
- tr("Select %1",
- "Select whatever type of files the page contains. Example: 'Loader Mods'")
- .arg(m_displayName),
- m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(),
- this->parentWidget());
- if (!list.empty())
- {
- for (auto filename : list)
- {
- m_mods->installMod(filename);
- }
- }
-}
-
-void ModFolderPage::on_actionEnable_triggered()
-{
- if(!m_controlsEnabled) {
- return;
- }
- auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
- m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable);
-}
-
-void ModFolderPage::on_actionDisable_triggered()
+void ModFolderPage::installMods()
{
- if(!m_controlsEnabled) {
+ if (!m_controlsEnabled)
return;
- }
- auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
- m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable);
-}
-
-void ModFolderPage::on_actionRemove_triggered()
-{
- if(!m_controlsEnabled) {
+ if (m_instance->typeName() != "Minecraft")
+ return; // this is a null instance or a legacy instance
+
+ auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
+ if (profile->getModLoaders() == ModAPI::Unspecified) {
+ QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
- auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
- 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
- }
- auto profile = ((MinecraftInstance *)m_inst)->getPackProfile();
- if (profile->getModLoader() == ModAPI::Unspecified) {
- QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!"));
- return;
- }
- ModDownloadDialog mdownload(m_mods, this, m_inst);
+ ModDownloadDialog mdownload(m_model, this, m_instance);
if (mdownload.exec()) {
- SequentialTask* tasks = new SequentialTask(this);
+ ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
tasks->deleteLater();
});
+ connect(tasks, &Task::aborted, [this, tasks]() {
+ CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
+ tasks->deleteLater();
+ });
connect(tasks, &Task::succeeded, [this, tasks]() {
QStringList warnings = tasks->warnings();
- if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); }
+ if (warnings.count())
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+
tasks->deleteLater();
});
- for (auto task : mdownload.getTasks()) {
+ for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
+
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
- m_mods->update();
- }
-}
-
-void ModFolderPage::on_actionView_configs_triggered()
-{
- DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
-}
-
-void ModFolderPage::on_actionView_Folder_triggered()
-{
- DesktopServices::openDirectory(m_mods->dir().absolutePath(), true);
-}
-void ModFolderPage::modCurrent(const QModelIndex &current, const QModelIndex &previous)
-{
- if (!current.isValid())
- {
- ui->frame->clear();
- return;
+ m_model->update();
}
- auto sourceCurrent = m_filterModel->mapToSource(current);
- int row = sourceCurrent.row();
- Mod &m = m_mods->operator[](row);
- ui->frame->updateWithMod(m);
}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 72e2d404..19caa732 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -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
@@ -35,108 +36,31 @@
#pragma once
-#include <QMainWindow>
+#include "ExternalResourcesPage.h"
-#include "minecraft/MinecraftInstance.h"
-#include "ui/pages/BasePage.h"
-
-#include <Application.h>
-
-class ModFolderModel;
-namespace Ui
-{
-class ModFolderPage;
-}
-
-class ModFolderPage : public QMainWindow, public BasePage
-{
+class ModFolderPage : public ExternalResourcesPage {
Q_OBJECT
-public:
- explicit ModFolderPage(
- BaseInstance *inst,
- std::shared_ptr<ModFolderModel> mods,
- QString id,
- QString iconName,
- QString displayName,
- QString helpPage = "",
- QWidget *parent = 0
- );
- virtual ~ModFolderPage();
-
- void setFilter(const QString & filter)
- {
- m_fileSelectionFilter = filter;
- }
+ public:
+ explicit ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = nullptr);
+ virtual ~ModFolderPage() = default;
- virtual QString displayName() const override
- {
- return m_displayName;
- }
- virtual QIcon icon() const override
- {
- return APPLICATION->getThemedIcon(m_iconName);
- }
- virtual QString id() const override
- {
- return m_id;
- }
- virtual QString helpPage() const override
- {
- return m_helpName;
- }
- virtual bool shouldDisplay() const override;
- void retranslate() override;
-
- virtual void openedImpl() override;
- virtual void closedImpl() override;
-protected:
- bool eventFilter(QObject *obj, QEvent *ev) override;
- bool modListFilter(QKeyEvent *ev);
- QMenu * createPopupMenu() override;
+ void setFilter(const QString& filter) { m_fileSelectionFilter = filter; }
-protected:
- BaseInstance *m_inst = nullptr;
+ virtual QString displayName() const override { return tr("Mods"); }
+ virtual QIcon icon() const override { return APPLICATION->getThemedIcon("loadermods"); }
+ virtual QString id() const override { return "mods"; }
+ virtual QString helpPage() const override { return "Loader-mods"; }
-protected:
- Ui::ModFolderPage *ui = nullptr;
- std::shared_ptr<ModFolderModel> m_mods;
- QSortFilterProxyModel *m_filterModel = nullptr;
- QString m_iconName;
- QString m_id;
- QString m_displayName;
- QString m_helpName;
- QString m_fileSelectionFilter;
- QString m_viewFilter;
- bool m_controlsEnabled = true;
-
-public
-slots:
- void modCurrent(const QModelIndex &current, const QModelIndex &previous);
+ virtual bool shouldDisplay() const override;
-private
-slots:
- void modItemActivated(const QModelIndex &index);
- void on_filterTextChanged(const QString & newContents);
- void on_RunningState_changed(bool running);
- void on_actionAdd_triggered();
- 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);
+ private slots:
+ void installMods();
};
-class CoreModFolderPage : public ModFolderPage
-{
-public:
- explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id,
- QString iconName, QString displayName, QString helpPage = "",
- QWidget *parent = 0);
- virtual ~CoreModFolderPage()
- {
- }
+class CoreModFolderPage : public ModFolderPage {
+ public:
+ explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
+ virtual ~CoreModFolderPage() = default;
virtual bool shouldDisplay() const;
};
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index 8054926c..a6c9fdd3 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -35,24 +35,28 @@
#pragma once
-#include "ModFolderPage.h"
-#include "ui_ModFolderPage.h"
+#include "ExternalResourcesPage.h"
+#include "ui_ExternalResourcesPage.h"
-class ResourcePackPage : public ModFolderPage
+class ResourcePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks",
- "resourcepacks", tr("Resource packs"), "Resource-packs", parent)
+ : ExternalResourcesPage(instance, instance->resourcePackList(), parent)
{
- ui->actionView_configs->setVisible(false);
+ ui->actionViewConfigs->setVisible(false);
}
virtual ~ResourcePackPage() {}
+ QString displayName() const override { return tr("Resource packs"); }
+ QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
+ QString id() const override { return "resourcepacks"; }
+ QString helpPage() const override { return "Resource-packs"; }
+
virtual bool shouldDisplay() const override
{
- return !m_inst->traits().contains("no-texturepacks") &&
- !m_inst->traits().contains("texturepacks");
+ return !m_instance->traits().contains("no-texturepacks") &&
+ !m_instance->traits().contains("texturepacks");
}
};
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp
index 2cf17b32..c97253e4 100644
--- a/launcher/ui/pages/instance/ScreenshotsPage.cpp
+++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp
@@ -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
@@ -49,6 +50,7 @@
#include <QClipboard>
#include <QKeyEvent>
#include <QMenu>
+#include <QRegularExpression>
#include <Application.h>
@@ -153,7 +155,7 @@ public:
if (role == Qt::DisplayRole || role == Qt::EditRole)
{
QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
- return result.toString().remove(QRegExp("\\.png$"));
+ return result.toString().remove(QRegularExpression("\\.png$"));
}
if (role == Qt::DecorationRole)
{
@@ -269,7 +271,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent)
ui->listView->setViewMode(QListView::IconMode);
ui->listView->setResizeMode(QListView::Adjust);
ui->listView->installEventFilter(this);
- ui->listView->setEditTriggers(0);
+ ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->listView->setItemDelegate(new CenteredEditingDelegate(this));
ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu);
diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp
index 2af6164c..e5cce96c 100644
--- a/launcher/ui/pages/instance/ServersPage.cpp
+++ b/launcher/ui/pages/instance/ServersPage.cpp
@@ -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
@@ -287,7 +288,11 @@ public:
return false;
}
beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
+ m_servers.swapItemsAt(row-1, row);
+#else
m_servers.swap(row-1, row);
+#endif
endMoveRows();
scheduleSave();
return true;
@@ -305,7 +310,11 @@ public:
return false;
}
beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
+ m_servers.swapItemsAt(row+1, row);
+#else
m_servers.swap(row+1, row);
+#endif
endMoveRows();
scheduleSave();
return true;
@@ -614,7 +623,7 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent)
auto selectionModel = ui->serversView->selectionModel();
connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged);
- connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed);
+ connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged);
connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited);
connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited);
connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int)));
@@ -654,7 +663,7 @@ QMenu * ServersPage::createPopupMenu()
return filteredMenu;
}
-void ServersPage::on_RunningState_changed(bool running)
+void ServersPage::runningStateChanged(bool running)
{
if(m_locked == running)
{
diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h
index 5173712c..37399d49 100644
--- a/launcher/ui/pages/instance/ServersPage.h
+++ b/launcher/ui/pages/instance/ServersPage.h
@@ -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
@@ -97,7 +98,7 @@ private slots:
void on_actionMove_Down_triggered();
void on_actionJoin_triggered();
- void on_RunningState_changed(bool running);
+ void runningStateChanged(bool running);
void nameEdited(const QString & name);
void addressEdited(const QString & address);
diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h
index 7d4f5074..2cc056c8 100644
--- a/launcher/ui/pages/instance/ShaderPackPage.h
+++ b/launcher/ui/pages/instance/ShaderPackPage.h
@@ -35,21 +35,25 @@
#pragma once
-#include "ModFolderPage.h"
-#include "ui_ModFolderPage.h"
+#include "ExternalResourcesPage.h"
+#include "ui_ExternalResourcesPage.h"
-class ShaderPackPage : public ModFolderPage
+class ShaderPackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ModFolderPage(instance, instance->shaderPackList(), "shaderpacks",
- "shaderpacks", tr("Shader packs"), "Resource-packs", parent)
+ : ExternalResourcesPage(instance, instance->shaderPackList(), parent)
{
- ui->actionView_configs->setVisible(false);
+ ui->actionViewConfigs->setVisible(false);
}
virtual ~ShaderPackPage() {}
+ QString displayName() const override { return tr("Shader packs"); }
+ QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); }
+ QString id() const override { return "shaderpacks"; }
+ QString helpPage() const override { return "Resource-packs"; }
+
virtual bool shouldDisplay() const override
{
return true;
diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h
index e8cefe6e..f550a5bc 100644
--- a/launcher/ui/pages/instance/TexturePackPage.h
+++ b/launcher/ui/pages/instance/TexturePackPage.h
@@ -35,23 +35,27 @@
#pragma once
-#include "ModFolderPage.h"
-#include "ui_ModFolderPage.h"
+#include "ExternalResourcesPage.h"
+#include "ui_ExternalResourcesPage.h"
-class TexturePackPage : public ModFolderPage
+class TexturePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks",
- tr("Texture packs"), "Texture-packs", parent)
+ : ExternalResourcesPage(instance, instance->texturePackList(), parent)
{
- ui->actionView_configs->setVisible(false);
+ ui->actionViewConfigs->setVisible(false);
}
virtual ~TexturePackPage() {}
+ QString displayName() const override { return tr("Texture packs"); }
+ QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
+ QString id() const override { return "texturepacks"; }
+ QString helpPage() const override { return "Texture-packs"; }
+
virtual bool shouldDisplay() const override
{
- return m_inst->traits().contains("texturepacks");
+ return m_instance->traits().contains("texturepacks");
}
};
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 23e2367b..468ff35c 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -2,7 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * 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
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index 76725539..647b04a7 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -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
diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp
index c7bc13d8..30196aad 100644
--- a/launcher/ui/pages/modplatform/ImportPage.cpp
+++ b/launcher/ui/pages/modplatform/ImportPage.cpp
@@ -2,7 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * 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
@@ -110,14 +110,16 @@ void ImportPage::updateState()
{
// FIXME: actually do some validation of what's inside here... this is fake AF
QFileInfo fi(input);
- // 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"))
+ bool isZip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip");
+ // mrpack is a modrinth pack
+ bool isMRPack = fi.suffix() == "mrpack";
+
+ if(fi.exists() && (isZip || isMRPack))
{
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
@@ -130,7 +132,7 @@ void ImportPage::updateState()
}
// hook, line and sinker.
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
@@ -149,7 +151,8 @@ void ImportPage::setUrl(const QString& url)
void ImportPage::on_modpackBtn_clicked()
{
auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString();
- filter += ";;" + tr("Modrinth pack (*.mrpack)");
+ //: Option for filtering for *.mrpack files when importing
+ filter += ";;" + tr("Modrinth pack") + " (*.mrpack)";
const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter);
if (url.isValid())
{
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 540ee2fd..94b1f099 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -38,27 +38,48 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
}
ModPlatform::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;
+ switch (role) {
+ case Qt::DisplayRole: {
+ return pack.name;
}
- return pack.description;
- } else if (role == Qt::DecorationRole) {
- if (m_logoMap.contains(pack.logoName)) {
- return (m_logoMap.value(pack.logoName));
+ case 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;
}
- 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;
+ case Qt::DecorationRole: {
+ if (m_logoMap.contains(pack.logoName)) {
+ auto icon = m_logoMap.value(pack.logoName);
+ // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;(
+ auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48));
+
+ return icon_scaled;
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ // un-const-ify this
+ ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ case Qt::UserRole: {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ case Qt::FontRole: {
+ QFont font;
+ if (m_parent->getDialog()->isModSelected(pack.name)) {
+ font.setBold(true);
+ font.setUnderline(true);
+ }
+
+ return font;
+ }
+ default:
+ break;
}
return {};
@@ -68,7 +89,7 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
- m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
+ m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() });
}
void ListModel::performPaginatedSearch()
@@ -76,7 +97,12 @@ void ListModel::performPaginatedSearch()
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->searchMods(
- this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
+ this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
+}
+
+void ListModel::requestModInfo(ModPlatform::IndexedPack& current)
+{
+ m_parent->apiProvider()->getModInfo(this, current);
}
void ListModel::refresh()
@@ -193,6 +219,10 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
searchState = CanPossiblyFetchMore;
}
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
@@ -225,6 +255,21 @@ void ListModel::searchRequestFailed(QString reason)
}
}
+void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack)
+{
+ qDebug() << "Loading mod info";
+
+ try {
+ auto obj = Json::requireObject(doc);
+ loadExtraPackInfo(pack, obj);
+ } catch (const JSONValidationError& e) {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause();
+ }
+
+ m_parent->updateUi();
+}
+
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
{
auto& current = m_parent->getCurrent();
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index d460cff2..dd22407c 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -36,9 +36,11 @@ class ListModel : public QAbstractListModel {
void fetchMore(const QModelIndex& parent) override;
void refresh();
void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
+ void requestModInfo(ModPlatform::IndexedPack& current);
void requestModVersions(const ModPlatform::IndexedPack& current);
virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
+ virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {};
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0;
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
@@ -49,6 +51,8 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason);
+ void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack);
+
void versionRequestSucceeded(QJsonDocument doc, QString addonId);
protected slots:
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 6dd3a453..200fe59e 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -1,4 +1,40 @@
+// 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 "ModPage.h"
+#include "Application.h"
#include "ui_ModPage.h"
#include <QKeyEvent>
@@ -94,28 +130,6 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
if (!first.isValid()) { return; }
current = listModel->data(first, Qt::UserRole).value<ModPlatform::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 = [](ModPlatform::ModpackAuthor& author) -> QString {
- 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() << QString("Loading %1 mod versions").arg(debugName());
@@ -132,6 +146,13 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
updateSelectionButton();
}
+
+ if(!current.extraDataLoaded){
+ qDebug() << QString("Loading %1 mod info").arg(debugName());
+ listModel->requestModInfo(current);
+ }
+
+ updateUi();
}
void ModPage::onVersionSelectionChanged(QString data)
@@ -150,7 +171,8 @@ void ModPage::onModSelected()
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
- dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods));
+ bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
+ dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed));
}
updateSelectionButton();
@@ -175,7 +197,7 @@ void ModPage::updateModVersions(int prev_count)
bool valid = false;
for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loader, so passing it changes nothing.
- if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) {
+ if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) {
valid = true;
break;
}
@@ -207,3 +229,61 @@ void ModPage::updateSelectionButton()
ui->modSelectionButton->setText(tr("Deselect mod for download"));
}
}
+
+void ModPage::updateUi()
+{
+ 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 = [](ModPlatform::ModpackAuthor& author) -> QString {
+ 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(", ");
+ }
+
+
+ if(current.extraDataLoaded) {
+ if (!current.extraData.donate.isEmpty()) {
+ text += "<br><br>" + tr("Donate information: ");
+ auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
+ return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
+ };
+ QStringList donates;
+ for (auto& donate : current.extraData.donate) {
+ donates.append(donateToStr(donate));
+ }
+ text += donates.join(", ");
+ }
+
+ if (!current.extraData.issuesUrl.isEmpty()
+ || !current.extraData.sourceUrl.isEmpty()
+ || !current.extraData.wikiUrl.isEmpty()
+ || !current.extraData.discordUrl.isEmpty()) {
+ text += "<br><br>" + tr("External links:") + "<br>";
+ }
+
+ if (!current.extraData.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extraData.issuesUrl) + "<br>";
+ if (!current.extraData.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extraData.wikiUrl) + "<br>";
+ if (!current.extraData.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extraData.sourceUrl) + "<br>";
+ if (!current.extraData.discordUrl.isEmpty())
+ text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extraData.discordUrl) + "<br>";
+ }
+
+ text += "<hr>";
+
+ ui->packDescription->setHtml(text + current.description);
+}
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index eb89b0e2..cf00e16e 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -36,11 +36,14 @@ class ModPage : public QWidget, public BasePage {
void retranslate() override;
+ void updateUi();
+
auto shouldDisplay() const -> bool override = 0;
- virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0;
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
- auto apiProvider() const -> const ModAPI* { return api.get(); };
+ auto apiProvider() -> ModAPI* { return api.get(); };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
+ auto getDialog() const -> const ModDownloadDialog* { return dialog; }
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1);
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
index 26aa60af..004fdc57 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
@@ -43,8 +43,11 @@
#include "modplatform/atlauncher/ATLShareCode.h"
#include "Application.h"
-AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
- : QAbstractListModel(parent), m_mods(mods) {
+AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
+ : QAbstractListModel(parent)
+ , m_version(version)
+ , m_mods(mods)
+{
// fill mod index
for (int i = 0; i < m_mods.size(); i++) {
auto mod = m_mods.at(i);
@@ -97,6 +100,11 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const
return mod.description;
}
}
+ else if (role == Qt::ForegroundRole) {
+ if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) {
+ return QColor(QString("#%1").arg(m_version.colours[mod.colour]));
+ }
+ }
else if (role == Qt::CheckStateRole) {
if (index.column() == EnabledColumn) {
return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked;
@@ -223,7 +231,21 @@ void AtlOptionalModListModel::clearAll() {
}
void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) {
- setMod(mod, index, !m_selection[mod.name]);
+ auto enable = !m_selection[mod.name];
+
+ // If there is a warning for the mod, display that first (if we would be enabling the mod)
+ if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) {
+ auto message = QString("%1<br><br>%2")
+ .arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?"));
+
+ // fixme: avoid casting here
+ auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No);
+ if (result != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ setMod(mod, index, enable);
}
void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) {
@@ -287,12 +309,13 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool
}
}
-
-AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
- : QDialog(parent), ui(new Ui::AtlOptionalModDialog) {
+AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
+ : QDialog(parent)
+ , ui(new Ui::AtlOptionalModDialog)
+{
ui->setupUi(this);
- listModel = new AtlOptionalModListModel(this, mods);
+ listModel = new AtlOptionalModListModel(this, version, mods);
ui->treeView->setModel(listModel);
ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
index 953b288e..8e02444e 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
@@ -56,7 +56,7 @@ public:
DescriptionColumn,
};
- AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+ AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
QVector<QString> getResult();
@@ -86,7 +86,9 @@ private:
NetJob::Ptr m_jobPtr;
QByteArray m_response;
+ ATLauncher::PackVersion m_version;
QVector<ATLauncher::VersionMod> m_mods;
+
QMap<QString, bool> m_selection;
QMap<QString, int> m_index;
QMap<QString, QVector<QString>> m_dependants;
@@ -96,7 +98,7 @@ class AtlOptionalModDialog : public QDialog {
Q_OBJECT
public:
- AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+ AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
~AtlOptionalModDialog() override;
QVector<QString> getResult() {
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
index df9b9207..8de5211c 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
@@ -45,8 +45,12 @@
#include <BuildConfig.h>
-AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
- : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
+#include <QMessageBox>
+
+AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent)
+ : QWidget(parent)
+ , ui(new Ui::AtlPage)
+ , dialog(dialog)
{
ui->setupUi(this);
@@ -113,7 +117,7 @@ void AtlPage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion));
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion));
auto editedLogoName = selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
@@ -169,8 +173,9 @@ void AtlPage::onVersionSelectionChanged(QString data)
suggestCurrent();
}
-QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) {
- AtlOptionalModDialog optionalModDialog(this, mods);
+QVector<QString> AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
+{
+ AtlOptionalModDialog optionalModDialog(this, version, mods);
optionalModDialog.exec();
return optionalModDialog.getResult();
}
@@ -210,3 +215,8 @@ QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVers
vselect.exec();
return vselect.selectedVersion()->descriptor();
}
+
+void AtlPage::displayMessage(QString message)
+{
+ QMessageBox::information(this, tr("Installing"), message);
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
index c95b0127..aa6d5da1 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
@@ -84,7 +84,8 @@ private:
void suggestCurrent();
QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
- QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override;
+ QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
+ void displayMessage(QString message) override;
private slots:
void triggerSearch();
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
index 70759994..10d34218 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
@@ -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 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
@@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
}
-auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
+auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
{
- Q_UNUSED(loader);
+ Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer);
}
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h
index 27cbdb8c..445d0368 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h
@@ -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 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
@@ -55,7 +55,7 @@ class FlameModPage : public ModPage {
inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
- auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override;
+ auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index f97536e8..b9804681 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -57,6 +57,17 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
return QVariant();
}
+bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ int pos = index.row();
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid())
+ return false;
+
+ modpacks[pos] = value.value<Flame::IndexedPack>();
+
+ return true;
+}
+
void ListModel::logoLoaded(QString logo, QIcon out)
{
m_loadingLogos.removeAll(logo);
@@ -210,6 +221,11 @@ void Flame::ListModel::searchRequestFinished()
nextSearchOffset += 25;
searchState = CanPossiblyFetchMore;
}
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h
index 536f6add..cab666cc 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.h
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.h
@@ -34,6 +34,7 @@ public:
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool canFetchMore(const QModelIndex & parent) const override;
void fetchMore(const QModelIndex & parent) override;
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index ec774621..7d2ba2e2 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -107,41 +107,18 @@ void FlamePage::triggerSearch()
listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
}
-void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
+void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
- if (!first.isValid()) {
+ if (!curr.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
}
return;
}
- current = listModel->data(first, Qt::UserRole).value<Flame::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 = [](Flame::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);
+ current = listModel->data(curr, Qt::UserRole).value<Flame::IndexedPack>();
if (current.versionsLoaded == false) {
qDebug() << "Loading flame modpack versions";
@@ -150,7 +127,7 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response));
- QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] {
if (addonId != current.addonId) {
return; // wrong request
}
@@ -174,6 +151,16 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
}
+ QVariant current_updated;
+ current_updated.setValue(current);
+
+ if (!listModel->setData(curr, current_updated, Qt::UserRole))
+ qWarning() << "Failed to cache versions for the current pack!";
+
+ // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution.
+ if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) {
+ ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
+ }
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
@@ -188,6 +175,13 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
suggestCurrent();
}
+
+ // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution.
+ if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) {
+ ui->versionSelectionBox->addItem(tr("No version is available!"), -1);
+ }
+
+ updateUi();
}
void FlamePage::suggestCurrent()
@@ -196,12 +190,12 @@ void FlamePage::suggestCurrent()
return;
}
- if (selectedVersion.isEmpty()) {
+ if (selectedVersion.isEmpty() || selectedVersion == "-1") {
dialog->setSuggestedPack();
return;
}
- dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl,
@@ -217,3 +211,46 @@ void FlamePage::onVersionSelectionChanged(QString data)
selectedVersion = ui->versionSelectionBox->currentData().toString();
suggestCurrent();
}
+
+void FlamePage::updateUi()
+{
+ QString text = "";
+ QString name = current.name;
+
+ if (current.extra.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.extra.websiteUrl + "\">" + name + "</a>";
+ if (!current.authors.empty()) {
+ auto authorToStr = [](Flame::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(", ");
+ }
+
+ if(current.extraInfoLoaded) {
+ if (!current.extra.issuesUrl.isEmpty()
+ || !current.extra.sourceUrl.isEmpty()
+ || !current.extra.wikiUrl.isEmpty()) {
+ text += "<br><br>" + tr("External links:") + "<br>";
+ }
+
+ if (!current.extra.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>";
+ if (!current.extra.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>";
+ if (!current.extra.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>";
+ }
+
+ text += "<hr>";
+
+ ui->packDescription->setHtml(text + current.description);
+}
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h
index baac57c9..8130e416 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.h
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.h
@@ -79,6 +79,8 @@ public:
virtual bool shouldDisplay() const override;
void retranslate() override;
+ void updateUi();
+
void openedImpl() override;
bool eventFilter(QObject * watched, QEvent * event) override;
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui
index 6d8d8e10..aab16421 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.ui
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui
@@ -1,90 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>FlamePage</class>
- <widget class="QWidget" name="FlamePage">
- <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>
+ <class>FlamePage</class>
+ <widget class="QWidget" name="FlamePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Note: CurseForge allows creators to block access to third-party tools like PolyMC. As such, you may need to manually download some mods to be able to install a modpack.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <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>
- </widget>
- <tabstops>
- <tabstop>searchEdit</tabstop>
- <tabstop>searchButton</tabstop>
- <tabstop>packView</tabstop>
- <tabstop>packDescription</tabstop>
- <tabstop>sortByBox</tabstop>
- <tabstop>versionSelectionBox</tabstop>
- </tabstops>
- <resources/>
- <connections/>
+ </item>
+ <item row="2" column="0">
+ <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>
+ </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 row="3" column="0">
+ <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>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
</ui>
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
index 37244fed..ad15b6e6 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
@@ -122,10 +122,10 @@ void ListModel::requestFinished()
jobPtr.reset();
remainingPacks.clear();
- QJsonParseError parse_error;
+ QJsonParseError parse_error {};
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
@@ -169,7 +169,7 @@ void ListModel::packRequestFinished()
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
@@ -184,7 +184,7 @@ void ListModel::packRequestFinished()
catch (const JSONValidationError &e)
{
qDebug() << QString::fromUtf8(response);
- qWarning() << "Error while reading pack manifest from FTB: " << e.cause();
+ qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause();
return;
}
@@ -192,7 +192,7 @@ void ListModel::packRequestFinished()
// ignore those "dud" packs.
if (pack.versions.empty())
{
- qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions";
+ qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions";
}
else
{
@@ -270,7 +270,7 @@ void ListModel::requestLogo(QString logo, QString url)
bool stale = entry->isStale();
- NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo), APPLICATION->network());
+ NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
index 63b944c4..2d135e59 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -1,3 +1,38 @@
+// 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 "ListModel.h"
#include "Application.h"
@@ -133,7 +168,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const
((ListModel *)this)->requestLogo(pack.logo);
return icon;
}
- else if(role == Qt::TextColorRole)
+ else if(role == Qt::ForegroundRole)
{
if(pack.broken)
{
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
index 27a12cda..6ffbd312 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -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
@@ -151,7 +152,7 @@ void Page::openedImpl()
ftbFetchTask->fetch();
ftbPrivatePacks->load();
- ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList());
+ ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().values());
initialized = true;
}
suggestCurrent();
@@ -175,7 +176,7 @@ void Page::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName;
if(selected.logo.toLower().startsWith("ftb"))
{
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
index 15e5d432..f4231d8d 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
@@ -25,7 +25,7 @@
<widget class="QTreeView" name="publicPackList">
<property name="maximumSize">
<size>
- <width>250</width>
+ <width>16777215</width>
<height>16777215</height>
</size>
</property>
@@ -51,7 +51,7 @@
<widget class="QTreeView" name="thirdPartyPackList">
<property name="maximumSize">
<size>
- <width>250</width>
+ <width>16777215</width>
<height>16777215</height>
</size>
</property>
@@ -71,7 +71,7 @@
<widget class="QTreeView" name="privatePackList">
<property name="maximumSize">
<size>
- <width>250</width>
+ <width>16777215</width>
<height>16777215</height>
</size>
</property>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
index 1d9f4d60..af92e63e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp
@@ -30,6 +30,11 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
Modrinth::loadIndexedPack(m, obj);
}
+void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
+{
+ Modrinth::loadExtraPackData(m, obj);
+}
+
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
index ae7b0bdd..386897fd 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h
@@ -31,6 +31,7 @@ class ListModel : public ModPlatform::ListModel {
private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
index d3a1f859..5fa00b9b 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp
@@ -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 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
@@ -61,9 +61,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected);
}
-auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
+auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
{
- auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader);
+ auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders);
auto loaderCompatible = false;
for (auto remoteLoader : ver.loaders)
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
index b1e72bfe..94985f63 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h
@@ -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 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
@@ -55,7 +55,7 @@ class ModrinthModPage : public ModPage {
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 validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 7cacf37a..3633d575 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * 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
@@ -86,6 +87,7 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.iconName)) {
auto icon = m_logoMap.value(pack.iconName);
+ // FIXME: This doesn't really belong here, but Qt doesn't offer a good way right now ;(
auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48));
return icon_scaled;
@@ -102,6 +104,17 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
return {};
}
+bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ int pos = index.row();
+ if (pos >= modpacks.size() || pos < 0 || !index.isValid())
+ return false;
+
+ modpacks[pos] = value.value<Modrinth::Modpack>();
+
+ return true;
+}
+
void ModpackListModel::performPaginatedSearch()
{
// TODO: Move to standalone API
@@ -159,15 +172,15 @@ static auto sortFromIndex(int index) -> QString
{
switch(index){
default:
- case 1:
+ case 0:
return "relevance";
- case 2:
+ case 1:
return "downloads";
- case 3:
+ case 2:
return "follows";
- case 4:
+ case 3:
return "newest";
- case 5:
+ case 4:
return "updated";
}
@@ -277,6 +290,10 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
searchState = CanPossiblyFetchMore;
}
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
index 14aa6747..6f33e11e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h
@@ -63,6 +63,7 @@ class ModpackListModel : public QAbstractListModel {
/* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 9bd24b57..df29c0c3 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -101,18 +101,18 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)
return QObject::eventFilter(watched, event);
}
-void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
- if (!first.isValid()) {
+ if (!curr.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
}
return;
}
- current = m_model->data(first, Qt::UserRole).value<Modrinth::Modpack>();
+ current = m_model->data(curr, Qt::UserRole).value<Modrinth::Modpack>();
auto name = current.name;
if (!current.extraInfoLoaded) {
@@ -125,7 +125,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
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] {
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] {
if (id != current.id) {
return; // wrong request?
}
@@ -149,6 +149,13 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
}
updateUI();
+
+ QVariant current_updated;
+ current_updated.setValue(current);
+
+ if (!m_model->setData(curr, current_updated, Qt::UserRole))
+ qWarning() << "Failed to cache extra info for the current pack!";
+
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
@@ -170,7 +177,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
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] {
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] {
if (id != current.id) {
return; // wrong request?
}
@@ -192,9 +199,18 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
}
for (auto version : current.versions) {
- ui->versionSelectionBox->addItem(version.version, QVariant(version.id));
+ if (!version.name.contains(version.version))
+ ui->versionSelectionBox->addItem(QString("%1 — %2").arg(version.name, version.version), QVariant(version.id));
+ else
+ ui->versionSelectionBox->addItem(version.name, QVariant(version.id));
}
+ QVariant current_updated;
+ current_updated.setValue(current);
+
+ if (!m_model->setData(curr, current_updated, Qt::UserRole))
+ qWarning() << "Failed to cache versions for the current pack!";
+
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
@@ -205,7 +221,10 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second)
} else {
for (auto version : current.versions) {
- ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id));
+ if (!version.name.contains(version.version))
+ ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id));
+ else
+ ui->versionSelectionBox->addItem(version.name, QVariant(version.id));
}
suggestCurrent();
@@ -224,7 +243,37 @@ void ModrinthPage::updateUI()
// 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>";
+ if (current.extraInfoLoaded) {
+ if (!current.extra.donate.isEmpty()) {
+ text += "<br><br>" + tr("Donate information: ");
+ auto donateToStr = [](Modrinth::DonationData& donate) -> QString {
+ return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
+ };
+ QStringList donates;
+ for (auto& donate : current.extra.donate) {
+ donates.append(donateToStr(donate));
+ }
+ text += donates.join(", ");
+ }
+
+ if (!current.extra.issuesUrl.isEmpty()
+ || !current.extra.sourceUrl.isEmpty()
+ || !current.extra.wikiUrl.isEmpty()
+ || !current.extra.discordUrl.isEmpty()) {
+ text += "<br><br>" + tr("External links:") + "<br>";
+ }
+
+ if (!current.extra.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current.extra.issuesUrl) + "<br>";
+ if (!current.extra.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current.extra.wikiUrl) + "<br>";
+ if (!current.extra.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current.extra.sourceUrl) + "<br>";
+ if (!current.extra.discordUrl.isEmpty())
+ text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current.extra.discordUrl) + "<br>";
+ }
+
+ text += "<hr>";
HoeDown h;
text += h.process(current.extra.body.toUtf8());
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
index 4fb59cdf..6a34701d 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>837</width>
- <height>685</height>
+ <width>800</width>
+ <height>600</height>
</rect>
</property>
<layout class="QVBoxLayout">
@@ -24,6 +24,9 @@
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item>
@@ -60,9 +63,6 @@
<height>48</height>
</size>
</property>
- <property name="uniformItemSizes">
- <bool>true</bool>
- </property>
</widget>
</item>
<item>
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
index 9c9d1e75..742f4f2a 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
@@ -217,6 +217,11 @@ void Technic::ListModel::searchRequestFinished()
return;
}
searchState = Finished;
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (newList.size() == 0)
+ return;
+
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
modpacks.append(newList);
endInsertRows();
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui
index ca6a9b7e..15bf645f 100644
--- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui
@@ -60,7 +60,11 @@
</widget>
</item>
<item row="0" column="1">
- <widget class="QTextBrowser" name="packDescription"/>
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</item>