Merge branch 'develop' into feature/java-downloader

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad
2023-07-16 12:32:31 +01:00
committed by GitHub
879 changed files with 49762 additions and 21479 deletions

View File

@ -35,15 +35,16 @@
#pragma once
#include <QString>
#include <QIcon>
#include <QString>
#include <functional>
#include <memory>
#include "BasePageContainer.h"
class BasePage
{
public:
class BasePage {
public:
using updateExtraInfoFunc = std::function<void(QString, QString)>;
virtual ~BasePage() {}
virtual QString id() const = 0;
virtual QString displayName() const = 0;
@ -63,17 +64,16 @@ public:
}
virtual void openedImpl() {}
virtual void closedImpl() {}
virtual void setParentContainer(BasePageContainer * container)
{
m_container = container;
};
virtual void retranslate() { }
virtual void setParentContainer(BasePageContainer* container) { m_container = container; };
virtual void retranslate() {}
public:
public:
int stackIndex = -1;
int listIndex = -1;
protected:
BasePageContainer * m_container = nullptr;
updateExtraInfoFunc updateExtraInfo;
protected:
BasePageContainer* m_container = nullptr;
bool isOpened = false;
};

View File

@ -1,10 +1,13 @@
#pragma once
class BasePage;
class BasePageContainer
{
public:
virtual ~BasePageContainer(){};
virtual bool selectPage(QString pageId) = 0;
virtual BasePage* getPage(QString pageId) { return nullptr; };
virtual void refreshContainer() = 0;
virtual bool requestClose() = 0;
};

View File

@ -1,9 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - 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>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -80,6 +81,8 @@ APIPage::APIPage(QWidget *parent) :
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());
// NOTE: this allows http://, but we replace that with https later anyway
ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL));
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID));
ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey));
@ -147,6 +150,8 @@ void APIPage::loadSettings()
ui->metaURL->setText(metaURL);
QString flameKey = s->get("FlameKeyOverride").toString();
ui->flameKey->setText(flameKey);
QString modrinthToken = s->get("ModrinthToken").toString();
ui->modrinthToken->setText(modrinthToken);
QString customUserAgent = s->get("UserAgentOverride").toString();
ui->userAgentLineEdit->setText(customUserAgent);
}
@ -160,7 +165,7 @@ void APIPage::applySettings()
QString msaClientID = ui->msaClientID->text();
s->set("MSAClientIDOverride", msaClientID);
QUrl metaURL = ui->metaURL->text();
QUrl metaURL(ui->metaURL->text());
// Add required trailing slash
if (!metaURL.isEmpty() && !metaURL.path().endsWith('/'))
{
@ -174,9 +179,11 @@ void APIPage::applySettings()
metaURL.setScheme("https");
}
s->set("MetaURLOverride", metaURL);
s->set("MetaURLOverride", metaURL.toString());
QString flameKey = ui->flameKey->text();
s->set("FlameKeyOverride", flameKey);
QString modrinthToken = ui->modrinthToken->text();
s->set("ModrinthToken", modrinthToken);
s->set("UserAgentOverride", ui->userAgentLineEdit->text());
}

View File

@ -179,7 +179,7 @@
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Enter a custom client ID for Microsoft Authentication here. </string>
<string>Enter a custom client ID for Microsoft Authentication here.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
@ -195,6 +195,51 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_modrinth">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>&amp;Modrinth API</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/api-spec/#section/Authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Enter a custom API token for Modrinth 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="modrinthToken">
<property name="enabled">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>(None)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_flame">
<property name="enabled">
@ -203,16 +248,16 @@
<property name="title">
<string>&amp;CurseForge Core API</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<widget class="QLabel" name="label_10">
<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">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Enter a custom API Key for CurseForge here.</string>
</property>

View File

@ -59,9 +59,8 @@ JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage)
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
ui->maxMemSpinBox->setMaximum(sysMiB);
loadSettings();
updateThresholds();
}
JavaPage::~JavaPage()
@ -183,6 +182,11 @@ void JavaPage::on_javaDownloadBtn_clicked()
JavaDownloader::showPrompts(this);
}
void JavaPage::on_maxMemSpinBox_valueChanged(int i)
{
updateThresholds();
}
void JavaPage::checkerFinished()
{
checker.reset();
@ -192,3 +196,29 @@ void JavaPage::retranslate()
{
ui->retranslateUi(this);
}
void JavaPage::updateThresholds()
{
auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
unsigned int maxMem = ui->maxMemSpinBox->value();
QString iconName;
if (maxMem >= sysMiB) {
iconName = "status-bad";
ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity."));
} else if (maxMem > (sysMiB * 0.9)) {
iconName = "status-yellow";
ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity."));
} else {
iconName = "status-good";
ui->labelMaxMemIcon->setToolTip("");
}
{
auto height = ui->labelMaxMemIcon->fontInfo().pixelSize();
QIcon icon = APPLICATION->getThemedIcon(iconName);
QPixmap pix = icon.pixmap(height, height);
ui->labelMaxMemIcon->setPixmap(pix);
}
}

View File

@ -76,6 +76,8 @@ public:
bool apply() override;
void retranslate() override;
void updateThresholds();
private:
void applySettings();
void loadSettings();
@ -86,6 +88,7 @@ slots:
void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked();
void on_javaDownloadBtn_clicked();
void on_maxMemSpinBox_valueChanged(int i);
void checkerFinished();
private:

View File

@ -44,39 +44,7 @@
<property name="title">
<string>Memory</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelMinMem">
<property name="text">
<string>&amp;Minimum memory allocation:</string>
</property>
<property name="buddy">
<cstring>minMemSpinBox</cstring>
</property>
</widget>
</item>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0,0">
<item row="1" column="0">
<widget class="QLabel" name="labelMaxMem">
<property name="text">
@ -87,28 +55,6 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="minMemSpinBox">
<property name="toolTip">
<string>The amount of memory Minecraft is started with.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>256</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelPermGen">
<property name="text">
@ -119,7 +65,61 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="0" column="0">
<widget class="QLabel" name="labelMinMem">
<property name="text">
<string>&amp;Minimum memory allocation:</string>
</property>
<property name="buddy">
<cstring>minMemSpinBox</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QSpinBox" name="minMemSpinBox">
<property name="toolTip">
<string>The amount of memory Minecraft is started with.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>256</number>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="permGenSpinBox">
<property name="toolTip">
<string>The amount of memory available to store loaded Java classes.</string>
@ -141,6 +141,16 @@
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QLabel" name="labelMaxMemIcon">
<property name="text">
<string/>
</property>
<property name="buddy">
<cstring>maxMemSpinBox</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2022 dada513 <dada513@protonmail.com>
* Copyright (C) 2022 Tayou <tayou@gmx.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
@ -43,13 +44,13 @@
#include <QTextCharFormat>
#include <QMenuBar>
#include "updater/UpdateChecker.h"
#include "settings/SettingsObject.h"
#include <FileSystem.h>
#include "Application.h"
#include "BuildConfig.h"
#include "DesktopServices.h"
#include "ui/themes/ITheme.h"
#include "updater/ExternalUpdater.h"
#include <QApplication>
#include <QProcess>
@ -78,32 +79,12 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch
m_languageModel = APPLICATION->translations();
loadSettings();
if(BuildConfig.UPDATER_ENABLED)
{
QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList);
ui->updateSettingsBox->setHidden(!APPLICATION->updater());
if (APPLICATION->updateChecker()->hasChannels())
{
refreshUpdateChannelList();
}
else
{
APPLICATION->updateChecker()->updateChanList(false);
}
if (APPLICATION->updateChecker()->getExternalUpdater())
{
ui->updateChannelComboBox->setVisible(false);
ui->updateChannelDescLabel->setVisible(false);
ui->updateChannelLabel->setVisible(false);
}
}
else
{
ui->updateSettingsBox->setHidden(true);
}
connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged);
}
LauncherPage::~LauncherPage()
@ -143,7 +124,7 @@ void LauncherPage::on_instDirBrowseBtn_clicked()
ui->instDirTextBox->setText(cooked_dir);
}
}
else if(APPLICATION->isFlatpak() && raw_dir.startsWith("/run/user"))
else if(DesktopServices::isFlatpak() && raw_dir.startsWith("/run/user"))
{
QMessageBox warning;
warning.setText(tr("You're trying to specify an instance folder "
@ -159,8 +140,8 @@ void LauncherPage::on_instDirBrowseBtn_clicked()
if (result == QMessageBox::Ok)
{
ui->instDirTextBox->setText(cooked_dir);
}
}
}
}
else
{
ui->instDirTextBox->setText(cooked_dir);
@ -179,6 +160,7 @@ void LauncherPage::on_iconsDirBrowseBtn_clicked()
ui->iconsDirTextBox->setText(cooked_dir);
}
}
void LauncherPage::on_modsDirBrowseBtn_clicked()
{
QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text());
@ -191,143 +173,30 @@ void LauncherPage::on_modsDirBrowseBtn_clicked()
}
}
void LauncherPage::on_downloadsDirBrowseBtn_clicked()
{
QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Downloads Folder"), ui->downloadsDirTextBox->text());
if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
{
QString cooked_dir = FS::NormalizePath(raw_dir);
ui->downloadsDirTextBox->setText(cooked_dir);
}
}
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
// we don't need to update the
// description label constantly.
QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
SLOT(updateChannelSelectionChanged(int)));
QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
ui->updateChannelComboBox->clear();
int selection = -1;
for (int i = 0; i < channelList.count(); i++)
{
UpdateChecker::ChannelListEntry entry = channelList.at(i);
// When it comes to selection, we'll rely on the indexes of a channel entry being the
// same in the
// combo box as it is in the update checker's channel list.
// This probably isn't very safe, but the channel list doesn't change often enough (or
// at all) for
// this to be a big deal. Hope it doesn't break...
ui->updateChannelComboBox->addItem(entry.name);
// If the update channel we just added was the selected one, set the current index in
// the combo box to it.
if (entry.id == m_currentUpdateChannel)
{
qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel;
selection = i;
}
}
ui->updateChannelComboBox->setCurrentIndex(selection);
// Start listening for selection changes again and update the description label.
QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
SLOT(updateChannelSelectionChanged(int)));
refreshUpdateChannelDesc();
// Now that we've updated the channel list, we can enable the combo box.
// It starts off disabled so that if the channel list hasn't been loaded, it will be
// disabled.
ui->updateChannelComboBox->setEnabled(true);
}
void LauncherPage::updateChannelSelectionChanged(int index)
{
refreshUpdateChannelDesc();
}
void LauncherPage::refreshUpdateChannelDesc()
{
// Get the channel list.
QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
int selectedIndex = ui->updateChannelComboBox->currentIndex();
if (selectedIndex < 0)
{
return;
}
if (selectedIndex < channelList.count())
{
// Find the channel list entry with the given index.
UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex);
// Set the description text.
ui->updateChannelDescLabel->setText(selected.description);
// Set the currently selected channel ID.
m_currentUpdateChannel = selected.id;
}
}
void LauncherPage::applySettings()
{
auto s = APPLICATION->settings();
// Updates
if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater())
if (APPLICATION->updater())
{
APPLICATION->updateChecker()->getExternalUpdater()->setAutomaticallyChecksForUpdates(
ui->autoUpdateCheckBox->isChecked());
}
else
{
s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
}
s->set("UpdateChannel", m_currentUpdateChannel);
auto original = s->get("IconTheme").toString();
//FIXME: make generic
switch (ui->themeComboBox->currentIndex())
{
case 0:
s->set("IconTheme", "pe_colored");
break;
case 1:
s->set("IconTheme", "pe_light");
break;
case 2:
s->set("IconTheme", "pe_dark");
break;
case 3:
s->set("IconTheme", "pe_blue");
break;
case 4:
s->set("IconTheme", "OSX");
break;
case 5:
s->set("IconTheme", "iOS");
break;
case 6:
s->set("IconTheme", "flat");
break;
case 7:
s->set("IconTheme", "multimc");
break;
case 8:
s->set("IconTheme", "custom");
break;
}
if(original != s->get("IconTheme"))
{
APPLICATION->setIconTheme(s->get("IconTheme").toString());
}
auto originalAppTheme = s->get("ApplicationTheme").toString();
auto newAppTheme = ui->themeComboBoxColors->currentData().toString();
if(originalAppTheme != newAppTheme)
{
s->set("ApplicationTheme", newAppTheme);
APPLICATION->setApplicationTheme(newAppTheme, false);
APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked());
}
s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked());
@ -347,6 +216,8 @@ void LauncherPage::applySettings()
s->set("InstanceDir", ui->instDirTextBox->text());
s->set("CentralModsDir", ui->modsDirTextBox->text());
s->set("IconsDir", ui->iconsDirTextBox->text());
s->set("DownloadsDir", ui->downloadsDirTextBox->text());
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode)
@ -367,70 +238,11 @@ void LauncherPage::loadSettings()
{
auto s = APPLICATION->settings();
// Updates
if (BuildConfig.UPDATER_ENABLED && APPLICATION->updateChecker()->getExternalUpdater())
if (APPLICATION->updater())
{
ui->autoUpdateCheckBox->setChecked(
APPLICATION->updateChecker()->getExternalUpdater()->getAutomaticallyChecksForUpdates());
}
else
{
ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates());
}
m_currentUpdateChannel = s->get("UpdateChannel").toString();
//FIXME: make generic
auto theme = s->get("IconTheme").toString();
if (theme == "pe_colored")
{
ui->themeComboBox->setCurrentIndex(0);
}
else if (theme == "pe_light")
{
ui->themeComboBox->setCurrentIndex(1);
}
else if (theme == "pe_dark")
{
ui->themeComboBox->setCurrentIndex(2);
}
else if (theme == "pe_blue")
{
ui->themeComboBox->setCurrentIndex(3);
}
else if (theme == "OSX")
{
ui->themeComboBox->setCurrentIndex(4);
}
else if (theme == "iOS")
{
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);
}
{
auto currentTheme = s->get("ApplicationTheme").toString();
auto themes = APPLICATION->getValidApplicationThemes();
int idx = 0;
for(auto &theme: themes)
{
ui->themeComboBoxColors->addItem(theme->name(), theme->id());
if(currentTheme == theme->id())
{
ui->themeComboBoxColors->setCurrentIndex(idx);
}
idx++;
}
}
// Toolbar/menu bar settings (not applicable if native menu bar is present)
ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar());
@ -462,6 +274,8 @@ void LauncherPage::loadSettings()
ui->instDirTextBox->setText(s->get("InstanceDir").toString());
ui->modsDirTextBox->setText(s->get("CentralModsDir").toString());
ui->iconsDirTextBox->setText(s->get("IconsDir").toString());
ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString());
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
QString sortMode = s->get("InstSortMode").toString();

View File

@ -88,25 +88,14 @@ slots:
void on_instDirBrowseBtn_clicked();
void on_modsDirBrowseBtn_clicked();
void on_iconsDirBrowseBtn_clicked();
void on_downloadsDirBrowseBtn_clicked();
void on_metadataDisableBtn_clicked();
/*!
* Updates the list of update channels in the combo box.
*/
void refreshUpdateChannelList();
/*!
* Updates the channel description label.
*/
void refreshUpdateChannelDesc();
/*!
* Updates the font preview
*/
void refreshFontPreview();
void updateChannelSelectionChanged(int index);
private:
Ui::LauncherPage *ui;

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>514</width>
<width>511</width>
<height>629</height>
</rect>
</property>
@ -58,33 +58,6 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="updateChannelLabel">
<property name="text">
<string>Up&amp;date Channel:</string>
</property>
<property name="buddy">
<cstring>updateChannelComboBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="updateChannelComboBox">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="updateChannelDescLabel">
<property name="text">
<string>No channel selected.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -94,6 +67,45 @@
<string>Folders</string>
</property>
<layout class="QGridLayout" name="foldersBoxLayout">
<item row="3" column="0">
<widget class="QLabel" name="labelDownloadsDir">
<property name="text">
<string>&amp;Downloads:</string>
</property>
<property name="buddy">
<cstring>downloadsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>I&amp;nstances:</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="downloadsDirBrowseBtn">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="modsDirBrowseBtn">
<property name="text">
@ -101,6 +113,16 @@
</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="0" column="2">
<widget class="QToolButton" name="instDirBrowseBtn">
<property name="text">
@ -115,9 +137,6 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelIconsDir">
<property name="text">
@ -128,29 +147,13 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<item row="4" column="1" colspan="2">
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
<property name="toolTip">
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
</property>
<property name="text">
<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>
<string>Check downloads folder recursively</string>
</property>
</widget>
</item>
@ -166,10 +169,10 @@
<item>
<widget class="QCheckBox" name="metadataDisableBtn">
<property name="toolTip">
<string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string>
<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>
<string>Disable using metadata for mods</string>
</property>
</widget>
</item>
@ -243,97 +246,9 @@
<property name="title">
<string>Theme</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Icons</string>
</property>
<property name="buddy">
<cstring>themeComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="themeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<item>
<property name="text">
<string>Simple (Colored Icons)</string>
</property>
</item>
<item>
<property name="text">
<string>Simple (Light Icons)</string>
</property>
</item>
<item>
<property name="text">
<string>Simple (Dark Icons)</string>
</property>
</item>
<item>
<property name="text">
<string>Simple (Blue Icons)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">OSX</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">iOS</string>
</property>
</item>
<item>
<property name="text">
<string>Flat</string>
</property>
</item>
<item>
<property name="text">
<string>Legacy</string>
</property>
</item>
<item>
<property name="text">
<string>Custom</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Colors</string>
</property>
<property name="buddy">
<cstring>themeComboBoxColors</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="themeComboBoxColors">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="ThemeCustomizationWidget" name="themeCustomizationWidget" native="true"/>
</item>
</layout>
</widget>
@ -392,21 +307,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
<string>Show console while the game is &amp;running?</string>
<string>Show console while the game is &amp;running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
<string>&amp;Automatically close console when the game quits?</string>
<string>&amp;Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
<string>Show console when the game &amp;crashes?</string>
<string>Show console when the game &amp;crashes</string>
</property>
</widget>
</item>
@ -517,10 +432,17 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ThemeCustomizationWidget</class>
<extends>QWidget</extends>
<header>ui/widgets/ThemeCustomizationWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
<tabstop>updateChannelComboBox</tabstop>
<tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop>
@ -529,8 +451,6 @@
<tabstop>iconsDirBrowseBtn</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop>
<tabstop>themeComboBox</tabstop>
<tabstop>themeComboBoxColors</tabstop>
<tabstop>showConsoleCheck</tabstop>
<tabstop>autoCloseConsoleCheck</tabstop>
<tabstop>showConsoleErrorCheck</tabstop>

View File

@ -46,7 +46,6 @@
MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage)
{
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
loadSettings();
updateCheckboxStuff();
}

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>936</width>
<height>1134</height>
<height>541</height>
</rect>
</property>
<property name="sizePolicy">
@ -39,7 +39,7 @@
</property>
<widget class="QWidget" name="minecraftTab">
<attribute name="title">
<string notr="true">Minecraft</string>
<string notr="true">General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
@ -51,7 +51,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
<string>Start Minecraft &amp;maximized?</string>
<string>Start Minecraft &amp;maximized</string>
</property>
</widget>
</item>
@ -111,68 +111,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
<property name="title">
<string>Native library workarounds</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="useNativeGLFWCheck">
<property name="text">
<string>Use system installation of &amp;GLFW</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="useNativeOpenALCheck">
<property name="text">
<string>Use system installation of &amp;OpenAL</string>
</property>
</widget>
</item>
</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">
@ -247,6 +185,88 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tweaks</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
<property name="title">
<string>Native library workarounds</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QCheckBox" name="useNativeGLFWCheck">
<property name="text">
<string>Use system installation of &amp;GLFW</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="useNativeOpenALCheck">
<property name="text">
<string>Use system installation of &amp;OpenAL</string>
</property>
</widget>
</item>
</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>
<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>
@ -255,11 +275,6 @@
<tabstop>maximizedCheckBox</tabstop>
<tabstop>windowWidthSpinBox</tabstop>
<tabstop>windowHeightSpinBox</tabstop>
<tabstop>useNativeGLFWCheck</tabstop>
<tabstop>useNativeOpenALCheck</tabstop>
<tabstop>enableFeralGamemodeCheck</tabstop>
<tabstop>enableMangoHud</tabstop>
<tabstop>useDiscreteGpuCheck</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -1,4 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ExternalResourcesPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ExternalResourcesPage.h"
#include "DesktopServices.h"
@ -8,14 +44,13 @@
#include <QKeyEvent>
#include <QMenu>
#include <algorithm>
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
{
ui->setupUi(this);
ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning());
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
m_filterModel = model->createFilterProxyModel(this);
@ -25,6 +60,8 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
m_filterModel->setSourceModel(m_model.get());
m_filterModel->setFilterKeyColumn(-1);
ui->treeView->setModel(m_filterModel);
// must come after setModel
ui->treeView->setResizeModes(m_model->columnResizeModes());
ui->treeView->installEventFilter(this);
ui->treeView->sortByColumn(1, Qt::AscendingOrder);
@ -44,8 +81,21 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
auto selection_model = ui->treeView->selectionModel();
connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current);
auto updateExtra = [this]() {
if (updateExtraInfo)
updateExtraInfo(id(), extraHeaderInfoString());
};
connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra);
connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged);
connect(m_instance, &BaseInstance::runningStatusChanged, this, &ExternalResourcesPage::runningStateChanged);
auto viewHeader = ui->treeView->header();
viewHeader->setContextMenuPolicy(Qt::CustomContextMenu);
connect(viewHeader, &QHeaderView::customContextMenuRequested, this, &ExternalResourcesPage::ShowHeaderContextMenu);
m_model->loadHiddenColumns(ui->treeView);
}
ExternalResourcesPage::~ExternalResourcesPage()
@ -67,14 +117,31 @@ void ExternalResourcesPage::ShowContextMenu(const QPoint& pos)
delete menu;
}
void ExternalResourcesPage::ShowHeaderContextMenu(const QPoint& pos)
{
auto menu = m_model->createHeaderContextMenu(ui->treeView);
menu->exec(ui->treeView->mapToGlobal(pos));
menu->deleteLater();
}
void ExternalResourcesPage::openedImpl()
{
m_model->startWatching();
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
if (!APPLICATION->settings()->contains(setting_name))
m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name);
else
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
}
void ExternalResourcesPage::closedImpl()
{
m_model->stopWatching();
m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState());
}
void ExternalResourcesPage::retranslate()
@ -84,11 +151,7 @@ void ExternalResourcesPage::retranslate()
void ExternalResourcesPage::itemActivated(const QModelIndex&)
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
}
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
@ -97,14 +160,6 @@ void ExternalResourcesPage::filterTextChanged(const QString& newContents)
m_filterModel->setFilterRegularExpression(m_viewFilter);
}
void ExternalResourcesPage::runningStateChanged(bool running)
{
if (m_controlsEnabled == !running)
return;
m_controlsEnabled = !running;
}
bool ExternalResourcesPage::shouldDisplay() const
{
return true;
@ -129,7 +184,7 @@ 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);
@ -139,10 +194,6 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* 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());
@ -156,27 +207,80 @@ void ExternalResourcesPage::addItem()
void ExternalResourcesPage::removeItem()
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
int count = 0;
bool folder = false;
for (auto& i : selection.indexes()) {
if (i.column() == 0) {
count++;
// if a folder is selected, show the confirmation dialog
if (m_model->at(i.row()).fileinfo().isDir())
folder = true;
}
}
QString text;
bool multiple = count > 1;
if (multiple) {
text = tr("You are about to remove %1 items.\n"
"This may be permanent and they will be gone from the folder.\n\n"
"Are you sure?")
.arg(count);
} else if (folder) {
text = tr("You are about to remove the folder \"%1\".\n"
"This may be permanent and it will be gone from the parent folder.\n\n"
"Are you sure?")
.arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName());
}
if (!text.isEmpty()) {
auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), text, QMessageBox::Warning,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
removeItems(selection);
}
void ExternalResourcesPage::removeItems(const QItemSelection& selection)
{
if (m_instance != nullptr && m_instance->isRunning()) {
auto response = CustomMessageBox::selectable(this, "Confirm Delete",
"If you remove this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
m_model->deleteResources(selection.indexes());
}
void ExternalResourcesPage::enableItem()
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
}
void ExternalResourcesPage::disableItem()
{
if (!m_controlsEnabled)
return;
if (m_instance != nullptr && m_instance->isRunning()) {
auto response = CustomMessageBox::selectable(this, "Confirm disable",
"If you disable this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
}
@ -207,7 +311,15 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const
int row = sourceCurrent.row();
Resource const& resource = m_model->at(row);
ui->frame->updateWithResource(resource);
return true;
}
QString ExternalResourcesPage::extraHeaderInfoString()
{
if (ui && ui->treeView && ui->treeView->selectionModel()) {
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0)
return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count);
}
return tr(" (%1 installed)").arg(m_model->size());
}

View File

@ -4,6 +4,7 @@
#include <QSortFilterProxyModel>
#include "Application.h"
#include "settings/Setting.h"
#include "minecraft/MinecraftInstance.h"
#include "ui/pages/BasePage.h"
@ -28,6 +29,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
virtual QString helpPage() const override = 0;
virtual bool shouldDisplay() const override = 0;
QString extraHeaderInfoString();
void openedImpl() override;
void closedImpl() override;
@ -47,10 +49,10 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
protected slots:
void itemActivated(const QModelIndex& index);
void filterTextChanged(const QString& newContents);
virtual void runningStateChanged(bool running);
virtual void addItem();
virtual void removeItem();
void removeItem();
virtual void removeItems(const QItemSelection &selection);
virtual void enableItem();
virtual void disableItem();
@ -59,6 +61,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
virtual void viewConfigs();
void ShowContextMenu(const QPoint& pos);
void ShowHeaderContextMenu(const QPoint& pos);
protected:
BaseInstance* m_instance = nullptr;
@ -70,5 +73,5 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QString m_fileSelectionFilter;
QString m_viewFilter;
bool m_controlsEnabled = true;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
};

View File

@ -27,11 +27,7 @@
<item row="4" column="1" colspan="3">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="filterEdit">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
<widget class="QLineEdit" name="filterEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="filterLabel">
@ -66,6 +62,9 @@
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
@ -158,6 +157,17 @@
<string>Try to check or update all selected resources (all resources if none are selected)</string>
</property>
</action>
<action name="actionVisitItemPage">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Visit mod's page</string>
</property>
<property name="toolTip">
<string>Go to mods home page</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -48,29 +48,27 @@
#include "JavaCommon.h"
#include "Application.h"
#include "minecraft/auth/AccountList.h"
#include "JavaDownloader.h"
#include "java/JavaInstallList.h"
#include "java/JavaUtils.h"
#include "FileSystem.h"
InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
: QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst)
{
m_settings = inst->settings();
ui->setupUi(this);
auto sysMB = Sys::getSystemRam() / Sys::mebibyte;
ui->maxMemSpinBox->setMaximum(sysMB);
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&InstanceSettingsPage::changeInstanceAccount);
loadSettings();
}
bool InstanceSettingsPage::shouldDisplay() const
{
return !m_instance->isRunning();
updateThresholds();
}
InstanceSettingsPage::~InstanceSettingsPage()
@ -84,12 +82,12 @@ void InstanceSettingsPage::globalSettingsButtonClicked(bool)
case 0:
APPLICATION->ShowGlobalSettings(this, "java-settings");
return;
case 1:
APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
return;
case 2:
APPLICATION->ShowGlobalSettings(this, "custom-commands");
return;
default:
APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
return;
}
}
@ -276,6 +274,13 @@ void InstanceSettingsPage::applySettings()
m_settings->reset("JoinServerOnLaunchAddress");
}
// Use an account for this instance
bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked();
m_settings->set("UseAccountForInstance", useAccountForInstance);
if (!useAccountForInstance) {
m_settings->reset("InstanceAccountId");
}
// FIXME: This should probably be called by a signal instead
m_instance->updateRuntimeContext();
}
@ -373,6 +378,9 @@ void InstanceSettingsPage::loadSettings()
ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool());
ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString());
ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool());
updateAccountsMenu();
}
void InstanceSettingsPage::on_javaDownloadBtn_clicked()
@ -443,6 +451,44 @@ void InstanceSettingsPage::on_javaTestBtn_clicked()
checker->run();
}
void InstanceSettingsPage::updateAccountsMenu()
{
ui->instanceAccountSelector->clear();
auto accounts = APPLICATION->accounts();
int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString());
for (int i = 0; i < accounts->count(); i++) {
MinecraftAccountPtr account = accounts->at(i);
ui->instanceAccountSelector->addItem(getFaceForAccount(account), account->profileName(), i);
if (i == accountIndex)
ui->instanceAccountSelector->setCurrentIndex(i);
}
}
QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
{
if (auto face = account->getFace(); !face.isNull()) {
return face;
}
return APPLICATION->getThemedIcon("noaccount");
}
void InstanceSettingsPage::changeInstanceAccount(int index)
{
auto accounts = APPLICATION->accounts();
if (index != -1 && accounts->at(index) && ui->instanceAccountGroupBox->isChecked()) {
auto account = accounts->at(index);
m_settings->set("InstanceAccountId", account->profileId());
}
}
void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i)
{
updateThresholds();
}
void InstanceSettingsPage::checkerFinished()
{
checker.reset();
@ -453,3 +499,29 @@ void InstanceSettingsPage::retranslate()
ui->retranslateUi(this);
ui->customCommands->retranslate(); // TODO: why is this seperate from the others?
}
void InstanceSettingsPage::updateThresholds()
{
auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
unsigned int maxMem = ui->maxMemSpinBox->value();
QString iconName;
if (maxMem >= sysMiB) {
iconName = "status-bad";
ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity."));
} else if (maxMem > (sysMiB * 0.9)) {
iconName = "status-yellow";
ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity."));
} else {
iconName = "status-good";
ui->labelMaxMemIcon->setToolTip("");
}
{
auto height = ui->labelMaxMemIcon->fontInfo().pixelSize();
QIcon icon = APPLICATION->getThemedIcon(iconName);
QPixmap pix = icon.pixmap(height, height);
ui->labelMaxMemIcon->setPixmap(pix);
}
}

View File

@ -37,12 +37,13 @@
#include <QWidget>
#include "java/JavaChecker.h"
#include "BaseInstance.h"
#include <QObjectPtr.h>
#include "ui/pages/BasePage.h"
#include "JavaCommon.h"
#include <QMenu>
#include "Application.h"
#include "BaseInstance.h"
#include "JavaCommon.h"
#include "java/JavaChecker.h"
#include "ui/pages/BasePage.h"
class JavaChecker;
namespace Ui
@ -74,14 +75,16 @@ public:
{
return "Instance-settings";
}
virtual bool shouldDisplay() const override;
void retranslate() override;
private slots:
void updateThresholds();
private slots:
void on_javaDetectBtn_clicked();
void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked();
void on_javaDownloadBtn_clicked();
void on_maxMemSpinBox_valueChanged(int i);
void applySettings();
void loadSettings();
@ -90,6 +93,10 @@ private slots:
void globalSettingsButtonClicked(bool checked);
void updateAccountsMenu();
QIcon getFaceForAccount(MinecraftAccountPtr account);
void changeInstanceAccount(int index);
private:
Ui::InstanceSettingsPage *ui;
BaseInstance *m_instance;

View File

@ -119,7 +119,14 @@
<property name="checked">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0,0">
<item row="2" column="0">
<widget class="QLabel" name="labelPermGen">
<property name="text">
<string notr="true">PermGen:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelMinMem">
<property name="text">
@ -127,29 +134,21 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
<item row="1" column="0">
<widget class="QLabel" name="labelMaxMem">
<property name="text">
<string>Maximum memory allocation:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="3" column="0" colspan="3">
<widget class="QLabel" name="labelPermgenNote">
<property name="text">
<string>Note: Permgen is set automatically by Java 8 and later</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QSpinBox" name="minMemSpinBox">
<property name="toolTip">
<string>The amount of memory Minecraft is started with.</string>
@ -161,7 +160,7 @@
<number>128</number>
</property>
<property name="maximum">
<number>65536</number>
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
@ -171,7 +170,29 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="2">
<widget class="QSpinBox" name="maxMemSpinBox">
<property name="toolTip">
<string>The maximum amount of memory Minecraft is allowed to use.</string>
</property>
<property name="suffix">
<string notr="true"> MiB</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>1048576</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="permGenSpinBox">
<property name="toolTip">
<string>The amount of memory available to store loaded Java classes.</string>
@ -193,24 +214,16 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelPermGen">
<item row="1" column="3">
<widget class="QLabel" name="labelMaxMemIcon">
<property name="text">
<string notr="true">PermGen:</string>
<string notr="true"/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelMaxMem">
<property name="text">
<string>Maximum memory allocation:</string>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="labelPermgenNote">
<property name="text">
<string>Note: Permgen is set automatically by Java 8 and later</string>
<property name="buddy">
<cstring>maxMemSpinBox</cstring>
</property>
</widget>
</item>
@ -263,7 +276,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
<string>Start Minecraft maximized?</string>
<string>Start Minecraft maximized</string>
</property>
</widget>
</item>
@ -335,21 +348,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
<string>Show console while the game is running?</string>
<string>Show console while the game is running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
<string>Automatically close console when the game quits?</string>
<string>Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
<string>Show console when the game crashes?</string>
<string>Show console when the game crashes</string>
</property>
</widget>
</item>
@ -602,6 +615,41 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="instanceAccountGroupBox">
<property name="title">
<string>Override default account</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_15">
<item>
<layout class="QGridLayout" name="instanceAccountLayout">
<item row="0" column="0">
<widget class="QLabel" name="instanceAccountNameLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Account:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="instanceAccountSelector"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacerMiscellaneous">
<property name="orientation">

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -39,7 +40,7 @@
#include "Application.h"
#include <QIcon>
#include <QIdentityProxyModel>
#include <QScrollBar>
#include <QShortcut>
@ -277,28 +278,22 @@ void LogPage::on_btnPaste_clicked()
//FIXME: turn this into a proper task and move the upload logic out of GuiUtil!
m_model->append(
MessageLevel::Launcher,
QString("%2: Log upload triggered at: %1").arg(
QDateTime::currentDateTime().toString(Qt::RFC2822Date),
BuildConfig.LAUNCHER_DISPLAYNAME
QString("Log upload triggered at: %1").arg(
QDateTime::currentDateTime().toString(Qt::RFC2822Date)
)
);
auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this);
if(!url.isEmpty())
auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this);
if(!url.has_value())
{
m_model->append(
MessageLevel::Launcher,
QString("%2: Log uploaded to: %1").arg(
url,
BuildConfig.LAUNCHER_DISPLAYNAME
)
);
m_model->append(MessageLevel::Error, QString("Log upload canceled"));
}
else if (url->isNull())
{
m_model->append(MessageLevel::Error, QString("Log upload failed!"));
}
else
{
m_model->append(
MessageLevel::Error,
QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)
);
m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value()));
}
}

View File

@ -0,0 +1,477 @@
// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ManagedPackPage.h"
#include "ui_ManagedPackPage.h"
#include <QListView>
#include <QProxyStyle>
#include <QStyleFactory>
#include "Application.h"
#include "BuildConfig.h"
#include "InstanceImportTask.h"
#include "InstanceList.h"
#include "InstanceTask.h"
#include "Json.h"
#include "Markdown.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
/** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen.
* ... thanks Qt.
*/
class NoBigComboBoxStyle : public QProxyStyle {
Q_OBJECT
public:
// clang-format off
int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override
{
if (hint == QStyle::SH_ComboBox_Popup)
return false;
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
// clang-format on
/**
* Something about QProxyStyle and QStyle objects means they can't be free'd just
* because all the widgets using them are gone.
* They seems to be tied to the QApplicaiton lifecycle.
* So make singletons tied to the lifetime of the application to clean them up and ensure they aren't
* being remade over and over again, thus leaking memory.
*/
public:
static NoBigComboBoxStyle* getInstance(QStyle* style)
{
static QHash<QStyle*, NoBigComboBoxStyle*> s_singleton_instances_ = {};
static std::mutex s_singleton_instances_mutex_;
std::lock_guard<std::mutex> lock(s_singleton_instances_mutex_);
auto inst_iter = s_singleton_instances_.constFind(style);
NoBigComboBoxStyle* inst = nullptr;
if (inst_iter == s_singleton_instances_.constEnd() || *inst_iter == nullptr) {
inst = new NoBigComboBoxStyle(style);
inst->setParent(APPLICATION);
s_singleton_instances_.insert(style, inst);
qDebug() << "QProxyStyle NoBigComboBox created for" << style->objectName() << style;
} else {
inst = *inst_iter;
}
return inst;
}
private:
NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {}
};
ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent)
{
if (type == "modrinth")
return new ModrinthManagedPackPage(inst, nullptr, parent);
if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame))
return new FlameManagedPackPage(inst, nullptr, parent);
return new GenericManagedPackPage(inst, nullptr, parent);
}
ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst)
{
Q_ASSERT(inst);
ui->setupUi(this);
// NOTE: GTK2 themes crash with the proxy style.
// This seems like an upstream bug, so there's not much else that can be done.
if (!QStyleFactory::keys().contains("gtk2")) {
auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style());
ui->versionsComboBox->setStyle(comboStyle);
}
ui->reloadButton->setVisible(false);
connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) {
ui->reloadButton->setVisible(false);
m_loaded = false;
// Pretend we're opening the page again
openedImpl();
});
}
ManagedPackPage::~ManagedPackPage()
{
delete ui;
}
void ManagedPackPage::openedImpl()
{
ui->packName->setText(m_inst->getManagedPackName());
ui->packVersion->setText(m_inst->getManagedPackVersionName());
ui->packOrigin->setText(tr("Website: <a href=%1>%2</a> | Pack ID: %3 | Version ID: %4")
.arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID()));
parseManagedPack();
}
QString ManagedPackPage::displayName() const
{
auto type = m_inst->getManagedPackType();
if (type.isEmpty())
return {};
if (type == "flame")
type = "CurseForge";
return type.replace(0, 1, type[0].toUpper());
}
QIcon ManagedPackPage::icon() const
{
return APPLICATION->getThemedIcon(m_inst->getManagedPackType());
}
QString ManagedPackPage::helpPage() const
{
return {};
}
void ManagedPackPage::retranslate()
{
ui->retranslateUi(this);
}
bool ManagedPackPage::shouldDisplay() const
{
return m_inst->isManagedPack();
}
bool ManagedPackPage::runUpdateTask(InstanceTask* task)
{
Q_ASSERT(task);
unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task));
connect(task, &Task::failed,
[this](QString reason) { 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();
});
connect(task, &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show();
});
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
return task->wasSuccessful();
}
void ManagedPackPage::suggestVersion()
{
ui->updateButton->setText(tr("Update pack"));
ui->updateButton->setDisabled(false);
}
void ManagedPackPage::setFailState()
{
qDebug() << "Setting fail state!";
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {});
ui->versionsComboBox->blockSignals(false);
ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack."));
ui->updateButton->setText(tr("Cannot update!"));
ui->updateButton->setDisabled(true);
ui->reloadButton->setVisible(true);
}
ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update);
}
// MODRINTH
void ModrinthManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Modrinth pack";
// No need for the extra work because we already have everything we need.
if (m_loaded)
return;
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(
Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
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;
setFailState();
return;
}
try {
Modrinth::loadIndexedVersions(m_pack, doc);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading modrinth modpack version: " << e.cause();
setFailState();
return;
}
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->blockSignals(false);
for (auto version : m_pack.versions) {
QString name = version.version;
if (!version.name.contains(version.version))
name = QString("%1 — %2").arg(version.name, version.version);
// NOTE: the id from version isn't the same id in the modpack format spec...
// e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index..............
if (version.version == m_inst->getManagedPackVersionName())
name = tr("%1 (Current)").arg(name);
ui->versionsComboBox->addItem(name, QVariant(version.id));
}
suggestVersion();
m_loaded = true;
});
QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState);
QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState);
ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
m_fetch_job->start();
}
QString ModrinthManagedPackPage::url() const
{
return "https://modrinth.com/mod/" + m_inst->getManagedPackID();
}
void ModrinthManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8()));
ManagedPackPage::suggestVersion();
}
void ModrinthManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
// NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field.
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", version.id);
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info));
InstanceName inst_name(m_inst->getManagedPackName(), version.version);
inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version));
extracted->setName(inst_name);
extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id()));
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
}
// FLAME
FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update);
}
void FlameManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Flame pack";
// We need to tell the user to redownload the pack, since we didn't save the required info previously
if (m_inst->getManagedPackID().isEmpty()) {
setFailState();
QString message =
tr("<h1>Hey there!</h1>"
"<h4>"
"It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.<br/>"
"Unfortunately, we can't do the proper API requests without this information.<br/>"
"<br/>"
"So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.<br/>"
"<br/>"
"Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!"
"</h4>");
ui->changelogTextBrowser->setHtml(message);
return;
}
// No need for the extra work because we already have everything we need.
if (m_loaded)
return;
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
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;
setFailState();
return;
}
try {
auto obj = doc.object();
auto data = Json::ensureArray(obj, "data");
Flame::loadIndexedPackVersions(m_pack, data);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause();
setFailState();
return;
}
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->blockSignals(false);
for (auto version : m_pack.versions) {
QString name = version.version;
if (version.fileId == m_inst->getManagedPackVersionID().toInt())
name = tr("%1 (Current)").arg(name);
ui->versionsComboBox->addItem(name, QVariant(version.fileId));
}
suggestVersion();
m_loaded = true;
});
QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState);
QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState);
m_fetch_job->start();
}
QString FlameManagedPackPage::url() const
{
// FIXME: We should display the websiteUrl field, but this requires doing the API request first :(
return {};
}
void FlameManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId));
ManagedPackPage::suggestVersion();
}
void FlameManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", QString::number(version.fileId));
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info));
extracted->setName(m_inst->name());
extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id()));
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
}
#include "ManagedPackPage.moc"

View File

@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2022 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "BaseInstance.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
#include "net/NetJob.h"
#include "ui/pages/BasePage.h"
#include <QWidget>
namespace Ui {
class ManagedPackPage;
}
class InstanceTask;
class InstanceWindow;
class ManagedPackPage : public QWidget, public BasePage {
Q_OBJECT
public:
inline static ManagedPackPage* createPage(BaseInstance* inst, QWidget* parent = nullptr)
{
return ManagedPackPage::createPage(inst, inst->getManagedPackType(), parent);
}
static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr);
~ManagedPackPage() override;
[[nodiscard]] QString displayName() const override;
[[nodiscard]] QIcon icon() const override;
[[nodiscard]] QString helpPage() const override;
[[nodiscard]] QString id() const override { return "managed_pack"; }
[[nodiscard]] bool shouldDisplay() const override;
void openedImpl() override;
bool apply() override { return true; }
void retranslate() override;
/** Gets the necessary information about the managed pack, such as
* available versions*/
virtual void parseManagedPack(){};
/** URL of the managed pack.
* Not the version-specific one.
*/
[[nodiscard]] virtual QString url() const { return {}; };
void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; }
public slots:
/** Gets the current version selection and update the UI, including the update button and the changelog.
*/
virtual void suggestVersion();
virtual void update(){};
protected slots:
/** Does the necessary UI changes for when something failed.
*
* This includes:
* - Setting an appropriate text on the version selector to indicate a fail;
* - Setting an appropriate text on the changelog text browser to indicate a fail;
* - Disable the update button.
*/
void setFailState();
protected:
ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
/** Run the InstanceTask, with a progress dialog and all.
* Similar to MainWindow::instanceFromInstanceTask
*
* Returns whether the task was successful.
*/
bool runUpdateTask(InstanceTask*);
protected:
InstanceWindow* m_instance_window = nullptr;
Ui::ManagedPackPage* ui;
BaseInstance* m_inst;
bool m_loaded = false;
};
/** Simple page for when we aren't a managed pack. */
class GenericManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
GenericManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr)
: ManagedPackPage(inst, instance_window, parent)
{}
~GenericManagedPackPage() override = default;
// TODO: We may want to show this page with some useful info at some point.
[[nodiscard]] bool shouldDisplay() const override { return false; };
};
class ModrinthManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
~ModrinthManagedPackPage() override = default;
void parseManagedPack() override;
[[nodiscard]] QString url() const override;
public slots:
void suggestVersion() override;
void update() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Modrinth::Modpack m_pack;
ModrinthAPI m_api;
};
class FlameManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
~FlameManagedPackPage() override = default;
void parseManagedPack() override;
[[nodiscard]] QString url() const override;
public slots:
void suggestVersion() override;
void update() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Flame::IndexedPack m_pack;
FlameAPI m_api;
};

View File

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ManagedPackPage</class>
<widget class="QWidget" name="ManagedPackPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>731</width>
<height>538</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="packInformationBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Pack information</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<layout class="QHBoxLayout" name="packNameLayout">
<item>
<widget class="QLabel" name="packNameLabel">
<property name="text">
<string>Pack name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packName">
<property name="text">
<string notr="true">placeholder</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="packVersionLayout">
<item>
<widget class="QLabel" name="packVersionLabel">
<property name="text">
<string>Current version:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packVersion">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string notr="true">placeholder</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="packOriginLayout">
<item>
<widget class="QLabel" name="packOriginLabel">
<property name="text">
<string>Provider information:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packOrigin">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string notr="true">placeholder</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="updateToVersionLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Update to version:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionsComboBox"/>
</item>
<item>
<widget class="QPushButton" name="updateButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Fetching versions...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="changelogBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Changelog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextBrowser" name="changelogTextBrowser">
<property name="placeholderText">
<string>No changelog available for this version!</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="reloadButton">
<property name="text">
<string>Reload page</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,8 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -43,13 +45,14 @@
#include <QMenu>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include <algorithm>
#include "Application.h"
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/dialogs/ModUpdateDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "DesktopServices.h"
@ -58,7 +61,8 @@
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "Version.h"
#include "tasks/ConcurrentTask.h"
@ -84,45 +88,40 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
auto check_allow_update = [this] {
return (!m_instance || !m_instance->isRunning()) &&
(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
};
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
ui->actionsToolbar->addAction(ui->actionVisitItemPage);
connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
auto mods_list = m_model->selectedMods(selection);
auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(),
[](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; });
if (selected <= 1) {
ui->actionVisitItemPage->setText(tr("Visit mod's page"));
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
} else {
ui->actionVisitItemPage->setText(tr("Visit mods' pages"));
ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods"));
}
ui->actionVisitItemPage->setEnabled(selected != 0);
});
connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
});
connect(mods.get(), &ModFolderModel::rowsInserted, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
});
connect(mods.get(), &ModFolderModel::rowsRemoved, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
ui->actionUpdateItem->setEnabled(check_allow_update());
// Prevent a weird crash when trying to open the mods page twice in a session o.O
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
});
ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
connect(mods.get(), &ModFolderModel::updateFinished, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
}
}
void ModFolderPage::runningStateChanged(bool running)
{
ExternalResourcesPage::runningStateChanged(running);
ui->actionDownloadItem->setEnabled(!running);
ui->actionUpdateItem->setEnabled(!running);
ui->actionAddItem->setEnabled(!running);
ui->actionEnableItem->setEnabled(!running);
ui->actionDisableItem->setEnabled(!running);
ui->actionRemoveItem->setEnabled(!running);
}
bool ModFolderPage::shouldDisplay() const
{
return true;
@ -139,30 +138,33 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
return true;
}
void ModFolderPage::removeItem()
void ModFolderPage::removeItems(const QItemSelection& selection)
{
if (m_instance != nullptr && m_instance->isRunning()) {
auto response = CustomMessageBox::selectable(this, "Confirm Delete",
"If you remove mods while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
if (response != QMessageBox::Yes)
return;
}
m_model->deleteMods(selection.indexes());
}
void ModFolderPage::installMods()
{
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) {
if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
ModDownloadDialog mdownload(m_model, this, m_instance);
ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance);
if (mdownload.exec()) {
ConcurrentTask* tasks = new ConcurrentTask(this);
connect(tasks, &Task::failed, [this, tasks](QString reason) {
@ -218,8 +220,7 @@ void ModFolderPage::updateMods()
message = tr("All selected mods are up-to-date! :)");
}
}
CustomMessageBox::selectable(this, tr("Update checker"), message)
->exec();
CustomMessageBox::selectable(this, tr("Update checker"), message)->exec();
return;
}
@ -277,3 +278,22 @@ bool CoreModFolderPage::shouldDisplay() const
}
return false;
}
NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
: ModFolderPage(inst, mods, parent)
{}
bool NilModFolderPage::shouldDisplay() const
{
return m_model->dir().exists();
}
void ModFolderPage::visitModPages()
{
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
for (auto mod : m_model->selectedMods(selection)) {
auto url = mod->metaurl();
if (!url.isEmpty())
DesktopServices::openUrl(url);
}
}

View File

@ -1,8 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -53,16 +55,16 @@ class ModFolderPage : public ExternalResourcesPage {
virtual QString helpPage() const override { return "Loader-mods"; }
virtual bool shouldDisplay() const override;
void runningStateChanged(bool running) override;
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
void removeItem() override;
void removeItems(const QItemSelection& selection) override;
void installMods();
void updateMods();
void visitModPages();
protected:
std::shared_ptr<ModFolderModel> m_model;
@ -80,3 +82,16 @@ class CoreModFolderPage : public ModFolderPage {
virtual bool shouldDisplay() const override;
};
class NilModFolderPage : public ModFolderPage {
public:
explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
virtual ~NilModFolderPage() = default;
virtual QString displayName() const override { return tr("Nilmods"); }
virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); }
virtual QString id() const override { return "nilmods"; }
virtual QString helpPage() const override { return "Nilmods"; }
virtual bool shouldDisplay() const override;
};

View File

@ -17,17 +17,11 @@
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTextEdit" name="noteEditor">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -204,7 +205,7 @@ void OtherLogsPage::on_btnReload_clicked()
void OtherLogsPage::on_btnPaste_clicked()
{
GuiUtil::uploadPaste(ui->text->toPlainText(), this);
GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this);
}
void OtherLogsPage::on_btnCopy_clicked()
@ -219,13 +220,21 @@ void OtherLogsPage::on_btnDelete_clicked()
setControlsEnabled(false);
return;
}
if (QMessageBox::question(this, tr("Delete"),
tr("Do you really want to delete %1?").arg(m_currentFile),
QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
{
if (QMessageBox::question(this, tr("Confirm Deletion"),
tr("You are about to delete \"%1\".\n"
"This may be permanent and it will be gone from the logs folder.\n\n"
"Are you sure?")
.arg(m_currentFile),
QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) {
return;
}
QFile file(FS::PathCombine(m_path, m_currentFile));
if (FS::trash(file.fileName()))
{
return;
}
if (!file.remove())
{
QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2")
@ -243,15 +252,15 @@ void OtherLogsPage::on_btnClean_clicked()
return;
}
QMessageBox *messageBox = new QMessageBox(this);
messageBox->setWindowTitle(tr("Clean up"));
messageBox->setWindowTitle(tr("Confirm Cleanup"));
if(toDelete.size() > 5)
{
messageBox->setText(tr("Do you really want to delete all log files?"));
messageBox->setText(tr("Are you sure you want to delete all log files?"));
messageBox->setDetailedText(toDelete.join('\n'));
}
else
{
messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n')));
messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n')));
}
messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
messageBox->setDefaultButton(QMessageBox::Ok);
@ -267,6 +276,10 @@ void OtherLogsPage::on_btnClean_clicked()
for(auto item: toDelete)
{
QFile file(FS::PathCombine(m_path, item));
if (FS::trash(file.fileName()))
{
continue;
}
if (!file.remove())
{
failed.push_back(item);

View File

@ -48,9 +48,6 @@
<property name="enabled">
<bool>false</bool>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>

View File

@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* 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 "ResourcePackPage.h"
#include "ResourceDownloadTask.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionDownloadItem->setText(tr("Download packs"));
ui->actionDownloadItem->setToolTip(tr("Download resource packs from online platforms"));
ui->actionDownloadItem->setEnabled(true);
connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadRPs);
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
ui->actionViewConfigs->setVisible(false);
}
bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& rp = static_cast<ResourcePack&>(m_model->at(row));
ui->frame->updateWithResourcePack(rp);
return true;
}
void ResourcePackPage::downloadRPs()
{
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
ResourceDownload::ResourcePackDownloadDialog mdownload(this, std::static_pointer_cast<ResourcePackFolderModel>(m_model), m_instance);
if (mdownload.exec()) {
auto 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();
tasks->deleteLater();
});
for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
m_model->update();
}
}

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* This program is free software: you can redistribute it and/or modify
@ -44,12 +46,7 @@ class ResourcePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
virtual ~ResourcePackPage() {}
explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0);
QString displayName() const override { return tr("Resource packs"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
@ -63,13 +60,7 @@ public:
}
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& rp = static_cast<ResourcePack&>(m_model->at(row));
ui->frame->updateWithResourcePack(rp);
return true;
}
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
void downloadRPs();
};

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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,6 +36,7 @@
*/
#include "ScreenshotsPage.h"
#include "BuildConfig.h"
#include "ui_ScreenshotsPage.h"
#include <QModelIndex>
@ -95,37 +97,30 @@ public:
return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
return;
int tries = 5;
while (tries)
{
if (!m_cache->stale(m_path))
return;
QImage image(m_path);
if (image.isNull())
{
QThread::msleep(500);
tries--;
continue;
}
QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
if (!m_cache->stale(m_path))
return;
QImage image(m_path);
if (image.isNull()) {
m_resultEmitter.emitResultsFailed(m_path);
qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?";
return;
}
m_resultEmitter.emitResultsFailed(m_path);
QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
}
QString m_path;
SharedIconCachePtr m_cache;
@ -144,9 +139,12 @@ public:
m_thumbnailCache = std::make_shared<SharedIconCache>();
m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder"));
connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
// FIXME: the watched file set is not updated when files are removed
}
virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); }
virtual ~FilterModel() {
m_thumbnailingPool.clear();
if (!m_thumbnailingPool.waitForDone(500))
qDebug() << "Thumbnail pool took longer than 500ms to finish";
}
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
{
auto model = sourceModel();
@ -213,10 +211,12 @@ private slots:
void fileChanged(QString filepath)
{
m_thumbnailCache->setStale(filepath);
thumbnailImage(filepath);
// reinsert the path...
watcher.removePath(filepath);
watcher.addPath(filepath);
if (QFile::exists(filepath)) {
watcher.addPath(filepath);
thumbnailImage(filepath);
}
}
private:
@ -379,6 +379,26 @@ void ScreenshotsPage::on_actionUpload_triggered()
if (selection.isEmpty())
return;
QString text;
QUrl baseUrl(BuildConfig.IMGUR_BASE_URL);
if (selection.size() > 1)
text = tr("You are about to upload %1 screenshots to %2.\n"
"You should double-check for personal information.\n\n"
"Are you sure?")
.arg(QString::number(selection.size()), baseUrl.host());
else
text = tr("You are about to upload the selected screenshot to %1.\n"
"You should double-check for personal information.\n\n"
"Are you sure?")
.arg(baseUrl.host());
auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
QList<ScreenShot::Ptr> uploaded;
auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network()));
if(selection.size() < 2)
@ -491,17 +511,32 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered()
void ScreenshotsPage::on_actionDelete_triggered()
{
auto mbox = CustomMessageBox::selectable(
this, tr("Are you sure?"), tr("This will delete all selected screenshots."),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No);
std::unique_ptr<QMessageBox> box(mbox);
auto selected = ui->listView->selectionModel()->selectedIndexes();
if (box->exec() != QMessageBox::Yes)
int count = ui->listView->selectionModel()->selectedRows().size();
QString text;
if (count > 1)
text = tr("You are about to delete %1 screenshots.\n"
"This may be permanent and they will be gone from the folder.\n\n"
"Are you sure?")
.arg(count);
else
text = tr("You are about to delete the selected screenshot.\n"
"This may be permanent and it will be gone from the folder.\n\n"
"Are you sure?")
.arg(count);
auto response =
CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec();
if (response != QMessageBox::Yes)
return;
auto selected = ui->listView->selectionModel()->selectedIndexes();
for (auto item : selected)
{
if (FS::trash(m_model->filePath(item)))
continue;
m_model->remove(item);
}
}
@ -537,6 +572,19 @@ void ScreenshotsPage::openedImpl()
ui->listView->setModel(nullptr);
}
}
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
if (!APPLICATION->settings()->contains(setting_name))
m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name);
else
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
}
void ScreenshotsPage::closedImpl()
{
m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
}
#include "ScreenshotsPage.moc"

View File

@ -40,8 +40,11 @@
#include "ui/pages/BasePage.h"
#include <Application.h>
#include "settings/Setting.h"
class QFileSystemModel;
class QIdentityProxyModel;
class QItemSelection;
namespace Ui
{
class ScreenshotsPage;
@ -59,7 +62,8 @@ public:
explicit ScreenshotsPage(QString path, QWidget *parent = 0);
virtual ~ScreenshotsPage();
virtual void openedImpl() override;
void openedImpl() override;
void closedImpl() override;
enum
{
@ -110,4 +114,6 @@ private:
QString m_folder;
bool m_valid = false;
bool m_uploadActive = false;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
};

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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,6 +36,7 @@
*/
#include "ServersPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ServersPage.h"
#include <FileSystem.h>
@ -48,6 +50,7 @@
#include <QFileSystemWatcher>
#include <QMenu>
#include <QTimer>
static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
@ -400,11 +403,11 @@ public:
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
return m_servers.size();
return parent.isValid() ? 0 : m_servers.size();
}
int columnCount(const QModelIndex & parent) const override
{
return COLUMN_COUNT;
return parent.isValid() ? 0 : COLUMN_COUNT;
}
Server * at(int index)
@ -765,11 +768,21 @@ void ServersPage::updateState()
void ServersPage::openedImpl()
{
m_model->observe();
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
if (!APPLICATION->settings()->contains(setting_name))
m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name);
else
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
}
void ServersPage::closedImpl()
{
m_model->unobserve();
m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
}
void ServersPage::on_actionAdd_triggered()
@ -789,6 +802,17 @@ void ServersPage::on_actionAdd_triggered()
void ServersPage::on_actionRemove_triggered()
{
auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
tr("You are about to remove \"%1\".\n"
"This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n"
"Are you sure?")
.arg(m_model->at(currentServer)->m_name),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
m_model->removeRow(currentServer);
}

View File

@ -42,6 +42,8 @@
#include "ui/pages/BasePage.h"
#include <Application.h>
#include "settings/Setting.h"
namespace Ui
{
class ServersPage;
@ -68,7 +70,7 @@ public:
}
virtual QIcon icon() const override
{
return APPLICATION->getThemedIcon("unknown_server");
return APPLICATION->getThemedIcon("server");
}
virtual QString id() const override
{
@ -112,5 +114,7 @@ private: // data
Ui::ServersPage *ui = nullptr;
ServersModel * m_model = nullptr;
InstancePtr m_inst = nullptr;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
};

View File

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* 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 "ShaderPackPage.h"
#include "ui_ExternalResourcesPage.h"
#include "ResourceDownloadTask.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionDownloadItem->setText(tr("Download shaders"));
ui->actionDownloadItem->setToolTip(tr("Download shaders from online platforms"));
ui->actionDownloadItem->setEnabled(true);
connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaders);
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
ui->actionViewConfigs->setVisible(false);
}
void ShaderPackPage::downloadShaders()
{
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
ResourceDownload::ShaderPackDownloadDialog mdownload(this, std::static_pointer_cast<ShaderPackFolderModel>(m_model), m_instance);
if (mdownload.exec()) {
auto 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();
tasks->deleteLater();
});
for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
m_model->update();
}
}

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* This program is free software: you can redistribute it and/or modify
@ -36,28 +38,21 @@
#pragma once
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
class ShaderPackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
virtual ~ShaderPackPage() {}
explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = nullptr);
~ShaderPackPage() override = default;
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;
}
bool shouldDisplay() const override { return true; }
public slots:
void downloadShaders();
};

View File

@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* 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 "TexturePackPage.h"
#include "ResourceDownloadTask.h"
#include "minecraft/mod/TexturePack.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionDownloadItem->setText(tr("Download packs"));
ui->actionDownloadItem->setToolTip(tr("Download texture packs from online platforms"));
ui->actionDownloadItem->setEnabled(true);
connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTPs);
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
ui->actionViewConfigs->setVisible(false);
}
bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& rp = static_cast<TexturePack&>(m_model->at(row));
ui->frame->updateWithTexturePack(rp);
return true;
}
void TexturePackPage::downloadTPs()
{
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast<TexturePackFolderModel>(m_model), m_instance);
if (mdownload.exec()) {
auto 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();
tasks->deleteLater();
});
for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
m_model->update();
}
}

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* This program is free software: you can redistribute it and/or modify
@ -39,18 +41,12 @@
#include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/TexturePackFolderModel.h"
#include "minecraft/mod/TexturePack.h"
class TexturePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
virtual ~TexturePackPage() {}
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent = nullptr);
QString displayName() const override { return tr("Texture packs"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
@ -63,13 +59,6 @@ public:
}
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& rp = static_cast<TexturePack&>(m_model->at(row));
ui->frame->updateWithTexturePack(rp);
return true;
}
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
void downloadTPs();
};

View File

@ -1,8 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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,14 +40,13 @@
#include "Application.h"
#include <QMessageBox>
#include <QLabel>
#include <QAbstractItemModel>
#include <QEvent>
#include <QKeyEvent>
#include <QMenu>
#include <QAbstractItemModel>
#include <QMessageBox>
#include <QLabel>
#include <QListView>
#include <QMenu>
#include <QMessageBox>
#include <QString>
#include <QUrl>
@ -51,49 +54,42 @@
#include "ui_VersionPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/dialogs/NewComponentDialog.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/GuiUtil.h"
#include "DesktopServices.h"
#include "Exception.h"
#include "Version.h"
#include "icons/IconList.h"
#include "minecraft/PackProfile.h"
#include "minecraft/auth/AccountList.h"
#include "minecraft/mod/Mod.h"
#include "icons/IconList.h"
#include "Exception.h"
#include "Version.h"
#include "DesktopServices.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
class IconProxy : public QIdentityProxyModel
{
class IconProxy : public QIdentityProxyModel {
Q_OBJECT
public:
IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget)
public:
IconProxy(QWidget* parentWidget) : QIdentityProxyModel(parentWidget)
{
connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone);
m_parentWidget = parentWidget;
}
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override
virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override
{
QVariant var = QIdentityProxyModel::data(proxyIndex, role);
int column = proxyIndex.column();
if(column == 0 && role == Qt::DecorationRole && m_parentWidget)
{
if(!var.isNull())
{
if (column == 0 && role == Qt::DecorationRole && m_parentWidget) {
if (!var.isNull()) {
auto string = var.toString();
if(string == "warning")
{
if (string == "warning") {
return APPLICATION->getThemedIcon("status-yellow");
}
else if(string == "error")
{
} else if (string == "error") {
return APPLICATION->getThemedIcon("status-bad");
}
}
@ -101,14 +97,11 @@ public:
}
return var;
}
private slots:
void widgetGone()
{
m_parentWidget = nullptr;
}
private slots:
void widgetGone() { m_parentWidget = nullptr; }
private:
QWidget *m_parentWidget = nullptr;
private:
QWidget* m_parentWidget = nullptr;
};
QIcon VersionPage::icon() const
@ -125,15 +118,29 @@ void VersionPage::retranslate()
ui->retranslateUi(this);
}
QMenu * VersionPage::createPopupMenu()
void VersionPage::openedImpl()
{
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
if (!APPLICATION->settings()->contains(setting_name))
m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name);
else
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
}
void VersionPage::closedImpl()
{
m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
}
QMenu* VersionPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
filteredMenu->removeAction(ui->toolBar->toggleViewAction());
return filteredMenu;
}
VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
: QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
{
ui->setupUi(this);
@ -146,7 +153,7 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
auto proxy = new IconProxy(ui->packageView);
proxy->setSourceModel(m_profile.get());
m_filterModel = new QSortFilterProxyModel();
m_filterModel = new QSortFilterProxyModel(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@ -163,10 +170,8 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls);
controlsEnabled = !m_inst->isRunning();
updateVersionControls();
preselect(0);
connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus);
connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged);
}
@ -183,18 +188,16 @@ void VersionPage::showContextMenu(const QPoint& pos)
delete menu;
}
void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &previous)
void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& previous)
{
if (!current.isValid())
{
if (!current.isValid()) {
ui->frame->clear();
return;
}
int row = current.row();
auto patch = m_profile->getComponent(row);
auto severity = patch->getProblemSeverity();
switch(severity)
{
switch (severity) {
case ProblemSeverity::Warning:
ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break;
@ -207,16 +210,12 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
return;
}
auto &problems = patch->getProblems();
auto& problems = patch->getProblems();
QString problemOut;
for (auto &problem: problems)
{
if(problem.m_severity == ProblemSeverity::Error)
{
for (auto& problem : problems) {
if (problem.m_severity == ProblemSeverity::Error) {
problemOut += tr("Error: ");
}
else if(problem.m_severity == ProblemSeverity::Warning)
{
} else if (problem.m_severity == ProblemSeverity::Warning) {
problemOut += tr("Warning: ");
}
problemOut += problem.m_description;
@ -225,70 +224,47 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
ui->frame->setDescription(problemOut);
}
void VersionPage::updateRunningStatus(bool running)
{
if(controlsEnabled == running) {
controlsEnabled = !running;
updateVersionControls();
}
}
void VersionPage::updateVersionControls()
{
// FIXME: this is a dirty hack
auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
ui->actionInstall_Forge->setEnabled(controlsEnabled);
bool supportsFabric = minecraftVersion >= Version("1.14");
ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric);
ui->actionInstall_Fabric->setEnabled(supportsFabric);
bool supportsQuilt = minecraftVersion >= Version("1.14");
ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt);
ui->actionInstall_Quilt->setEnabled(supportsQuilt);
bool supportsLiteLoader = minecraftVersion <= Version("1.12.2");
ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader);
ui->actionInstall_LiteLoader->setEnabled(supportsLiteLoader);
updateButtons();
}
void VersionPage::updateButtons(int row)
{
if(row == -1)
if (row == -1)
row = currentRow();
auto patch = m_profile->getComponent(row);
ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable());
ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable());
ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable());
ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable());
ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom());
ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable());
ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible());
ui->actionDownload_All->setEnabled(controlsEnabled);
ui->actionAdd_Empty->setEnabled(controlsEnabled);
ui->actionReload->setEnabled(controlsEnabled);
ui->actionInstall_mods->setEnabled(controlsEnabled);
ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled);
ui->actionRemove->setEnabled(patch && patch->isRemovable());
ui->actionMove_down->setEnabled(patch && patch->isMoveable());
ui->actionMove_up->setEnabled(patch && patch->isMoveable());
ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable());
ui->actionEdit->setEnabled(patch && patch->isCustom());
ui->actionCustomize->setEnabled(patch && patch->isCustomizable());
ui->actionRevert->setEnabled(patch && patch->isRevertible());
}
bool VersionPage::reloadPackProfile()
{
try
{
try {
m_profile->reload(Net::Mode::Online);
return true;
}
catch (const Exception &e)
{
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
return false;
}
catch (...)
{
QMessageBox::critical(
this, tr("Error"),
tr("Couldn't load the instance profile."));
} catch (...) {
QMessageBox::critical(this, tr("Error"), tr("Couldn't load the instance profile."));
return false;
}
}
@ -301,13 +277,26 @@ void VersionPage::on_actionReload_triggered()
void VersionPage::on_actionRemove_triggered()
{
if (ui->packageView->currentIndex().isValid())
{
// FIXME: use actual model, not reloading.
if (!m_profile->remove(ui->packageView->currentIndex().row()))
{
QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
}
if (!ui->packageView->currentIndex().isValid()) {
return;
}
int index = ui->packageView->currentIndex().row();
auto component = m_profile->getComponent(index);
if (component->isCustom()) {
auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
tr("You are about to remove \"%1\".\n"
"This is permanent and will completely remove the custom component.\n\n"
"Are you sure?")
.arg(component->getName()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
// FIXME: use actual model, not reloading.
if (!m_profile->remove(index)) {
QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
}
updateButtons();
reloadPackProfile();
@ -316,17 +305,16 @@ void VersionPage::on_actionRemove_triggered()
void VersionPage::on_actionInstall_mods_triggered()
{
if(m_container)
{
if (m_container) {
m_container->selectPage("mods");
}
}
void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
{
auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if(!list.empty())
{
auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"),
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.empty()) {
m_profile->installJarMods(list);
}
updateButtons();
@ -334,22 +322,45 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
void VersionPage::on_actionReplace_Minecraft_jar_triggered()
{
auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if(!jarPath.isEmpty())
{
auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"),
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!jarPath.isEmpty()) {
m_profile->installCustomJar(jarPath);
}
updateButtons();
}
void VersionPage::on_actionImport_Components_triggered()
{
QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components (*.json)"),
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.isEmpty()) {
if (!m_profile->installComponents(list)) {
QMessageBox::warning(this, tr("Failed to import components"),
tr("Some components could not be imported. Check logs for details"));
}
}
updateButtons();
}
void VersionPage::on_actionAdd_Agents_triggered()
{
QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents (*.jar)"),
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.isEmpty())
m_profile->installAgents(list);
updateButtons();
}
void VersionPage::on_actionMove_up_triggered()
{
try
{
try {
m_profile->move(currentRow(), PackProfile::MoveUp);
}
catch (const Exception &e)
{
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
}
updateButtons();
@ -357,12 +368,9 @@ void VersionPage::on_actionMove_up_triggered()
void VersionPage::on_actionMove_down_triggered()
{
try
{
try {
m_profile->move(currentRow(), PackProfile::MoveDown);
}
catch (const Exception &e)
{
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
}
updateButtons();
@ -371,39 +379,32 @@ void VersionPage::on_actionMove_down_triggered()
void VersionPage::on_actionChange_version_triggered()
{
auto versionRow = currentRow();
if(versionRow == -1)
{
if (versionRow == -1) {
return;
}
auto patch = m_profile->getComponent(versionRow);
auto name = patch->getName();
auto list = patch->getVersionList();
if(!list)
{
if (!list) {
return;
}
auto uid = list->uid();
// FIXME: this is a horrible HACK. Get version filtering information from the actual metadata...
if(uid == "net.minecraftforge")
{
if (uid == "net.minecraftforge") {
on_actionInstall_Forge_triggered();
return;
}
else if (uid == "com.mumfrey.liteloader")
{
} else if (uid == "com.mumfrey.liteloader") {
on_actionInstall_LiteLoader_triggered();
return;
}
VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this);
if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed")
{
if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") {
vselect.setEmptyString(tr("No intermediary mappings versions are currently available."));
vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!"));
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
}
auto currentVersion = patch->getVersion();
if(!currentVersion.isEmpty())
{
if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (!vselect.exec() || !vselect.selectedVersion())
@ -411,8 +412,7 @@ void VersionPage::on_actionChange_version_triggered()
qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor();
bool important = false;
if(uid == "net.minecraft")
{
if (uid == "net.minecraft") {
important = true;
}
m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important);
@ -422,23 +422,21 @@ void VersionPage::on_actionChange_version_triggered()
void VersionPage::on_actionDownload_All_triggered()
{
if (!APPLICATION->accounts()->anyAccountIsValid())
{
CustomMessageBox::selectable(
this, tr("Error"),
tr("Cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Mojang or Minecraft account."),
QMessageBox::Warning)->show();
if (!APPLICATION->accounts()->anyAccountIsValid()) {
CustomMessageBox::selectable(this, tr("Error"),
tr("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;
}
auto updateTask = m_inst->createUpdateTask(Net::Mode::Online);
if (!updateTask)
{
if (!updateTask) {
return;
}
ProgressDialog tDialog(this);
connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
connect(updateTask.get(), &Task::failed, this, &VersionPage::onGameUpdateError);
// FIXME: unused return value
tDialog.execWithTask(updateTask.get());
updateButtons();
@ -448,28 +446,26 @@ void VersionPage::on_actionDownload_All_triggered()
void VersionPage::on_actionInstall_Forge_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge");
if(!vlist)
{
if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") +
m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.minecraftforge");
if(!currentVersion.isEmpty())
{
if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (vselect.exec() && vselect.selectedVersion())
{
if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion();
preselect(m_profile->rowCount(QModelIndex())-1);
preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@ -477,8 +473,7 @@ void VersionPage::on_actionInstall_Forge_triggered()
void VersionPage::on_actionInstall_Fabric_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader");
if(!vlist)
{
if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this);
@ -486,17 +481,15 @@ void VersionPage::on_actionInstall_Fabric_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader");
if(!currentVersion.isEmpty())
{
if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (vselect.exec() && vselect.selectedVersion())
{
if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
preselect(m_profile->rowCount(QModelIndex())-1);
preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@ -504,8 +497,7 @@ void VersionPage::on_actionInstall_Fabric_triggered()
void VersionPage::on_actionInstall_Quilt_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader");
if(!vlist)
{
if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this);
@ -513,17 +505,15 @@ void VersionPage::on_actionInstall_Quilt_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader");
if(!currentVersion.isEmpty())
{
if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (vselect.exec() && vselect.selectedVersion())
{
if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
preselect(m_profile->rowCount(QModelIndex())-1);
preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@ -532,14 +522,12 @@ void VersionPage::on_actionAdd_Empty_triggered()
{
NewComponentDialog compdialog(QString(), QString(), this);
QStringList blacklist;
for(int i = 0; i < m_profile->rowCount(); i++)
{
for (int i = 0; i < m_profile->rowCount(); i++) {
auto comp = m_profile->getComponent(i);
blacklist.push_back(comp->getID());
}
compdialog.setBlacklist(blacklist);
if (compdialog.exec())
{
if (compdialog.exec()) {
qDebug() << "name:" << compdialog.name();
qDebug() << "uid:" << compdialog.uid();
m_profile->installEmpty(compdialog.uid(), compdialog.name());
@ -549,28 +537,26 @@ void VersionPage::on_actionAdd_Empty_triggered()
void VersionPage::on_actionInstall_LiteLoader_triggered()
{
auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader");
if(!vlist)
{
if (!vlist) {
return;
}
VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") +
m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!"));
auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader");
if(!currentVersion.isEmpty())
{
if (!currentVersion.isEmpty()) {
vselect.setCurrentVersion(currentVersion);
}
if (vselect.exec() && vselect.selectedVersion())
{
if (vselect.exec() && vselect.selectedVersion()) {
auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion(vselect.selectedVersion());
preselect(m_profile->rowCount(QModelIndex())-1);
preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer();
}
}
@ -585,7 +571,7 @@ void VersionPage::on_actionMinecraftFolder_triggered()
DesktopServices::openDirectory(m_inst->gameRoot(), true);
}
void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &previous)
void VersionPage::versionCurrent(const QModelIndex& current, const QModelIndex& previous)
{
currentIdx = current.row();
updateButtons(currentIdx);
@ -593,16 +579,13 @@ void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &
void VersionPage::preselect(int row)
{
if(row < 0)
{
if (row < 0) {
row = 0;
}
if(row >= m_profile->rowCount(QModelIndex()))
{
if (row >= m_profile->rowCount(QModelIndex())) {
row = m_profile->rowCount(QModelIndex()) - 1;
}
if(row < 0)
{
if (row < 0) {
return;
}
auto model_index = m_profile->index(row);
@ -615,11 +598,10 @@ void VersionPage::onGameUpdateError(QString error)
CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show();
}
Component * VersionPage::current()
ComponentPtr VersionPage::current()
{
auto row = currentRow();
if(row < 0)
{
if (row < 0) {
return nullptr;
}
return m_profile->getComponent(row);
@ -627,8 +609,7 @@ Component * VersionPage::current()
int VersionPage::currentRow()
{
if (ui->packageView->selectionModel()->selectedRows().isEmpty())
{
if (ui->packageView->selectionModel()->selectedRows().isEmpty()) {
return -1;
}
return ui->packageView->selectionModel()->selectedRows().first().row();
@ -637,18 +618,15 @@ int VersionPage::currentRow()
void VersionPage::on_actionCustomize_triggered()
{
auto version = currentRow();
if(version == -1)
{
if (version == -1) {
return;
}
auto patch = m_profile->getComponent(version);
if(!patch->getVersionFile())
{
if (!patch->getVersionFile()) {
// TODO: wait for the update task to finish here...
return;
}
if(!m_profile->customize(version))
{
if (!m_profile->customize(version)) {
// TODO: some error box here
}
updateButtons();
@ -658,13 +636,11 @@ void VersionPage::on_actionCustomize_triggered()
void VersionPage::on_actionEdit_triggered()
{
auto version = current();
if(!version)
{
if (!version) {
return;
}
auto filename = version->getFilename();
if(!QFileInfo::exists(filename))
{
if (!QFileInfo::exists(filename)) {
qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!";
return;
}
@ -674,12 +650,23 @@ void VersionPage::on_actionEdit_triggered()
void VersionPage::on_actionRevert_triggered()
{
auto version = currentRow();
if(version == -1)
{
if (version == -1) {
return;
}
if(!m_profile->revertToBase(version))
{
auto component = m_profile->getComponent(version);
auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"),
tr("You are about to revert \"%1\".\n"
"This is permanent and will completely revert your customizations.\n\n"
"Are you sure?")
.arg(component->getName()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
if (!m_profile->revertToBase(version)) {
// TODO: some error box here
}
updateButtons();
@ -687,7 +674,7 @@ void VersionPage::on_actionRevert_triggered()
m_container->refreshContainer();
}
void VersionPage::onFilterTextChanged(const QString &newContents)
void VersionPage::onFilterTextChanged(const QString& newContents)
{
m_filterModel->setFilterFixedString(newContents);
}

View File

@ -1,7 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -41,35 +46,27 @@
#include "minecraft/PackProfile.h"
#include "ui/pages/BasePage.h"
namespace Ui
{
namespace Ui {
class VersionPage;
}
class VersionPage : public QMainWindow, public BasePage
{
class VersionPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0);
public:
explicit VersionPage(MinecraftInstance* inst, QWidget* parent = 0);
virtual ~VersionPage();
virtual QString displayName() const override
{
return tr("Version");
}
virtual QString displayName() const override { return tr("Version"); }
virtual QIcon icon() const override;
virtual QString id() const override
{
return "version";
}
virtual QString helpPage() const override
{
return "Instance-Version";
}
virtual QString id() const override { return "version"; }
virtual QString helpPage() const override { return "Instance-Version"; }
virtual bool shouldDisplay() const override;
void retranslate() override;
private slots:
void openedImpl() override;
void closedImpl() override;
private slots:
void on_actionChange_version_triggered();
void on_actionInstall_Forge_triggered();
void on_actionInstall_Fabric_triggered();
@ -82,6 +79,8 @@ private slots:
void on_actionMove_down_triggered();
void on_actionAdd_to_Minecraft_jar_triggered();
void on_actionReplace_Minecraft_jar_triggered();
void on_actionImport_Components_triggered();
void on_actionAdd_Agents_triggered();
void on_actionRevert_triggered();
void on_actionEdit_triggered();
void on_actionInstall_mods_triggered();
@ -93,34 +92,34 @@ private slots:
void updateVersionControls();
private:
Component * current();
private:
ComponentPtr current();
int currentRow();
void updateButtons(int row = -1);
void preselect(int row = 0);
int doUpdate();
protected:
QMenu * createPopupMenu() override;
protected:
QMenu* createPopupMenu() override;
/// FIXME: this shouldn't be necessary!
bool reloadPackProfile();
private:
Ui::VersionPage *ui;
QSortFilterProxyModel *m_filterModel;
private:
Ui::VersionPage* ui;
QSortFilterProxyModel* m_filterModel;
std::shared_ptr<PackProfile> m_profile;
MinecraftInstance *m_inst;
MinecraftInstance* m_inst;
int currentIdx = 0;
bool controlsEnabled = false;
public slots:
void versionCurrent(const QModelIndex &current, const QModelIndex &previous);
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
private slots:
void updateRunningStatus(bool running);
public slots:
void versionCurrent(const QModelIndex& current, const QModelIndex& previous);
private slots:
void onGameUpdateError(QString error);
void packageCurrent(const QModelIndex &current, const QModelIndex &previous);
void showContextMenu(const QPoint &pos);
void onFilterTextChanged(const QString & newContents);
void packageCurrent(const QModelIndex& current, const QModelIndex& previous);
void showContextMenu(const QPoint& pos);
void onFilterTextChanged(const QString& newContents);
};

View File

@ -28,9 +28,6 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="ModListView" name="packageView">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
@ -48,11 +45,7 @@
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QLineEdit" name="filterEdit">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
<widget class="QLineEdit" name="filterEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="filterLabel">
@ -109,15 +102,15 @@
<addaction name="actionInstall_Fabric"/>
<addaction name="actionInstall_Quilt"/>
<addaction name="actionInstall_LiteLoader"/>
<addaction name="actionInstall_mods"/>
<addaction name="separator"/>
<addaction name="actionAdd_to_Minecraft_jar"/>
<addaction name="actionReplace_Minecraft_jar"/>
<addaction name="actionAdd_Agents"/>
<addaction name="actionAdd_Empty"/>
<addaction name="actionImport_Components"/>
<addaction name="separator"/>
<addaction name="actionMinecraftFolder"/>
<addaction name="actionLibrariesFolder"/>
<addaction name="separator"/>
<addaction name="actionReload"/>
<addaction name="actionDownload_All"/>
</widget>
@ -209,14 +202,6 @@
<string>Install the LiteLoader package.</string>
</property>
</action>
<action name="actionInstall_mods">
<property name="text">
<string>Install mods</string>
</property>
<property name="toolTip">
<string>Install normal mods.</string>
</property>
</action>
<action name="actionAdd_to_Minecraft_jar">
<property name="text">
<string>Add to Minecraft.jar</string>
@ -230,6 +215,14 @@
<string>Replace Minecraft.jar</string>
</property>
</action>
<action name="actionAdd_Agents">
<property name="text">
<string>Add Agents</string>
</property>
<property name="toolTip">
<string>Add Java agents.</string>
</property>
</action>
<action name="actionAdd_Empty">
<property name="text">
<string>Add Empty</string>
@ -270,6 +263,14 @@
<string>Open the instance's local libraries folder.</string>
</property>
</action>
<action name="actionImport_Components">
<property name="text">
<string>Import Components</string>
</property>
<property name="toolTip">
<string>Import existing component JSON files.</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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,6 +36,7 @@
*/
#include "WorldListPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_WorldListPage.h"
#include "minecraft/WorldList.h"
@ -43,9 +45,9 @@
#include <QKeyEvent>
#include <QClipboard>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include <QTreeView>
#include <QInputDialog>
#include <QProcess>
#include <Qt>
#include "tools/MCEditTool.h"
@ -105,6 +107,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl
auto head = ui->worldTreeView->header();
head->setSectionResizeMode(0, QHeaderView::Stretch);
head->setSectionResizeMode(1, QHeaderView::ResizeToContents);
head->setSectionResizeMode(4, QHeaderView::ResizeToContents);
connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged);
worldChanged(QModelIndex(), QModelIndex());
@ -113,11 +116,21 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl
void WorldListPage::openedImpl()
{
m_worlds->startWatching();
auto const setting_name = QString("WideBarVisibility_%1").arg(id());
if (!APPLICATION->settings()->contains(setting_name))
m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name);
else
m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name);
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
}
void WorldListPage::closedImpl()
{
m_worlds->stopWatching();
m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
}
WorldListPage::~WorldListPage()
@ -182,12 +195,14 @@ void WorldListPage::on_actionRemove_triggered()
if(!proxiedIndex.isValid())
return;
auto result = QMessageBox::question(this,
tr("Are you sure?"),
tr("This will remove the selected world permenantly.\n"
"The world will be gone forever (A LONG TIME).\n"
"\n"
"Do you want to continue?"));
auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
tr("You are about to delete \"%1\".\n"
"The world may be gone forever (A LONG TIME).\n\n"
"Are you sure?")
.arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if(result != QMessageBox::Yes)
{
return;
@ -324,6 +339,7 @@ void WorldListPage::mceditState(LoggedProcess::State state)
{
failed = true;
}
/* fallthrough */
case LoggedProcess::Running:
case LoggedProcess::Finished:
{

View File

@ -42,6 +42,8 @@
#include <Application.h>
#include <LoggedProcess.h>
#include "settings/Setting.h"
class WorldList;
namespace Ui
{
@ -102,6 +104,8 @@ private:
unique_qobject_ptr<LoggedProcess> m_mceditProcess;
bool m_mceditStarting = false;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
private slots:
void on_actionCopy_Seed_triggered();
void on_actionMCEdit_triggered();

View File

@ -109,7 +109,7 @@
</action>
<action name="actionRemove">
<property name="text">
<string>Remove</string>
<string>Delete</string>
</property>
</action>
<action name="actionMCEdit">

View File

@ -33,8 +33,8 @@
* limitations under the License.
*/
#include "VanillaPage.h"
#include "ui_VanillaPage.h"
#include "CustomPage.h"
#include "ui_CustomPage.h"
#include <QTabBar>
@ -46,32 +46,32 @@
#include "minecraft/VanillaInstanceCreationTask.h"
#include "ui/dialogs/NewInstanceDialog.h"
VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
: QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage)
CustomPage::CustomPage(NewInstanceDialog *dialog, QWidget *parent)
: QWidget(parent), dialog(dialog), ui(new Ui::CustomPage)
{
ui->setupUi(this);
ui->tabWidget->tabBar()->hide();
connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion);
connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion);
filterChanged();
connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged);
connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh);
connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion);
connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
connect(ui->quiltFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged);
connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh);
connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion);
connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged);
connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh);
}
void VanillaPage::openedImpl()
void CustomPage::openedImpl()
{
if(!initialized)
{
@ -85,19 +85,19 @@ void VanillaPage::openedImpl()
}
}
void VanillaPage::refresh()
void CustomPage::refresh()
{
ui->versionList->loadList();
}
void VanillaPage::loaderRefresh()
void CustomPage::loaderRefresh()
{
if(ui->noneFilter->isChecked())
return;
ui->loaderVersionList->loadList();
}
void VanillaPage::filterChanged()
void CustomPage::filterChanged()
{
QStringList out;
if(ui->alphaFilter->isChecked())
@ -116,7 +116,7 @@ void VanillaPage::filterChanged()
ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
}
void VanillaPage::loaderFilterChanged()
void CustomPage::loaderFilterChanged()
{
QString minecraftVersion;
if (m_selectedVersion)
@ -172,37 +172,37 @@ void VanillaPage::loaderFilterChanged()
ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
}
VanillaPage::~VanillaPage()
CustomPage::~CustomPage()
{
delete ui;
}
bool VanillaPage::shouldDisplay() const
bool CustomPage::shouldDisplay() const
{
return true;
}
void VanillaPage::retranslate()
void CustomPage::retranslate()
{
ui->retranslateUi(this);
}
BaseVersionPtr VanillaPage::selectedVersion() const
BaseVersion::Ptr CustomPage::selectedVersion() const
{
return m_selectedVersion;
}
BaseVersionPtr VanillaPage::selectedLoaderVersion() const
BaseVersion::Ptr CustomPage::selectedLoaderVersion() const
{
return m_selectedLoaderVersion;
}
QString VanillaPage::selectedLoader() const
QString CustomPage::selectedLoader() const
{
return m_selectedLoader;
}
void VanillaPage::suggestCurrent()
void CustomPage::suggestCurrent()
{
if (!isOpened)
{
@ -227,14 +227,14 @@ void VanillaPage::suggestCurrent()
dialog->setSuggestedIcon("default");
}
void VanillaPage::setSelectedVersion(BaseVersionPtr version)
void CustomPage::setSelectedVersion(BaseVersion::Ptr version)
{
m_selectedVersion = version;
suggestCurrent();
loaderFilterChanged();
}
void VanillaPage::setSelectedLoaderVersion(BaseVersionPtr version)
void CustomPage::setSelectedLoaderVersion(BaseVersion::Ptr version)
{
m_selectedLoaderVersion = version;
suggestCurrent();

View File

@ -43,21 +43,21 @@
namespace Ui
{
class VanillaPage;
class CustomPage;
}
class NewInstanceDialog;
class VanillaPage : public QWidget, public BasePage
class CustomPage : public QWidget, public BasePage
{
Q_OBJECT
public:
explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0);
virtual ~VanillaPage();
explicit CustomPage(NewInstanceDialog *dialog, QWidget *parent = 0);
virtual ~CustomPage();
virtual QString displayName() const override
{
return tr("Vanilla");
return tr("Custom");
}
virtual QIcon icon() const override
{
@ -76,13 +76,13 @@ public:
void openedImpl() override;
BaseVersionPtr selectedVersion() const;
BaseVersionPtr selectedLoaderVersion() const;
BaseVersion::Ptr selectedVersion() const;
BaseVersion::Ptr selectedLoaderVersion() const;
QString selectedLoader() const;
public slots:
void setSelectedVersion(BaseVersionPtr version);
void setSelectedLoaderVersion(BaseVersionPtr version);
void setSelectedVersion(BaseVersion::Ptr version);
void setSelectedLoaderVersion(BaseVersion::Ptr version);
private slots:
void filterChanged();
@ -96,9 +96,9 @@ private:
private:
bool initialized = false;
NewInstanceDialog *dialog = nullptr;
Ui::VanillaPage *ui = nullptr;
Ui::CustomPage *ui = nullptr;
bool m_versionSetByUser = false;
BaseVersionPtr m_selectedVersion;
BaseVersionPtr m_selectedLoaderVersion;
BaseVersion::Ptr m_selectedVersion;
BaseVersion::Ptr m_selectedLoaderVersion;
QString m_selectedLoader;
};

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>VanillaPage</class>
<widget class="QWidget" name="VanillaPage">
<class>CustomPage</class>
<widget class="QWidget" name="CustomPage">
<property name="geometry">
<rect>
<x>0</x>

View File

@ -57,7 +57,7 @@ public:
virtual ~ImportPage();
virtual QString displayName() const override
{
return tr("Import from zip");
return tr("Import");
}
virtual QIcon icon() const override
{

View File

@ -1,343 +1,82 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ModModel.h"
#include "BuildConfig.h"
#include "Json.h"
#include "ModPage.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include "minecraft/mod/ModFolderModel.h"
#include <QMessageBox>
#include <algorithm>
namespace ModPlatform {
namespace ResourceDownload {
// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted.
// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better?
static QHash<ListModel*, bool> s_running;
ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); }
ListModel::~ListModel()
{
s_running.find(this).value() = false;
}
auto ListModel::debugName() const -> QString
{
return m_parent->debugName();
}
ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
void ListModel::fetchMore(const QModelIndex& parent)
ResourceAPI::SearchArgs ModModel::createSearchArguments()
{
if (parent.isValid())
return;
if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
performPaginatedSearch();
}
auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
Q_ASSERT(profile);
Q_ASSERT(m_filter);
std::optional<std::list<Version>> versions{};
{ // Version filter
if (!m_filter->versions.empty())
versions = m_filter->versions;
}
ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) {
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;
}
case Qt::DecorationRole: {
if (m_logoMap.contains(pack.logoName)) {
return m_logoMap.value(pack.logoName);
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
// un-const-ify this
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
// Custom data
case UserDataTypes::TITLE:
return pack.name;
case UserDataTypes::DESCRIPTION:
return pack.description;
case UserDataTypes::SELECTED:
return m_parent->getDialog()->isModSelected(pack.name);
default:
break;
}
auto sort = getCurrentSortingMethodByIndex();
return {};
return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions };
}
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role)
ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid())
return false;
auto& pack = *m_packs[entry.row()];
auto profile = static_cast<MinecraftInstance const&>(m_base_instance).getPackProfile();
modpacks[pos] = value.value<ModPlatform::IndexedPack>();
Q_ASSERT(profile);
Q_ASSERT(m_filter);
return true;
std::optional<std::list<Version>> versions{};
if (!m_filter->versions.empty())
versions = m_filter->versions;
return { pack, versions, profile->getModLoaders() };
}
void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() },
[this, current, index](QJsonDocument& doc, QString addonId) {
if (!s_running.constFind(this).value())
return;
versionRequestSucceeded(doc, addonId, index);
});
auto& pack = *m_packs[entry.row()];
return { pack };
}
void ListModel::performPaginatedSearch()
void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->searchMods(
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() });
}
void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index)
{
m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) {
if (!s_running.constFind(this).value())
return;
infoRequestFinished(doc, pack, index);
});
}
void ListModel::refresh()
{
if (jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
} else {
beginResetModel();
modpacks.clear();
endResetModel();
searchState = None;
}
nextSearchOffset = 0;
performPaginatedSearch();
}
void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
{
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) {
return;
}
currentSearchTerm = term;
currentSort = sort;
setSearchTerm(term);
m_current_sort_index = sort;
refresh();
}
void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const
{
if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()
->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))
->getFullPath());
} else {
requestLogo(logo, logoUrl);
}
}
void ListModel::requestLogo(QString logo, QString url)
{
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) {
return;
}
MetaEntryPtr entry =
APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
}
auto allMods = static_cast<MinecraftInstance&>(m_base_instance).loaderModList()->allMods();
return std::any_of(allMods.cbegin(), allMods.cend(), [pack](Mod* mod) {
if (auto meta = mod->metadata(); meta)
return meta->provider == pack->provider && meta->project_id == pack->addonId;
return false;
});
QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
job->deleteLater();
emit logoFailed(logo);
});
job->start();
m_loadingLogos.append(logo);
}
/******** Request callbacks ********/
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::searchRequestFinished(QJsonDocument& doc)
{
jobPtr.reset();
QList<ModPlatform::IndexedPack> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ModPlatform::IndexedPack pack;
try {
loadIndexedPack(pack, packObj);
newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause();
continue;
}
}
if (packs.size() < 25) {
searchState = Finished;
} else {
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();
}
void ListModel::searchRequestFailed(QString reason)
{
auto failed_action = jobPtr->getFailedActions().at(0);
if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
} else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
QString("%1 %2")
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
}
jobPtr.reset();
if (searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
endResetModel();
nextSearchOffset = 0;
performPaginatedSearch();
} else {
searchState = Finished;
}
}
void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
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();
}
// Check if the index is still valid for this mod or not
if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) {
// Cache info :^)
QVariant new_pack;
new_pack.setValue(pack);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache mod info!";
}
}
m_parent->updateUi();
}
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index)
{
auto& current = m_parent->getCurrent();
if (addonId != current.addonId) {
return;
}
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
try {
loadIndexedPackVersions(current, arr);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause();
}
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache mod versions!";
}
m_parent->updateModVersions();
}
} // namespace ModPlatform
/******** Helpers ********/
auto ModPlatform::ListModel::getMineVersions() const -> std::list<Version>
{
return m_parent->getFilter()->versions;
}
} // namespace ResourceDownload

View File

@ -1,91 +1,54 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include "modplatform/ModIndex.h"
#include "net/NetJob.h"
#include "BaseInstance.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ModFilterWidget.h"
class ModPage;
class Version;
namespace ModPlatform {
namespace ResourceDownload {
using LogoMap = QMap<QString, QIcon>;
using LogoCallback = std::function<void (QString)>;
class ModPage;
class ListModel : public QAbstractListModel {
class ModModel : public ResourceModel {
Q_OBJECT
public:
ListModel(ModPage* parent);
~ListModel() override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; };
inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); };
auto debugName() const -> QString;
/* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
inline NetJob* activeJob() { return jobPtr.get(); }
ModModel(BaseInstance&, ResourceAPI* api);
/* Ask the API for more information */
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, QModelIndex index);
void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
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 loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0;
virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0;
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; };
void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
public slots:
void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason);
void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index);
void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index);
protected slots:
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
void performPaginatedSearch();
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0;
virtual auto getSorts() const -> const char** = 0;
void requestLogo(QString file, QString url);
inline auto getMineVersions() const -> std::list<Version>;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override;
protected:
ModPage* m_parent;
BaseInstance& m_base_instance;
QList<ModPlatform::IndexedPack> modpacks;
LogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
QString currentSearchTerm;
int currentSort = 0;
int nextSearchOffset = 0;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
NetJob::Ptr jobPtr;
std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
} // namespace ModPlatform
} // namespace ResourceDownload

View File

@ -1,7 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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,57 +37,29 @@
*/
#include "ModPage.h"
#include "Application.h"
#include "ui_ModPage.h"
#include "ui_ResourcePage.h"
#include <QDesktopServices>
#include <QKeyEvent>
#include <QRegularExpression>
#include <memory>
#include <HoeDown.h>
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
: QWidget(dialog)
, m_instance(instance)
, ui(new Ui::ModPage)
, dialog(dialog)
, m_fetch_progress(this, false)
, api(api)
namespace ResourceDownload {
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(ui->packView, &QListView::doubleClicked, this, &ModPage::onModSelected);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch);
ui->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount());
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packView->installEventFilter(this);
}
ModPage::~ModPage()
{
delete ui;
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
}
void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
@ -94,59 +69,17 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget.swap(widget);
ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount());
m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance));
m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: underline");
});
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: none");
});
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this,
[&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); });
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this,
[&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); });
}
/******** Qt things ********/
void ModPage::openedImpl()
{
updateSelectionButton();
triggerSearch();
}
auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
{
if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
keyEvent->accept();
return true;
} else {
if (m_search_timer.isActive())
m_search_timer.stop();
m_search_timer.start(350);
}
} else if (watched == ui->packView && event->type() == QEvent::KeyPress) {
auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) {
onModSelected();
// To have the 'select mod' button outlined instead of the 'review and confirm' one
ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
keyEvent->accept();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void ModPage::filterMods()
@ -156,110 +89,42 @@ void ModPage::filterMods()
void ModPage::triggerSearch()
{
auto changed = m_filter_widget->changed();
m_filter = m_filter_widget->getFilter();
if(changed){
ui->packView->clearSelection();
ui->packDescription->clear();
ui->versionSelectionBox->clear();
updateSelectionButton();
}
listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed);
m_fetch_progress.watch(listModel->activeJob());
}
QString ModPage::getSearchTerm() const
{
return ui->searchEdit->text();
}
void ModPage::setSearchTerm(QString term)
{
ui->searchEdit->setText(term);
}
void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
ui->versionSelectionBox->clear();
if (!curr.isValid()) { return; }
current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (!current.versionsLoaded) {
qDebug() << QString("Loading %1 mod versions").arg(debugName());
ui->modSelectionButton->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false);
listModel->requestModVersions(current, curr);
} 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)); }
updateSelectionButton();
}
if(!current.extraDataLoaded){
qDebug() << QString("Loading %1 mod info").arg(debugName());
listModel->requestModInfo(current, curr);
}
updateUi();
}
void ModPage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ModPage::onModSelected()
{
if (selectedVersion < 0)
return;
auto& version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name);
} else {
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed));
}
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
updateSelectionButton();
/* Force redraw on the mods list when the selection changes */
ui->packView->adjustSize();
static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
QMap<QString, QString> ModPage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth");
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
/******** Make changes to the UI ********/
void ModPage::retranslate()
void ModPage::updateVersionList()
{
ui->retranslateUi(this);
}
void ModPage::updateModVersions(int prev_count)
{
auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile();
m_ui->versionSelectionBox->clear();
auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
for (int i = 0; i < current.versions.size(); i++) {
auto version = current.versions[i];
auto current_pack = getCurrentPack();
if (!current_pack)
return;
for (int i = 0; i < current_pack->versions.size(); i++) {
auto version = current_pack->versions[i];
bool valid = false;
for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loader, so passing it changes nothing.
for (auto& mcVer : m_filter->versions) {
// NOTE: Flame doesn't care about loader, so passing it changes nothing.
if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) {
valid = true;
break;
@ -268,89 +133,22 @@ void ModPage::updateModVersions(int prev_count)
// Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out
if ((valid || m_filter->versions.empty()) && !optedOut(version))
ui->versionSelectionBox->addItem(version.version, QVariant(i));
m_ui->versionSelectionBox->addItem(version.version, QVariant(i));
}
if (ui->versionSelectionBox->count() == 0 && prev_count != 0) {
ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
void ModPage::updateSelectionButton()
void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& version,
const std::shared_ptr<ResourceFolderModel> base_model)
{
if (!isOpened || selectedVersion < 0) {
ui->modSelectionButton->setEnabled(false);
return;
}
ui->modSelectionButton->setEnabled(true);
auto& version = current.versions[selectedVersion];
if (!dialog->isModSelected(current.name, version.fileName)) {
ui->modSelectionButton->setText(tr("Select mod for download"));
} else {
ui->modSelectionButton->setText(tr("Deselect mod for download"));
}
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_model->addPack(pack, version, base_model, is_indexed);
}
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>";
HoeDown h;
ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8())));
ui->packDescription->flush();
}
} // namespace ResourceDownload

View File

@ -1,104 +1,79 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QWidget>
#include "Application.h"
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
namespace Ui {
class ResourcePage;
}
namespace ResourceDownload {
class ModDownloadDialog;
namespace Ui {
class ModPage;
}
/* This page handles most logic related to browsing and selecting mods to download. */
class ModPage : public QWidget, public BasePage {
class ModPage : public ResourcePage {
Q_OBJECT
public:
template<typename T>
static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
template <typename T>
static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<ModModel*>(page->getModel());
auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page);
auto filter_widget =
ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
page->setFilterWidget(filter_widget);
model->setFilter(page->getFilter());
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
~ModPage() override;
//: The plural version of 'mod'
[[nodiscard]] inline QString resourcesString() const override { return tr("mods"); }
//: The singular version of 'mods'
[[nodiscard]] inline QString resourceString() const override { return tr("mod"); }
/* Affects what the user sees */
auto displayName() const -> QString override = 0;
auto icon() const -> QIcon override = 0;
auto id() const -> QString override = 0;
auto helpPage() const -> QString override = 0;
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
/* Used internally */
virtual auto metaEntryBase() const -> QString = 0;
virtual auto debugName() const -> QString = 0;
void addResourceToPage(ModPlatform::IndexedPack::Ptr,
ModPlatform::IndexedVersion&,
const std::shared_ptr<ResourceFolderModel>) override;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver,
QString mineVer,
std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
void retranslate() override;
void updateUi();
auto shouldDisplay() const -> bool override = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0;
virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
auto apiProvider() -> ModAPI* { return api.get(); };
[[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
/** Get the current term in the search bar. */
auto getSearchTerm() const -> QString;
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1);
void openedImpl() override;
auto eventFilter(QObject* watched, QEvent* event) -> bool override;
BaseInstance* m_instance;
public slots:
void updateVersionList() override;
protected:
ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
void updateSelectionButton();
ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
protected slots:
virtual void filterMods();
void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onModSelected();
void triggerSearch() override;
protected:
Ui::ModPage* ui = nullptr;
ModDownloadDialog* dialog = nullptr;
unique_qobject_ptr<ModFilterWidget> m_filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter;
ProgressWidget m_fetch_progress;
ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> api;
int selectedVersion = -1;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,493 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ResourceModel.h"
#include <QCryptographicHash>
#include <QIcon>
#include <QList>
#include <QMessageBox>
#include <QPixmapCache>
#include <QUrl>
#include <algorithm>
#include <memory>
#include "Application.h"
#include "BuildConfig.h"
#include "Json.h"
#include "net/Download.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
#include "ui/widgets/ProjectItem.h"
namespace ResourceDownload {
QHash<ResourceModel*, bool> ResourceModel::s_running_models;
ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api)
{
s_running_models.insert(this, true);
}
ResourceModel::~ResourceModel()
{
s_running_models.find(this).value() = false;
}
auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
{
int pos = index.row();
if (pos >= m_packs.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
auto pack = m_packs.at(pos);
switch (role) {
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;
}
case Qt::DecorationRole: {
if (auto icon_or_none = const_cast<ResourceModel*>(this)->getIcon(const_cast<QModelIndex&>(index), pack->logoUrl);
icon_or_none.has_value())
return icon_or_none.value();
return APPLICATION->getThemedIcon("screenshot-placeholder");
}
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
// Custom data
case UserDataTypes::TITLE:
return pack->name;
case UserDataTypes::DESCRIPTION:
return pack->description;
case UserDataTypes::SELECTED:
return pack->isAnyVersionSelected();
case UserDataTypes::INSTALLED:
return this->isPackInstalled(pack);
default:
break;
}
return {};
}
QHash<int, QByteArray> ResourceModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[Qt::ToolTipRole] = "toolTip";
roles[Qt::DecorationRole] = "decoration";
roles[Qt::SizeHintRole] = "sizeHint";
roles[Qt::UserRole] = "pack";
roles[UserDataTypes::TITLE] = "title";
roles[UserDataTypes::DESCRIPTION] = "description";
roles[UserDataTypes::SELECTED] = "selected";
roles[UserDataTypes::INSTALLED] = "installed";
return roles;
}
bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
int pos = index.row();
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
return false;
m_packs[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
emit dataChanged(index, index);
return true;
}
QString ResourceModel::debugName() const
{
return "ResourceDownload (Model)";
}
void ResourceModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid() || m_search_state == SearchState::Finished)
return;
search();
}
void ResourceModel::search()
{
if (hasActiveSearchJob())
return;
auto args{ createSearchArguments() };
auto callbacks{ createSearchCallbacks() };
// Use defaults if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this](auto& doc) {
if (!s_running_models.constFind(this).value())
return;
searchRequestSucceeded(doc);
};
if (!callbacks.on_fail)
callbacks.on_fail = [this](QString reason, int network_error_code) {
if (!s_running_models.constFind(this).value())
return;
searchRequestFailed(reason, network_error_code);
};
if (!callbacks.on_abort)
callbacks.on_abort = [this] {
if (!s_running_models.constFind(this).value())
return;
searchRequestAborted();
};
if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job)
runSearchJob(job);
}
void ResourceModel::loadEntry(QModelIndex& entry)
{
auto const& pack = m_packs[entry.row()];
if (!hasActiveInfoJob())
m_current_info_job.clear();
if (!pack->versionsLoaded) {
auto args{ createVersionsArguments(entry) };
auto callbacks{ createVersionsCallbacks(entry) };
// Use default if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
if (!s_running_models.constFind(this).value())
return;
versionRequestSucceeded(doc, pack, entry);
};
if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job)
runInfoJob(job);
}
if (!pack->extraDataLoaded) {
auto args{ createInfoArguments(entry) };
auto callbacks{ createInfoCallbacks(entry) };
// Use default if no callbacks are set
if (!callbacks.on_succeed)
callbacks.on_succeed = [this, entry](auto& doc, auto pack) {
if (!s_running_models.constFind(this).value())
return;
infoRequestSucceeded(doc, pack, entry);
};
if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)
runInfoJob(job);
}
}
void ResourceModel::refresh()
{
bool reset_requested = false;
if (hasActiveInfoJob()) {
m_current_info_job.abort();
reset_requested = true;
}
if (hasActiveSearchJob()) {
m_current_search_job->abort();
reset_requested = true;
}
if (reset_requested) {
m_search_state = SearchState::ResetRequested;
return;
}
clearData();
m_search_state = SearchState::None;
m_next_search_offset = 0;
search();
}
void ResourceModel::clearData()
{
beginResetModel();
m_packs.clear();
endResetModel();
}
void ResourceModel::runSearchJob(Task::Ptr ptr)
{
m_current_search_job.reset(ptr); // clean up first
m_current_search_job->start();
}
void ResourceModel::runInfoJob(Task::Ptr ptr)
{
if (!m_current_info_job.isRunning())
m_current_info_job.clear();
m_current_info_job.addTask(ptr);
if (!m_current_info_job.isRunning())
m_current_info_job.run();
}
std::optional<ResourceAPI::SortingMethod> ResourceModel::getCurrentSortingMethodByIndex() const
{
std::optional<ResourceAPI::SortingMethod> sort{};
{ // Find sorting method by ID
auto sorting_methods = getSortingMethods();
auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(),
[this](auto const& e) { return m_current_sort_index == e.index; });
if (method != sorting_methods.constEnd())
sort = *method;
}
return sort;
}
std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
{
QPixmap pixmap;
if (QPixmapCache::find(url.toString(), &pixmap))
return { pixmap };
if (!m_current_icon_job)
m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network()));
if (m_currently_running_icon_actions.contains(url))
return {};
if (m_failed_icon_actions.contains(url))
return {};
auto cache_entry = APPLICATION->metacache()->resolveEntry(
metaEntryBase(),
QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
auto icon_fetch_action = Net::Download::makeCached(url, cache_entry);
auto full_file_path = cache_entry->getFullPath();
connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
auto icon = QIcon(full_file_path);
QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
m_currently_running_icon_actions.remove(url);
emit dataChanged(index, index, { Qt::DecorationRole });
});
connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
m_currently_running_icon_actions.remove(url);
m_failed_icon_actions.insert(url);
});
m_currently_running_icon_actions.insert(url);
m_current_icon_job->addNetAction(icon_fetch_action);
if (!m_current_icon_job->isRunning())
QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start);
return {};
}
// No 'forgor to implement' shall pass here :blobfox_knife:
#define NEED_FOR_CALLBACK_ASSERT(name) \
Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.")
QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const
{
NEED_FOR_CALLBACK_ASSERT("documentToArray");
return {};
}
void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&)
{
NEED_FOR_CALLBACK_ASSERT("loadIndexedPack");
}
void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&)
{
NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo");
}
void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&)
{
NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions");
}
/* Default callbacks */
void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
{
QList<ModPlatform::IndexedPack::Ptr> newList;
auto packs = documentToArray(doc);
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
[&pack](const DownloadTaskPtr i) {
const auto ipack = i->getPack();
return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
});
sel != m_selected.end()) {
newList.append(sel->get()->getPack());
} else
newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
}
}
if (packs.size() < 25) {
m_search_state = SearchState::Finished;
} else {
m_next_search_offset += 25;
m_search_state = SearchState::CanFetchMore;
}
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (newList.size() == 0)
return;
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
m_packs.append(newList);
endInsertRows();
}
void ResourceModel::searchRequestFailed(QString reason, int network_error_code)
{
switch (network_error_code) {
default:
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
break;
case 409:
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME)));
break;
}
m_search_state = SearchState::Finished;
}
void ResourceModel::searchRequestAborted()
{
if (m_search_state != SearchState::ResetRequested)
qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!";
// Retry fetching
clearData();
m_next_search_offset = 0;
search();
}
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
if (pack.addonId != current_pack->addonId)
return;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
loadIndexedPackVersions(*current_pack, arr);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
}
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current_pack);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache resource versions!";
return;
}
emit versionListUpdated();
}
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
if (pack.addonId != current_pack->addonId)
return;
try {
auto obj = Json::requireObject(doc);
loadExtraPackInfo(*current_pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
}
// Cache info :^)
QVariant new_pack;
new_pack.setValue(current_pack);
if (!setData(index, new_pack, Qt::UserRole)) {
qWarning() << "Failed to cache resource info!";
return;
}
emit projectInfoUpdated();
}
void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed,
QString custom_target_folder)
{
version.is_currently_selected = true;
m_selected.append(makeShared<ResourceDownloadTask>(pack, version, packs, is_indexed, custom_target_folder));
}
void ResourceModel::removePack(const QString& rem)
{
auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); };
#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
m_selected.removeIf(pred);
#else
{
for (auto it = m_selected.begin(); it != m_selected.end();)
if (pred(*it))
it = m_selected.erase(it);
else
++it;
}
#endif
auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; });
if (pack == m_packs.end()) { // ignore it if is not in the current search
return;
}
if (!pack->get()->versionsLoaded) {
return;
}
for (auto& ver : pack->get()->versions)
ver.is_currently_selected = false;
}
} // namespace ResourceDownload

View File

@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <optional>
#include <QAbstractListModel>
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/ConcurrentTask.h"
class NetJob;
class ResourceAPI;
namespace ModPlatform {
struct IndexedPack;
}
namespace ResourceDownload {
class ResourceModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
public:
using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
ResourceModel(ResourceAPI* api);
~ResourceModel() override;
[[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override;
[[nodiscard]] auto roleNames() const -> QHash<int, QByteArray> override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
[[nodiscard]] virtual auto debugName() const -> QString;
[[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
[[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); }
[[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; }
[[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }
[[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); }
[[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); }
[[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; }
[[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); }
public slots:
void fetchMore(const QModelIndex& parent) override;
// NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
inline bool canFetchMore(const QModelIndex& parent) const override
{
return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore;
}
void setSearchTerm(QString term) { m_search_term = term; }
virtual ResourceAPI::SearchArgs createSearchArguments() = 0;
virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; }
virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0;
virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; }
virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0;
virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; }
/** Requests the API for more entries. */
virtual void search();
/** Applies any processing / extra requests needed to fully load the specified entry's information. */
virtual void loadEntry(QModelIndex&);
/** Schedule a refresh, clearing the current state. */
void refresh();
/** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
void addPack(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed = false,
QString custom_target_folder = {});
void removePack(const QString& rem);
QList<DownloadTaskPtr> selectedPacks() { return m_selected; }
protected:
/** Resets the model's data. */
void clearData();
void runSearchJob(Task::Ptr);
void runInfoJob(Task::Ptr);
[[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional<ResourceAPI::SortingMethod>;
/** Converts a JSON document to a common array format.
*
* This is needed so that different providers, with different JSON structures, can be parsed
* uniformally. You NEED to re-implement this if you intend on using the default callbacks.
*/
[[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray;
/** Functions to load data into a pack.
*
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
*/
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; }
protected:
/* Basic search parameters */
enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
int m_next_search_offset = 0;
QString m_search_term;
unsigned int m_current_sort_index = 0;
std::unique_ptr<ResourceAPI> m_api;
// Job for searching for new entries
shared_qobject_ptr<Task> m_current_search_job;
// Job for fetching versions and extra info on existing entries
ConcurrentTask m_current_info_job;
shared_qobject_ptr<NetJob> m_current_icon_job;
QSet<QUrl> m_currently_running_icon_actions;
QSet<QUrl> m_failed_icon_actions;
QList<ModPlatform::IndexedPack::Ptr> m_packs;
QList<DownloadTaskPtr> m_selected;
// HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
// This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
static QHash<ResourceModel*, bool> s_running_models;
private:
/* Default search request callbacks */
void searchRequestSucceeded(QJsonDocument&);
void searchRequestFailed(QString reason, int network_error_code);
void searchRequestAborted();
void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&);
signals:
void versionListUpdated();
void projectInfoUpdated();
};
} // namespace ResourceDownload

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ResourcePackModel.h"
#include <QMessageBox>
namespace ResourceDownload {
ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst){};
/******** Make data requests ********/
ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments()
{
auto sort = getCurrentSortingMethodByIndex();
return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort };
}
ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort)
{
if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) {
return;
}
setSearchTerm(term);
m_current_sort_index = sort;
refresh();
}
} // namespace ResourceDownload

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include "BaseInstance.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/modplatform/ResourceModel.h"
class Version;
namespace ResourceDownload {
class ResourcePackResourceModel : public ResourceModel {
Q_OBJECT
public:
ResourcePackResourceModel(BaseInstance const&, ResourceAPI*);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ResourcePackPage.h"
#include "ui_ResourcePage.h"
#include "ResourcePackModel.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include <QRegularExpression>
namespace ResourceDownload {
ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance)
: ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected);
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void ResourcePackResourcePage::triggerSearch()
{
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
updateSelectionButton();
static_cast<ResourcePackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
QMap<QString, QString> ResourcePackResourcePage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth");
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), "curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
} // namespace ResourceDownload

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
namespace Ui {
class ResourcePage;
}
namespace ResourceDownload {
class ResourcePackDownloadDialog;
class ResourcePackResourcePage : public ResourcePage {
Q_OBJECT
public:
template <typename T>
static T* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<ResourcePackResourceModel*>(page->getModel());
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
//: The plural version of 'resource pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("resource packs"); }
//: The singular version of 'resource packs'
[[nodiscard]] inline QString resourceString() const override { return tr("resource pack"); }
[[nodiscard]] bool supportsFiltering() const override { return false; };
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
protected:
ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance);
protected slots:
void triggerSearch() override;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,435 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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 "ResourcePage.h"
#include "modplatform/ModIndex.h"
#include "ui_ResourcePage.h"
#include <QDesktopServices>
#include <QKeyEvent>
#include "Markdown.h"
#include "ResourceDownloadTask.h"
#include "minecraft/MinecraftInstance.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProjectItem.h"
namespace ResourceDownload {
ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance)
: QWidget(parent), m_base_instance(base_instance), m_ui(new Ui::ResourcePage), m_parent_dialog(parent), m_fetch_progress(this, false)
{
m_ui->setupUi(this);
m_ui->searchEdit->installEventFilter(this);
m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
m_search_timer.setTimerType(Qt::TimerType::CoarseTimer);
m_search_timer.setSingleShot(true);
connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch);
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
m_ui->packView->installEventFilter(this);
connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl);
}
ResourcePage::~ResourcePage()
{
delete m_ui;
if (m_model)
delete m_model;
}
void ResourcePage::retranslate()
{
m_ui->retranslateUi(this);
}
void ResourcePage::openedImpl()
{
if (!supportsFiltering())
m_ui->resourceFilterButton->setVisible(false);
//: String in the search bar of the mod downloading dialog
m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString()));
m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
updateSelectionButton();
triggerSearch();
m_ui->searchEdit->setFocus();
}
auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool
{
if (event->type() == QEvent::KeyPress) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
if (watched == m_ui->searchEdit) {
if (keyEvent->key() == Qt::Key_Return) {
triggerSearch();
keyEvent->accept();
return true;
} else {
if (m_search_timer.isActive())
m_search_timer.stop();
m_search_timer.start(350);
}
} else if (watched == m_ui->packView) {
if (keyEvent->key() == Qt::Key_Return) {
onResourceSelected();
// To have the 'select mod' button outlined instead of the 'review and confirm' one
m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason);
m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason);
keyEvent->accept();
return true;
}
}
}
return QWidget::eventFilter(watched, event);
}
QString ResourcePage::getSearchTerm() const
{
return m_ui->searchEdit->text();
}
void ResourcePage::setSearchTerm(QString term)
{
m_ui->searchEdit->setText(term);
}
void ResourcePage::addSortings()
{
Q_ASSERT(m_model);
auto sorts = m_model->getSortingMethods();
std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; });
for (auto&& sorting : sorts)
m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
}
bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack)
{
QVariant v;
v.setValue(pack);
return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
}
ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const
{
return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
}
void ResourcePage::updateUi()
{
auto current_pack = getCurrentPack();
if (!current_pack) {
m_ui->packDescription->setHtml({});
m_ui->packDescription->flush();
return;
}
QString text = "";
QString name = current_pack->name;
if (current_pack->websiteUrl.isEmpty())
text = name;
else
text = "<a href=\"" + current_pack->websiteUrl + "\">" + name + "</a>";
if (!current_pack->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_pack->authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
}
if (current_pack->extraDataLoaded) {
if (!current_pack->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_pack->extraData.donate) {
donates.append(donateToStr(donate));
}
text += donates.join(", ");
}
if (!current_pack->extraData.issuesUrl.isEmpty() || !current_pack->extraData.sourceUrl.isEmpty() ||
!current_pack->extraData.wikiUrl.isEmpty() || !current_pack->extraData.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
if (!current_pack->extraData.issuesUrl.isEmpty())
text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack->extraData.issuesUrl) + "<br>";
if (!current_pack->extraData.wikiUrl.isEmpty())
text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack->extraData.wikiUrl) + "<br>";
if (!current_pack->extraData.sourceUrl.isEmpty())
text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack->extraData.sourceUrl) + "<br>";
if (!current_pack->extraData.discordUrl.isEmpty())
text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack->extraData.discordUrl) + "<br>";
}
text += "<hr>";
m_ui->packDescription->setHtml(
text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)));
m_ui->packDescription->flush();
}
void ResourcePage::updateSelectionButton()
{
if (!isOpened || m_selected_version_index < 0) {
m_ui->resourceSelectionButton->setEnabled(false);
return;
}
m_ui->resourceSelectionButton->setEnabled(true);
if (auto current_pack = getCurrentPack(); current_pack) {
if (!current_pack->isVersionSelected(m_selected_version_index))
m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
else
m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
} else {
qWarning() << "Tried to update the selected button but there is not a pack selected";
}
}
void ResourcePage::updateVersionList()
{
auto current_pack = getCurrentPack();
m_ui->versionSelectionBox->blockSignals(true);
m_ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->blockSignals(false);
if (current_pack)
for (int i = 0; i < current_pack->versions.size(); i++) {
auto& version = current_pack->versions[i];
if (optedOut(version))
continue;
m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i));
}
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
{
if (!curr.isValid()) {
return;
}
auto current_pack = getCurrentPack();
bool request_load = false;
if (!current_pack || !current_pack->versionsLoaded) {
m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
m_ui->resourceSelectionButton->setEnabled(false);
request_load = true;
} else {
updateVersionList();
}
if (current_pack && !current_pack->extraDataLoaded)
request_load = true;
if (request_load)
m_model->loadEntry(curr);
updateUi();
}
void ResourcePage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
m_selected_version_index = -1;
return;
}
m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt();
updateSelectionButton();
}
void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version)
{
m_parent_dialog->addResource(pack, version);
}
void ResourcePage::removeResourceFromDialog(const QString& pack_name)
{
m_parent_dialog->removeResource(pack_name);
}
void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& ver,
const std::shared_ptr<ResourceFolderModel> base_model)
{
m_model->addPack(pack, ver, base_model);
}
void ResourcePage::removeResourceFromPage(const QString& name)
{
m_model->removePack(name);
}
void ResourcePage::onResourceSelected()
{
if (m_selected_version_index < 0)
return;
auto current_pack = getCurrentPack();
if (!current_pack || !current_pack->versionsLoaded)
return;
auto& version = current_pack->versions[m_selected_version_index];
if (version.is_currently_selected)
removeResourceFromDialog(current_pack->name);
else
addResourceToDialog(current_pack, version);
// Save the modified pack (and prevent warning in release build)
[[maybe_unused]] bool set = setCurrentPack(current_pack);
Q_ASSERT(set);
updateSelectionButton();
/* Force redraw on the resource list when the selection changes */
m_ui->packView->repaint();
}
void ResourcePage::openUrl(const QUrl& url)
{
// do not allow other url schemes for security reasons
if (!(url.scheme() == "http" || url.scheme() == "https")) {
qWarning() << "Unsupported scheme" << url.scheme();
return;
}
// detect URLs and search instead
const QString address = url.host() + url.path();
QRegularExpressionMatch match;
QString page;
auto handlers = urlHandlers();
for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) {
auto&& [regex, candidate] = *it;
if (match = QRegularExpression(regex).match(address); match.hasMatch()) {
page = candidate;
break;
}
}
if (!page.isNull()) {
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) {
m_parent_dialog->selectPage(page);
auto newPage = m_parent_dialog->getSelectedPage();
QLineEdit* searchEdit = newPage->m_ui->searchEdit;
auto model = newPage->m_model;
QListView* view = newPage->m_ui->packView;
auto jump = [url, slug, model, view] {
for (int row = 0; row < model->rowCount({}); row++) {
const QModelIndex index = model->index(row);
const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (pack.slug == slug) {
view->setCurrentIndex(index);
return;
}
}
// The final fallback.
QDesktopServices::openUrl(url);
};
searchEdit->setText(slug);
newPage->triggerSearch();
if (model->hasActiveSearchJob())
connect(model->activeSearchJob().get(), &Task::finished, jump);
else
jump();
return;
}
}
// open in the user's web browser
QDesktopServices::openUrl(url);
}
} // namespace ResourceDownload

View File

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QTimer>
#include <QWidget>
#include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProgressWidget.h"
namespace Ui {
class ResourcePage;
}
class BaseInstance;
namespace ResourceDownload {
class ResourceDownloadDialog;
class ResourceModel;
class ResourcePage : public QWidget, public BasePage {
Q_OBJECT
public:
using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
~ResourcePage() override;
/* Affects what the user sees */
[[nodiscard]] auto displayName() const -> QString override = 0;
[[nodiscard]] auto icon() const -> QIcon override = 0;
[[nodiscard]] auto id() const -> QString override = 0;
[[nodiscard]] auto helpPage() const -> QString override = 0;
[[nodiscard]] bool shouldDisplay() const override = 0;
/* Used internally */
[[nodiscard]] virtual auto metaEntryBase() const -> QString = 0;
[[nodiscard]] virtual auto debugName() const -> QString = 0;
//: The plural version of 'resource'
[[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); }
//: The singular version of 'resources'
[[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); }
/* Features this resource's page supports */
[[nodiscard]] virtual bool supportsFiltering() const = 0;
void retranslate() override;
void openedImpl() override;
auto eventFilter(QObject* watched, QEvent* event) -> bool override;
/** Get the current term in the search bar. */
[[nodiscard]] auto getSearchTerm() const -> QString;
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
[[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr);
[[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr;
[[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
[[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
protected:
ResourcePage(ResourceDownloadDialog* parent, BaseInstance&);
void addSortings();
public slots:
virtual void updateUi();
virtual void updateSelectionButton();
virtual void updateVersionList();
void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
void removeResourceFromDialog(const QString& pack_name);
virtual void removeResourceFromPage(const QString& name);
virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, const std::shared_ptr<ResourceFolderModel>);
QList<DownloadTaskPtr> selectedPacks() { return m_model->selectedPacks(); }
bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); }
protected slots:
virtual void triggerSearch() {}
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onResourceSelected();
// NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12
/** Associates regex expressions to pages in the order they're given in the map. */
virtual QMap<QString, QString> urlHandlers() const = 0;
virtual void openUrl(const QUrl&);
/** Whether the version is opted out or not. Currently only makes sense in CF. */
virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; };
public:
BaseInstance& m_base_instance;
protected:
Ui::ResourcePage* m_ui;
ResourceDownloadDialog* m_parent_dialog = nullptr;
ResourceModel* m_model = nullptr;
int m_selected_version_index = -1;
ProgressWidget m_fetch_progress;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
};
} // namespace ResourceDownload

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModPage</class>
<widget class="QWidget" name="ModPage">
<class>ResourcePage</class>
<widget class="QWidget" name="ResourcePage">
<property name="geometry">
<rect>
<x>0</x>
@ -16,10 +16,10 @@
<item row="1" column="2">
<widget class="ProjectDescriptionPage" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
<bool>false</bool>
</property>
<property name="openLinks">
<bool>true</bool>
<bool>false</bool>
</property>
</widget>
</item>
@ -49,11 +49,7 @@
</widget>
</item>
<item row="0" column="0">
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search for mods...</string>
</property>
</widget>
<widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0">
@ -74,16 +70,12 @@
<widget class="QComboBox" name="sortByBox"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="modSelectionButton">
<property name="text">
<string>Select mod for download</string>
</property>
</widget>
<widget class="QPushButton" name="resourceSelectionButton"/>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="modFilterButton">
<widget class="QPushButton" name="resourceFilterButton">
<property name="text">
<string>Filter options</string>
</property>

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ShaderPackModel.h"
#include <QMessageBox>
namespace ResourceDownload {
ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst){};
/******** Make data requests ********/
ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments()
{
auto sort = getCurrentSortingMethodByIndex();
return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort };
}
ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)
{
if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) {
return;
}
setSearchTerm(term);
m_current_sort_index = sort;
refresh();
}
} // namespace ResourceDownload

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include "BaseInstance.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/modplatform/ResourceModel.h"
class Version;
namespace ResourceDownload {
class ShaderPackResourceModel : public ResourceModel {
Q_OBJECT
public:
ShaderPackResourceModel(BaseInstance const&, ResourceAPI*);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ShaderPackPage.h"
#include "ui_ResourcePage.h"
#include "ShaderPackModel.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include <QRegularExpression>
namespace ResourceDownload {
ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected);
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void ShaderPackResourcePage::triggerSearch()
{
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
updateSelectionButton();
static_cast<ShaderPackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
QMap<QString, QString> ShaderPackResourcePage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth");
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"),
"curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& version,
const std::shared_ptr<ResourceFolderModel> base_model)
{
QString custom_target_folder;
if (version.loaders.contains(QStringLiteral("canvas")))
custom_target_folder = QStringLiteral("resourcepacks");
m_model->addPack(pack, version, base_model, false, custom_target_folder);
}
} // namespace ResourceDownload

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ShaderPackModel.h"
namespace Ui {
class ResourcePage;
}
namespace ResourceDownload {
class ShaderPackDownloadDialog;
class ShaderPackResourcePage : public ResourcePage {
Q_OBJECT
public:
template <typename T>
static T* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<ShaderPackResourceModel*>(page->getModel());
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
//: The plural version of 'shader pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("shader packs"); }
//: The singular version of 'shader packs'
[[nodiscard]] inline QString resourceString() const override { return tr("shader pack"); }
[[nodiscard]] bool supportsFiltering() const override { return false; };
void addResourceToPage(ModPlatform::IndexedPack::Ptr,
ModPlatform::IndexedVersion&,
const std::shared_ptr<ResourceFolderModel>) override;
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
protected:
ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance);
protected slots:
void triggerSearch() override;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "TexturePackModel.h"
#include "Application.h"
#include "meta/Index.h"
#include "meta/Version.h"
static std::list<Version> s_availableVersions = {};
namespace ResourceDownload {
TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api)
: ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft"))
{
if (!m_version_list->isLoaded()) {
qDebug() << "Loading version list...";
auto task = m_version_list->getLoadTask();
if (!task->isRunning())
task->start();
}
}
void waitOnVersionListLoad(Meta::VersionList::Ptr version_list)
{
QEventLoop load_version_list_loop;
QTimer time_limit_for_list_load;
time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer);
time_limit_for_list_load.setSingleShot(true);
time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit);
time_limit_for_list_load.start(4000);
auto task = version_list->getLoadTask();
QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit);
load_version_list_loop.exec();
if (time_limit_for_list_load.isActive())
time_limit_for_list_load.stop();
}
ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments()
{
if (s_availableVersions.empty())
waitOnVersionListLoad(m_version_list);
auto args = ResourcePackResourceModel::createSearchArguments();
if (!m_version_list->isLoaded()) {
qCritical() << "The version list could not be loaded. Falling back to showing all entries.";
return args;
}
if (s_availableVersions.empty()) {
for (auto&& version : m_version_list->versions()) {
// FIXME: This duplicates the logic in meta for the 'texturepacks' trait. However, we don't have access to that
// information from the index file alone. Also, downloading every version's file isn't a very good idea.
if (auto ver = version->toComparableVersion(); ver <= maximumTexturePackVersion())
s_availableVersions.push_back(ver);
}
}
Q_ASSERT(!s_availableVersions.empty());
args.versions = s_availableVersions;
return args;
}
ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto args = ResourcePackResourceModel::createVersionsArguments(entry);
if (!m_version_list->isLoaded()) {
qCritical() << "The version list could not be loaded. Falling back to showing all entries.";
return args;
}
args.mcVersions = s_availableVersions;
return args;
}
} // namespace ResourceDownload

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "meta/VersionList.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
namespace ResourceDownload {
class TexturePackResourceModel : public ResourcePackResourceModel {
Q_OBJECT
public:
TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api);
[[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; }
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
protected:
Meta::VersionList::Ptr m_version_list;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "ui_ResourcePage.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include "ui/pages/modplatform/ResourcePackPage.h"
#include "ui/pages/modplatform/TexturePackModel.h"
namespace Ui {
class ResourcePage;
}
namespace ResourceDownload {
class TexturePackDownloadDialog;
class TexturePackResourcePage : public ResourcePackResourcePage {
Q_OBJECT
public:
template <typename T>
static T* create(TexturePackDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<TexturePackResourceModel*>(page->getModel());
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
//: The plural version of 'texture pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("texture packs"); }
//: The singular version of 'texture packs'
[[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); }
protected:
TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
: ResourcePackResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &TexturePackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected);
}
};
} // namespace ResourceDownload

View File

@ -20,7 +20,8 @@
#include <modplatform/atlauncher/ATLPackIndex.h>
#include <Version.h>
#include <MMCStrings.h>
#include "StringUtils.h"
namespace Atl {
@ -86,7 +87,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co
return lv < rv;
}
else if (currentSorting == ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
// Invalid sorting set, somehow...

View File

@ -16,62 +16,49 @@
#include "AtlListModel.h"
#include <BuildConfig.h>
#include <Application.h>
#include <BuildConfig.h>
#include <Json.h>
namespace Atl {
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
ListModel::~ListModel() {}
int ListModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : modpacks.size();
}
ListModel::~ListModel()
int ListModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : 1;
}
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
QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
{
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
ATLauncher::IndexedPack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
if (role == Qt::DisplayRole) {
return pack.name;
}
else if (role == Qt::ToolTipRole)
{
} else if (role == Qt::ToolTipRole) {
return pack.name;
}
else if(role == Qt::DecorationRole)
{
if(m_logoMap.contains(pack.safeName))
{
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.safeName)) {
return (m_logoMap.value(pack.safeName));
}
auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder");
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
((ListModel *)this)->requestLogo(pack.safeName, url);
((ListModel*)this)->requestLogo(pack.safeName, url);
return icon;
}
else if(role == Qt::UserRole)
{
} else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@ -86,14 +73,14 @@ void ListModel::request()
modpacks.clear();
endResetModel();
auto *netJob = new NetJob("Atl::Request", APPLICATION->network());
auto netJob = makeShared<NetJob>("Atl::Request", APPLICATION->network());
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json");
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished);
QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::requestFinished()
@ -101,36 +88,38 @@ void ListModel::requestFinished()
jobPtr.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
qWarning() << *response;
return;
}
QList<ATLauncher::IndexedPack> newList;
auto packs = doc.array();
for(auto packRaw : packs) {
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
ATLauncher::IndexedPack pack;
try {
ATLauncher::loadIndexedPack(pack, packObj);
}
catch (const JSONValidationError &e) {
qDebug() << QString::fromUtf8(response);
} catch (const JSONValidationError& e) {
qDebug() << QString::fromUtf8(*response);
qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause();
return;
}
// ignore packs without a published version
if(pack.versions.length() == 0) continue;
if (pack.versions.length() == 0)
continue;
// only display public packs (for now)
if(pack.type != ATLauncher::PackType::Public) continue;
if (pack.type != ATLauncher::PackType::Public)
continue;
// ignore "system" packs (Vanilla, Vanilla with Forge, etc)
if(pack.system) continue;
if (pack.system)
continue;
newList.append(pack);
}
@ -145,14 +134,12 @@ void ListModel::requestFailed(QString reason)
jobPtr.reset();
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
if (m_logoMap.contains(logo)) {
callback(
APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
} else {
requestLogo(logo, logoUrl);
}
}
@ -168,36 +155,34 @@ 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].safeName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
for (int i = 0; i < modpacks.size(); i++) {
if (modpacks[i].safeName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
void ListModel::requestLogo(QString file, QString url)
{
if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
{
if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network());
auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath]
{
QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] {
job->deleteLater();
emit logoLoaded(file, QIcon(fullPath));
if(waitingCallbacks.contains(file))
{
if (waitingCallbacks.contains(file)) {
waitingCallbacks.value(file)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, file]
{
QObject::connect(job, &NetJob::failed, this, [this, file, job] {
job->deleteLater();
emit logoFailed(file);
});
@ -206,4 +191,4 @@ void ListModel::requestLogo(QString file, QString url)
m_loadingLogos.append(file);
}
}
} // namespace Atl

View File

@ -18,42 +18,41 @@
#include <QAbstractListModel>
#include "net/NetJob.h"
#include <QIcon>
#include <modplatform/atlauncher/ATLPackIndex.h>
#include <QIcon>
#include "net/NetJob.h"
namespace Atl {
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
class ListModel : public QAbstractListModel {
Q_OBJECT
public:
ListModel(QObject *parent);
public:
ListModel(QObject* 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;
int rowCount(const QModelIndex& parent) const override;
int columnCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role) const override;
void request();
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
private slots:
private slots:
void requestFinished();
void requestFailed(QString reason);
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
private:
private:
void requestLogo(QString file, QString url);
private:
private:
QList<ATLauncher::IndexedPack> modpacks;
QStringList m_failedLogos;
@ -62,7 +61,7 @@ private:
QMap<QString, LogoCallback> waitingCallbacks;
NetJob::Ptr jobPtr;
QByteArray response;
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
}
} // namespace Atl

View File

@ -75,12 +75,12 @@ QVector<QString> AtlOptionalModListModel::getResult() {
}
int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const {
return m_mods.size();
return parent.isValid() ? 0 : m_mods.size();
}
int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const {
// Enabled, Name, Description
return 3;
return parent.isValid() ? 0 : 3;
}
QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const {
@ -152,7 +152,7 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const {
void AtlOptionalModListModel::useShareCode(const QString& code) {
m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network()));
auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code);
m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), &m_response));
m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), m_response));
connect(m_jobPtr.get(), &NetJob::succeeded,
this, &AtlOptionalModListModel::shareCodeSuccess);
@ -166,10 +166,10 @@ void AtlOptionalModListModel::shareCodeSuccess() {
m_jobPtr.reset();
QJsonParseError parse_error {};
auto doc = QJsonDocument::fromJson(m_response, &parse_error);
auto doc = QJsonDocument::fromJson(*m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << m_response;
qWarning() << *m_response;
return;
}
auto obj = doc.object();
@ -179,7 +179,7 @@ void AtlOptionalModListModel::shareCodeSuccess() {
ATLauncher::loadShareCodeResponse(response, obj);
}
catch (const JSONValidationError& e) {
qDebug() << QString::fromUtf8(m_response);
qDebug() << QString::fromUtf8(*m_response);
qWarning() << "Error while reading response from ATLauncher: " << e.cause();
return;
}

View File

@ -82,9 +82,9 @@ private:
void toggleMod(ATLauncher::VersionMod mod, int index);
void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
private:
private:
NetJob::Ptr m_jobPtr;
QByteArray m_response;
std::shared_ptr<QByteArray> m_response = std::make_shared<QByteArray>();
ATLauncher::PackVersion m_version;
QVector<ATLauncher::VersionMod> m_mods;

View File

@ -53,7 +53,7 @@ std::optional<QVector<QString>> AtlUserInteractionSupportImpl::chooseOptionalMod
return optionalModDialog.getResult();
}
QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion)
QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion)
{
VersionSelectDialog vselect(vlist.get(), "Choose Version", m_parent, false);
if (minecraftVersion != nullptr) {
@ -68,7 +68,7 @@ QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionListPtr vlist,
// select recommended build
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
auto reqs = version->requires();
auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
if (minecraftVersion != nullptr) {

View File

@ -42,15 +42,15 @@
class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport {
Q_OBJECT
public:
public:
AtlUserInteractionSupportImpl(QWidget* parent);
virtual ~AtlUserInteractionSupportImpl() = default;
private:
QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
private:
QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override;
std::optional<QVector<QString>> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
void displayMessage(QString message) override;
private:
private:
QWidget* m_parent;
};

View File

@ -1,31 +0,0 @@
#include "FlameModModel.h"
#include "Json.h"
#include "modplatform/flame/FlameModIndex.h"
namespace FlameMod {
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" };
void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);
}
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
} // namespace FlameMod

View File

@ -1,26 +0,0 @@
#pragma once
#include "FlameModPage.h"
namespace FlameMod {
class ListModel : public ModPlatform::ListModel {
Q_OBJECT
public:
ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {}
~ListModel() override = default;
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;
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
static const char* sorts[6];
inline auto getSorts() const -> const char** override { return sorts; };
};
} // namespace FlameMod

View File

@ -1,80 +0,0 @@
// 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 "FlameModPage.h"
#include "ui_ModPage.h"
#include "FlameModModel.h"
#include "ui/dialogs/ModDownloadDialog.h"
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
: ModPage(dialog, instance, new FlameAPI())
{
listModel = new FlameMod::ListModel(this);
ui->packView->setModel(listModel);
// 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"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
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);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
ui->packDescription->setMetaEntry(metaEntryBase());
}
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
{
Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
}
bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return ver.downloadUrl.isEmpty();
}
// I don't know why, but doing this on the parent class makes it so that
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto FlameModPage::shouldDisplay() const -> bool { return true; }

View File

@ -1,67 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/flame/FlameAPI.h"
class FlameModPage : public ModPage {
Q_OBJECT
public:
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~FlameModPage() override = default;
inline auto displayName() const -> QString override { return "CurseForge"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
inline auto id() const -> QString override { return "curseforge"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
auto shouldDisplay() const -> bool override;
};

View File

@ -3,7 +3,6 @@
#include "Application.h"
#include "ui/widgets/ProjectItem.h"
#include <MMCStrings.h>
#include <Version.h>
#include <QtMath>
@ -16,12 +15,12 @@ ListModel::~ListModel() {}
int ListModel::rowCount(const QModelIndex& parent) const
{
return modpacks.size();
return parent.isValid() ? 0 : modpacks.size();
}
int ListModel::columnCount(const QModelIndex& parent) const
{
return 1;
return parent.isValid() ? 0 : 1;
}
QVariant ListModel::data(const QModelIndex& index, int role) const
@ -61,6 +60,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
return pack.description;
case UserDataTypes::SELECTED:
return false;
case UserDataTypes::INSTALLED:
return false;
default:
break;
}
@ -156,7 +157,7 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch()
{
NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network());
auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network());
auto searchUrl = QString(
"https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
@ -170,11 +171,11 @@ void ListModel::performPaginatedSearch()
.arg(currentSearchTerm)
.arg(currentSort + 1);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
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);
QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished);
QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed);
}
void ListModel::searchWithTerm(const QString& term, int sort)
@ -203,11 +204,11 @@ void Flame::ListModel::searchRequestFinished()
jobPtr.reset();
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << response;
qWarning() << *response;
return;
}

View File

@ -3,46 +3,44 @@
#include <RWStorage.h>
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
#include <QThreadPool>
#include <QIcon>
#include <QStyledItemDelegate>
#include <QList>
#include <QMetaType>
#include <QSortFilterProxyModel>
#include <QString>
#include <QStringList>
#include <QMetaType>
#include <QStyledItemDelegate>
#include <QThreadPool>
#include <functional>
#include <net/NetJob.h>
#include <functional>
#include <modplatform/flame/FlamePackIndex.h>
namespace Flame {
typedef QMap<QString, QIcon> LogoMap;
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
class ListModel : public QAbstractListModel {
Q_OBJECT
public:
ListModel(QObject *parent);
public:
ListModel(QObject* 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;
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;
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;
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
void searchWithTerm(const QString & term, const int sort);
void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);
void searchWithTerm(const QString& term, const int sort);
private slots:
private slots:
void performPaginatedSearch();
void logoFailed(QString logo);
@ -51,10 +49,10 @@ private slots:
void searchRequestFinished();
void searchRequestFailed(QString reason);
private:
private:
void requestLogo(QString file, QString url);
private:
private:
QList<IndexedPack> modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
@ -64,14 +62,9 @@ private:
QString currentSearchTerm;
int currentSort = 0;
int nextSearchOffset = 0;
enum SearchState {
None,
CanPossiblyFetchMore,
ResetRequested,
Finished
} searchState = None;
enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None;
NetJob::Ptr jobPtr;
QByteArray response;
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
};
}
} // namespace Flame

View File

@ -130,7 +130,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
if (current.versionsLoaded == false) {
qDebug() << "Loading flame modpack versions";
auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = new QByteArray();
auto response = std::make_shared<QByteArray>();
int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response));
@ -170,10 +170,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
}
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
netJob->deleteLater();
delete response;
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); });
netJob->start();
} else {
for (auto version : current.versions) {
@ -197,12 +194,18 @@ void FlamePage::suggestCurrent()
return;
}
if (selectedVersion.isEmpty() || selectedVersion == "-1") {
if (m_selected_version_index == -1) {
dialog->setSuggestedPack();
return;
}
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
auto version = current.versions.at(m_selected_version_index);
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", QString::number(current.addonId));
extra_info.insert("pack_version_id", QString::number(version.fileId));
dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl,
@ -211,11 +214,18 @@ void FlamePage::suggestCurrent()
void FlamePage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = "";
bool is_blocked = false;
ui->versionSelectionBox->currentData().toInt(&is_blocked);
if (data.isNull() || data.isEmpty() || is_blocked) {
m_selected_version_index = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toString();
m_selected_version_index = ui->versionSelectionBox->currentIndex();
Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString());
suggestCurrent();
}

View File

@ -99,5 +99,5 @@ private:
Flame::ListModel* listModel = nullptr;
Flame::IndexedPack current;
QString selectedVersion;
int m_selected_version_index = -1;
};

View File

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "FlameResourceModels.h"
#include "Json.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
namespace ResourceDownload {
FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {}
void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
{
return FlameMod::loadDependencyVersions(m, arr);
};
auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameResourcePackModel::FlameResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new FlameAPI) {}
void FlameResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {}
void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadIndexedPack(m, obj);
}
// We already deal with the URLs when initializing the pack, due to the API response's structure
void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
FlameMod::loadBody(m, obj);
}
void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size());
// FIXME: Client-side version filtering. This won't take into account any user-selected filtering.
for (auto const& version : m.versions) {
auto const& mc_versions = version.mcVersion;
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
[this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); }))
filtered_versions.push_back(version);
}
m.versions = filtered_versions;
}
ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments()
{
auto args = TexturePackResourceModel::createSearchArguments();
auto profile = static_cast<const MinecraftInstance&>(m_base_instance).getPackProfile();
QString instance_minecraft_version = profile->getComponentVersion("net.minecraft");
// Bypass the texture pack logic, because we can't do multiple versions in the API query
args.versions = { instance_minecraft_version };
return args;
}
ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry)
{
auto args = TexturePackResourceModel::createVersionsArguments(entry);
// Bypass the texture pack logic, because we can't do multiple versions in the API query
args.mcVersions = {};
return args;
}
auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
}
} // namespace ResourceDownload

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
#include "ui/pages/modplatform/flame/FlameResourcePages.h"
namespace ResourceDownload {
class FlameModModel : public ModModel {
Q_OBJECT
public:
FlameModModel(BaseInstance&);
~FlameModModel() override = default;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
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 loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class FlameResourcePackModel : public ResourcePackResourceModel {
Q_OBJECT
public:
FlameResourcePackModel(const BaseInstance&);
~FlameResourcePackModel() override = default;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
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;
};
class FlameTexturePackModel : public TexturePackResourceModel {
Q_OBJECT
public:
FlameTexturePackModel(const BaseInstance&);
~FlameTexturePackModel() override = default;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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 "FlameResourcePages.h"
#include "ui_ResourcePage.h"
#include "FlameResourceModels.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
namespace ResourceDownload {
static bool isOptedOut(ModPlatform::IndexedVersion const& ver)
{
return ver.downloadUrl.isEmpty();
}
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance)
: ModPage(dialog, instance)
{
m_model = new FlameModModel(instance);
m_ui->packView->setModel(m_model);
addSortings();
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged);
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected);
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders) const -> bool
{
Q_UNUSED(loaders);
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
}
bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameModPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
QString query = url.query(QUrl::FullyDecoded);
if (query.startsWith("remoteUrl=")) {
// attempt to resolve url from warning page
query.remove(0, 10);
ModPage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary
return;
}
}
ModPage::openUrl(url);
}
FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
: ResourcePackResourcePage(dialog, instance)
{
m_model = new FlameResourcePackModel(instance);
m_ui->packView->setModel(m_model);
addSortings();
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameResourcePackPage::onVersionSelectionChanged);
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected);
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
bool FlameResourcePackPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameResourcePackPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
QString query = url.query(QUrl::FullyDecoded);
if (query.startsWith("remoteUrl=")) {
// attempt to resolve url from warning page
query.remove(0, 10);
ResourcePackResourcePage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary
return;
}
}
ResourcePackResourcePage::openUrl(url);
}
FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
: TexturePackResourcePage(dialog, instance)
{
m_model = new FlameTexturePackModel(instance);
m_ui->packView->setModel(m_model);
addSortings();
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameTexturePackPage::onVersionSelectionChanged);
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected);
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
bool FlameTexturePackPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameTexturePackPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
QString query = url.query(QUrl::FullyDecoded);
if (query.startsWith("remoteUrl=")) {
// attempt to resolve url from warning page
query.remove(0, 10);
ResourcePackResourcePage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary
return;
}
}
TexturePackResourcePage::openUrl(url);
}
// I don't know why, but doing this on the parent class makes it so that
// other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class...
auto FlameModPage::shouldDisplay() const -> bool { return true; }
auto FlameResourcePackPage::shouldDisplay() const -> bool { return true; }
auto FlameTexturePackPage::shouldDisplay() const -> bool { return true; }
} // namespace ResourceDownload

View File

@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "Application.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "ui/pages/modplatform/ResourcePackPage.h"
#include "ui/pages/modplatform/TexturePackPage.h"
namespace ResourceDownload {
namespace Flame {
static inline QString displayName() { return "CurseForge"; }
static inline QIcon icon() { return APPLICATION->getThemedIcon("flame"); }
static inline QString id() { return "curseforge"; }
static inline QString debugName() { return "Flame"; }
static inline QString metaEntryBase() { return "FlameMods"; }
}
class FlameModPage : public ModPage {
Q_OBJECT
public:
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance);
~FlameModPage() override = default;
[[nodiscard]] bool shouldDisplay() const override;
[[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
[[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
[[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
[[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
[[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const override;
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
class FlameResourcePackPage : public ResourcePackResourcePage {
Q_OBJECT
public:
static FlameResourcePackPage* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
{
return ResourcePackResourcePage::create<FlameResourcePackPage>(dialog, instance);
}
FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance);
~FlameResourcePackPage() override = default;
[[nodiscard]] bool shouldDisplay() const override;
[[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
[[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
[[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
[[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
class FlameTexturePackPage : public TexturePackResourcePage {
Q_OBJECT
public:
static FlameTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance)
{
return TexturePackResourcePage::create<FlameTexturePackPage>(dialog, instance);
}
FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance);
~FlameTexturePackPage() override = default;
[[nodiscard]] bool shouldDisplay() const override;
[[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
[[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
[[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
[[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
} // namespace ResourceDownload

View File

@ -1,92 +0,0 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* 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 "FtbFilterModel.h"
#include <QDebug>
#include "modplatform/modpacksch/FTBPackManifest.h"
#include <MMCStrings.h>
namespace Ftb {
FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByPlays;
sortings.insert(tr("Sort by Plays"), Sorting::ByPlays);
sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls);
sortings.insert(tr("Sort by Name"), Sorting::ByName);
}
const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
{
return sortings;
}
QString FilterModel::translateCurrentSorting()
{
return sortings.key(currentSorting);
}
void FilterModel::setSorting(Sorting sorting)
{
currentSorting = sorting;
invalidate();
}
FilterModel::Sorting FilterModel::getCurrentSorting()
{
return currentSorting;
}
void FilterModel::setSearchTerm(const QString& term)
{
searchTerm = term.trimmed();
invalidate();
}
bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (searchTerm.isEmpty()) {
return true;
}
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>();
return pack.name.contains(searchTerm, Qt::CaseInsensitive);
}
bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
if (currentSorting == ByPlays) {
return leftPack.plays < rightPack.plays;
}
else if (currentSorting == ByInstalls) {
return leftPack.installs < rightPack.installs;
}
else if (currentSorting == ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
// Invalid sorting set, somehow...
qWarning() << "Invalid sorting set!";
return true;
}
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QtCore/QSortFilterProxyModel>
namespace Ftb {
class FilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
FilterModel(QObject* parent = Q_NULLPTR);
enum Sorting {
ByPlays,
ByInstalls,
ByName,
};
const QMap<QString, Sorting> getAvailableSortings();
QString translateCurrentSorting();
void setSorting(Sorting sorting);
Sorting getCurrentSorting();
void setSearchTerm(const QString& term);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
private:
QMap<QString, Sorting> sortings;
Sorting currentSorting;
QString searchTerm { "" };
};
}

View File

@ -1,304 +0,0 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* 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 "FtbListModel.h"
#include "BuildConfig.h"
#include "Application.h"
#include "Json.h"
#include <QPainter>
namespace Ftb {
ListModel::ListModel(QObject *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);
}
ModpacksCH::Modpack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
return pack.name;
}
else if (role == Qt::ToolTipRole)
{
return pack.synopsis;
}
else if(role == Qt::DecorationRole)
{
QIcon placeholder = APPLICATION->getThemedIcon("screenshot-placeholder");
auto iter = m_logoMap.find(pack.name);
if (iter != m_logoMap.end()) {
auto & logo = *iter;
if(!logo.result.isNull()) {
return logo.result;
}
return placeholder;
}
for(auto art : pack.art) {
if(art.type == "square") {
((ListModel *)this)->requestLogo(pack.name, art.url);
}
}
return placeholder;
}
else if(role == Qt::UserRole)
{
QVariant v;
v.setValue(pack);
return v;
}
return QVariant();
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
requestLogo(logo, logoUrl);
}
}
void ListModel::request()
{
m_aborted = false;
beginResetModel();
modpacks.clear();
endResetModel();
auto *netJob = new NetJob("Ftb::Request", APPLICATION->network());
auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all");
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
}
void ListModel::abortRequest()
{
m_aborted = jobPtr->abort();
jobPtr.reset();
}
void ListModel::requestFinished()
{
jobPtr.reset();
remainingPacks.clear();
QJsonParseError parse_error {};
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
auto packs = doc.object().value("packs").toArray();
for(auto pack : packs) {
auto packId = pack.toInt();
remainingPacks.append(packId);
}
if(!remainingPacks.isEmpty()) {
currentPack = remainingPacks.at(0);
requestPack();
}
}
void ListModel::requestFailed(QString reason)
{
jobPtr.reset();
remainingPacks.clear();
}
void ListModel::requestPack()
{
auto *netJob = new NetJob("Ftb::Search", APPLICATION->network());
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1").arg(currentPack);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
}
void ListModel::packRequestFinished()
{
if (!jobPtr || m_aborted)
return;
jobPtr.reset();
remainingPacks.removeOne(currentPack);
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
auto obj = doc.object();
ModpacksCH::Modpack pack;
try
{
ModpacksCH::loadModpack(pack, obj);
}
catch (const JSONValidationError &e)
{
qDebug() << QString::fromUtf8(response);
qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause();
return;
}
// Since there is no guarantee that packs have a version, this will just
// ignore those "dud" packs.
if (pack.versions.empty())
{
qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions";
}
else
{
beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
modpacks.append(pack);
endInsertRows();
}
if(!remainingPacks.isEmpty()) {
currentPack = remainingPacks.at(0);
requestPack();
}
}
void ListModel::packRequestFailed(QString reason)
{
jobPtr.reset();
remainingPacks.removeOne(currentPack);
}
void ListModel::logoLoaded(QString logo, bool stale)
{
auto & logoObj = m_logoMap[logo];
logoObj.downloadJob.reset();
QString smallPath = logoObj.fullpath + ".small";
QFileInfo smallInfo(smallPath);
if(stale || !smallInfo.exists()) {
QImage image(logoObj.fullpath);
if (image.isNull())
{
logoObj.failed = true;
return;
}
QImage small;
if (image.width() > image.height()) {
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
}
else {
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
}
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
square.save(logoObj.fullpath + ".small", "PNG");
}
logoObj.result = QIcon(logoObj.fullpath + ".small");
for(int i = 0; i < modpacks.size(); i++) {
if(modpacks[i].name == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
}
}
}
void ListModel::logoFailed(QString logo)
{
m_logoMap[logo].failed = true;
m_logoMap[logo].downloadJob.reset();
}
void ListModel::requestLogo(QString logo, QString url)
{
if(m_logoMap.contains(logo)) {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
bool stale = entry->isStale();
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();
QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
{
logoLoaded(logo, stale);
});
QObject::connect(job, &NetJob::failed, this, [this, logo]
{
logoFailed(logo);
});
auto &newLogoEntry = m_logoMap[logo];
newLogoEntry.downloadJob = job;
newLogoEntry.fullpath = fullPath;
job->start();
}
}

View File

@ -1,83 +0,0 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QAbstractListModel>
#include "modplatform/modpacksch/FTBPackManifest.h"
#include "net/NetJob.h"
#include <QIcon>
namespace Ftb {
struct Logo {
QString fullpath;
NetJob::Ptr downloadJob;
QIcon result;
bool failed = false;
};
typedef QMap<QString, Logo> LogoMap;
typedef std::function<void(QString)> LogoCallback;
class ListModel : public QAbstractListModel
{
Q_OBJECT
public:
ListModel(QObject *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;
void request();
void abortRequest();
void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
[[nodiscard]] bool isMakingRequest() const { return jobPtr.get(); }
[[nodiscard]] bool wasAborted() const { return m_aborted; }
private slots:
void requestFinished();
void requestFailed(QString reason);
void requestPack();
void packRequestFinished();
void packRequestFailed(QString reason);
void logoFailed(QString logo);
void logoLoaded(QString logo, bool stale);
private:
void requestLogo(QString file, QString url);
private:
bool m_aborted = false;
QList<ModpacksCH::Modpack> modpacks;
LogoMap m_logoMap;
NetJob::Ptr jobPtr;
int currentPack;
QList<int> remainingPacks;
QByteArray response;
};
}

View File

@ -1,200 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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
* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2021 Philip T <me@phit.link>
*
* 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 "FtbPage.h"
#include "ui_FtbPage.h"
#include <QKeyEvent>
#include "ui/dialogs/NewInstanceDialog.h"
#include "modplatform/modpacksch/FTBPackInstallTask.h"
#include "HoeDown.h"
FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
{
ui->setupUi(this);
filterModel = new Ftb::FilterModel(this);
listModel = new Ftb::ListModel(this);
filterModel->setSourceModel(listModel);
ui->packView->setModel(filterModel);
ui->packView->setSortingEnabled(true);
ui->packView->header()->hide();
ui->packView->setIndentation(0);
ui->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
{
ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
}
ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
connect(ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch);
connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
ui->packDescription->setMetaEntry("FTBPacks");
}
FtbPage::~FtbPage()
{
delete ui;
}
bool FtbPage::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 FtbPage::shouldDisplay() const
{
return true;
}
void FtbPage::retranslate()
{
ui->retranslateUi(this);
}
void FtbPage::openedImpl()
{
if(!initialised || listModel->wasAborted())
{
listModel->request();
initialised = true;
}
suggestCurrent();
}
void FtbPage::closedImpl()
{
if (listModel->isMakingRequest())
listModel->abortRequest();
}
void FtbPage::suggestCurrent()
{
if(!isOpened)
{
return;
}
if (selectedVersion.isEmpty())
{
dialog->setSuggestedPack();
return;
}
dialog->setSuggestedPack(selected.name, selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this));
for(auto art : selected.art) {
if(art.type == "square") {
QString editedLogoName;
editedLogoName = selected.name;
listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
});
}
}
}
void FtbPage::triggerSearch()
{
filterModel->setSearchTerm(ui->searchEdit->text());
}
void FtbPage::onSortingSelectionChanged(QString data)
{
auto toSet = filterModel->getAvailableSortings().value(data);
filterModel->setSorting(toSet);
}
void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
{
ui->versionSelectionBox->clear();
if(!first.isValid())
{
if(isOpened)
{
dialog->setSuggestedPack();
}
return;
}
selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
HoeDown hoedown;
QString output = hoedown.process(selected.description.toUtf8());
ui->packDescription->setHtml(output);
// reverse foreach, so that the newest versions are first
for (auto i = selected.versions.size(); i--;) {
ui->versionSelectionBox->addItem(selected.versions.at(i).name);
}
suggestCurrent();
}
void FtbPage::onVersionSelectionChanged(QString data)
{
if(data.isNull() || data.isEmpty())
{
selectedVersion = "";
return;
}
selectedVersion = data;
suggestCurrent();
}

View File

@ -1,105 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "FtbFilterModel.h"
#include "FtbListModel.h"
#include <QWidget>
#include "Application.h"
#include "ui/pages/BasePage.h"
#include "tasks/Task.h"
namespace Ui
{
class FtbPage;
}
class NewInstanceDialog;
class FtbPage : public QWidget, public BasePage
{
Q_OBJECT
public:
explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
virtual ~FtbPage();
virtual QString displayName() const override
{
return "FTB";
}
virtual QIcon icon() const override
{
return APPLICATION->getThemedIcon("ftb_logo");
}
virtual QString id() const override
{
return "ftb";
}
virtual QString helpPage() const override
{
return "FTB-platform";
}
virtual bool shouldDisplay() const override;
void retranslate() override;
void openedImpl() override;
void closedImpl() override;
bool eventFilter(QObject * watched, QEvent * event) override;
private:
void suggestCurrent();
private slots:
void triggerSearch();
void onSortingSelectionChanged(QString data);
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
private:
Ui::FtbPage *ui = nullptr;
NewInstanceDialog* dialog = nullptr;
Ftb::ListModel* listModel = nullptr;
Ftb::FilterModel* filterModel = nullptr;
ModpacksCH::Modpack selected;
QString selectedVersion;
bool initialised { false };
};

View File

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FtbPage</class>
<widget class="QWidget" name="FtbPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>875</width>
<height>745</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<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="0">
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QTreeView" name="packView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ProjectDescriptionPage" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ProjectDescriptionPage</class>
<extends>QTextBrowser</extends>
<header>ui/widgets/ProjectDescriptionPage.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>versionSelectionBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -35,12 +35,14 @@
#include "ListModel.h"
#include "Application.h"
#include "net/HttpMetaCache.h"
#include "net/NetJob.h"
#include <MMCStrings.h>
#include <Version.h>
#include "StringUtils.h"
#include <QtMath>
#include <QLabel>
#include <QtMath>
#include <RWStorage.h>
@ -48,33 +50,33 @@
namespace LegacyFTB {
FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByGameVersion;
sortings.insert(tr("Sort by Name"), Sorting::ByName);
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
}
bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
if(currentSorting == Sorting::ByGameVersion) {
if (currentSorting == Sorting::ByGameVersion) {
Version lv(leftPack.mcVersion);
Version rv(rightPack.mcVersion);
return lv < rv;
} else if(currentSorting == Sorting::ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
} else if (currentSorting == Sorting::ByName) {
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
//UHM, some inavlid value set?!
// UHM, some inavlid value set?!
qWarning() << "Invalid sorting set!";
return true;
}
bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
return true;
}
@ -100,18 +102,13 @@ FilterModel::Sorting FilterModel::getCurrentSorting()
return currentSorting;
}
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
ListModel::~ListModel()
{
}
ListModel::~ListModel() {}
QString ListModel::translatePackType(PackType type) const
{
switch(type)
{
switch (type) {
case PackType::Public:
return tr("Public Modpack");
case PackType::ThirdParty:
@ -123,67 +120,51 @@ QString ListModel::translatePackType(PackType type) const
return QString();
}
int ListModel::rowCount(const QModelIndex &parent) const
int ListModel::rowCount(const QModelIndex& parent) const
{
return modpacks.size();
return parent.isValid() ? 0 : modpacks.size();
}
int ListModel::columnCount(const QModelIndex &parent) const
int ListModel::columnCount(const QModelIndex& parent) const
{
return 1;
return parent.isValid() ? 0 : 1;
}
QVariant ListModel::data(const QModelIndex &index, int role) const
QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
{
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
Modpack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
if (role == Qt::DisplayRole) {
return pack.name + "\n" + translatePackType(pack.type);
}
else if (role == Qt::ToolTipRole)
{
if(pack.description.length() > 100)
{
//some magic to prevent to long tooltips and replace html linebreaks
} 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.logo))
{
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.logo)) {
return (m_logoMap.value(pack.logo));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel *)this)->requestLogo(pack.logo);
((ListModel*)this)->requestLogo(pack.logo);
return icon;
}
else if(role == Qt::ForegroundRole)
{
if(pack.broken)
{
//FIXME: Hardcoded color
} else if (role == Qt::ForegroundRole) {
if (pack.broken) {
// FIXME: Hardcoded color
return QColor(255, 0, 50);
}
else if(pack.bugged)
{
//FIXME: Hardcoded color
//bugged pack, currently only indicates bugged xml
} else if (pack.bugged) {
// FIXME: Hardcoded color
// bugged pack, currently only indicates bugged xml
return QColor(244, 229, 66);
}
}
else if(role == Qt::UserRole)
{
} else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@ -220,8 +201,7 @@ Modpack ListModel::at(int row)
void ListModel::remove(int row)
{
if(row < 0 || row >= modpacks.size())
{
if (row < 0 || row >= modpacks.size()) {
qWarning() << "Attempt to remove FTB modpacks with invalid row" << row;
return;
}
@ -245,27 +225,25 @@ void ListModel::logoFailed(QString logo)
void ListModel::requestLogo(QString file)
{
if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
{
if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) {
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network());
NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network());
job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
{
QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] {
job->deleteLater();
emit logoLoaded(file, QIcon(fullPath));
if(waitingCallbacks.contains(file))
{
if (waitingCallbacks.contains(file)) {
waitingCallbacks.value(file)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, file]
{
QObject::connect(job, &NetJob::failed, this, [this, file, job] {
job->deleteLater();
emit logoFailed(file);
});
@ -274,21 +252,18 @@ void ListModel::requestLogo(QString file)
m_loadingLogos.append(file);
}
void ListModel::getLogo(const QString &logo, LogoCallback callback)
void ListModel::getLogo(const QString& logo, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
} else {
requestLogo(logo);
}
}
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
{
return QAbstractListModel::flags(index);
}
}
} // namespace LegacyFTB

View File

@ -116,8 +116,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget *parent)
connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);
connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged);
connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
connect(ui->addPackBtn, &QPushButton::clicked, this, &Page::onAddPackClicked);
connect(ui->removePackBtn, &QPushButton::clicked, this, &Page::onRemovePackClicked);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);

View File

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

View File

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

View File

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

View File

@ -1,66 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
class ModrinthModPage : public ModPage {
Q_OBJECT
public:
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
{
return ModPage::create<ModrinthModPage>(dialog, instance);
}
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~ModrinthModPage() override = default;
inline auto displayName() const -> QString override { return "Modrinth"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};

View File

@ -40,7 +40,6 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox>
@ -107,6 +106,8 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
return pack.description;
case UserDataTypes::SELECTED:
return false;
case UserDataTypes::INSTALLED:
return false;
default:
break;
}
@ -128,35 +129,35 @@ bool ModpackListModel::setData(const QModelIndex &index, const QVariant &value,
void ModpackListModel::performPaginatedSearch()
{
// TODO: Move to standalone API
NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network());
auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network());
auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL +
"/search?"
"offset=%1&"
"limit=%2&"
"query=%3&"
"index=%4&"
"facets=[[\"project_type:modpack\"]]")
"/search?"
"offset=%1&"
"limit=%2&"
"query=%3&"
"index=%4&"
"facets=[[\"project_type:modpack\"]]")
.arg(nextSearchOffset)
.arg(m_modpacks_per_page)
.arg(currentSearchTerm)
.arg(currentSort);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response));
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), m_all_response));
QObject::connect(netJob, &NetJob::succeeded, this, [this] {
QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] {
QJsonParseError parse_error_all{};
QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all);
QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all);
if (parse_error_all.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset
<< " reason: " << parse_error_all.errorString();
qWarning() << m_all_response;
qWarning() << *m_all_response;
return;
}
searchRequestFinished(doc_all);
});
QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed);
jobPtr = netJob;
jobPtr->start();

Some files were not shown because too many files have changed in this diff Show More