diff options
Diffstat (limited to 'launcher/dialogs')
-rw-r--r-- | launcher/dialogs/ProfileSetupDialog.cpp | 248 | ||||
-rw-r--r-- | launcher/dialogs/ProfileSetupDialog.h | 88 | ||||
-rw-r--r-- | launcher/dialogs/ProfileSetupDialog.ui | 74 |
3 files changed, 410 insertions, 0 deletions
diff --git a/launcher/dialogs/ProfileSetupDialog.cpp b/launcher/dialogs/ProfileSetupDialog.cpp new file mode 100644 index 00000000..e3d7aec2 --- /dev/null +++ b/launcher/dialogs/ProfileSetupDialog.cpp @@ -0,0 +1,248 @@ +/* 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 "ProfileSetupDialog.h" +#include "ui_ProfileSetupDialog.h" + +#include <QPushButton> +#include <QAction> +#include <QRegExpValidator> +#include <QDebug> + +#include <dialogs/ProgressDialog.h> + +#include <Launcher.h> +#include <minecraft/auth/flows/AuthRequest.h> +#include <minecraft/auth/flows/Parsers.h> + +ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent) + : QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog) +{ + ui->setupUi(this); + ui->errorLabel->setVisible(false); + + goodIcon = LAUNCHER->getThemedIcon("status-good"); + yellowIcon = LAUNCHER->getThemedIcon("status-yellow"); + badIcon = LAUNCHER->getThemedIcon("status-bad"); + + QRegExp permittedNames("[a-zA-Z0-9_]{3,16}"); + auto nameEdit = ui->nameEdit; + nameEdit->setValidator(new QRegExpValidator(permittedNames)); + nameEdit->setClearButtonEnabled(true); + validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); + connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); + + checkStartTimer.setSingleShot(true); + connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); + + setNameStatus(NameStatus::NotSet, QString()); +} + +ProfileSetupDialog::~ProfileSetupDialog() +{ + delete ui; +} + +void ProfileSetupDialog::on_buttonBox_accepted() +{ + setupProfile(currentCheck); +} + +void ProfileSetupDialog::on_buttonBox_rejected() +{ + reject(); +} + +void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, QString errorString = QString()) +{ + nameStatus = status; + auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + switch(nameStatus) + { + case NameStatus::Available: { + validityAction->setIcon(goodIcon); + okButton->setEnabled(true); + } + break; + case NameStatus::NotSet: + case NameStatus::Pending: + validityAction->setIcon(yellowIcon); + okButton->setEnabled(false); + break; + case NameStatus::Exists: + case NameStatus::Error: + validityAction->setIcon(badIcon); + okButton->setEnabled(false); + break; + } + if(!errorString.isEmpty()) { + ui->errorLabel->setText(errorString); + ui->errorLabel->setVisible(true); + } + else { + ui->errorLabel->setVisible(false); + } +} + +void ProfileSetupDialog::nameEdited(const QString& name) +{ + if(!ui->nameEdit->hasAcceptableInput()) { + setNameStatus(NameStatus::NotSet, tr("Name is too short - must be between 3 and 16 characters long.")); + return; + } + scheduleCheck(name); +} + +void ProfileSetupDialog::scheduleCheck(const QString& name) { + queuedCheck = name; + setNameStatus(NameStatus::Pending); + checkStartTimer.start(1000); +} + +void ProfileSetupDialog::startCheck() { + if(isChecking) { + return; + } + if(queuedCheck.isNull()) { + return; + } + checkName(queuedCheck); +} + + +void ProfileSetupDialog::checkName(const QString &name) { + if(isChecking) { + return; + } + + currentCheck = name; + isChecking = true; + + auto token = m_accountToSetup->accessToken(); + + 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()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished); + requestor->get(request); +} + +void ProfileSetupDialog::checkFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + if(error == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(data); + auto root = doc.object(); + auto statusValue = root.value("status").toString("INVALID"); + if(statusValue == "AVAILABLE") { + setNameStatus(NameStatus::Available); + } + else if (statusValue == "DUPLICATE") { + setNameStatus(NameStatus::Exists, tr("Minecraft profile with name %1 already exists.").arg(currentCheck)); + } + else if (statusValue == "NOT_ALLOWED") { + setNameStatus(NameStatus::Exists, tr("The name %1 is not allowed.").arg(currentCheck)); + } + else { + setNameStatus(NameStatus::Error, tr("Unhandled profile name status: %1").arg(statusValue)); + } + } + else { + setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); + } + isChecking = false; +} + +void ProfileSetupDialog::setupProfile(const QString &profileName) { + if(isWorking) { + 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 data = payloadTemplate.arg(profileName).toUtf8(); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished); + requestor->post(request, data); + isWorking = true; + + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(false); +} + +namespace { + +struct MojangError{ + static MojangError fromJSON(QByteArray data) { + MojangError out; + out.error = QString::fromUtf8(data); + auto doc = QJsonDocument::fromJson(data, &out.parseError); + auto object = doc.object(); + + out.fullyParsed = true; + out.fullyParsed &= Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + + return out; + } + + QString rawError; + QJsonParseError parseError; + bool fullyParsed; + + QString path; + QString error; + QString errorMessage; +}; + +} + +void ProfileSetupDialog::setupProfileFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers +) { + isWorking = false; + if(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(data); + ui->errorLabel->setVisible(true); + ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage); + qDebug() << parsedError.rawError; + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(true); + } +} diff --git a/launcher/dialogs/ProfileSetupDialog.h b/launcher/dialogs/ProfileSetupDialog.h new file mode 100644 index 00000000..6f413ebd --- /dev/null +++ b/launcher/dialogs/ProfileSetupDialog.h @@ -0,0 +1,88 @@ +/* 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 <QDialog> +#include <QIcon> +#include <QTimer> +#include <QNetworkReply> + +#include <memory> +#include <minecraft/auth/MinecraftAccount.h> + +namespace Ui +{ +class ProfileSetupDialog; +} + +class ProfileSetupDialog : public QDialog +{ + Q_OBJECT +public: + + explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent = 0); + ~ProfileSetupDialog(); + + enum class NameStatus + { + NotSet, + Pending, + Available, + Exists, + Error + } nameStatus = NameStatus::NotSet; + +private slots: + void on_buttonBox_accepted(); + 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 + ); +protected: + void scheduleCheck(const QString &name); + void checkName(const QString &name); + void setNameStatus(NameStatus status, QString errorString); + + void setupProfile(const QString & profileName); + +private: + MinecraftAccountPtr m_accountToSetup; + Ui::ProfileSetupDialog *ui; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + QAction * validityAction = nullptr; + + QString queuedCheck; + + bool isChecking = false; + bool isWorking = false; + QString currentCheck; + + QTimer checkStartTimer; +}; + diff --git a/launcher/dialogs/ProfileSetupDialog.ui b/launcher/dialogs/ProfileSetupDialog.ui new file mode 100644 index 00000000..9dbabb4b --- /dev/null +++ b/launcher/dialogs/ProfileSetupDialog.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSetupDialog</class> + <widget class="QDialog" name="ProfileSetupDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>615</width> + <height>208</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose Minecraft name</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="descriptionLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>You just need to take one more step to be able to play Minecraft on this account. + +Choose your name carefully:</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="buddy"> + <cstring>nameEdit</cstring> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLineEdit" name="nameEdit"/> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="errorLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string notr="true">Errors go here</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> |