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

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-06-16 00:18:11 +03:00
commit c01f3107a4
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
182 changed files with 4843 additions and 2070 deletions

View File

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

18
flake.lock generated
View File

@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1715865404, "lastModified": 1717285511,
"narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=", "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9", "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -75,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1716619601, "lastModified": 1717774105,
"narHash": "sha256-9dUxZf8MOqJH3vjbhrz7LH4qTcnRsPSBU1Q50T7q/X8=", "narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "47e03a624662ce399e55c45a5f6da698fc72c797", "rev": "d226935fd75012939397c83f6c385e4d6d832288",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -103,11 +103,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1716213921, "lastModified": 1717664902,
"narHash": "sha256-xrsYFST8ij4QWaV6HEokCUNIZLjjLP1bYC60K8XiBVA=", "narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "0e8fcc54b842ad8428c9e705cb5994eaf05c26a0", "rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -64,7 +64,8 @@ modules:
config-opts: config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON - -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON - -DGLFW_USE_WAYLAND:BOOL=ON
- -DGLFW_BUILD_DOCS:BOOL=OFF
sources: sources:
- type: git - type: git
url: https://github.com/glfw/glfw.git url: https://github.com/glfw/glfw.git

View File

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

View File

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

View File

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

View File

@ -647,6 +647,19 @@ void ExternalLinkFileProcess::runLinkFile()
qDebug() << "Process exited"; qDebug() << "Process exited";
} }
bool moveByCopy(const QString& source, const QString& dest)
{
if (!copy(source, dest)()) { // copy
qDebug() << "Copy of" << source << "to" << dest << "failed!";
return false;
}
if (!deletePath(source)) { // remove original
qDebug() << "Deletion of" << source << "failed!";
return false;
};
return true;
}
bool move(const QString& source, const QString& dest) bool move(const QString& source, const QString& dest)
{ {
std::error_code err; std::error_code err;
@ -654,13 +667,14 @@ bool move(const QString& source, const QString& dest)
ensureFilePathExists(dest); ensureFilePathExists(dest);
fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
if (err) { if (err.value() != 0) {
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()); if (moveByCopy(source, dest))
qDebug() << "Source file:" << source; return true;
qDebug() << "Destination file:" << dest; qDebug() << "Move of" << source << "to" << dest << "failed!";
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value());
return false;
} }
return true;
return err.value() == 0;
} }
bool deletePath(QString path) bool deletePath(QString path)
@ -801,25 +815,78 @@ QString NormalizePath(QString path)
} }
} }
static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n"; QString removeDuplicates(QString a)
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/"; {
auto b = a.split("");
b.removeDuplicates();
return b.join("");
}
static const QString BAD_WIN_CHARS = "\"?<>:*|\r\n";
static const QString BAD_FAT_CHARS = "<>:\"|?*+.,;=[]!";
static const QString BAD_NTFS_CHARS = "<>:\"|?*";
static const QString BAD_HFS_CHARS = ":";
static const QString BAD_FILENAME_CHARS = removeDuplicates(BAD_WIN_CHARS + BAD_FAT_CHARS + BAD_NTFS_CHARS + BAD_HFS_CHARS) + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith; string[i] = replaceWith;
return string; return string;
} }
QString RemoveInvalidPathChars(QString string, QChar replaceWith) QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) QString invalidChars;
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i))) #ifdef Q_OS_WIN
string[i] = replaceWith; invalidChars = BAD_WIN_CHARS;
#endif
return string; // the null character is ignored in this check as it was not a problem until now
switch (statFS(path).fsType) {
case FilesystemType::FAT:
invalidChars += BAD_FAT_CHARS;
break;
case FilesystemType::NTFS:
/* fallthrough */
case FilesystemType::REFS: // similar to NTFS(should be available only on windows)
invalidChars += BAD_NTFS_CHARS;
break;
// case FilesystemType::EXT:
// case FilesystemType::EXT_2_OLD:
// case FilesystemType::EXT_2_3_4:
// case FilesystemType::XFS:
// case FilesystemType::BTRFS:
// case FilesystemType::NFS:
// case FilesystemType::ZFS:
case FilesystemType::APFS:
/* fallthrough */
case FilesystemType::HFS:
/* fallthrough */
case FilesystemType::HFSPLUS:
/* fallthrough */
case FilesystemType::HFSX:
invalidChars += BAD_HFS_CHARS;
break;
// case FilesystemType::FUSEBLK:
// case FilesystemType::F2FS:
// case FilesystemType::UNKNOWN:
default:
break;
}
if (invalidChars.size() != 0) {
for (int i = 0; i < path.length(); i++) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
path[i] = replaceWith;
}
}
}
return path;
} }
QString DirNameFromString(QString string, QString inDir) QString DirNameFromString(QString string, QString inDir)

View File

@ -240,6 +240,7 @@ class create_link : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalLinked() { return m_linked; } int totalLinked() { return m_linked; }
int totalToLink() { return static_cast<int>(m_links_to_make.size()); }
void runPrivileged() { runPrivileged(QString()); } void runPrivileged() { runPrivileged(QString()); }
void runPrivileged(const QString& offset); void runPrivileged(const QString& offset);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -84,7 +84,7 @@ void LaunchController::decideAccount()
// Find an account to use. // Find an account to use.
auto accounts = APPLICATION->accounts(); auto accounts = APPLICATION->accounts();
if (accounts->count() <= 0) { if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) {
// Tell the user they need to log in at least one account in order to play. // Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Microsoft " tr("In order to play Minecraft, you must have at least one Microsoft "
@ -128,12 +128,63 @@ void LaunchController::decideAccount()
} }
} }
bool LaunchController::askPlayDemo()
{
QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?"));
box.setText(
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play "
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
return box.clickedButton() == demoButton;
}
QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok)
{
// we ask the user for a player name
QString message = tr("Choose your offline mode player name.");
if (demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok)
return {};
if (name.length()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
return usedname;
}
void LaunchController::login() void LaunchController::login()
{ {
decideAccount(); decideAccount();
// if no account is selected, we bail
if (!m_accountToUse) { if (!m_accountToUse) {
// if no account is selected, ask about demo
if (!m_demo) {
m_demo = askPlayDemo();
}
if (m_demo) {
// we ask the user for a player name
bool ok = false;
auto name = askOfflineName("Player", m_demo, ok);
if (ok) {
m_session = std::make_shared<AuthSession>();
m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]")));
launchInstance();
return;
}
}
// if no account is selected, we bail
emitFailed(tr("No account selected for launch.")); emitFailed(tr("No account selected for launch."));
return; return;
} }
@ -180,24 +231,12 @@ void LaunchController::login()
if (!m_session->wants_online) { if (!m_session->wants_online) {
// we ask the user for a player name // we ask the user for a player name
bool ok = false; bool ok = false;
auto name = askOfflineName(m_session->player_name, m_session->demo, ok);
QString message = tr("Choose your offline mode player name.");
if (m_session->demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok) { if (!ok) {
tryagain = false; tryagain = false;
break; break;
} }
if (name.length()) { m_session->MakeOffline(name);
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
m_session->MakeOffline(usedname);
// offline flavored game from here :3 // offline flavored game from here :3
} }
if (m_accountToUse->ownsMinecraft()) { if (m_accountToUse->ownsMinecraft()) {
@ -217,20 +256,10 @@ void LaunchController::login()
return; return;
} else { } else {
// play demo ? // play demo ?
QMessageBox box(m_parentWidget); if (!m_session->demo) {
box.setWindowTitle(tr("Play demo?")); m_session->demo = askPlayDemo();
box.setText( }
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " if (m_session->demo) { // play demo here
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
if (box.clickedButton() == demoButton) {
// play demo here
m_session->MakeDemo();
launchInstance(); launchInstance();
} else { } else {
emitFailed(tr("Launch cancelled - account does not own Minecraft.")); emitFailed(tr("Launch cancelled - account does not own Minecraft."));
@ -315,7 +344,7 @@ void LaunchController::launchInstance()
online_mode = "online"; online_mode = "online";
// Prepend Server Status // Prepend Server Status
QStringList servers = { "authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
QString resolved_servers = ""; QString resolved_servers = "";
QHostInfo host_info; QHostInfo host_info;

View File

@ -74,6 +74,8 @@ class LaunchController : public Task {
void login(); void login();
void launchInstance(); void launchInstance();
void decideAccount(); void decideAccount();
bool askPlayDemo();
QString askOfflineName(QString playerName, bool demo, bool& ok);
private slots: private slots:
void readyForLaunch(); void readyForLaunch();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
{ {
bool local = isLocal(); bool local = isLocal();
auto actualPath = [&](QString relPath) { auto actualPath = [&](QString relPath) {
relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
if (local && !overridePath.isEmpty()) { if (local && !overridePath.isEmpty()) {
QString fileName = out.fileName(); QString fileName = out.fileName();

View File

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

View File

@ -206,8 +206,8 @@ int64_t calculateWorldSize(const QFileInfo& file)
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
int64_t total = 0; int64_t total = 0;
while (it.hasNext()) { while (it.hasNext()) {
total += it.fileInfo().size();
it.next(); it.next();
total += it.fileInfo().size();
} }
return total; return total;
} }

View File

@ -30,8 +30,13 @@ bool AuthSession::MakeOffline(QString offline_playername)
return true; return true;
} }
void AuthSession::MakeDemo() void AuthSession::MakeDemo(QString name, QString u)
{ {
player_name = "Player"; wants_online = false;
demo = true; demo = true;
} uuid = u;
session = "-";
access_token = "0";
player_name = name;
status = PlayableOnline; // needs online to download the assets
};

View File

@ -10,7 +10,7 @@ class QNetworkAccessManager;
struct AuthSession { struct AuthSession {
bool MakeOffline(QString offline_playername); bool MakeOffline(QString offline_playername);
void MakeDemo(); void MakeDemo(QString name, QString uuid);
QString serializeUserProperties(); QString serializeUserProperties();

View File

@ -83,8 +83,6 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username; account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Validity::Certain; account->data.minecraftProfile.validity = Validity::Certain;
@ -253,6 +251,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
session->player_name = data.profileName(); session->player_name = data.profileName();
// profile ID // profile ID
session->uuid = data.profileId(); session->uuid = data.profileId();
if (session->uuid.isEmpty())
session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]"));
// 'legacy' or 'mojang', depending on account type // 'legacy' or 'mojang', depending on account type
session->user_type = typeString(); session->user_type = typeString();
if (!session->access_token.isEmpty()) { if (!session->access_token.isEmpty()) {

View File

@ -116,7 +116,7 @@ class MinecraftAccount : public QObject, public Usable {
[[nodiscard]] AccountType accountType() const noexcept { return data.type; } [[nodiscard]] AccountType accountType() const noexcept { return data.type; }
bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; }
bool hasProfile() const { return data.profileId().size() != 0; } bool hasProfile() const { return data.profileId().size() != 0; }

View File

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

View File

@ -65,29 +65,24 @@ std::pair<Version, Version> DataPack::compatibleVersions() const
return s_pack_format_versions.constFind(m_pack_format).value(); return s_pack_format_versions.constFind(m_pack_format).value();
} }
std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) const int DataPack::compare(const Resource& other, SortType type) const
{ {
auto const& cast_other = static_cast<DataPack const&>(other); auto const& cast_other = static_cast<DataPack const&>(other);
switch (type) { switch (type) {
default: { default:
auto res = Resource::compare(other, type); return Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
auto other_ver = cast_other.packFormat(); auto other_ver = cast_other.packFormat();
if (this_ver > other_ver) if (this_ver > other_ver)
return { 1, type == SortType::PACK_FORMAT }; return 1;
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return -1;
break; break;
} }
} }
return { 0, false }; return 0;
} }
bool DataPack::applyFilter(QRegularExpression filter) const bool DataPack::applyFilter(QRegularExpression filter) const

View File

@ -56,7 +56,7 @@ class DataPack : public Resource {
bool valid() const override; bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; [[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
protected: protected:

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -52,4 +53,6 @@ class Metadata {
static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_slug); } static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_slug); }
static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); }
static auto modSideToString(ModSide side) -> QString { return Packwiz::V1::sideToString(side); }
}; };

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -45,7 +46,9 @@
#include "MetadataHandler.h" #include "MetadataHandler.h"
#include "Version.h" #include "Version.h"
#include "minecraft/mod/ModDetails.h" #include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h"
static ModPlatform::ProviderCapabilities ProviderCaps; static ModPlatform::ProviderCapabilities ProviderCaps;
@ -77,7 +80,7 @@ void Mod::setDetails(const ModDetails& details)
m_local_details = details; m_local_details = details;
} }
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const int Mod::compare(const Resource& other, SortType type) const
{ {
auto cast_other = dynamic_cast<Mod const*>(&other); auto cast_other = dynamic_cast<Mod const*>(&other);
if (!cast_other) if (!cast_other)
@ -87,30 +90,49 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
default: default:
case SortType::ENABLED: case SortType::ENABLED:
case SortType::NAME: case SortType::NAME:
case SortType::DATE: { case SortType::DATE:
auto res = Resource::compare(other, type); case SortType::SIZE:
if (res.first != 0) return Resource::compare(other, type);
return res;
break;
}
case SortType::VERSION: { case SortType::VERSION: {
auto this_ver = Version(version()); auto this_ver = Version(version());
auto other_ver = Version(cast_other->version()); auto other_ver = Version(cast_other->version());
if (this_ver > other_ver) if (this_ver > other_ver)
return { 1, type == SortType::VERSION }; return 1;
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::VERSION }; return -1;
break; break;
} }
case SortType::PROVIDER: { case SortType::PROVIDER: {
auto compare_result = return QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); }
if (compare_result != 0) case SortType::SIDE: {
return { compare_result, type == SortType::PROVIDER }; if (side() > cast_other->side())
return 1;
else if (side() < cast_other->side())
return -1;
break;
}
case SortType::LOADERS: {
if (loaders() > cast_other->loaders())
return 1;
else if (loaders() < cast_other->loaders())
return -1;
break;
}
case SortType::MC_VERSIONS: {
auto thisVersion = mcVersions().join(",");
auto otherVersion = cast_other->mcVersions().join(",");
return QString::compare(thisVersion, otherVersion, Qt::CaseInsensitive);
}
case SortType::RELEASE_TYPE: {
if (releaseType() > cast_other->releaseType())
return 1;
else if (releaseType() < cast_other->releaseType())
return -1;
break; break;
} }
} }
return { 0, false }; return 0;
} }
bool Mod::applyFilter(QRegularExpression filter) const bool Mod::applyFilter(QRegularExpression filter) const
@ -232,6 +254,34 @@ auto Mod::provider() const -> std::optional<QString>
return {}; return {};
} }
auto Mod::side() const -> Metadata::ModSide
{
if (metadata())
return metadata()->side;
return Metadata::ModSide::UniversalSide;
}
auto Mod::releaseType() const -> ModPlatform::IndexedVersionType
{
if (metadata())
return metadata()->releaseType;
return ModPlatform::IndexedVersionType::VersionType::Unknown;
}
auto Mod::loaders() const -> ModPlatform::ModLoaderTypes
{
if (metadata())
return metadata()->loaders;
return {};
}
auto Mod::mcVersions() const -> QStringList
{
if (metadata())
return metadata()->mcVersions;
return {};
}
auto Mod::licenses() const -> const QList<ModLicense>& auto Mod::licenses() const -> const QList<ModLicense>&
{ {
return details().licenses; return details().licenses;

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -47,6 +48,7 @@
#include "ModDetails.h" #include "ModDetails.h"
#include "Resource.h" #include "Resource.h"
#include "modplatform/ModIndex.h"
class Mod : public Resource { class Mod : public Resource {
Q_OBJECT Q_OBJECT
@ -70,6 +72,10 @@ class Mod : public Resource {
auto licenses() const -> const QList<ModLicense>&; auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString; auto issueTracker() const -> QString;
auto metaurl() const -> QString; auto metaurl() const -> QString;
auto side() const -> Metadata::ModSide;
auto loaders() const -> ModPlatform::ModLoaderTypes;
auto mcVersions() const -> QStringList;
auto releaseType() const -> ModPlatform::IndexedVersionType;
/** Get the intneral path to the mod's icon file*/ /** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; } QString iconPath() const { return m_local_details.icon_file; }
@ -88,7 +94,7 @@ class Mod : public Resource {
bool valid() const override; bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; [[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod // Delete all the files of this mod

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -47,11 +48,12 @@
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <algorithm>
#include "Application.h" #include "Application.h"
#include "Json.h" #include "Json.h"
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h"
@ -62,12 +64,18 @@
ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir)
: ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider" }); m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders",
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); "Miecraft Versions", "Release Type" });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"),
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, tr("Size"), tr("Side"), tr("Loaders"), tr("Miecraft Versions"), tr("Release Type") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION,
SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE,
SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true }; m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, false, false, true, true, true, true };
} }
QVariant ModFolderModel::data(const QModelIndex& index, int role) const QVariant ModFolderModel::data(const QModelIndex& index, int role) const
@ -105,12 +113,34 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
return provider.value(); return provider.value();
} }
case SideColumn: {
return Metadata::modSideToString(at(row)->side());
}
case LoadersColumn: {
QStringList loaders;
auto modLoaders = at(row)->loaders();
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader,
ModPlatform::Fabric, ModPlatform::Quilt }) {
if (modLoaders & loader) {
loaders << getModLoaderAsString(loader);
}
}
return loaders.join(", ");
}
case McVersionsColumn: {
return at(row)->mcVersions().join(", ");
}
case ReleaseTypeColumn: {
return at(row)->releaseType().toString();
}
case SizeColumn:
return m_resources[row]->sizeStr();
default: default:
return QVariant(); return QVariant();
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
if (column == NAME_COLUMN) { if (column == NameColumn) {
if (at(row)->isSymLinkUnder(instDirPath())) { if (at(row)->isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
@ -124,7 +154,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
} }
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::DecorationRole: { case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow"); return APPLICATION->getThemedIcon("status-yellow");
if (column == ImageColumn) { if (column == ImageColumn) {
return at(row)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); return at(row)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
@ -159,6 +189,11 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
case DateColumn: case DateColumn:
case ProviderColumn: case ProviderColumn:
case ImageColumn: case ImageColumn:
case SideColumn:
case LoadersColumn:
case McVersionsColumn:
case ReleaseTypeColumn:
case SizeColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return QVariant(); return QVariant();
@ -176,6 +211,16 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
return tr("The date and time this mod was last changed (or added)."); return tr("The date and time this mod was last changed (or added).");
case ProviderColumn: case ProviderColumn:
return tr("Where the mod was downloaded from."); return tr("Where the mod was downloaded from.");
case SideColumn:
return tr("On what environment the mod is running.");
case LoadersColumn:
return tr("The mod loader.");
case McVersionsColumn:
return tr("The supported minecraft versions.");
case ReleaseTypeColumn:
return tr("The release type.");
case SizeColumn:
return tr("The size of the mod.");
default: default:
return QVariant(); return QVariant();
} }

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -61,7 +62,20 @@ class QFileSystemWatcher;
class ModFolderModel : public ResourceFolderModel { class ModFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, NUM_COLUMNS }; enum Columns {
ActiveColumn = 0,
ImageColumn,
NameColumn,
VersionColumn,
DateColumn,
ProviderColumn,
SizeColumn,
SideColumn,
LoadersColumn,
McVersionsColumn,
ReleaseTypeColumn,
NUM_COLUMNS
};
enum ModStatusAction { Disable, Enable, Toggle }; enum ModStatusAction { Disable, Enable, Toggle };
ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);

View File

@ -1,9 +1,12 @@
#include "Resource.h" #include "Resource.h"
#include <QDirIterator>
#include <QFileInfo> #include <QFileInfo>
#include <QRegularExpression> #include <QRegularExpression>
#include <tuple>
#include "FileSystem.h" #include "FileSystem.h"
#include "StringUtils.h"
Resource::Resource(QObject* parent) : QObject(parent) {} Resource::Resource(QObject* parent) : QObject(parent) {}
@ -18,6 +21,20 @@ void Resource::setFile(QFileInfo file_info)
parseFile(); parseFile();
} }
std::tuple<QString, qint64> calculateFileSize(const QFileInfo& file)
{
if (file.isDir()) {
auto dir = QDir(file.absoluteFilePath());
dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
auto count = dir.count();
auto str = QObject::tr("item");
if (count != 1)
str = QObject::tr("items");
return { QString("%1 %2").arg(QString::number(count), str), count };
}
return { StringUtils::humanReadableFileSize(file.size(), true), file.size() };
}
void Resource::parseFile() void Resource::parseFile()
{ {
QString file_name{ m_file_info.fileName() }; QString file_name{ m_file_info.fileName() };
@ -26,6 +43,7 @@ void Resource::parseFile()
m_internal_id = file_name; m_internal_id = file_name;
std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info);
if (m_file_info.isDir()) { if (m_file_info.isDir()) {
m_type = ResourceType::FOLDER; m_type = ResourceType::FOLDER;
m_name = file_name; m_name = file_name;
@ -61,15 +79,15 @@ static void removeThePrefix(QString& string)
string = string.trimmed(); string = string.trimmed();
} }
std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const int Resource::compare(const Resource& other, SortType type) const
{ {
switch (type) { switch (type) {
default: default:
case SortType::ENABLED: case SortType::ENABLED:
if (enabled() && !other.enabled()) if (enabled() && !other.enabled())
return { 1, type == SortType::ENABLED }; return 1;
if (!enabled() && other.enabled()) if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED }; return -1;
break; break;
case SortType::NAME: { case SortType::NAME: {
QString this_name{ name() }; QString this_name{ name() };
@ -78,20 +96,31 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
removeThePrefix(this_name); removeThePrefix(this_name);
removeThePrefix(other_name); removeThePrefix(other_name);
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); return QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::NAME };
break;
} }
case SortType::DATE: case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged()) if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE }; return 1;
if (dateTimeChanged() < other.dateTimeChanged()) if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE }; return -1;
break; break;
case SortType::SIZE: {
if (this->type() != other.type()) {
if (this->type() == ResourceType::FOLDER)
return -1;
if (other.type() == ResourceType::FOLDER)
return 1;
} }
return { 0, false }; if (sizeInfo() > other.sizeInfo())
return 1;
if (sizeInfo() < other.sizeInfo())
return -1;
break;
}
}
return 0;
} }
bool Resource::applyFilter(QRegularExpression filter) const bool Resource::applyFilter(QRegularExpression filter) const

View File

@ -1,3 +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/>.
*
* 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.
*/
#pragma once #pragma once
#include <QDateTime> #include <QDateTime>
@ -15,7 +50,7 @@ enum class ResourceType {
LITEMOD, //!< The resource is a litemod LITEMOD, //!< The resource is a litemod
}; };
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER }; enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, LOADERS, MC_VERSIONS, RELEASE_TYPE };
enum class EnableAction { ENABLE, DISABLE, TOGGLE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE };
@ -45,6 +80,8 @@ class Resource : public QObject {
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
[[nodiscard]] auto type() const -> ResourceType { return m_type; } [[nodiscard]] auto type() const -> ResourceType { return m_type; }
[[nodiscard]] bool enabled() const { return m_enabled; } [[nodiscard]] bool enabled() const { return m_enabled; }
[[nodiscard]] QString sizeStr() const { return m_size_str; }
[[nodiscard]] qint64 sizeInfo() const { return m_size_info; }
[[nodiscard]] virtual auto name() const -> QString { return m_name; } [[nodiscard]] virtual auto name() const -> QString { return m_name; }
[[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
@ -53,10 +90,8 @@ class Resource : public QObject {
* > 0: 'this' comes after 'other' * > 0: 'this' comes after 'other'
* = 0: 'this' is equal to 'other' * = 0: 'this' is equal to 'other'
* < 0: 'this' comes before 'other' * < 0: 'this' comes before 'other'
*
* The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
*/ */
[[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>; [[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const;
/** Returns whether the given filter should filter out 'this' (false), /** Returns whether the given filter should filter out 'this' (false),
* or if such filter includes the Resource (true). * or if such filter includes the Resource (true).
@ -117,4 +152,6 @@ class Resource : public QObject {
bool m_is_resolving = false; bool m_is_resolving = false;
bool m_is_resolved = false; bool m_is_resolved = false;
int m_resolution_ticket = 0; int m_resolution_ticket = 0;
QString m_size_str;
qint64 m_size_info;
}; };

View File

@ -16,6 +16,7 @@
#include "FileSystem.h" #include "FileSystem.h"
#include "QVariantUtils.h" #include "QVariantUtils.h"
#include "StringUtils.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "settings/Setting.h" #include "settings/Setting.h"
@ -111,7 +112,7 @@ bool ResourceFolderModel::installResource(QString original_path)
case ResourceType::ZIPFILE: case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: { case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
if (!QFile::remove(new_path)) { if (!FS::deletePath(new_path)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
return false; return false;
} }
@ -416,15 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (column) { switch (column) {
case NAME_COLUMN: case NameColumn:
return m_resources[row]->name(); return m_resources[row]->name();
case DATE_COLUMN: case DateColumn:
return m_resources[row]->dateTimeChanged(); return m_resources[row]->dateTimeChanged();
case SizeColumn:
return m_resources[row]->sizeStr();
default: default:
return {}; return {};
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
if (column == NAME_COLUMN) { if (column == NameColumn) {
if (at(row).isSymLinkUnder(instDirPath())) { if (at(row).isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
@ -440,14 +443,14 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::DecorationRole: { case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow"); return APPLICATION->getThemedIcon("status-yellow");
return {}; return {};
} }
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { switch (column) {
case ACTIVE_COLUMN: case ActiveColumn:
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
default: default:
return {}; return {};
@ -486,24 +489,27 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (section) { switch (section) {
case ACTIVE_COLUMN: case ActiveColumn:
case NAME_COLUMN: case NameColumn:
case DATE_COLUMN: case DateColumn:
case SizeColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
} }
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
switch (section) { switch (section) {
case ACTIVE_COLUMN: case ActiveColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("Is the resource enabled?"); return tr("Is the resource enabled?");
case NAME_COLUMN: case NameColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The name of the resource."); return tr("The name of the resource.");
case DATE_COLUMN: case DateColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The date and time this resource was last changed (or added)."); return tr("The date and time this resource was last changed (or added).");
case SizeColumn:
return tr("The size of the resource.");
default: default:
return {}; return {};
} }
@ -533,6 +539,10 @@ void ResourceFolderModel::saveColumns(QTreeView* tree)
void ResourceFolderModel::loadColumns(QTreeView* tree) void ResourceFolderModel::loadColumns(QTreeView* tree)
{ {
for (auto i = 0; i < m_columnsHiddenByDefault.size(); ++i) {
tree->setColumnHidden(i, m_columnsHiddenByDefault[i]);
}
auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); auto const setting_name = QString("UI/%1_Page/Columns").arg(id());
auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name)
: m_instance->settings()->registerSetting(setting_name); : m_instance->settings()->registerSetting(setting_name);
@ -610,12 +620,10 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
auto const& resource_right = model->at(source_right.row()); auto const& resource_right = model->at(source_right.row());
auto compare_result = resource_left.compare(resource_right, column_sort_key); auto compare_result = resource_left.compare(resource_right, column_sort_key);
if (compare_result.first == 0) if (compare_result == 0)
return QSortFilterProxyModel::lessThan(source_left, source_right); return QSortFilterProxyModel::lessThan(source_left, source_right);
if (compare_result.second || sortOrder() != Qt::DescendingOrder) return compare_result < 0;
return (compare_result.first < 0);
return (compare_result.first > 0);
} }
QString ResourceFolderModel::instDirPath() const QString ResourceFolderModel::instDirPath() const

View File

@ -96,7 +96,7 @@ class ResourceFolderModel : public QAbstractListModel {
/* Qt behavior */ /* Qt behavior */
/* Basic columns */ /* Basic columns */
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; enum Columns { ActiveColumn = 0, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS };
QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }
[[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); } [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
@ -195,11 +195,13 @@ class ResourceFolderModel : public QAbstractListModel {
protected: protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key. // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
// As such, the order in with they appear is very important! // As such, the order in with they appear is very important!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE };
QStringList m_column_names = { "Enable", "Name", "Last Modified" }; QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" };
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") }; QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Size") };
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QList<bool> m_columnsHideable = { false, false, true }; QHeaderView::Interactive };
QList<bool> m_columnsHideable = { false, false, true, true };
QList<bool> m_columnsHiddenByDefault = { false, false, false, false };
QDir m_dir; QDir m_dir;
BaseInstance* m_instance; BaseInstance* m_instance;

View File

@ -94,29 +94,24 @@ std::pair<Version, Version> ResourcePack::compatibleVersions() const
return s_pack_format_versions.constFind(m_pack_format).value(); return s_pack_format_versions.constFind(m_pack_format).value();
} }
std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const int ResourcePack::compare(const Resource& other, SortType type) const
{ {
auto const& cast_other = static_cast<ResourcePack const&>(other); auto const& cast_other = static_cast<ResourcePack const&>(other);
switch (type) { switch (type) {
default: { default:
auto res = Resource::compare(other, type); return Resource::compare(other, type);
if (res.first != 0)
return res;
break;
}
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
auto other_ver = cast_other.packFormat(); auto other_ver = cast_other.packFormat();
if (this_ver > other_ver) if (this_ver > other_ver)
return { 1, type == SortType::PACK_FORMAT }; return 1;
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return -1;
break; break;
} }
} }
return { 0, false }; return 0;
} }
bool ResourcePack::applyFilter(QRegularExpression filter) const bool ResourcePack::applyFilter(QRegularExpression filter) const

View File

@ -44,7 +44,7 @@ class ResourcePack : public Resource {
bool valid() const override; bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; [[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
protected: protected:

View File

@ -44,17 +44,19 @@
#include "Application.h" #include "Application.h"
#include "Version.h" #include "Version.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::SIZE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
QHeaderView::Interactive }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true }; m_columnsHideable = { false, true, false, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, false };
} }
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
@ -85,6 +87,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
} }
case DateColumn: case DateColumn:
return m_resources[row]->dateTimeChanged(); return m_resources[row]->dateTimeChanged();
case SizeColumn:
return m_resources[row]->sizeStr();
default: default:
return {}; return {};
@ -144,6 +148,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
case PackFormatColumn: case PackFormatColumn:
case DateColumn: case DateColumn:
case ImageColumn: case ImageColumn:
case SizeColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -160,6 +165,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
case DateColumn: case DateColumn:
return tr("The date and time this resource pack was last changed (or added)."); return tr("The date and time this resource pack was last changed (or added).");
case SizeColumn:
return tr("The size of the resource pack.");
default: default:
return {}; return {};
} }

View File

@ -7,7 +7,7 @@
class ResourcePackFolderModel : public ResourceFolderModel { class ResourcePackFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, SizeColumn, NUM_COLUMNS };
explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance); explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance);

View File

@ -37,6 +37,7 @@
#include "Application.h" #include "Application.h"
#include "StringUtils.h"
#include "TexturePackFolderModel.h" #include "TexturePackFolderModel.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
@ -44,11 +45,12 @@
TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" }); m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Size" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Size") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
m_columnsHideable = { false, true, false, true }; QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true };
} }
Task* TexturePackFolderModel::createUpdateTask() Task* TexturePackFolderModel::createUpdateTask()
@ -76,6 +78,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
return m_resources[row]->name(); return m_resources[row]->name();
case DateColumn: case DateColumn:
return m_resources[row]->dateTimeChanged(); return m_resources[row]->dateTimeChanged();
case SizeColumn:
return m_resources[row]->sizeStr();
default: default:
return {}; return {};
} }
@ -127,6 +131,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
case NameColumn: case NameColumn:
case DateColumn: case DateColumn:
case ImageColumn: case ImageColumn:
case SizeColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -135,13 +140,15 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
switch (section) { switch (section) {
case ActiveColumn: case ActiveColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("Is the resource enabled?"); return tr("Is the texture pack enabled?");
case NameColumn: case NameColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The name of the resource."); return tr("The name of the texture pack.");
case DateColumn: case DateColumn:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The date and time this resource was last changed (or added)."); return tr("The date and time this texture pack was last changed (or added).");
case SizeColumn:
return tr("The size of the texture pack.");
default: default:
return {}; return {};
} }

View File

@ -44,7 +44,7 @@ class TexturePackFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, NUM_COLUMNS }; enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS };
explicit TexturePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance); explicit TexturePackFolderModel(const QString& dir, std::shared_ptr<const BaseInstance> instance);

View File

@ -23,6 +23,7 @@
#include <memory> #include <memory>
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/MetadataHandler.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h" #include "modplatform/ResourceAPI.h"
@ -44,6 +45,14 @@ static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst)
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders().value(); return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders().value();
} }
static bool checkDependencies(std::shared_ptr<GetModDependenciesTask::PackDependency> sel,
Version mcVersion,
ModPlatform::ModLoaderTypes loaders)
{
return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) &&
(!loaders || !sel->version.loaders || sel->version.loaders & loaders);
}
GetModDependenciesTask::GetModDependenciesTask(QObject* parent, GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
BaseInstance* instance, BaseInstance* instance,
ModFolderModel* folder, ModFolderModel* folder,
@ -68,6 +77,7 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
void GetModDependenciesTask::prepare() void GetModDependenciesTask::prepare()
{ {
for (auto sel : m_selected) { for (auto sel : m_selected) {
if (checkDependencies(sel, m_version, m_loaderType))
for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) {
addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); addTask(prepareDependencyTask(dep, sel->pack->provider, 20));
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -94,10 +95,9 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
{ {
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1; QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) { switch (p) {
case ResourceProvider::MODRINTH: { case ResourceProvider::MODRINTH:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512; algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break; break;
}
case ResourceProvider::FLAME: case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5; algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break; break;
@ -117,7 +117,7 @@ QString getMetaURL(ResourceProvider provider, QVariant projectID)
projectID.toString(); projectID.toString();
} }
auto getModLoaderString(ModLoaderType type) -> const QString auto getModLoaderAsString(ModLoaderType type) -> const QString
{ {
switch (type) { switch (type) {
case NeoForge: case NeoForge:
@ -138,4 +138,21 @@ auto getModLoaderString(ModLoaderType type) -> const QString
return ""; return "";
} }
auto getModLoaderFromString(QString type) -> ModLoaderType
{
if (type == "neoforge")
return NeoForge;
if (type == "forge")
return Forge;
if (type == "cauldron")
return Cauldron;
if (type == "liteloader")
return LiteLoader;
if (type == "fabric")
return Fabric;
if (type == "quilt")
return Quilt;
return {};
}
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -109,6 +109,7 @@ struct IndexedVersion {
bool is_preferred = true; bool is_preferred = true;
QString changelog; QString changelog;
QList<Dependency> dependencies; QList<Dependency> dependencies;
QString side; // this is for flame API
// For internal use, not provided by APIs // For internal use, not provided by APIs
bool is_currently_selected = false; bool is_currently_selected = false;
@ -183,7 +184,8 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
QString getMetaURL(ResourceProvider provider, QVariant projectID); QString getMetaURL(ResourceProvider provider, QVariant projectID);
auto getModLoaderString(ModLoaderType type) -> const QString; auto getModLoaderAsString(ModLoaderType type) -> const QString;
auto getModLoaderFromString(QString type) -> ModLoaderType;
constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
{ {
@ -191,6 +193,11 @@ constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
return x && !(x & (x - 1)); return x && !(x & (x - 1));
} }
struct Category {
QString name;
QString id;
};
} // namespace ModPlatform } // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack) Q_DECLARE_METATYPE(ModPlatform::IndexedPack)

View File

@ -4,6 +4,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -73,6 +74,8 @@ class ResourceAPI {
std::optional<SortingMethod> sorting; std::optional<SortingMethod> sorting;
std::optional<ModPlatform::ModLoaderTypes> loaders; std::optional<ModPlatform::ModLoaderTypes> loaders;
std::optional<std::list<Version> > versions; std::optional<std::list<Version> > versions;
std::optional<QString> side;
std::optional<QStringList> categoryIds;
}; };
struct SearchCallbacks { struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed; std::function<void(QJsonDocument&)> on_succeed;

View File

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

View File

@ -3,10 +3,12 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
#include "FlameAPI.h" #include "FlameAPI.h"
#include <memory>
#include "FlameModIndex.h" #include "FlameModIndex.h"
#include "Application.h" #include "Application.h"
#include "Json.h" #include "Json.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/ApiUpload.h" #include "net/ApiUpload.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -220,3 +222,42 @@ QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
{ 7, "Category", QObject::tr("Sort by Category") }, { 7, "Category", QObject::tr("Sort by Category") },
{ 8, "GameVersion", QObject::tr("Sort by Game Version") } }; { 8, "GameVersion", QObject::tr("Sort by Game Version") } };
} }
Task::Ptr FlameAPI::getModCategories(std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::GetCategories"), APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl("https://api.curseforge.com/v1/categories?gameId=432&classId=6"), response));
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; });
return netJob;
}
QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteArray> response)
{
QList<ModPlatform::Category> categories;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return categories;
}
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto id = Json::requireInteger(cat, "id");
auto name = Json::requireString(cat, "name");
categories.push_back({ name, QString::number(id) });
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
return categories;
};

View File

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <QList>
#include <algorithm> #include <algorithm>
#include <memory> #include <memory>
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
@ -22,6 +23,9 @@ class FlameAPI : public NetworkResourceAPI {
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const; Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override; [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
@ -96,6 +100,9 @@ class FlameAPI : public NetworkResourceAPI {
get_arguments.append("sortOrder=desc"); get_arguments.append("sortOrder=desc");
if (args.loaders.has_value()) if (args.loaders.has_value())
get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value())));
if (args.categoryIds.has_value() && !args.categoryIds->empty())
get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(",")));
get_arguments.append(gameVersionStr); get_arguments.append(gameVersionStr);
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');

View File

@ -322,7 +322,7 @@ bool FlameCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
setError(tr("Could not understand pack manifest:\n") + e.cause()); setError(tr("Could not understand pack manifest:\n") + e.cause());
@ -336,7 +336,7 @@ bool FlameCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, overridePath); Override::createOverrides("overrides", parent_folder, overridePath);
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath)) { if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
return false; return false;
} }
@ -538,9 +538,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
} }
for (const auto& result : results) { for (const auto& result : results) {
auto fileName = result.fileName; auto fileName = result.fileName;
#ifdef Q_OS_WIN
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
#endif
auto relpath = FS::PathCombine(result.targetFolder, fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName);
if (!result.required && !selectedOptionalMods.contains(relpath)) { if (!result.required && !selectedOptionalMods.contains(relpath)) {

View File

@ -80,10 +80,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const BaseInstance* inst) const BaseInstance* inst)
{ {
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
for (auto versionIter : arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
@ -91,8 +87,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
if (!file.addonId.isValid()) if (!file.addonId.isValid())
file.addonId = pack.addonId; file.addonId = pack.addonId;
if (file.fileId.isValid() && if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
unsortedVersions.append(file); unsortedVersions.append(file);
} }
@ -118,19 +113,25 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
if (str.contains('.')) if (str.contains('.'))
file.mcVersion.append(str); file.mcVersion.append(str);
auto loader = str.toLower();
if (loader == "neoforge") if (auto loader = str.toLower(); loader == "neoforge")
file.loaders |= ModPlatform::NeoForge; file.loaders |= ModPlatform::NeoForge;
if (loader == "forge") else if (loader == "forge")
file.loaders |= ModPlatform::Forge; file.loaders |= ModPlatform::Forge;
if (loader == "cauldron") else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron; file.loaders |= ModPlatform::Cauldron;
if (loader == "liteloader") else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader; file.loaders |= ModPlatform::LiteLoader;
if (loader == "fabric") else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric; file.loaders |= ModPlatform::Fabric;
if (loader == "quilt") else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt; file.loaders |= ModPlatform::Quilt;
else if (loader == "server" || loader == "client") {
if (file.side.isEmpty())
file.side = loader;
else if (file.side != loader)
file.side = "both";
}
} }
file.addonId = Json::requireInteger(obj, "modId"); file.addonId = Json::requireInteger(obj, "modId");
@ -139,9 +140,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.version = Json::requireString(obj, "displayName"); file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName"); file.fileName = Json::requireString(obj, "fileName");
#ifdef Q_OS_WIN
file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.fileName = FS::RemoveInvalidPathChars(file.fileName);
#endif
ModPlatform::IndexedVersionType::VersionType ver_type; ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) { switch (Json::requireInteger(obj, "releaseType")) {

View File

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

View File

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

View File

@ -120,3 +120,41 @@ QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
{ 4, "newest", QObject::tr("Sort by Newest") }, { 4, "newest", QObject::tr("Sort by Newest") },
{ 5, "updated", QObject::tr("Sort by Last Updated") } }; { 5, "updated", QObject::tr("Sort by Last Updated") } };
} }
Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCategories"), APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response));
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; });
return netJob;
}
QList<ModPlatform::Category> ModrinthAPI::loadModCategories(std::shared_ptr<QByteArray> response)
{
QList<ModPlatform::Category> categories;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return categories;
}
try {
auto arr = Json::requireArray(doc);
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto name = Json::requireString(cat, "name");
if (Json::ensureString(cat, "project_type", "") == "mod")
categories.push_back({ name, name });
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
return categories;
};

View File

@ -30,6 +30,9 @@ class ModrinthAPI : public NetworkResourceAPI {
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override; Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
public: public:
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override; [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
@ -41,7 +44,7 @@ class ModrinthAPI : public NetworkResourceAPI {
for (auto loader : for (auto loader :
{ ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) {
if (types & loader) { if (types & loader) {
l << getModLoaderString(loader); l << getModLoaderAsString(loader);
} }
} }
return l; return l;
@ -56,6 +59,27 @@ class ModrinthAPI : public NetworkResourceAPI {
return l.join(','); return l.join(',');
} }
static auto getCategoriesFilters(QStringList categories) -> const QString
{
QStringList l;
for (auto cat : categories) {
l << QString("\"categories:%1\"").arg(cat);
}
return l.join(',');
}
static auto getSideFilters(QString side) -> const QString
{
if (side.isEmpty() || side == "both") {
return {};
}
if (side == "client")
return QString("\"client_side:required\",\"client_side:optional\"");
if (side == "server")
return QString("\"server_side:required\",\"server_side:optional\"");
return {};
}
private: private:
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
{ {
@ -73,6 +97,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return ""; return "";
} }
[[nodiscard]] QString createFacets(SearchArgs const& args) const [[nodiscard]] QString createFacets(SearchArgs const& args) const
{ {
QStringList facets_list; QStringList facets_list;
@ -81,6 +106,14 @@ class ModrinthAPI : public NetworkResourceAPI {
facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
if (args.versions.has_value()) if (args.versions.has_value())
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
if (args.side.has_value()) {
auto side = getSideFilters(args.side.value());
if (!side.isEmpty())
facets_list.append(QString("[%1]").arg(side));
}
if (args.categoryIds.has_value() && !args.categoryIds->empty())
facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value())));
facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
return QString("[%1]").arg(facets_list.join(',')); return QString("[%1]").arg(facets_list.join(','));

View File

@ -112,7 +112,7 @@ void ModrinthCheckUpdate::executeTask()
ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt }; ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt };
for (auto flag : flags) { for (auto flag : flags) {
if (m_loaders.value().testFlag(flag)) { if (m_loaders.value().testFlag(flag)) {
loader_filter = ModPlatform::getModLoaderString(flag); loader_filter = ModPlatform::getModLoaderAsString(flag);
break; break;
} }
} }

View File

@ -173,7 +173,7 @@ bool ModrinthCreationTask::createInstance()
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(new_index_place);
QFile::rename(index_path, new_index_place); FS::move(index_path, new_index_place);
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
@ -183,7 +183,7 @@ bool ModrinthCreationTask::createInstance()
Override::createOverrides("overrides", parent_folder, override_path); Override::createOverrides("overrides", parent_folder, override_path);
// Apply the overrides // Apply the overrides
if (!QFile::rename(override_path, mcPath)) { if (!FS::move(override_path, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides"); setError(tr("Could not rename the overrides folder:\n") + "overrides");
return false; return false;
} }
@ -241,9 +241,7 @@ bool ModrinthCreationTask::createInstance()
for (auto file : m_files) { for (auto file : m_files) {
auto fileName = file.path; auto fileName = file.path;
#ifdef Q_OS_WIN
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
#endif
auto file_path = FS::PathCombine(root_modpack_path, fileName); auto file_path = FS::PathCombine(root_modpack_path, fileName);
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) { if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
// This means we somehow got out of the root folder, so abort here to prevent exploits // This means we somehow got out of the root folder, so abort here to prevent exploits

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -115,16 +116,11 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst)
{ {
QVector<ModPlatform::IndexedVersion> unsortedVersions; QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
for (auto versionIter : arr) { for (auto versionIter : arr) {
auto obj = versionIter.toObject(); auto obj = versionIter.toObject();
auto file = loadIndexedPackVersion(obj); auto file = loadIndexedPackVersion(obj);
if (file.fileId.isValid() && if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
unsortedVersions.append(file); unsortedVersions.append(file);
} }
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
@ -155,15 +151,15 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
for (auto loader : loaders) { for (auto loader : loaders) {
if (loader == "neoforge") if (loader == "neoforge")
file.loaders |= ModPlatform::NeoForge; file.loaders |= ModPlatform::NeoForge;
if (loader == "forge") else if (loader == "forge")
file.loaders |= ModPlatform::Forge; file.loaders |= ModPlatform::Forge;
if (loader == "cauldron") else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron; file.loaders |= ModPlatform::Cauldron;
if (loader == "liteloader") else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader; file.loaders |= ModPlatform::LiteLoader;
if (loader == "fabric") else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric; file.loaders |= ModPlatform::Fabric;
if (loader == "quilt") else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt; file.loaders |= ModPlatform::Quilt;
} }
file.version = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "name");
@ -227,9 +223,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
if (parent.contains("url")) { if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url"); file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename"); file.fileName = Json::requireString(parent, "filename");
#ifdef Q_OS_WIN
file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.fileName = FS::RemoveInvalidPathChars(file.fileName);
#endif
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes"); auto hash_list = Json::requireObject(parent, "hashes");

View File

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

View File

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

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -113,7 +114,11 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP
mod.provider = mod_pack.provider; mod.provider = mod_pack.provider;
mod.file_id = mod_version.fileId; mod.file_id = mod_version.fileId;
mod.project_id = mod_pack.addonId; mod.project_id = mod_pack.addonId;
mod.side = stringToSide(mod_pack.side); mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side);
mod.loaders = mod_version.loaders;
mod.mcVersions = mod_version.mcVersion;
mod.mcVersions.sort();
mod.releaseType = mod_version.version_type;
return mod; return mod;
} }
@ -181,6 +186,18 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
break; break;
} }
toml::array loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
ModPlatform::Quilt }) {
if (mod.loaders & loader) {
loaders.push_back(getModLoaderAsString(loader).toStdString());
}
}
toml::array mcVersions;
for (auto version : mod.mcVersions) {
mcVersions.push_back(version.toStdString());
}
if (!index_file.open(QIODevice::ReadWrite)) { if (!index_file.open(QIODevice::ReadWrite)) {
qCritical() << QString("Could not open file %1!").arg(normalized_fname); qCritical() << QString("Could not open file %1!").arg(normalized_fname);
return; return;
@ -192,6 +209,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
auto tbl = toml::table{ { "name", mod.name.toStdString() }, auto tbl = toml::table{ { "name", mod.name.toStdString() },
{ "filename", mod.filename.toStdString() }, { "filename", mod.filename.toStdString() },
{ "side", sideToString(mod.side).toStdString() }, { "side", sideToString(mod.side).toStdString() },
{ "loaders", loaders },
{ "mcVersions", mcVersions },
{ "releaseType", mod.releaseType.toString().toStdString() },
{ "download", { "download",
toml::table{ toml::table{
{ "mode", mod.mode.toStdString() }, { "mode", mod.mode.toStdString() },
@ -276,6 +296,25 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
mod.name = stringEntry(table, "name"); mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename"); mod.filename = stringEntry(table, "filename");
mod.side = stringToSide(stringEntry(table, "side")); mod.side = stringToSide(stringEntry(table, "side"));
mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType"));
if (auto loaders = table["loaders"]; loaders && loaders.is_array()) {
for (auto&& loader : *loaders.as_array()) {
if (loader.is_string()) {
mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or("")));
}
}
}
if (auto versions = table["mcVersions"]; versions && versions.is_array()) {
for (auto&& version : *versions.as_array()) {
if (version.is_string()) {
auto ver = QString::fromStdString(version.as_string()->value_or(""));
if (!ver.isEmpty()) {
mod.mcVersions << ver;
}
}
}
mod.mcVersions.sort();
}
} }
{ // [download] info { // [download] info

View File

@ -2,6 +2,7 @@
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -41,6 +42,9 @@ class V1 {
QString name{}; QString name{};
QString filename{}; QString filename{};
Side side{ Side::UniversalSide }; Side side{ Side::UniversalSide };
ModPlatform::ModLoaderTypes loaders;
QStringList mcVersions;
ModPlatform::IndexedVersionType releaseType;
// [download] // [download]
QString mode{}; QString mode{};

View File

@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
data = file.readAll(); data = file.readAll();
file.close(); file.close();
} else { } else {
if (minecraftVersion.isEmpty()) if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
return;
}
components->setComponentVersion("net.minecraft", minecraftVersion, true); components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({ modpackJar }); components->installJarMods({ modpackJar });
@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
file.close(); file.close();
} else { } else {
// This is the "Vanilla" modpack, excluded by the search code // This is the "Vanilla" modpack, excluded by the search code
emit failed(tr("Unable to find a \"version.json\"!")); components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->saveNow();
emit succeeded();
return; return;
} }

View File

@ -84,9 +84,7 @@ auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPt
auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
{ {
#ifdef Q_OS_WIN
resource_path = FS::RemoveInvalidPathChars(resource_path); resource_path = FS::RemoveInvalidPathChars(resource_path);
#endif
auto entry = getEntry(base, resource_path); auto entry = getEntry(base, resource_path);
// it's not present? generate a default stale entry // it's not present? generate a default stale entry
if (!entry) { if (!entry) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@
Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots) Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots)
{ {
auto up = makeShared<ImgurAlbumCreation>(); auto up = makeShared<ImgurAlbumCreation>();
up->m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; up->m_url = BuildConfig.IMGUR_BASE_URL + "album";
up->m_sink.reset(new Sink(output)); up->m_sink.reset(new Sink(output));
up->m_screenshots = screenshots; up->m_screenshots = screenshots;
return up; return up;
@ -72,7 +72,7 @@ void ImgurAlbumCreation::init()
qDebug() << "Setting up imgur upload"; qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy( auto api_headers = new Net::StaticHeaderProxy(
QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" }, QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" },
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() }, { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() },
{ "Accept", "application/json" } }); { "Accept", "application/json" } });
addHeaderProxy(api_headers); addHeaderProxy(api_headers);
} }

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