Add custom login support

This commit is contained in:
0xf8 2023-06-20 14:35:02 -04:00
parent a3e64b6f1b
commit 94f3d61302
Signed by: 0xf8
GPG Key ID: 446580D758689584
24 changed files with 653 additions and 12 deletions

12
PollyMC.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/libraries/javacheck" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/libraries/launcher" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -222,6 +222,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/flows/Offline.h
minecraft/auth/flows/Elyby.cpp
minecraft/auth/flows/Elyby.h
minecraft/auth/flows/Custom.cpp
minecraft/auth/flows/Custom.h
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
@ -229,6 +231,10 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/ElybyProfileStep.h
minecraft/auth/steps/ElybyStep.cpp
minecraft/auth/steps/ElybyStep.h
minecraft/auth/steps/CustomProfileStep.cpp
minecraft/auth/steps/CustomProfileStep.h
minecraft/auth/steps/CustomStep.cpp
minecraft/auth/steps/CustomStep.h
minecraft/auth/steps/GetSkinStep.cpp
minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp

View File

@ -1031,7 +1031,11 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// authlib patch
if (session->user_type == "elyby")
{
process->appendStep(makeShared<InjectAuthlib>(pptr, &m_injector));
process->appendStep(makeShared<InjectAuthlib>(pptr, &m_injector, "ely.by"));
}
else if (session->user_type == "custom")
{
process->appendStep(makeShared<InjectAuthlib>(pptr, &m_injector, session.url));
}
process->appendStep(makeShared<Update>(pptr, Net::Mode::Online));

View File

@ -354,6 +354,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
type = AccountType::Offline;
} else if (typeS == "Elyby") {
type = AccountType::Elyby;
} else if (typeS == "Custom") {
type = AccountType::Custom
} else {
qWarning() << "Failed to parse account data: type is not recognized.";
return false;
@ -414,6 +416,9 @@ QJsonObject AccountData::saveState() const {
else if (type == AccountType::Elyby) {
output["type"] = "Elyby";
}
else if (type == AccountType::Custom) {
output["type"] = "Custom";
}
tokenToJSONV3(output, yggdrasilToken, "ygg");
profileToJSONV3(output, minecraftProfile, "profile");
@ -433,14 +438,14 @@ QString AccountData::accessToken() const {
}
QString AccountData::clientToken() const {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return QString();
}
return yggdrasilToken.extra["clientToken"].toString();
}
void AccountData::setClientToken(QString clientToken) {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return;
}
yggdrasilToken.extra["clientToken"] = clientToken;
@ -454,7 +459,7 @@ void AccountData::generateClientTokenIfMissing() {
}
void AccountData::invalidateClientToken() {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) {
return;
}
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
@ -475,12 +480,11 @@ QString AccountData::profileName() const {
QString AccountData::accountDisplayString() const {
switch(type) {
case AccountType::Custom:
case AccountType::Elyby:
case AccountType::Mojang: {
return userName();
}
case AccountType::Elyby: {
return userName();
}
case AccountType::Offline: {
return QObject::tr("<Offline>");
}

View File

@ -75,7 +75,8 @@ enum class AccountType {
MSA,
Mojang,
Offline,
Elyby
Elyby,
Custom
};
enum class AccountState {
@ -114,6 +115,8 @@ struct AccountData {
QString lastError() const;
QString customUrl const;
AccountType type = AccountType::MSA;
bool legacy = false;
bool canMigrateToMSA = false;

View File

@ -40,6 +40,8 @@ struct AuthSession
QString uuid;
// 'legacy' or 'mojang', depending on account type
QString user_type;
// Yggdrasil server url
QString url;
// Did the auth server reply?
bool auth_server_online = false;
// Did the user request online mode?

View File

@ -52,6 +52,7 @@
#include "flows/Mojang.h"
#include "flows/Offline.h"
#include "flows/Elyby.h"
#include "flows/Custom.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@ -118,6 +119,17 @@ MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username)
return account;
}
MinecraftAccountPtr MinecraftAccount::createCustom(const QString &username, const QString &url)
{
MinecraftAccountPtr account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Custom;
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true;
return account;
}
QJsonObject MinecraftAccount::saveToJson() const
{
@ -185,6 +197,17 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginElyby(QString password) {
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginCustom(QString password, QString url) {
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new customLogin(&data, password, url));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
if(m_currentTask) {
return m_currentTask;
@ -199,6 +222,9 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
else if(data.type == AccountType::Elyby) {
m_currentTask.reset(new ElybyRefresh(&data));
}
else if (data.type == AccountType::Custom) {
m_currentTask.reset(new CustomRefresh(&data));
}
else {
m_currentTask.reset(new MojangRefresh(&data));
}
@ -328,6 +354,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
session->uuid = data.profileId();
// 'legacy' or 'mojang', depending on account type
session->user_type = typeString();
session->url = data.customUrl;
if (!session->access_token.isEmpty())
{
session->session = "token:" + data.accessToken() + ":" + data.profileId();

View File

@ -97,6 +97,8 @@ public: /* construction */
static MinecraftAccountPtr createElyby(const QString &username);
static MinecraftAccountPtr createCustom(const QString &username, const QString &url);
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
@ -117,6 +119,8 @@ public: /* manipulation */
shared_qobject_ptr<AccountTask> loginElyby(QString password);
shared_qobject_ptr<AccountTask> loginCustom(QString password, QString url);
shared_qobject_ptr<AccountTask> refresh();
shared_qobject_ptr<AccountTask> currentTask();
@ -146,6 +150,10 @@ public: /* queries */
return data.profileName();
}
qString customUrl() const {
return data.customUrl();
}
bool isActive() const;
bool canMigrate() const {
@ -168,6 +176,10 @@ public: /* queries */
return data.type == AccountType::Elyby;
}
bool isCustom() const {
return data.type == AccountType::Custom;
}
bool ownsMinecraft() const {
return data.minecraftEntitlement.ownsMinecraft;
}
@ -192,10 +204,15 @@ public: /* queries */
case AccountType::Offline: {
return "offline";
}
break;
case AccountType::Elyby: {
return "elyby";
}
break;
case AccountType::Custom {
return "custom";
}
break;
default: {
return "unknown";
}

View File

@ -129,6 +129,8 @@ void Yggdrasil::login(QString password, QString baseUrl) {
QJsonDocument doc(req);
this->customUrl = baseUrl;
QUrl reqUrl(baseUrl + "authenticate");
QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson();

View File

@ -97,6 +97,8 @@ protected:
QTimer counter;
int count = 0; // num msec since time reset
QString customUrl;
const int timeout_max = 30000;
const int time_step = 50;
};

View File

@ -0,0 +1,25 @@
#include "Custom.h"
#include "minecraft/auth/steps/CustomStep.h"
#include "minecraft/auth/steps/CustomProfileStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
customRefresh::customRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(makeShared<CustomStep>(m_data, QString()));
m_steps.append(makeShared<CustomProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}
customLogin::customLogin(
AccountData *data,
QString password,
QString url,
QObject *parent
): AuthFlow(data, parent), m_password(password), m_url(url) {
m_steps.append(makeShared<CustomStep>(m_data, m_password, m_url));
m_steps.append(makeShared<CustomProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}

View File

@ -0,0 +1,27 @@
#pragma once
#include "AuthFlow.h"
class customRefresh : public AuthFlow
{
Q_OBJECT
public:
explicit customRefresh(
AccountData *data,
QObject *parent = 0
);
};
class customLogin : public AuthFlow
{
Q_OBJECT
public:
explicit customLogin(
AccountData *data,
QString password,
QString url,
QObject *parent = 0
);
private:
QString m_password;
};

View File

@ -0,0 +1,94 @@
#include "CustomProfileStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
CustomProfileStep::CustomProfileStep(AccountData* data) : AuthStep(data) {
}
CustomProfileStep::~CustomProfileStep() noexcept = default;
QString CustomProfileStep::describe() {
return tr("Fetching the Minecraft profile.");
}
void CustomProfileStep::perform() {
if (m_data->minecraftProfile.id.isEmpty()) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
return;
}
// m_data->
QUrl url = QUrl(m_data.customUrl + "/session/profile/" + m_data->minecraftProfile.id);
QNetworkRequest req = QNetworkRequest(url);
AuthRequest *request = new AuthRequest(this);
connect(request, &AuthRequest::finished, this, &CustomProfileStep::onRequestDone);
request->get(req);
}
void CustomProfileStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void CustomProfileStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_SUCCEEDED,
tr("Account has no Minecraft profile.")
);
return;
}
if (error != QNetworkReply::NoError) {
qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << requestor->httpStatus_;
qWarning() << " Internal error no.: " << error;
qWarning() << " Error string: " << requestor->errorString_;
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
if (Net::isApplicationError(error)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
else {
emit finished(
AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
);
}
return;
}
if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile response could not be parsed")
);
return;
}
emit finished(
AccountTaskState::STATE_WORKING,
tr("Minecraft Java profile acquisition succeeded.")
);
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class CustomProfileStep : public AuthStep {
Q_OBJECT
public:
explicit CustomProfileStep(AccountData *data);
virtual ~CustomProfileStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,52 @@
#include "CustomStep.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/Yggdrasil.h"
CustomStep::CustomStep(AccountData* data, QString password, QString url) : AuthStep(data), m_password(password), m_url(url) {
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &CustomStep::onAuthFailed);
connect(m_yggdrasil, &Task::succeeded, this, &CustomStep::onAuthSucceeded);
connect(m_yggdrasil, &Task::aborted, this, &CustomStep::onAuthFailed);
}
CustomStep::~CustomStep() noexcept = default;
QString CustomStep::describe() {
return tr("Logging in with Custom account.");
}
void CustomStep::rehydrate() {
// NOOP, for now.
}
void CustomStep::perform() {
if(m_password.size()) {
m_yggdrasil->login(m_password, m_url + "/auth/");
}
else {
m_yggdrasil->refresh(m_url + "/auth/");
}
}
void CustomStep::onAuthSucceeded() {
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Custom"));
}
void CustomStep::onAuthFailed() {
// TODO: hook these in again, expand to MSA
// m_error = m_yggdrasil->m_error;
// m_aborted = m_yggdrasil->m_aborted;
auto state = m_yggdrasil->taskState();
QString errorMessage = tr("Custom user authentication failed.");
// NOTE: soft error in the first step means 'offline'
if(state == AccountTaskState::STATE_FAILED_SOFT) {
state = AccountTaskState::STATE_OFFLINE;
errorMessage = tr("Custom user authentication ended with a network error. Is MutliFactor Auth current?");
}
emit finished(state, errorMessage);
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class Yggdrasil;
class CustomStep : public AuthStep {
Q_OBJECT
public:
explicit CustomStep(AccountData *data, QString password, QString url);
virtual ~CustomStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onAuthSucceeded();
void onAuthFailed();
private:
Yggdrasil *m_yggdrasil = nullptr;
QString m_password;
};

View File

@ -20,9 +20,10 @@
#include <Application.h>
#include <Json.h>
InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent)
InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector, QString url) : LaunchStep(parent)
{
m_injector = injector;
m_url = url;
}
void InjectAuthlib::executeTask()
@ -130,7 +131,7 @@ void InjectAuthlib::onVersionDownloadSucceeded()
void InjectAuthlib::onDownloadSucceeded()
{
QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by");
QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg(m_url);
qDebug()
<< "Injecting " << injector;

View File

@ -71,5 +71,6 @@ private:
bool m_offlineMode;
QString m_versionName;
QString m_authServer;
QString m_url;
AuthlibInjectorPtr *m_injector;
};

View File

@ -0,0 +1,131 @@
/* 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 "CustomLoginDialog.h"
#include "ui_CustomLoginDialog.h"
#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
CustomLoginDialog::CustomLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CustomLoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
CustomLoginDialog::~CustomLoginDialog()
{
delete ui;
}
// Stage 1: User interaction
void CustomLoginDialog::accept()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createCustom(ui->userTextBox->text());
if (ui->mfaTextBox->text().length() > 0) {
m_loginTask = m_account->loginCustom(ui->passTextBox->text() + ':' + ui->mfaTextBox->text(), ui->urlTextBox->text());
}
else {
m_loginTask = m_account->loginCustom(ui->passTextBox->text(), ui->urlTextBox->text());
}
connect(m_loginTask.get(), &Task::failed, this, &CustomLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &CustomLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &CustomLoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &CustomLoginDialog::onTaskProgress);
m_loginTask->start();
}
void CustomLoginDialog::setUserInputsEnabled(bool enable)
{
ui->userTextBox->setEnabled(enable);
ui->passTextBox->setEnabled(enable);
ui->urlTextBox->setEnabled(enable);
ui->mfaTextBox->setEnabled(enable);
ui->buttonBox->setEnabled(enable);
}
// Enable the OK button only when both textboxes contain something.
void CustomLoginDialog::on_userTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty());
}
void CustomLoginDialog::on_passTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty());
}
void CustomLoginDialog::on_urlTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->passTextBox->text().isEmpty());
}
void CustomLoginDialog::onTaskFailed(const QString &reason)
{
// Set message
auto lines = reason.split('\n');
QString processed;
for(auto line: lines) {
if(line.size()) {
processed += "<font color='red'>" + line + "</font><br />";
}
else {
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
}
void CustomLoginDialog::onTaskSucceeded()
{
QDialog::accept();
}
void CustomLoginDialog::onTaskStatus(const QString &status)
{
ui->label->setText(status);
}
void CustomLoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
}
// Public interface
MinecraftAccountPtr CustomLoginDialog::newAccount(QWidget *parent, QString msg)
{
CustomLoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted)
{
return dlg.m_account;
}
return nullptr;
}

View File

@ -0,0 +1,60 @@
/* 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 <QtWidgets/QDialog>
#include <QtCore/QEventLoop>
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
namespace Ui
{
class CustomLoginDialog;
}
class CustomLoginDialog : public QDialog
{
Q_OBJECT
public:
~CustomLoginDialog();
static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
private:
explicit CustomLoginDialog(QWidget *parent = 0);
void setUserInputsEnabled(bool enable);
protected
slots:
void accept();
void onTaskFailed(const QString &reason);
void onTaskSucceeded();
void onTaskStatus(const QString &status);
void onTaskProgress(qint64 current, qint64 total);
void on_userTextBox_textEdited(const QString &newText);
void on_passTextBox_textEdited(const QString &newText);
void on_urlTextBox_textEdited(const QString &newText);
private:
Ui::CustomLoginDialog *ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
};

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CustomLoginDialog</class>
<widget class="QDialog" name="CustomLoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>421</width>
<height>198</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Add Account</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="urlTextBox">
<property name="placeholderText">
<string>Url</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="mfaTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>2FA Code (Optional)</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -49,6 +49,7 @@
#include "ui/dialogs/LoginDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/ElybyLoginDialog.h"
#include "ui/dialogs/CustomLoginDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/SkinUploadDialog.h"
@ -219,6 +220,22 @@ void AccountListPage::on_actionAddElyby_triggered()
}
}
void AccountListPage::on_actionAddCustom_triggered()
{
MinecraftAccountPtr account = CustomLoginDialog::newAccount(
this,
tr("Enter the custom yggdrasil server url, along with your username and password to add your account.")
);
if (account)
{
m_accounts->addAccount(account);
if (m_accounts->count() == 1) {
m_account->setDefaultAccount(account);
}
}
}
void AccountListPage::on_actionRemove_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
@ -263,6 +280,7 @@ void AccountListPage::updateButtonStates()
bool accountIsReady = false;
bool accountIsOnline = false;
bool accountIsElyby = false;
bool AccountIsCustom = false;
if (hasSelection)
{
QModelIndex selected = selection.first();
@ -270,12 +288,13 @@ void AccountListPage::updateButtonStates()
accountIsReady = !account->isActive();
accountIsOnline = !account->isOffline();
accountIsElyby = account->isElyby();
accountIsCustom = account->isCustom();
}
ui->actionRemove->setEnabled(accountIsReady);
ui->actionSetDefault->setEnabled(accountIsReady);
ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom);
ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom);
ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline);
if(m_accounts->defaultAccount().get() == nullptr) {

View File

@ -86,6 +86,7 @@ public slots:
void on_actionAddMicrosoft_triggered();
void on_actionAddOffline_triggered();
void on_actionAddElyby_triggered();
void on_actionAddCustom_triggered();
void on_actionRemove_triggered();
void on_actionRefresh_triggered();
void on_actionSetDefault_triggered();

View File

@ -56,6 +56,7 @@
<addaction name="actionAddMojang"/>
<addaction name="actionAddOffline"/>
<addaction name="actionAddElyby"/>
<addaction name="actionAddCustom"/>
<addaction name="actionRefresh"/>
<addaction name="actionRemove"/>
<addaction name="actionSetDefault"/>
@ -115,6 +116,11 @@
<string>Add &amp;Ely.by</string>
</property>
</action>
<action name="actionAddCustom">
<property name="text">
<string>Add &amp;Custom</string>
</property>
</action>
<action name="actionRefresh">
<property name="text">
<string>&amp;Refresh</string>