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:
commit
3f0fa54fe8
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Create backport PRs
|
||||
uses: korthout/backport-action@v2.5.0
|
||||
uses: korthout/backport-action@v3.0.2
|
||||
with:
|
||||
# Config README: https://github.com/korthout/backport-action#backport-action
|
||||
pull_description: |-
|
||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -611,7 +611,7 @@ jobs:
|
||||
flatpak:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08
|
||||
image: bilelmoussaoui/flatpak-github-actions:kde-6.7
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -163,7 +163,6 @@ class Config {
|
||||
|
||||
QString RESOURCE_BASE = "https://resources.download.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 FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists
|
||||
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists
|
||||
|
52
flake.lock
generated
52
flake.lock
generated
@ -23,11 +23,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714641030,
|
||||
"narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=",
|
||||
"lastModified": 1717285511,
|
||||
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e",
|
||||
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -36,24 +36,6 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@ -93,11 +75,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1715413075,
|
||||
"narHash": "sha256-FCi3R1MeS5bVp0M0xTheveP6hhcCYfW/aghSTPebYL4=",
|
||||
"lastModified": 1717774105,
|
||||
"narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e4e7a43a9db7e22613accfeb1005cca1b2b1ee0d",
|
||||
"rev": "d226935fd75012939397c83f6c385e4d6d832288",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -112,7 +94,6 @@
|
||||
"flake-compat": [
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
@ -122,11 +103,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714478972,
|
||||
"narHash": "sha256-q//cgb52vv81uOuwz1LaXElp3XAe1TqrABXODAEF6Sk=",
|
||||
"lastModified": 1717664902,
|
||||
"narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "2849da033884f54822af194400f8dff435ada242",
|
||||
"rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -143,21 +124,6 @@
|
||||
"nixpkgs": "nixpkgs",
|
||||
"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",
|
||||
|
@ -1,6 +1,6 @@
|
||||
id: org.prismlauncher.PrismLauncher
|
||||
runtime: org.kde.Platform
|
||||
runtime-version: 5.15-23.08
|
||||
runtime-version: 6.7
|
||||
sdk: org.kde.Sdk
|
||||
sdk-extensions:
|
||||
- org.freedesktop.Sdk.Extension.openjdk21
|
||||
@ -38,7 +38,6 @@ modules:
|
||||
config-opts:
|
||||
- -DLauncher_BUILD_PLATFORM=flatpak
|
||||
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
- -DLauncher_QT_VERSION_MAJOR=5
|
||||
build-options:
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
|
||||
|
@ -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 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
|
||||
for (auto i = 0; i <= 4; i++)
|
||||
if (auto oldName = baseLogFile.arg(i);
|
||||
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--)
|
||||
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)));
|
||||
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("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
|
||||
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
|
||||
m_settings->registerSetting("SkinsDir", "skins");
|
||||
|
||||
// Editors
|
||||
m_settings->registerSetting("JsonEditor", QString());
|
||||
@ -1211,6 +1207,12 @@ void Application::performMainStartupAction()
|
||||
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()) {
|
||||
qDebug() << "<> Importing from url:" << m_urlsToImport;
|
||||
m_mainWindow->processURLs(m_urlsToImport);
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include <QFile>
|
||||
|
||||
#include "BaseInstaller.h"
|
||||
#include "FileSystem.h"
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
|
||||
BaseInstaller::BaseInstaller() {}
|
||||
@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to)
|
||||
|
||||
bool BaseInstaller::remove(MinecraftInstance* from)
|
||||
{
|
||||
return QFile::remove(filename(from->instanceRoot()));
|
||||
return FS::deletePath(filename(from->instanceRoot()));
|
||||
}
|
||||
|
||||
QString BaseInstaller::filename(const QString& root) const
|
||||
|
@ -362,13 +362,17 @@ set(MINECRAFT_SOURCES
|
||||
minecraft/AssetsUtils.h
|
||||
minecraft/AssetsUtils.cpp
|
||||
|
||||
# Minecraft services
|
||||
minecraft/services/CapeChange.cpp
|
||||
minecraft/services/CapeChange.h
|
||||
minecraft/services/SkinUpload.cpp
|
||||
minecraft/services/SkinUpload.h
|
||||
minecraft/services/SkinDelete.cpp
|
||||
minecraft/services/SkinDelete.h
|
||||
# Minecraft skins
|
||||
minecraft/skins/CapeChange.cpp
|
||||
minecraft/skins/CapeChange.h
|
||||
minecraft/skins/SkinUpload.cpp
|
||||
minecraft/skins/SkinUpload.h
|
||||
minecraft/skins/SkinDelete.cpp
|
||||
minecraft/skins/SkinDelete.h
|
||||
minecraft/skins/SkinModel.cpp
|
||||
minecraft/skins/SkinModel.h
|
||||
minecraft/skins/SkinList.cpp
|
||||
minecraft/skins/SkinList.h
|
||||
|
||||
minecraft/Agent.h)
|
||||
|
||||
@ -787,8 +791,6 @@ SET(LAUNCHER_SOURCES
|
||||
ui/InstanceWindow.cpp
|
||||
|
||||
# FIXME: maybe find a better home for this.
|
||||
SkinUtils.cpp
|
||||
SkinUtils.h
|
||||
FileIgnoreProxy.cpp
|
||||
FileIgnoreProxy.h
|
||||
FastFileIconProvider.cpp
|
||||
@ -1015,8 +1017,6 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/ReviewMessageBox.h
|
||||
ui/dialogs/VersionSelectDialog.cpp
|
||||
ui/dialogs/VersionSelectDialog.h
|
||||
ui/dialogs/SkinUploadDialog.cpp
|
||||
ui/dialogs/SkinUploadDialog.h
|
||||
ui/dialogs/ResourceDownloadDialog.cpp
|
||||
ui/dialogs/ResourceDownloadDialog.h
|
||||
ui/dialogs/ScrollMessageBox.cpp
|
||||
@ -1030,6 +1030,9 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/InstallLoaderDialog.cpp
|
||||
ui/dialogs/InstallLoaderDialog.h
|
||||
|
||||
ui/dialogs/skins/SkinManageDialog.cpp
|
||||
ui/dialogs/skins/SkinManageDialog.h
|
||||
|
||||
# GUI - widgets
|
||||
ui/widgets/Common.cpp
|
||||
ui/widgets/Common.h
|
||||
@ -1159,7 +1162,6 @@ qt_wrap_ui(LAUNCHER_UI
|
||||
ui/dialogs/NewComponentDialog.ui
|
||||
ui/dialogs/NewsDialog.ui
|
||||
ui/dialogs/ProfileSelectDialog.ui
|
||||
ui/dialogs/SkinUploadDialog.ui
|
||||
ui/dialogs/ExportInstanceDialog.ui
|
||||
ui/dialogs/ExportPackDialog.ui
|
||||
ui/dialogs/ExportToModListDialog.ui
|
||||
@ -1173,6 +1175,8 @@ qt_wrap_ui(LAUNCHER_UI
|
||||
ui/dialogs/ScrollMessageBox.ui
|
||||
ui/dialogs/BlockedModsDialog.ui
|
||||
ui/dialogs/ChooseProviderDialog.ui
|
||||
|
||||
ui/dialogs/skins/SkinManageDialog.ui
|
||||
)
|
||||
|
||||
qt_wrap_ui(PRISM_UPDATE_UI
|
||||
|
@ -647,6 +647,19 @@ void ExternalLinkFileProcess::runLinkFile()
|
||||
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)
|
||||
{
|
||||
std::error_code err;
|
||||
@ -654,13 +667,14 @@ bool move(const QString& source, const QString& dest)
|
||||
ensureFilePathExists(dest);
|
||||
fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
|
||||
|
||||
if (err) {
|
||||
qWarning() << "Failed to move file:" << QString::fromStdString(err.message());
|
||||
qDebug() << "Source file:" << source;
|
||||
qDebug() << "Destination file:" << dest;
|
||||
if (err.value() != 0) {
|
||||
if (moveByCopy(source, dest))
|
||||
return true;
|
||||
qDebug() << "Move of" << source << "to" << dest << "failed!";
|
||||
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value());
|
||||
return false;
|
||||
}
|
||||
|
||||
return err.value() == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deletePath(QString path)
|
||||
|
@ -240,6 +240,7 @@ class create_link : public QObject {
|
||||
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
|
||||
|
||||
int totalLinked() { return m_linked; }
|
||||
int totalToLink() { return static_cast<int>(m_links_to_make.size()); }
|
||||
|
||||
void runPrivileged() { runPrivileged(QString()); }
|
||||
void runPrivileged(const QString& offset);
|
||||
@ -378,6 +379,7 @@ enum class FilesystemType {
|
||||
HFSX,
|
||||
FUSEBLK,
|
||||
F2FS,
|
||||
BCACHEFS,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
@ -406,6 +408,7 @@ static const QMap<FilesystemType, QStringList> s_filesystem_type_names = { { Fil
|
||||
{ FilesystemType::HFSX, { "HFSX" } },
|
||||
{ FilesystemType::FUSEBLK, { "FUSEBLK" } },
|
||||
{ FilesystemType::F2FS, { "F2FS" } },
|
||||
{ FilesystemType::BCACHEFS, { "BCACHEFS" } },
|
||||
{ FilesystemType::UNKNOWN, { "UNKNOWN" } } };
|
||||
|
||||
/**
|
||||
@ -458,7 +461,7 @@ QString nearestExistentAncestor(const QString& path);
|
||||
FilesystemInfo statFS(const QString& path);
|
||||
|
||||
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
|
||||
|
@ -1,10 +1,12 @@
|
||||
#include "InstanceCopyTask.h"
|
||||
#include <QDebug>
|
||||
#include <QtConcurrentRun>
|
||||
#include <memory>
|
||||
#include "FileSystem.h"
|
||||
#include "NullInstance.h"
|
||||
#include "pathmatcher/RegexpMatcher.h"
|
||||
#include "settings/INISettingsObject.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
|
||||
{
|
||||
@ -38,38 +40,50 @@ void InstanceCopyTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
|
||||
|
||||
auto copySaves = [&]() {
|
||||
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
|
||||
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
|
||||
QString staging_mc_dir;
|
||||
if (dotMCDir.exists() && !mcDir.exists())
|
||||
staging_mc_dir = dotMCDir.filePath();
|
||||
else
|
||||
staging_mc_dir = mcDir.filePath();
|
||||
|
||||
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
|
||||
savesCopy.followSymlinks(true);
|
||||
|
||||
return savesCopy();
|
||||
};
|
||||
|
||||
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, 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();
|
||||
} else if (m_useLinks || m_useHardLinks) {
|
||||
}
|
||||
if (m_useLinks || m_useHardLinks) {
|
||||
std::unique_ptr<FS::copy> savesCopy;
|
||||
if (m_copySaves) {
|
||||
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
|
||||
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
|
||||
QString staging_mc_dir;
|
||||
if (dotMCDir.exists() && !mcDir.exists())
|
||||
staging_mc_dir = dotMCDir.filePath();
|
||||
else
|
||||
staging_mc_dir = mcDir.filePath();
|
||||
|
||||
savesCopy = std::make_unique<FS::copy>(FS::PathCombine(m_origInstance->gameRoot(), "saves"),
|
||||
FS::PathCombine(staging_mc_dir, "saves"));
|
||||
savesCopy->followSymlinks(true);
|
||||
(*savesCopy)(true);
|
||||
setProgress(0, savesCopy->totalCopied());
|
||||
connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
|
||||
}
|
||||
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
|
||||
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;
|
||||
|
||||
if (!folderLink()) {
|
||||
#if defined Q_OS_WIN32
|
||||
if (!m_useHardLinks) {
|
||||
setProgress(0, m_progressTotal);
|
||||
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
|
||||
|
||||
qDebug() << "attempting to run with privelage";
|
||||
@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_copySaves) {
|
||||
there_were_errors |= !copySaves();
|
||||
if (savesCopy) {
|
||||
there_were_errors |= !(*savesCopy)();
|
||||
}
|
||||
|
||||
return got_priv_results && !there_were_errors;
|
||||
} else {
|
||||
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
|
||||
}
|
||||
#else
|
||||
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
|
||||
@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_copySaves) {
|
||||
there_were_errors |= !copySaves();
|
||||
if (savesCopy) {
|
||||
there_were_errors |= !(*savesCopy)();
|
||||
}
|
||||
|
||||
return !there_were_errors;
|
||||
} else {
|
||||
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
|
||||
folderCopy.followSymlinks(false).matcher(m_matcher.get());
|
||||
|
||||
return folderCopy();
|
||||
}
|
||||
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
|
||||
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();
|
||||
});
|
||||
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
|
||||
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
|
||||
@ -170,3 +184,14 @@ void InstanceCopyTask::copyAborted()
|
||||
emitFailed(tr("Instance folder copy has been aborted."));
|
||||
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;
|
||||
}
|
@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask {
|
||||
protected:
|
||||
//! Entry point for tasks.
|
||||
virtual void executeTask() override;
|
||||
bool abort() override;
|
||||
void copyFinished();
|
||||
void copyAborted();
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include "FileSystem.h"
|
||||
|
||||
void InstanceCreationTask::executeTask()
|
||||
{
|
||||
@ -45,7 +46,7 @@ void InstanceCreationTask::executeTask()
|
||||
if (!QFile::exists(path))
|
||||
continue;
|
||||
qDebug() << "Removing" << path;
|
||||
if (!QFile::remove(path)) {
|
||||
if (!FS::deletePath(path)) {
|
||||
qCritical() << "Couldn't remove the old conflicting files.";
|
||||
emitFailed(tr("Failed to remove old conflicting files."));
|
||||
return;
|
||||
|
@ -56,6 +56,7 @@
|
||||
|
||||
#include <QtConcurrentRun>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include <quazip/quazipdir.h>
|
||||
|
||||
@ -68,15 +69,8 @@ bool InstanceImportTask::abort()
|
||||
if (!canAbort())
|
||||
return false;
|
||||
|
||||
if (m_filesNetJob)
|
||||
m_filesNetJob->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();
|
||||
}
|
||||
|
||||
if (task)
|
||||
task->abort();
|
||||
return Task::abort();
|
||||
}
|
||||
|
||||
@ -89,7 +83,6 @@ void InstanceImportTask::executeTask()
|
||||
processZipPack();
|
||||
} else {
|
||||
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
|
||||
m_downloadRequired = true;
|
||||
|
||||
downloadFromUrl();
|
||||
}
|
||||
@ -97,115 +90,132 @@ void InstanceImportTask::executeTask()
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
|
||||
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
|
||||
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
|
||||
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
|
||||
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
|
||||
m_filesNetJob->start();
|
||||
auto filesNetJob = makeShared<NetJob>(tr("Modpack download"), APPLICATION->network());
|
||||
filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
|
||||
|
||||
connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack);
|
||||
connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress);
|
||||
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();
|
||||
m_filesNetJob.reset();
|
||||
}
|
||||
if (!isRunning()) {
|
||||
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)
|
||||
{
|
||||
emitFailed(reason);
|
||||
m_filesNetJob.reset();
|
||||
}
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
|
||||
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
|
||||
{
|
||||
setProgress(current, total);
|
||||
}
|
||||
// Recurse the search to non-ignored subfolders
|
||||
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
|
||||
if ("overrides/" == fileName)
|
||||
continue;
|
||||
|
||||
void InstanceImportTask::downloadAborted()
|
||||
{
|
||||
emitAborted();
|
||||
m_filesNetJob.reset();
|
||||
QString result = getRootFromZip(zip, root + fileName);
|
||||
if (!result.isEmpty())
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void InstanceImportTask::processZipPack()
|
||||
{
|
||||
setStatus(tr("Extracting modpack"));
|
||||
setStatus(tr("Attempting to determine instance type"));
|
||||
QDir extractDir(m_stagingPath);
|
||||
qDebug() << "Attempting to create instance from" << m_archivePath;
|
||||
|
||||
// open the zip and find relevant files in it
|
||||
m_packZip.reset(new QuaZip(m_archivePath));
|
||||
if (!m_packZip->open(QuaZip::mdUnzip)) {
|
||||
auto packZip = std::make_shared<QuaZip>(m_archivePath);
|
||||
if (!packZip->open(QuaZip::mdUnzip)) {
|
||||
emitFailed(tr("Unable to open supplied modpack zip file."));
|
||||
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;
|
||||
|
||||
// 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
|
||||
if (modrinthFound) {
|
||||
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
|
||||
if (packZipDir.exists("/modrinth.index.json")) {
|
||||
// process as Modrinth pack
|
||||
qDebug() << "Modrinth:" << modrinthFound;
|
||||
qDebug() << "Modrinth:" << true;
|
||||
m_modpackType = ModpackType::Modrinth;
|
||||
} else if (technicFound) {
|
||||
} else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
|
||||
// process as Technic pack
|
||||
qDebug() << "Technic:" << technicFound;
|
||||
qDebug() << "Technic:" << true;
|
||||
extractDir.mkpath("minecraft");
|
||||
extractDir.cd("minecraft");
|
||||
m_modpackType = ModpackType::Technic;
|
||||
} else {
|
||||
QStringList paths_to_ignore{ "overrides/" };
|
||||
|
||||
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;
|
||||
}
|
||||
root = getRootFromZip(packZip.get());
|
||||
setDetails("");
|
||||
}
|
||||
if (m_modpackType == ModpackType::Unknown) {
|
||||
emitFailed(tr("Archive does not contain a recognized modpack type."));
|
||||
return;
|
||||
}
|
||||
setStatus(tr("Extracting modpack"));
|
||||
|
||||
// make sure we extract just the pack
|
||||
m_extractFuture =
|
||||
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
|
||||
m_extractFutureWatcher.setFuture(m_extractFuture);
|
||||
auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
|
||||
|
||||
auto progressStep = std::make_shared<TaskStepProgress>();
|
||||
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()
|
||||
{
|
||||
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);
|
||||
|
||||
qDebug() << "Fixing permissions for extracted pack files...";
|
||||
@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC()
|
||||
m_instIcon = instance.iconKey();
|
||||
|
||||
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)) {
|
||||
// import icon
|
||||
auto iconList = APPLICATION->icons();
|
||||
if (iconList->iconFileExists(m_instIcon)) {
|
||||
iconList->deleteIcon(m_instIcon);
|
||||
}
|
||||
iconList->installIcons({ importIconPath });
|
||||
iconList->installIcon(importIconPath, m_instIcon);
|
||||
}
|
||||
}
|
||||
emitSucceeded();
|
||||
|
@ -39,11 +39,8 @@
|
||||
#include <QFutureWatcher>
|
||||
#include <QUrl>
|
||||
#include "InstanceTask.h"
|
||||
#include "QObjectPtr.h"
|
||||
#include "modplatform/flame/PackManifest.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "settings/SettingsObject.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class QuaZip;
|
||||
@ -54,35 +51,26 @@ class InstanceImportTask : public InstanceTask {
|
||||
explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
|
||||
|
||||
bool abort() override;
|
||||
const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; }
|
||||
|
||||
protected:
|
||||
//! Entry point for tasks.
|
||||
virtual void executeTask() override;
|
||||
|
||||
private:
|
||||
void processZipPack();
|
||||
void processMultiMC();
|
||||
void processTechnic();
|
||||
void processFlame();
|
||||
void processModrinth();
|
||||
QString getRootFromZip(QuaZip* zip, const QString& root = "");
|
||||
|
||||
private slots:
|
||||
void downloadSucceeded();
|
||||
void downloadFailed(QString reason);
|
||||
void downloadProgressChanged(qint64 current, qint64 total);
|
||||
void downloadAborted();
|
||||
void processZipPack();
|
||||
void extractFinished();
|
||||
|
||||
private: /* data */
|
||||
NetJob::Ptr m_filesNetJob;
|
||||
QUrl m_sourceUrl;
|
||||
QString m_archivePath;
|
||||
bool m_downloadRequired = false;
|
||||
std::unique_ptr<QuaZip> m_packZip;
|
||||
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||
QVector<Flame::File> m_blockedMods;
|
||||
Task::Ptr task;
|
||||
enum class ModpackType {
|
||||
Unknown,
|
||||
MultiMC,
|
||||
|
@ -972,7 +972,6 @@ bool InstanceList::commitStagedInstance(const QString& path,
|
||||
if (groupName.isEmpty() && !groupName.isNull())
|
||||
groupName = QString();
|
||||
|
||||
QDir dir;
|
||||
QString instID;
|
||||
InstancePtr inst;
|
||||
|
||||
@ -996,7 +995,7 @@ bool InstanceList::commitStagedInstance(const QString& path,
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!dir.rename(path, destination)) {
|
||||
if (!FS::move(path, destination)) {
|
||||
qWarning() << "Failed to move" << path << "to" << destination;
|
||||
return false;
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ void LaunchController::launchInstance()
|
||||
online_mode = "online";
|
||||
|
||||
// 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 = "";
|
||||
QHostInfo host_info;
|
||||
|
||||
|
@ -42,6 +42,7 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
#if defined(LAUNCHER_APPLICATION)
|
||||
@ -122,7 +123,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
|
||||
zip.setUtf8Enabled(true);
|
||||
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
|
||||
if (!zip.open(QuaZip::mdCreate)) {
|
||||
QFile::remove(fileCompressed);
|
||||
FS::deletePath(fileCompressed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -130,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
|
||||
|
||||
zip.close();
|
||||
if (zip.getZipError() != 0) {
|
||||
QFile::remove(fileCompressed);
|
||||
FS::deletePath(fileCompressed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -144,7 +145,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
QuaZip zipOut(targetJarPath);
|
||||
zipOut.setUtf8Enabled(true);
|
||||
if (!zipOut.open(QuaZip::mdCreate)) {
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to open the minecraft.jar for modding";
|
||||
return false;
|
||||
}
|
||||
@ -162,7 +163,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
if (mod->type() == ResourceType::ZIPFILE) {
|
||||
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
@ -171,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
auto filename = mod->fileinfo();
|
||||
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
@ -194,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
|
||||
if (!compressDirFiles(&zipOut, parent_dir, files)) {
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
|
||||
return false;
|
||||
}
|
||||
@ -202,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
} else {
|
||||
// Make sure we do not continue launching when something is missing or undefined...
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
|
||||
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"); })) {
|
||||
zipOut.close();
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to insert minecraft.jar contents.";
|
||||
return false;
|
||||
}
|
||||
@ -218,7 +219,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
|
||||
// Recompress the jar
|
||||
zipOut.close();
|
||||
if (zipOut.getZipError() != 0) {
|
||||
QFile::remove(targetJarPath);
|
||||
FS::deletePath(targetJarPath);
|
||||
qCritical() << "Failed to finalize minecraft.jar!";
|
||||
return false;
|
||||
}
|
||||
@ -332,9 +333,20 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
|
||||
}
|
||||
|
||||
extracted.append(target_file_path);
|
||||
QFile::setPermissions(target_file_path,
|
||||
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
|
||||
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)) {
|
||||
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
|
||||
} while (zip->goToNextFile());
|
||||
|
||||
@ -492,10 +504,10 @@ auto ExportToZipTask::exportZip() -> ZipResult
|
||||
void ExportToZipTask::finish()
|
||||
{
|
||||
if (m_build_zip_future.isCanceled()) {
|
||||
QFile::remove(m_output_path);
|
||||
FS::deletePath(m_output_path);
|
||||
emitAborted();
|
||||
} 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());
|
||||
} else {
|
||||
emitSucceeded();
|
||||
@ -512,6 +524,123 @@ bool ExportToZipTask::abort()
|
||||
}
|
||||
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
|
||||
|
@ -205,5 +205,30 @@ class ExportToZipTask : public Task {
|
||||
QFuture<ZipResult> m_build_zip_future;
|
||||
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
|
||||
} // namespace MMCZip
|
||||
|
@ -1,52 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SkinUtils.h"
|
||||
#include "Application.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPainter>
|
||||
|
||||
namespace SkinUtils {
|
||||
/*
|
||||
* Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise
|
||||
*/
|
||||
QPixmap getFaceFromCache(QString username, int height, int width)
|
||||
{
|
||||
QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath());
|
||||
|
||||
if (fskin.exists()) {
|
||||
QPixmap skinTexture(fskin.fileName());
|
||||
if (!skinTexture.isNull()) {
|
||||
QPixmap skin = QPixmap(8, 8);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
skin.fill(QColorConstants::Transparent);
|
||||
#else
|
||||
skin.fill(QColor(0, 0, 0, 0));
|
||||
#endif
|
||||
QPainter painter(&skin);
|
||||
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
|
||||
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
|
||||
return skin.scaled(height, width, Qt::KeepAspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
return QPixmap();
|
||||
}
|
||||
} // namespace SkinUtils
|
@ -1,22 +0,0 @@
|
||||
/* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
namespace SkinUtils {
|
||||
QPixmap getFaceFromCache(QString id, int height = 64, int width = 64);
|
||||
}
|
@ -212,3 +212,25 @@ QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegular
|
||||
right = s.mid(end);
|
||||
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;
|
||||
}
|
@ -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, const QRegularExpression& re);
|
||||
|
||||
QString htmlListPatch(QString htmlStr);
|
||||
|
||||
} // namespace StringUtils
|
||||
|
@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const
|
||||
|
||||
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)
|
||||
|
@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey)
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
auto fileInfo = it.fileInfo();
|
||||
|
||||
if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix()))
|
||||
if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix()))
|
||||
return fileInfo.absoluteFilePath();
|
||||
}
|
||||
return {};
|
||||
|
@ -207,7 +207,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
|
||||
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
|
||||
|
||||
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) {
|
||||
// Read the JavaHome value to find where Java is installed.
|
||||
DWORD valueSz = 0;
|
||||
@ -283,6 +283,12 @@ QList<QString> JavaUtils::FindJavaPaths()
|
||||
QList<JavaInstallPtr> ADOPTIUMJDK64s =
|
||||
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
|
||||
QList<JavaInstallPtr> MICROSOFTJDK64s =
|
||||
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(ADOPTOPENJRE64s);
|
||||
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/jre7/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(FOUNDATIONJDK64s);
|
||||
java_candidates.append(ADOPTIUMJDK64s);
|
||||
java_candidates.append(SEMERUJDK64s);
|
||||
java_candidates.append(MICROSOFTJDK64s);
|
||||
java_candidates.append(ZULU64s);
|
||||
java_candidates.append(LIBERICA64s);
|
||||
@ -316,6 +324,7 @@ QList<QString> JavaUtils::FindJavaPaths()
|
||||
java_candidates.append(NEWJRE32s);
|
||||
java_candidates.append(ADOPTOPENJRE32s);
|
||||
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/jre7/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(FOUNDATIONJDK32s);
|
||||
java_candidates.append(ADOPTIUMJDK32s);
|
||||
java_candidates.append(SEMERUJDK32s);
|
||||
java_candidates.append(ZULU32s);
|
||||
java_candidates.append(LIBERICA32s);
|
||||
|
||||
@ -410,6 +420,7 @@ QList<QString> JavaUtils::FindJavaPaths()
|
||||
// manually installed JDKs in /opt
|
||||
scanJavaDirs("/opt/jdk");
|
||||
scanJavaDirs("/opt/jdks");
|
||||
scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
|
||||
// flatpak
|
||||
scanJavaDirs("/app/jdk");
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
#include "BaseEntity.h"
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
#include "net/ApiDownload.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
@ -83,8 +84,7 @@ bool Meta::BaseEntity::loadLocalFile()
|
||||
} catch (const Exception& e) {
|
||||
qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
|
||||
// just make sure it's gone and we never consider it again.
|
||||
QFile::remove(fname);
|
||||
return false;
|
||||
return !FS::deletePath(fname);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,7 +336,7 @@ bool Component::revert()
|
||||
bool result = true;
|
||||
// just kill the file and reload
|
||||
if (QFile::exists(filename)) {
|
||||
result = QFile::remove(filename);
|
||||
result = FS::deletePath(filename);
|
||||
}
|
||||
if (result) {
|
||||
// file gone...
|
||||
|
@ -1000,7 +1000,7 @@ QString MinecraftInstance::getStatusbarDescription()
|
||||
QString description;
|
||||
description.append(tr("Minecraft %1").arg(mcVersion));
|
||||
if (m_settings->get("ShowGameTime").toBool()) {
|
||||
if (lastTimePlayed() > 0) {
|
||||
if (lastTimePlayed() > 0 && lastLaunch() > 0) {
|
||||
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
|
||||
description.append(
|
||||
tr(", last played on %1 for %2")
|
||||
|
@ -839,7 +839,7 @@ bool PackProfile::installCustomJar_internal(QString filepath)
|
||||
|
||||
QFileInfo jarInfo(finalPath);
|
||||
if (jarInfo.exists()) {
|
||||
if (!QFile::remove(finalPath)) {
|
||||
if (!FS::deletePath(finalPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -206,8 +206,8 @@ int64_t calculateWorldSize(const QFileInfo& file)
|
||||
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
|
||||
int64_t total = 0;
|
||||
while (it.hasNext()) {
|
||||
total += it.fileInfo().size();
|
||||
it.next();
|
||||
total += it.fileInfo().size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
@ -347,7 +347,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
|
||||
Skin skinOut;
|
||||
// fill in default skin info ourselves, as this endpoint doesn't provide it
|
||||
bool steve = isDefaultModelSteve(output.id);
|
||||
skinOut.variant = steve ? "classic" : "slim";
|
||||
skinOut.variant = steve ? "CLASSIC" : "SLIM";
|
||||
skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX;
|
||||
// sadly we can't figure this out, but I don't think it really matters...
|
||||
skinOut.id = "00000000-0000-0000-0000-000000000000";
|
||||
|
@ -65,29 +65,24 @@ std::pair<Version, Version> DataPack::compatibleVersions() const
|
||||
return s_pack_format_versions.constFind(m_pack_format).value();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
switch (type) {
|
||||
default: {
|
||||
auto res = Resource::compare(other, type);
|
||||
if (res.first != 0)
|
||||
return res;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return Resource::compare(other, type);
|
||||
case SortType::PACK_FORMAT: {
|
||||
auto this_ver = packFormat();
|
||||
auto other_ver = cast_other.packFormat();
|
||||
|
||||
if (this_ver > other_ver)
|
||||
return { 1, type == SortType::PACK_FORMAT };
|
||||
return 1;
|
||||
if (this_ver < other_ver)
|
||||
return { -1, type == SortType::PACK_FORMAT };
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { 0, false };
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool DataPack::applyFilter(QRegularExpression filter) const
|
||||
|
@ -56,7 +56,7 @@ class DataPack : public Resource {
|
||||
|
||||
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;
|
||||
|
||||
protected:
|
||||
|
@ -45,6 +45,7 @@
|
||||
#include "MetadataHandler.h"
|
||||
#include "Version.h"
|
||||
#include "minecraft/mod/ModDetails.h"
|
||||
#include "minecraft/mod/Resource.h"
|
||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||
|
||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||
@ -77,7 +78,7 @@ void Mod::setDetails(const ModDetails& 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);
|
||||
if (!cast_other)
|
||||
@ -87,30 +88,23 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
|
||||
default:
|
||||
case SortType::ENABLED:
|
||||
case SortType::NAME:
|
||||
case SortType::DATE: {
|
||||
auto res = Resource::compare(other, type);
|
||||
if (res.first != 0)
|
||||
return res;
|
||||
break;
|
||||
}
|
||||
case SortType::DATE:
|
||||
case SortType::SIZE:
|
||||
return Resource::compare(other, type);
|
||||
case SortType::VERSION: {
|
||||
auto this_ver = Version(version());
|
||||
auto other_ver = Version(cast_other->version());
|
||||
if (this_ver > other_ver)
|
||||
return { 1, type == SortType::VERSION };
|
||||
return 1;
|
||||
if (this_ver < other_ver)
|
||||
return { -1, type == SortType::VERSION };
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
case SortType::PROVIDER: {
|
||||
auto compare_result =
|
||||
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 QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
|
||||
}
|
||||
}
|
||||
return { 0, false };
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Mod::applyFilter(QRegularExpression filter) const
|
||||
|
@ -88,7 +88,7 @@ class Mod : public Resource {
|
||||
|
||||
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;
|
||||
|
||||
// Delete all the files of this mod
|
||||
|
@ -52,6 +52,8 @@
|
||||
#include "Application.h"
|
||||
|
||||
#include "Json.h"
|
||||
#include "StringUtils.h"
|
||||
#include "minecraft/mod/Resource.h"
|
||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
||||
@ -62,12 +64,14 @@
|
||||
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)
|
||||
{
|
||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" });
|
||||
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") });
|
||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER };
|
||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
|
||||
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"), tr("Size") });
|
||||
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 };
|
||||
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
|
||||
@ -105,12 +109,14 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
|
||||
|
||||
return provider.value();
|
||||
}
|
||||
case SizeColumn:
|
||||
return m_resources[row]->sizeStr();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
if (column == NAME_COLUMN) {
|
||||
if (column == NameColumn) {
|
||||
if (at(row)->isSymLinkUnder(instDirPath())) {
|
||||
return m_resources[row]->internal_id() +
|
||||
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();
|
||||
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");
|
||||
if (column == ImageColumn) {
|
||||
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 ProviderColumn:
|
||||
case ImageColumn:
|
||||
case SizeColumn:
|
||||
return columnNames().at(section);
|
||||
default:
|
||||
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).");
|
||||
case ProviderColumn:
|
||||
return tr("Where the mod was downloaded from.");
|
||||
case SizeColumn:
|
||||
return tr("The size of the mod.");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class QFileSystemWatcher;
|
||||
class ModFolderModel : public ResourceFolderModel {
|
||||
Q_OBJECT
|
||||
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 };
|
||||
ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
#include "Resource.h"
|
||||
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <tuple>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
Resource::Resource(QObject* parent) : QObject(parent) {}
|
||||
|
||||
@ -18,6 +21,20 @@ void Resource::setFile(QFileInfo file_info)
|
||||
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()
|
||||
{
|
||||
QString file_name{ m_file_info.fileName() };
|
||||
@ -26,6 +43,7 @@ void Resource::parseFile()
|
||||
|
||||
m_internal_id = file_name;
|
||||
|
||||
std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info);
|
||||
if (m_file_info.isDir()) {
|
||||
m_type = ResourceType::FOLDER;
|
||||
m_name = file_name;
|
||||
@ -61,15 +79,15 @@ static void removeThePrefix(QString& string)
|
||||
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) {
|
||||
default:
|
||||
case SortType::ENABLED:
|
||||
if (enabled() && !other.enabled())
|
||||
return { 1, type == SortType::ENABLED };
|
||||
return 1;
|
||||
if (!enabled() && other.enabled())
|
||||
return { -1, type == SortType::ENABLED };
|
||||
return -1;
|
||||
break;
|
||||
case SortType::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(other_name);
|
||||
|
||||
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
|
||||
if (compare_result != 0)
|
||||
return { compare_result, type == SortType::NAME };
|
||||
break;
|
||||
return QString::compare(this_name, other_name, Qt::CaseInsensitive);
|
||||
}
|
||||
case SortType::DATE:
|
||||
if (dateTimeChanged() > other.dateTimeChanged())
|
||||
return { 1, type == SortType::DATE };
|
||||
return 1;
|
||||
if (dateTimeChanged() < other.dateTimeChanged())
|
||||
return { -1, type == SortType::DATE };
|
||||
return -1;
|
||||
break;
|
||||
case SortType::SIZE: {
|
||||
if (this->type() != other.type()) {
|
||||
if (this->type() == ResourceType::FOLDER)
|
||||
return -1;
|
||||
if (other.type() == ResourceType::FOLDER)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (sizeInfo() > other.sizeInfo())
|
||||
return 1;
|
||||
if (sizeInfo() < other.sizeInfo())
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { 0, false };
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Resource::applyFilter(QRegularExpression filter) const
|
||||
|
@ -15,7 +15,7 @@ enum class ResourceType {
|
||||
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 };
|
||||
|
||||
@ -45,6 +45,8 @@ class Resource : public QObject {
|
||||
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
|
||||
[[nodiscard]] auto type() const -> ResourceType { return m_type; }
|
||||
[[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 bool valid() const { return m_type != ResourceType::UNKNOWN; }
|
||||
@ -53,10 +55,8 @@ class Resource : public QObject {
|
||||
* > 0: 'this' comes after 'other'
|
||||
* = 0: 'this' is equal to '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),
|
||||
* or if such filter includes the Resource (true).
|
||||
@ -117,4 +117,6 @@ class Resource : public QObject {
|
||||
bool m_is_resolving = false;
|
||||
bool m_is_resolved = false;
|
||||
int m_resolution_ticket = 0;
|
||||
QString m_size_str;
|
||||
qint64 m_size_info;
|
||||
};
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "FileSystem.h"
|
||||
|
||||
#include "QVariantUtils.h"
|
||||
#include "StringUtils.h"
|
||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||
|
||||
#include "settings/Setting.h"
|
||||
@ -111,7 +112,7 @@ bool ResourceFolderModel::installResource(QString original_path)
|
||||
case ResourceType::ZIPFILE:
|
||||
case ResourceType::LITEMOD: {
|
||||
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!";
|
||||
return false;
|
||||
}
|
||||
@ -416,15 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
switch (column) {
|
||||
case NAME_COLUMN:
|
||||
case NameColumn:
|
||||
return m_resources[row]->name();
|
||||
case DATE_COLUMN:
|
||||
case DateColumn:
|
||||
return m_resources[row]->dateTimeChanged();
|
||||
case SizeColumn:
|
||||
return m_resources[row]->sizeStr();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
case Qt::ToolTipRole:
|
||||
if (column == NAME_COLUMN) {
|
||||
if (column == NameColumn) {
|
||||
if (at(row).isSymLinkUnder(instDirPath())) {
|
||||
return m_resources[row]->internal_id() +
|
||||
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();
|
||||
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 {};
|
||||
}
|
||||
case Qt::CheckStateRole:
|
||||
switch (column) {
|
||||
case ACTIVE_COLUMN:
|
||||
case ActiveColumn:
|
||||
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
default:
|
||||
return {};
|
||||
@ -486,24 +489,27 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
switch (section) {
|
||||
case ACTIVE_COLUMN:
|
||||
case NAME_COLUMN:
|
||||
case DATE_COLUMN:
|
||||
case ActiveColumn:
|
||||
case NameColumn:
|
||||
case DateColumn:
|
||||
case SizeColumn:
|
||||
return columnNames().at(section);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
case Qt::ToolTipRole: {
|
||||
switch (section) {
|
||||
case ACTIVE_COLUMN:
|
||||
case ActiveColumn:
|
||||
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||
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.
|
||||
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.
|
||||
return tr("The date and time this resource was last changed (or added).");
|
||||
case SizeColumn:
|
||||
return tr("The size of the resource.");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@ -610,12 +616,10 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
|
||||
auto const& resource_right = model->at(source_right.row());
|
||||
|
||||
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);
|
||||
|
||||
if (compare_result.second || sortOrder() != Qt::DescendingOrder)
|
||||
return (compare_result.first < 0);
|
||||
return (compare_result.first > 0);
|
||||
return compare_result < 0;
|
||||
}
|
||||
|
||||
QString ResourceFolderModel::instDirPath() const
|
||||
|
@ -96,7 +96,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
||||
/* Qt behavior */
|
||||
|
||||
/* 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; }
|
||||
|
||||
[[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:
|
||||
// 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!
|
||||
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
|
||||
QStringList m_column_names = { "Enable", "Name", "Last Modified" };
|
||||
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") };
|
||||
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive };
|
||||
QList<bool> m_columnsHideable = { false, false, true };
|
||||
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE };
|
||||
QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" };
|
||||
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,
|
||||
QHeaderView::Interactive };
|
||||
QList<bool> m_columnsHideable = { false, false, true, true };
|
||||
|
||||
QDir m_dir;
|
||||
BaseInstance* m_instance;
|
||||
|
@ -94,29 +94,24 @@ std::pair<Version, Version> ResourcePack::compatibleVersions() const
|
||||
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);
|
||||
|
||||
switch (type) {
|
||||
default: {
|
||||
auto res = Resource::compare(other, type);
|
||||
if (res.first != 0)
|
||||
return res;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return Resource::compare(other, type);
|
||||
case SortType::PACK_FORMAT: {
|
||||
auto this_ver = packFormat();
|
||||
auto other_ver = cast_other.packFormat();
|
||||
|
||||
if (this_ver > other_ver)
|
||||
return { 1, type == SortType::PACK_FORMAT };
|
||||
return 1;
|
||||
if (this_ver < other_ver)
|
||||
return { -1, type == SortType::PACK_FORMAT };
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { 0, false };
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ResourcePack::applyFilter(QRegularExpression filter) const
|
||||
|
@ -44,7 +44,7 @@ class ResourcePack : public Resource {
|
||||
|
||||
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;
|
||||
|
||||
protected:
|
||||
|
@ -42,19 +42,21 @@
|
||||
#include <QStyle>
|
||||
|
||||
#include "Application.h"
|
||||
#include "StringUtils.h"
|
||||
#include "Version.h"
|
||||
|
||||
#include "minecraft/mod/Resource.h"
|
||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
|
||||
|
||||
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_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
|
||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
|
||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
||||
QHeaderView::Interactive };
|
||||
m_columnsHideable = { false, true, false, true, true };
|
||||
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"), tr("Size") });
|
||||
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, QHeaderView::Interactive, QHeaderView::Interactive };
|
||||
m_columnsHideable = { false, true, false, true, true, true };
|
||||
}
|
||||
|
||||
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
||||
@ -85,6 +87,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
||||
}
|
||||
case DateColumn:
|
||||
return m_resources[row]->dateTimeChanged();
|
||||
case SizeColumn:
|
||||
return m_resources[row]->sizeStr();
|
||||
|
||||
default:
|
||||
return {};
|
||||
@ -144,6 +148,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
|
||||
case PackFormatColumn:
|
||||
case DateColumn:
|
||||
case ImageColumn:
|
||||
case SizeColumn:
|
||||
return columnNames().at(section);
|
||||
default:
|
||||
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.");
|
||||
case DateColumn:
|
||||
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:
|
||||
return {};
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
class ResourcePackFolderModel : public ResourceFolderModel {
|
||||
Q_OBJECT
|
||||
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);
|
||||
|
||||
|
@ -37,6 +37,7 @@
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
#include "StringUtils.h"
|
||||
#include "TexturePackFolderModel.h"
|
||||
|
||||
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||
@ -44,11 +45,12 @@
|
||||
|
||||
TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
|
||||
{
|
||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" });
|
||||
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") });
|
||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE };
|
||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive };
|
||||
m_columnsHideable = { false, true, false, true };
|
||||
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Size" });
|
||||
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, SortType::SIZE };
|
||||
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
|
||||
QHeaderView::Interactive };
|
||||
m_columnsHideable = { false, true, false, true, true };
|
||||
}
|
||||
|
||||
Task* TexturePackFolderModel::createUpdateTask()
|
||||
@ -76,6 +78,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
|
||||
return m_resources[row]->name();
|
||||
case DateColumn:
|
||||
return m_resources[row]->dateTimeChanged();
|
||||
case SizeColumn:
|
||||
return m_resources[row]->sizeStr();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@ -127,6 +131,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
|
||||
case NameColumn:
|
||||
case DateColumn:
|
||||
case ImageColumn:
|
||||
case SizeColumn:
|
||||
return columnNames().at(section);
|
||||
default:
|
||||
return {};
|
||||
@ -135,13 +140,15 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
|
||||
switch (section) {
|
||||
case ActiveColumn:
|
||||
//: 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:
|
||||
//: 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:
|
||||
//: 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:
|
||||
return {};
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class TexturePackFolderModel : public ResourceFolderModel {
|
||||
Q_OBJECT
|
||||
|
||||
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);
|
||||
|
||||
|
@ -178,6 +178,88 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
|
||||
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
|
||||
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", {});
|
||||
|
||||
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) {
|
||||
qWarning() << "JsonException: " << e.what() << e.cause();
|
||||
return false;
|
||||
|
@ -34,6 +34,7 @@ bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
|
||||
bool processZIP(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 processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);
|
||||
|
||||
|
@ -1,121 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "CapeChange.h"
|
||||
|
||||
#include <QHttpMultiPart>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {}
|
||||
|
||||
void CapeChange::setCape([[maybe_unused]] QString& cape)
|
||||
{
|
||||
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
|
||||
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
|
||||
QNetworkReply* rep = APPLICATION->network()->put(request, requestString.toUtf8());
|
||||
|
||||
setStatus(tr("Equipping cape"));
|
||||
|
||||
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
|
||||
connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
|
||||
#else
|
||||
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
|
||||
#endif
|
||||
connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
|
||||
connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
|
||||
}
|
||||
|
||||
void CapeChange::clearCape()
|
||||
{
|
||||
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
|
||||
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
|
||||
QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
|
||||
|
||||
setStatus(tr("Removing cape"));
|
||||
|
||||
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
|
||||
connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError);
|
||||
#else
|
||||
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError);
|
||||
#endif
|
||||
connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors);
|
||||
connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished);
|
||||
}
|
||||
|
||||
void CapeChange::executeTask()
|
||||
{
|
||||
if (m_capeId.isEmpty()) {
|
||||
clearCape();
|
||||
} else {
|
||||
setCape(m_capeId);
|
||||
}
|
||||
}
|
||||
|
||||
void CapeChange::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
qCritical() << "Network error: " << error;
|
||||
emitFailed(m_reply->errorString());
|
||||
}
|
||||
|
||||
void CapeChange::sslErrors(const QList<QSslError>& errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors) {
|
||||
qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void CapeChange::downloadFinished()
|
||||
{
|
||||
// if the download failed
|
||||
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
|
||||
m_reply.reset();
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFile>
|
||||
#include <QtNetwork/QtNetwork>
|
||||
#include <memory>
|
||||
#include "QObjectPtr.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class CapeChange : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
CapeChange(QObject* parent, QString token, QString capeId);
|
||||
virtual ~CapeChange() {}
|
||||
|
||||
private:
|
||||
void setCape(QString& cape);
|
||||
void clearCape();
|
||||
|
||||
private:
|
||||
QString m_capeId;
|
||||
QString m_token;
|
||||
shared_qobject_ptr<QNetworkReply> m_reply;
|
||||
|
||||
protected:
|
||||
virtual void executeTask();
|
||||
|
||||
public slots:
|
||||
void downloadError(QNetworkReply::NetworkError);
|
||||
void sslErrors(const QList<QSslError>& errors);
|
||||
void downloadFinished();
|
||||
};
|
@ -1,90 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SkinDelete.h"
|
||||
|
||||
#include <QHttpMultiPart>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {}
|
||||
|
||||
void SkinDelete::executeTask()
|
||||
{
|
||||
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
|
||||
QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
|
||||
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
|
||||
|
||||
setStatus(tr("Deleting skin"));
|
||||
connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError);
|
||||
#else
|
||||
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinDelete::downloadError);
|
||||
#endif
|
||||
connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors);
|
||||
connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished);
|
||||
}
|
||||
|
||||
void SkinDelete::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
qCritical() << "Network error: " << error;
|
||||
emitFailed(m_reply->errorString());
|
||||
}
|
||||
|
||||
void SkinDelete::sslErrors(const QList<QSslError>& errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors) {
|
||||
qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void SkinDelete::downloadFinished()
|
||||
{
|
||||
// if the download failed
|
||||
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
|
||||
m_reply.reset();
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFile>
|
||||
#include <QtNetwork/QtNetwork>
|
||||
#include "tasks/Task.h"
|
||||
|
||||
using SkinDeletePtr = shared_qobject_ptr<class SkinDelete>;
|
||||
|
||||
class SkinDelete : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SkinDelete(QObject* parent, QString token);
|
||||
virtual ~SkinDelete() = default;
|
||||
|
||||
private:
|
||||
QString m_token;
|
||||
shared_qobject_ptr<QNetworkReply> m_reply;
|
||||
|
||||
protected:
|
||||
virtual void executeTask();
|
||||
|
||||
public slots:
|
||||
void downloadError(QNetworkReply::NetworkError);
|
||||
void sslErrors(const QList<QSslError>& errors);
|
||||
void downloadFinished();
|
||||
};
|
@ -1,118 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SkinUpload.h"
|
||||
|
||||
#include <QHttpMultiPart>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
QByteArray getVariant(SkinUpload::Model model)
|
||||
{
|
||||
switch (model) {
|
||||
default:
|
||||
qDebug() << "Unknown skin type!";
|
||||
case SkinUpload::STEVE:
|
||||
return "CLASSIC";
|
||||
case SkinUpload::ALEX:
|
||||
return "SLIM";
|
||||
}
|
||||
}
|
||||
|
||||
SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin, SkinUpload::Model model)
|
||||
: Task(parent), m_model(model), m_skin(skin), m_token(token)
|
||||
{}
|
||||
|
||||
void SkinUpload::executeTask()
|
||||
{
|
||||
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
|
||||
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
QHttpPart skin;
|
||||
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
|
||||
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
|
||||
skin.setBody(m_skin);
|
||||
|
||||
QHttpPart model;
|
||||
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
|
||||
model.setBody(getVariant(m_model));
|
||||
|
||||
multiPart->append(skin);
|
||||
multiPart->append(model);
|
||||
|
||||
QNetworkReply* rep = APPLICATION->network()->post(request, multiPart);
|
||||
m_reply = shared_qobject_ptr<QNetworkReply>(rep);
|
||||
|
||||
setStatus(tr("Uploading skin"));
|
||||
connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError);
|
||||
#else
|
||||
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinUpload::downloadError);
|
||||
#endif
|
||||
connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors);
|
||||
connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished);
|
||||
}
|
||||
|
||||
void SkinUpload::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
qCritical() << "Network error: " << error;
|
||||
emitFailed(m_reply->errorString());
|
||||
}
|
||||
|
||||
void SkinUpload::sslErrors(const QList<QSslError>& errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors) {
|
||||
qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void SkinUpload::downloadFinished()
|
||||
{
|
||||
// if the download failed
|
||||
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
|
||||
m_reply.reset();
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFile>
|
||||
#include <QtNetwork/QtNetwork>
|
||||
#include <memory>
|
||||
#include "tasks/Task.h"
|
||||
|
||||
using SkinUploadPtr = shared_qobject_ptr<class SkinUpload>;
|
||||
|
||||
class SkinUpload : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Model { STEVE, ALEX };
|
||||
|
||||
// Note this class takes ownership of the file.
|
||||
SkinUpload(QObject* parent, QString token, QByteArray skin, Model model = STEVE);
|
||||
virtual ~SkinUpload() {}
|
||||
|
||||
private:
|
||||
Model m_model;
|
||||
QByteArray m_skin;
|
||||
QString m_token;
|
||||
shared_qobject_ptr<QNetworkReply> m_reply;
|
||||
|
||||
protected:
|
||||
virtual void executeTask();
|
||||
|
||||
public slots:
|
||||
|
||||
void downloadError(QNetworkReply::NetworkError);
|
||||
void sslErrors(const QList<QSslError>& errors);
|
||||
|
||||
void downloadFinished();
|
||||
};
|
74
launcher/minecraft/skins/CapeChange.cpp
Normal file
74
launcher/minecraft/skins/CapeChange.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "CapeChange.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "net/ByteArraySink.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token)
|
||||
{
|
||||
logCat = taskMCSkinsLogC;
|
||||
}
|
||||
|
||||
QNetworkReply* CapeChange::getReply(QNetworkRequest& request)
|
||||
{
|
||||
if (m_capeId.isEmpty()) {
|
||||
setStatus(tr("Removing cape"));
|
||||
return m_network->deleteResource(request);
|
||||
} else {
|
||||
setStatus(tr("Equipping cape"));
|
||||
return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
void CapeChange::init()
|
||||
{
|
||||
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
|
||||
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
|
||||
}));
|
||||
}
|
||||
|
||||
CapeChange::Ptr CapeChange::make(QString token, QString capeId)
|
||||
{
|
||||
auto up = makeShared<CapeChange>(token, capeId);
|
||||
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active");
|
||||
up->setObjectName(QString("BYTES:") + up->m_url.toString());
|
||||
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
|
||||
return up;
|
||||
}
|
39
launcher/minecraft/skins/CapeChange.h
Normal file
39
launcher/minecraft/skins/CapeChange.h
Normal file
@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
class CapeChange : public Net::NetRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<CapeChange>;
|
||||
CapeChange(QString token, QString capeId);
|
||||
virtual ~CapeChange() = default;
|
||||
|
||||
static CapeChange::Ptr make(QString token, QString capeId);
|
||||
void init() override;
|
||||
|
||||
protected:
|
||||
virtual QNetworkReply* getReply(QNetworkRequest&) override;
|
||||
|
||||
private:
|
||||
QString m_capeId;
|
||||
QString m_token;
|
||||
};
|
66
launcher/minecraft/skins/SkinDelete.cpp
Normal file
66
launcher/minecraft/skins/SkinDelete.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SkinDelete.h"
|
||||
|
||||
#include "net/ByteArraySink.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token)
|
||||
{
|
||||
logCat = taskMCSkinsLogC;
|
||||
}
|
||||
|
||||
QNetworkReply* SkinDelete::getReply(QNetworkRequest& request)
|
||||
{
|
||||
setStatus(tr("Deleting skin"));
|
||||
return m_network->deleteResource(request);
|
||||
}
|
||||
|
||||
void SkinDelete::init()
|
||||
{
|
||||
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
|
||||
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
|
||||
}));
|
||||
}
|
||||
|
||||
SkinDelete::Ptr SkinDelete::make(QString token)
|
||||
{
|
||||
auto up = makeShared<SkinDelete>(token);
|
||||
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active");
|
||||
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
|
||||
return up;
|
||||
}
|
38
launcher/minecraft/skins/SkinDelete.h
Normal file
38
launcher/minecraft/skins/SkinDelete.h
Normal file
@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
class SkinDelete : public Net::NetRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<SkinDelete>;
|
||||
SkinDelete(QString token);
|
||||
virtual ~SkinDelete() = default;
|
||||
|
||||
static SkinDelete::Ptr make(QString token);
|
||||
void init() override;
|
||||
|
||||
protected:
|
||||
virtual QNetworkReply* getReply(QNetworkRequest&) override;
|
||||
|
||||
private:
|
||||
QString m_token;
|
||||
};
|
389
launcher/minecraft/skins/SkinList.cpp
Normal file
389
launcher/minecraft/skins/SkinList.cpp
Normal file
@ -0,0 +1,389 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SkinList.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QMimeData>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
#include "minecraft/skins/SkinModel.h"
|
||||
|
||||
SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct)
|
||||
{
|
||||
FS::ensureFolderPathExists(m_dir.absolutePath());
|
||||
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
|
||||
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
|
||||
m_watcher.reset(new QFileSystemWatcher(this));
|
||||
is_watching = false;
|
||||
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged);
|
||||
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged);
|
||||
directoryChanged(path);
|
||||
}
|
||||
|
||||
void SkinList::startWatching()
|
||||
{
|
||||
if (is_watching) {
|
||||
return;
|
||||
}
|
||||
update();
|
||||
is_watching = m_watcher->addPath(m_dir.absolutePath());
|
||||
if (is_watching) {
|
||||
qDebug() << "Started watching " << m_dir.absolutePath();
|
||||
} else {
|
||||
qDebug() << "Failed to start watching " << m_dir.absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
void SkinList::stopWatching()
|
||||
{
|
||||
save();
|
||||
if (!is_watching) {
|
||||
return;
|
||||
}
|
||||
is_watching = !m_watcher->removePath(m_dir.absolutePath());
|
||||
if (!is_watching) {
|
||||
qDebug() << "Stopped watching " << m_dir.absolutePath();
|
||||
} else {
|
||||
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
bool SkinList::update()
|
||||
{
|
||||
QVector<SkinModel> newSkins;
|
||||
m_dir.refresh();
|
||||
|
||||
auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json"));
|
||||
if (manifestInfo.exists()) {
|
||||
try {
|
||||
auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file");
|
||||
const auto root = doc.object();
|
||||
auto skins = Json::ensureArray(root, "skins");
|
||||
for (auto jSkin : skins) {
|
||||
SkinModel s(m_dir, Json::ensureObject(jSkin));
|
||||
if (s.isValid()) {
|
||||
newSkins << s;
|
||||
}
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
qCritical() << "Couldn't load skins json:" << e.cause();
|
||||
}
|
||||
}
|
||||
|
||||
bool needsSave = false;
|
||||
const auto& skin = m_acct->accountData()->minecraftProfile.skin;
|
||||
if (!skin.url.isEmpty() && !skin.data.isEmpty()) {
|
||||
QPixmap skinTexture;
|
||||
SkinModel* nskin = nullptr;
|
||||
for (auto i = 0; i < newSkins.size(); i++) {
|
||||
if (newSkins[i].getURL() == skin.url) {
|
||||
nskin = &newSkins[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!nskin) {
|
||||
auto name = m_acct->profileName() + ".png";
|
||||
if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) {
|
||||
name = QUrl(skin.url).fileName() + ".png";
|
||||
}
|
||||
auto path = m_dir.absoluteFilePath(name);
|
||||
if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) {
|
||||
SkinModel s(path);
|
||||
s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
|
||||
s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
|
||||
s.setURL(skin.url);
|
||||
newSkins << s;
|
||||
needsSave = true;
|
||||
}
|
||||
} else {
|
||||
nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape);
|
||||
nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
|
||||
}
|
||||
}
|
||||
|
||||
auto folderContents = m_dir.entryInfoList();
|
||||
// if there are any untracked files...
|
||||
for (QFileInfo entry : folderContents) {
|
||||
if (!entry.isFile() && entry.suffix() != "png")
|
||||
continue;
|
||||
|
||||
SkinModel w(entry.absoluteFilePath());
|
||||
if (w.isValid()) {
|
||||
auto add = true;
|
||||
for (auto s : newSkins) {
|
||||
if (s.name() == w.name()) {
|
||||
add = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (add) {
|
||||
newSkins.append(w);
|
||||
needsSave = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(newSkins.begin(), newSkins.end(),
|
||||
[](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; });
|
||||
beginResetModel();
|
||||
m_skin_list.swap(newSkins);
|
||||
endResetModel();
|
||||
if (needsSave)
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkinList::directoryChanged(const QString& path)
|
||||
{
|
||||
QDir new_dir(path);
|
||||
if (!new_dir.exists())
|
||||
if (!FS::ensureFolderPathExists(new_dir.absolutePath()))
|
||||
return;
|
||||
if (m_dir.absolutePath() != new_dir.absolutePath()) {
|
||||
m_dir.setPath(path);
|
||||
m_dir.refresh();
|
||||
if (is_watching)
|
||||
stopWatching();
|
||||
startWatching();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void SkinList::fileChanged(const QString& path)
|
||||
{
|
||||
qDebug() << "Checking " << path;
|
||||
QFileInfo checkfile(path);
|
||||
if (!checkfile.exists())
|
||||
return;
|
||||
|
||||
for (int i = 0; i < m_skin_list.count(); i++) {
|
||||
if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) {
|
||||
m_skin_list[i].refresh();
|
||||
dataChanged(index(i), index(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList SkinList::mimeTypes() const
|
||||
{
|
||||
return { "text/uri-list" };
|
||||
}
|
||||
|
||||
Qt::DropActions SkinList::supportedDropActions() const
|
||||
{
|
||||
return Qt::CopyAction;
|
||||
}
|
||||
|
||||
bool SkinList::dropMimeData(const QMimeData* data,
|
||||
Qt::DropAction action,
|
||||
[[maybe_unused]] int row,
|
||||
[[maybe_unused]] int column,
|
||||
[[maybe_unused]] const QModelIndex& parent)
|
||||
{
|
||||
if (action == Qt::IgnoreAction)
|
||||
return true;
|
||||
// check if the action is supported
|
||||
if (!data || !(action & supportedDropActions()))
|
||||
return false;
|
||||
|
||||
// files dropped from outside?
|
||||
if (data->hasUrls()) {
|
||||
auto urls = data->urls();
|
||||
QStringList skinFiles;
|
||||
for (auto url : urls) {
|
||||
// only local files may be dropped...
|
||||
if (!url.isLocalFile())
|
||||
continue;
|
||||
skinFiles << url.toLocalFile();
|
||||
}
|
||||
installSkins(skinFiles);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::ItemFlags SkinList::flags(const QModelIndex& index) const
|
||||
{
|
||||
Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index);
|
||||
if (index.isValid()) {
|
||||
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
QVariant SkinList::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
int row = index.row();
|
||||
|
||||
if (row < 0 || row >= m_skin_list.size())
|
||||
return QVariant();
|
||||
auto skin = m_skin_list[row];
|
||||
switch (role) {
|
||||
case Qt::DecorationRole:
|
||||
return skin.getTexture();
|
||||
case Qt::DisplayRole:
|
||||
return skin.name();
|
||||
case Qt::UserRole:
|
||||
return skin.name();
|
||||
case Qt::EditRole:
|
||||
return skin.name();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
int SkinList::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_skin_list.size();
|
||||
}
|
||||
|
||||
void SkinList::installSkins(const QStringList& iconFiles)
|
||||
{
|
||||
for (QString file : iconFiles)
|
||||
installSkin(file);
|
||||
}
|
||||
|
||||
QString SkinList::installSkin(const QString& file, const QString& name)
|
||||
{
|
||||
if (file.isEmpty())
|
||||
return tr("Path is empty.");
|
||||
QFileInfo fileinfo(file);
|
||||
if (!fileinfo.exists())
|
||||
return tr("File doesn't exist.");
|
||||
if (!fileinfo.isFile())
|
||||
return tr("Not a file.");
|
||||
if (!fileinfo.isReadable())
|
||||
return tr("File is not readable.");
|
||||
if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid())
|
||||
return tr("Skin images must be 64x64 or 64x32 pixel PNG files.");
|
||||
|
||||
QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
|
||||
|
||||
return QFile::copy(file, target) ? "" : tr("Unable to copy file");
|
||||
}
|
||||
|
||||
int SkinList::getSkinIndex(const QString& key) const
|
||||
{
|
||||
for (int i = 0; i < m_skin_list.count(); i++) {
|
||||
if (m_skin_list[i].name() == key) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const SkinModel* SkinList::skin(const QString& key) const
|
||||
{
|
||||
int idx = getSkinIndex(key);
|
||||
if (idx == -1)
|
||||
return nullptr;
|
||||
return &m_skin_list[idx];
|
||||
}
|
||||
|
||||
SkinModel* SkinList::skin(const QString& key)
|
||||
{
|
||||
int idx = getSkinIndex(key);
|
||||
if (idx == -1)
|
||||
return nullptr;
|
||||
return &m_skin_list[idx];
|
||||
}
|
||||
|
||||
bool SkinList::deleteSkin(const QString& key, const bool trash)
|
||||
{
|
||||
int idx = getSkinIndex(key);
|
||||
if (idx != -1) {
|
||||
auto s = m_skin_list[idx];
|
||||
if (trash) {
|
||||
if (FS::trash(s.getPath(), nullptr)) {
|
||||
m_skin_list.remove(idx);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
} else if (QFile::remove(s.getPath())) {
|
||||
m_skin_list.remove(idx);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SkinList::save()
|
||||
{
|
||||
QJsonObject doc;
|
||||
QJsonArray arr;
|
||||
for (auto s : m_skin_list) {
|
||||
arr << s.toJSON();
|
||||
}
|
||||
doc["skins"] = arr;
|
||||
Json::write(doc, m_dir.absoluteFilePath("index.json"));
|
||||
}
|
||||
|
||||
int SkinList::getSelectedAccountSkin()
|
||||
{
|
||||
const auto& skin = m_acct->accountData()->minecraftProfile.skin;
|
||||
for (int i = 0; i < m_skin_list.count(); i++) {
|
||||
if (m_skin_list[i].getURL() == skin.url) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role)
|
||||
{
|
||||
if (!idx.isValid() || role != Qt::EditRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int row = idx.row();
|
||||
if (row < 0 || row >= m_skin_list.size())
|
||||
return false;
|
||||
auto& skin = m_skin_list[row];
|
||||
auto newName = value.toString();
|
||||
if (skin.name() != newName) {
|
||||
skin.rename(newName);
|
||||
save();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkinList::updateSkin(SkinModel* s)
|
||||
{
|
||||
auto done = false;
|
||||
for (auto i = 0; i < m_skin_list.size(); i++) {
|
||||
if (m_skin_list[i].getPath() == s->getPath()) {
|
||||
m_skin_list[i].setCapeId(s->getCapeId());
|
||||
m_skin_list[i].setModel(s->getModel());
|
||||
m_skin_list[i].setURL(s->getURL());
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!done) {
|
||||
beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1);
|
||||
m_skin_list.append(*s);
|
||||
endInsertRows();
|
||||
}
|
||||
save();
|
||||
}
|
80
launcher/minecraft/skins/SkinList.h
Normal file
80
launcher/minecraft/skins/SkinList.h
Normal file
@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDir>
|
||||
#include <QFileSystemWatcher>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "SkinModel.h"
|
||||
#include "minecraft/auth/MinecraftAccount.h"
|
||||
|
||||
class SkinList : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct);
|
||||
virtual ~SkinList() { save(); };
|
||||
|
||||
int getSkinIndex(const QString& key) const;
|
||||
|
||||
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex& idx, const QVariant& value, int role) override;
|
||||
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
virtual QStringList mimeTypes() const override;
|
||||
virtual Qt::DropActions supportedDropActions() const override;
|
||||
virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
|
||||
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
|
||||
bool deleteSkin(const QString& key, const bool trash);
|
||||
|
||||
void installSkins(const QStringList& iconFiles);
|
||||
QString installSkin(const QString& file, const QString& name = {});
|
||||
|
||||
const SkinModel* skin(const QString& key) const;
|
||||
SkinModel* skin(const QString& key);
|
||||
|
||||
void startWatching();
|
||||
void stopWatching();
|
||||
|
||||
QString getDir() const { return m_dir.absolutePath(); }
|
||||
void save();
|
||||
int getSelectedAccountSkin();
|
||||
|
||||
void updateSkin(SkinModel* s);
|
||||
|
||||
private:
|
||||
// hide copy constructor
|
||||
SkinList(const SkinList&) = delete;
|
||||
// hide assign op
|
||||
SkinList& operator=(const SkinList&) = delete;
|
||||
|
||||
protected slots:
|
||||
void directoryChanged(const QString& path);
|
||||
void fileChanged(const QString& path);
|
||||
bool update();
|
||||
|
||||
private:
|
||||
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
|
||||
bool is_watching;
|
||||
QVector<SkinModel> m_skin_list;
|
||||
QDir m_dir;
|
||||
MinecraftAccountPtr m_acct;
|
||||
};
|
78
launcher/minecraft/skins/SkinModel.cpp
Normal file
78
launcher/minecraft/skins/SkinModel.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SkinModel.h"
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QTransform>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
|
||||
SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {}
|
||||
|
||||
SkinModel::SkinModel(QDir skinDir, QJsonObject obj)
|
||||
: m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url"))
|
||||
{
|
||||
auto name = Json::ensureString(obj, "name");
|
||||
|
||||
if (auto model = Json::ensureString(obj, "model"); model == "SLIM") {
|
||||
m_model = Model::SLIM;
|
||||
}
|
||||
m_path = skinDir.absoluteFilePath(name) + ".png";
|
||||
m_texture = QPixmap(m_path);
|
||||
}
|
||||
|
||||
QString SkinModel::name() const
|
||||
{
|
||||
return QFileInfo(m_path).baseName();
|
||||
}
|
||||
|
||||
bool SkinModel::rename(QString newName)
|
||||
{
|
||||
auto info = QFileInfo(m_path);
|
||||
m_path = FS::PathCombine(info.absolutePath(), newName + ".png");
|
||||
return FS::move(info.absoluteFilePath(), m_path);
|
||||
}
|
||||
|
||||
QJsonObject SkinModel::toJSON() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["name"] = name();
|
||||
obj["capeId"] = m_cape_id;
|
||||
obj["url"] = m_url;
|
||||
obj["model"] = getModelString();
|
||||
return obj;
|
||||
}
|
||||
|
||||
QString SkinModel::getModelString() const
|
||||
{
|
||||
switch (m_model) {
|
||||
case CLASSIC:
|
||||
return "CLASSIC";
|
||||
case SLIM:
|
||||
return "SLIM";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SkinModel::isValid() const
|
||||
{
|
||||
return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64;
|
||||
}
|
57
launcher/minecraft/skins/SkinModel.h
Normal file
57
launcher/minecraft/skins/SkinModel.h
Normal file
@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDir>
|
||||
#include <QJsonObject>
|
||||
#include <QPixmap>
|
||||
|
||||
class SkinModel {
|
||||
public:
|
||||
enum Model { CLASSIC, SLIM };
|
||||
|
||||
SkinModel() = default;
|
||||
SkinModel(QString path);
|
||||
SkinModel(QDir skinDir, QJsonObject obj);
|
||||
virtual ~SkinModel() = default;
|
||||
|
||||
QString name() const;
|
||||
QString getModelString() const;
|
||||
bool isValid() const;
|
||||
QString getPath() const { return m_path; }
|
||||
QPixmap getTexture() const { return m_texture; }
|
||||
QString getCapeId() const { return m_cape_id; }
|
||||
Model getModel() const { return m_model; }
|
||||
QString getURL() const { return m_url; }
|
||||
|
||||
bool rename(QString newName);
|
||||
void setCapeId(QString capeID) { m_cape_id = capeID; }
|
||||
void setModel(Model model) { m_model = model; }
|
||||
void setURL(QString url) { m_url = url; }
|
||||
void refresh() { m_texture = QPixmap(m_path); }
|
||||
|
||||
QJsonObject toJSON() const;
|
||||
|
||||
private:
|
||||
QString m_path;
|
||||
QPixmap m_texture;
|
||||
QString m_cape_id;
|
||||
Model m_model;
|
||||
QString m_url;
|
||||
};
|
84
launcher/minecraft/skins/SkinUpload.cpp
Normal file
84
launcher/minecraft/skins/SkinUpload.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "SkinUpload.h"
|
||||
|
||||
#include <QHttpMultiPart>
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "net/ByteArraySink.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetRequest(), m_token(token), m_path(path), m_variant(variant)
|
||||
{
|
||||
logCat = taskMCSkinsLogC;
|
||||
}
|
||||
|
||||
QNetworkReply* SkinUpload::getReply(QNetworkRequest& request)
|
||||
{
|
||||
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this);
|
||||
|
||||
QHttpPart skin;
|
||||
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
|
||||
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
|
||||
|
||||
skin.setBody(FS::read(m_path));
|
||||
|
||||
QHttpPart model;
|
||||
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
|
||||
model.setBody(m_variant.toUtf8());
|
||||
|
||||
multiPart->append(skin);
|
||||
multiPart->append(model);
|
||||
setStatus(tr("Uploading skin"));
|
||||
return m_network->post(request, multiPart);
|
||||
}
|
||||
|
||||
void SkinUpload::init()
|
||||
{
|
||||
addHeaderProxy(new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
|
||||
{ "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() },
|
||||
}));
|
||||
}
|
||||
|
||||
SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant)
|
||||
{
|
||||
auto up = makeShared<SkinUpload>(token, path, variant);
|
||||
up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins");
|
||||
up->setObjectName(QString("BYTES:") + up->m_url.toString());
|
||||
up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>()));
|
||||
return up;
|
||||
}
|
42
launcher/minecraft/skins/SkinUpload.h
Normal file
42
launcher/minecraft/skins/SkinUpload.h
Normal file
@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
class SkinUpload : public Net::NetRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<SkinUpload>;
|
||||
|
||||
// Note this class takes ownership of the file.
|
||||
SkinUpload(QString token, QString path, QString variant);
|
||||
virtual ~SkinUpload() = default;
|
||||
|
||||
static SkinUpload::Ptr make(QString token, QString path, QString variant);
|
||||
void init() override;
|
||||
|
||||
protected:
|
||||
virtual QNetworkReply* getReply(QNetworkRequest&) override;
|
||||
|
||||
private:
|
||||
QString m_token;
|
||||
QString m_path;
|
||||
QString m_variant;
|
||||
};
|
@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles()
|
||||
|
||||
// Delete the files
|
||||
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
|
||||
QFileInfo fileInfo(to);
|
||||
if (fileInfo.exists()) {
|
||||
if (!QFile::remove(to)) {
|
||||
if (!FS::deletePath(to)) {
|
||||
qWarning() << "Failed to delete" << to;
|
||||
return false;
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ bool FlameCreationTask::createInstance()
|
||||
// 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"));
|
||||
FS::ensureFilePathExists(new_index_place);
|
||||
QFile::rename(index_path, new_index_place);
|
||||
FS::move(index_path, new_index_place);
|
||||
|
||||
} catch (const JSONValidationError& e) {
|
||||
setError(tr("Could not understand pack manifest:\n") + e.cause());
|
||||
@ -336,7 +336,7 @@ bool FlameCreationTask::createInstance()
|
||||
Override::createOverrides("overrides", parent_folder, overridePath);
|
||||
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
@ -42,17 +42,28 @@ QString toHTML(QList<Mod*> mods, OptionalData extraData)
|
||||
}
|
||||
if (extraData & Authors && !mod->authors().isEmpty())
|
||||
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));
|
||||
}
|
||||
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)
|
||||
{
|
||||
QStringList lines;
|
||||
|
||||
for (auto mod : mods) {
|
||||
auto meta = mod->metadata();
|
||||
auto modName = mod->name();
|
||||
auto modName = toMarkdownEscaped(mod->name());
|
||||
if (extraData & Url) {
|
||||
auto url = mod->metaurl();
|
||||
if (!url.isEmpty())
|
||||
@ -60,14 +71,16 @@ QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
|
||||
}
|
||||
auto line = modName;
|
||||
if (extraData & Version) {
|
||||
auto ver = mod->version();
|
||||
auto ver = toMarkdownEscaped(mod->version());
|
||||
if (ver.isEmpty() && meta != nullptr)
|
||||
ver = meta->version().toString();
|
||||
ver = toMarkdownEscaped(meta->version().toString());
|
||||
if (!ver.isEmpty())
|
||||
line += QString(" [%1]").arg(ver);
|
||||
}
|
||||
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;
|
||||
}
|
||||
return lines.join("\n");
|
||||
@ -95,6 +108,8 @@ QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
|
||||
}
|
||||
if (extraData & Authors && !mod->authors().isEmpty())
|
||||
line += " by " + mod->authors().join(", ");
|
||||
if (extraData & FileName)
|
||||
line += QString(" (%1)").arg(mod->fileinfo().fileName());
|
||||
lines << line;
|
||||
}
|
||||
return lines.join("\n");
|
||||
@ -122,6 +137,8 @@ QString toJSON(QList<Mod*> mods, OptionalData extraData)
|
||||
}
|
||||
if (extraData & Authors && !mod->authors().isEmpty())
|
||||
line["authors"] = QJsonArray::fromStringList(mod->authors());
|
||||
if (extraData & FileName)
|
||||
line["filename"] = mod->fileinfo().fileName();
|
||||
lines << line;
|
||||
}
|
||||
QJsonDocument doc;
|
||||
@ -154,6 +171,8 @@ QString toCSV(QList<Mod*> mods, OptionalData extraData)
|
||||
authors = QString("\"%1\"").arg(mod->authors().join(","));
|
||||
data << authors;
|
||||
}
|
||||
if (extraData & FileName)
|
||||
data << mod->fileinfo().fileName();
|
||||
lines << data.join(",");
|
||||
}
|
||||
return lines.join("\n");
|
||||
@ -189,11 +208,13 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
|
||||
if (ver.isEmpty() && meta != nullptr)
|
||||
ver = meta->version().toString();
|
||||
auto authors = mod->authors().join(", ");
|
||||
auto filename = mod->fileinfo().fileName();
|
||||
lines << QString(lineTemplate)
|
||||
.replace("{name}", modName)
|
||||
.replace("{url}", url)
|
||||
.replace("{version}", ver)
|
||||
.replace("{authors}", authors);
|
||||
.replace("{authors}", authors)
|
||||
.replace("{filename}", filename);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
@ -23,11 +23,7 @@
|
||||
namespace ExportToModList {
|
||||
|
||||
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
|
||||
enum OptionalData {
|
||||
Authors = 1 << 0,
|
||||
Url = 1 << 1,
|
||||
Version = 1 << 2,
|
||||
};
|
||||
enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 };
|
||||
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
|
||||
QString exportToModList(QList<Mod*> mods, QString lineTemplate);
|
||||
} // namespace ExportToModList
|
||||
|
@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS
|
||||
{
|
||||
QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
|
||||
if (QFile::exists(file_path))
|
||||
QFile::remove(file_path);
|
||||
FS::deletePath(file_path);
|
||||
|
||||
FS::ensureFilePathExists(file_path);
|
||||
|
||||
|
@ -137,7 +137,7 @@ void PackInstallTask::install()
|
||||
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
|
||||
if (unzipMcDir.exists()) {
|
||||
// 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!"));
|
||||
return;
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ bool ModrinthCreationTask::createInstance()
|
||||
// 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"));
|
||||
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);
|
||||
|
||||
@ -183,7 +183,7 @@ bool ModrinthCreationTask::createInstance()
|
||||
Override::createOverrides("overrides", parent_folder, override_path);
|
||||
|
||||
// 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");
|
||||
return false;
|
||||
}
|
||||
|
@ -131,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion
|
||||
|
||||
file.name = Json::requireString(obj, "name");
|
||||
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.changelog = Json::ensureString(obj, "changelog");
|
||||
|
||||
|
@ -84,6 +84,7 @@ struct ModpackExtra {
|
||||
struct ModpackVersion {
|
||||
QString name;
|
||||
QString version;
|
||||
QString gameVersion;
|
||||
ModPlatform::IndexedVersionType version_type;
|
||||
QString changelog;
|
||||
|
||||
|
@ -22,5 +22,6 @@
|
||||
Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net")
|
||||
Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download")
|
||||
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(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http")
|
||||
|
@ -24,5 +24,6 @@
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskNetLogC)
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC)
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC)
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC)
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC)
|
||||
Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC)
|
||||
|
@ -43,11 +43,15 @@
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#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)
|
||||
setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt());
|
||||
if (max_concurrent < 0)
|
||||
max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt();
|
||||
#endif
|
||||
if (max_concurrent > 0)
|
||||
setMaxConcurrent(max_concurrent);
|
||||
}
|
||||
|
||||
auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool
|
||||
@ -146,11 +150,11 @@ void NetJob::emitFailed(QString reason)
|
||||
#if defined(LAUNCHER_APPLICATION)
|
||||
if (m_ask_retry) {
|
||||
auto response = CustomMessageBox::selectable(nullptr, "Confirm retry",
|
||||
"The tasks failed\n"
|
||||
"The tasks failed.\n"
|
||||
"Failed urls\n" +
|
||||
getFailedFiles().join("\n\t") +
|
||||
"\n"
|
||||
"If this continues to happen please check the logs of the application"
|
||||
".\n"
|
||||
"If this continues to happen please check the logs of the application.\n"
|
||||
"Do you want to retry?",
|
||||
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
|
||||
->exec();
|
||||
|
@ -52,7 +52,7 @@ class NetJob : public ConcurrentTask {
|
||||
public:
|
||||
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;
|
||||
|
||||
auto size() const -> int;
|
||||
|
@ -5,6 +5,7 @@
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -4,6 +4,7 @@
|
||||
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -74,6 +75,7 @@ class NetRequest : public Task {
|
||||
virtual void init() {}
|
||||
|
||||
QUrl url() const;
|
||||
void setUrl(QUrl url) { m_url = url; }
|
||||
int replyStatusCode() const;
|
||||
QNetworkReply::NetworkError error() const;
|
||||
QString errorString() const;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -77,7 +77,6 @@
|
||||
#include <DesktopServices.h>
|
||||
#include <InstanceList.h>
|
||||
#include <MMCZip.h>
|
||||
#include <SkinUtils.h>
|
||||
#include <icons/IconList.h>
|
||||
#include <java/JavaInstallList.h>
|
||||
#include <java/JavaUtils.h>
|
||||
@ -96,7 +95,6 @@
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/ExportInstanceDialog.h"
|
||||
#include "ui/dialogs/ExportPackDialog.h"
|
||||
#include "ui/dialogs/ExportToModListDialog.h"
|
||||
#include "ui/dialogs/IconPickerDialog.h"
|
||||
#include "ui/dialogs/ImportResourceDialog.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->actionExportInstanceMrPack);
|
||||
exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
|
||||
exportInstanceMenu->addAction(ui->actionExportInstanceToModList);
|
||||
ui->actionExportInstance->setMenu(exportInstanceMenu);
|
||||
}
|
||||
|
||||
@ -1212,6 +1209,11 @@ void MainWindow::on_actionViewCentralModsFolder_triggered()
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
if (m_selectedInstance) {
|
||||
|
@ -120,6 +120,8 @@ class MainWindow : public QMainWindow {
|
||||
void on_actionViewIconsFolder_triggered();
|
||||
void on_actionViewLogsFolder_triggered();
|
||||
|
||||
void on_actionViewSkinsFolder_triggered();
|
||||
|
||||
void on_actionViewSelectedInstFolder_triggered();
|
||||
|
||||
void refreshInstances();
|
||||
@ -158,7 +160,6 @@ class MainWindow : public QMainWindow {
|
||||
void on_actionExportInstanceZip_triggered();
|
||||
void on_actionExportInstanceMrPack_triggered();
|
||||
void on_actionExportInstanceFlamePack_triggered();
|
||||
void on_actionExportInstanceToModList_triggered();
|
||||
|
||||
void on_actionRenameInstance_triggered();
|
||||
|
||||
|
@ -131,7 +131,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>20</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="fileMenu">
|
||||
@ -191,6 +191,7 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionViewInstanceFolder"/>
|
||||
<addaction name="actionViewCentralModsFolder"/>
|
||||
<addaction name="actionViewSkinsFolder"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionViewIconThemeFolder"/>
|
||||
<addaction name="actionViewWidgetThemeFolder"/>
|
||||
@ -491,15 +492,6 @@
|
||||
<string>CurseForge (zip)</string>
|
||||
</property>
|
||||
</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">
|
||||
<property name="icon">
|
||||
<iconset theme="shortcut">
|
||||
@ -587,6 +579,18 @@
|
||||
<string>Open the central mods folder in a file browser.</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionViewSkinsFolder">
|
||||
<property name="icon">
|
||||
<iconset theme="viewfolder">
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Skins</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Open the skins folder in a file browser.</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionViewIconsFolder">
|
||||
<property name="icon">
|
||||
<iconset theme="viewfolder">
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "Markdown.h"
|
||||
#include "StringUtils.h"
|
||||
#include "ui_AboutDialog.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));
|
||||
|
||||
QString chtml = getCreditsHtml();
|
||||
ui->creditsText->setHtml(chtml);
|
||||
ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml));
|
||||
|
||||
QString lhtml = getLicenseHtml();
|
||||
ui->licenseText->setHtml(lhtml);
|
||||
ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml));
|
||||
|
||||
ui->urlLabel->setOpenExternalLinks(true);
|
||||
|
||||
|
@ -22,8 +22,7 @@
|
||||
#include <QTextEdit>
|
||||
#include "FileSystem.h"
|
||||
#include "Markdown.h"
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "StringUtils.h"
|
||||
#include "modplatform/helpers/ExportToModList.h"
|
||||
#include "ui_ExportToModListDialog.h"
|
||||
|
||||
@ -41,38 +40,31 @@ const QHash<ExportToModList::Formats, QString> ExportToModListDialog::exampleLin
|
||||
{ ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" },
|
||||
};
|
||||
|
||||
ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent)
|
||||
: QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog)
|
||||
ExportToModListDialog::ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent)
|
||||
: QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
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->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger);
|
||||
connect(ui->versionCheckBox, &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->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); });
|
||||
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] {
|
||||
if (ui->templateText->toPlainText() != exampleLines[format])
|
||||
if (ui->templateText->toPlainText() != exampleLines[m_format])
|
||||
ui->formatComboBox->setCurrentIndex(5);
|
||||
else
|
||||
triggerImp();
|
||||
triggerImp();
|
||||
});
|
||||
connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) {
|
||||
this->ui->finalText->selectAll();
|
||||
this->ui->finalText->copy();
|
||||
});
|
||||
triggerImp();
|
||||
}
|
||||
|
||||
ExportToModListDialog::~ExportToModListDialog()
|
||||
@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index)
|
||||
case 0: {
|
||||
enableCustom(false);
|
||||
ui->resultText->show();
|
||||
format = ExportToModList::HTML;
|
||||
m_format = ExportToModList::HTML;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
enableCustom(false);
|
||||
ui->resultText->show();
|
||||
format = ExportToModList::MARKDOWN;
|
||||
m_format = ExportToModList::MARKDOWN;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
enableCustom(false);
|
||||
ui->resultText->hide();
|
||||
format = ExportToModList::PLAINTXT;
|
||||
m_format = ExportToModList::PLAINTXT;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
enableCustom(false);
|
||||
ui->resultText->hide();
|
||||
format = ExportToModList::JSON;
|
||||
m_format = ExportToModList::JSON;
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
enableCustom(false);
|
||||
ui->resultText->hide();
|
||||
format = ExportToModList::CSV;
|
||||
m_format = ExportToModList::CSV;
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
m_template_changed = true;
|
||||
enableCustom(true);
|
||||
ui->resultText->hide();
|
||||
format = ExportToModList::CUSTOM;
|
||||
m_format = ExportToModList::CUSTOM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index)
|
||||
|
||||
void ExportToModListDialog::triggerImp()
|
||||
{
|
||||
if (format == ExportToModList::CUSTOM) {
|
||||
ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText()));
|
||||
if (m_format == ExportToModList::CUSTOM) {
|
||||
ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText()));
|
||||
return;
|
||||
}
|
||||
auto opt = 0;
|
||||
@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp()
|
||||
opt |= ExportToModList::Version;
|
||||
if (ui->urlCheckBox->isChecked())
|
||||
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);
|
||||
switch (format) {
|
||||
switch (m_format) {
|
||||
case ExportToModList::CUSTOM:
|
||||
return;
|
||||
case ExportToModList::HTML:
|
||||
ui->resultText->setHtml(txt);
|
||||
ui->resultText->setHtml(StringUtils::htmlListPatch(txt));
|
||||
break;
|
||||
case ExportToModList::MARKDOWN:
|
||||
ui->resultText->setHtml(markdownToHTML(txt));
|
||||
ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt)));
|
||||
break;
|
||||
case ExportToModList::PLAINTXT:
|
||||
break;
|
||||
@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp()
|
||||
case ExportToModList::CSV:
|
||||
break;
|
||||
}
|
||||
auto exampleLine = exampleLines[format];
|
||||
auto exampleLine = exampleLines[m_format];
|
||||
if (!m_template_changed && ui->templateText->toPlainText() != exampleLine)
|
||||
ui->templateText->setPlainText(exampleLine);
|
||||
}
|
||||
@ -163,9 +157,9 @@ void ExportToModListDialog::triggerImp()
|
||||
void ExportToModListDialog::done(int result)
|
||||
{
|
||||
if (result == Accepted) {
|
||||
const QString filename = FS::RemoveInvalidFilenameChars(name);
|
||||
const QString filename = FS::RemoveInvalidFilenameChars(m_name);
|
||||
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);
|
||||
|
||||
if (output.isEmpty())
|
||||
@ -178,7 +172,7 @@ void ExportToModListDialog::done(int result)
|
||||
|
||||
QString ExportToModListDialog::extension()
|
||||
{
|
||||
switch (format) {
|
||||
switch (m_format) {
|
||||
case ExportToModList::HTML:
|
||||
return ".html";
|
||||
case ExportToModList::MARKDOWN:
|
||||
@ -197,7 +191,7 @@ QString ExportToModListDialog::extension()
|
||||
|
||||
void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
|
||||
{
|
||||
if (format != ExportToModList::CUSTOM)
|
||||
if (m_format != ExportToModList::CUSTOM)
|
||||
return;
|
||||
switch (option) {
|
||||
case ExportToModList::Authors:
|
||||
@ -209,6 +203,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option)
|
||||
case ExportToModList::Version:
|
||||
ui->templateText->insertPlainText("{version}");
|
||||
break;
|
||||
case ExportToModList::FileName:
|
||||
ui->templateText->insertPlainText("{filename}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ExportToModListDialog::enableCustom(bool enabled)
|
||||
@ -221,4 +218,7 @@ void ExportToModListDialog::enableCustom(bool enabled)
|
||||
|
||||
ui->urlCheckBox->setHidden(enabled);
|
||||
ui->urlButton->setHidden(!enabled);
|
||||
|
||||
ui->filenameCheckBox->setHidden(enabled);
|
||||
ui->filenameButton->setHidden(!enabled);
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include "BaseInstance.h"
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "modplatform/helpers/ExportToModList.h"
|
||||
|
||||
@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr);
|
||||
explicit ExportToModListDialog(QString name, QList<Mod*> mods, QWidget* parent = nullptr);
|
||||
~ExportToModListDialog();
|
||||
|
||||
void done(int result) override;
|
||||
@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog {
|
||||
private:
|
||||
QString extension();
|
||||
void enableCustom(bool enabled);
|
||||
QList<Mod*> m_allMods;
|
||||
|
||||
QList<Mod*> m_mods;
|
||||
bool m_template_changed;
|
||||
QString name;
|
||||
ExportToModList::Formats format = ExportToModList::Formats::HTML;
|
||||
QString m_name;
|
||||
ExportToModList::Formats m_format = ExportToModList::Formats::HTML;
|
||||
Ui::ExportToModListDialog* ui;
|
||||
static const QHash<ExportToModList::Formats, QString> exampleLines;
|
||||
};
|
||||
|
@ -117,6 +117,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="filenameCheckBox">
|
||||
<property name="text">
|
||||
<string>Filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="versionButton">
|
||||
<property name="text">
|
||||
@ -138,6 +145,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="filenameButton">
|
||||
<property name="text">
|
||||
<string>Filename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "CustomMessageBox.h"
|
||||
#include "ProgressDialog.h"
|
||||
#include "ScrollMessageBox.h"
|
||||
#include "StringUtils.h"
|
||||
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
|
||||
#include "modplatform/ModIndex.h"
|
||||
#include "modplatform/flame/FlameAPI.h"
|
||||
@ -473,7 +474,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
|
||||
break;
|
||||
}
|
||||
|
||||
changelog_area->setHtml(text);
|
||||
changelog_area->setHtml(StringUtils::htmlListPatch(text));
|
||||
changelog_area->setOpenExternalLinks(true);
|
||||
changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth);
|
||||
changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
|
||||
|
@ -52,6 +52,7 @@
|
||||
#include <QFileDialog>
|
||||
#include <QLayout>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QValidator>
|
||||
#include <utility>
|
||||
|
||||
@ -63,6 +64,7 @@
|
||||
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
|
||||
#include "ui/pages/modplatform/technic/TechnicPage.h"
|
||||
#include "ui/widgets/PageContainer.h"
|
||||
|
||||
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
|
||||
const QString& url,
|
||||
const QMap<QString, QString>& extra_info,
|
||||
@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
|
||||
|
||||
updateDialogState();
|
||||
|
||||
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
|
||||
if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) {
|
||||
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()
|
||||
|
@ -20,7 +20,6 @@
|
||||
#include <QItemSelectionModel>
|
||||
|
||||
#include "Application.h"
|
||||
#include "SkinUtils.h"
|
||||
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
|
||||
|
@ -1,164 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QPainter>
|
||||
|
||||
#include <FileSystem.h>
|
||||
|
||||
#include <minecraft/services/CapeChange.h>
|
||||
#include <minecraft/services/SkinUpload.h>
|
||||
#include <tasks/SequentialTask.h>
|
||||
|
||||
#include "CustomMessageBox.h"
|
||||
#include "ProgressDialog.h"
|
||||
#include "SkinUploadDialog.h"
|
||||
#include "ui_SkinUploadDialog.h"
|
||||
|
||||
void SkinUploadDialog::on_buttonBox_rejected()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void SkinUploadDialog::on_buttonBox_accepted()
|
||||
{
|
||||
QString fileName;
|
||||
QString input = ui->skinPathTextBox->text();
|
||||
ProgressDialog prog(this);
|
||||
SequentialTask skinUpload;
|
||||
|
||||
if (!input.isEmpty()) {
|
||||
QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
|
||||
bool isLocalFile = false;
|
||||
// it has an URL prefix -> it is an URL
|
||||
if (urlPrefixMatcher.match(input).hasMatch()) {
|
||||
QUrl fileURL = input;
|
||||
if (fileURL.isValid()) {
|
||||
// local?
|
||||
if (fileURL.isLocalFile()) {
|
||||
isLocalFile = true;
|
||||
fileName = fileURL.toLocalFile();
|
||||
} else {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Using remote URLs for setting skins is not implemented yet."),
|
||||
QMessageBox::Warning)
|
||||
->exec();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("You cannot use an invalid URL for uploading skins."),
|
||||
QMessageBox::Warning)
|
||||
->exec();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// just assume it's a path then
|
||||
isLocalFile = true;
|
||||
fileName = ui->skinPathTextBox->text();
|
||||
}
|
||||
if (isLocalFile && !QFile::exists(fileName)) {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
SkinUpload::Model model = SkinUpload::STEVE;
|
||||
if (ui->steveBtn->isChecked()) {
|
||||
model = SkinUpload::STEVE;
|
||||
} else if (ui->alexBtn->isChecked()) {
|
||||
model = SkinUpload::ALEX;
|
||||
}
|
||||
skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
|
||||
}
|
||||
|
||||
auto selectedCape = ui->capeCombo->currentData().toString();
|
||||
if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
|
||||
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
|
||||
}
|
||||
if (prog.execWithTask(&skinUpload) != QDialog::Accepted) {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec();
|
||||
close();
|
||||
}
|
||||
|
||||
void SkinUploadDialog::on_skinBrowseBtn_clicked()
|
||||
{
|
||||
auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
|
||||
QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
|
||||
if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) {
|
||||
return;
|
||||
}
|
||||
QString cooked_path = FS::NormalizePath(raw_path);
|
||||
ui->skinPathTextBox->setText(cooked_path);
|
||||
}
|
||||
|
||||
SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// FIXME: add a model for this, download/refresh the capes on demand
|
||||
auto& accountData = *acct->accountData();
|
||||
int index = 0;
|
||||
ui->capeCombo->addItem(tr("No Cape"), QVariant());
|
||||
auto currentCape = accountData.minecraftProfile.currentCape;
|
||||
if (currentCape.isEmpty()) {
|
||||
ui->capeCombo->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
for (auto& cape : accountData.minecraftProfile.capes) {
|
||||
index++;
|
||||
if (cape.data.size()) {
|
||||
QPixmap capeImage;
|
||||
if (capeImage.loadFromData(cape.data, "PNG")) {
|
||||
QPixmap preview = QPixmap(10, 16);
|
||||
QPainter painter(&preview);
|
||||
painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
|
||||
ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
|
||||
if (currentCape == cape.id) {
|
||||
ui->capeCombo->setCurrentIndex(index);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ui->capeCombo->addItem(cape.alias, cape.id);
|
||||
if (currentCape == cape.id) {
|
||||
ui->capeCombo->setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <minecraft/auth/MinecraftAccount.h>
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class SkinUploadDialog;
|
||||
}
|
||||
|
||||
class SkinUploadDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0);
|
||||
virtual ~SkinUploadDialog(){};
|
||||
|
||||
public slots:
|
||||
void on_buttonBox_accepted();
|
||||
|
||||
void on_buttonBox_rejected();
|
||||
|
||||
void on_skinBrowseBtn_clicked();
|
||||
|
||||
protected:
|
||||
MinecraftAccountPtr m_acct;
|
||||
|
||||
private:
|
||||
Ui::SkinUploadDialog* ui;
|
||||
};
|
@ -1,95 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SkinUploadDialog</class>
|
||||
<widget class="QDialog" name="SkinUploadDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>394</width>
|
||||
<height>360</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Skin Upload</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="fileBox">
|
||||
<property name="title">
|
||||
<string>Skin File</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="skinPathTextBox">
|
||||
<property name="placeholderText">
|
||||
<string>Leave empty to keep current skin</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="skinBrowseBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Browse</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="modelBox">
|
||||
<property name="title">
|
||||
<string>Player Model</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_1">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="steveBtn">
|
||||
<property name="text">
|
||||
<string>Steve Model</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="alexBtn">
|
||||
<property name="text">
|
||||
<string>Alex Model</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="capeBox">
|
||||
<property name="title">
|
||||
<string>Cape</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QComboBox" name="capeCombo"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -25,6 +25,7 @@
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "Markdown.h"
|
||||
#include "StringUtils.h"
|
||||
#include "ui_UpdateAvailableDialog.h"
|
||||
|
||||
UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
|
||||
@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion,
|
||||
ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64));
|
||||
|
||||
auto releaseNotesHtml = markdownToHTML(releaseNotes);
|
||||
ui->releaseNotes->setHtml(releaseNotesHtml);
|
||||
ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml));
|
||||
ui->releaseNotes->setOpenExternalLinks(true);
|
||||
|
||||
connect(ui->skipButton, &QPushButton::clicked, this, [this]() {
|
||||
|
500
launcher/ui/dialogs/skins/SkinManageDialog.cpp
Normal file
500
launcher/ui/dialogs/skins/SkinManageDialog.cpp
Normal file
@ -0,0 +1,500 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SkinManageDialog.h"
|
||||
#include "ui_SkinManageDialog.h"
|
||||
|
||||
#include <FileSystem.h>
|
||||
#include <QAction>
|
||||
#include <QDialog>
|
||||
#include <QEventLoop>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QKeyEvent>
|
||||
#include <QListView>
|
||||
#include <QMimeDatabase>
|
||||
#include <QPainter>
|
||||
#include <QUrl>
|
||||
|
||||
#include "Application.h"
|
||||
#include "DesktopServices.h"
|
||||
#include "Json.h"
|
||||
#include "QObjectPtr.h"
|
||||
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "minecraft/skins/CapeChange.h"
|
||||
#include "minecraft/skins/SkinDelete.h"
|
||||
#include "minecraft/skins/SkinList.h"
|
||||
#include "minecraft/skins/SkinModel.h"
|
||||
#include "minecraft/skins/SkinUpload.h"
|
||||
|
||||
#include "net/Download.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
#include "ui/instanceview/InstanceDelegate.h"
|
||||
|
||||
SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
|
||||
: QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
setWindowModality(Qt::WindowModal);
|
||||
|
||||
auto contentsWidget = ui->listView;
|
||||
contentsWidget->setViewMode(QListView::IconMode);
|
||||
contentsWidget->setFlow(QListView::LeftToRight);
|
||||
contentsWidget->setIconSize(QSize(48, 48));
|
||||
contentsWidget->setMovement(QListView::Static);
|
||||
contentsWidget->setResizeMode(QListView::Adjust);
|
||||
contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
contentsWidget->setSpacing(5);
|
||||
contentsWidget->setWordWrap(false);
|
||||
contentsWidget->setWrapping(true);
|
||||
contentsWidget->setUniformItemSizes(true);
|
||||
contentsWidget->setTextElideMode(Qt::ElideRight);
|
||||
contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
contentsWidget->installEventFilter(this);
|
||||
contentsWidget->setItemDelegate(new ListViewDelegate(this));
|
||||
|
||||
contentsWidget->setAcceptDrops(true);
|
||||
contentsWidget->setDropIndicatorShown(true);
|
||||
contentsWidget->viewport()->setAcceptDrops(true);
|
||||
contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
|
||||
contentsWidget->setDefaultDropAction(Qt::CopyAction);
|
||||
|
||||
contentsWidget->installEventFilter(this);
|
||||
contentsWidget->setModel(&m_list);
|
||||
|
||||
connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
|
||||
|
||||
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
||||
SLOT(selectionChanged(QItemSelection, QItemSelection)));
|
||||
connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu);
|
||||
|
||||
setupCapes();
|
||||
|
||||
ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin()));
|
||||
}
|
||||
|
||||
SkinManageDialog::~SkinManageDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void SkinManageDialog::activated(QModelIndex index)
|
||||
{
|
||||
m_selected_skin = index.data(Qt::UserRole).toString();
|
||||
accept();
|
||||
}
|
||||
|
||||
void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
|
||||
{
|
||||
if (selected.empty())
|
||||
return;
|
||||
|
||||
QString key = selected.first().indexes().first().data(Qt::UserRole).toString();
|
||||
if (key.isEmpty())
|
||||
return;
|
||||
m_selected_skin = key;
|
||||
auto skin = m_list.skin(key);
|
||||
if (!skin)
|
||||
return;
|
||||
ui->selectedModel->setPixmap(skin->getTexture().scaled(128, 128, Qt::KeepAspectRatio, Qt::FastTransformation));
|
||||
ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId()));
|
||||
ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC);
|
||||
ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM);
|
||||
}
|
||||
|
||||
void SkinManageDialog::delayed_scroll(QModelIndex model_index)
|
||||
{
|
||||
auto contentsWidget = ui->listView;
|
||||
contentsWidget->scrollTo(model_index);
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_openDirBtn_clicked()
|
||||
{
|
||||
DesktopServices::openPath(m_list.getDir(), true);
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_fileBtn_clicked()
|
||||
{
|
||||
auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString();
|
||||
QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter);
|
||||
auto message = m_list.installSkin(raw_path, {});
|
||||
if (!message.isEmpty()) {
|
||||
CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap previewCape(QPixmap capeImage)
|
||||
{
|
||||
QPixmap preview = QPixmap(10, 16);
|
||||
QPainter painter(&preview);
|
||||
painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
|
||||
return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation);
|
||||
}
|
||||
|
||||
void SkinManageDialog::setupCapes()
|
||||
{
|
||||
// FIXME: add a model for this, download/refresh the capes on demand
|
||||
auto& accountData = *m_acct->accountData();
|
||||
int index = 0;
|
||||
ui->capeCombo->addItem(tr("No Cape"), QVariant());
|
||||
auto currentCape = accountData.minecraftProfile.currentCape;
|
||||
if (currentCape.isEmpty()) {
|
||||
ui->capeCombo->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
auto capesDir = FS::PathCombine(m_list.getDir(), "capes");
|
||||
NetJob::Ptr job{ new NetJob(tr("Download capes"), APPLICATION->network()) };
|
||||
bool needsToDownload = false;
|
||||
for (auto& cape : accountData.minecraftProfile.capes) {
|
||||
auto path = FS::PathCombine(capesDir, cape.id + ".png");
|
||||
if (cape.data.size()) {
|
||||
QPixmap capeImage;
|
||||
if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) {
|
||||
m_capes[cape.id] = previewCape(capeImage);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (QFileInfo(path).exists()) {
|
||||
continue;
|
||||
}
|
||||
if (!cape.url.isEmpty()) {
|
||||
needsToDownload = true;
|
||||
job->addNetAction(Net::Download::makeFile(cape.url, path));
|
||||
}
|
||||
}
|
||||
if (needsToDownload) {
|
||||
ProgressDialog dlg(this);
|
||||
dlg.execWithTask(job.get());
|
||||
}
|
||||
for (auto& cape : accountData.minecraftProfile.capes) {
|
||||
index++;
|
||||
QPixmap capeImage;
|
||||
if (!m_capes.contains(cape.id)) {
|
||||
auto path = FS::PathCombine(capesDir, cape.id + ".png");
|
||||
if (QFileInfo(path).exists() && capeImage.load(path)) {
|
||||
capeImage = previewCape(capeImage);
|
||||
m_capes[cape.id] = capeImage;
|
||||
}
|
||||
}
|
||||
if (!capeImage.isNull()) {
|
||||
ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
|
||||
} else {
|
||||
ui->capeCombo->addItem(cape.alias, cape.id);
|
||||
}
|
||||
|
||||
m_capes_idx[cape.id] = index;
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
|
||||
{
|
||||
auto id = ui->capeCombo->currentData();
|
||||
ui->capeImage->setPixmap(m_capes.value(id.toString(), {}));
|
||||
if (auto skin = m_list.skin(m_selected_skin); skin) {
|
||||
skin->setCapeId(id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_steveBtn_toggled(bool checked)
|
||||
{
|
||||
if (auto skin = m_list.skin(m_selected_skin); skin) {
|
||||
skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM);
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManageDialog::accept()
|
||||
{
|
||||
auto skin = m_list.skin(m_selected_skin);
|
||||
if (!skin) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
auto path = skin->getPath();
|
||||
|
||||
ProgressDialog prog(this);
|
||||
NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) };
|
||||
|
||||
if (!QFile::exists(path)) {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString()));
|
||||
|
||||
auto selectedCape = skin->getCapeId();
|
||||
if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
|
||||
skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape));
|
||||
}
|
||||
|
||||
skinUpload->addTask(m_acct->refresh().staticCast<Task>());
|
||||
if (prog.execWithTask(skinUpload.get()) != QDialog::Accepted) {
|
||||
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
skin->setURL(m_acct->accountData()->minecraftProfile.skin.url);
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_resetBtn_clicked()
|
||||
{
|
||||
ProgressDialog prog(this);
|
||||
NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) };
|
||||
skinReset->addNetAction(SkinDelete::make(m_acct->accessToken()));
|
||||
skinReset->addTask(m_acct->refresh().staticCast<Task>());
|
||||
if (prog.execWithTask(skinReset.get()) != QDialog::Accepted) {
|
||||
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void SkinManageDialog::show_context_menu(const QPoint& pos)
|
||||
{
|
||||
QMenu myMenu(tr("Context menu"), this);
|
||||
myMenu.addAction(ui->action_Rename_Skin);
|
||||
myMenu.addAction(ui->action_Delete_Skin);
|
||||
|
||||
myMenu.exec(ui->listView->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
|
||||
{
|
||||
if (obj == ui->listView) {
|
||||
if (ev->type() == QEvent::KeyPress) {
|
||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(ev);
|
||||
switch (keyEvent->key()) {
|
||||
case Qt::Key_Delete:
|
||||
on_action_Delete_Skin_triggered(false);
|
||||
return true;
|
||||
case Qt::Key_F2:
|
||||
on_action_Rename_Skin_triggered(false);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(obj, ev);
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked)
|
||||
{
|
||||
if (!m_selected_skin.isEmpty()) {
|
||||
ui->listView->edit(ui->listView->currentIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked)
|
||||
{
|
||||
if (m_selected_skin.isEmpty())
|
||||
return;
|
||||
|
||||
if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) {
|
||||
CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec();
|
||||
return;
|
||||
}
|
||||
|
||||
auto skin = m_list.skin(m_selected_skin);
|
||||
if (!skin)
|
||||
return;
|
||||
|
||||
auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
|
||||
tr("You are about to delete \"%1\".\n"
|
||||
"Are you sure?")
|
||||
.arg(skin->name()),
|
||||
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
|
||||
->exec();
|
||||
|
||||
if (response == QMessageBox::Yes) {
|
||||
if (!m_list.deleteSkin(m_selected_skin, true)) {
|
||||
m_list.deleteSkin(m_selected_skin, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManageDialog::on_urlBtn_clicked()
|
||||
{
|
||||
auto url = QUrl(ui->urlLine->text());
|
||||
if (!url.isValid()) {
|
||||
CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show();
|
||||
return;
|
||||
}
|
||||
|
||||
NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) };
|
||||
job->setAskRetry(false);
|
||||
|
||||
auto path = FS::PathCombine(m_list.getDir(), url.fileName());
|
||||
job->addNetAction(Net::Download::makeFile(url, path));
|
||||
ProgressDialog dlg(this);
|
||||
dlg.execWithTask(job.get());
|
||||
SkinModel s(path);
|
||||
if (!s.isValid()) {
|
||||
CustomMessageBox::selectable(this, tr("URL is not a valid skin"),
|
||||
QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.")
|
||||
: tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()),
|
||||
QMessageBox::Critical)
|
||||
->show();
|
||||
QFile::remove(path);
|
||||
return;
|
||||
}
|
||||
ui->urlLine->setText("");
|
||||
if (QFileInfo(path).suffix().isEmpty()) {
|
||||
QFile::rename(path, path + ".png");
|
||||
}
|
||||
}
|
||||
|
||||
class WaitTask : public Task {
|
||||
public:
|
||||
WaitTask() : m_loop(), m_done(false){};
|
||||
virtual ~WaitTask() = default;
|
||||
|
||||
public slots:
|
||||
void quit()
|
||||
{
|
||||
m_done = true;
|
||||
m_loop.quit();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void executeTask()
|
||||
{
|
||||
if (!m_done)
|
||||
m_loop.exec();
|
||||
emitSucceeded();
|
||||
};
|
||||
|
||||
private:
|
||||
QEventLoop m_loop;
|
||||
bool m_done;
|
||||
};
|
||||
|
||||
void SkinManageDialog::on_userBtn_clicked()
|
||||
{
|
||||
auto user = ui->urlLine->text();
|
||||
if (user.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
MinecraftProfile mcProfile;
|
||||
auto path = FS::PathCombine(m_list.getDir(), user + ".png");
|
||||
|
||||
NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) };
|
||||
job->setAskRetry(false);
|
||||
|
||||
auto uuidOut = std::make_shared<QByteArray>();
|
||||
auto profileOut = std::make_shared<QByteArray>();
|
||||
|
||||
auto uuidLoop = makeShared<WaitTask>();
|
||||
auto profileLoop = makeShared<WaitTask>();
|
||||
|
||||
auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut);
|
||||
auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut);
|
||||
auto downloadSkin = Net::Download::makeFile(QUrl(), path);
|
||||
|
||||
QString failReason;
|
||||
|
||||
connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit);
|
||||
connect(getUUID.get(), &Task::failed, this, [&failReason](QString reason) {
|
||||
qCritical() << "Couldn't get user UUID:" << reason;
|
||||
failReason = tr("failed to get user UUID");
|
||||
});
|
||||
connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit);
|
||||
connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit);
|
||||
connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit);
|
||||
connect(getProfile.get(), &Task::failed, this, [&failReason](QString reason) {
|
||||
qCritical() << "Couldn't get user profile:" << reason;
|
||||
failReason = tr("failed to get user profile");
|
||||
});
|
||||
connect(downloadSkin.get(), &Task::failed, this, [&failReason](QString reason) {
|
||||
qCritical() << "Couldn't download skin:" << reason;
|
||||
failReason = tr("failed to download skin");
|
||||
});
|
||||
|
||||
connect(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile, &failReason] {
|
||||
try {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Error while parsing JSON response from Minecraft skin service at " << parse_error.offset
|
||||
<< " reason: " << parse_error.errorString();
|
||||
failReason = tr("failed to parse get user UUID response");
|
||||
uuidLoop->quit();
|
||||
return;
|
||||
}
|
||||
const auto root = doc.object();
|
||||
auto id = Json::ensureString(root, "id");
|
||||
if (!id.isEmpty()) {
|
||||
getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id);
|
||||
} else {
|
||||
failReason = tr("user id is empty");
|
||||
job->abort();
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
qCritical() << "Couldn't load skin json:" << e.cause();
|
||||
failReason = tr("failed to parse get user UUID response");
|
||||
}
|
||||
uuidLoop->quit();
|
||||
});
|
||||
|
||||
connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] {
|
||||
if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) {
|
||||
downloadSkin->setUrl(mcProfile.skin.url);
|
||||
} else {
|
||||
failReason = tr("failed to parse get user profile response");
|
||||
job->abort();
|
||||
}
|
||||
profileLoop->quit();
|
||||
});
|
||||
|
||||
job->addNetAction(getUUID);
|
||||
job->addTask(uuidLoop);
|
||||
job->addNetAction(getProfile);
|
||||
job->addTask(profileLoop);
|
||||
job->addNetAction(downloadSkin);
|
||||
ProgressDialog dlg(this);
|
||||
dlg.execWithTask(job.get());
|
||||
|
||||
SkinModel s(path);
|
||||
if (!s.isValid()) {
|
||||
if (failReason.isEmpty()) {
|
||||
failReason = tr("the skin is invalid");
|
||||
}
|
||||
CustomMessageBox::selectable(this, tr("Usename not found"),
|
||||
tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical)
|
||||
->show();
|
||||
QFile::remove(path);
|
||||
return;
|
||||
}
|
||||
ui->urlLine->setText("");
|
||||
s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC);
|
||||
s.setURL(mcProfile.skin.url);
|
||||
if (m_capes.contains(mcProfile.currentCape)) {
|
||||
s.setCapeId(mcProfile.currentCape);
|
||||
}
|
||||
m_list.updateSkin(&s);
|
||||
}
|
64
launcher/ui/dialogs/skins/SkinManageDialog.h
Normal file
64
launcher/ui/dialogs/skins/SkinManageDialog.h
Normal file
@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QItemSelection>
|
||||
#include <QPixmap>
|
||||
|
||||
#include "minecraft/auth/MinecraftAccount.h"
|
||||
#include "minecraft/skins/SkinList.h"
|
||||
|
||||
namespace Ui {
|
||||
class SkinManageDialog;
|
||||
}
|
||||
|
||||
class SkinManageDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct);
|
||||
virtual ~SkinManageDialog();
|
||||
|
||||
public slots:
|
||||
void selectionChanged(QItemSelection, QItemSelection);
|
||||
void activated(QModelIndex);
|
||||
void delayed_scroll(QModelIndex);
|
||||
void on_openDirBtn_clicked();
|
||||
void on_fileBtn_clicked();
|
||||
void on_urlBtn_clicked();
|
||||
void on_userBtn_clicked();
|
||||
void accept() override;
|
||||
void on_capeCombo_currentIndexChanged(int index);
|
||||
void on_steveBtn_toggled(bool checked);
|
||||
void on_resetBtn_clicked();
|
||||
void show_context_menu(const QPoint& pos);
|
||||
bool eventFilter(QObject* obj, QEvent* ev) override;
|
||||
void on_action_Rename_Skin_triggered(bool checked);
|
||||
void on_action_Delete_Skin_triggered(bool checked);
|
||||
|
||||
private:
|
||||
void setupCapes();
|
||||
|
||||
MinecraftAccountPtr m_acct;
|
||||
Ui::SkinManageDialog* ui;
|
||||
SkinList m_list;
|
||||
QString m_selected_skin;
|
||||
QHash<QString, QPixmap> m_capes;
|
||||
QHash<QString, int> m_capes_idx;
|
||||
};
|
220
launcher/ui/dialogs/skins/SkinManageDialog.ui
Normal file
220
launcher/ui/dialogs/skins/SkinManageDialog.ui
Normal file
@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SkinManageDialog</class>
|
||||
<widget class="QDialog" name="SkinManageDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>968</width>
|
||||
<height>757</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Skin Upload</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="mainHlLayout" stretch="3,8">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="selectedVLayout" stretch="2,1,3">
|
||||
<item>
|
||||
<widget class="QLabel" name="selectedModel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="modelBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Model</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="steveBtn">
|
||||
<property name="text">
|
||||
<string>Classic</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="alexBtn">
|
||||
<property name="text">
|
||||
<string>Slim</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="capeBox">
|
||||
<property name="title">
|
||||
<string>Cape</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QComboBox" name="capeCombo"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="capeImage">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="listView">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="modelColumn">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonsHLayout" stretch="0,0,3,0,0,0,1">
|
||||
<item>
|
||||
<widget class="QPushButton" name="openDirBtn">
|
||||
<property name="text">
|
||||
<string>Open Folder</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetBtn">
|
||||
<property name="text">
|
||||
<string>Reset Skin</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="urlLine">
|
||||
<property name="placeholderText">
|
||||
<string extracomment="URL or username"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="urlBtn">
|
||||
<property name="text">
|
||||
<string>Import URL</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="userBtn">
|
||||
<property name="text">
|
||||
<string>Import user</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="fileBtn">
|
||||
<property name="text">
|
||||
<string>Import File</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
<action name="action_Delete_Skin">
|
||||
<property name="text">
|
||||
<string>&Delete Skin</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Deletes selected skin</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Del</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Rename_Skin">
|
||||
<property name="text">
|
||||
<string>&Rename Skin</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Rename selected skin</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F2</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SkinManageDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>617</x>
|
||||
<y>736</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>483</x>
|
||||
<y>378</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SkinManageDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>617</x>
|
||||
<y>736</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>483</x>
|
||||
<y>378</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -66,6 +66,9 @@ void VisualGroup::update()
|
||||
rows[currentRow].height = maxRowHeight;
|
||||
rows[currentRow].top = offsetFromTop;
|
||||
currentRow++;
|
||||
if (currentRow >= rows.size()) {
|
||||
currentRow = rows.size() - 1;
|
||||
}
|
||||
offsetFromTop += maxRowHeight + 5;
|
||||
positionInRow = 0;
|
||||
maxRowHeight = 0;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user