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