Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into fix_retry_dialog

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-06-30 22:41:49 +03:00
commit 4b4da8e6f6
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
28 changed files with 637 additions and 228 deletions

View File

@ -79,6 +79,14 @@
<string>curseforge</string> <string>curseforge</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleURLName</key>
<string>Prismlauncher</string>
<key>CFBundleURLSchemes</key>
<array>
<string>prismlauncher</string>
</array>
</dict>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -292,12 +292,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString adjustedBy; QString adjustedBy;
QString dataPath; QString dataPath;
// change folder // change folder
QString dataDirEnv;
QString dirParam = parser.value("dir"); QString dirParam = parser.value("dir");
if (!dirParam.isEmpty()) { if (!dirParam.isEmpty()) {
// the dir param. it makes multimc data path point to whatever the user specified // the dir param. it makes multimc data path point to whatever the user specified
// on command line // on command line
adjustedBy = "Command line"; adjustedBy = "Command line";
dataPath = dirParam; dataPath = dirParam;
} else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper()));
!dataDirEnv.isEmpty()) {
adjustedBy = "System environment";
dataPath = dataDirEnv;
} else { } else {
QDir foo; QDir foo;
if (DesktopServices::isSnap()) { if (DesktopServices::isSnap()) {
@ -443,7 +448,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// search the dataPath() // search the dataPath()
// seach app data standard path // seach app data standard path
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) { if (!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "..."; qDebug() << "Found" << logRulesPath << "...";
@ -555,6 +560,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentTasks", 10);
m_settings->registerSetting("NumberOfConcurrentDownloads", 6); m_settings->registerSetting("NumberOfConcurrentDownloads", 6);
m_settings->registerSetting("NumberOfManualRetries", 1); m_settings->registerSetting("NumberOfManualRetries", 1);
m_settings->registerSetting("RequestTimeout", 60);
QString defaultMonospace; QString defaultMonospace;
int defaultSize = 11; int defaultSize = 11;

View File

@ -48,7 +48,6 @@
#include <BaseInstance.h> #include <BaseInstance.h>
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "ui/themes/CatPack.h"
class LaunchController; class LaunchController;
class LocalPeer; class LocalPeer;
@ -193,6 +192,8 @@ class Application : public QApplication {
void globalSettingsClosed(); void globalSettingsClosed();
int currentCatChanged(int index); int currentCatChanged(int index);
void oauthReplyRecieved(QVariantMap);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
void clickedOnDock(); void clickedOnDock();
#endif #endif

View File

@ -59,6 +59,9 @@ void AuthFlow::executeTask()
void AuthFlow::nextStep() void AuthFlow::nextStep()
{ {
if (!Task::isRunning()) {
return;
}
if (m_steps.size() == 0) { if (m_steps.size() == 0) {
// we got to the end without an incident... assume this is all. // we got to the end without an incident... assume this is all.
m_currentStep.reset(); m_currentStep.reset();
@ -144,3 +147,10 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason)
} }
} }
} }
bool AuthFlow::abort()
{
emitAborted();
if (m_currentStep)
m_currentStep->abort();
return true;
}

View File

@ -24,6 +24,9 @@ class AuthFlow : public Task {
AccountTaskState taskState() { return m_taskState; } AccountTaskState taskState() { return m_taskState; }
public slots:
bool abort() override;
signals: signals:
void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);

View File

@ -34,6 +34,7 @@ class AuthStep : public QObject {
public slots: public slots:
virtual void perform() = 0; virtual void perform() = 0;
virtual void abort() {}
signals: signals:
void finished(AccountTaskState resultingState, QString message); void finished(AccountTaskState resultingState, QString message);

View File

@ -29,5 +29,5 @@ void GetSkinStep::onRequestDone()
{ {
if (m_task->error() == QNetworkReply::NoError) if (m_task->error() == QNetworkReply::NoError)
m_data->minecraftProfile.skin.data = *m_response; m_data->minecraftProfile.skin.data = *m_response;
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); emit finished(AccountTaskState::STATE_WORKING, tr("Got skin"));
} }

View File

@ -46,6 +46,8 @@
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
} }
QString MSADeviceCodeStep::describe() QString MSADeviceCodeStep::describe()
@ -133,9 +135,7 @@ void MSADeviceCodeStep::deviceAutorizationFinished()
m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); m_expiration_timer.setTimerType(Qt::VeryCoarseTimer);
m_expiration_timer.setInterval(rsp.expires_in * 1000); m_expiration_timer.setInterval(rsp.expires_in * 1000);
m_expiration_timer.setSingleShot(true); m_expiration_timer.setSingleShot(true);
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
m_expiration_timer.start(); m_expiration_timer.start();
m_pool_timer.setTimerType(Qt::VeryCoarseTimer); m_pool_timer.setTimerType(Qt::VeryCoarseTimer);
m_pool_timer.setSingleShot(true); m_pool_timer.setSingleShot(true);
startPoolTimer(); startPoolTimer();
@ -157,8 +157,12 @@ void MSADeviceCodeStep::startPoolTimer()
if (m_is_aborted) { if (m_is_aborted) {
return; return;
} }
if (m_expiration_timer.remainingTime() < interval * 1000) {
perform();
return;
}
m_pool_timer.setInterval(interval * 1000); m_pool_timer.setInterval(interval * 1000);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
m_pool_timer.start(); m_pool_timer.start();
} }

View File

@ -51,7 +51,7 @@ class MSADeviceCodeStep : public AuthStep {
QString describe() override; QString describe() override;
public slots: public slots:
void abort(); void abort() override;
signals: signals:
void authorizeWithBrowser(QString url, QString code, int expiresIn); void authorizeWithBrowser(QString url, QString code, int expiresIn);

View File

@ -35,22 +35,74 @@
#include "MSAStep.h" #include "MSAStep.h"
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
#include <QAbstractOAuth2> #include <QAbstractOAuth2>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QOAuthHttpServerReplyHandler>
#include <QOAuthOobReplyHandler>
#include "Application.h" #include "Application.h"
#include "BuildConfig.h"
#include "FileSystem.h"
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
bool isSchemeHandlerRegistered()
{
#ifdef Q_OS_LINUX
QProcess process;
process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME });
process.waitForFinished();
QString output = process.readAllStandardOutput().trimmed();
return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#elif defined(Q_OS_WIN)
QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
QSettings settings(regPath, QSettings::NativeFormat);
return settings.contains("shell/open/command/.");
#endif
return true;
}
class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler {
Q_OBJECT
public:
explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent)
{
connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived);
}
~CustomOAuthOobReplyHandler() override
{
disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived);
}
QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; }
};
MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") ||
QFile::exists(FS::PathCombine(APPLICATION->root(), "portable.txt")) || !isSchemeHandlerRegistered())
auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this); {
replyHandler->setCallbackText( auto replyHandler = new QOAuthHttpServerReplyHandler(this);
" <iframe src=\"https://prismlauncher.org/successful-login\" title=\"PrismLauncher Microsoft login\" style=\"position:fixed; " replyHandler->setCallbackText(R"XXX(
"top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; " <noscript>
"z-index:999999;\"/> "); <meta http-equiv="Refresh" content="0; URL=https://prismlauncher.org/successful-login" />
</noscript>
Login Successful, redirecting...
<script>
window.location.replace("https://prismlauncher.org/successful-login");
</script>
)XXX");
oauth2.setReplyHandler(replyHandler); oauth2.setReplyHandler(replyHandler);
} else {
oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this));
}
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
@ -67,8 +119,26 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile
emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
}); });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) { connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); auto state = AccountTaskState::STATE_FAILED_HARD;
if (oauth2.status() == QAbstractOAuth::Status::Granted || silent) {
if (err == QAbstractOAuth2::Error::NetworkError) {
state = AccountTaskState::STATE_OFFLINE;
} else {
state = AccountTaskState::STATE_FAILED_SOFT;
}
}
auto message = tr("Microsoft user authentication failed.");
if (silent) {
message = tr("Failed to refresh token.");
}
qWarning() << message;
emit finished(state, message);
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::error, this,
[this](const QString& error, const QString& errorDescription, const QUrl& uri) {
qWarning() << "Failed to login because" << error << errorDescription;
emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription);
}); });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
@ -89,20 +159,27 @@ void MSAStep::perform()
if (m_data->msaClientID != m_clientId) { if (m_data->msaClientID != m_clientId) {
emit finished(AccountTaskState::STATE_DISABLED, emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed.")); tr("Microsoft user authentication failed - client identification has changed."));
return;
}
if (m_data->msaToken.refresh_token.isEmpty()) {
emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty."));
return;
} }
oauth2.setRefreshToken(m_data->msaToken.refresh_token); oauth2.setRefreshToken(m_data->msaToken.refresh_token);
oauth2.refreshAccessToken(); oauth2.refreshAccessToken();
} else { } else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) { oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#else #else
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) { oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#endif #endif
map->insert("prompt", "select_account");
});
*m_data = AccountData(); *m_data = AccountData();
m_data->msaClientID = m_clientId; m_data->msaClientID = m_clientId;
oauth2.grant(); oauth2.grant();
} }
} }
#include "MSAStep.moc"

View File

@ -36,7 +36,7 @@ void MinecraftProfileStep::onRequestDone()
if (m_task->error() == QNetworkReply::ContentNotFoundError) { if (m_task->error() == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile(); m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); emit finished(AccountTaskState::STATE_WORKING, tr("Account has no Minecraft profile."));
return; return;
} }
if (m_task->error() != QNetworkReply::NoError) { if (m_task->error() != QNetworkReply::NoError) {

View File

@ -107,7 +107,11 @@ void NetRequest::executeTask()
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
#if defined(LAUNCHER_APPLICATION)
request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000);
#else
request.setTransferTimeout(); request.setTransferTimeout();
#endif
#endif #endif
m_last_progress_time = m_clock.now(); m_last_progress_time = m_clock.now();

View File

@ -2,6 +2,7 @@
<RCC version="1.0"> <RCC version="1.0">
<qresource prefix="/documents"> <qresource prefix="/documents">
<file>../../../COPYING.md</file> <file>../../../COPYING.md</file>
<file>login-qr.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -0,0 +1,8 @@
<svg width="33" height="33" version="1.1" viewBox="0 0 8.7312 8.7312" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(.26458)">
<rect width="33" height="33" fill="#fff"/>
<g>
<path d="m29 29h-6v-1h1v-3h-1v2h-1v-1h-1v1h-2v-1h1v-2h-1v-1h1v-1h-1v-1h1v-1h1v-1h1v1h1v-1h1v-1h-5v3h-2v-1h1v-1h-1v1h-2v-1h1v-1h1v-1h-1v1h-1v1h-1v-2h1v-2h2v-2h1v2h2v1h-1v1h2v-2h-1v-3h-1v-1h1v-2h-2v-1h-1v-1h-1v2h-2v1h-1v-1h-1v-3h1v1h1v1h1v-2h2v-2h1v3h3v5h1v1h-1v1h2v-1h1v1h1v1h2v1h2v2h-1v-1h-2v-1h-1v4h3v-1h1v7h-1v1h-1v1h2zm-10 0h-5v-1h3v-1h1v1h1zm-6 0h-1v-4h3v-1h1v2h1v1h-2v-1h-2zm-2 0h-7v-7h7zm15-1v-2h-1v2zm-16 0v-5h-5v5zm-1-1h-3v-3h3zm9-1h-1v-1h1zm10-1v-1h-1v-1h-1v-2h-1v3h1v1zm-9 0h-1v-1h1zm5-1v-3h-3v3zm-7 0h-1v-2h-1v1h-2v-1h-1v-1h-3v-1h6v1h2v1h2v1h-2zm6-1h-1v-1h1zm5-1v-1h-1v1zm-21-1h-1v-2h1zm-2-1h-1v-1h1zm23-1h-2v-1h2zm-17 0h-4v-1h1v-1h1v1h2zm-5 0h-1v-3h1v-1h1v-1h-1v1h-2v-3h4v1h2v1h-2v1h2v1h2v-1h1v1h1v1h-5v-1h-1v1h-2zm18-2v-2h-2v1h1v1zm-13-2h-1v-1h1zm17-1h-1v-1h1zm-14 0h-1v-1h1zm-2 0h-1v-1h1zm17-1h-1v-1h1zm-2 0h-3v-1h3zm-8 0h-1v-1h1zm-6 0h-1v-1h1zm-2 0h-1v-1h1zm5-1h-1v-1h1zm-2 0h-1v-1h1zm15-1h-7v-7h7zm-10 0h-1v-1h1zm-2 0h-1v-2h1zm-2 0h-1v-1h1zm-2 0h-1v-1h1zm-2 0h-7v-7h7zm17-1v-5h-5v5zm-18 0v-5h-5v5zm17-1h-3v-3h3zm-18 0h-3v-3h3zm5-4h-2v-1h2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -972,6 +972,14 @@ void MainWindow::processURLs(QList<QUrl> urls)
dlUrlDialod.execWithTask(job.get()); dlUrlDialod.execWithTask(job.get());
} }
} else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) {
QVariantMap receivedData;
const QUrlQuery query(url.query());
const auto items = query.queryItems();
for (auto it = items.begin(), end = items.end(); it != end; ++it)
receivedData.insert(it->first, it->second);
emit APPLICATION->oauthReplyRecieved(receivedData);
continue;
} else { } else {
dl_url = url; dl_url = url;
} }

View File

@ -34,6 +34,8 @@
*/ */
#include "MSALoginDialog.h" #include "MSALoginDialog.h"
#include "Application.h"
#include "ui_MSALoginDialog.h" #include "ui_MSALoginDialog.h"
#include "DesktopServices.h" #include "DesktopServices.h"
@ -41,6 +43,7 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QPixmap>
#include <QUrl> #include <QUrl>
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
@ -48,28 +51,48 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS
{ {
ui->setupUi(this); ui->setupUi(this);
ui->cancel->setEnabled(false); // make font monospace
ui->link->setVisible(false); QFont font;
ui->copy->setVisible(false); font.setPixelSize(ui->code->fontInfo().pixelSize());
ui->progressBar->setVisible(false); font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString());
font.setStyleHint(QFont::Monospace);
font.setFixedPitch(true);
ui->code->setFont(font);
connect(ui->cancel, &QPushButton::pressed, this, &QDialog::reject); connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); });
connect(ui->copy, &QPushButton::pressed, this, &MSALoginDialog::copyUrl); 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() int MSALoginDialog::exec()
{ {
// Setup the login task and start it // Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA(); m_account = MinecraftAccount::createBlankMSA();
m_task = m_account->login(m_using_device_code); m_authflow_task = m_account->login(false);
connect(m_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_task.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept);
connect(m_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
connect(m_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(ui->cancel, &QPushButton::pressed, m_task.get(), &Task::abort); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(&m_external_timer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort);
m_task->start();
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(); return QDialog::exec();
} }
@ -79,9 +102,12 @@ MSALoginDialog::~MSALoginDialog()
delete ui; delete ui;
} }
void MSALoginDialog::onTaskFailed(const QString& reason) void MSALoginDialog::onTaskFailed(QString reason)
{ {
// Set message // Set message
m_authflow_task->disconnect();
m_devicecode_task->disconnect();
ui->stackedWidget->setCurrentIndex(0);
auto lines = reason.split('\n'); auto lines = reason.split('\n');
QString processed; QString processed;
for (auto line : lines) { for (auto line : lines) {
@ -91,91 +117,53 @@ void MSALoginDialog::onTaskFailed(const QString& reason)
processed += "<br />"; processed += "<br />";
} }
} }
ui->message->setText(processed); 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->message->setText(status); ui->stackedWidget->setCurrentIndex(1);
ui->cancel->setEnabled(false);
ui->link->setVisible(false); const auto linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
ui->copy->setVisible(false); ui->code->setText(code);
ui->progressBar->setVisible(false); 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::onTaskStatus(QString status)
{
ui->stackedWidget->setCurrentIndex(0);
ui->status->setText(status);
} }
// Public interface // Public interface
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg, bool usingDeviceCode) MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent)
{ {
MSALoginDialog dlg(parent); MSALoginDialog dlg(parent);
dlg.m_using_device_code = usingDeviceCode;
dlg.ui->message->setText(msg);
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account; return dlg.m_account;
} }
return nullptr; return nullptr;
} }
void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
{
ui->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(url.toDisplayString());
ui->message->setText(
tr("Browser opened to complete the login process."
"<br /><br />"
"If your browser hasn't opened, please manually open the below link in your browser:"));
}
void MSALoginDialog::copyUrl()
{
QClipboard* cb = QApplication::clipboard();
cb->setText(ui->link->text());
}
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
{
m_external_elapsed = 0;
m_external_timeout = expiresIn;
m_external_timer.setInterval(1000);
m_external_timer.setSingleShot(false);
m_external_timer.start();
ui->progressBar->setMaximum(expiresIn);
ui->progressBar->setValue(m_external_elapsed);
QString linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
if (url == "https://www.microsoft.com/link" && !code.isEmpty()) {
url += QString("?otc=%1").arg(code);
ui->message->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->message->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->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
ui->progressBar->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(code);
}
void MSALoginDialog::externalLoginTick()
{
m_external_elapsed++;
ui->progressBar->setValue(m_external_elapsed);
ui->progressBar->repaint();
if (m_external_elapsed >= m_external_timeout) {
m_external_timer.stop();
}
}

View File

@ -32,29 +32,23 @@ class MSALoginDialog : public QDialog {
public: public:
~MSALoginDialog(); ~MSALoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message, bool usingDeviceCode = false); static MinecraftAccountPtr newAccount(QWidget* parent);
int exec() override; int exec() override;
private: private:
explicit MSALoginDialog(QWidget* parent = 0); explicit MSALoginDialog(QWidget* parent = 0);
protected slots: protected slots:
void onTaskFailed(const QString& reason); void onTaskFailed(QString reason);
void onTaskSucceeded(); void onTaskStatus(QString status);
void onTaskStatus(const QString& status);
void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
void copyUrl();
void externalLoginTick();
private: private:
Ui::MSALoginDialog* ui; Ui::MSALoginDialog* ui;
MinecraftAccountPtr m_account; MinecraftAccountPtr m_account;
shared_qobject_ptr<AuthFlow> m_task; shared_qobject_ptr<AuthFlow> m_devicecode_task;
shared_qobject_ptr<AuthFlow> m_authflow_task;
int m_external_elapsed; QUrl m_url;
int m_external_timeout;
QTimer m_external_timer;
bool m_using_device_code = false;
}; };

View File

@ -6,39 +6,325 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>491</width> <width>440</width>
<height>208</height> <height>430</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="minimumSize">
<sizepolicy hsizetype="Fixed" vsizetype="MinimumExpanding"> <size>
<horstretch>0</horstretch> <width>0</width>
<verstretch>0</verstretch> <height>430</height>
</sizepolicy> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Add Microsoft Account</string> <string>Add Microsoft Account</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout_6">
<item> <item>
<widget class="QLabel" name="message"> <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"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </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"> <property name="maximumSize">
<size> <size>
<width>500</width> <width>150</width>
<height>500</height> <height>150</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string notr="true"/> <string/>
</property> </property>
<property name="textFormat"> <property name="scaledContents">
<enum>Qt::RichText</enum> <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>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@ -47,46 +333,18 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="textInteractionFlags"> <property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> <set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="linkLayout">
<item>
<widget class="QLineEdit" name="link">
<property name="readOnly">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copy">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="copy">
<normaloff>.</normaloff>.</iconset>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </widget>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="cancel"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="text"> <property name="standardButtons">
<string>Cancel</string> <set>QDialogButtonBox::Cancel</set>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -145,6 +145,7 @@ void ResourceDownloadDialog::confirm()
confirm_dialog->retranslateUi(resourcesString()); confirm_dialog->retranslateUi(resourcesString());
QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo; QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo;
QStringList depNames;
if (auto task = getModDependenciesTask(); task) { if (auto task = getModDependenciesTask(); task) {
connect(task.get(), &Task::failed, this, connect(task.get(), &Task::failed, this,
[&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
@ -167,8 +168,10 @@ void ResourceDownloadDialog::confirm()
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return; return;
} else { } else {
for (auto dep : task->getDependecies()) for (auto dep : task->getDependecies()) {
addResource(dep->pack, dep->version); addResource(dep->pack, dep->version);
depNames << dep->pack->name;
}
dependencyExtraInfo = task->getExtraInfo(); dependencyExtraInfo = task->getExtraInfo();
} }
} }
@ -193,6 +196,9 @@ void ResourceDownloadDialog::confirm()
} }
this->accept(); this->accept();
} else {
for (auto name : depNames)
removeResource(name);
} }
} }

View File

@ -130,20 +130,7 @@ void AccountListPage::listChanged()
void AccountListPage::on_actionAddMicrosoft_triggered() void AccountListPage::on_actionAddMicrosoft_triggered()
{ {
QMessageBox box(this); auto account = MSALoginDialog::newAccount(this);
box.setWindowTitle(tr("Add account"));
box.setText(tr("How do you want to login?"));
box.setIcon(QMessageBox::Question);
auto deviceCode = box.addButton(tr("Legacy"), QMessageBox::ButtonRole::YesRole);
auto authCode = box.addButton(tr("Recommended"), QMessageBox::ButtonRole::NoRole);
auto cancel = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
box.setDefaultButton(authCode);
box.exec();
if ((box.clickedButton() != deviceCode && box.clickedButton() != authCode) || box.clickedButton() == cancel)
return;
MinecraftAccountPtr account = MSALoginDialog::newAccount(
this, tr("Please enter your Mojang account email and password to add your account."), box.clickedButton() == deviceCode);
if (account) { if (account) {
m_accounts->addAccount(account); m_accounts->addAccount(account);
if (m_accounts->count() == 1) { if (m_accounts->count() == 1) {

View File

@ -204,6 +204,7 @@ void LauncherPage::applySettings()
s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value()); s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value());
s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value()); s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value());
s->set("NumberOfManualRetries", ui->numberOfManualRetriesSpinBox->value()); s->set("NumberOfManualRetries", ui->numberOfManualRetriesSpinBox->value());
s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value());
// Console settings // Console settings
s->set("ShowConsole", ui->showConsoleCheck->isChecked()); s->set("ShowConsole", ui->showConsoleCheck->isChecked());
@ -261,6 +262,7 @@ void LauncherPage::loadSettings()
ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt());
ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt()); ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt());
ui->numberOfManualRetriesSpinBox->setValue(s->get("NumberOfManualRetries").toInt()); ui->numberOfManualRetriesSpinBox->setValue(s->get("NumberOfManualRetries").toInt());
ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt());
// Console settings // Console settings
ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool());

View File

@ -252,6 +252,13 @@
<string>Miscellaneous</string> <string>Miscellaneous</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QSpinBox" name="numberOfConcurrentDownloadsSpinBox">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="numberOfConcurrentTasksLabel"> <widget class="QLabel" name="numberOfConcurrentTasksLabel">
<property name="text"> <property name="text">
@ -273,21 +280,31 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="0">
<widget class="QSpinBox" name="numberOfConcurrentDownloadsSpinBox"> <widget class="QLabel" name="timeoutSecondsLabel">
<property name="minimum"> <property name="toolTip">
<number>1</number> <string>Seconds to wait until the requests are terminated</string>
</property>
<property name="text">
<string>Timeout for HTTP requests</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="1">
<widget class="QSpinBox" name="timeoutSecondsSpinBox">
<property name="suffix">
<string>s</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="numberOfManualRetriesLabel"> <widget class="QLabel" name="numberOfManualRetriesLabel">
<property name="text"> <property name="text">
<string>Number of manual retries</string> <string>Number of manual retries</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="QSpinBox" name="numberOfManualRetriesSpinBox"> <widget class="QSpinBox" name="numberOfManualRetriesSpinBox">
<property name="minimum"> <property name="minimum">
<number>0</number> <number>0</number>

View File

@ -104,7 +104,7 @@ void ImportPage::updateState()
return; return;
} }
if (ui->modpackEdit->hasAcceptableInput()) { if (ui->modpackEdit->hasAcceptableInput()) {
QString input = ui->modpackEdit->text(); QString input = ui->modpackEdit->text().trimmed();
auto url = QUrl::fromUserInput(input); auto url = QUrl::fromUserInput(input);
if (url.isLocalFile()) { if (url.isLocalFile()) {
// FIXME: actually do some validation of what's inside here... this is fake AF // FIXME: actually do some validation of what's inside here... this is fake AF

View File

@ -85,6 +85,11 @@ PrismExternalUpdater::~PrismExternalUpdater()
} }
void PrismExternalUpdater::checkForUpdates() void PrismExternalUpdater::checkForUpdates()
{
checkForUpdates(true);
}
void PrismExternalUpdater::checkForUpdates(bool triggeredByUser)
{ {
QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent);
progress.setCancelButton(nullptr); progress.setCancelButton(nullptr);
@ -160,7 +165,7 @@ void PrismExternalUpdater::checkForUpdates()
switch (exit_code) { switch (exit_code) {
case 0: case 0:
// no update available // no update available
{ if (triggeredByUser) {
qDebug() << "No update available"; qDebug() << "No update available";
auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."), auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."),
QMessageBox::Ok, priv->parent); QMessageBox::Ok, priv->parent);
@ -288,7 +293,7 @@ void PrismExternalUpdater::disconnectTimer()
void PrismExternalUpdater::autoCheckTimerFired() void PrismExternalUpdater::autoCheckTimerFired()
{ {
qDebug() << "Auto update Timer fired"; qDebug() << "Auto update Timer fired";
checkForUpdates(); checkForUpdates(false);
} }
void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes) void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes)

View File

@ -41,6 +41,7 @@ class PrismExternalUpdater : public ExternalUpdater {
* Check for updates manually, showing the user a progress bar and an alert if no updates are found. * Check for updates manually, showing the user a progress bar and an alert if no updates are found.
*/ */
void checkForUpdates() override; void checkForUpdates() override;
void checkForUpdates(bool triggeredByUser);
/*! /*!
* Indicates whether or not to check for updates automatically. * Indicates whether or not to check for updates automatically.

View File

@ -244,8 +244,9 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
auto updater_executable = QCoreApplication::applicationFilePath(); auto updater_executable = QCoreApplication::applicationFilePath();
if (BuildConfig.BUILD_ARTIFACT.toLower() == "macos") #ifdef Q_OS_MACOS
showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS"));
#endif
if (updater_executable.startsWith("/tmp/.mount_")) { if (updater_executable.startsWith("/tmp/.mount_")) {
m_isAppimage = true; m_isAppimage = true;
@ -327,6 +328,19 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
// on command line // on command line
adjustedBy = "Command line"; adjustedBy = "Command line";
m_dataPath = dirParam; m_dataPath = dirParam;
#ifndef Q_OS_MACOS
if (QDir(FS::PathCombine(m_rootPath, "UserData")).exists()) {
m_isPortable = true;
}
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
m_isPortable = true;
}
#endif
} else if (auto dataDirEnv =
QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper()));
!dataDirEnv.isEmpty()) {
adjustedBy = "System environment";
m_dataPath = dataDirEnv;
#ifndef Q_OS_MACOS #ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
m_isPortable = true; m_isPortable = true;
@ -338,7 +352,11 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
adjustedBy = "Persistent data path"; adjustedBy = "Persistent data path";
#ifndef Q_OS_MACOS #ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) {
m_dataPath = portableUserData;
adjustedBy = "Portable user data path";
m_isPortable = true;
} else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
m_dataPath = m_rootPath; m_dataPath = m_rootPath;
adjustedBy = "Portable data path"; adjustedBy = "Portable data path";
m_isPortable = true; m_isPortable = true;
@ -580,12 +598,6 @@ void PrismUpdaterApp::run()
return exit(result ? 0 : 1); return exit(result ? 0 : 1);
} }
if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux" && !m_isPortable) {
showFatalErrorMessage(tr("Updating Not Supported"),
tr("Updating non-portable linux installations is not supported. Please use your system package manager"));
return;
}
if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) { if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) {
GitHubRelease update_release = latest; GitHubRelease update_release = latest;
if (!m_userSelectedVersion.isEmpty()) { if (!m_userSelectedVersion.isEmpty()) {
@ -787,6 +799,10 @@ QList<GitHubReleaseAsset> PrismUpdaterApp::validReleaseArtifacts(const GitHubRel
if (BuildConfig.BUILD_ARTIFACT.isEmpty()) if (BuildConfig.BUILD_ARTIFACT.isEmpty())
qWarning() << "Build platform is not set!"; qWarning() << "Build platform is not set!";
for (auto asset : release.assets) { for (auto asset : release.assets) {
if (asset.name.endsWith(".zsync")) {
qDebug() << "Rejecting zsync file" << asset.name;
continue;
}
if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) { if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) {
qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; qDebug() << "Rejecting" << asset.name << "because it is an AppImage";
continue; continue;
@ -1020,7 +1036,7 @@ void PrismUpdaterApp::performInstall(QFileInfo file)
FS::write(changelog_path, m_install_release.body.toUtf8()); FS::write(changelog_path, m_install_release.body.toUtf8());
logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name)); logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name));
if (m_isPortable || file.suffix().toLower() == "zip") { if (m_isPortable || file.fileName().endsWith(".zip") || file.fileName().endsWith(".tar.gz")) {
write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath); write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath);
logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); logUpdate(tr("Updating portable install at %1").arg(m_rootPath));
unpackAndInstall(file); unpackAndInstall(file);
@ -1094,7 +1110,7 @@ void PrismUpdaterApp::backupAppDir()
if (file_list.isEmpty()) { if (file_list.isEmpty()) {
// best guess // best guess
if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux") { if (BuildConfig.BUILD_ARTIFACT.toLower().contains("linux")) {
file_list.append({ "PrismLauncher", "bin", "share", "lib" }); file_list.append({ "PrismLauncher", "bin", "share", "lib" });
} else { // windows by process of elimination } else { // windows by process of elimination
file_list.append({ file_list.append({

View File

@ -1,13 +1,13 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=Prism Launcher Name=@Launcher_DisplayName@
Comment=Discover, manage, and play Minecraft instances Comment=Discover, manage, and play Minecraft instances
Type=Application Type=Application
Terminal=false Terminal=false
Exec=@Launcher_APP_BINARY_NAME@ %U Exec=@Launcher_APP_BINARY_NAME@ %U
StartupNotify=true StartupNotify=true
Icon=org.prismlauncher.PrismLauncher Icon=org.@Launcher_APP_BINARY_NAME@.@Launcher_CommonName@
Categories=Game;ActionGame;AdventureGame;Simulation; Categories=Game;ActionGame;AdventureGame;Simulation;
Keywords=game;minecraft;mc; Keywords=game;minecraft;mc;
StartupWMClass=PrismLauncher StartupWMClass=@Launcher_CommonName@
MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge; MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/@Launcher_APP_BINARY_NAME@;

View File

@ -368,6 +368,10 @@ Section "@Launcher_DisplayName@"
WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" "" WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" ""
WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"'
; Write the URL Handler into registry for prismlauncher
WriteRegStr HKCU Software\Classes\prismlauncher "URL Protocol" ""
WriteRegStr HKCU Software\Classes\prismlauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"'
; Write the uninstall keys for Windows ; Write the uninstall keys for Windows
${GetParameters} $R0 ${GetParameters} $R0
${GetOptions} $R0 "/NoUninstaller" $R1 ${GetOptions} $R0 "/NoUninstaller" $R1