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:
Trial97 2024-06-10 09:34:48 +03:00
commit 11ae169087
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
65 changed files with 2129 additions and 1054 deletions

View File

@ -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

View File

@ -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

View File

@ -1,52 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinUtils.h"
#include "Application.h"
#include "net/HttpMetaCache.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
namespace SkinUtils {
/*
* Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise
*/
QPixmap getFaceFromCache(QString username, int height, int width)
{
QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath());
if (fskin.exists()) {
QPixmap skinTexture(fskin.fileName());
if (!skinTexture.isNull()) {
QPixmap skin = QPixmap(8, 8);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
skin.fill(QColorConstants::Transparent);
#else
skin.fill(QColor(0, 0, 0, 0));
#endif
QPainter painter(&skin);
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
return skin.scaled(height, width, Qt::KeepAspectRatio);
}
}
return QPixmap();
}
} // namespace SkinUtils

View File

@ -1,22 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QPixmap>
namespace SkinUtils {
QPixmap getFaceFromCache(QString id, int height = 64, int width = 64);
}

View File

@ -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;
} }

View File

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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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();
} }

View File

@ -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);

View File

@ -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;
} }
return { 0, false }; if (sizeInfo() > other.sizeInfo())
return 1;
if (sizeInfo() < other.sizeInfo())
return -1;
break;
}
}
return 0;
} }
bool Resource::applyFilter(QRegularExpression filter) const bool Resource::applyFilter(QRegularExpression filter) const

View File

@ -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;
}; };

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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:

View File

@ -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 {};
} }

View File

@ -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);

View File

@ -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 {};
} }

View File

@ -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);

View File

@ -1,121 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "CapeChange.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {}
void CapeChange::setCape([[maybe_unused]] QString& cape)
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply* rep = APPLICATION->network()->put(request, requestString.toUtf8());
setStatus(tr("Equipping cape"));
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
#else
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
}
void CapeChange::clearCape()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
setStatus(tr("Removing cape"));
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
#else
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
}
void CapeChange::executeTask()
{
if (m_capeId.isEmpty()) {
clearCape();
} else {
setCape(m_capeId);
}
}
void CapeChange::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void CapeChange::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void CapeChange::downloadFinished()
{
// if the download failed
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
m_reply.reset();
return;
}
emitSucceeded();
}

View File

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

View File

@ -1,90 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinDelete.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {}
void SkinDelete::executeTask()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
setStatus(tr("Deleting skin"));
connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError);
#else
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinDelete::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors);
connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished);
}
void SkinDelete::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void SkinDelete::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void SkinDelete::downloadFinished()
{
// if the download failed
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
m_reply.reset();
return;
}
emitSucceeded();
}

View File

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

View File

@ -1,118 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinUpload.h"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include "Application.h"
QByteArray getVariant(SkinUpload::Model model)
{
switch (model) {
default:
qDebug() << "Unknown skin type!";
case SkinUpload::STEVE:
return "CLASSIC";
case SkinUpload::ALEX:
return "SLIM";
}
}
SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin, SkinUpload::Model model)
: Task(parent), m_model(model), m_skin(skin), m_token(token)
{}
void SkinUpload::executeTask()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart skin;
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setBody(m_skin);
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
model.setBody(getVariant(m_model));
multiPart->append(skin);
multiPart->append(model);
QNetworkReply* rep = APPLICATION->network()->post(request, multiPart);
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
setStatus(tr("Uploading skin"));
connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError);
#else
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinUpload::downloadError);
#endif
connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors);
connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished);
}
void SkinUpload::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void SkinUpload::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void SkinUpload::downloadFinished()
{
// if the download failed
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
m_reply.reset();
return;
}
emitSucceeded();
}

View File

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

View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "CapeChange.h"
#include <memory>
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* CapeChange::getReply(QNetworkRequest& request)
{
if (m_capeId.isEmpty()) {
setStatus(tr("Removing cape"));
return m_network->deleteResource(request);
} else {
setStatus(tr("Equipping cape"));
return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8());
}
}
void CapeChange::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
CapeChange::Ptr CapeChange::make(QString token, QString capeId)
{
auto up = makeShared<CapeChange>(token, capeId);
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active");
up->setObjectName(QString("BYTES:") + up->m_url.toString());
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
return up;
}

View File

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

View File

@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinDelete.h"
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* SkinDelete::getReply(QNetworkRequest& request)
{
setStatus(tr("Deleting skin"));
return m_network->deleteResource(request);
}
void SkinDelete::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
SkinDelete::Ptr SkinDelete::make(QString token)
{
auto up = makeShared<SkinDelete>(token);
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active");
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
return up;
}

View File

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

View File

@ -0,0 +1,389 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SkinList.h"
#include <QFileInfo>
#include <QMimeData>
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/skins/SkinModel.h"
SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher.reset(new QFileSystemWatcher(this));
is_watching = false;
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged);
directoryChanged(path);
}
void SkinList::startWatching()
{
if (is_watching) {
return;
}
update();
is_watching = m_watcher->addPath(m_dir.absolutePath());
if (is_watching) {
qDebug() << "Started watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
}
}
void SkinList::stopWatching()
{
save();
if (!is_watching) {
return;
}
is_watching = !m_watcher->removePath(m_dir.absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
}
}
bool SkinList::update()
{
QVector<SkinModel> newSkins;
m_dir.refresh();
auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json"));
if (manifestInfo.exists()) {
try {
auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file");
const auto root = doc.object();
auto skins = Json::ensureArray(root, "skins");
for (auto jSkin : skins) {
SkinModel s(m_dir, Json::ensureObject(jSkin));
if (s.isValid()) {
newSkins << s;
}
}
} catch (const Exception& e) {
qCritical() << "Couldn't load skins json:" << e.cause();
}
}
bool needsSave = false;
const auto& skin = m_acct->accountData()->minecraftProfile.skin;
if (!skin.url.isEmpty() && !skin.data.isEmpty()) {
QPixmap skinTexture;
SkinModel* nskin = nullptr;
for (auto i = 0; i < newSkins.size(); i++) {
if (newSkins[i].getURL() == skin.url) {
nskin = &newSkins[i];
break;
}
}
if (!nskin) {
auto name = m_acct->profileName() + ".png";
if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) {
name = QUrl(skin.url).fileName() + ".png";
}
auto path = m_dir.absoluteFilePath(name);
if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) {
SkinModel s(path);
s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
s.setURL(skin.url);
newSkins << s;
needsSave = true;
}
} else {
nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
}
}
auto folderContents = m_dir.entryInfoList();
// if there are any untracked files...
for (QFileInfo entry : folderContents) {
if (!entry.isFile() && entry.suffix() != "png")
continue;
SkinModel w(entry.absoluteFilePath());
if (w.isValid()) {
auto add = true;
for (auto s : newSkins) {
if (s.name() == w.name()) {
add = false;
break;
}
}
if (add) {
newSkins.append(w);
needsSave = true;
}
}
}
std::sort(newSkins.begin(), newSkins.end(),
[](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; });
beginResetModel();
m_skin_list.swap(newSkins);
endResetModel();
if (needsSave)
save();
return true;
}
void SkinList::directoryChanged(const QString& path)
{
QDir new_dir(path);
if (!new_dir.exists())
if (!FS::ensureFolderPathExists(new_dir.absolutePath()))
return;
if (m_dir.absolutePath() != new_dir.absolutePath()) {
m_dir.setPath(path);
m_dir.refresh();
if (is_watching)
stopWatching();
startWatching();
}
update();
}
void SkinList::fileChanged(const QString& path)
{
qDebug() << "Checking " << path;
QFileInfo checkfile(path);
if (!checkfile.exists())
return;
for (int i = 0; i < m_skin_list.count(); i++) {
if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) {
m_skin_list[i].refresh();
dataChanged(index(i), index(i));
break;
}
}
}
QStringList SkinList::mimeTypes() const
{
return { "text/uri-list" };
}
Qt::DropActions SkinList::supportedDropActions() const
{
return Qt::CopyAction;
}
bool SkinList::dropMimeData(const QMimeData* data,
Qt::DropAction action,
[[maybe_unused]] int row,
[[maybe_unused]] int column,
[[maybe_unused]] const QModelIndex& parent)
{
if (action == Qt::IgnoreAction)
return true;
// check if the action is supported
if (!data || !(action & supportedDropActions()))
return false;
// files dropped from outside?
if (data->hasUrls()) {
auto urls = data->urls();
QStringList skinFiles;
for (auto url : urls) {
// only local files may be dropped...
if (!url.isLocalFile())
continue;
skinFiles << url.toLocalFile();
}
installSkins(skinFiles);
return true;
}
return false;
}
Qt::ItemFlags SkinList::flags(const QModelIndex& index) const
{
Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index);
if (index.isValid()) {
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}
return f;
}
QVariant SkinList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return QVariant();
int row = index.row();
if (row < 0 || row >= m_skin_list.size())
return QVariant();
auto skin = m_skin_list[row];
switch (role) {
case Qt::DecorationRole:
return skin.getTexture();
case Qt::DisplayRole:
return skin.name();
case Qt::UserRole:
return skin.name();
case Qt::EditRole:
return skin.name();
default:
return QVariant();
}
}
int SkinList::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : m_skin_list.size();
}
void SkinList::installSkins(const QStringList& iconFiles)
{
for (QString file : iconFiles)
installSkin(file);
}
QString SkinList::installSkin(const QString& file, const QString& name)
{
if (file.isEmpty())
return tr("Path is empty.");
QFileInfo fileinfo(file);
if (!fileinfo.exists())
return tr("File doesn't exist.");
if (!fileinfo.isFile())
return tr("Not a file.");
if (!fileinfo.isReadable())
return tr("File is not readable.");
if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid())
return tr("Skin images must be 64x64 or 64x32 pixel PNG files.");
QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
return QFile::copy(file, target) ? "" : tr("Unable to copy file");
}
int SkinList::getSkinIndex(const QString& key) const
{
for (int i = 0; i < m_skin_list.count(); i++) {
if (m_skin_list[i].name() == key) {
return i;
}
}
return -1;
}
const SkinModel* SkinList::skin(const QString& key) const
{
int idx = getSkinIndex(key);
if (idx == -1)
return nullptr;
return &m_skin_list[idx];
}
SkinModel* SkinList::skin(const QString& key)
{
int idx = getSkinIndex(key);
if (idx == -1)
return nullptr;
return &m_skin_list[idx];
}
bool SkinList::deleteSkin(const QString& key, const bool trash)
{
int idx = getSkinIndex(key);
if (idx != -1) {
auto s = m_skin_list[idx];
if (trash) {
if (FS::trash(s.getPath(), nullptr)) {
m_skin_list.remove(idx);
save();
return true;
}
} else if (QFile::remove(s.getPath())) {
m_skin_list.remove(idx);
save();
return true;
}
}
return false;
}
void SkinList::save()
{
QJsonObject doc;
QJsonArray arr;
for (auto s : m_skin_list) {
arr << s.toJSON();
}
doc["skins"] = arr;
Json::write(doc, m_dir.absoluteFilePath("index.json"));
}
int SkinList::getSelectedAccountSkin()
{
const auto& skin = m_acct->accountData()->minecraftProfile.skin;
for (int i = 0; i < m_skin_list.count(); i++) {
if (m_skin_list[i].getURL() == skin.url) {
return i;
}
}
return -1;
}
bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role)
{
if (!idx.isValid() || role != Qt::EditRole) {
return false;
}
int row = idx.row();
if (row < 0 || row >= m_skin_list.size())
return false;
auto& skin = m_skin_list[row];
auto newName = value.toString();
if (skin.name() != newName) {
skin.rename(newName);
save();
}
return true;
}
void SkinList::updateSkin(SkinModel* s)
{
auto done = false;
for (auto i = 0; i < m_skin_list.size(); i++) {
if (m_skin_list[i].getPath() == s->getPath()) {
m_skin_list[i].setCapeId(s->getCapeId());
m_skin_list[i].setModel(s->getModel());
m_skin_list[i].setURL(s->getURL());
done = true;
break;
}
}
if (!done) {
beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1);
m_skin_list.append(*s);
endInsertRows();
}
save();
}

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
#include "QObjectPtr.h"
#include "SkinModel.h"
#include "minecraft/auth/MinecraftAccount.h"
class SkinList : public QAbstractListModel {
Q_OBJECT
public:
explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct);
virtual ~SkinList() { save(); };
int getSkinIndex(const QString& key) const;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex& idx, const QVariant& value, int role) override;
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QStringList mimeTypes() const override;
virtual Qt::DropActions supportedDropActions() const override;
virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
bool deleteSkin(const QString& key, const bool trash);
void installSkins(const QStringList& iconFiles);
QString installSkin(const QString& file, const QString& name = {});
const SkinModel* skin(const QString& key) const;
SkinModel* skin(const QString& key);
void startWatching();
void stopWatching();
QString getDir() const { return m_dir.absolutePath(); }
void save();
int getSelectedAccountSkin();
void updateSkin(SkinModel* s);
private:
// hide copy constructor
SkinList(const SkinList&) = delete;
// hide assign op
SkinList& operator=(const SkinList&) = delete;
protected slots:
void directoryChanged(const QString& path);
void fileChanged(const QString& path);
bool update();
private:
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
bool is_watching;
QVector<SkinModel> m_skin_list;
QDir m_dir;
MinecraftAccountPtr m_acct;
};

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SkinModel.h"
#include <QFileInfo>
#include <QImage>
#include <QPainter>
#include <QTransform>
#include "FileSystem.h"
#include "Json.h"
SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {}
SkinModel::SkinModel(QDir skinDir, QJsonObject obj)
: m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url"))
{
auto name = Json::ensureString(obj, "name");
if (auto model = Json::ensureString(obj, "model"); model == "SLIM") {
m_model = Model::SLIM;
}
m_path = skinDir.absoluteFilePath(name) + ".png";
m_texture = QPixmap(m_path);
}
QString SkinModel::name() const
{
return QFileInfo(m_path).baseName();
}
bool SkinModel::rename(QString newName)
{
auto info = QFileInfo(m_path);
m_path = FS::PathCombine(info.absolutePath(), newName + ".png");
return FS::move(info.absoluteFilePath(), m_path);
}
QJsonObject SkinModel::toJSON() const
{
QJsonObject obj;
obj["name"] = name();
obj["capeId"] = m_cape_id;
obj["url"] = m_url;
obj["model"] = getModelString();
return obj;
}
QString SkinModel::getModelString() const
{
switch (m_model) {
case CLASSIC:
return "CLASSIC";
case SLIM:
return "SLIM";
}
return {};
}
bool SkinModel::isValid() const
{
return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64;
}

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDir>
#include <QJsonObject>
#include <QPixmap>
class SkinModel {
public:
enum Model { CLASSIC, SLIM };
SkinModel() = default;
SkinModel(QString path);
SkinModel(QDir skinDir, QJsonObject obj);
virtual ~SkinModel() = default;
QString name() const;
QString getModelString() const;
bool isValid() const;
QString getPath() const { return m_path; }
QPixmap getTexture() const { return m_texture; }
QString getCapeId() const { return m_cape_id; }
Model getModel() const { return m_model; }
QString getURL() const { return m_url; }
bool rename(QString newName);
void setCapeId(QString capeID) { m_cape_id = capeID; }
void setModel(Model model) { m_model = model; }
void setURL(QString url) { m_url = url; }
void refresh() { m_texture = QPixmap(m_path); }
QJsonObject toJSON() const;
private:
QString m_path;
QPixmap m_texture;
QString m_cape_id;
Model m_model;
QString m_url;
};

View File

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkinUpload.h"
#include <QHttpMultiPart>
#include "FileSystem.h"
#include "net/ByteArraySink.h"
#include "net/StaticHeaderProxy.h"
SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetRequest(), m_token(token), m_path(path), m_variant(variant)
{
logCat = taskMCSkinsLogC;
}
QNetworkReply* SkinUpload::getReply(QNetworkRequest& request)
{
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this);
QHttpPart skin;
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setBody(FS::read(m_path));
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
model.setBody(m_variant.toUtf8());
multiPart->append(skin);
multiPart->append(model);
setStatus(tr("Uploading skin"));
return m_network->post(request, multiPart);
}
void SkinUpload::init()
{
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
}));
}
SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant)
{
auto up = makeShared<SkinUpload>(token, path, variant);
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins");
up->setObjectName(QString("BYTES:") + up->m_url.toString());
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
return up;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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());

View File

@ -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();
/*! /*!

View File

@ -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>&amp;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&amp;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>&amp;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>&amp;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>&amp;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&amp;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>

View File

@ -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);
}); });

View File

@ -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 (!wasPathAdded(currentPath)) {
auto modpack = parseDirectory(currentPath);
if (!modpack.path.isEmpty()) if (!modpack.path.isEmpty())
modpacks.append(modpack); 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

View File

@ -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

View File

@ -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