Merge branch 'develop' into feature/sparkle-mac

# Conflicts:
#	.github/workflows/build.yml
This commit is contained in:
Sefa Eyeoglu
2022-07-10 19:38:30 +02:00
374 changed files with 12410 additions and 6477 deletions

View File

@ -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()

View File

@ -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();

View File

@ -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>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<widget class="QLabel" name="pasteServiceTypeLabel">
<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>Paste Service &amp;Type</string>
</property>
<property name="scaledContents">
<bool>false</bool>
<property name="buddy">
<cstring>pasteTypeComboBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="urlChoices">
<property name="editable">
<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>
<property name="insertPolicy">
<enum>QComboBox::NoInsert</enum>
</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="baseURLNote">
<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>Note: you probably want to change or clear the Base URL after changing the paste service type.</string>
</property>
<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="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>
</widget>
</item>
<item>
<widget class="QLineEdit" name="metaURL">
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Enter a custom URL for meta here.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
@ -95,19 +130,32 @@
</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="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">
@ -147,6 +195,51 @@
</layout>
</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">
@ -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>

View File

@ -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);

View File

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

View File

@ -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();

View File

@ -150,6 +150,35 @@
<string>Java Runtime</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="1">
<widget class="QPushButton" name="javaDetectBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Auto-detect...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelJVMArgs">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>JVM arguments:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelJavaPath">
<property name="sizePolicy">
@ -166,40 +195,8 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelJVMArgs">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>J&amp;VM arguments:</string>
</property>
<property name="buddy">
<cstring>jvmArgsTextBox</cstring>
</property>
</widget>
</item>
<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;Skip Java compatibility checks</string>
</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>

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -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()

View File

@ -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.

View File

@ -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,28 +108,15 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods:</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</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="2" column="2">
<widget class="QToolButton" name="iconsDirBrowseBtn">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
<item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelIconsDir">
@ -147,10 +128,58 @@
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="iconsDirBrowseBtn">
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<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="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods:</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
</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>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>

View File

@ -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());

View File

@ -134,6 +134,45 @@
</layout>
</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">
@ -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/>

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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();
}

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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;
};

View File

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

View File

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

View File

@ -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/>

View File

@ -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);

View File

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

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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);

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
: ExternalResourcesPage(inst, mods, parent)
{
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)
{
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);
// 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"));
ui->actionAdd->setText(tr("Add .jar"));
ui->actionAdd->setToolTip(tr("Add mods via local file"));
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods);
}
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);
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);
}
void ModFolderPage::modItemActivated(const QModelIndex&)
{
if(!m_controlsEnabled) {
return;
}
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)
void ModFolderPage::installMods()
{
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) {
if (!m_controlsEnabled)
return;
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 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()
{
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) {
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();
m_model->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;
}
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
Mod &m = m_mods->operator[](row);
ui->frame->updateWithMod(m);
}

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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();
public:
explicit ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = nullptr);
virtual ~ModFolderPage() = default;
void setFilter(const QString & filter)
{
m_fileSelectionFilter = filter;
}
void setFilter(const QString& filter) { m_fileSelectionFilter = filter; }
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"; }
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;
protected:
BaseInstance *m_inst = nullptr;
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);
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;
};

View File

@ -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");
}
};

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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);

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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)
{

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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);

View File

@ -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;

View File

@ -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");
}
};

View File

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

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -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())
{

View File

@ -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();

View File

@ -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:

View File

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

View File

@ -36,11 +36,14 @@ class ModPage : public QWidget, public BasePage {
void retranslate() override;
auto shouldDisplay() const -> bool override = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0;
void updateUi();
auto apiProvider() const -> const ModAPI* { return api.get(); };
auto shouldDisplay() const -> bool override = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
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);

View File

@ -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);

View File

@ -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() {

View File

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

View File

@ -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();

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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);
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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;
};

View File

@ -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();

View File

@ -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;

View File

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

View File

@ -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;

View File

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

View File

@ -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();

View File

@ -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)
{

View File

@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -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"))
{

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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)

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 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;
};

View File

@ -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();

View File

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

View File

@ -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());

View File

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

View File

@ -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();

View File

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