Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into retry_auth

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-06-10 09:03:04 +03:00
commit 3f0fa54fe8
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
139 changed files with 3168 additions and 1467 deletions

View File

@ -25,7 +25,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@v2.5.0 uses: korthout/backport-action@v3.0.2
with: with:
# Config README: https://github.com/korthout/backport-action#backport-action # Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |- pull_description: |-

View File

@ -611,7 +611,7 @@ jobs:
flatpak: flatpak:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08 image: bilelmoussaoui/flatpak-github-actions:kde-6.7
options: --privileged options: --privileged
steps: steps:
- name: Checkout - name: Checkout

View File

@ -163,7 +163,6 @@ class Config {
QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/";
QString AUTH_BASE = "https://authserver.mojang.com/";
QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists

52
flake.lock generated
View File

@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1714641030, "lastModified": 1717285511,
"narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -36,24 +36,6 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": { "gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -93,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1715413075, "lastModified": 1717774105,
"narHash": "sha256-FCi3R1MeS5bVp0M0xTheveP6hhcCYfW/aghSTPebYL4=", "narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e4e7a43a9db7e22613accfeb1005cca1b2b1ee0d", "rev": "d226935fd75012939397c83f6c385e4d6d832288",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -112,7 +94,6 @@
"flake-compat": [ "flake-compat": [
"flake-compat" "flake-compat"
], ],
"flake-utils": "flake-utils",
"gitignore": "gitignore", "gitignore": "gitignore",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
@ -122,11 +103,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1714478972, "lastModified": 1717664902,
"narHash": "sha256-q//cgb52vv81uOuwz1LaXElp3XAe1TqrABXODAEF6Sk=", "narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "2849da033884f54822af194400f8dff435ada242", "rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -143,21 +124,6 @@
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -1,6 +1,6 @@
id: org.prismlauncher.PrismLauncher id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: 5.15-23.08 runtime-version: 6.7
sdk: org.kde.Sdk sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk21 - org.freedesktop.Sdk.Extension.openjdk21
@ -38,7 +38,6 @@ modules:
config-opts: config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak - -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DLauncher_QT_VERSION_MAJOR=5
build-options: build-options:
env: env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17

View File

@ -395,20 +395,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ {
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
static const QString logBase = FS::PathCombine("logs", baseLogFile); static const QString logBase = FS::PathCombine("logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // if this did not fail if (FS::ensureFolderPathExists("logs")) { // if this did not fail
for (auto i = 0; i <= 4; i++) for (auto i = 0; i <= 4; i++)
if (auto oldName = baseLogFile.arg(i); if (auto oldName = baseLogFile.arg(i);
QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
moveFile(oldName, logBase.arg(i)); FS::move(oldName, logBase.arg(i));
} }
for (auto i = 4; i > 0; i--) for (auto i = 4; i > 0; i--)
moveFile(logBase.arg(i - 1), logBase.arg(i)); FS::move(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0))); logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
@ -593,6 +588,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("DownloadsDirWatchRecursive", false);
m_settings->registerSetting("SkinsDir", "skins");
// Editors // Editors
m_settings->registerSetting("JsonEditor", QString()); m_settings->registerSetting("JsonEditor", QString());
@ -1211,6 +1207,12 @@ void Application::performMainStartupAction()
qDebug() << "<> Updater started."; qDebug() << "<> Updater started.";
} }
{ // delete instances tmp dirctory
auto instDir = m_settings->get("InstanceDir").toString();
const QString tempRoot = FS::PathCombine(instDir, ".tmp");
FS::deletePath(tempRoot);
}
if (!m_urlsToImport.isEmpty()) { if (!m_urlsToImport.isEmpty()) {
qDebug() << "<> Importing from url:" << m_urlsToImport; qDebug() << "<> Importing from url:" << m_urlsToImport;
m_mainWindow->processURLs(m_urlsToImport); m_mainWindow->processURLs(m_urlsToImport);

View File

@ -16,6 +16,7 @@
#include <QFile> #include <QFile>
#include "BaseInstaller.h" #include "BaseInstaller.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
BaseInstaller::BaseInstaller() {} BaseInstaller::BaseInstaller() {}
@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to)
bool BaseInstaller::remove(MinecraftInstance* from) bool BaseInstaller::remove(MinecraftInstance* from)
{ {
return QFile::remove(filename(from->instanceRoot())); return FS::deletePath(filename(from->instanceRoot()));
} }
QString BaseInstaller::filename(const QString& root) const QString BaseInstaller::filename(const QString& root) const

View File

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

View File

@ -647,6 +647,19 @@ void ExternalLinkFileProcess::runLinkFile()
qDebug() << "Process exited"; qDebug() << "Process exited";
} }
bool moveByCopy(const QString& source, const QString& dest)
{
if (!copy(source, dest)()) { // copy
qDebug() << "Copy of" << source << "to" << dest << "failed!";
return false;
}
if (!deletePath(source)) { // remove original
qDebug() << "Deletion of" << source << "failed!";
return false;
};
return true;
}
bool move(const QString& source, const QString& dest) bool move(const QString& source, const QString& dest)
{ {
std::error_code err; std::error_code err;
@ -654,13 +667,14 @@ bool move(const QString& source, const QString& dest)
ensureFilePathExists(dest); ensureFilePathExists(dest);
fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
if (err) { if (err.value() != 0) {
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()); if (moveByCopy(source, dest))
qDebug() << "Source file:" << source; return true;
qDebug() << "Destination file:" << dest; qDebug() << "Move of" << source << "to" << dest << "failed!";
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value());
return false;
} }
return true;
return err.value() == 0;
} }
bool deletePath(QString path) bool deletePath(QString path)

View File

@ -240,6 +240,7 @@ class create_link : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalLinked() { return m_linked; } int totalLinked() { return m_linked; }
int totalToLink() { return static_cast<int>(m_links_to_make.size()); }
void runPrivileged() { runPrivileged(QString()); } void runPrivileged() { runPrivileged(QString()); }
void runPrivileged(const QString& offset); void runPrivileged(const QString& offset);
@ -378,6 +379,7 @@ enum class FilesystemType {
HFSX, HFSX,
FUSEBLK, FUSEBLK,
F2FS, F2FS,
BCACHEFS,
UNKNOWN UNKNOWN
}; };
@ -406,6 +408,7 @@ static const QMap<FilesystemType, QStringList> s_filesystem_type_names = { { Fil
{ FilesystemType::HFSX, { "HFSX" } }, { FilesystemType::HFSX, { "HFSX" } },
{ FilesystemType::FUSEBLK, { "FUSEBLK" } }, { FilesystemType::FUSEBLK, { "FUSEBLK" } },
{ FilesystemType::F2FS, { "F2FS" } }, { FilesystemType::F2FS, { "F2FS" } },
{ FilesystemType::BCACHEFS, { "BCACHEFS" } },
{ FilesystemType::UNKNOWN, { "UNKNOWN" } } }; { FilesystemType::UNKNOWN, { "UNKNOWN" } } };
/** /**
@ -458,7 +461,7 @@ QString nearestExistentAncestor(const QString& path);
FilesystemInfo statFS(const QString& path); FilesystemInfo statFS(const QString& path);
static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
FilesystemType::XFS, FilesystemType::REFS }; FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS };
/** /**
* @brief if the Filesystem is reflink/clone capable * @brief if the Filesystem is reflink/clone capable

View File

@ -1,10 +1,12 @@
#include "InstanceCopyTask.h" #include "InstanceCopyTask.h"
#include <QDebug> #include <QDebug>
#include <QtConcurrentRun> #include <QtConcurrentRun>
#include <memory>
#include "FileSystem.h" #include "FileSystem.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/RegexpMatcher.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "tasks/Task.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{ {
@ -38,7 +40,20 @@ void InstanceCopyTask::executeTask()
{ {
setStatus(tr("Copying instance %1").arg(m_origInstance->name())); setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() { m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get());
folderClone(true);
setProgress(0, folderClone.totalCloned());
connect(&folderClone, &FS::clone::fileCloned,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
return folderClone();
}
if (m_useLinks || m_useHardLinks) {
std::unique_ptr<FS::copy> savesCopy;
if (m_copySaves) {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
@ -48,28 +63,27 @@ void InstanceCopyTask::executeTask()
else else
staging_mc_dir = mcDir.filePath(); staging_mc_dir = mcDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); savesCopy = std::make_unique<FS::copy>(FS::PathCombine(m_origInstance->gameRoot(), "saves"),
savesCopy.followSymlinks(true); FS::PathCombine(staging_mc_dir, "saves"));
savesCopy->followSymlinks(true);
return savesCopy(); (*savesCopy)(true);
}; setProgress(0, savesCopy->totalCopied());
connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] { }
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get());
return folderClone();
} else if (m_useLinks || m_useHardLinks) {
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink());
connect(&folderLink, &FS::create_link::fileLinked,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
bool there_were_errors = false; bool there_were_errors = false;
if (!folderLink()) { if (!folderLink()) {
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
if (!m_useHardLinks) { if (!m_useHardLinks) {
setProgress(0, m_progressTotal);
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
qDebug() << "attempting to run with privelage"; qDebug() << "attempting to run with privelage";
@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask()
} }
} }
if (m_copySaves) { if (savesCopy) {
there_were_errors |= !copySaves(); there_were_errors |= !(*savesCopy)();
} }
return got_priv_results && !there_were_errors; return got_priv_results && !there_were_errors;
} else {
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
} }
#else #else
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask()
return false; return false;
} }
if (m_copySaves) { if (savesCopy) {
there_were_errors |= !copySaves(); there_were_errors |= !(*savesCopy)();
} }
return !there_were_errors; return !there_were_errors;
} else { }
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get()); folderCopy.followSymlinks(false).matcher(m_matcher.get());
folderCopy(true);
setProgress(0, folderCopy.totalCopied());
connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
return folderCopy(); return folderCopy();
}
}); });
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted); connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
@ -170,3 +184,14 @@ void InstanceCopyTask::copyAborted()
emitFailed(tr("Instance folder copy has been aborted.")); emitFailed(tr("Instance folder copy has been aborted."));
return; return;
} }
bool InstanceCopyTask::abort()
{
if (m_copyFutureWatcher.isRunning()) {
m_copyFutureWatcher.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}

View File

@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask {
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
virtual void executeTask() override; virtual void executeTask() override;
bool abort() override;
void copyFinished(); void copyFinished();
void copyAborted(); void copyAborted();

View File

@ -2,6 +2,7 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include "FileSystem.h"
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
{ {
@ -45,7 +46,7 @@ void InstanceCreationTask::executeTask()
if (!QFile::exists(path)) if (!QFile::exists(path))
continue; continue;
qDebug() << "Removing" << path; qDebug() << "Removing" << path;
if (!QFile::remove(path)) { if (!FS::deletePath(path)) {
qCritical() << "Couldn't remove the old conflicting files."; qCritical() << "Couldn't remove the old conflicting files.";
emitFailed(tr("Failed to remove old conflicting files.")); emitFailed(tr("Failed to remove old conflicting files."));
return; return;

View File

@ -56,6 +56,7 @@
#include <QtConcurrentRun> #include <QtConcurrentRun>
#include <algorithm> #include <algorithm>
#include <memory>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
@ -68,15 +69,8 @@ bool InstanceImportTask::abort()
if (!canAbort()) if (!canAbort())
return false; return false;
if (m_filesNetJob) if (task)
m_filesNetJob->abort(); task->abort();
if (m_extractFuture.isRunning()) {
// NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled,
// but we can use this call to check the state when the extraction finishes.
m_extractFuture.cancel();
m_extractFuture.waitForFinished();
}
return Task::abort(); return Task::abort();
} }
@ -89,7 +83,6 @@ void InstanceImportTask::executeTask()
processZipPack(); processZipPack();
} else { } else {
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true;
downloadFromUrl(); downloadFromUrl();
} }
@ -97,115 +90,132 @@ void InstanceImportTask::executeTask()
void InstanceImportTask::downloadFromUrl() void InstanceImportTask::downloadFromUrl()
{ {
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path());
auto entry = APPLICATION->metacache()->resolveEntry("general", path); auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true); entry->setStale(true);
m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath(); m_archivePath = entry->getFullPath();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); auto filesNetJob = makeShared<NetJob>(tr("Modpack download"), APPLICATION->network());
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress);
m_filesNetJob->start(); connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed);
connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted);
task.reset(filesNetJob);
filesNetJob->start();
} }
void InstanceImportTask::downloadSucceeded() QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root)
{ {
processZipPack(); if (!isRunning()) {
m_filesNetJob.reset(); return {};
} }
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
setDetails(fileName);
if (fileName == "instance.cfg") {
qDebug() << "MultiMC:" << true;
m_modpackType = ModpackType::MultiMC;
return root;
}
if (fileName == "manifest.json") {
qDebug() << "Flame:" << true;
m_modpackType = ModpackType::Flame;
return root;
}
void InstanceImportTask::downloadFailed(QString reason) QCoreApplication::processEvents();
{ }
emitFailed(reason);
m_filesNetJob.reset();
}
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) // Recurse the search to non-ignored subfolders
{ for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
setProgress(current, total); if ("overrides/" == fileName)
} continue;
void InstanceImportTask::downloadAborted() QString result = getRootFromZip(zip, root + fileName);
{ if (!result.isEmpty())
emitAborted(); return result;
m_filesNetJob.reset(); }
return {};
} }
void InstanceImportTask::processZipPack() void InstanceImportTask::processZipPack()
{ {
setStatus(tr("Extracting modpack")); setStatus(tr("Attempting to determine instance type"));
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
qDebug() << "Attempting to create instance from" << m_archivePath; qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it // open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath)); auto packZip = std::make_shared<QuaZip>(m_archivePath);
if (!m_packZip->open(QuaZip::mdUnzip)) { if (!packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied modpack zip file.")); emitFailed(tr("Unable to open supplied modpack zip file."));
return; return;
} }
QuaZipDir packZipDir(m_packZip.get()); QuaZipDir packZipDir(packZip.get());
qDebug() << "Attempting to determine instance type";
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
bool modrinthFound = packZipDir.exists("/modrinth.index.json");
bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json");
QString root; QString root;
// NOTE: Prioritize modpack platforms that aren't searched for recursively. // NOTE: Prioritize modpack platforms that aren't searched for recursively.
// Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
if (modrinthFound) { // https://docs.modrinth.com/docs/modpacks/format_definition/#storage
if (packZipDir.exists("/modrinth.index.json")) {
// process as Modrinth pack // process as Modrinth pack
qDebug() << "Modrinth:" << modrinthFound; qDebug() << "Modrinth:" << true;
m_modpackType = ModpackType::Modrinth; m_modpackType = ModpackType::Modrinth;
} else if (technicFound) { } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
// process as Technic pack // process as Technic pack
qDebug() << "Technic:" << technicFound; qDebug() << "Technic:" << true;
extractDir.mkpath("minecraft"); extractDir.mkpath("minecraft");
extractDir.cd("minecraft"); extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic; m_modpackType = ModpackType::Technic;
} else { } else {
QStringList paths_to_ignore{ "overrides/" }; root = getRootFromZip(packZip.get());
setDetails("");
if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) {
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
} else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore);
!flameRoot.isNull()) {
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
root = flameRoot;
m_modpackType = ModpackType::Flame;
}
} }
if (m_modpackType == ModpackType::Unknown) { if (m_modpackType == ModpackType::Unknown) {
emitFailed(tr("Archive does not contain a recognized modpack type.")); emitFailed(tr("Archive does not contain a recognized modpack type."));
return; return;
} }
setStatus(tr("Extracting modpack"));
// make sure we extract just the pack // make sure we extract just the pack
m_extractFuture = auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished); auto progressStep = std::make_shared<TaskStepProgress>();
m_extractFutureWatcher.setFuture(m_extractFuture); connect(zipTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished);
connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
task.reset(zipTask);
zipTask->start();
} }
void InstanceImportTask::extractFinished() void InstanceImportTask::extractFinished()
{ {
m_packZip.reset();
if (m_extractFuture.isCanceled())
return;
if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files..."; qDebug() << "Fixing permissions for extracted pack files...";
@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC()
m_instIcon = instance.iconKey(); m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) { if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon // import icon
auto iconList = APPLICATION->icons(); auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(m_instIcon)) { if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon); iconList->deleteIcon(m_instIcon);
} }
iconList->installIcons({ importIconPath }); iconList->installIcon(importIconPath, m_instIcon);
} }
} }
emitSucceeded(); emitSucceeded();

View File

@ -39,11 +39,8 @@
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QUrl> #include <QUrl>
#include "InstanceTask.h" #include "InstanceTask.h"
#include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include <memory>
#include <optional> #include <optional>
class QuaZip; class QuaZip;
@ -54,35 +51,26 @@ class InstanceImportTask : public InstanceTask {
explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {}); explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override; bool abort() override;
const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; }
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
virtual void executeTask() override; virtual void executeTask() override;
private: private:
void processZipPack();
void processMultiMC(); void processMultiMC();
void processTechnic(); void processTechnic();
void processFlame(); void processFlame();
void processModrinth(); void processModrinth();
QString getRootFromZip(QuaZip* zip, const QString& root = "");
private slots: private slots:
void downloadSucceeded(); void processZipPack();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished(); void extractFinished();
private: /* data */ private: /* data */
NetJob::Ptr m_filesNetJob;
QUrl m_sourceUrl; QUrl m_sourceUrl;
QString m_archivePath; QString m_archivePath;
bool m_downloadRequired = false; Task::Ptr task;
std::unique_ptr<QuaZip> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
enum class ModpackType { enum class ModpackType {
Unknown, Unknown,
MultiMC, MultiMC,

View File

@ -972,7 +972,6 @@ bool InstanceList::commitStagedInstance(const QString& path,
if (groupName.isEmpty() && !groupName.isNull()) if (groupName.isEmpty() && !groupName.isNull())
groupName = QString(); groupName = QString();
QDir dir;
QString instID; QString instID;
InstancePtr inst; InstancePtr inst;
@ -996,7 +995,7 @@ bool InstanceList::commitStagedInstance(const QString& path,
return false; return false;
} }
} else { } else {
if (!dir.rename(path, destination)) { if (!FS::move(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination; qWarning() << "Failed to move" << path << "to" << destination;
return false; return false;
} }

View File

@ -315,7 +315,7 @@ void LaunchController::launchInstance()
online_mode = "online"; online_mode = "online";
// Prepend Server Status // Prepend Server Status
QStringList servers = { "authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
QString resolved_servers = ""; QString resolved_servers = "";
QHostInfo host_info; QHostInfo host_info;

View File

@ -42,6 +42,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include <QUrl> #include <QUrl>
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
@ -122,7 +123,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
zip.setUtf8Enabled(true); zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) { if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed); FS::deletePath(fileCompressed);
return false; return false;
} }
@ -130,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
zip.close(); zip.close();
if (zip.getZipError() != 0) { if (zip.getZipError() != 0) {
QFile::remove(fileCompressed); FS::deletePath(fileCompressed);
return false; return false;
} }
@ -144,7 +145,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
QuaZip zipOut(targetJarPath); QuaZip zipOut(targetJarPath);
zipOut.setUtf8Enabled(true); zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) { if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding"; qCritical() << "Failed to open the minecraft.jar for modding";
return false; return false;
} }
@ -162,7 +163,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (mod->type() == ResourceType::ZIPFILE) { if (mod->type() == ResourceType::ZIPFILE) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -171,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
auto filename = mod->fileinfo(); auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -194,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!compressDirFiles(&zipOut, parent_dir, files)) { if (!compressDirFiles(&zipOut, parent_dir, files)) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -202,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
} else { } else {
// Make sure we do not continue launching when something is missing or undefined... // Make sure we do not continue launching when something is missing or undefined...
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
return false; return false;
} }
@ -210,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) { if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close(); zipOut.close();
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents."; qCritical() << "Failed to insert minecraft.jar contents.";
return false; return false;
} }
@ -218,7 +219,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
// Recompress the jar // Recompress the jar
zipOut.close(); zipOut.close();
if (zipOut.getZipError() != 0) { if (zipOut.getZipError() != 0) {
QFile::remove(targetJarPath); FS::deletePath(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!"; qCritical() << "Failed to finalize minecraft.jar!";
return false; return false;
} }
@ -332,9 +333,20 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
} }
extracted.append(target_file_path); extracted.append(target_file_path);
QFile::setPermissions(target_file_path, auto fileInfo = QFileInfo(target_file_path);
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile()); } while (zip->goToNextFile());
@ -492,10 +504,10 @@ auto ExportToZipTask::exportZip() -> ZipResult
void ExportToZipTask::finish() void ExportToZipTask::finish()
{ {
if (m_build_zip_future.isCanceled()) { if (m_build_zip_future.isCanceled()) {
QFile::remove(m_output_path); FS::deletePath(m_output_path);
emitAborted(); emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) { } else if (auto result = m_build_zip_future.result(); result.has_value()) {
QFile::remove(m_output_path); FS::deletePath(m_output_path);
emitFailed(result.value()); emitFailed(result.value());
} else { } else {
emitSucceeded(); emitSucceeded();
@ -512,6 +524,123 @@ bool ExportToZipTask::abort()
} }
return false; return false;
} }
#endif
void ExtractZipTask::executeTask()
{
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
m_zip_watcher.setFuture(m_zip_future);
}
auto ExtractZipTask::extractZip() -> ZipResult
{
auto target = m_output_dir.absolutePath();
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target;
auto numEntries = m_input->getEntriesCount();
if (numEntries < 0) {
return ZipResult(tr("Failed to enumerate files in archive"));
}
if (numEntries == 0) {
logWarning(tr("Extracting empty archives seems odd..."));
return ZipResult();
}
if (!m_input->goToFirstFile()) {
return ZipResult(tr("Failed to seek to first file in zip"));
}
setStatus("Extracting files...");
setProgress(0, numEntries);
do {
if (m_zip_future.isCanceled())
return ZipResult();
setProgress(m_progress + 1, m_progressTotal);
QString file_name = m_input->getCurrentFileName();
if (!file_name.startsWith(m_subdirectory))
continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, m_subdirectory.size()));
auto original_name = relative_file_name;
setStatus("Unziping: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
.arg(relative_file_name, target));
}
if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) {
JlCompress::removeFile(extracted);
return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
}
extracted.append(target_file_path);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (m_input->goToNextFile());
return ZipResult();
}
void ExtractZipTask::finish()
{
if (m_zip_future.isCanceled()) {
emitAborted();
} else if (auto result = m_zip_future.result(); result.has_value()) {
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExtractZipTask::abort()
{
if (m_zip_future.isRunning()) {
m_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
#endif
} // namespace MMCZip } // namespace MMCZip

View File

@ -205,5 +205,30 @@ class ExportToZipTask : public Task {
QFuture<ZipResult> m_build_zip_future; QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher; QFutureWatcher<ZipResult> m_build_zip_watcher;
}; };
class ExtractZipTask : public Task {
public:
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
typedef std::optional<QString> ZipResult;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
std::shared_ptr<QuaZip> m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
#endif #endif
} // namespace MMCZip } // namespace MMCZip

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

@ -212,3 +212,25 @@ QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegular
right = s.mid(end); right = s.mid(end);
return qMakePair(left, right); return qMakePair(left, right);
} }
static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
QString StringUtils::htmlListPatch(QString htmlStr)
{
int pos = htmlStr.indexOf(ulMatcher);
int imgPos;
while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the </ul> tag. Add one for zeroeth index
imgPos = htmlStr.indexOf("<img ", pos);
if (imgPos == -1)
break; // no image after the tag
auto textBetween = htmlStr.mid(pos, imgPos - pos).trimmed(); // trim all white spaces
if (textBetween.isEmpty())
htmlStr.insert(pos, "<br>");
pos = htmlStr.indexOf(ulMatcher, pos);
}
return htmlStr;
}

View File

@ -85,4 +85,6 @@ QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::Cas
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re); QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re);
QString htmlListPatch(QString htmlStr);
} // namespace StringUtils } // namespace StringUtils

View File

@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const
bool IconList::deleteIcon(const QString& key) bool IconList::deleteIcon(const QString& key)
{ {
return iconFileExists(key) && QFile::remove(icon(key)->getFilePath()); return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath());
} }
bool IconList::trashIcon(const QString& key) bool IconList::trashIcon(const QString& key)

View File

@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey)
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
auto fileInfo = it.fileInfo(); auto fileInfo = it.fileInfo();
if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix()))
if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix()))
return fileInfo.absoluteFilePath(); return fileInfo.absoluteFilePath();
} }
return {}; return {};

View File

@ -207,7 +207,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
HKEY newKey; HKEY newKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) == if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) ==
ERROR_SUCCESS) { ERROR_SUCCESS) {
// Read the JavaHome value to find where Java is installed. // Read the JavaHome value to find where Java is installed.
DWORD valueSz = 0; DWORD valueSz = 0;
@ -283,6 +283,12 @@ QList<QString> JavaUtils::FindJavaPaths()
QList<JavaInstallPtr> ADOPTIUMJDK64s = QList<JavaInstallPtr> ADOPTIUMJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI");
// IBM Semeru
QList<JavaInstallPtr> SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
// Microsoft // Microsoft
QList<JavaInstallPtr> MICROSOFTJDK64s = QList<JavaInstallPtr> MICROSOFTJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
@ -300,6 +306,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE64s); java_candidates.append(NEWJRE64s);
java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTOPENJRE64s);
java_candidates.append(ADOPTIUMJRE64s); java_candidates.append(ADOPTIUMJRE64s);
java_candidates.append(SEMERUJRE64s);
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
@ -308,6 +315,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(ADOPTOPENJDK64s);
java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(FOUNDATIONJDK64s);
java_candidates.append(ADOPTIUMJDK64s); java_candidates.append(ADOPTIUMJDK64s);
java_candidates.append(SEMERUJDK64s);
java_candidates.append(MICROSOFTJDK64s); java_candidates.append(MICROSOFTJDK64s);
java_candidates.append(ZULU64s); java_candidates.append(ZULU64s);
java_candidates.append(LIBERICA64s); java_candidates.append(LIBERICA64s);
@ -316,6 +324,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE32s); java_candidates.append(NEWJRE32s);
java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTOPENJRE32s);
java_candidates.append(ADOPTIUMJRE32s); java_candidates.append(ADOPTIUMJRE32s);
java_candidates.append(SEMERUJRE32s);
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
@ -324,6 +333,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(ADOPTOPENJDK32s);
java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(FOUNDATIONJDK32s);
java_candidates.append(ADOPTIUMJDK32s); java_candidates.append(ADOPTIUMJDK32s);
java_candidates.append(SEMERUJDK32s);
java_candidates.append(ZULU32s); java_candidates.append(ZULU32s);
java_candidates.append(LIBERICA32s); java_candidates.append(LIBERICA32s);
@ -410,6 +420,7 @@ QList<QString> JavaUtils::FindJavaPaths()
// manually installed JDKs in /opt // manually installed JDKs in /opt
scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdk");
scanJavaDirs("/opt/jdks"); scanJavaDirs("/opt/jdks");
scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
// flatpak // flatpak
scanJavaDirs("/app/jdk"); scanJavaDirs("/app/jdk");

View File

@ -15,6 +15,7 @@
#include "BaseEntity.h" #include "BaseEntity.h"
#include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/HttpMetaCache.h" #include "net/HttpMetaCache.h"
@ -83,8 +84,7 @@ bool Meta::BaseEntity::loadLocalFile()
} catch (const Exception& e) { } catch (const Exception& e) {
qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
// just make sure it's gone and we never consider it again. // just make sure it's gone and we never consider it again.
QFile::remove(fname); return !FS::deletePath(fname);
return false;
} }
} }

View File

@ -336,7 +336,7 @@ bool Component::revert()
bool result = true; bool result = true;
// just kill the file and reload // just kill the file and reload
if (QFile::exists(filename)) { if (QFile::exists(filename)) {
result = QFile::remove(filename); result = FS::deletePath(filename);
} }
if (result) { if (result) {
// file gone... // file gone...

View File

@ -1000,7 +1000,7 @@ QString MinecraftInstance::getStatusbarDescription()
QString description; QString description;
description.append(tr("Minecraft %1").arg(mcVersion)); description.append(tr("Minecraft %1").arg(mcVersion));
if (m_settings->get("ShowGameTime").toBool()) { if (m_settings->get("ShowGameTime").toBool()) {
if (lastTimePlayed() > 0) { if (lastTimePlayed() > 0 && lastLaunch() > 0) {
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append( description.append(
tr(", last played on %1 for %2") tr(", last played on %1 for %2")

View File

@ -839,7 +839,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
QFileInfo jarInfo(finalPath); QFileInfo jarInfo(finalPath);
if (jarInfo.exists()) { if (jarInfo.exists()) {
if (!QFile::remove(finalPath)) { if (!FS::deletePath(finalPath)) {
return false; return false;
} }
} }

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"
@ -111,7 +112,7 @@ bool ResourceFolderModel::installResource(QString original_path)
case ResourceType::ZIPFILE: case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: { case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
if (!QFile::remove(new_path)) { if (!FS::deletePath(new_path)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
return false; return false;
} }
@ -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

@ -178,6 +178,88 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
return true; return true;
} }
QString buildStyle(const QJsonObject& obj)
{
QStringList styles;
if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) {
styles << QString("color: %1;").arg(color);
}
if (obj.contains("bold")) {
QString weight = "normal";
if (Json::ensureBoolean(obj, "bold", false)) {
weight = "bold";
}
styles << QString("font-weight: %1;").arg(weight);
}
if (obj.contains("italic")) {
QString style = "normal";
if (Json::ensureBoolean(obj, "italic", false)) {
style = "italic";
}
styles << QString("font-style: %1;").arg(style);
}
return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" "));
}
QString processComponent(const QJsonArray& value, bool strikethrough, bool underline)
{
QString result;
for (auto current : value)
result += processComponent(current, strikethrough, underline);
return result;
}
QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline)
{
underline = Json::ensureBoolean(obj, "underlined", underline);
strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough);
QString result = Json::ensureString(obj, "text");
if (underline) {
result = QString("<u>%1</u>").arg(result);
}
if (strikethrough) {
result = QString("<s>%1</s>").arg(result);
}
// the extra needs to be a array
result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline);
if (auto style = buildStyle(obj); !style.isEmpty()) {
result = QString("<span %1>%2</span>").arg(style, result);
}
if (obj.contains("clickEvent")) {
auto click_event = Json::ensureObject(obj, "clickEvent");
auto action = Json::ensureString(click_event, "action");
auto value = Json::ensureString(click_event, "value");
if (action == "open_url" && !value.isEmpty()) {
result = QString("<a href=\"%1\">%2</a>").arg(value, result);
}
}
return result;
}
QString processComponent(const QJsonValue& value, bool strikethrough, bool underline)
{
if (value.isString()) {
return value.toString();
}
if (value.isBool()) {
return value.toBool() ? "true" : "false";
}
if (value.isDouble()) {
return QString::number(value.toDouble());
}
if (value.isArray()) {
return processComponent(value.toArray(), strikethrough, underline);
}
if (value.isObject()) {
return processComponent(value.toObject(), strikethrough, underline);
}
qWarning() << "Invalid component type!";
return {};
}
// https://minecraft.wiki/w/Raw_JSON_text_format
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{ {
@ -186,7 +268,9 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack.setDescription(Json::ensureString(pack_obj, "description", ""));
pack.setDescription(processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause(); qWarning() << "JsonException: " << e.what() << e.cause();
return false; return false;

View File

@ -34,6 +34,7 @@ bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);

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

@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles()
// Delete the files // Delete the files
for (const auto& item : filesToDelete) { for (const auto& item : filesToDelete) {
QFile::remove(item); FS::deletePath(item);
} }
} }
@ -987,7 +987,7 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
// the copy from the Configs.zip // the copy from the Configs.zip
QFileInfo fileInfo(to); QFileInfo fileInfo(to);
if (fileInfo.exists()) { if (fileInfo.exists()) {
if (!QFile::remove(to)) { if (!FS::deletePath(to)) {
qWarning() << "Failed to delete" << to; qWarning() << "Failed to delete" << to;
return false; return false;
} }

View File

@ -322,7 +322,7 @@ bool FlameCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause()); setError(tr("Could not understand pack manifest:\n") + e.cause());
@ -336,7 +336,7 @@ bool FlameCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, overridePath); Override::createOverrides("overrides", parent_folder, overridePath);
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath)) { if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
return false; return false;
} }

View File

@ -42,17 +42,28 @@ QString toHTML(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", ").toHtmlEscaped(); line += " by " + mod->authors().join(", ").toHtmlEscaped();
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped());
lines.append(QString("<li>%1</li>").arg(line)); lines.append(QString("<li>%1</li>").arg(line));
} }
return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t")); return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
} }
QString toMarkdownEscaped(QString src)
{
for (auto ch : "\\`*_{}[]<>()#+-.!|")
src.replace(ch, QString("\\%1").arg(ch));
return src;
}
QString toMarkdown(QList<Mod*> mods, OptionalData extraData) QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
{ {
QStringList lines; QStringList lines;
for (auto mod : mods) { for (auto mod : mods) {
auto meta = mod->metadata(); auto meta = mod->metadata();
auto modName = mod->name(); auto modName = toMarkdownEscaped(mod->name());
if (extraData & Url) { if (extraData & Url) {
auto url = mod->metaurl(); auto url = mod->metaurl();
if (!url.isEmpty()) if (!url.isEmpty())
@ -60,14 +71,16 @@ QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
} }
auto line = modName; auto line = modName;
if (extraData & Version) { if (extraData & Version) {
auto ver = mod->version(); auto ver = toMarkdownEscaped(mod->version());
if (ver.isEmpty() && meta != nullptr) if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString(); ver = toMarkdownEscaped(meta->version().toString());
if (!ver.isEmpty()) if (!ver.isEmpty())
line += QString(" [%1]").arg(ver); line += QString(" [%1]").arg(ver);
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", "); line += " by " + toMarkdownEscaped(mod->authors().join(", "));
if (extraData & FileName)
line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName()));
lines << "- " + line; lines << "- " + line;
} }
return lines.join("\n"); return lines.join("\n");
@ -95,6 +108,8 @@ QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line += " by " + mod->authors().join(", "); line += " by " + mod->authors().join(", ");
if (extraData & FileName)
line += QString(" (%1)").arg(mod->fileinfo().fileName());
lines << line; lines << line;
} }
return lines.join("\n"); return lines.join("\n");
@ -122,6 +137,8 @@ QString toJSON(QList<Mod*> mods, OptionalData extraData)
} }
if (extraData & Authors && !mod->authors().isEmpty()) if (extraData & Authors && !mod->authors().isEmpty())
line["authors"] = QJsonArray::fromStringList(mod->authors()); line["authors"] = QJsonArray::fromStringList(mod->authors());
if (extraData & FileName)
line["filename"] = mod->fileinfo().fileName();
lines << line; lines << line;
} }
QJsonDocument doc; QJsonDocument doc;
@ -154,6 +171,8 @@ QString toCSV(QList<Mod*> mods, OptionalData extraData)
authors = QString("\"%1\"").arg(mod->authors().join(",")); authors = QString("\"%1\"").arg(mod->authors().join(","));
data << authors; data << authors;
} }
if (extraData & FileName)
data << mod->fileinfo().fileName();
lines << data.join(","); lines << data.join(",");
} }
return lines.join("\n"); return lines.join("\n");
@ -189,11 +208,13 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
if (ver.isEmpty() && meta != nullptr) if (ver.isEmpty() && meta != nullptr)
ver = meta->version().toString(); ver = meta->version().toString();
auto authors = mod->authors().join(", "); auto authors = mod->authors().join(", ");
auto filename = mod->fileinfo().fileName();
lines << QString(lineTemplate) lines << QString(lineTemplate)
.replace("{name}", modName) .replace("{name}", modName)
.replace("{url}", url) .replace("{url}", url)
.replace("{version}", ver) .replace("{version}", ver)
.replace("{authors}", authors); .replace("{authors}", authors)
.replace("{filename}", filename);
} }
return lines.join("\n"); return lines.join("\n");
} }

View File

@ -23,11 +23,7 @@
namespace ExportToModList { namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData { enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 };
Authors = 1 << 0,
Url = 1 << 1,
Version = 1 << 2,
};
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData); QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate); QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList } // namespace ExportToModList

View File

@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS
{ {
QString file_path(FS::PathCombine(parent_folder, name + ".txt")); QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
if (QFile::exists(file_path)) if (QFile::exists(file_path))
QFile::remove(file_path); FS::deletePath(file_path);
FS::ensureFilePathExists(file_path); FS::ensureFilePathExists(file_path);

View File

@ -137,7 +137,7 @@ void PackInstallTask::install()
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if (unzipMcDir.exists()) { if (unzipMcDir.exists()) {
// ok, found minecraft dir, move contents to instance dir // ok, found minecraft dir, move contents to instance dir
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) {
emitFailed(tr("Failed to move unzipped Minecraft!")); emitFailed(tr("Failed to move unzipped Minecraft!"));
return; return;
} }

View File

@ -173,7 +173,7 @@ bool ModrinthCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
@ -183,7 +183,7 @@ bool ModrinthCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, override_path); Override::createOverrides("overrides", parent_folder, override_path);
// Apply the overrides // Apply the overrides
if (!QFile::rename(override_path, mcPath)) { if (!FS::move(override_path, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides"); setError(tr("Could not rename the overrides folder:\n") + "overrides");
return false; return false;
} }

View File

@ -131,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
file.name = Json::requireString(obj, "name"); file.name = Json::requireString(obj, "name");
file.version = Json::requireString(obj, "version_number"); file.version = Json::requireString(obj, "version_number");
auto gameVersions = Json::ensureArray(obj, "game_versions");
if (!gameVersions.isEmpty()) {
file.gameVersion = Json::ensureString(gameVersions[0]);
}
file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
file.changelog = Json::ensureString(obj, "changelog"); file.changelog = Json::ensureString(obj, "changelog");

View File

@ -84,6 +84,7 @@ struct ModpackExtra {
struct ModpackVersion { struct ModpackVersion {
QString name; QString name;
QString version; QString version;
QString gameVersion;
ModPlatform::IndexedVersionType version_type; ModPlatform::IndexedVersionType version_type;
QString changelog; QString changelog;

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
@ -146,11 +150,11 @@ void NetJob::emitFailed(QString reason)
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
if (m_ask_retry) { 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();

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;

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>
@ -96,7 +95,6 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportPackDialog.h" #include "ui/dialogs/ExportPackDialog.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/IconPickerDialog.h"
#include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/ImportResourceDialog.h"
#include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewInstanceDialog.h"
@ -209,7 +207,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceZip);
exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
exportInstanceMenu->addAction(ui->actionExportInstanceToModList);
ui->actionExportInstance->setMenu(exportInstanceMenu); ui->actionExportInstance->setMenu(exportInstanceMenu);
} }
@ -1212,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);
@ -1416,14 +1418,6 @@ void MainWindow::on_actionExportInstanceMrPack_triggered()
} }
} }
void MainWindow::on_actionExportInstanceToModList_triggered()
{
if (m_selectedInstance) {
ExportToModListDialog dlg(m_selectedInstance, this);
dlg.exec();
}
}
void MainWindow::on_actionExportInstanceFlamePack_triggered() void MainWindow::on_actionExportInstanceFlamePack_triggered()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {

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();
@ -158,7 +160,6 @@ class MainWindow : public QMainWindow {
void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceZip_triggered();
void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceMrPack_triggered();
void on_actionExportInstanceFlamePack_triggered(); void on_actionExportInstanceFlamePack_triggered();
void on_actionExportInstanceToModList_triggered();
void on_actionRenameInstance_triggered(); void on_actionRenameInstance_triggered();

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"/>
@ -491,15 +492,6 @@
<string>CurseForge (zip)</string> <string>CurseForge (zip)</string>
</property> </property>
</action> </action>
<action name="actionExportInstanceToModList">
<property name="icon">
<iconset theme="new">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Mod List</string>
</property>
</action>
<action name="actionCreateInstanceShortcut"> <action name="actionCreateInstanceShortcut">
<property name="icon"> <property name="icon">
<iconset theme="shortcut"> <iconset theme="shortcut">
@ -587,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

@ -38,6 +38,7 @@
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui_AboutDialog.h" #include "ui_AboutDialog.h"
#include <net/NetJob.h> #include <net/NetJob.h>
@ -139,10 +140,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia
setWindowTitle(tr("About %1").arg(launcherName)); setWindowTitle(tr("About %1").arg(launcherName));
QString chtml = getCreditsHtml(); QString chtml = getCreditsHtml();
ui->creditsText->setHtml(chtml); ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
QString lhtml = getLicenseHtml(); QString lhtml = getLicenseHtml();
ui->licenseText->setHtml(lhtml); ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
ui->urlLabel->setOpenExternalLinks(true); ui->urlLabel->setOpenExternalLinks(true);

View File

@ -22,8 +22,7 @@
#include <QTextEdit> #include <QTextEdit>
#include "FileSystem.h" #include "FileSystem.h"
#include "Markdown.h" #include "Markdown.h"
#include "minecraft/MinecraftInstance.h" #include "StringUtils.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/ExportToModList.h" #include "modplatform/helpers/ExportToModList.h"
#include "ui_ExportToModListDialog.h" #include "ui_ExportToModListDialog.h"
@ -41,38 +40,31 @@ const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLin
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
}; };
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent) ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent)
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog) : QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog)
{ {
ui->setupUi(this); ui->setupUi(this);
enableCustom(false); enableCustom(false);
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() {
m_allMods = mcInstance->loaderModList()->allMods();
triggerImp();
});
}
connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); connect(ui->formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged);
connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); });
connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); });
connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); });
connect(ui->templateText, &QTextEdit::textChanged, this, [this] { connect(ui->templateText, &QTextEdit::textChanged, this, [this] {
if (ui->templateText->toPlainText() != exampleLines[format]) if (ui->templateText->toPlainText() != exampleLines[m_format])
ui->formatComboBox->setCurrentIndex(5); ui->formatComboBox->setCurrentIndex(5);
else
triggerImp(); triggerImp();
}); });
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
this->ui->finalText->selectAll(); this->ui->finalText->selectAll();
this->ui->finalText->copy(); this->ui->finalText->copy();
}); });
triggerImp();
} }
ExportToModListDialog::~ExportToModListDialog() ExportToModListDialog::~ExportToModListDialog()
@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index)
case 0: { case 0: {
enableCustom(false); enableCustom(false);
ui->resultText->show(); ui->resultText->show();
format = ExportToModList::HTML; m_format = ExportToModList::HTML;
break; break;
} }
case 1: { case 1: {
enableCustom(false); enableCustom(false);
ui->resultText->show(); ui->resultText->show();
format = ExportToModList::MARKDOWN; m_format = ExportToModList::MARKDOWN;
break; break;
} }
case 2: { case 2: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::PLAINTXT; m_format = ExportToModList::PLAINTXT;
break; break;
} }
case 3: { case 3: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::JSON; m_format = ExportToModList::JSON;
break; break;
} }
case 4: { case 4: {
enableCustom(false); enableCustom(false);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::CSV; m_format = ExportToModList::CSV;
break; break;
} }
case 5: { case 5: {
m_template_changed = true; m_template_changed = true;
enableCustom(true); enableCustom(true);
ui->resultText->hide(); ui->resultText->hide();
format = ExportToModList::CUSTOM; m_format = ExportToModList::CUSTOM;
break; break;
} }
} }
@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index)
void ExportToModListDialog::triggerImp() void ExportToModListDialog::triggerImp()
{ {
if (format == ExportToModList::CUSTOM) { if (m_format == ExportToModList::CUSTOM) {
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText())); ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText()));
return; return;
} }
auto opt = 0; auto opt = 0;
@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp()
opt |= ExportToModList::Version; opt |= ExportToModList::Version;
if (ui->urlCheckBox->isChecked()) if (ui->urlCheckBox->isChecked())
opt |= ExportToModList::Url; opt |= ExportToModList::Url;
auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast<ExportToModList::OptionalData>(opt)); if (ui->filenameCheckBox->isChecked())
opt |= ExportToModList::FileName;
auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast<ExportToModList::OptionalData>(opt));
ui->finalText->setPlainText(txt); ui->finalText->setPlainText(txt);
switch (format) { switch (m_format) {
case ExportToModList::CUSTOM: case ExportToModList::CUSTOM:
return; return;
case ExportToModList::HTML: case ExportToModList::HTML:
ui->resultText->setHtml(txt); ui->resultText->setHtml(StringUtils::htmlListPatch(txt));
break; break;
case ExportToModList::MARKDOWN: case ExportToModList::MARKDOWN:
ui->resultText->setHtml(markdownToHTML(txt)); ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt)));
break; break;
case ExportToModList::PLAINTXT: case ExportToModList::PLAINTXT:
break; break;
@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp()
case ExportToModList::CSV: case ExportToModList::CSV:
break; break;
} }
auto exampleLine = exampleLines[format]; auto exampleLine = exampleLines[m_format];
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
ui->templateText->setPlainText(exampleLine); ui->templateText->setPlainText(exampleLine);
} }
@ -163,9 +157,9 @@ void ExportToModListDialog::triggerImp()
void ExportToModListDialog::done(int result) void ExportToModListDialog::done(int result)
{ {
if (result == Accepted) { if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(name); const QString filename = FS::RemoveInvalidFilenameChars(m_name);
const QString output = const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()), QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr); "File (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty()) if (output.isEmpty())
@ -178,7 +172,7 @@ void ExportToModListDialog::done(int result)
QString ExportToModListDialog::extension() QString ExportToModListDialog::extension()
{ {
switch (format) { switch (m_format) {
case ExportToModList::HTML: case ExportToModList::HTML:
return ".html"; return ".html";
case ExportToModList::MARKDOWN: case ExportToModList::MARKDOWN:
@ -197,7 +191,7 @@ QString ExportToModListDialog::extension()
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
{ {
if (format != ExportToModList::CUSTOM) if (m_format != ExportToModList::CUSTOM)
return; return;
switch (option) { switch (option) {
case ExportToModList::Authors: case ExportToModList::Authors:
@ -209,6 +203,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
case ExportToModList::Version: case ExportToModList::Version:
ui->templateText->insertPlainText("{version}"); ui->templateText->insertPlainText("{version}");
break; break;
case ExportToModList::FileName:
ui->templateText->insertPlainText("{filename}");
break;
} }
} }
void ExportToModListDialog::enableCustom(bool enabled) void ExportToModListDialog::enableCustom(bool enabled)
@ -221,4 +218,7 @@ void ExportToModListDialog::enableCustom(bool enabled)
ui->urlCheckBox->setHidden(enabled); ui->urlCheckBox->setHidden(enabled);
ui->urlButton->setHidden(!enabled); ui->urlButton->setHidden(!enabled);
ui->filenameCheckBox->setHidden(enabled);
ui->filenameButton->setHidden(!enabled);
} }

View File

@ -20,7 +20,6 @@
#include <QDialog> #include <QDialog>
#include <QList> #include <QList>
#include "BaseInstance.h"
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "modplatform/helpers/ExportToModList.h" #include "modplatform/helpers/ExportToModList.h"
@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr); explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr);
~ExportToModListDialog(); ~ExportToModListDialog();
void done(int result) override; void done(int result) override;
@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog {
private: private:
QString extension(); QString extension();
void enableCustom(bool enabled); void enableCustom(bool enabled);
QList<Mod*> m_allMods;
QList<Mod*> m_mods;
bool m_template_changed; bool m_template_changed;
QString name; QString m_name;
ExportToModList::Formats format = ExportToModList::Formats::HTML; ExportToModList::Formats m_format = ExportToModList::Formats::HTML;
Ui::ExportToModListDialog* ui; Ui::ExportToModListDialog* ui;
static const QHash<ExportToModList::Formats, QString> exampleLines; static const QHash<ExportToModList::Formats, QString> exampleLines;
}; };

View File

@ -117,6 +117,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="filenameCheckBox">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="versionButton"> <widget class="QPushButton" name="versionButton">
<property name="text"> <property name="text">
@ -138,6 +145,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="filenameButton">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -3,6 +3,7 @@
#include "CustomMessageBox.h" #include "CustomMessageBox.h"
#include "ProgressDialog.h" #include "ProgressDialog.h"
#include "ScrollMessageBox.h" #include "ScrollMessageBox.h"
#include "StringUtils.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
@ -473,7 +474,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
break; break;
} }
changelog_area->setHtml(text); changelog_area->setHtml(StringUtils::htmlListPatch(text));
changelog_area->setOpenExternalLinks(true); changelog_area->setOpenExternalLinks(true);
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);

View File

@ -52,6 +52,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QLayout> #include <QLayout>
#include <QPushButton> #include <QPushButton>
#include <QScreen>
#include <QValidator> #include <QValidator>
#include <utility> #include <utility>
@ -63,6 +64,7 @@
#include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h"
#include "ui/widgets/PageContainer.h" #include "ui/widgets/PageContainer.h"
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
const QString& url, const QString& url,
const QMap<QString, QString>& extra_info, const QMap<QString, QString>& extra_info,
@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
updateDialogState(); updateDialogState();
if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
} else {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto screen = parent->screen();
#else
auto screen = QGuiApplication::primaryScreen();
#endif
auto geometry = screen->availableSize();
resize(width(), qMin(geometry.height() - 50, 710));
}
} }
void NewInstanceDialog::reject() void NewInstanceDialog::reject()

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

@ -25,6 +25,7 @@
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Markdown.h" #include "Markdown.h"
#include "StringUtils.h"
#include "ui_UpdateAvailableDialog.h" #include "ui_UpdateAvailableDialog.h"
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
auto releaseNotesHtml = markdownToHTML(releaseNotes); auto releaseNotesHtml = markdownToHTML(releaseNotes);
ui->releaseNotes->setHtml(releaseNotesHtml); ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
ui->releaseNotes->setOpenExternalLinks(true); ui->releaseNotes->setOpenExternalLinks(true);
connect(ui->skipButton, &QPushButton::clicked, this, [this]() { connect(ui->skipButton, &QPushButton::clicked, this, [this]() {

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

@ -66,6 +66,9 @@ void VisualGroup::update()
rows[currentRow].height = maxRowHeight; rows[currentRow].height = maxRowHeight;
rows[currentRow].top = offsetFromTop; rows[currentRow].top = offsetFromTop;
currentRow++; currentRow++;
if (currentRow >= rows.size()) {
currentRow = rows.size() - 1;
}
offsetFromTop += maxRowHeight + 5; offsetFromTop += maxRowHeight + 5;
positionInRow = 0; positionInRow = 0;
maxRowHeight = 0; maxRowHeight = 0;

Some files were not shown because too many files have changed in this diff Show More