mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-06-12 05:07:46 +02:00
Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into filters
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
@ -20,7 +20,6 @@
|
||||
#include <QItemSelectionModel>
|
||||
|
||||
#include "Application.h"
|
||||
#include "SkinUtils.h"
|
||||
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
@ -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>
|
500
launcher/ui/dialogs/skins/SkinManageDialog.cpp
Normal file
500
launcher/ui/dialogs/skins/SkinManageDialog.cpp
Normal 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);
|
||||
}
|
64
launcher/ui/dialogs/skins/SkinManageDialog.h
Normal file
64
launcher/ui/dialogs/skins/SkinManageDialog.h
Normal 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;
|
||||
};
|
220
launcher/ui/dialogs/skins/SkinManageDialog.ui
Normal file
220
launcher/ui/dialogs/skins/SkinManageDialog.ui
Normal 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>&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>&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>
|
Reference in New Issue
Block a user