From 609eaa67abd9e9844f5cd52f81c70826689cd90a Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 1 Sep 2023 21:23:51 +0300 Subject: [PATCH 01/15] refactored skin apis Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 1 + launcher/minecraft/services/CapeChange.cpp | 89 +++++--------------- launcher/minecraft/services/CapeChange.h | 30 +++---- launcher/minecraft/services/SkinDelete.cpp | 59 ++++--------- launcher/minecraft/services/SkinDelete.h | 26 +++--- launcher/minecraft/services/SkinUpload.cpp | 84 +++++++----------- launcher/minecraft/services/SkinUpload.h | 31 +++---- launcher/net/Logging.cpp | 1 + launcher/net/Logging.h | 1 + launcher/net/NetRequest.cpp | 2 + launcher/net/StaticHeaderProxy.h | 39 +++++++++ launcher/ui/dialogs/SkinUploadDialog.cpp | 5 +- launcher/ui/pages/global/AccountListPage.cpp | 2 +- 13 files changed, 145 insertions(+), 225 deletions(-) create mode 100644 launcher/net/StaticHeaderProxy.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 18e0acab1..3b6218b7f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -139,6 +139,7 @@ set(NET_SOURCES net/HeaderProxy.h net/RawHeaderProxy.h net/ApiHeaderProxy.h + net/StaticHeaderProxy.h net/ApiDownload.h net/ApiDownload.cpp net/ApiUpload.cpp diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index 2ba38a6af..5a7820b54 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -35,87 +35,38 @@ #include "CapeChange.h" -#include -#include +#include -#include "Application.h" +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" -CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {} - -void CapeChange::setCape([[maybe_unused]] QString& cape) +CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token) { - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->put(request, requestString.toUtf8()); + logCat = taskMCServicesLogC; +}; - setStatus(tr("Equipping cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::clearCape() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - - setStatus(tr("Removing cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::executeTask() +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) { if (m_capeId.isEmpty()) { - clearCape(); + setStatus(tr("Removing cape")); + return m_network->deleteResource(request); } else { - setCape(m_capeId); + setStatus(tr("Equipping cape")); + return m_network->post(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); } } -void CapeChange::downloadError(QNetworkReply::NetworkError error) +void CapeChange::init() { - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); } -void CapeChange::sslErrors(const QList& errors) +CapeChange::Ptr CapeChange::make(QString token, QString capeId) { - int i = 1; - for (auto error : errors) { - qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void CapeChange::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); + auto up = makeShared(token, capeId); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; } diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h index d0c893c44..74805ef43 100644 --- a/launcher/minecraft/services/CapeChange.h +++ b/launcher/minecraft/services/CapeChange.h @@ -1,31 +1,21 @@ #pragma once -#include -#include -#include -#include "QObjectPtr.h" -#include "tasks/Task.h" +#include "net/NetRequest.h" -class CapeChange : public Task { +class CapeChange : public Net::NetRequest { Q_OBJECT public: - CapeChange(QObject* parent, QString token, QString capeId); - virtual ~CapeChange() {} + using Ptr = shared_qobject_ptr; + CapeChange(QString token, QString capeId); + virtual ~CapeChange() = default; - private: - void setCape(QString& cape); - void clearCape(); + static CapeChange::Ptr make(QString token, QString capeId); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; private: QString m_capeId; QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); }; diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index 9e9020692..7944637f6 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -35,56 +35,31 @@ #include "SkinDelete.h" -#include -#include +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" -#include "Application.h" - -SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {} - -void SkinDelete::executeTask() +SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token) { - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - m_reply = shared_qobject_ptr(rep); + logCat = taskMCServicesLogC; +}; +QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) +{ setStatus(tr("Deleting skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinDelete::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished); + return m_network->deleteResource(request); } -void SkinDelete::downloadError(QNetworkReply::NetworkError error) +void SkinDelete::init() { - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); } -void SkinDelete::sslErrors(const QList& errors) +SkinDelete::Ptr SkinDelete::make(QString token) { - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinDelete::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); + auto up = makeShared(token); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; } diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h index d5b2e63db..b0fb866cd 100644 --- a/launcher/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -1,26 +1,20 @@ #pragma once -#include -#include -#include "tasks/Task.h" +#include "net/NetRequest.h" -typedef shared_qobject_ptr SkinDeletePtr; - -class SkinDelete : public Task { +class SkinDelete : public Net::NetRequest { Q_OBJECT public: - SkinDelete(QObject* parent, QString token); + using Ptr = shared_qobject_ptr; + SkinDelete(QString token); virtual ~SkinDelete() = default; + static SkinDelete::Ptr make(QString token); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + private: QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); }; diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index 163b481b1..0400fa0f4 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -36,30 +36,17 @@ #include "SkinUpload.h" #include -#include -#include "Application.h" +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" -QByteArray getVariant(SkinUpload::Model model) +SkinUpload::SkinUpload(QString token, QByteArray skin, SkinUpload::Model model) : NetRequest(), m_model(model), m_skin(skin), m_token(token) { - switch (model) { - default: - qDebug() << "Unknown skin type!"; - case SkinUpload::STEVE: - return "CLASSIC"; - case SkinUpload::ALEX: - return "SLIM"; - } -} + logCat = taskMCServicesLogC; +}; -SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin, SkinUpload::Model model) - : Task(parent), m_model(model), m_skin(skin), m_token(token) -{} - -void SkinUpload::executeTask() +QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) { - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart skin; @@ -69,50 +56,37 @@ void SkinUpload::executeTask() QHttpPart model; model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); - model.setBody(getVariant(m_model)); + + switch (m_model) { + default: + qDebug() << "Unknown skin type!"; + emitFailed("Unknown skin type!"); + return nullptr; + case SkinUpload::STEVE: + model.setBody("CLASSIC"); + break; + case SkinUpload::ALEX: + model.setBody("SLIM"); + break; + } multiPart->append(skin); multiPart->append(model); - - QNetworkReply* rep = APPLICATION->network()->post(request, multiPart); - m_reply = shared_qobject_ptr(rep); - setStatus(tr("Uploading skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinUpload::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished); + return m_network->post(request, multiPart); } -void SkinUpload::downloadError(QNetworkReply::NetworkError error) +void SkinUpload::init() { - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); } -void SkinUpload::sslErrors(const QList& errors) +SkinUpload::Ptr SkinUpload::make(QString token, QByteArray skin, SkinUpload::Model model) { - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinUpload::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); + auto up = makeShared(token, skin, model); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; } diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h index 5716aa996..2da836d52 100644 --- a/launcher/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -1,34 +1,25 @@ #pragma once -#include -#include -#include -#include "tasks/Task.h" +#include "net/NetRequest.h" -typedef shared_qobject_ptr SkinUploadPtr; - -class SkinUpload : public Task { +class SkinUpload : public Net::NetRequest { Q_OBJECT public: + using Ptr = shared_qobject_ptr; enum Model { STEVE, ALEX }; // Note this class takes ownership of the file. - SkinUpload(QObject* parent, QString token, QByteArray skin, Model model = STEVE); - virtual ~SkinUpload() {} + SkinUpload(QString token, QByteArray skin, Model model = STEVE); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, QByteArray skin, Model model = STEVE); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; private: Model m_model; QByteArray m_skin; QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - - void downloadFinished(); }; diff --git a/launcher/net/Logging.cpp b/launcher/net/Logging.cpp index a9b9db7cf..45d2dcc20 100644 --- a/launcher/net/Logging.cpp +++ b/launcher/net/Logging.cpp @@ -22,5 +22,6 @@ Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") +Q_LOGGING_CATEGORY(taskMCServicesLogC, "launcher.task.minecraft.servicies") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") diff --git a/launcher/net/Logging.h b/launcher/net/Logging.h index 4deed2b49..d3a11cdce 100644 --- a/launcher/net/Logging.h +++ b/launcher/net/Logging.h @@ -24,5 +24,6 @@ Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMCServicesLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index ff59da18b..eef550e15 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -111,6 +111,8 @@ void NetRequest::executeTask() m_last_progress_bytes = 0; QNetworkReply* rep = getReply(request); + if (rep == nullptr) // it failed + return; m_reply.reset(rep); connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::downloadProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); diff --git a/launcher/net/StaticHeaderProxy.h b/launcher/net/StaticHeaderProxy.h new file mode 100644 index 000000000..0e62d80ff --- /dev/null +++ b/launcher/net/StaticHeaderProxy.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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 . + * + */ + +#pragma once + +#include "net/HeaderProxy.h" + +namespace Net { + +class StaticHeaderProxy : public HeaderProxy { + public: + StaticHeaderProxy(QList hdrs = {}) : HeaderProxy(), m_hdrs(hdrs){}; + virtual ~StaticHeaderProxy() = default; + + public: + virtual QList headers(const QNetworkRequest&) const override { return m_hdrs; }; + void setHeaders(QList hdrs) { m_hdrs = hdrs; }; + + private: + QList m_hdrs; +}; + +} // namespace Net \ No newline at end of file diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 5b3ebfa23..70f1e6760 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -101,12 +102,12 @@ void SkinUploadDialog::on_buttonBox_accepted() } else if (ui->alexBtn->isChecked()) { model = SkinUpload::ALEX; } - skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); + skinUpload.addTask(SkinUpload::make(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(new CapeChange(this, m_acct->accessToken(), selectedCape))); + skinUpload.addTask(CapeChange::make(m_acct->accessToken(), selectedCape)); } if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index c95bfabdd..3dcf05e0a 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -268,7 +268,7 @@ void AccountListPage::on_actionDeleteSkin_triggered() QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); ProgressDialog prog(this); - auto deleteSkinTask = std::make_shared(this, account->accessToken()); + auto deleteSkinTask = SkinDelete::make(account->accessToken()); if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); return; From 9ad029e0286ee18a6531d673c052a48a5f40d8d5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 1 Sep 2023 23:10:33 +0300 Subject: [PATCH 02/15] added skins directory Signed-off-by: Trial97 --- launcher/Application.cpp | 1 + launcher/ui/MainWindow.cpp | 5 ++++ launcher/ui/MainWindow.h | 2 ++ launcher/ui/MainWindow.ui | 13 +++++++++++ launcher/ui/pages/global/LauncherPage.cpp | 13 +++++++++++ launcher/ui/pages/global/LauncherPage.h | 1 + launcher/ui/pages/global/LauncherPage.ui | 28 +++++++++++++++++++---- 7 files changed, 59 insertions(+), 4 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 66044d9ac..9cff11546 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -536,6 +536,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); + m_settings->registerSetting("SkinsDir", "skins"); // Editors m_settings->registerSetting("JsonEditor", QString()); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 5e55a5abb..a82932e08 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1186,6 +1186,11 @@ void MainWindow::on_actionViewCentralModsFolder_triggered() DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); } +void MainWindow::on_actionViewSkinsFolder_triggered() +{ + DesktopServices::openDirectory(APPLICATION->settings()->get("SkinsDir").toString(), true); +} + void MainWindow::on_actionViewIconThemeFolder_triggered() { DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0b6144522..5d816b5d0 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -118,6 +118,8 @@ class MainWindow : public QMainWindow { void on_actionViewWidgetThemeFolder_triggered(); void on_actionViewCatPackFolder_triggered(); + void on_actionViewSkinsFolder_triggered(); + void on_actionViewSelectedInstFolder_triggered(); void refreshInstances(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 91b2c2703..266551588 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -190,6 +190,7 @@ + @@ -575,6 +576,18 @@ Open the central mods folder in a file browser. + + + + .. + + + View &Skins Folder + + + Open the skins folder in a file browser. + + Themes diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 7f22fdb50..5576a0339 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -173,6 +173,17 @@ void LauncherPage::on_downloadsDirBrowseBtn_clicked() } } +void LauncherPage::on_skinsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->skinsDirTextBox->setText(cooked_dir); + } +} + void LauncherPage::on_metadataDisableBtn_clicked() { ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); @@ -205,6 +216,7 @@ void LauncherPage::applySettings() s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text()); + s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); @@ -259,6 +271,7 @@ void LauncherPage::loadSettings() ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); + ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); QString sortMode = s->get("InstSortMode").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index e733224d2..f9aefb171 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -74,6 +74,7 @@ class LauncherPage : public QWidget, public BasePage { void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked(); + void on_skinsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); /*! diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index bc259a9b8..87c4b5ef9 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -67,7 +67,7 @@ Folders - + &Downloads: @@ -90,13 +90,16 @@ - + - + + + + Browse @@ -147,7 +150,24 @@ - + + + + Browse + + + + + + + &Skins: + + + skinsDirTextBox + + + + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). From c86b8b0f70894e447c8a08e136b919987c3dffae Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 5 Sep 2023 00:18:36 +0300 Subject: [PATCH 03/15] added skin manage dialog Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 28 +- launcher/SkinUtils.cpp | 52 --- launcher/SkinUtils.h | 22 - launcher/minecraft/services/CapeChange.h | 21 - launcher/minecraft/services/SkinDelete.h | 20 - launcher/minecraft/services/SkinUpload.h | 25 -- .../{services => skins}/CapeChange.cpp | 6 +- launcher/minecraft/skins/CapeChange.h | 39 ++ .../{services => skins}/SkinDelete.cpp | 3 +- launcher/minecraft/skins/SkinDelete.h | 38 ++ launcher/minecraft/skins/SkinList.cpp | 393 ++++++++++++++++++ launcher/minecraft/skins/SkinList.h | 80 ++++ launcher/minecraft/skins/SkinModel.cpp | 128 ++++++ launcher/minecraft/skins/SkinModel.h | 58 +++ .../{services => skins}/SkinUpload.cpp | 28 +- launcher/minecraft/skins/SkinUpload.h | 42 ++ launcher/net/Logging.cpp | 2 +- launcher/net/Logging.h | 2 +- launcher/net/NetJob.h | 4 +- launcher/net/NetRequest.cpp | 1 + launcher/net/NetRequest.h | 1 + launcher/net/StaticHeaderProxy.h | 2 +- launcher/ui/MainWindow.cpp | 1 - launcher/ui/dialogs/ProfileSelectDialog.cpp | 1 - launcher/ui/dialogs/SkinUploadDialog.cpp | 165 -------- launcher/ui/dialogs/SkinUploadDialog.h | 28 -- launcher/ui/dialogs/SkinUploadDialog.ui | 95 ----- .../ui/dialogs/skins/SkinManageDialog.cpp | 339 +++++++++++++++ launcher/ui/dialogs/skins/SkinManageDialog.h | 62 +++ launcher/ui/dialogs/skins/SkinManageDialog.ui | 193 +++++++++ launcher/ui/pages/global/AccountListPage.cpp | 34 +- launcher/ui/pages/global/AccountListPage.h | 3 +- launcher/ui/pages/global/AccountListPage.ui | 24 +- 33 files changed, 1425 insertions(+), 515 deletions(-) delete mode 100644 launcher/SkinUtils.cpp delete mode 100644 launcher/SkinUtils.h delete mode 100644 launcher/minecraft/services/CapeChange.h delete mode 100644 launcher/minecraft/services/SkinDelete.h delete mode 100644 launcher/minecraft/services/SkinUpload.h rename launcher/minecraft/{services => skins}/CapeChange.cpp (90%) create mode 100644 launcher/minecraft/skins/CapeChange.h rename launcher/minecraft/{services => skins}/SkinDelete.cpp (96%) create mode 100644 launcher/minecraft/skins/SkinDelete.h create mode 100644 launcher/minecraft/skins/SkinList.cpp create mode 100644 launcher/minecraft/skins/SkinList.h create mode 100644 launcher/minecraft/skins/SkinModel.cpp create mode 100644 launcher/minecraft/skins/SkinModel.h rename launcher/minecraft/{services => skins}/SkinUpload.cpp (79%) create mode 100644 launcher/minecraft/skins/SkinUpload.h delete mode 100644 launcher/ui/dialogs/SkinUploadDialog.cpp delete mode 100644 launcher/ui/dialogs/SkinUploadDialog.h delete mode 100644 launcher/ui/dialogs/SkinUploadDialog.ui create mode 100644 launcher/ui/dialogs/skins/SkinManageDialog.cpp create mode 100644 launcher/ui/dialogs/skins/SkinManageDialog.h create mode 100644 launcher/ui/dialogs/skins/SkinManageDialog.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3b6218b7f..6914b3385 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -377,13 +377,17 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp - # Minecraft services - minecraft/services/CapeChange.cpp - minecraft/services/CapeChange.h - minecraft/services/SkinUpload.cpp - minecraft/services/SkinUpload.h - minecraft/services/SkinDelete.cpp - minecraft/services/SkinDelete.h + # Minecraft skins + minecraft/skins/CapeChange.cpp + minecraft/skins/CapeChange.h + minecraft/skins/SkinUpload.cpp + minecraft/skins/SkinUpload.h + minecraft/skins/SkinDelete.cpp + minecraft/skins/SkinDelete.h + minecraft/skins/SkinModel.cpp + minecraft/skins/SkinModel.h + minecraft/skins/SkinList.cpp + minecraft/skins/SkinList.h minecraft/Agent.h) @@ -742,8 +746,6 @@ SET(LAUNCHER_SOURCES ui/InstanceWindow.cpp # FIXME: maybe find a better home for this. - SkinUtils.cpp - SkinUtils.h FileIgnoreProxy.cpp FileIgnoreProxy.h FastFileIconProvider.cpp @@ -965,8 +967,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ReviewMessageBox.h ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.h - ui/dialogs/SkinUploadDialog.cpp - ui/dialogs/SkinUploadDialog.h ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp @@ -980,6 +980,9 @@ SET(LAUNCHER_SOURCES ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + ui/dialogs/skins/SkinManageDialog.cpp + ui/dialogs/skins/SkinManageDialog.h + # GUI - widgets ui/widgets/Common.cpp ui/widgets/Common.h @@ -1096,7 +1099,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/NewComponentDialog.ui ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui - ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportToModListDialog.ui @@ -1111,6 +1113,8 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui + + ui/dialogs/skins/SkinManageDialog.ui ) qt_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp deleted file mode 100644 index 989114ad5..000000000 --- a/launcher/SkinUtils.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* 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 "SkinUtils.h" -#include "Application.h" -#include "net/HttpMetaCache.h" - -#include -#include -#include -#include -#include - -namespace SkinUtils { -/* - * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise - */ -QPixmap getFaceFromCache(QString username, int height, int width) -{ - QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath()); - - if (fskin.exists()) { - QPixmap skinTexture(fskin.fileName()); - if (!skinTexture.isNull()) { - QPixmap skin = QPixmap(8, 8); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - skin.fill(QColorConstants::Transparent); -#else - skin.fill(QColor(0, 0, 0, 0)); -#endif - QPainter painter(&skin); - painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); - painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(height, width, Qt::KeepAspectRatio); - } - } - - return QPixmap(); -} -} // namespace SkinUtils diff --git a/launcher/SkinUtils.h b/launcher/SkinUtils.h deleted file mode 100644 index 11bc8bc6f..000000000 --- a/launcher/SkinUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -/* 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 - -namespace SkinUtils { -QPixmap getFaceFromCache(QString id, int height = 64, int width = 64); -} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h deleted file mode 100644 index 74805ef43..000000000 --- a/launcher/minecraft/services/CapeChange.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "net/NetRequest.h" - -class CapeChange : public Net::NetRequest { - Q_OBJECT - public: - using Ptr = shared_qobject_ptr; - CapeChange(QString token, QString capeId); - virtual ~CapeChange() = default; - - static CapeChange::Ptr make(QString token, QString capeId); - void init() override; - - protected: - virtual QNetworkReply* getReply(QNetworkRequest&) override; - - private: - QString m_capeId; - QString m_token; -}; diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h deleted file mode 100644 index b0fb866cd..000000000 --- a/launcher/minecraft/services/SkinDelete.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "net/NetRequest.h" - -class SkinDelete : public Net::NetRequest { - Q_OBJECT - public: - using Ptr = shared_qobject_ptr; - SkinDelete(QString token); - virtual ~SkinDelete() = default; - - static SkinDelete::Ptr make(QString token); - void init() override; - - protected: - virtual QNetworkReply* getReply(QNetworkRequest&) override; - - private: - QString m_token; -}; diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h deleted file mode 100644 index 2da836d52..000000000 --- a/launcher/minecraft/services/SkinUpload.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "net/NetRequest.h" - -class SkinUpload : public Net::NetRequest { - Q_OBJECT - public: - using Ptr = shared_qobject_ptr; - enum Model { STEVE, ALEX }; - - // Note this class takes ownership of the file. - SkinUpload(QString token, QByteArray skin, Model model = STEVE); - virtual ~SkinUpload() = default; - - static SkinUpload::Ptr make(QString token, QByteArray skin, Model model = STEVE); - void init() override; - - protected: - virtual QNetworkReply* getReply(QNetworkRequest&) override; - - private: - Model m_model; - QByteArray m_skin; - QString m_token; -}; diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/skins/CapeChange.cpp similarity index 90% rename from launcher/minecraft/services/CapeChange.cpp rename to launcher/minecraft/skins/CapeChange.cpp index 5a7820b54..863e89844 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -42,7 +43,7 @@ CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token) { - logCat = taskMCServicesLogC; + logCat = taskMCSkinsLogC; }; QNetworkReply* CapeChange::getReply(QNetworkRequest& request) @@ -52,7 +53,7 @@ QNetworkReply* CapeChange::getReply(QNetworkRequest& request) return m_network->deleteResource(request); } else { setStatus(tr("Equipping cape")); - return m_network->post(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); + return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); } } @@ -67,6 +68,7 @@ CapeChange::Ptr CapeChange::make(QString token, QString capeId) { auto up = makeShared(token, capeId); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); return up; } diff --git a/launcher/minecraft/skins/CapeChange.h b/launcher/minecraft/skins/CapeChange.h new file mode 100644 index 000000000..bcafcde87 --- /dev/null +++ b/launcher/minecraft/skins/CapeChange.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "net/NetRequest.h" + +class CapeChange : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + CapeChange(QString token, QString capeId); + virtual ~CapeChange() = default; + + static CapeChange::Ptr make(QString token, QString capeId); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_capeId; + QString m_token; +}; diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/skins/SkinDelete.cpp similarity index 96% rename from launcher/minecraft/services/SkinDelete.cpp rename to launcher/minecraft/skins/SkinDelete.cpp index 7944637f6..982cac1b7 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/skins/SkinDelete.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -40,7 +41,7 @@ SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token) { - logCat = taskMCServicesLogC; + logCat = taskMCSkinsLogC; }; QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) diff --git a/launcher/minecraft/skins/SkinDelete.h b/launcher/minecraft/skins/SkinDelete.h new file mode 100644 index 000000000..5d02e0cc4 --- /dev/null +++ b/launcher/minecraft/skins/SkinDelete.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinDelete : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + SkinDelete(QString token); + virtual ~SkinDelete() = default; + + static SkinDelete::Ptr make(QString token); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_token; +}; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp new file mode 100644 index 000000000..be329564b --- /dev/null +++ b/launcher/minecraft/skins/SkinList.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "SkinList.h" + +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/skins/SkinModel.h" + +SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher.reset(new QFileSystemWatcher(this)); + is_watching = false; + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); + directoryChanged(path); +} + +void SkinList::startWatching() +{ + if (is_watching) { + return; + } + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void SkinList::stopWatching() +{ + save(); + if (!is_watching) { + return; + } + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool SkinList::update() +{ + QVector newSkins; + m_dir.refresh(); + + auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); + if (manifestInfo.exists()) { + try { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureArray(root, "skins"); + for (auto jSkin : skins) { + SkinModel s(m_dir, Json::ensureObject(jSkin)); + if (s.isValid()) { + newSkins << s; + } + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skins json:" << e.cause(); + } + } else { + newSkins = loadMinecraftSkins(); + } + + bool needsSave = false; + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + if (!skin.url.isEmpty() && !skin.data.isEmpty()) { + QPixmap skinTexture; + SkinModel* nskin = nullptr; + for (auto i = 0; i < newSkins.size(); i++) { + if (newSkins[i].getURL() == skin.url) { + nskin = &newSkins[i]; + break; + } + } + if (!nskin) { + auto name = m_acct->profileName() + ".png"; + if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) { + name = QUrl(skin.url).fileName() + ".png"; + } + auto path = m_dir.absoluteFilePath(name); + if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { + SkinModel s(path); + s.setModel(SkinModel::CLASSIC); // maybe better model detection + s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + s.setURL(skin.url); + newSkins << s; + needsSave = true; + } + } else { + nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + } + } + + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) { + if (!entry.isFile() && entry.suffix() != "png") + continue; + + SkinModel w(entry.absoluteFilePath()); + if (w.isValid()) { + auto add = true; + for (auto s : newSkins) { + if (s.name() == w.name()) { + add = false; + break; + } + } + if (add) { + newSkins.append(w); + needsSave = true; + } + } + } + std::sort(newSkins.begin(), newSkins.end(), + [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); + beginResetModel(); + m_skin_list.swap(newSkins); + endResetModel(); + if (needsSave) + save(); + return true; +} + +void SkinList::directoryChanged(const QString& path) +{ + QDir new_dir(path); + if (!new_dir.exists()) + if (!FS::ensureFolderPathExists(new_dir.absolutePath())) + return; + if (m_dir.absolutePath() != new_dir.absolutePath()) { + m_dir.setPath(path); + m_dir.refresh(); + if (is_watching) + stopWatching(); + startWatching(); + } + update(); +} + +void SkinList::fileChanged(const QString& path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) { + m_skin_list[i].refresh(); + dataChanged(index(i), index(i)); + break; + } + } +} + +QStringList SkinList::mimeTypes() const +{ + return { "text/uri-list" }; +} + +Qt::DropActions SkinList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool SkinList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + QStringList iconFiles; + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + iconFiles += url.toLocalFile(); + } + installSkins(iconFiles); + return true; + } + return false; +} + +Qt::ItemFlags SkinList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); + if (index.isValid()) { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +QVariant SkinList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= m_skin_list.size()) + return QVariant(); + auto skin = m_skin_list[row]; + switch (role) { + case Qt::DecorationRole: + return skin.getTexture(); + case Qt::DisplayRole: + return skin.name(); + case Qt::UserRole: + return skin.name(); + case Qt::EditRole: + return skin.name(); + default: + return QVariant(); + } +} + +int SkinList::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_skin_list.size(); +} + +void SkinList::installSkins(const QStringList& iconFiles) +{ + for (QString file : iconFiles) + installSkin(file, {}); +} + +void SkinList::installSkin(const QString& file, const QString& name) +{ + QFileInfo fileinfo(file); + if (!fileinfo.isReadable() || !fileinfo.isFile()) + return; + + if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) + return; + + QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + QFile::copy(file, target); +} + +int SkinList::getSkinIndex(const QString& key) const +{ + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].name() == key) { + return i; + } + } + return -1; +} + +const SkinModel* SkinList::skin(const QString& key) const +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +SkinModel* SkinList::skin(const QString& key) +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +bool SkinList::deleteSkin(const QString& key, const bool trash) +{ + int idx = getSkinIndex(key); + if (idx != -1) { + auto s = m_skin_list[idx]; + if (trash) { + if (FS::trash(s.getPath(), nullptr)) { + m_skin_list.remove(idx); + return true; + } + } else if (QFile::remove(s.getPath())) { + m_skin_list.remove(idx); + return true; + } + } + return false; +} + +void SkinList::save() +{ + QJsonObject doc; + QJsonArray arr; + for (auto s : m_skin_list) { + arr << s.toJSON(); + } + doc["skins"] = arr; + Json::write(doc, m_dir.absoluteFilePath("index.json")); +} + +int SkinList::getSelectedAccountSkin() +{ + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getURL() == skin.url) { + return i; + } + } + return -1; +} + +bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || role != Qt::EditRole) { + return false; + } + + int row = idx.row(); + if (row < 0 || row >= m_skin_list.size()) + return false; + auto skin = m_skin_list[row]; + auto newName = value.toString(); + if (skin.name() != newName) { + skin.rename(newName); + save(); + } + return true; +} + +QVector SkinList::loadMinecraftSkins() +{ + QString partialPath; +#if defined(Q_OS_OSX) + partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); +#elif defined(Q_OS_WIN32) + partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); +#else + partialPath = QDir::homePath(); +#endif + QVector newSkins; + auto path = FS::PathCombine(partialPath, ".minecraft", "launcher_custom_skins.json"); + auto manifestInfo = QFileInfo(path); + if (!manifestInfo.exists()) + return {}; + try { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureObject(root, "customSkins"); + for (auto key : skins.keys()) { + SkinModel s(m_dir, Json::ensureObject(skins, key)); + if (s.isValid()) { + newSkins << s; + } + } + } catch (const Exception& e) { + qCritical() << "Couldn't load minecraft skins json:" << e.cause(); + } + return newSkins; +} diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h new file mode 100644 index 000000000..8d8266d79 --- /dev/null +++ b/launcher/minecraft/skins/SkinList.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" +#include "SkinModel.h" +#include "minecraft/auth/MinecraftAccount.h" + +class SkinList : public QAbstractListModel { + Q_OBJECT + public: + explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); + virtual ~SkinList() { save(); }; + + int getSkinIndex(const QString& key) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool deleteSkin(const QString& key, const bool trash); + + void installSkins(const QStringList& iconFiles); + void installSkin(const QString& file, const QString& name); + + const SkinModel* skin(const QString& key) const; + SkinModel* skin(const QString& key); + + void startWatching(); + void stopWatching(); + + QString getDir() const { return m_dir.absolutePath(); } + void save(); + int getSelectedAccountSkin(); + + private: + // hide copy constructor + SkinList(const SkinList&) = delete; + // hide assign op + SkinList& operator=(const SkinList&) = delete; + + QVector loadMinecraftSkins(); + + protected slots: + void directoryChanged(const QString& path); + void fileChanged(const QString& path); + bool update(); + + private: + shared_qobject_ptr m_watcher; + bool is_watching; + QVector m_skin_list; + QDir m_dir; + MinecraftAccountPtr m_acct; +}; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp new file mode 100644 index 000000000..3b467019c --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "SkinModel.h" +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" + +SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {} + +SkinModel::SkinModel(QDir skinDir, QJsonObject obj) + : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) +{ + auto name = Json::ensureString(obj, "name"); + auto skinImage = Json::ensureString(obj, "skinImage"); + if (!skinImage.isEmpty()) { // minecraft skin model + skinImage = skinImage.mid(22); + m_texture.loadFromData(QByteArray::fromBase64(skinImage.toUtf8()), "PNG"); + auto textureId = Json::ensureString(obj, "textureId"); + if (name.isEmpty()) { + name = textureId; + } + if (Json::ensureBoolean(obj, "slim", false)) { + m_model = Model::SLIM; + } + } else { + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { + m_model = Model::SLIM; + } + } + m_path = skinDir.absoluteFilePath(name) + ".png"; + if (!QFileInfo(m_path).exists() && isValid()) { + m_texture.save(m_path, "PNG"); + } else { + m_texture = QPixmap(m_path); + } +} + +QString SkinModel::name() const +{ + return QFileInfo(m_path).baseName(); +} + +bool SkinModel::rename(QString newName) +{ + auto info = QFileInfo(m_path); + m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + return FS::move(info.absoluteFilePath(), m_path); +} + +QJsonObject SkinModel::toJSON() const +{ + QJsonObject obj; + obj["name"] = name(); + obj["capeId"] = m_cape_id; + obj["url"] = m_url; + obj["model"] = getModelString(); + return obj; +} + +QString SkinModel::getModelString() const +{ + switch (m_model) { + case CLASSIC: + return "CLASSIC"; + case SLIM: + return "SLIM"; + } + return {}; +} + +bool SkinModel::isValid() const +{ + return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; +} + +QPixmap SkinModel::renderFrontBody() const +{ + auto isSlim = m_model == SLIM; + auto slimOffset = isSlim ? 1 : 0; + auto isOldSkin = m_texture.height() < 64; + + auto head = m_texture.copy(QRect(8, 8, 16, 16)); + auto torso = m_texture.copy(QRect(20, 20, 28, 32)); + auto rightArm = m_texture.copy(QRect(44, 20, 48 - slimOffset, 32)); + auto rightLeg = m_texture.copy(QRect(4, 20, 8, 32)); + QPixmap leftArm, leftLeg; + + if (isOldSkin) { + leftArm = rightArm.transformed(QTransform().scale(-1, 1)); + leftLeg = rightLeg.transformed(QTransform().scale(-1, 1)); + } else { + leftArm = m_texture.copy(QRect(36, 52, 40 - slimOffset, 64)); + leftLeg = m_texture.copy(QRect(20, 52, 24, 64)); + } + QPixmap output(16, 32); + output.fill(Qt::black); + QPainter p; + if (!p.begin(&output)) + return {}; + p.drawPixmap(QPoint(4, 0), head); + p.drawPixmap(QPoint(4, 8), torso); + p.drawPixmap(QPoint(12, 8), leftArm); + p.drawPixmap(QPoint(slimOffset, 8), rightArm); + p.drawPixmap(QPoint(8, 20), leftLeg); + p.drawPixmap(QPoint(4, 20), leftArm); + + return output.scaled(128, 128, Qt::KeepAspectRatioByExpanding, Qt::FastTransformation); +} \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h new file mode 100644 index 000000000..6d135c7f7 --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +class SkinModel { + public: + enum Model { CLASSIC, SLIM }; + + SkinModel(QString path); + SkinModel(QDir skinDir, QJsonObject obj); + virtual ~SkinModel() = default; + + QString name() const; + QString getModelString() const; + bool isValid() const; + QString getPath() const { return m_path; } + QPixmap getTexture() const { return m_texture; } + QString getCapeId() const { return m_cape_id; } + Model getModel() const { return m_model; } + QString getURL() const { return m_url; } + + bool rename(QString newName); + void setCapeId(QString capeID) { m_cape_id = capeID; } + void setModel(Model model) { m_model = model; } + void setURL(QString url) { m_url = url; } + void refresh() { m_texture = QPixmap(m_path); } + + QJsonObject toJSON() const; + + QPixmap renderFrontBody() const; + + private: + QString m_path; + QPixmap m_texture; + QString m_cape_id; + Model m_model; + QString m_url; +}; \ No newline at end of file diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp similarity index 79% rename from launcher/minecraft/services/SkinUpload.cpp rename to launcher/minecraft/skins/SkinUpload.cpp index 0400fa0f4..4e56bd7e6 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -37,12 +38,13 @@ #include +#include "FileSystem.h" #include "net/ByteArraySink.h" #include "net/StaticHeaderProxy.h" -SkinUpload::SkinUpload(QString token, QByteArray skin, SkinUpload::Model model) : NetRequest(), m_model(model), m_skin(skin), m_token(token) +SkinUpload::SkinUpload(QString token, SkinModel* skin) : NetRequest(), m_skin(skin), m_token(token) { - logCat = taskMCServicesLogC; + logCat = taskUploadLogC; }; QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) @@ -52,23 +54,12 @@ QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); - skin.setBody(m_skin); + + skin.setBody(FS::read(m_skin->getPath())); QHttpPart model; model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); - - switch (m_model) { - default: - qDebug() << "Unknown skin type!"; - emitFailed("Unknown skin type!"); - return nullptr; - case SkinUpload::STEVE: - model.setBody("CLASSIC"); - break; - case SkinUpload::ALEX: - model.setBody("SLIM"); - break; - } + model.setBody(m_skin->getModelString().toUtf8()); multiPart->append(skin); multiPart->append(model); @@ -83,10 +74,11 @@ void SkinUpload::init() })); } -SkinUpload::Ptr SkinUpload::make(QString token, QByteArray skin, SkinUpload::Model model) +SkinUpload::Ptr SkinUpload::make(QString token, SkinModel* skin) { - auto up = makeShared(token, skin, model); + auto up = makeShared(token, skin); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); return up; } diff --git a/launcher/minecraft/skins/SkinUpload.h b/launcher/minecraft/skins/SkinUpload.h new file mode 100644 index 000000000..d070f301d --- /dev/null +++ b/launcher/minecraft/skins/SkinUpload.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "minecraft/skins/SkinModel.h" +#include "net/NetRequest.h" + +class SkinUpload : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + // Note this class takes ownership of the file. + SkinUpload(QString token, SkinModel* skin); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, SkinModel* skin); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + SkinModel* m_skin; + QString m_token; +}; diff --git a/launcher/net/Logging.cpp b/launcher/net/Logging.cpp index 45d2dcc20..cd0c88d3c 100644 --- a/launcher/net/Logging.cpp +++ b/launcher/net/Logging.cpp @@ -22,6 +22,6 @@ Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") -Q_LOGGING_CATEGORY(taskMCServicesLogC, "launcher.task.minecraft.servicies") +Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") diff --git a/launcher/net/Logging.h b/launcher/net/Logging.h index d3a11cdce..2536f31aa 100644 --- a/launcher/net/Logging.h +++ b/launcher/net/Logging.h @@ -24,6 +24,6 @@ Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) -Q_DECLARE_LOGGING_CATEGORY(taskMCServicesLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index cc63f4497..26791bc0d 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -52,8 +52,8 @@ class NetJob : public ConcurrentTask { public: using Ptr = shared_qobject_ptr; - explicit NetJob(QString job_name, shared_qobject_ptr network) - : ConcurrentTask(nullptr, job_name), m_network(network) + explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = 6) + : ConcurrentTask(nullptr, job_name, max_concurrent), m_network(network) {} ~NetJob() override = default; diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index eef550e15..853873528 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -5,6 +5,7 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index ee47ab2a6..917495ed9 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -4,6 +4,7 @@ * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 diff --git a/launcher/net/StaticHeaderProxy.h b/launcher/net/StaticHeaderProxy.h index 0e62d80ff..aabbc9c92 100644 --- a/launcher/net/StaticHeaderProxy.h +++ b/launcher/net/StaticHeaderProxy.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a82932e08..4858e7d46 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -77,7 +77,6 @@ #include #include #include -#include #include #include #include diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index a62238bdb..fe03e1b6b 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -20,7 +20,6 @@ #include #include "Application.h" -#include "SkinUtils.h" #include "ui/dialogs/ProgressDialog.h" diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp deleted file mode 100644 index 70f1e6760..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * 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 . - * - * 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 -#include -#include -#include - -#include - -#include -#include -#include - -#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(SkinUpload::make(m_acct->accessToken(), FS::read(fileName), model)); - } - - auto selectedCape = ui->capeCombo->currentData().toString(); - if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { - skinUpload.addTask(CapeChange::make(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); - } - } -} diff --git a/launcher/ui/dialogs/SkinUploadDialog.h b/launcher/ui/dialogs/SkinUploadDialog.h deleted file mode 100644 index 81d6140cc..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -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; -}; diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui deleted file mode 100644 index c6df92df3..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ /dev/null @@ -1,95 +0,0 @@ - - - SkinUploadDialog - - - - 0 - 0 - 394 - 360 - - - - Skin Upload - - - - - - Skin File - - - - - - Leave empty to keep current skin - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - Cape - - - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp new file mode 100644 index 000000000..1ba7e7055 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "DesktopServices.h" +#include "QObjectPtr.h" +#include "SkinManageDialog.h" + +#include "minecraft/auth/AccountTask.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/NetJob.h" +#include "tasks/Task.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/instanceview/InstanceDelegate.h" +#include "ui_SkinManageDialog.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()); + + 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) + return; + ui->selectedModel->setPixmap(skin->getTexture().scaled(128, 128, 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::openDirectory(m_list.getDir(), true); +} + +void SkinManageDialog::on_addBtn_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; + } + if (!SkinModel(raw_path).isValid()) { + CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), + tr("Skin images must be 64x64 or 64x32 pixel PNG files."), QMessageBox::Critical) + ->show(); + return; + } + m_list.installSkin(raw_path, {}); +} + +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(); + ui->capeImage->setPixmap(m_capes.value(id.toString(), {})); + 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(); + 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)); + + 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()); + 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()); + 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(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); + 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); + } + } +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h new file mode 100644 index 000000000..8c55c3310 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#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(); + + public slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void on_openDirBtn_clicked(); + void on_addBtn_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 m_capes; + QHash m_capes_idx; +}; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui new file mode 100644 index 000000000..6ad826478 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -0,0 +1,193 @@ + + + SkinManageDialog + + + + 0 + 0 + 968 + 757 + + + + Skin Upload + + + + + + + + + + + + + true + + + + + + + + 0 + 0 + + + + Model + + + + + + Clasic + + + true + + + + + + + Slim + + + + + + + + + + Cape + + + + + + + + + + + + true + + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + 0 + + + + + + + + + + + Open Folder + + + + + + + Import Skin + + + + + + + Reset Skin + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + &Delete Skin + + + Deletes selected skin + + + Del + + + + + &Rename Skin + + + Rename selected skin + + + F2 + + + + + + + buttonBox + rejected() + SkinManageDialog + reject() + + + 617 + 736 + + + 483 + 378 + + + + + buttonBox + accepted() + SkinManageDialog + accept() + + + 617 + 736 + + + 483 + 378 + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 3dcf05e0a..25ccfd0d7 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -35,6 +35,7 @@ */ #include "AccountListPage.h" +#include "ui/dialogs/skins/SkinManageDialog.h" #include "ui_AccountListPage.h" #include @@ -42,23 +43,13 @@ #include -#include "net/NetJob.h" - #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h" -#include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/SkinUploadDialog.h" - -#include "minecraft/auth/AccountTask.h" -#include "minecraft/services/SkinDelete.h" -#include "tasks/Task.h" #include "Application.h" -#include "BuildConfig.h" - AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new Ui::AccountListPage) { ui->setupUi(this); @@ -235,8 +226,7 @@ void AccountListPage::updateButtonStates() } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionManageSkins->setEnabled(accountIsReady && accountIsOnline); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if (m_accounts->defaultAccount().get() == nullptr) { @@ -248,29 +238,13 @@ void AccountListPage::updateButtonStates() } } -void AccountListPage::on_actionUploadSkin_triggered() +void AccountListPage::on_actionManageSkins_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - SkinUploadDialog dialog(account, this); + SkinManageDialog dialog(this, account); dialog.exec(); } } - -void AccountListPage::on_actionDeleteSkin_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() <= 0) - return; - - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - ProgressDialog prog(this); - auto deleteSkinTask = SkinDelete::make(account->accessToken()); - if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); - return; - } -} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index add0f4aa0..64702cff7 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -77,8 +77,7 @@ class AccountListPage : public QMainWindow, public BasePage { void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); - void on_actionUploadSkin_triggered(); - void on_actionDeleteSkin_triggered(); + void on_actionManageSkins_triggered(); void listChanged(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 469955b51..0e73f8c52 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -60,19 +60,13 @@ - - + Add &Mojang - - - Remo&ve - - &Set Default @@ -86,17 +80,12 @@ &No Default - + - &Upload Skin - - - - - &Delete Skin + &Manage Skins - Delete the currently active skin and go back to the default one + Manage Skins @@ -117,6 +106,11 @@ Refresh the account tokens + + + Remo&ve + + From 8bad255a9191cd76808a73942da366c981643d35 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 5 Sep 2023 20:13:16 +0300 Subject: [PATCH 04/15] added more import options Signed-off-by: Trial97 --- launcher/minecraft/skins/CapeChange.cpp | 2 +- launcher/minecraft/skins/SkinDelete.cpp | 2 +- launcher/minecraft/skins/SkinList.cpp | 74 ++++---- launcher/minecraft/skins/SkinList.h | 6 +- launcher/minecraft/skins/SkinModel.cpp | 58 +------ launcher/minecraft/skins/SkinModel.h | 3 +- launcher/minecraft/skins/SkinUpload.cpp | 4 +- launcher/net/NetAction.h | 1 + .../ui/dialogs/skins/SkinManageDialog.cpp | 158 ++++++++++++++++-- launcher/ui/dialogs/skins/SkinManageDialog.h | 4 +- launcher/ui/dialogs/skins/SkinManageDialog.ui | 43 ++++- 11 files changed, 229 insertions(+), 126 deletions(-) diff --git a/launcher/minecraft/skins/CapeChange.cpp b/launcher/minecraft/skins/CapeChange.cpp index 863e89844..4db28e245 100644 --- a/launcher/minecraft/skins/CapeChange.cpp +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -44,7 +44,7 @@ CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token) { logCat = taskMCSkinsLogC; -}; +} QNetworkReply* CapeChange::getReply(QNetworkRequest& request) { diff --git a/launcher/minecraft/skins/SkinDelete.cpp b/launcher/minecraft/skins/SkinDelete.cpp index 982cac1b7..3c50cf313 100644 --- a/launcher/minecraft/skins/SkinDelete.cpp +++ b/launcher/minecraft/skins/SkinDelete.cpp @@ -42,7 +42,7 @@ SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token) { logCat = taskMCSkinsLogC; -}; +} QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) { diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index be329564b..15d0b0a8e 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -85,8 +85,6 @@ bool SkinList::update() } catch (const Exception& e) { qCritical() << "Couldn't load skins json:" << e.cause(); } - } else { - newSkins = loadMinecraftSkins(); } bool needsSave = false; @@ -108,7 +106,7 @@ bool SkinList::update() auto path = m_dir.absoluteFilePath(name); if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { SkinModel s(path); - s.setModel(SkinModel::CLASSIC); // maybe better model detection + s.setModel(skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); s.setURL(skin.url); newSkins << s; @@ -116,6 +114,7 @@ bool SkinList::update() } } else { nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + nskin->setModel(skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); } } @@ -207,14 +206,14 @@ bool SkinList::dropMimeData(const QMimeData* data, // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); - QStringList iconFiles; + QStringList skinFiles; for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; - iconFiles += url.toLocalFile(); + skinFiles << url.toLocalFile(); } - installSkins(iconFiles); + installSkins(skinFiles); return true; } return false; @@ -261,20 +260,26 @@ int SkinList::rowCount(const QModelIndex& parent) const void SkinList::installSkins(const QStringList& iconFiles) { for (QString file : iconFiles) - installSkin(file, {}); + installSkin(file); } -void SkinList::installSkin(const QString& file, const QString& name) +QString SkinList::installSkin(const QString& file, const QString& name) { + if (file.isEmpty()) + return tr("Path is empty."); QFileInfo fileinfo(file); - if (!fileinfo.isReadable() || !fileinfo.isFile()) - return; - + if (!fileinfo.exists()) + return tr("File doesn't exist."); + if (!fileinfo.isFile()) + return tr("Not a file."); + if (!fileinfo.isReadable()) + return tr("File is not readable."); if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) - return; + return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); - QFile::copy(file, target); + + return QFile::copy(file, target) ? "" : tr("Unable to copy file"); } int SkinList::getSkinIndex(const QString& key) const @@ -311,10 +316,12 @@ bool SkinList::deleteSkin(const QString& key, const bool trash) if (trash) { if (FS::trash(s.getPath(), nullptr)) { m_skin_list.remove(idx); + save(); return true; } } else if (QFile::remove(s.getPath())) { m_skin_list.remove(idx); + save(); return true; } } @@ -361,33 +368,22 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) return true; } -QVector SkinList::loadMinecraftSkins() +void SkinList::updateSkin(SkinModel s) { - QString partialPath; -#if defined(Q_OS_OSX) - partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); -#elif defined(Q_OS_WIN32) - partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); -#else - partialPath = QDir::homePath(); -#endif - QVector newSkins; - auto path = FS::PathCombine(partialPath, ".minecraft", "launcher_custom_skins.json"); - auto manifestInfo = QFileInfo(path); - if (!manifestInfo.exists()) - return {}; - try { - auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); - const auto root = doc.object(); - auto skins = Json::ensureObject(root, "customSkins"); - for (auto key : skins.keys()) { - SkinModel s(m_dir, Json::ensureObject(skins, key)); - if (s.isValid()) { - newSkins << s; - } + auto done = false; + for (auto i = 0; i < m_skin_list.size(); i++) { + if (m_skin_list[i].getPath() == s.getPath()) { + m_skin_list[i].setCapeId(s.getCapeId()); + m_skin_list[i].setModel(s.getModel()); + m_skin_list[i].setURL(s.getURL()); + done = true; + break; } - } catch (const Exception& e) { - qCritical() << "Couldn't load minecraft skins json:" << e.cause(); } - return newSkins; + if (!done) { + beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); + m_skin_list.append(s); + endInsertRows(); + } + save(); } diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index 8d8266d79..b6981e1b4 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -46,7 +46,7 @@ class SkinList : public QAbstractListModel { bool deleteSkin(const QString& key, const bool trash); void installSkins(const QStringList& iconFiles); - void installSkin(const QString& file, const QString& name); + QString installSkin(const QString& file, const QString& name = {}); const SkinModel* skin(const QString& key) const; SkinModel* skin(const QString& key); @@ -58,14 +58,14 @@ class SkinList : public QAbstractListModel { void save(); int getSelectedAccountSkin(); + void updateSkin(SkinModel s); + private: // hide copy constructor SkinList(const SkinList&) = delete; // hide assign op SkinList& operator=(const SkinList&) = delete; - QVector loadMinecraftSkins(); - protected slots: void directoryChanged(const QString& path); void fileChanged(const QString& path); diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index 3b467019c..d53b9e762 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -31,28 +31,12 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) { auto name = Json::ensureString(obj, "name"); - auto skinImage = Json::ensureString(obj, "skinImage"); - if (!skinImage.isEmpty()) { // minecraft skin model - skinImage = skinImage.mid(22); - m_texture.loadFromData(QByteArray::fromBase64(skinImage.toUtf8()), "PNG"); - auto textureId = Json::ensureString(obj, "textureId"); - if (name.isEmpty()) { - name = textureId; - } - if (Json::ensureBoolean(obj, "slim", false)) { - m_model = Model::SLIM; - } - } else { - if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { - m_model = Model::SLIM; - } + + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { + m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; - if (!QFileInfo(m_path).exists() && isValid()) { - m_texture.save(m_path, "PNG"); - } else { - m_texture = QPixmap(m_path); - } + m_texture = QPixmap(m_path); } QString SkinModel::name() const @@ -92,37 +76,3 @@ bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } - -QPixmap SkinModel::renderFrontBody() const -{ - auto isSlim = m_model == SLIM; - auto slimOffset = isSlim ? 1 : 0; - auto isOldSkin = m_texture.height() < 64; - - auto head = m_texture.copy(QRect(8, 8, 16, 16)); - auto torso = m_texture.copy(QRect(20, 20, 28, 32)); - auto rightArm = m_texture.copy(QRect(44, 20, 48 - slimOffset, 32)); - auto rightLeg = m_texture.copy(QRect(4, 20, 8, 32)); - QPixmap leftArm, leftLeg; - - if (isOldSkin) { - leftArm = rightArm.transformed(QTransform().scale(-1, 1)); - leftLeg = rightLeg.transformed(QTransform().scale(-1, 1)); - } else { - leftArm = m_texture.copy(QRect(36, 52, 40 - slimOffset, 64)); - leftLeg = m_texture.copy(QRect(20, 52, 24, 64)); - } - QPixmap output(16, 32); - output.fill(Qt::black); - QPainter p; - if (!p.begin(&output)) - return {}; - p.drawPixmap(QPoint(4, 0), head); - p.drawPixmap(QPoint(4, 8), torso); - p.drawPixmap(QPoint(12, 8), leftArm); - p.drawPixmap(QPoint(slimOffset, 8), rightArm); - p.drawPixmap(QPoint(8, 20), leftLeg); - p.drawPixmap(QPoint(4, 20), leftArm); - - return output.scaled(128, 128, Qt::KeepAspectRatioByExpanding, Qt::FastTransformation); -} \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h index 6d135c7f7..46e9d6cf1 100644 --- a/launcher/minecraft/skins/SkinModel.h +++ b/launcher/minecraft/skins/SkinModel.h @@ -26,6 +26,7 @@ class SkinModel { public: enum Model { CLASSIC, SLIM }; + SkinModel() = default; SkinModel(QString path); SkinModel(QDir skinDir, QJsonObject obj); virtual ~SkinModel() = default; @@ -47,8 +48,6 @@ class SkinModel { QJsonObject toJSON() const; - QPixmap renderFrontBody() const; - private: QString m_path; QPixmap m_texture; diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp index 4e56bd7e6..4496f3f1c 100644 --- a/launcher/minecraft/skins/SkinUpload.cpp +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -44,8 +44,8 @@ SkinUpload::SkinUpload(QString token, SkinModel* skin) : NetRequest(), m_skin(skin), m_token(token) { - logCat = taskUploadLogC; -}; + logCat = taskMCSkinsLogC; +} QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) { diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index b66b91941..6440d38b9 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -55,6 +55,7 @@ class NetAction : public Task { virtual ~NetAction() = default; QUrl url() { return m_url; } + void setUrl(QUrl url) { m_url = url; } void setNetwork(shared_qobject_ptr network) { m_network = network; } diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 1ba7e7055..0afb6cbc4 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -16,37 +16,41 @@ * along with this program. If not, see . */ -#include -#include -#include -#include +#include "SkinManageDialog.h" +#include "ui_SkinManageDialog.h" #include #include #include +#include +#include #include #include #include +#include +#include +#include #include "Application.h" #include "DesktopServices.h" +#include "Json.h" #include "QObjectPtr.h" -#include "SkinManageDialog.h" #include "minecraft/auth/AccountTask.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" -#include "ui_SkinManageDialog.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) @@ -132,20 +136,15 @@ void SkinManageDialog::on_openDirBtn_clicked() DesktopServices::openDirectory(m_list.getDir(), true); } -void SkinManageDialog::on_addBtn_clicked() +void SkinManageDialog::on_fileBtn_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)) { + 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; } - if (!SkinModel(raw_path).isValid()) { - CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), - tr("Skin images must be 64x64 or 64x32 pixel PNG files."), QMessageBox::Critical) - ->show(); - return; - } - m_list.installSkin(raw_path, {}); } QPixmap previewCape(QPixmap capeImage) @@ -337,3 +336,132 @@ void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) } } } + +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; + } + ui->urlLine->setText(""); + + NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) }; + + 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"), tr("Skin images must be 64x64 or 64x32 pixel PNG files."), + QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + 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; + } + ui->urlLine->setText(""); + MinecraftProfile mcProfile; + auto path = FS::PathCombine(m_list.getDir(), user + ".png"); + + NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; + + auto uuidOut = std::make_shared(); + auto profileOut = std::make_shared(); + + auto uuidLoop = makeShared(); + auto profileLoop = makeShared(); + + 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); + + connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit); + 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(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile] { + 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(); + 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 { + job->abort(); + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skin json:" << e.cause(); + } + uuidLoop->quit(); + }); + + connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin] { + if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) { + downloadSkin->setUrl(mcProfile.skin.url); + } else { + 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); + s.setModel(mcProfile.skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setURL(mcProfile.skin.url); + if (m_capes.contains(mcProfile.currentCape)) { + s.setCapeId(mcProfile.currentCape); + } + m_list.updateSkin(s); +} \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h index 8c55c3310..ce8fc9348 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.h +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -40,7 +40,9 @@ class SkinManageDialog : public QDialog { void activated(QModelIndex); void delayed_scroll(QModelIndex); void on_openDirBtn_clicked(); - void on_addBtn_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); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index 6ad826478..c2ce9143c 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -100,7 +100,7 @@ - + @@ -108,13 +108,6 @@ - - - - Import Skin - - - @@ -122,8 +115,42 @@ + + + + + + + + + + + Import URL + + + + + + + Import user + + + + + + + Import File + + + + + + 0 + 0 + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok From 8c8e4329d72afb1b5141725127c883b52513761b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 5 Sep 2023 23:45:32 +0300 Subject: [PATCH 05/15] fix codeql Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinList.cpp | 12 ++++++------ launcher/minecraft/skins/SkinList.h | 2 +- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 15d0b0a8e..b3a593454 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -368,21 +368,21 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) return true; } -void SkinList::updateSkin(SkinModel s) +void SkinList::updateSkin(SkinModel* s) { auto done = false; for (auto i = 0; i < m_skin_list.size(); i++) { - if (m_skin_list[i].getPath() == s.getPath()) { - m_skin_list[i].setCapeId(s.getCapeId()); - m_skin_list[i].setModel(s.getModel()); - m_skin_list[i].setURL(s.getURL()); + if (m_skin_list[i].getPath() == s->getPath()) { + m_skin_list[i].setCapeId(s->getCapeId()); + m_skin_list[i].setModel(s->getModel()); + m_skin_list[i].setURL(s->getURL()); done = true; break; } } if (!done) { beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); - m_skin_list.append(s); + m_skin_list.append(*s); endInsertRows(); } save(); diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index b6981e1b4..66af6a17b 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -58,7 +58,7 @@ class SkinList : public QAbstractListModel { void save(); int getSelectedAccountSkin(); - void updateSkin(SkinModel s); + void updateSkin(SkinModel* s); private: // hide copy constructor diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 0afb6cbc4..4ef91a2bf 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -463,5 +463,5 @@ void SkinManageDialog::on_userBtn_clicked() if (m_capes.contains(mcProfile.currentCape)) { s.setCapeId(mcProfile.currentCape); } - m_list.updateSkin(s); + m_list.updateSkin(&s); } \ No newline at end of file From 6ec1cf6e4933013e04acb9086f3fcf4a844d181a Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 8 Sep 2023 19:50:46 +0300 Subject: [PATCH 06/15] made skin upload more generic Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinUpload.cpp | 10 +++++----- launcher/minecraft/skins/SkinUpload.h | 8 ++++---- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp index 4496f3f1c..4a88faedf 100644 --- a/launcher/minecraft/skins/SkinUpload.cpp +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -42,7 +42,7 @@ #include "net/ByteArraySink.h" #include "net/StaticHeaderProxy.h" -SkinUpload::SkinUpload(QString token, SkinModel* skin) : NetRequest(), m_skin(skin), m_token(token) +SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetRequest(), m_token(token), m_path(path), m_variant(variant) { logCat = taskMCSkinsLogC; } @@ -55,11 +55,11 @@ QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); - skin.setBody(FS::read(m_skin->getPath())); + skin.setBody(FS::read(m_path)); QHttpPart model; model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); - model.setBody(m_skin->getModelString().toUtf8()); + model.setBody(m_variant.toUtf8()); multiPart->append(skin); multiPart->append(model); @@ -74,9 +74,9 @@ void SkinUpload::init() })); } -SkinUpload::Ptr SkinUpload::make(QString token, SkinModel* skin) +SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) { - auto up = makeShared(token, skin); + auto up = makeShared(token, path, variant); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); diff --git a/launcher/minecraft/skins/SkinUpload.h b/launcher/minecraft/skins/SkinUpload.h index d070f301d..f24cef5a2 100644 --- a/launcher/minecraft/skins/SkinUpload.h +++ b/launcher/minecraft/skins/SkinUpload.h @@ -18,7 +18,6 @@ #pragma once -#include "minecraft/skins/SkinModel.h" #include "net/NetRequest.h" class SkinUpload : public Net::NetRequest { @@ -27,16 +26,17 @@ class SkinUpload : public Net::NetRequest { using Ptr = shared_qobject_ptr; // Note this class takes ownership of the file. - SkinUpload(QString token, SkinModel* skin); + SkinUpload(QString token, QString path, QString variant); virtual ~SkinUpload() = default; - static SkinUpload::Ptr make(QString token, SkinModel* skin); + static SkinUpload::Ptr make(QString token, QString path, QString variant); void init() override; protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; private: - SkinModel* m_skin; QString m_token; + QString m_path; + QString m_variant; }; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 4ef91a2bf..24197baeb 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -242,7 +242,7 @@ void SkinManageDialog::accept() return; } - skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin)); + skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString())); auto selectedCape = skin->getCapeId(); if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { From 263dc5af67b21719a690c3577c3e9d028fc1106a Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 25 Oct 2023 19:56:26 +0300 Subject: [PATCH 07/15] Fixed remane and delete of selected skin Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinList.cpp | 2 +- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index b3a593454..1b046a781 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -359,7 +359,7 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) int row = idx.row(); if (row < 0 || row >= m_skin_list.size()) return false; - auto skin = m_skin_list[row]; + auto& skin = m_skin_list[row]; auto newName = value.toString(); if (skin.name() != newName) { skin.rename(newName); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 24197baeb..5419d3eed 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -229,8 +229,10 @@ void SkinManageDialog::on_steveBtn_toggled(bool checked) void SkinManageDialog::accept() { auto skin = m_list.skin(m_selected_skin); - if (!skin) + if (!skin) { reject(); + return; + } auto path = skin->getPath(); ProgressDialog prog(this); @@ -315,7 +317,7 @@ void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) 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); + CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); return; } From 44cdf3f6979969c2de7953b58fd08836049fe5d5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 26 Oct 2023 13:33:00 +0300 Subject: [PATCH 08/15] Fixed skin variant Signed-off-by: Trial97 --- launcher/minecraft/auth/Parsers.cpp | 2 +- launcher/minecraft/skins/SkinList.cpp | 4 ++-- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index f6179a93e..a2b002c78 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -347,7 +347,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) Skin skinOut; // fill in default skin info ourselves, as this endpoint doesn't provide it bool steve = isDefaultModelSteve(output.id); - skinOut.variant = steve ? "classic" : "slim"; + skinOut.variant = steve ? "CLASSIC" : "SLIM"; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; // sadly we can't figure this out, but I don't think it really matters... skinOut.id = "00000000-0000-0000-0000-000000000000"; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 1b046a781..0505e4ced 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -106,7 +106,7 @@ bool SkinList::update() auto path = m_dir.absoluteFilePath(name); if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { SkinModel s(path); - s.setModel(skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setModel(skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); s.setURL(skin.url); newSkins << s; @@ -114,7 +114,7 @@ bool SkinList::update() } } else { nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); - nskin->setModel(skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); + nskin->setModel(skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); } } diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 5419d3eed..5c69be0ba 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -460,7 +460,7 @@ void SkinManageDialog::on_userBtn_clicked() dlg.execWithTask(job.get()); SkinModel s(path); - s.setModel(mcProfile.skin.variant == "slim" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setModel(mcProfile.skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setURL(mcProfile.skin.url); if (m_capes.contains(mcProfile.currentCape)) { s.setCapeId(mcProfile.currentCape); From 8b8ea2d27057dd9cebe20586f2ab5fc8426d1c99 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 26 Oct 2023 13:37:25 +0300 Subject: [PATCH 09/15] ensured that the variant is allways uppercase Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinList.cpp | 4 ++-- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 0505e4ced..fd883ad52 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -106,7 +106,7 @@ bool SkinList::update() auto path = m_dir.absoluteFilePath(name); if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { SkinModel s(path); - s.setModel(skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); s.setURL(skin.url); newSkins << s; @@ -114,7 +114,7 @@ bool SkinList::update() } } else { nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); - nskin->setModel(skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); } } diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 5c69be0ba..9320101c2 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -460,7 +460,7 @@ void SkinManageDialog::on_userBtn_clicked() dlg.execWithTask(job.get()); SkinModel s(path); - s.setModel(mcProfile.skin.variant == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + 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); From 321bbf1fa8bbdf37a5b81aa8c69512e1b04028eb Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 26 Oct 2023 19:06:38 +0300 Subject: [PATCH 10/15] fixed asan stuff Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinUpload.cpp | 2 +- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp index 4a88faedf..dc1bc0bf9 100644 --- a/launcher/minecraft/skins/SkinUpload.cpp +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -49,7 +49,7 @@ SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetReques QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) { - QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 9320101c2..8028a719c 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -74,7 +74,7 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->installEventFilter(this); - contentsWidget->setItemDelegate(new ListViewDelegate()); + contentsWidget->setItemDelegate(new ListViewDelegate(this)); contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); From 0f2736306eec092e7dd4e7eb05ab0c7911515927 Mon Sep 17 00:00:00 2001 From: Alexandru Ionut Tripon Date: Sat, 18 May 2024 10:36:15 +0300 Subject: [PATCH 11/15] Update launcher/ui/dialogs/skins/SkinManageDialog.ui Co-authored-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Signed-off-by: Alexandru Ionut Tripon --- launcher/ui/dialogs/skins/SkinManageDialog.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index c2ce9143c..ed8b7e530 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -43,7 +43,7 @@ - Clasic + Classic true From 2ac6efaa4acac61a6c3e45948188181b4fdb118b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 18 May 2024 11:56:43 +0300 Subject: [PATCH 12/15] Fix reggresion Signed-off-by: Trial97 --- launcher/ui/MainWindow.cpp | 2 +- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 0307476e4..301df2af4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1213,7 +1213,7 @@ void MainWindow::on_actionViewCentralModsFolder_triggered() void MainWindow::on_actionViewSkinsFolder_triggered() { - DesktopServices::openDirectory(APPLICATION->settings()->get("SkinsDir").toString(), true); + DesktopServices::openPath(APPLICATION->settings()->get("SkinsDir").toString(), true); } void MainWindow::on_actionViewIconThemeFolder_triggered() diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 8028a719c..9e71b867c 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -133,7 +133,7 @@ void SkinManageDialog::delayed_scroll(QModelIndex model_index) void SkinManageDialog::on_openDirBtn_clicked() { - DesktopServices::openDirectory(m_list.getDir(), true); + DesktopServices::openPath(m_list.getDir(), true); } void SkinManageDialog::on_fileBtn_clicked() From eaccdca02deb536a670a063902e666a2a341751f Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 30 May 2024 16:21:08 +0300 Subject: [PATCH 13/15] added error message for import skin from user Signed-off-by: Trial97 --- launcher/net/NetJob.cpp | 33 +++++++++++-------- launcher/net/NetJob.h | 2 ++ .../ui/dialogs/skins/SkinManageDialog.cpp | 13 ++++++-- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index a6b1207c0..a331a9769 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -148,21 +148,28 @@ void NetJob::updateState() void NetJob::emitFailed(QString reason) { #if defined(LAUNCHER_APPLICATION) - auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", - "The tasks failed\n" - "Failed urls\n" + - getFailedFiles().join("\n\t") + - "\n" - "If this continues to happen please check the logs of the application" - "Do you want to retry?", - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); + if (m_ask_retry) { + auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", + "The tasks failed.\n" + "Failed urls\n" + + getFailedFiles().join("\n\t") + + ".\n" + "If this continues to happen please check the logs of the application.\n" + "Do you want to retry?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); - if (response == QMessageBox::Yes) { - m_try = 0; - executeNextSubTask(); - return; + if (response == QMessageBox::Yes) { + m_try = 0; + executeNextSubTask(); + return; + } } #endif ConcurrentTask::emitFailed(reason); +} + +void NetJob::setAskRetry(bool askRetry) +{ + m_ask_retry = askRetry; } \ No newline at end of file diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index a50d7f91e..af09f03ba 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -62,6 +62,7 @@ class NetJob : public ConcurrentTask { auto getFailedActions() -> QList; auto getFailedFiles() -> QList; + void setAskRetry(bool askRetry); public slots: // Qt can't handle auto at the start for some reason? @@ -78,4 +79,5 @@ class NetJob : public ConcurrentTask { shared_qobject_ptr m_network; int m_try = 1; + bool m_ask_retry = true; }; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 0b60e4248..19a1f6d52 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -345,9 +345,9 @@ void SkinManageDialog::on_urlBtn_clicked() CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); return; } - ui->urlLine->setText(""); 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)); @@ -361,6 +361,7 @@ void SkinManageDialog::on_urlBtn_clicked() QFile::remove(path); return; } + ui->urlLine->setText(""); if (QFileInfo(path).suffix().isEmpty()) { QFile::rename(path, path + ".png"); } @@ -397,11 +398,11 @@ void SkinManageDialog::on_userBtn_clicked() if (user.isEmpty()) { return; } - ui->urlLine->setText(""); 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(); auto profileOut = std::make_shared(); @@ -459,6 +460,14 @@ void SkinManageDialog::on_userBtn_clicked() dlg.execWithTask(job.get()); SkinModel s(path); + if (!s.isValid()) { + CustomMessageBox::selectable(this, tr("Usename not found"), tr("Unable to find the skin for '%1'.").arg(user), + 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)) { From 04b2ac2efa7ccd4a83b88e6305744c39683c72b9 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 9 Jun 2024 20:48:49 +0300 Subject: [PATCH 14/15] update fail url skin messagebox Signed-off-by: Trial97 --- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 19a1f6d52..0ec11c59d 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -355,7 +355,9 @@ void SkinManageDialog::on_urlBtn_clicked() dlg.execWithTask(job.get()); SkinModel s(path); if (!s.isValid()) { - CustomMessageBox::selectable(this, tr("URL is not a valid skin"), tr("Skin images must be 64x64 or 64x32 pixel PNG files."), + 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); From 1e0db46728d9532d2803801d425454ebf2b4cc71 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 9 Jun 2024 21:29:56 +0300 Subject: [PATCH 15/15] add a more verbose error message in case user skin import fails Signed-off-by: Trial97 --- .../ui/dialogs/skins/SkinManageDialog.cpp | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 0ec11c59d..eb22102ee 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -416,18 +416,33 @@ void SkinManageDialog::on_userBtn_clicked() 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] { + 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; } @@ -436,18 +451,21 @@ void SkinManageDialog::on_userBtn_clicked() 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] { + 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(); @@ -463,8 +481,11 @@ void SkinManageDialog::on_userBtn_clicked() SkinModel s(path); if (!s.isValid()) { - CustomMessageBox::selectable(this, tr("Usename not found"), tr("Unable to find the skin for '%1'.").arg(user), - QMessageBox::Critical) + if (failReason.isEmpty()) { + failReason = tr("the skin is invalid"); + } + CustomMessageBox::selectable(this, tr("Usename not found"), + tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical) ->show(); QFile::remove(path); return;