Merge remote-tracking branch 'upstream/develop' into resource-meta

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad
2024-10-08 17:15:42 +01:00
534 changed files with 14803 additions and 8430 deletions

View File

@@ -38,6 +38,7 @@
#include "Application.h"
#include "BuildConfig.h"
#include "Markdown.h"
#include "StringUtils.h"
#include "ui_AboutDialog.h"
#include <net/NetJob.h>
@@ -139,10 +140,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia
setWindowTitle(tr("About %1").arg(launcherName));
QString chtml = getCreditsHtml();
ui->creditsText->setHtml(chtml);
ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
QString lhtml = getLicenseHtml();
ui->licenseText->setHtml(lhtml);
ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
ui->urlLabel->setOpenExternalLinks(true);

View File

@@ -40,6 +40,7 @@
#include <QMimeData>
#include <QPushButton>
#include <QStandardPaths>
#include <QTimer>
BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods, QString hash_type)
: QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type)
@@ -60,8 +61,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons
qDebug() << "[Blocked Mods Dialog] Mods List: " << mods;
setupWatch();
scanPaths();
// defer setup of file system watchers until after the dialog is shown
// this allows OS (namely macOS) permission prompts to show after the relevant dialog appears
QTimer::singleShot(0, this, [this] {
setupWatch();
scanPaths();
update();
});
this->setWindowTitle(title);
ui->labelDescription->setText(text);
@@ -158,7 +164,8 @@ void BlockedModsDialog::update()
QString watching;
for (auto& dir : m_watcher.directories()) {
watching += QString("<a href=\"%1\">%1</a><br/>").arg(dir);
QUrl fileURL = QUrl::fromLocalFile(dir);
watching += QString("<a href=\"%1\">%2</a><br/>").arg(fileURL.toString(), dir);
}
ui->textBrowserWatched->setText(watching);
@@ -194,6 +201,10 @@ void BlockedModsDialog::setupWatch()
void BlockedModsDialog::watchPath(QString path, bool watch_recursive)
{
auto to_watch = QFileInfo(path);
if (!to_watch.isReadable()) {
qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path;
return;
}
auto to_watch_path = to_watch.canonicalFilePath();
if (m_watcher.directories().contains(to_watch_path))
return; // don't watch the same path twice (no loops!)
@@ -255,7 +266,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path)
{
auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, m_hash_type);
auto hash_task = Hashing::createHasher(path, m_hash_type);
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;

View File

@@ -146,7 +146,7 @@ void ExportInstanceDialog::doExport()
return;
}
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true);
auto task = makeShared<MMCZip::ExportToZipTask>(output, m_instance->instanceRoot(), files, "", true, true);
connect(task.get(), &Task::failed, this,
[this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });

View File

@@ -129,14 +129,14 @@ void ExportPackDialog::done(int result)
QString output;
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
"Modrinth pack (*.mrpack *.zip)", nullptr);
tr("Modrinth pack") + " (*.mrpack *.zip)", nullptr);
if (output.isEmpty())
return;
if (!(output.endsWith(".zip") || output.endsWith(".mrpack")))
output.append(".mrpack");
} else {
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"),
"CurseForge pack (*.zip)", nullptr);
tr("CurseForge pack") + " (*.zip)", nullptr);
if (output.isEmpty())
return;
if (!output.endsWith(".zip"))

View File

@@ -22,8 +22,7 @@
#include <QTextEdit>
#include "FileSystem.h"
#include "Markdown.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"
#include "StringUtils.h"
#include "modplatform/helpers/ExportToModList.h"
#include "ui_ExportToModListDialog.h"
@@ -41,38 +40,31 @@ const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLin
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
};
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog)
ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent)
: QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog)
{
ui->setupUi(this);
enableCustom(false);
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
m_allMods = mcInstance->loaderModList()->allMods();
triggerImp();
});
}
connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); });
connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
if (ui->templateText->toPlainText() != exampleLines[format])
if (ui->templateText->toPlainText() != exampleLines[m_format])
ui->formatComboBox->setCurrentIndex(5);
else
triggerImp();
triggerImp();
});
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
this->ui->finalText->selectAll();
this->ui->finalText->copy();
});
triggerImp();
}
ExportToModListDialog::~ExportToModListDialog()
@@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index)
case 0: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::HTML;
m_format = ExportToModList::HTML;
break;
}
case 1: {
enableCustom(false);
ui->resultText->show();
format = ExportToModList::MARKDOWN;
m_format = ExportToModList::MARKDOWN;
break;
}
case 2: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::PLAINTXT;
m_format = ExportToModList::PLAINTXT;
break;
}
case 3: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::JSON;
m_format = ExportToModList::JSON;
break;
}
case 4: {
enableCustom(false);
ui->resultText->hide();
format = ExportToModList::CSV;
m_format = ExportToModList::CSV;
break;
}
case 5: {
m_template_changed = true;
enableCustom(true);
ui->resultText->hide();
format = ExportToModList::CUSTOM;
m_format = ExportToModList::CUSTOM;
break;
}
}
@@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index)
void ExportToModListDialog::triggerImp()
{
if (format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText()));
if (m_format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText()));
return;
}
auto opt = 0;
@@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp()
opt |= ExportToModList::Version;
if (ui->urlCheckBox->isChecked())
opt |= ExportToModList::Url;
auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt));
if (ui->filenameCheckBox->isChecked())
opt |= ExportToModList::FileName;
auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast<ExportToModList::OptionalData>(opt));
ui->finalText->setPlainText(txt);
switch (format) {
switch (m_format) {
case ExportToModList::CUSTOM:
return;
case ExportToModList::HTML:
ui->resultText->setHtml(txt);
ui->resultText->setHtml(StringUtils::htmlListPatch(txt));
break;
case ExportToModList::MARKDOWN:
ui->resultText->setHtml(markdownToHTML(txt));
ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt)));
break;
case ExportToModList::PLAINTXT:
break;
@@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp()
case ExportToModList::CSV:
break;
}
auto exampleLine = exampleLines[format];
auto exampleLine = exampleLines[m_format];
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
ui->templateText->setPlainText(exampleLine);
}
@@ -163,14 +157,19 @@ void ExportToModListDialog::triggerImp()
void ExportToModListDialog::done(int result)
{
if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(name);
const QString filename = FS::RemoveInvalidFilenameChars(m_name);
const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr);
QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()),
tr("File") + " (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty())
return;
FS::write(output, ui->finalText->toPlainText().toUtf8());
try {
FS::write(output, ui->finalText->toPlainText().toUtf8());
} catch (const FS::FileSystemException& e) {
qCritical() << "Failed to save mod list file :" << e.cause();
}
}
QDialog::done(result);
@@ -178,7 +177,7 @@ void ExportToModListDialog::done(int result)
QString ExportToModListDialog::extension()
{
switch (format) {
switch (m_format) {
case ExportToModList::HTML:
return ".html";
case ExportToModList::MARKDOWN:
@@ -197,7 +196,7 @@ QString ExportToModListDialog::extension()
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
{
if (format != ExportToModList::CUSTOM)
if (m_format != ExportToModList::CUSTOM)
return;
switch (option) {
case ExportToModList::Authors:
@@ -209,6 +208,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
case ExportToModList::Version:
ui->templateText->insertPlainText("{version}");
break;
case ExportToModList::FileName:
ui->templateText->insertPlainText("{filename}");
break;
}
}
void ExportToModListDialog::enableCustom(bool enabled)
@@ -221,4 +223,7 @@ void ExportToModListDialog::enableCustom(bool enabled)
ui->urlCheckBox->setHidden(enabled);
ui->urlButton->setHidden(!enabled);
ui->filenameCheckBox->setHidden(enabled);
ui->filenameButton->setHidden(!enabled);
}

View File

@@ -20,7 +20,6 @@
#include <QDialog>
#include <QList>
#include "BaseInstance.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/helpers/ExportToModList.h"
@@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog {
Q_OBJECT
public:
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr);
explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr);
~ExportToModListDialog();
void done(int result) override;
@@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog {
private:
QString extension();
void enableCustom(bool enabled);
QList<Mod*> m_allMods;
QList<Mod*> m_mods;
bool m_template_changed;
QString name;
ExportToModList::Formats format = ExportToModList::Formats::HTML;
QString m_name;
ExportToModList::Formats m_format = ExportToModList::Formats::HTML;
Ui::ExportToModListDialog* ui;
static const QHash<ExportToModList::Formats, QString> exampleLines;
};

View File

@@ -117,6 +117,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="filenameCheckBox">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="versionButton">
<property name="text">
@@ -138,6 +145,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="filenameButton">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -34,43 +34,65 @@
*/
#include "MSALoginDialog.h"
#include "Application.h"
#include "ui_MSALoginDialog.h"
#include "DesktopServices.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthFlow.h"
#include <QApplication>
#include <QClipboard>
#include <QPixmap>
#include <QUrl>
#include <QtWidgets/QPushButton>
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->actionButton->setVisible(false);
// ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
// make font monospace
QFont font;
font.setPixelSize(ui->code->fontInfo().pixelSize());
font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString());
font.setStyleHint(QFont::Monospace);
font.setFixedPitch(true);
ui->code->setFont(font);
connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); });
ui->qr->setPixmap(QIcon((":/documents/login-qr.svg")).pixmap(QSize(150, 150)));
connect(ui->loginButton, &QPushButton::clicked, this, [this] {
if (m_url.isValid()) {
if (!DesktopServices::openUrl(m_url)) {
QApplication::clipboard()->setText(m_url.toString());
}
}
});
}
int MSALoginDialog::exec()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA();
m_loginTask = m_account->loginMSA();
connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode);
connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode);
connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
m_loginTask->start();
m_authflow_task = m_account->login(false);
connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept);
connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort);
m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode, this));
connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept);
connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort);
QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection);
QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection);
return QDialog::exec();
}
@@ -80,63 +102,12 @@ MSALoginDialog::~MSALoginDialog()
delete ui;
}
void MSALoginDialog::externalLoginTick()
{
m_externalLoginElapsed++;
ui->progressBar->setValue(m_externalLoginElapsed);
ui->progressBar->repaint();
if (m_externalLoginElapsed >= m_externalLoginTimeout) {
m_externalLoginTimer.stop();
}
}
void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn)
{
m_externalLoginElapsed = 0;
m_externalLoginTimeout = expiresIn;
m_externalLoginTimer.setInterval(1000);
m_externalLoginTimer.setSingleShot(false);
m_externalLoginTimer.start();
ui->progressBar->setMaximum(expiresIn);
ui->progressBar->setValue(m_externalLoginElapsed);
QString urlString = uri.toString();
QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString);
if (urlString == "https://www.microsoft.com/link" && !code.isEmpty()) {
urlString += QString("?otc=%1").arg(code);
DesktopServices::openUrl(urlString);
ui->label->setText(tr("<p>Please login in the opened browser. If no browser was opened, please open up %1 in "
"a browser and put in the code <b>%2</b> to proceed with login.</p>")
.arg(linkString, code));
} else {
ui->label->setText(
tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
}
ui->actionButton->setVisible(true);
connect(ui->actionButton, &QPushButton::clicked, [=]() {
DesktopServices::openUrl(uri);
QClipboard* cb = QApplication::clipboard();
cb->setText(code);
});
}
void MSALoginDialog::hideVerificationUriAndCode()
{
m_externalLoginTimer.stop();
ui->actionButton->setVisible(false);
}
void MSALoginDialog::setUserInputsEnabled(bool enable)
{
ui->buttonBox->setEnabled(enable);
}
void MSALoginDialog::onTaskFailed(const QString& reason)
void MSALoginDialog::onTaskFailed(QString reason)
{
// Set message
m_authflow_task->disconnect();
m_devicecode_task->disconnect();
ui->stackedWidget->setCurrentIndex(0);
auto lines = reason.split('\n');
QString processed;
for (auto line : lines) {
@@ -146,37 +117,53 @@ void MSALoginDialog::onTaskFailed(const QString& reason)
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
ui->actionButton->setVisible(false);
ui->status->setText(processed);
auto task = m_authflow_task;
if (task->failReason().isEmpty()) {
task = m_devicecode_task;
}
if (task) {
ui->loadingLabel->setText(task->getStatus());
}
disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort);
disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort);
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject);
}
void MSALoginDialog::onTaskSucceeded()
void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
{
QDialog::accept();
ui->stackedWidget->setCurrentIndex(1);
ui->loginButton->setToolTip(QString("<div style='width: 200px;'>%1</div>").arg(url.toString()));
m_url = url;
}
void MSALoginDialog::onTaskStatus(const QString& status)
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
{
ui->label->setText(status);
ui->stackedWidget->setCurrentIndex(1);
const auto linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
ui->code->setText(code);
auto isDefaultUrl = url == "https://www.microsoft.com/link";
ui->qr->setVisible(isDefaultUrl);
if (isDefaultUrl) {
ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code.").arg(linkString));
} else {
ui->qrMessage->setText(tr("Open %1 and enter the above code.").arg(linkString));
}
}
void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
void MSALoginDialog::onTaskStatus(QString status)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
ui->stackedWidget->setCurrentIndex(0);
ui->status->setText(status);
}
// Public interface
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg)
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent)
{
MSALoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account;
}
return nullptr;
}
}

View File

@@ -19,6 +19,7 @@
#include <QtCore/QEventLoop>
#include <QtWidgets/QDialog>
#include "minecraft/auth/AuthFlow.h"
#include "minecraft/auth/MinecraftAccount.h"
namespace Ui {
@@ -31,29 +32,23 @@ class MSALoginDialog : public QDialog {
public:
~MSALoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
static MinecraftAccountPtr newAccount(QWidget* parent);
int exec() override;
private:
explicit MSALoginDialog(QWidget* parent = 0);
void setUserInputsEnabled(bool enable);
protected slots:
void onTaskFailed(const QString& reason);
void onTaskSucceeded();
void onTaskStatus(const QString& status);
void onTaskProgress(qint64 current, qint64 total);
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
void externalLoginTick();
void onTaskFailed(QString reason);
void onTaskStatus(QString status);
void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
private:
Ui::MSALoginDialog* ui;
MinecraftAccountPtr m_account;
shared_qobject_ptr<AccountTask> m_loginTask;
QTimer m_externalLoginTimer;
int m_externalLoginElapsed = 0;
int m_externalLoginTimeout = 0;
shared_qobject_ptr<AuthFlow> m_devicecode_task;
shared_qobject_ptr<AuthFlow> m_authflow_task;
QUrl m_url;
};

View File

@@ -6,69 +6,348 @@
<rect>
<x>0</x>
<y>0</y>
<width>491</width>
<height>143</height>
<width>440</width>
<height>430</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="minimumSize">
<size>
<width>0</width>
<height>430</height>
</size>
</property>
<property name="windowTitle">
<string>Add Microsoft Account</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.
aaaaa</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="loadingPage">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="loadingLabel">
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Please wait...</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Status</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="mpPage">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,0,0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="minimumSize">
<size>
<width>250</width>
<height>40</height>
</size>
</property>
<property name="text">
<string>Sign in with Microsoft</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="Line" name="line_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="orLabel">
<property name="font">
<font>
<pointsize>16</pointsize>
</font>
</property>
<property name="text">
<string>Or</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="qr">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="code">
<property name="font">
<font>
<pointsize>30</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string>CODE</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyCode">
<property name="toolTip">
<string>Copy code to clipboard</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="copy">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="qrMessage">
<property name="text">
<string>Info</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="actionButton">
<property name="text">
<string>Open page and copy code</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>

View File

@@ -52,6 +52,7 @@
#include <QFileDialog>
#include <QLayout>
#include <QPushButton>
#include <QScreen>
#include <QValidator>
#include <utility>
@@ -63,6 +64,7 @@
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h"
#include "ui/widgets/PageContainer.h"
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
const QString& url,
const QMap<QString, QString>& extra_info,
@@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
updateDialogState();
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
} else {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto screen = parent->screen();
#else
auto screen = QGuiApplication::primaryScreen();
#endif
auto geometry = screen->availableSize();
resize(width(), qMin(geometry.height() - 50, 710));
}
}
void NewInstanceDialog::reject()
@@ -188,7 +200,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task
importVersion.clear();
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
importIcon = false;
}
@@ -204,7 +216,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, I
importVersion = std::move(version);
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
importIcon = false;
}
@@ -224,6 +236,9 @@ void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, const QStr
void NewInstanceDialog::setSuggestedIcon(const QString& key)
{
if (key == "default")
return;
auto icon = APPLICATION->icons()->getIcon(key);
importIcon = false;

View File

@@ -1,8 +1,6 @@
#include "OfflineLoginDialog.h"
#include "ui_OfflineLoginDialog.h"
#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog)
@@ -28,7 +26,7 @@ void OfflineLoginDialog::accept()
// Setup the login task and start it
m_account = MinecraftAccount::createOffline(ui->userTextBox->text());
m_loginTask = m_account->loginOffline();
m_loginTask = m_account->login();
connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus);

View File

@@ -20,7 +20,6 @@
#include <QItemSelectionModel>
#include "Application.h"
#include "SkinUtils.h"
#include "ui/dialogs/ProgressDialog.h"

View File

@@ -34,6 +34,7 @@
*/
#include "ProfileSetupDialog.h"
#include "net/RawHeaderProxy.h"
#include "ui_ProfileSetupDialog.h"
#include <QAction>
@@ -45,8 +46,8 @@
#include "ui/dialogs/ProgressDialog.h"
#include <Application.h>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/Upload.h"
ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent)
: QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog)
@@ -150,28 +151,27 @@ void ProfileSetupDialog::checkName(const QString& name)
currentCheck = name;
isChecking = true;
auto token = m_accountToSetup->accessToken();
QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name));
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } };
auto url = QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name);
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
m_check_response.reset(new QByteArray());
if (m_check_task)
disconnect(m_check_task.get(), nullptr, this, nullptr);
m_check_task = Net::Download::makeByteArray(url, m_check_response);
m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers));
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished);
requestor->get(request);
connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished);
m_check_task->setNetwork(APPLICATION->network());
m_check_task->start();
}
void ProfileSetupDialog::checkFinished(QNetworkReply::NetworkError error,
QByteArray profileData,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
void ProfileSetupDialog::checkFinished()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(profileData);
if (m_check_task->error() == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(*m_check_response);
auto root = doc.object();
auto statusValue = root.value("status").toString("INVALID");
if (statusValue == "AVAILABLE") {
@@ -195,20 +195,22 @@ void ProfileSetupDialog::setupProfile(const QString& profileName)
return;
}
auto token = m_accountToSetup->accessToken();
auto url = QString("https://api.minecraftservices.com/minecraft/profile");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
QString payloadTemplate("{\"profileName\":\"%1\"}");
auto profileData = payloadTemplate.arg(profileName).toUtf8();
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished);
requestor->post(request, profileData);
QUrl url("https://api.minecraftservices.com/minecraft/profile");
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } };
m_profile_response.reset(new QByteArray());
m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8());
m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers));
connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished);
m_profile_task->setNetwork(APPLICATION->network());
m_profile_task->start();
isWorking = true;
auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
@@ -244,22 +246,17 @@ struct MojangError {
} // namespace
void ProfileSetupDialog::setupProfileFinished(QNetworkReply::NetworkError error,
QByteArray errorData,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
void ProfileSetupDialog::setupProfileFinished()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
isWorking = false;
if (error == QNetworkReply::NoError) {
if (m_profile_task->error() == QNetworkReply::NoError) {
/*
* data contains the profile in the response
* ... we could parse it and update the account, but let's just return back to the normal login flow instead...
*/
accept();
} else {
auto parsedError = MojangError::fromJSON(errorData);
auto parsedError = MojangError::fromJSON(*m_profile_response);
ui->errorLabel->setVisible(true);
ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage);
qDebug() << parsedError.rawError;

View File

@@ -22,6 +22,8 @@
#include <minecraft/auth/MinecraftAccount.h>
#include <memory>
#include "net/Download.h"
#include "net/Upload.h"
namespace Ui {
class ProfileSetupDialog;
@@ -40,10 +42,10 @@ class ProfileSetupDialog : public QDialog {
void on_buttonBox_rejected();
void nameEdited(const QString& name);
void checkFinished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
void startCheck();
void setupProfileFinished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
void checkFinished();
void setupProfileFinished();
protected:
void scheduleCheck(const QString& name);
@@ -67,4 +69,10 @@ class ProfileSetupDialog : public QDialog {
QString currentCheck;
QTimer checkStartTimer;
std::shared_ptr<QByteArray> m_check_response;
Net::Download::Ptr m_check_task;
std::shared_ptr<QByteArray> m_profile_response;
Net::Upload::Ptr m_profile_task;
};

View File

@@ -27,6 +27,7 @@
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourcePackFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
@@ -91,6 +92,19 @@ void ResourceDownloadDialog::accept()
void ResourceDownloadDialog::reject()
{
auto selected = getTasks();
if (selected.count() > 0) {
auto reply = CustomMessageBox::selectable(this, tr("Confirmation Needed"),
tr("You have %1 selected resources.\n"
"Are you sure you want to close this dialog?")
.arg(selected.count()),
QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (reply != QMessageBox::Yes) {
return;
}
}
if (!geometrySaveKey().isEmpty())
APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
@@ -131,6 +145,7 @@ void ResourceDownloadDialog::confirm()
confirm_dialog->retranslateUi(resourcesString());
QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo;
QStringList depNames;
if (auto task = getModDependenciesTask(); task) {
connect(task.get(), &Task::failed, this,
[&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
@@ -153,8 +168,10 @@ void ResourceDownloadDialog::confirm()
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return;
} else {
for (auto dep : task->getDependecies())
for (auto dep : task->getDependecies()) {
addResource(dep->pack, dep->version);
depNames << dep->pack->name;
}
dependencyExtraInfo = task->getExtraInfo();
}
}
@@ -179,6 +196,9 @@ void ResourceDownloadDialog::confirm()
}
this->accept();
} else {
for (auto name : depNames)
removeResource(name);
}
}
@@ -355,4 +375,20 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
return pages;
}
void ModDownloadDialog::setModMetadata(std::shared_ptr<Metadata::ModStruct> meta)
{
switch (meta->provider) {
case ModPlatform::ResourceProvider::MODRINTH:
selectPage(Modrinth::id());
break;
case ModPlatform::ResourceProvider::FLAME:
selectPage(Flame::id());
break;
}
setWindowTitle(tr("Change %1 version").arg(meta->name));
m_container->hidePageList();
m_buttons.hide();
auto page = selectedPage();
page->openProject(meta->project_id);
}
} // namespace ResourceDownload

View File

@@ -107,6 +107,8 @@ class ModDownloadDialog final : public ResourceDownloadDialog {
QList<BasePage*> getPages() override;
GetModDependenciesTask::Ptr getModDependenciesTask() override;
void setModMetadata(std::shared_ptr<Metadata::ModStruct>);
private:
BaseInstance* m_instance;
};

View File

@@ -3,6 +3,7 @@
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
#include "ScrollMessageBox.h"
#include "StringUtils.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
@@ -29,9 +30,9 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
}
static std::optional<ModPlatform::ModLoaderTypes> mcLoaders(BaseInstance* inst)
static QList<ModPlatform::ModLoaderType> mcLoadersList(BaseInstance* inst)
{
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders() };
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoadersList();
}
ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
@@ -40,7 +41,7 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
QList<Resource*>& search_for,
bool include_deps,
bool filter_loaders)
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
: ReviewMessageBox(parent, tr("Confirm resources to update"), "")
, m_parent(parent)
, m_resource_model(resource_model)
, m_candidates(search_for)
@@ -52,8 +53,8 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
{
ReviewMessageBox::setGeometry(0, 0, 800, 600);
ui->explainLabel->setText(tr("You're about to update the following mods:"));
ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!"));
ui->explainLabel->setText(tr("You're about to update the following resources:"));
ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!"));
}
void ResourceUpdateDialog::checkCandidates()
@@ -75,8 +76,8 @@ void ResourceUpdateDialog::checkCandidates()
}
ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"),
tr("Could not generate metadata for the following mods:<br>"
"Do you wish to proceed without those mods?"),
tr("Could not generate metadata for the following resources:<br>"
"Do you wish to proceed without those resources?"),
text);
message_dialog.setModal(true);
if (message_dialog.exec() == QDialog::Rejected) {
@@ -87,12 +88,12 @@ void ResourceUpdateDialog::checkCandidates()
}
auto versions = mcVersions(m_instance);
auto loaders = m_filter_loaders ? mcLoaders(m_instance) : std::optional<ModPlatform::ModLoaderTypes>();
auto loadersList = m_filter_loaders ? mcLoadersList(m_instance) : QList<ModPlatform::ModLoaderType>();
SequentialTask check_task(m_parent, tr("Checking for updates"));
if (!m_modrinth_to_update.empty()) {
m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_resource_model));
m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_resource_model));
connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Resource* resource, QString reason, QUrl recover_url) {
m_failed_check_update.append({ resource, reason, recover_url });
@@ -101,7 +102,7 @@ void ResourceUpdateDialog::checkCandidates()
}
if (!m_flame_to_update.empty()) {
m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_resource_model));
m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_resource_model));
connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Resource* resource, QString reason, QUrl recover_url) {
m_failed_check_update.append({ resource, reason, recover_url });
@@ -179,8 +180,8 @@ void ResourceUpdateDialog::checkCandidates()
}
ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"),
tr("Could not check or get the following mods for updates:<br>"
"Do you wish to proceed without those mods?"),
tr("Could not check or get the following resources for updates:<br>"
"Do you wish to proceed without those resources?"),
text);
message_dialog.setModal(true);
if (message_dialog.exec() == QDialog::Rejected) {
@@ -474,7 +475,7 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q
break;
}
changelog_area->setHtml(text);
changelog_area->setHtml(StringUtils::htmlListPatch(text));
changelog_area->setOpenExternalLinks(true);
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);

View File

@@ -1,164 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - 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 <QFileDialog>
#include <QFileInfo>
#include <QPainter>
#include <FileSystem.h>
#include <minecraft/services/CapeChange.h>
#include <minecraft/services/SkinUpload.h>
#include <tasks/SequentialTask.h>
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
#include "SkinUploadDialog.h"
#include "ui_SkinUploadDialog.h"
void SkinUploadDialog::on_buttonBox_rejected()
{
close();
}
void SkinUploadDialog::on_buttonBox_accepted()
{
QString fileName;
QString input = ui->skinPathTextBox->text();
ProgressDialog prog(this);
SequentialTask skinUpload;
if (!input.isEmpty()) {
QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
bool isLocalFile = false;
// it has an URL prefix -> it is an URL
if (urlPrefixMatcher.match(input).hasMatch()) {
QUrl fileURL = input;
if (fileURL.isValid()) {
// local?
if (fileURL.isLocalFile()) {
isLocalFile = true;
fileName = fileURL.toLocalFile();
} else {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Using remote URLs for setting skins is not implemented yet."),
QMessageBox::Warning)
->exec();
close();
return;
}
} else {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("You cannot use an invalid URL for uploading skins."),
QMessageBox::Warning)
->exec();
close();
return;
}
} else {
// just assume it's a path then
isLocalFile = true;
fileName = ui->skinPathTextBox->text();
}
if (isLocalFile && !QFile::exists(fileName)) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
close();
return;
}
SkinUpload::Model model = SkinUpload::STEVE;
if (ui->steveBtn->isChecked()) {
model = SkinUpload::STEVE;
} else if (ui->alexBtn->isChecked()) {
model = SkinUpload::ALEX;
}
skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
}
auto selectedCape = ui->capeCombo->currentData().toString();
if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
}
if (prog.execWithTask(&skinUpload) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
close();
return;
}
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec();
close();
}
void SkinUploadDialog::on_skinBrowseBtn_clicked()
{
auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) {
return;
}
QString cooked_path = FS::NormalizePath(raw_path);
ui->skinPathTextBox->setText(cooked_path);
}
SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
{
ui->setupUi(this);
// FIXME: add a model for this, download/refresh the capes on demand
auto& accountData = *acct->accountData();
int index = 0;
ui->capeCombo->addItem(tr("No Cape"), QVariant());
auto currentCape = accountData.minecraftProfile.currentCape;
if (currentCape.isEmpty()) {
ui->capeCombo->setCurrentIndex(index);
}
for (auto& cape : accountData.minecraftProfile.capes) {
index++;
if (cape.data.size()) {
QPixmap capeImage;
if (capeImage.loadFromData(cape.data, "PNG")) {
QPixmap preview = QPixmap(10, 16);
QPainter painter(&preview);
painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
if (currentCape == cape.id) {
ui->capeCombo->setCurrentIndex(index);
}
continue;
}
}
ui->capeCombo->addItem(cape.alias, cape.id);
if (currentCape == cape.id) {
ui->capeCombo->setCurrentIndex(index);
}
}
}

View File

@@ -1,28 +0,0 @@
#pragma once
#include <minecraft/auth/MinecraftAccount.h>
#include <QDialog>
namespace Ui {
class SkinUploadDialog;
}
class SkinUploadDialog : public QDialog {
Q_OBJECT
public:
explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0);
virtual ~SkinUploadDialog(){};
public slots:
void on_buttonBox_accepted();
void on_buttonBox_rejected();
void on_skinBrowseBtn_clicked();
protected:
MinecraftAccountPtr m_acct;
private:
Ui::SkinUploadDialog* ui;
};

View File

@@ -1,95 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SkinUploadDialog</class>
<widget class="QDialog" name="SkinUploadDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>394</width>
<height>360</height>
</rect>
</property>
<property name="windowTitle">
<string>Skin Upload</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="fileBox">
<property name="title">
<string>Skin File</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="skinPathTextBox">
<property name="placeholderText">
<string>Leave empty to keep current skin</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="skinBrowseBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="modelBox">
<property name="title">
<string>Player Model</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
<item>
<widget class="QRadioButton" name="steveBtn">
<property name="text">
<string>Steve Model</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="alexBtn">
<property name="text">
<string>Alex Model</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="capeBox">
<property name="title">
<string>Cape</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QComboBox" name="capeCombo"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -25,6 +25,7 @@
#include "Application.h"
#include "BuildConfig.h"
#include "Markdown.h"
#include "StringUtils.h"
#include "ui_UpdateAvailableDialog.h"
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
@@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
auto releaseNotesHtml = markdownToHTML(releaseNotes);
ui->releaseNotes->setHtml(releaseNotesHtml);
ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
ui->releaseNotes->setOpenExternalLinks(true);
connect(ui->skipButton, &QPushButton::clicked, this, [this]() {

View File

@@ -121,7 +121,7 @@ void VersionSelectDialog::setResizeOn(int column)
int VersionSelectDialog::exec()
{
QDialog::open();
m_versionWidget->initialize(m_vlist);
m_versionWidget->initialize(m_vlist, true);
m_versionWidget->selectSearch();
if (resizeOnColumn != -1) {
m_versionWidget->setResizeOn(resizeOnColumn);

View File

@@ -26,10 +26,6 @@ class QDialogButtonBox;
class VersionSelectWidget;
class QPushButton;
namespace Ui {
class VersionSelectDialog;
}
class VersionProxyModel;
class VersionSelectDialog : public QDialog {
@@ -37,7 +33,7 @@ class VersionSelectDialog : public QDialog {
public:
explicit VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true);
virtual ~VersionSelectDialog(){};
virtual ~VersionSelectDialog() = default;
int exec() override;

View File

@@ -0,0 +1,520 @@
// 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/>.
*/
#include "SkinManageDialog.h"
#include "ui_SkinManageDialog.h"
#include <FileSystem.h>
#include <QAction>
#include <QDialog>
#include <QEventLoop>
#include <QFileDialog>
#include <QFileInfo>
#include <QKeyEvent>
#include <QListView>
#include <QMimeDatabase>
#include <QPainter>
#include <QUrl>
#include "Application.h"
#include "DesktopServices.h"
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/skins/CapeChange.h"
#include "minecraft/skins/SkinDelete.h"
#include "minecraft/skins/SkinList.h"
#include "minecraft/skins/SkinModel.h"
#include "minecraft/skins/SkinUpload.h"
#include "net/Download.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/instanceview/InstanceDelegate.h"
SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
: QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct)
{
ui->setupUi(this);
setWindowModality(Qt::WindowModal);
auto contentsWidget = ui->listView;
contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setFlow(QListView::LeftToRight);
contentsWidget->setIconSize(QSize(48, 48));
contentsWidget->setMovement(QListView::Static);
contentsWidget->setResizeMode(QListView::Adjust);
contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection);
contentsWidget->setSpacing(5);
contentsWidget->setWordWrap(false);
contentsWidget->setWrapping(true);
contentsWidget->setUniformItemSizes(true);
contentsWidget->setTextElideMode(Qt::ElideRight);
contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
contentsWidget->installEventFilter(this);
contentsWidget->setItemDelegate(new ListViewDelegate(this));
contentsWidget->setAcceptDrops(true);
contentsWidget->setDropIndicatorShown(true);
contentsWidget->viewport()->setAcceptDrops(true);
contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
contentsWidget->setDefaultDropAction(Qt::CopyAction);
contentsWidget->installEventFilter(this);
contentsWidget->setModel(&m_list);
connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection)));
connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu);
setupCapes();
ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin()));
}
SkinManageDialog::~SkinManageDialog()
{
delete ui;
}
void SkinManageDialog::activated(QModelIndex index)
{
m_selected_skin = index.data(Qt::UserRole).toString();
accept();
}
void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
{
if (selected.empty())
return;
QString key = selected.first().indexes().first().data(Qt::UserRole).toString();
if (key.isEmpty())
return;
m_selected_skin = key;
auto skin = m_list.skin(key);
if (!skin || !skin->isValid())
return;
ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId()));
ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC);
ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM);
}
void SkinManageDialog::delayed_scroll(QModelIndex model_index)
{
auto contentsWidget = ui->listView;
contentsWidget->scrollTo(model_index);
}
void SkinManageDialog::on_openDirBtn_clicked()
{
DesktopServices::openPath(m_list.getDir(), true);
}
void SkinManageDialog::on_fileBtn_clicked()
{
auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
auto message = m_list.installSkin(raw_path, {});
if (!message.isEmpty()) {
CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show();
return;
}
}
QPixmap previewCape(QPixmap capeImage)
{
QPixmap preview = QPixmap(10, 16);
QPainter painter(&preview);
painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation);
}
void SkinManageDialog::setupCapes()
{
// FIXME: add a model for this, download/refresh the capes on demand
auto& accountData = *m_acct->accountData();
int index = 0;
ui->capeCombo->addItem(tr("No Cape"), QVariant());
auto currentCape = accountData.minecraftProfile.currentCape;
if (currentCape.isEmpty()) {
ui->capeCombo->setCurrentIndex(index);
}
auto capesDir = FS::PathCombine(m_list.getDir(), "capes");
NetJob::Ptr job{ new NetJob(tr("Download capes"), APPLICATION->network()) };
bool needsToDownload = false;
for (auto& cape : accountData.minecraftProfile.capes) {
auto path = FS::PathCombine(capesDir, cape.id + ".png");
if (cape.data.size()) {
QPixmap capeImage;
if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) {
m_capes[cape.id] = previewCape(capeImage);
continue;
}
}
if (QFileInfo(path).exists()) {
continue;
}
if (!cape.url.isEmpty()) {
needsToDownload = true;
job->addNetAction(Net::Download::makeFile(cape.url, path));
}
}
if (needsToDownload) {
ProgressDialog dlg(this);
dlg.execWithTask(job.get());
}
for (auto& cape : accountData.minecraftProfile.capes) {
index++;
QPixmap capeImage;
if (!m_capes.contains(cape.id)) {
auto path = FS::PathCombine(capesDir, cape.id + ".png");
if (QFileInfo(path).exists() && capeImage.load(path)) {
capeImage = previewCape(capeImage);
m_capes[cape.id] = capeImage;
}
}
if (!capeImage.isNull()) {
ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
} else {
ui->capeCombo->addItem(cape.alias, cape.id);
}
m_capes_idx[cape.id] = index;
}
}
void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
{
auto id = ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
ui->capeImage->setPixmap(cape.scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
}
if (auto skin = m_list.skin(m_selected_skin); skin) {
skin->setCapeId(id.toString());
}
}
void SkinManageDialog::on_steveBtn_toggled(bool checked)
{
if (auto skin = m_list.skin(m_selected_skin); skin) {
skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM);
}
}
void SkinManageDialog::accept()
{
auto skin = m_list.skin(m_selected_skin);
if (!skin) {
reject();
return;
}
auto path = skin->getPath();
ProgressDialog prog(this);
NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) };
if (!QFile::exists(path)) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
reject();
return;
}
skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString()));
auto selectedCape = skin->getCapeId();
if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape));
}
skinUpload->addTask(m_acct->refresh().staticCast<Task>());
if (prog.execWithTask(skinUpload.get()) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
reject();
return;
}
skin->setURL(m_acct->accountData()->minecraftProfile.skin.url);
QDialog::accept();
}
void SkinManageDialog::on_resetBtn_clicked()
{
ProgressDialog prog(this);
NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) };
skinReset->addNetAction(SkinDelete::make(m_acct->accessToken()));
skinReset->addTask(m_acct->refresh().staticCast<Task>());
if (prog.execWithTask(skinReset.get()) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
reject();
return;
}
QDialog::accept();
}
void SkinManageDialog::show_context_menu(const QPoint& pos)
{
QMenu myMenu(tr("Context menu"), this);
myMenu.addAction(ui->action_Rename_Skin);
myMenu.addAction(ui->action_Delete_Skin);
myMenu.exec(ui->listView->mapToGlobal(pos));
}
bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
{
if (obj == ui->listView) {
if (ev->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
switch (keyEvent->key()) {
case Qt::Key_Delete:
on_action_Delete_Skin_triggered(false);
return true;
case Qt::Key_F2:
on_action_Rename_Skin_triggered(false);
return true;
default:
break;
}
}
}
return QDialog::eventFilter(obj, ev);
}
void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked)
{
if (!m_selected_skin.isEmpty()) {
ui->listView->edit(ui->listView->currentIndex());
}
}
void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked)
{
if (m_selected_skin.isEmpty())
return;
if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) {
CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec();
return;
}
auto skin = m_list.skin(m_selected_skin);
if (!skin)
return;
auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
tr("You are about to delete \"%1\".\n"
"Are you sure?")
.arg(skin->name()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response == QMessageBox::Yes) {
if (!m_list.deleteSkin(m_selected_skin, true)) {
m_list.deleteSkin(m_selected_skin, false);
}
}
}
void SkinManageDialog::on_urlBtn_clicked()
{
auto url = QUrl(ui->urlLine->text());
if (!url.isValid()) {
CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show();
return;
}
NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) };
job->setAskRetry(false);
auto path = FS::PathCombine(m_list.getDir(), url.fileName());
job->addNetAction(Net::Download::makeFile(url, path));
ProgressDialog dlg(this);
dlg.execWithTask(job.get());
SkinModel s(path);
if (!s.isValid()) {
CustomMessageBox::selectable(this, tr("URL is not a valid skin"),
QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.")
: tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()),
QMessageBox::Critical)
->show();
QFile::remove(path);
return;
}
ui->urlLine->setText("");
if (QFileInfo(path).suffix().isEmpty()) {
QFile::rename(path, path + ".png");
}
}
class WaitTask : public Task {
public:
WaitTask() : m_loop(), m_done(false) {};
virtual ~WaitTask() = default;
public slots:
void quit()
{
m_done = true;
m_loop.quit();
}
protected:
virtual void executeTask()
{
if (!m_done)
m_loop.exec();
emitSucceeded();
};
private:
QEventLoop m_loop;
bool m_done;
};
void SkinManageDialog::on_userBtn_clicked()
{
auto user = ui->urlLine->text();
if (user.isEmpty()) {
return;
}
MinecraftProfile mcProfile;
auto path = FS::PathCombine(m_list.getDir(), user + ".png");
NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) };
job->setAskRetry(false);
auto uuidOut = std::make_shared<QByteArray>();
auto profileOut = std::make_shared<QByteArray>();
auto uuidLoop = makeShared<WaitTask>();
auto profileLoop = makeShared<WaitTask>();
auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut);
auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut);
auto downloadSkin = Net::Download::makeFile(QUrl(), path);
QString failReason;
connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit);
connect(getUUID.get(), &Task::failed, this, [&failReason](QString reason) {
qCritical() << "Couldn't get user UUID:" << reason;
failReason = tr("failed to get user UUID");
});
connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit);
connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit);
connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit);
connect(getProfile.get(), &Task::failed, this, [&failReason](QString reason) {
qCritical() << "Couldn't get user profile:" << reason;
failReason = tr("failed to get user profile");
});
connect(downloadSkin.get(), &Task::failed, this, [&failReason](QString reason) {
qCritical() << "Couldn't download skin:" << reason;
failReason = tr("failed to download skin");
});
connect(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile, &failReason] {
try {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Minecraft skin service at " << parse_error.offset
<< " reason: " << parse_error.errorString();
failReason = tr("failed to parse get user UUID response");
uuidLoop->quit();
return;
}
const auto root = doc.object();
auto id = Json::ensureString(root, "id");
if (!id.isEmpty()) {
getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id);
} else {
failReason = tr("user id is empty");
job->abort();
}
} catch (const Exception& e) {
qCritical() << "Couldn't load skin json:" << e.cause();
failReason = tr("failed to parse get user UUID response");
}
uuidLoop->quit();
});
connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] {
if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) {
downloadSkin->setUrl(mcProfile.skin.url);
} else {
failReason = tr("failed to parse get user profile response");
job->abort();
}
profileLoop->quit();
});
job->addNetAction(getUUID);
job->addTask(uuidLoop);
job->addNetAction(getProfile);
job->addTask(profileLoop);
job->addNetAction(downloadSkin);
ProgressDialog dlg(this);
dlg.execWithTask(job.get());
SkinModel s(path);
if (!s.isValid()) {
if (failReason.isEmpty()) {
failReason = tr("the skin is invalid");
}
CustomMessageBox::selectable(this, tr("Username not found"),
tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical)
->show();
QFile::remove(path);
return;
}
ui->urlLine->setText("");
s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
s.setURL(mcProfile.skin.url);
if (m_capes.contains(mcProfile.currentCape)) {
s.setCapeId(mcProfile.currentCape);
}
m_list.updateSkin(&s);
}
void SkinManageDialog::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QSize s = size() * (1. / 3);
if (auto skin = m_list.skin(m_selected_skin); skin) {
if (skin->isValid()) {
ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
}
}
auto id = ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
}
}

View File

@@ -0,0 +1,65 @@
// 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/>.
*/
#pragma once
#include <QDialog>
#include <QItemSelection>
#include <QPixmap>
#include "minecraft/auth/MinecraftAccount.h"
#include "minecraft/skins/SkinList.h"
namespace Ui {
class SkinManageDialog;
}
class SkinManageDialog : public QDialog {
Q_OBJECT
public:
explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct);
virtual ~SkinManageDialog();
void resizeEvent(QResizeEvent* event) override;
public slots:
void selectionChanged(QItemSelection, QItemSelection);
void activated(QModelIndex);
void delayed_scroll(QModelIndex);
void on_openDirBtn_clicked();
void on_fileBtn_clicked();
void on_urlBtn_clicked();
void on_userBtn_clicked();
void accept() override;
void on_capeCombo_currentIndexChanged(int index);
void on_steveBtn_toggled(bool checked);
void on_resetBtn_clicked();
void show_context_menu(const QPoint& pos);
bool eventFilter(QObject* obj, QEvent* ev) override;
void on_action_Rename_Skin_triggered(bool checked);
void on_action_Delete_Skin_triggered(bool checked);
private:
void setupCapes();
MinecraftAccountPtr m_acct;
Ui::SkinManageDialog* ui;
SkinList m_list;
QString m_selected_skin;
QHash<QString, QPixmap> m_capes;
QHash<QString, int> m_capes_idx;
};

View File

@@ -0,0 +1,226 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SkinManageDialog</class>
<widget class="QDialog" name="SkinManageDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>968</width>
<height>757</height>
</rect>
</property>
<property name="windowTitle">
<string>Skin Upload</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="mainHlLayout" stretch="3,8">
<item>
<layout class="QVBoxLayout" name="selectedVLayout" stretch="2,1,3">
<item>
<widget class="QLabel" name="selectedModel">
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="modelBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Model</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="steveBtn">
<property name="text">
<string>Classic</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="alexBtn">
<property name="text">
<string>Slim</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="capeBox">
<property name="title">
<string>Cape</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QComboBox" name="capeCombo"/>
</item>
<item>
<widget class="QLabel" name="capeImage">
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="listView">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="modelColumn">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="buttonsHLayout" stretch="0,0,3,0,0,0,1">
<item>
<widget class="QPushButton" name="openDirBtn">
<property name="text">
<string>Open Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetBtn">
<property name="text">
<string>Reset Skin</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="urlLine">
<property name="placeholderText">
<string extracomment="URL or username"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="urlBtn">
<property name="text">
<string>Import URL</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="userBtn">
<property name="text">
<string>Import user</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="fileBtn">
<property name="text">
<string>Import File</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<action name="action_Delete_Skin">
<property name="text">
<string>&amp;Delete Skin</string>
</property>
<property name="toolTip">
<string>Deletes selected skin</string>
</property>
<property name="shortcut">
<string>Del</string>
</property>
</action>
<action name="action_Rename_Skin">
<property name="text">
<string>&amp;Rename Skin</string>
</property>
<property name="toolTip">
<string>Rename selected skin</string>
</property>
<property name="shortcut">
<string>F2</string>
</property>
</action>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SkinManageDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>617</x>
<y>736</y>
</hint>
<hint type="destinationlabel">
<x>483</x>
<y>378</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SkinManageDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>617</x>
<y>736</y>
</hint>
<hint type="destinationlabel">
<x>483</x>
<y>378</y>
</hint>
</hints>
</connection>
</connections>
</ui>