Merge pull request #1609 from Trial97/skin_selector

Skin management
This commit is contained in:
Alexandru Ionut Tripon 2024-06-10 08:43:37 +03:00 committed by GitHub
commit dc39698c48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1857 additions and 861 deletions

View File

@ -588,6 +588,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("DownloadsDirWatchRecursive", false);
m_settings->registerSetting("SkinsDir", "skins");
// Editors // Editors
m_settings->registerSetting("JsonEditor", QString()); m_settings->registerSetting("JsonEditor", QString());

View File

@ -362,13 +362,17 @@ set(MINECRAFT_SOURCES
minecraft/AssetsUtils.h minecraft/AssetsUtils.h
minecraft/AssetsUtils.cpp minecraft/AssetsUtils.cpp
# Minecraft services # Minecraft skins
minecraft/services/CapeChange.cpp minecraft/skins/CapeChange.cpp
minecraft/services/CapeChange.h minecraft/skins/CapeChange.h
minecraft/services/SkinUpload.cpp minecraft/skins/SkinUpload.cpp
minecraft/services/SkinUpload.h minecraft/skins/SkinUpload.h
minecraft/services/SkinDelete.cpp minecraft/skins/SkinDelete.cpp
minecraft/services/SkinDelete.h minecraft/skins/SkinDelete.h
minecraft/skins/SkinModel.cpp
minecraft/skins/SkinModel.h
minecraft/skins/SkinList.cpp
minecraft/skins/SkinList.h
minecraft/Agent.h) minecraft/Agent.h)
@ -787,8 +791,6 @@ SET(LAUNCHER_SOURCES
ui/InstanceWindow.cpp ui/InstanceWindow.cpp
# FIXME: maybe find a better home for this. # FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
FileIgnoreProxy.cpp FileIgnoreProxy.cpp
FileIgnoreProxy.h FileIgnoreProxy.h
FastFileIconProvider.cpp FastFileIconProvider.cpp
@ -1015,8 +1017,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ReviewMessageBox.h ui/dialogs/ReviewMessageBox.h
ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.cpp
ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.cpp
@ -1030,6 +1030,9 @@ SET(LAUNCHER_SOURCES
ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.cpp
ui/dialogs/InstallLoaderDialog.h ui/dialogs/InstallLoaderDialog.h
ui/dialogs/skins/SkinManageDialog.cpp
ui/dialogs/skins/SkinManageDialog.h
# GUI - widgets # GUI - widgets
ui/widgets/Common.cpp ui/widgets/Common.cpp
ui/widgets/Common.h ui/widgets/Common.h
@ -1159,7 +1162,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/NewComponentDialog.ui ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui ui/dialogs/ExportToModListDialog.ui
@ -1173,6 +1175,8 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ScrollMessageBox.ui ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui ui/dialogs/ChooseProviderDialog.ui
ui/dialogs/skins/SkinManageDialog.ui
) )
qt_wrap_ui(PRISM_UPDATE_UI qt_wrap_ui(PRISM_UPDATE_UI

View File

@ -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 <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
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

View File

@ -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 <QPixmap>
namespace SkinUtils {
QPixmap getFaceFromCache(QString id, int height = 64, int width = 64);
}

View File

@ -347,7 +347,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
Skin skinOut; Skin skinOut;
// fill in default skin info ourselves, as this endpoint doesn't provide it // fill in default skin info ourselves, as this endpoint doesn't provide it
bool steve = isDefaultModelSteve(output.id); bool steve = isDefaultModelSteve(output.id);
skinOut.variant = steve ? "classic" : "slim"; skinOut.variant = steve ? "CLASSIC" : "SLIM";
skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX;
// sadly we can't figure this out, but I don't think it really matters... // sadly we can't figure this out, but I don't think it really matters...
skinOut.id = "00000000-0000-0000-0000-000000000000"; skinOut.id = "00000000-0000-0000-0000-000000000000";

View File

@ -1,121 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "CapeChange.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {}
void CapeChange::setCape([[maybe_unused]] QString& cape)
{
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());
setStatus(tr("Equipping cape"));
m_reply = shared_qobject_ptr<QNetworkReply>(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<QNetworkReply::NetworkError>::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<QNetworkReply>(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<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
}
void CapeChange::executeTask()
{
if (m_capeId.isEmpty()) {
clearCape();
} else {
setCape(m_capeId);
}
}
void CapeChange::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void CapeChange::sslErrors(const QList<QSslError>& errors)
{
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();
}

View File

@ -1,31 +0,0 @@
#pragma once
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <memory>
#include "QObjectPtr.h"
#include "tasks/Task.h"
class CapeChange : public Task {
Q_OBJECT
public:
CapeChange(QObject* parent, QString token, QString capeId);
virtual ~CapeChange() {}
private:
void setCape(QString& cape);
void clearCape();
private:
QString m_capeId;
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:
virtual void executeTask();
public slots:
void downloadError(QNetworkReply::NetworkError);
void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};

View File

@ -1,90 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinDelete.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {}
void SkinDelete::executeTask()
{
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<QNetworkReply>(rep);
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<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinDelete::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors);
connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished);
}
void SkinDelete::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void SkinDelete::sslErrors(const QList<QSslError>& errors)
{
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();
}

View File

@ -1,26 +0,0 @@
#pragma once
#include <QFile>
#include <QtNetwork/QtNetwork>
#include "tasks/Task.h"
using SkinDeletePtr = shared_qobject_ptr<class SkinDelete>;
class SkinDelete : public Task {
Q_OBJECT
public:
SkinDelete(QObject* parent, QString token);
virtual ~SkinDelete() = default;
private:
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:
virtual void executeTask();
public slots:
void downloadError(QNetworkReply::NetworkError);
void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};

View File

@ -1,118 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinUpload.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
QByteArray getVariant(SkinUpload::Model model)
{
switch (model) {
default:
qDebug() << "Unknown skin type!";
case SkinUpload::STEVE:
return "CLASSIC";
case SkinUpload::ALEX:
return "SLIM";
}
}
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()
{
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;
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setBody(m_skin);
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
model.setBody(getVariant(m_model));
multiPart->append(skin);
multiPart->append(model);
QNetworkReply* rep = APPLICATION->network()->post(request, multiPart);
m_reply = shared_qobject_ptr<QNetworkReply>(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<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinUpload::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors);
connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished);
}
void SkinUpload::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void SkinUpload::sslErrors(const QList<QSslError>& errors)
{
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();
}

View File

@ -1,34 +0,0 @@
#pragma once
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <memory>
#include "tasks/Task.h"
using SkinUploadPtr = shared_qobject_ptr<class SkinUpload>;
class SkinUpload : public Task {
Q_OBJECT
public:
enum Model { STEVE, ALEX };
// Note this class takes ownership of the file.
SkinUpload(QObject* parent, QString token, QByteArray skin, Model model = STEVE);
virtual ~SkinUpload() {}
private:
Model m_model;
QByteArray m_skin;
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:
virtual void executeTask();
public slots:
void downloadError(QNetworkReply::NetworkError);
void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
};

View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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 "CapeChange.h"
#include <memory>
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* CapeChange::getReply(QNetworkRequest& request)
{
if (m_capeId.isEmpty()) {
setStatus(tr("Removing cape"));
return m_network->deleteResource(request);
} else {
setStatus(tr("Equipping cape"));
return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8());
}
}
void CapeChange::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
CapeChange::Ptr CapeChange::make(QString token, QString capeId)
{
auto up = makeShared<CapeChange>(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<QByteArray>()));
return up;
}

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "net/NetRequest.h"
class CapeChange : public Net::NetRequest {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<CapeChange>;
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;
};

View File

@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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 "SkinDelete.h"
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* SkinDelete::getReply(QNetworkRequest& request)
{
setStatus(tr("Deleting skin"));
return m_network->deleteResource(request);
}
void SkinDelete::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
SkinDelete::Ptr SkinDelete::make(QString token)
{
auto up = makeShared<SkinDelete>(token);
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active");
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
return up;
}

View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "net/NetRequest.h"
class SkinDelete : public Net::NetRequest {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<SkinDelete>;
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;
};

View File

@ -0,0 +1,389 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SkinList.h"
#include <QFileInfo>
#include <QMimeData>
#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<SkinModel> 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();
}
}
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(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
s.setURL(skin.url);
newSkins << s;
needsSave = true;
}
} else {
nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
}
}
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 skinFiles;
for (auto url : urls) {
// only local files may be dropped...
if (!url.isLocalFile())
continue;
skinFiles << url.toLocalFile();
}
installSkins(skinFiles);
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);
}
QString SkinList::installSkin(const QString& file, const QString& name)
{
if (file.isEmpty())
return tr("Path is empty.");
QFileInfo fileinfo(file);
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 tr("Skin images must be 64x64 or 64x32 pixel PNG files.");
QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
return QFile::copy(file, target) ? "" : tr("Unable to copy file");
}
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);
save();
return true;
}
} else if (QFile::remove(s.getPath())) {
m_skin_list.remove(idx);
save();
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;
}
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());
done = true;
break;
}
}
if (!done) {
beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1);
m_skin_list.append(*s);
endInsertRows();
}
save();
}

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
#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);
QString 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();
void updateSkin(SkinModel* s);
private:
// hide copy constructor
SkinList(const SkinList&) = delete;
// hide assign op
SkinList& operator=(const SkinList&) = delete;
protected slots:
void directoryChanged(const QString& path);
void fileChanged(const QString& path);
bool update();
private:
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
bool is_watching;
QVector<SkinModel> m_skin_list;
QDir m_dir;
MinecraftAccountPtr m_acct;
};

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SkinModel.h"
#include <QFileInfo>
#include <QImage>
#include <QPainter>
#include <QTransform>
#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");
if (auto model = Json::ensureString(obj, "model"); model == "SLIM") {
m_model = Model::SLIM;
}
m_path = skinDir.absoluteFilePath(name) + ".png";
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;
}

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QJsonObject>
#include <QPixmap>
class SkinModel {
public:
enum Model { CLASSIC, SLIM };
SkinModel() = default;
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;
private:
QString m_path;
QPixmap m_texture;
QString m_cape_id;
Model m_model;
QString m_url;
};

View File

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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 "SkinUpload.h"
#include <QHttpMultiPart>
#include "FileSystem.h"
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetRequest(), m_token(token), m_path(path), m_variant(variant)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* SkinUpload::getReply(QNetworkRequest& request)
{
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this);
QHttpPart skin;
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setBody(FS::read(m_path));
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
model.setBody(m_variant.toUtf8());
multiPart->append(skin);
multiPart->append(model);
setStatus(tr("Uploading skin"));
return m_network->post(request, multiPart);
}
void SkinUpload::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant)
{
auto up = makeShared<SkinUpload>(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<QByteArray>()));
return up;
}

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "net/NetRequest.h"
class SkinUpload : public Net::NetRequest {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<SkinUpload>;
// Note this class takes ownership of the file.
SkinUpload(QString token, QString path, QString variant);
virtual ~SkinUpload() = default;
static SkinUpload::Ptr make(QString token, QString path, QString variant);
void init() override;
protected:
virtual QNetworkReply* getReply(QNetworkRequest&) override;
private:
QString m_token;
QString m_path;
QString m_variant;
};

View File

@ -22,5 +22,6 @@
Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net")
Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download")
Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload")
Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins")
Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache")
Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http")

View File

@ -24,5 +24,6 @@
Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskNetLogC)
Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC)
Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC)
Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC)
Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC)
Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC)

View File

@ -43,11 +43,15 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#endif #endif
NetJob::NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : ConcurrentTask(nullptr, job_name), m_network(network) NetJob::NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network, int max_concurrent)
: ConcurrentTask(nullptr, job_name), m_network(network)
{ {
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); if (max_concurrent < 0)
max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt();
#endif #endif
if (max_concurrent > 0)
setMaxConcurrent(max_concurrent);
} }
auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool
@ -144,21 +148,28 @@ void NetJob::updateState()
void NetJob::emitFailed(QString reason) void NetJob::emitFailed(QString reason)
{ {
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", if (m_ask_retry) {
"The tasks failed\n" auto response = CustomMessageBox::selectable(nullptr, "Confirm retry",
"Failed urls\n" + "The tasks failed.\n"
getFailedFiles().join("\n\t") + "Failed urls\n" +
"\n" getFailedFiles().join("\n\t") +
"If this continues to happen please check the logs of the application" ".\n"
"Do you want to retry?", "If this continues to happen please check the logs of the application.\n"
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) "Do you want to retry?",
->exec(); QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response == QMessageBox::Yes) { if (response == QMessageBox::Yes) {
m_try = 0; m_try = 0;
executeNextSubTask(); executeNextSubTask();
return; return;
}
} }
#endif #endif
ConcurrentTask::emitFailed(reason); ConcurrentTask::emitFailed(reason);
} }
void NetJob::setAskRetry(bool askRetry)
{
m_ask_retry = askRetry;
}

View File

@ -52,7 +52,7 @@ class NetJob : public ConcurrentTask {
public: public:
using Ptr = shared_qobject_ptr<NetJob>; using Ptr = shared_qobject_ptr<NetJob>;
explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network); explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network, int max_concurrent = -1);
~NetJob() override = default; ~NetJob() override = default;
auto size() const -> int; auto size() const -> int;
@ -62,6 +62,7 @@ class NetJob : public ConcurrentTask {
auto getFailedActions() -> QList<Net::NetRequest*>; auto getFailedActions() -> QList<Net::NetRequest*>;
auto getFailedFiles() -> QList<QString>; auto getFailedFiles() -> QList<QString>;
void setAskRetry(bool askRetry);
public slots: public slots:
// Qt can't handle auto at the start for some reason? // Qt can't handle auto at the start for some reason?
@ -78,4 +79,5 @@ class NetJob : public ConcurrentTask {
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
int m_try = 1; int m_try = 1;
bool m_ask_retry = true;
}; };

View File

@ -5,6 +5,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by

View File

@ -4,6 +4,7 @@
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -74,6 +75,7 @@ class NetRequest : public Task {
virtual void init() {} virtual void init() {}
QUrl url() const; QUrl url() const;
void setUrl(QUrl url) { m_url = url; }
int replyStatusCode() const; int replyStatusCode() const;
QNetworkReply::NetworkError error() const; QNetworkReply::NetworkError error() const;
QString errorString() const; QString errorString() const;

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by

View File

@ -77,7 +77,6 @@
#include <DesktopServices.h> #include <DesktopServices.h>
#include <InstanceList.h> #include <InstanceList.h>
#include <MMCZip.h> #include <MMCZip.h>
#include <SkinUtils.h>
#include <icons/IconList.h> #include <icons/IconList.h>
#include <java/JavaInstallList.h> #include <java/JavaInstallList.h>
#include <java/JavaUtils.h> #include <java/JavaUtils.h>
@ -1210,6 +1209,11 @@ void MainWindow::on_actionViewCentralModsFolder_triggered()
DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true); DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true);
} }
void MainWindow::on_actionViewSkinsFolder_triggered()
{
DesktopServices::openPath(APPLICATION->settings()->get("SkinsDir").toString(), true);
}
void MainWindow::on_actionViewIconThemeFolder_triggered() void MainWindow::on_actionViewIconThemeFolder_triggered()
{ {
DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true); DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true);

View File

@ -120,6 +120,8 @@ class MainWindow : public QMainWindow {
void on_actionViewIconsFolder_triggered(); void on_actionViewIconsFolder_triggered();
void on_actionViewLogsFolder_triggered(); void on_actionViewLogsFolder_triggered();
void on_actionViewSkinsFolder_triggered();
void on_actionViewSelectedInstFolder_triggered(); void on_actionViewSelectedInstFolder_triggered();
void refreshInstances(); void refreshInstances();

View File

@ -131,7 +131,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>800</width>
<height>20</height> <height>22</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="fileMenu"> <widget class="QMenu" name="fileMenu">
@ -191,6 +191,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionViewInstanceFolder"/> <addaction name="actionViewInstanceFolder"/>
<addaction name="actionViewCentralModsFolder"/> <addaction name="actionViewCentralModsFolder"/>
<addaction name="actionViewSkinsFolder"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionViewIconThemeFolder"/> <addaction name="actionViewIconThemeFolder"/>
<addaction name="actionViewWidgetThemeFolder"/> <addaction name="actionViewWidgetThemeFolder"/>
@ -578,6 +579,18 @@
<string>Open the central mods folder in a file browser.</string> <string>Open the central mods folder in a file browser.</string>
</property> </property>
</action> </action>
<action name="actionViewSkinsFolder">
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>&amp;Skins</string>
</property>
<property name="toolTip">
<string>Open the skins folder in a file browser.</string>
</property>
</action>
<action name="actionViewIconsFolder"> <action name="actionViewIconsFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder">

View File

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

View File

@ -1,164 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QFileDialog>
#include <QFileInfo>
#include <QPainter>
#include <FileSystem.h>
#include <minecraft/services/CapeChange.h>
#include <minecraft/services/SkinUpload.h>
#include <tasks/SequentialTask.h>
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
#include "SkinUploadDialog.h"
#include "ui_SkinUploadDialog.h"
void SkinUploadDialog::on_buttonBox_rejected()
{
close();
}
void SkinUploadDialog::on_buttonBox_accepted()
{
QString fileName;
QString input = ui->skinPathTextBox->text();
ProgressDialog prog(this);
SequentialTask skinUpload;
if (!input.isEmpty()) {
QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
bool isLocalFile = false;
// it has an URL prefix -> it is an URL
if (urlPrefixMatcher.match(input).hasMatch()) {
QUrl fileURL = input;
if (fileURL.isValid()) {
// local?
if (fileURL.isLocalFile()) {
isLocalFile = true;
fileName = fileURL.toLocalFile();
} else {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Using remote URLs for setting skins is not implemented yet."),
QMessageBox::Warning)
->exec();
close();
return;
}
} else {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("You cannot use an invalid URL for uploading skins."),
QMessageBox::Warning)
->exec();
close();
return;
}
} else {
// just assume it's a path then
isLocalFile = true;
fileName = ui->skinPathTextBox->text();
}
if (isLocalFile && !QFile::exists(fileName)) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
close();
return;
}
SkinUpload::Model model = SkinUpload::STEVE;
if (ui->steveBtn->isChecked()) {
model = SkinUpload::STEVE;
} else if (ui->alexBtn->isChecked()) {
model = SkinUpload::ALEX;
}
skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
}
auto selectedCape = ui->capeCombo->currentData().toString();
if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
}
if (prog.execWithTask(&skinUpload) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
close();
return;
}
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec();
close();
}
void SkinUploadDialog::on_skinBrowseBtn_clicked()
{
auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) {
return;
}
QString cooked_path = FS::NormalizePath(raw_path);
ui->skinPathTextBox->setText(cooked_path);
}
SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
{
ui->setupUi(this);
// FIXME: add a model for this, download/refresh the capes on demand
auto& accountData = *acct->accountData();
int index = 0;
ui->capeCombo->addItem(tr("No Cape"), QVariant());
auto currentCape = accountData.minecraftProfile.currentCape;
if (currentCape.isEmpty()) {
ui->capeCombo->setCurrentIndex(index);
}
for (auto& cape : accountData.minecraftProfile.capes) {
index++;
if (cape.data.size()) {
QPixmap capeImage;
if (capeImage.loadFromData(cape.data, "PNG")) {
QPixmap preview = QPixmap(10, 16);
QPainter painter(&preview);
painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
if (currentCape == cape.id) {
ui->capeCombo->setCurrentIndex(index);
}
continue;
}
}
ui->capeCombo->addItem(cape.alias, cape.id);
if (currentCape == cape.id) {
ui->capeCombo->setCurrentIndex(index);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QItemSelection>
#include <QPixmap>
#include "minecraft/auth/MinecraftAccount.h"
#include "minecraft/skins/SkinList.h"
namespace Ui {
class SkinManageDialog;
}
class SkinManageDialog : public QDialog {
Q_OBJECT
public:
explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct);
virtual ~SkinManageDialog();
public slots:
void selectionChanged(QItemSelection, QItemSelection);
void activated(QModelIndex);
void delayed_scroll(QModelIndex);
void on_openDirBtn_clicked();
void on_fileBtn_clicked();
void on_urlBtn_clicked();
void on_userBtn_clicked();
void accept() override;
void on_capeCombo_currentIndexChanged(int index);
void on_steveBtn_toggled(bool checked);
void on_resetBtn_clicked();
void show_context_menu(const QPoint& pos);
bool eventFilter(QObject* obj, QEvent* ev) override;
void on_action_Rename_Skin_triggered(bool checked);
void on_action_Delete_Skin_triggered(bool checked);
private:
void setupCapes();
MinecraftAccountPtr m_acct;
Ui::SkinManageDialog* ui;
SkinList m_list;
QString m_selected_skin;
QHash<QString, QPixmap> m_capes;
QHash<QString, int> m_capes_idx;
};

View File

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

View File

@ -35,7 +35,7 @@
*/ */
#include "AccountListPage.h" #include "AccountListPage.h"
#include "minecraft/auth/AccountData.h" #include "ui/dialogs/skins/SkinManageDialog.h"
#include "ui_AccountListPage.h" #include "ui_AccountListPage.h"
#include <QItemSelectionModel> #include <QItemSelectionModel>
@ -47,11 +47,6 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/OfflineLoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/SkinUploadDialog.h"
#include "minecraft/services/SkinDelete.h"
#include "tasks/Task.h"
#include "Application.h" #include "Application.h"
@ -233,8 +228,7 @@ void AccountListPage::updateButtonStates()
} }
ui->actionRemove->setEnabled(accountIsReady); ui->actionRemove->setEnabled(accountIsReady);
ui->actionSetDefault->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady);
ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); ui->actionManageSkins->setEnabled(accountIsReady && accountIsOnline);
ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline);
ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline);
if (m_accounts->defaultAccount().get() == nullptr) { if (m_accounts->defaultAccount().get() == nullptr) {
@ -247,29 +241,13 @@ void AccountListPage::updateButtonStates()
ui->listView->resizeColumnToContents(3); ui->listView->resizeColumnToContents(3);
} }
void AccountListPage::on_actionUploadSkin_triggered() void AccountListPage::on_actionManageSkins_triggered()
{ {
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.size() > 0) { if (selection.size() > 0) {
QModelIndex selected = selection.first(); QModelIndex selected = selection.first();
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
SkinUploadDialog dialog(account, this); SkinManageDialog dialog(this, account);
dialog.exec(); 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<MinecraftAccountPtr>();
ProgressDialog prog(this);
auto deleteSkinTask = std::make_shared<SkinDelete>(this, 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;
}
}

View File

@ -76,8 +76,7 @@ class AccountListPage : public QMainWindow, public BasePage {
void on_actionRefresh_triggered(); void on_actionRefresh_triggered();
void on_actionSetDefault_triggered(); void on_actionSetDefault_triggered();
void on_actionNoDefault_triggered(); void on_actionNoDefault_triggered();
void on_actionUploadSkin_triggered(); void on_actionManageSkins_triggered();
void on_actionDeleteSkin_triggered();
void listChanged(); void listChanged();

View File

@ -59,14 +59,8 @@
<addaction name="actionSetDefault"/> <addaction name="actionSetDefault"/>
<addaction name="actionNoDefault"/> <addaction name="actionNoDefault"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionUploadSkin"/> <addaction name="actionManageSkins"/>
<addaction name="actionDeleteSkin"/>
</widget> </widget>
<action name="actionRemove">
<property name="text">
<string>Remo&amp;ve</string>
</property>
</action>
<action name="actionSetDefault"> <action name="actionSetDefault">
<property name="text"> <property name="text">
<string>&amp;Set Default</string> <string>&amp;Set Default</string>
@ -80,17 +74,12 @@
<string>&amp;No Default</string> <string>&amp;No Default</string>
</property> </property>
</action> </action>
<action name="actionUploadSkin"> <action name="actionManageSkins">
<property name="text"> <property name="text">
<string>&amp;Upload Skin</string> <string>&amp;Manage Skins</string>
</property>
</action>
<action name="actionDeleteSkin">
<property name="text">
<string>&amp;Delete Skin</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Delete the currently active skin and go back to the default one</string> <string>Manage Skins</string>
</property> </property>
</action> </action>
<action name="actionAddMicrosoft"> <action name="actionAddMicrosoft">
@ -111,6 +100,11 @@
<string>Refresh the account tokens</string> <string>Refresh the account tokens</string>
</property> </property>
</action> </action>
<action name="actionRemove">
<property name="text">
<string>Remo&amp;ve</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -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() void LauncherPage::on_metadataDisableBtn_clicked()
{ {
ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
@ -208,6 +219,7 @@ void LauncherPage::applySettings()
s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("CentralModsDir", ui->modsDirTextBox->text());
s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text());
s->set("DownloadsDir", ui->downloadsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text());
s->set("SkinsDir", ui->skinsDirTextBox->text());
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
@ -269,6 +281,7 @@ void LauncherPage::loadSettings()
ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->modsDirTextBox->setText(s->get("CentralModsDir").toString());
ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString());
ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString());
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
QString sortMode = s->get("InstSortMode").toString(); QString sortMode = s->get("InstSortMode").toString();

View File

@ -74,6 +74,7 @@ class LauncherPage : public QWidget, public BasePage {
void on_modsDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked();
void on_iconsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked();
void on_downloadsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked();
void on_skinsDirBrowseBtn_clicked();
void on_metadataDisableBtn_clicked(); void on_metadataDisableBtn_clicked();
/*! /*!

View File

@ -67,7 +67,7 @@
<string>Folders</string> <string>Folders</string>
</property> </property>
<layout class="QGridLayout" name="foldersBoxLayout"> <layout class="QGridLayout" name="foldersBoxLayout">
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="labelDownloadsDir"> <widget class="QLabel" name="labelDownloadsDir">
<property name="text"> <property name="text">
<string>&amp;Downloads:</string> <string>&amp;Downloads:</string>
@ -90,13 +90,16 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/> <widget class="QLineEdit" name="instDirTextBox"/>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/> <widget class="QLineEdit" name="downloadsDirTextBox"/>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/> <widget class="QLineEdit" name="iconsDirTextBox"/>
</item> </item>
<item row="3" column="2"> <item row="3" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="4" column="2">
<widget class="QToolButton" name="downloadsDirBrowseBtn"> <widget class="QToolButton" name="downloadsDirBrowseBtn">
<property name="text"> <property name="text">
<string>Browse</string> <string>Browse</string>
@ -147,7 +150,24 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" colspan="2"> <item row="3" column="2">
<widget class="QToolButton" name="skinsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelSkinsDir">
<property name="text">
<string>&amp;Skins:</string>
</property>
<property name="buddy">
<cstring>skinsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox"> <widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>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).</string> <string>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).</string>