Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into feature/java-downloader
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
commit
11ae169087
@ -590,6 +590,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");
|
||||||
m_settings->registerSetting("JavaDir", "java");
|
m_settings->registerSetting("JavaDir", "java");
|
||||||
|
|
||||||
// Editors
|
// Editors
|
||||||
|
@ -366,13 +366,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)
|
||||||
|
|
||||||
@ -803,8 +807,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
|
||||||
@ -1031,8 +1033,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
|
||||||
@ -1046,6 +1046,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
|
||||||
@ -1175,7 +1178,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
|
||||||
@ -1189,6 +1191,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
|
||||||
|
@ -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
|
|
@ -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);
|
|
||||||
}
|
|
@ -206,8 +206,8 @@ int64_t calculateWorldSize(const QFileInfo& file)
|
|||||||
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
|
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
|
||||||
int64_t total = 0;
|
int64_t total = 0;
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
total += it.fileInfo().size();
|
|
||||||
it.next();
|
it.next();
|
||||||
|
total += it.fileInfo().size();
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -65,29 +65,24 @@ std::pair<Version, Version> DataPack::compatibleVersions() const
|
|||||||
return s_pack_format_versions.constFind(m_pack_format).value();
|
return s_pack_format_versions.constFind(m_pack_format).value();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) const
|
int DataPack::compare(const Resource& other, SortType type) const
|
||||||
{
|
{
|
||||||
auto const& cast_other = static_cast<DataPack const&>(other);
|
auto const& cast_other = static_cast<DataPack const&>(other);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default: {
|
default:
|
||||||
auto res = Resource::compare(other, type);
|
return Resource::compare(other, type);
|
||||||
if (res.first != 0)
|
|
||||||
return res;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SortType::PACK_FORMAT: {
|
case SortType::PACK_FORMAT: {
|
||||||
auto this_ver = packFormat();
|
auto this_ver = packFormat();
|
||||||
auto other_ver = cast_other.packFormat();
|
auto other_ver = cast_other.packFormat();
|
||||||
|
|
||||||
if (this_ver > other_ver)
|
if (this_ver > other_ver)
|
||||||
return { 1, type == SortType::PACK_FORMAT };
|
return 1;
|
||||||
if (this_ver < other_ver)
|
if (this_ver < other_ver)
|
||||||
return { -1, type == SortType::PACK_FORMAT };
|
return -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { 0, false };
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DataPack::applyFilter(QRegularExpression filter) const
|
bool DataPack::applyFilter(QRegularExpression filter) const
|
||||||
|
@ -56,7 +56,7 @@ class DataPack : public Resource {
|
|||||||
|
|
||||||
bool valid() const override;
|
bool valid() const override;
|
||||||
|
|
||||||
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
|
[[nodiscard]] int compare(Resource const& other, SortType type) const override;
|
||||||
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
#include "MetadataHandler.h"
|
#include "MetadataHandler.h"
|
||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
#include "minecraft/mod/ModDetails.h"
|
#include "minecraft/mod/ModDetails.h"
|
||||||
|
#include "minecraft/mod/Resource.h"
|
||||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||||
|
|
||||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||||
@ -77,7 +78,7 @@ void Mod::setDetails(const ModDetails& details)
|
|||||||
m_local_details = details;
|
m_local_details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
|
int Mod::compare(const Resource& other, SortType type) const
|
||||||
{
|
{
|
||||||
auto cast_other = dynamic_cast<Mod const*>(&other);
|
auto cast_other = dynamic_cast<Mod const*>(&other);
|
||||||
if (!cast_other)
|
if (!cast_other)
|
||||||
@ -87,30 +88,23 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
|
|||||||
default:
|
default:
|
||||||
case SortType::ENABLED:
|
case SortType::ENABLED:
|
||||||
case SortType::NAME:
|
case SortType::NAME:
|
||||||
case SortType::DATE: {
|
case SortType::DATE:
|
||||||
auto res = Resource::compare(other, type);
|
case SortType::SIZE:
|
||||||
if (res.first != 0)
|
return Resource::compare(other, type);
|
||||||
return res;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SortType::VERSION: {
|
case SortType::VERSION: {
|
||||||
auto this_ver = Version(version());
|
auto this_ver = Version(version());
|
||||||
auto other_ver = Version(cast_other->version());
|
auto other_ver = Version(cast_other->version());
|
||||||
if (this_ver > other_ver)
|
if (this_ver > other_ver)
|
||||||
return { 1, type == SortType::VERSION };
|
return 1;
|
||||||
if (this_ver < other_ver)
|
if (this_ver < other_ver)
|
||||||
return { -1, type == SortType::VERSION };
|
return -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SortType::PROVIDER: {
|
case SortType::PROVIDER: {
|
||||||
auto compare_result =
|
return QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
|
||||||
QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
|
|
||||||
if (compare_result != 0)
|
|
||||||
return { compare_result, type == SortType::PROVIDER };
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { 0, false };
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Mod::applyFilter(QRegularExpression filter) const
|
bool Mod::applyFilter(QRegularExpression filter) const
|
||||||
|
@ -88,7 +88,7 @@ class Mod : public Resource {
|
|||||||
|
|
||||||
bool valid() const override;
|
bool valid() const override;
|
||||||
|
|
||||||
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
|
[[nodiscard]] int compare(Resource const& other, SortType type) const override;
|
||||||
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||||
|
|
||||||
// Delete all the files of this mod
|
// Delete all the files of this mod
|
||||||
|
@ -52,6 +52,8 @@
|
|||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
|
#include "minecraft/mod/Resource.h"
|
||||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||||
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
||||||
@ -62,12 +64,14 @@
|
|||||||
ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
|
ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
|
||||||
: ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
|
: ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
|
||||||
{
|
{
|
||||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" });
|
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size" });
|
||||||
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") });
|
m_column_names_translated =
|
||||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
|
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size") });
|
||||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
|
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION,
|
||||||
|
SortType::DATE, SortType::PROVIDER, SortType::SIZE };
|
||||||
|
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
||||||
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
|
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
|
||||||
m_columnsHideable = { false, true, false, true, true, true };
|
m_columnsHideable = { false, true, false, true, true, true, true };
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant ModFolderModel::data(const QModelIndex& index, int role) const
|
QVariant ModFolderModel::data(const QModelIndex& index, int role) const
|
||||||
@ -105,12 +109,14 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
|
|
||||||
return provider.value();
|
return provider.value();
|
||||||
}
|
}
|
||||||
|
case SizeColumn:
|
||||||
|
return m_resources[row]->sizeStr();
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
case Qt::ToolTipRole:
|
case Qt::ToolTipRole:
|
||||||
if (column == NAME_COLUMN) {
|
if (column == NameColumn) {
|
||||||
if (at(row)->isSymLinkUnder(instDirPath())) {
|
if (at(row)->isSymLinkUnder(instDirPath())) {
|
||||||
return m_resources[row]->internal_id() +
|
return m_resources[row]->internal_id() +
|
||||||
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
|
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
|
||||||
@ -124,7 +130,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
}
|
}
|
||||||
return m_resources[row]->internal_id();
|
return m_resources[row]->internal_id();
|
||||||
case Qt::DecorationRole: {
|
case Qt::DecorationRole: {
|
||||||
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
|
if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
|
||||||
return APPLICATION->getThemedIcon("status-yellow");
|
return APPLICATION->getThemedIcon("status-yellow");
|
||||||
if (column == ImageColumn) {
|
if (column == ImageColumn) {
|
||||||
return at(row)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
|
return at(row)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
|
||||||
@ -159,6 +165,7 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
|
|||||||
case DateColumn:
|
case DateColumn:
|
||||||
case ProviderColumn:
|
case ProviderColumn:
|
||||||
case ImageColumn:
|
case ImageColumn:
|
||||||
|
case SizeColumn:
|
||||||
return columnNames().at(section);
|
return columnNames().at(section);
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
@ -176,6 +183,8 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
|
|||||||
return tr("The date and time this mod was last changed (or added).");
|
return tr("The date and time this mod was last changed (or added).");
|
||||||
case ProviderColumn:
|
case ProviderColumn:
|
||||||
return tr("Where the mod was downloaded from.");
|
return tr("Where the mod was downloaded from.");
|
||||||
|
case SizeColumn:
|
||||||
|
return tr("The size of the mod.");
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ class QFileSystemWatcher;
|
|||||||
class ModFolderModel : public ResourceFolderModel {
|
class ModFolderModel : public ResourceFolderModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, NUM_COLUMNS };
|
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS };
|
||||||
enum ModStatusAction { Disable, Enable, Toggle };
|
enum ModStatusAction { Disable, Enable, Toggle };
|
||||||
ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);
|
ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
#include "Resource.h"
|
#include "Resource.h"
|
||||||
|
|
||||||
|
#include <QDirIterator>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
Resource::Resource(QObject* parent) : QObject(parent) {}
|
Resource::Resource(QObject* parent) : QObject(parent) {}
|
||||||
|
|
||||||
@ -18,6 +21,20 @@ void Resource::setFile(QFileInfo file_info)
|
|||||||
parseFile();
|
parseFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::tuple<QString, qint64> calculateFileSize(const QFileInfo& file)
|
||||||
|
{
|
||||||
|
if (file.isDir()) {
|
||||||
|
auto dir = QDir(file.absoluteFilePath());
|
||||||
|
dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
|
||||||
|
auto count = dir.count();
|
||||||
|
auto str = QObject::tr("item");
|
||||||
|
if (count != 1)
|
||||||
|
str = QObject::tr("items");
|
||||||
|
return { QString("%1 %2").arg(QString::number(count), str), count };
|
||||||
|
}
|
||||||
|
return { StringUtils::humanReadableFileSize(file.size(), true), file.size() };
|
||||||
|
}
|
||||||
|
|
||||||
void Resource::parseFile()
|
void Resource::parseFile()
|
||||||
{
|
{
|
||||||
QString file_name{ m_file_info.fileName() };
|
QString file_name{ m_file_info.fileName() };
|
||||||
@ -26,6 +43,7 @@ void Resource::parseFile()
|
|||||||
|
|
||||||
m_internal_id = file_name;
|
m_internal_id = file_name;
|
||||||
|
|
||||||
|
std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info);
|
||||||
if (m_file_info.isDir()) {
|
if (m_file_info.isDir()) {
|
||||||
m_type = ResourceType::FOLDER;
|
m_type = ResourceType::FOLDER;
|
||||||
m_name = file_name;
|
m_name = file_name;
|
||||||
@ -61,15 +79,15 @@ static void removeThePrefix(QString& string)
|
|||||||
string = string.trimmed();
|
string = string.trimmed();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
|
int Resource::compare(const Resource& other, SortType type) const
|
||||||
{
|
{
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case SortType::ENABLED:
|
case SortType::ENABLED:
|
||||||
if (enabled() && !other.enabled())
|
if (enabled() && !other.enabled())
|
||||||
return { 1, type == SortType::ENABLED };
|
return 1;
|
||||||
if (!enabled() && other.enabled())
|
if (!enabled() && other.enabled())
|
||||||
return { -1, type == SortType::ENABLED };
|
return -1;
|
||||||
break;
|
break;
|
||||||
case SortType::NAME: {
|
case SortType::NAME: {
|
||||||
QString this_name{ name() };
|
QString this_name{ name() };
|
||||||
@ -78,20 +96,31 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
|
|||||||
removeThePrefix(this_name);
|
removeThePrefix(this_name);
|
||||||
removeThePrefix(other_name);
|
removeThePrefix(other_name);
|
||||||
|
|
||||||
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
|
return QString::compare(this_name, other_name, Qt::CaseInsensitive);
|
||||||
if (compare_result != 0)
|
|
||||||
return { compare_result, type == SortType::NAME };
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case SortType::DATE:
|
case SortType::DATE:
|
||||||
if (dateTimeChanged() > other.dateTimeChanged())
|
if (dateTimeChanged() > other.dateTimeChanged())
|
||||||
return { 1, type == SortType::DATE };
|
return 1;
|
||||||
if (dateTimeChanged() < other.dateTimeChanged())
|
if (dateTimeChanged() < other.dateTimeChanged())
|
||||||
return { -1, type == SortType::DATE };
|
return -1;
|
||||||
break;
|
break;
|
||||||
|
case SortType::SIZE: {
|
||||||
|
if (this->type() != other.type()) {
|
||||||
|
if (this->type() == ResourceType::FOLDER)
|
||||||
|
return -1;
|
||||||
|
if (other.type() == ResourceType::FOLDER)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeInfo() > other.sizeInfo())
|
||||||
|
return 1;
|
||||||
|
if (sizeInfo() < other.sizeInfo())
|
||||||
|
return -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { 0, false };
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Resource::applyFilter(QRegularExpression filter) const
|
bool Resource::applyFilter(QRegularExpression filter) const
|
||||||
|
@ -15,7 +15,7 @@ enum class ResourceType {
|
|||||||
LITEMOD, //!< The resource is a litemod
|
LITEMOD, //!< The resource is a litemod
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER };
|
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE };
|
||||||
|
|
||||||
enum class EnableAction { ENABLE, DISABLE, TOGGLE };
|
enum class EnableAction { ENABLE, DISABLE, TOGGLE };
|
||||||
|
|
||||||
@ -45,6 +45,8 @@ class Resource : public QObject {
|
|||||||
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
|
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
|
||||||
[[nodiscard]] auto type() const -> ResourceType { return m_type; }
|
[[nodiscard]] auto type() const -> ResourceType { return m_type; }
|
||||||
[[nodiscard]] bool enabled() const { return m_enabled; }
|
[[nodiscard]] bool enabled() const { return m_enabled; }
|
||||||
|
[[nodiscard]] QString sizeStr() const { return m_size_str; }
|
||||||
|
[[nodiscard]] qint64 sizeInfo() const { return m_size_info; }
|
||||||
|
|
||||||
[[nodiscard]] virtual auto name() const -> QString { return m_name; }
|
[[nodiscard]] virtual auto name() const -> QString { return m_name; }
|
||||||
[[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
|
[[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
|
||||||
@ -53,10 +55,8 @@ class Resource : public QObject {
|
|||||||
* > 0: 'this' comes after 'other'
|
* > 0: 'this' comes after 'other'
|
||||||
* = 0: 'this' is equal to 'other'
|
* = 0: 'this' is equal to 'other'
|
||||||
* < 0: 'this' comes before 'other'
|
* < 0: 'this' comes before 'other'
|
||||||
*
|
|
||||||
* The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
|
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
|
[[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const;
|
||||||
|
|
||||||
/** Returns whether the given filter should filter out 'this' (false),
|
/** Returns whether the given filter should filter out 'this' (false),
|
||||||
* or if such filter includes the Resource (true).
|
* or if such filter includes the Resource (true).
|
||||||
@ -117,4 +117,6 @@ class Resource : public QObject {
|
|||||||
bool m_is_resolving = false;
|
bool m_is_resolving = false;
|
||||||
bool m_is_resolved = false;
|
bool m_is_resolved = false;
|
||||||
int m_resolution_ticket = 0;
|
int m_resolution_ticket = 0;
|
||||||
|
QString m_size_str;
|
||||||
|
qint64 m_size_info;
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
|
||||||
#include "QVariantUtils.h"
|
#include "QVariantUtils.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||||
|
|
||||||
#include "settings/Setting.h"
|
#include "settings/Setting.h"
|
||||||
@ -416,15 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case NAME_COLUMN:
|
case NameColumn:
|
||||||
return m_resources[row]->name();
|
return m_resources[row]->name();
|
||||||
case DATE_COLUMN:
|
case DateColumn:
|
||||||
return m_resources[row]->dateTimeChanged();
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
case SizeColumn:
|
||||||
|
return m_resources[row]->sizeStr();
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case Qt::ToolTipRole:
|
case Qt::ToolTipRole:
|
||||||
if (column == NAME_COLUMN) {
|
if (column == NameColumn) {
|
||||||
if (at(row).isSymLinkUnder(instDirPath())) {
|
if (at(row).isSymLinkUnder(instDirPath())) {
|
||||||
return m_resources[row]->internal_id() +
|
return m_resources[row]->internal_id() +
|
||||||
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
|
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
|
||||||
@ -440,14 +443,14 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
|
|
||||||
return m_resources[row]->internal_id();
|
return m_resources[row]->internal_id();
|
||||||
case Qt::DecorationRole: {
|
case Qt::DecorationRole: {
|
||||||
if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
|
if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
|
||||||
return APPLICATION->getThemedIcon("status-yellow");
|
return APPLICATION->getThemedIcon("status-yellow");
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case Qt::CheckStateRole:
|
case Qt::CheckStateRole:
|
||||||
switch (column) {
|
switch (column) {
|
||||||
case ACTIVE_COLUMN:
|
case ActiveColumn:
|
||||||
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
@ -486,24 +489,27 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
|
|||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case ACTIVE_COLUMN:
|
case ActiveColumn:
|
||||||
case NAME_COLUMN:
|
case NameColumn:
|
||||||
case DATE_COLUMN:
|
case DateColumn:
|
||||||
|
case SizeColumn:
|
||||||
return columnNames().at(section);
|
return columnNames().at(section);
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case Qt::ToolTipRole: {
|
case Qt::ToolTipRole: {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case ACTIVE_COLUMN:
|
case ActiveColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("Is the resource enabled?");
|
return tr("Is the resource enabled?");
|
||||||
case NAME_COLUMN:
|
case NameColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("The name of the resource.");
|
return tr("The name of the resource.");
|
||||||
case DATE_COLUMN:
|
case DateColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("The date and time this resource was last changed (or added).");
|
return tr("The date and time this resource was last changed (or added).");
|
||||||
|
case SizeColumn:
|
||||||
|
return tr("The size of the resource.");
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -610,12 +616,10 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
|
|||||||
auto const& resource_right = model->at(source_right.row());
|
auto const& resource_right = model->at(source_right.row());
|
||||||
|
|
||||||
auto compare_result = resource_left.compare(resource_right, column_sort_key);
|
auto compare_result = resource_left.compare(resource_right, column_sort_key);
|
||||||
if (compare_result.first == 0)
|
if (compare_result == 0)
|
||||||
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
||||||
|
|
||||||
if (compare_result.second || sortOrder() != Qt::DescendingOrder)
|
return compare_result < 0;
|
||||||
return (compare_result.first < 0);
|
|
||||||
return (compare_result.first > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ResourceFolderModel::instDirPath() const
|
QString ResourceFolderModel::instDirPath() const
|
||||||
|
@ -96,7 +96,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
|||||||
/* Qt behavior */
|
/* Qt behavior */
|
||||||
|
|
||||||
/* Basic columns */
|
/* Basic columns */
|
||||||
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
|
enum Columns { ActiveColumn = 0, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS };
|
||||||
QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }
|
QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }
|
||||||
|
|
||||||
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
|
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
|
||||||
@ -195,11 +195,12 @@ class ResourceFolderModel : public QAbstractListModel {
|
|||||||
protected:
|
protected:
|
||||||
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
|
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
|
||||||
// As such, the order in with they appear is very important!
|
// As such, the order in with they appear is very important!
|
||||||
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
|
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE };
|
||||||
QStringList m_column_names = { "Enable", "Name", "Last Modified" };
|
QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" };
|
||||||
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") };
|
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Size") };
|
||||||
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive };
|
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
||||||
QList<bool> m_columnsHideable = { false, false, true };
|
QHeaderView::Interactive };
|
||||||
|
QList<bool> m_columnsHideable = { false, false, true, true };
|
||||||
|
|
||||||
QDir m_dir;
|
QDir m_dir;
|
||||||
BaseInstance* m_instance;
|
BaseInstance* m_instance;
|
||||||
|
@ -94,29 +94,24 @@ std::pair<Version, Version> ResourcePack::compatibleVersions() const
|
|||||||
return s_pack_format_versions.constFind(m_pack_format).value();
|
return s_pack_format_versions.constFind(m_pack_format).value();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const
|
int ResourcePack::compare(const Resource& other, SortType type) const
|
||||||
{
|
{
|
||||||
auto const& cast_other = static_cast<ResourcePack const&>(other);
|
auto const& cast_other = static_cast<ResourcePack const&>(other);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default: {
|
default:
|
||||||
auto res = Resource::compare(other, type);
|
return Resource::compare(other, type);
|
||||||
if (res.first != 0)
|
|
||||||
return res;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SortType::PACK_FORMAT: {
|
case SortType::PACK_FORMAT: {
|
||||||
auto this_ver = packFormat();
|
auto this_ver = packFormat();
|
||||||
auto other_ver = cast_other.packFormat();
|
auto other_ver = cast_other.packFormat();
|
||||||
|
|
||||||
if (this_ver > other_ver)
|
if (this_ver > other_ver)
|
||||||
return { 1, type == SortType::PACK_FORMAT };
|
return 1;
|
||||||
if (this_ver < other_ver)
|
if (this_ver < other_ver)
|
||||||
return { -1, type == SortType::PACK_FORMAT };
|
return -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { 0, false };
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResourcePack::applyFilter(QRegularExpression filter) const
|
bool ResourcePack::applyFilter(QRegularExpression filter) const
|
||||||
|
@ -44,7 +44,7 @@ class ResourcePack : public Resource {
|
|||||||
|
|
||||||
bool valid() const override;
|
bool valid() const override;
|
||||||
|
|
||||||
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
|
[[nodiscard]] int compare(Resource const& other, SortType type) const override;
|
||||||
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -42,19 +42,21 @@
|
|||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/Resource.h"
|
||||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||||
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
|
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
|
||||||
|
|
||||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
|
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
|
||||||
{
|
{
|
||||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" });
|
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size" });
|
||||||
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
|
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size") });
|
||||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
|
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::SIZE };
|
||||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
|
||||||
QHeaderView::Interactive };
|
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
|
||||||
m_columnsHideable = { false, true, false, true, true };
|
m_columnsHideable = { false, true, false, true, true, true };
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
||||||
@ -85,6 +87,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
}
|
}
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
return m_resources[row]->dateTimeChanged();
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
case SizeColumn:
|
||||||
|
return m_resources[row]->sizeStr();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
@ -144,6 +148,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
|
|||||||
case PackFormatColumn:
|
case PackFormatColumn:
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
case ImageColumn:
|
case ImageColumn:
|
||||||
|
case SizeColumn:
|
||||||
return columnNames().at(section);
|
return columnNames().at(section);
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
@ -160,6 +165,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
|
|||||||
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
|
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
return tr("The date and time this resource pack was last changed (or added).");
|
return tr("The date and time this resource pack was last changed (or added).");
|
||||||
|
case SizeColumn:
|
||||||
|
return tr("The size of the resource pack.");
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
class ResourcePackFolderModel : public ResourceFolderModel {
|
class ResourcePackFolderModel : public ResourceFolderModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS };
|
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, SizeColumn, NUM_COLUMNS };
|
||||||
|
|
||||||
explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance);
|
explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance);
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
|
#include "StringUtils.h"
|
||||||
#include "TexturePackFolderModel.h"
|
#include "TexturePackFolderModel.h"
|
||||||
|
|
||||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||||
@ -44,11 +45,12 @@
|
|||||||
|
|
||||||
TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
|
TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
|
||||||
{
|
{
|
||||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" });
|
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Size" });
|
||||||
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") });
|
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Size") });
|
||||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE };
|
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE };
|
||||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive };
|
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
||||||
m_columnsHideable = { false, true, false, true };
|
QHeaderView::Interactive };
|
||||||
|
m_columnsHideable = { false, true, false, true, true };
|
||||||
}
|
}
|
||||||
|
|
||||||
Task* TexturePackFolderModel::createUpdateTask()
|
Task* TexturePackFolderModel::createUpdateTask()
|
||||||
@ -76,6 +78,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
|
|||||||
return m_resources[row]->name();
|
return m_resources[row]->name();
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
return m_resources[row]->dateTimeChanged();
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
case SizeColumn:
|
||||||
|
return m_resources[row]->sizeStr();
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -127,6 +131,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
|
|||||||
case NameColumn:
|
case NameColumn:
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
case ImageColumn:
|
case ImageColumn:
|
||||||
|
case SizeColumn:
|
||||||
return columnNames().at(section);
|
return columnNames().at(section);
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
@ -135,13 +140,15 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
|
|||||||
switch (section) {
|
switch (section) {
|
||||||
case ActiveColumn:
|
case ActiveColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("Is the resource enabled?");
|
return tr("Is the texture pack enabled?");
|
||||||
case NameColumn:
|
case NameColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("The name of the resource.");
|
return tr("The name of the texture pack.");
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
return tr("The date and time this resource was last changed (or added).");
|
return tr("The date and time this texture pack was last changed (or added).");
|
||||||
|
case SizeColumn:
|
||||||
|
return tr("The size of the texture pack.");
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class TexturePackFolderModel : public ResourceFolderModel {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, NUM_COLUMNS };
|
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS };
|
||||||
|
|
||||||
explicit TexturePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance);
|
explicit TexturePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance);
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
}
|
|
@ -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();
|
|
||||||
};
|
|
@ -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();
|
|
||||||
}
|
|
@ -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();
|
|
||||||
};
|
|
@ -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();
|
|
||||||
}
|
|
@ -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();
|
|
||||||
};
|
|
74
launcher/minecraft/skins/CapeChange.cpp
Normal file
74
launcher/minecraft/skins/CapeChange.cpp
Normal 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;
|
||||||
|
}
|
39
launcher/minecraft/skins/CapeChange.h
Normal file
39
launcher/minecraft/skins/CapeChange.h
Normal 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;
|
||||||
|
};
|
66
launcher/minecraft/skins/SkinDelete.cpp
Normal file
66
launcher/minecraft/skins/SkinDelete.cpp
Normal 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;
|
||||||
|
}
|
38
launcher/minecraft/skins/SkinDelete.h
Normal file
38
launcher/minecraft/skins/SkinDelete.h
Normal 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;
|
||||||
|
};
|
389
launcher/minecraft/skins/SkinList.cpp
Normal file
389
launcher/minecraft/skins/SkinList.cpp
Normal 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();
|
||||||
|
}
|
80
launcher/minecraft/skins/SkinList.h
Normal file
80
launcher/minecraft/skins/SkinList.h
Normal 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;
|
||||||
|
};
|
78
launcher/minecraft/skins/SkinModel.cpp
Normal file
78
launcher/minecraft/skins/SkinModel.cpp
Normal 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;
|
||||||
|
}
|
57
launcher/minecraft/skins/SkinModel.h
Normal file
57
launcher/minecraft/skins/SkinModel.h
Normal 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;
|
||||||
|
};
|
84
launcher/minecraft/skins/SkinUpload.cpp
Normal file
84
launcher/minecraft/skins/SkinUpload.cpp
Normal 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;
|
||||||
|
}
|
42
launcher/minecraft/skins/SkinUpload.h
Normal file
42
launcher/minecraft/skins/SkinUpload.h
Normal 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;
|
||||||
|
};
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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>&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">
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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>
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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&ve</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionSetDefault">
|
<action name="actionSetDefault">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Set Default</string>
|
<string>&Set Default</string>
|
||||||
@ -80,17 +74,12 @@
|
|||||||
<string>&No Default</string>
|
<string>&No Default</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionUploadSkin">
|
<action name="actionManageSkins">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Upload Skin</string>
|
<string>&Manage Skins</string>
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionDeleteSkin">
|
|
||||||
<property name="text">
|
|
||||||
<string>&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&ve</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -183,6 +183,17 @@ void LauncherPage::on_javaDirBrowseBtn_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());
|
||||||
@ -218,6 +229,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("JavaDir", ui->javaDirTextBox->text());
|
s->set("JavaDir", ui->javaDirTextBox->text());
|
||||||
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
|
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
|
||||||
|
|
||||||
@ -280,6 +292,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->javaDirTextBox->setText(s->get("JavaDir").toString());
|
ui->javaDirTextBox->setText(s->get("JavaDir").toString());
|
||||||
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
|
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ class LauncherPage : public QWidget, public BasePage {
|
|||||||
void on_iconsDirBrowseBtn_clicked();
|
void on_iconsDirBrowseBtn_clicked();
|
||||||
void on_downloadsDirBrowseBtn_clicked();
|
void on_downloadsDirBrowseBtn_clicked();
|
||||||
void on_javaDirBrowseBtn_clicked();
|
void on_javaDirBrowseBtn_clicked();
|
||||||
|
void on_skinsDirBrowseBtn_clicked();
|
||||||
void on_metadataDisableBtn_clicked();
|
void on_metadataDisableBtn_clicked();
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>511</width>
|
<width>511</width>
|
||||||
<height>654</height>
|
<height>726</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -67,58 +67,38 @@
|
|||||||
<string>Folders</string>
|
<string>Folders</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="foldersBoxLayout">
|
<layout class="QGridLayout" name="foldersBoxLayout">
|
||||||
<item row="0" column="2">
|
<item row="8" column="0">
|
||||||
<widget class="QToolButton" name="instDirBrowseBtn">
|
<widget class="QLabel" name="labelDownloadsDir">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Browse</string>
|
<string>&Downloads:</string>
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1" colspan="2">
|
|
||||||
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
|
|
||||||
<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>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Check downloads folder recursively</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="labelInstDir">
|
|
||||||
<property name="text">
|
|
||||||
<string>I&nstances:</string>
|
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
<property name="buddy">
|
||||||
<cstring>instDirTextBox</cstring>
|
<cstring>downloadsDirTextBox</cstring>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="8" column="2">
|
||||||
<widget class="QToolButton" name="downloadsDirBrowseBtn">
|
<widget class="QToolButton" name="downloadsDirBrowseBtn">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Browse</string>
|
<string>Browse</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLineEdit" name="instDirTextBox"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLineEdit" name="modsDirTextBox"/>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="2">
|
|
||||||
<widget class="QToolButton" name="iconsDirBrowseBtn">
|
|
||||||
<property name="text">
|
|
||||||
<string>Browse</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</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="4" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QLineEdit" name="downloadsDirTextBox"/>
|
<widget class="QLineEdit" name="javaDirTextBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="labelSkinsDir">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Skins:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>skinsDirTextBox</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="labelIconsDir">
|
<widget class="QLabel" name="labelIconsDir">
|
||||||
@ -130,20 +110,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="9" column="1" colspan="2">
|
||||||
<widget class="QLabel" name="labelDownloadsDir">
|
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
|
||||||
<property name="text">
|
<property name="toolTip">
|
||||||
<string>&Downloads:</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>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
<property name="text">
|
||||||
<cstring>downloadsDirTextBox</cstring>
|
<string>Check downloads folder recursively</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
<item row="8" column="1">
|
||||||
<widget class="QToolButton" name="modsDirBrowseBtn">
|
<widget class="QLineEdit" name="downloadsDirTextBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="labelJavaDir">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Browse</string>
|
<string>&Java:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>javaDirTextBox</cstring>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -157,15 +143,45 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="1">
|
||||||
<widget class="QLabel" name="labelJavaDir">
|
<widget class="QLineEdit" name="skinsDirTextBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="modsDirTextBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLineEdit" name="instDirTextBox"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QToolButton" name="modsDirBrowseBtn">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Java:</string>
|
<string>Browse</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="0" column="2">
|
||||||
<widget class="QLineEdit" name="javaDirTextBox"/>
|
<widget class="QToolButton" name="instDirBrowseBtn">
|
||||||
|
<property name="text">
|
||||||
|
<string>Browse</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QToolButton" name="iconsDirBrowseBtn">
|
||||||
|
<property name="text">
|
||||||
|
<string>Browse</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="labelInstDir">
|
||||||
|
<property name="text">
|
||||||
|
<string>I&nstances:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>instDirTextBox</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="2">
|
<item row="3" column="2">
|
||||||
<widget class="QToolButton" name="javaDirBrowseBtn">
|
<widget class="QToolButton" name="javaDirBrowseBtn">
|
||||||
@ -174,6 +190,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="4" column="2">
|
||||||
|
<widget class="QToolButton" name="skinsDirBrowseBtn">
|
||||||
|
<property name="text">
|
||||||
|
<string>Browse</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#include "ui_ImportFTBPage.h"
|
#include "ui_ImportFTBPage.h"
|
||||||
|
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
#include "ListModel.h"
|
#include "ListModel.h"
|
||||||
@ -58,8 +59,8 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg
|
|||||||
connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
|
connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch);
|
||||||
|
|
||||||
connect(ui->browseButton, &QPushButton::clicked, this, [this] {
|
connect(ui->browseButton, &QPushButton::clicked, this, [this] {
|
||||||
auto path = listModel->getPath();
|
QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(),
|
||||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly);
|
QFileDialog::ShowDirsOnly);
|
||||||
if (!dir.isEmpty())
|
if (!dir.isEmpty())
|
||||||
listModel->setPath(dir);
|
listModel->setPath(dir);
|
||||||
});
|
});
|
||||||
|
@ -24,45 +24,76 @@
|
|||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
#include "Exception.h"
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
#include "Json.h"
|
||||||
#include "StringUtils.h"
|
#include "StringUtils.h"
|
||||||
#include "modplatform/import_ftb/PackHelpers.h"
|
#include "modplatform/import_ftb/PackHelpers.h"
|
||||||
#include "ui/widgets/ProjectItem.h"
|
#include "ui/widgets/ProjectItem.h"
|
||||||
|
|
||||||
namespace FTBImportAPP {
|
namespace FTBImportAPP {
|
||||||
|
|
||||||
QString getStaticPath()
|
QString getFTBRoot()
|
||||||
{
|
{
|
||||||
QString partialPath;
|
QString partialPath = QDir::homePath();
|
||||||
#if defined(Q_OS_OSX)
|
#if defined(Q_OS_OSX)
|
||||||
partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
|
partialPath = FS::PathCombine(partialPath, "Library/Application Support");
|
||||||
#elif defined(Q_OS_WIN32)
|
|
||||||
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
|
|
||||||
#else
|
|
||||||
partialPath = QDir::homePath();
|
|
||||||
#endif
|
#endif
|
||||||
return FS::PathCombine(partialPath, ".ftba");
|
return FS::PathCombine(partialPath, ".ftba");
|
||||||
}
|
}
|
||||||
|
|
||||||
static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances");
|
QString getDynamicPath()
|
||||||
|
{
|
||||||
|
auto settingsPath = FS::PathCombine(getFTBRoot(), "storage", "settings.json");
|
||||||
|
if (!QFileInfo::exists(settingsPath))
|
||||||
|
settingsPath = FS::PathCombine(getFTBRoot(), "bin", "settings.json");
|
||||||
|
if (!QFileInfo::exists(settingsPath)) {
|
||||||
|
qWarning() << "The ftb app setings doesn't exist.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
auto doc = Json::requireDocument(FS::read(settingsPath));
|
||||||
|
return Json::requireString(Json::requireObject(doc), "instanceLocation");
|
||||||
|
} catch (const Exception& e) {
|
||||||
|
qCritical() << "Could not read ftb settings file: " << e.cause();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel::ListModel(QObject* parent) : QAbstractListModel(parent), m_instances_path(getDynamicPath()) {}
|
||||||
|
|
||||||
void ListModel::update()
|
void ListModel::update()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
modpacks.clear();
|
m_modpacks.clear();
|
||||||
|
|
||||||
QString instancesPath = getPath();
|
auto wasPathAdded = [this](QString path) {
|
||||||
if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) {
|
for (auto pack : m_modpacks) {
|
||||||
QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
|
if (pack.path == path)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto scanPath = [this, wasPathAdded](QString path) {
|
||||||
|
if (path.isEmpty())
|
||||||
|
return;
|
||||||
|
if (auto instancesInfo = QFileInfo(path); !instancesInfo.exists() || !instancesInfo.isDir())
|
||||||
|
return;
|
||||||
|
QDirIterator directoryIterator(path, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden,
|
||||||
QDirIterator::FollowSymlinks);
|
QDirIterator::FollowSymlinks);
|
||||||
while (directoryIterator.hasNext()) {
|
while (directoryIterator.hasNext()) {
|
||||||
auto modpack = parseDirectory(directoryIterator.next());
|
auto currentPath = directoryIterator.next();
|
||||||
if (!modpack.path.isEmpty())
|
if (!wasPathAdded(currentPath)) {
|
||||||
modpacks.append(modpack);
|
auto modpack = parseDirectory(currentPath);
|
||||||
|
if (!modpack.path.isEmpty())
|
||||||
|
m_modpacks.append(modpack);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
qDebug() << "Couldn't find ftb instances folder: " << instancesPath;
|
|
||||||
}
|
scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString());
|
||||||
|
scanPath(m_instances_path);
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
@ -70,11 +101,11 @@ void ListModel::update()
|
|||||||
QVariant ListModel::data(const QModelIndex& index, int role) const
|
QVariant ListModel::data(const QModelIndex& index, int role) const
|
||||||
{
|
{
|
||||||
int pos = index.row();
|
int pos = index.row();
|
||||||
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
|
if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) {
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto pack = modpacks.at(pos);
|
auto pack = m_modpacks.at(pos);
|
||||||
if (role == Qt::ToolTipRole) {
|
if (role == Qt::ToolTipRole) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,9 +141,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
|
|||||||
|
|
||||||
FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
|
FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent)
|
||||||
{
|
{
|
||||||
currentSorting = Sorting::ByGameVersion;
|
m_currentSorting = Sorting::ByGameVersion;
|
||||||
sortings.insert(tr("Sort by Name"), Sorting::ByName);
|
m_sortings.insert(tr("Sort by Name"), Sorting::ByName);
|
||||||
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
|
m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
|
bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
|
||||||
@ -120,12 +151,12 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co
|
|||||||
Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
|
Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
|
||||||
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
|
Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
|
||||||
|
|
||||||
if (currentSorting == Sorting::ByGameVersion) {
|
if (m_currentSorting == Sorting::ByGameVersion) {
|
||||||
Version lv(leftPack.mcVersion);
|
Version lv(leftPack.mcVersion);
|
||||||
Version rv(rightPack.mcVersion);
|
Version rv(rightPack.mcVersion);
|
||||||
return lv < rv;
|
return lv < rv;
|
||||||
|
|
||||||
} else if (currentSorting == Sorting::ByName) {
|
} else if (m_currentSorting == Sorting::ByName) {
|
||||||
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
|
return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,39 +167,39 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co
|
|||||||
|
|
||||||
bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
|
bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
|
||||||
{
|
{
|
||||||
if (searchTerm.isEmpty()) {
|
if (m_searchTerm.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>();
|
Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>();
|
||||||
return pack.name.contains(searchTerm, Qt::CaseInsensitive);
|
return pack.name.contains(m_searchTerm, Qt::CaseInsensitive);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilterModel::setSearchTerm(const QString term)
|
void FilterModel::setSearchTerm(const QString term)
|
||||||
{
|
{
|
||||||
searchTerm = term.trimmed();
|
m_searchTerm = term.trimmed();
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
|
const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
|
||||||
{
|
{
|
||||||
return sortings;
|
return m_sortings;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FilterModel::translateCurrentSorting()
|
QString FilterModel::translateCurrentSorting()
|
||||||
{
|
{
|
||||||
return sortings.key(currentSorting);
|
return m_sortings.key(m_currentSorting);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilterModel::setSorting(Sorting s)
|
void FilterModel::setSorting(Sorting s)
|
||||||
{
|
{
|
||||||
currentSorting = s;
|
m_currentSorting = s;
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterModel::Sorting FilterModel::getCurrentSorting()
|
FilterModel::Sorting FilterModel::getCurrentSorting()
|
||||||
{
|
{
|
||||||
return currentSorting;
|
return m_currentSorting;
|
||||||
}
|
}
|
||||||
void ListModel::setPath(QString path)
|
void ListModel::setPath(QString path)
|
||||||
{
|
{
|
||||||
@ -176,11 +207,11 @@ void ListModel::setPath(QString path)
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ListModel::getPath()
|
QString ListModel::getUserPath()
|
||||||
{
|
{
|
||||||
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
|
auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString();
|
||||||
if (path.isEmpty() || !QFileInfo(path).exists())
|
if (path.isEmpty())
|
||||||
path = FTB_APP_PATH;
|
path = m_instances_path;
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
} // namespace FTBImportAPP
|
} // namespace FTBImportAPP
|
@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel {
|
|||||||
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
|
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QMap<QString, Sorting> sortings;
|
QMap<QString, Sorting> m_sortings;
|
||||||
Sorting currentSorting;
|
Sorting m_currentSorting;
|
||||||
QString searchTerm;
|
QString m_searchTerm;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ListModel : public QAbstractListModel {
|
class ListModel : public QAbstractListModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ListModel(QObject* parent) : QAbstractListModel(parent) {}
|
ListModel(QObject* parent);
|
||||||
virtual ~ListModel() = default;
|
virtual ~ListModel() = default;
|
||||||
|
|
||||||
int rowCount(const QModelIndex& parent) const { return modpacks.size(); }
|
int rowCount(const QModelIndex& parent) const { return m_modpacks.size(); }
|
||||||
int columnCount(const QModelIndex& parent) const { return 1; }
|
int columnCount(const QModelIndex& parent) const { return 1; }
|
||||||
QVariant data(const QModelIndex& index, int role) const;
|
QVariant data(const QModelIndex& index, int role) const;
|
||||||
|
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
QString getPath();
|
QString getUserPath();
|
||||||
void setPath(QString path);
|
void setPath(QString path);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ModpackList modpacks;
|
ModpackList m_modpacks;
|
||||||
|
const QString m_instances_path;
|
||||||
};
|
};
|
||||||
} // namespace FTBImportAPP
|
} // namespace FTBImportAPP
|
@ -1,7 +1,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Name=Prism Launcher
|
Name=Prism Launcher
|
||||||
Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
|
Comment=Discover, manage, and play Minecraft instances
|
||||||
Type=Application
|
Type=Application
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Exec=@Launcher_APP_BINARY_NAME@ %U
|
Exec=@Launcher_APP_BINARY_NAME@ %U
|
||||||
|
Loading…
Reference in New Issue
Block a user