Merge branch 'develop' into better-component-installation

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2023-07-11 11:14:06 +01:00 committed by GitHub
commit 4c69290381
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1443 additions and 662 deletions

28
.github/workflows/update-flake.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Update Flake Lockfile
on:
schedule:
# run weekly on sunday
- cron: "0 0 * * 0"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake:
if: github.repository == 'PrismLauncher/PrismLauncher'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/update-flake-lock@v19
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
pr-labels: |
Linux
simple change

48
flake.lock generated
View File

@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1683560683, "lastModified": 1688254665,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=", "narHash": "sha256-8FHEgBrr7gYNiS/NzCxIO3m4hvtLRW9YY1nYo1ivm3o=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17", "rev": "267149c58a14d15f7f81b4d737308421de9d7152",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -35,12 +35,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1685518550,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -88,11 +91,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1685012353, "lastModified": 1688221086,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=", "narHash": "sha256-cdW6qUL71cNWhHCpMPOJjlw0wzSRP0pVlRn2vqX/VVg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "aeb75dba965e790de427b73315d5addf91a54955", "rev": "cd99c2b3c9f160cd004318e0697f90bbd5960825",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -105,11 +108,11 @@
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"dir": "lib", "dir": "lib",
"lastModified": 1682879489, "lastModified": 1688049487,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", "narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", "rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -135,11 +138,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1684842236, "lastModified": 1688386108,
"narHash": "sha256-rYWsIXHvNhVQ15RQlBUv67W3YnM+Pd+DuXGMvCBq2IE=", "narHash": "sha256-Vffto9QaVonzYAcPlAzd0soqWYpPpKk60dfNLSIXcFA=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "61e567d6497bc9556f391faebe5e410e6623217f", "rev": "42587d3414d1747999a5f71e92a83cf6547b62da",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -156,6 +159,21 @@
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -687,9 +687,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase"); m_settings->reset("PastebinCustomAPIBase");
} }
} }
// meta URL {
// Meta URL
m_settings->registerSetting("MetaURLOverride", ""); m_settings->registerSetting("MetaURLOverride", "");
QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
// get rid of invalid meta urls
if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
m_settings->reset("MetaURLOverride");
}
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("QuitAfterGameStop", false);

View File

@ -514,6 +514,8 @@ set(FLAME_SOURCES
modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp modplatform/flame/FlameInstanceCreationTask.cpp
modplatform/flame/FlamePackExportTask.h
modplatform/flame/FlamePackExportTask.cpp
) )
set(MODRINTH_SOURCES set(MODRINTH_SOURCES
@ -907,8 +909,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportMrPackDialog.cpp ui/dialogs/ExportPackDialog.cpp
ui/dialogs/ExportMrPackDialog.h ui/dialogs/ExportPackDialog.h
ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.cpp
@ -1057,7 +1059,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportMrPackDialog.ui ui/dialogs/ExportPackDialog.ui
ui/dialogs/IconPickerDialog.ui ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui ui/dialogs/MSALoginDialog.ui

View File

@ -36,6 +36,7 @@
*/ */
#include "FileSystem.h" #include "FileSystem.h"
#include <QPair>
#include "BuildConfig.h" #include "BuildConfig.h"
@ -102,7 +103,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h> #include <linux/fs.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
#include <sys/attr.h> #include <sys/attr.h>
#include <sys/clonefile.h> #include <sys/clonefile.h>
#elif defined(Q_OS_WIN) #elif defined(Q_OS_WIN)
@ -246,6 +247,7 @@ bool copy::operator()(const QString& offset, bool dryRun)
{ {
using copy_opts = fs::copy_options; using copy_opts = fs::copy_options;
m_copied = 0; // reset counter m_copied = 0; // reset counter
m_failedPaths.clear();
// NOTE always deep copy on windows. the alternatives are too messy. // NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
@ -277,6 +279,9 @@ bool copy::operator()(const QString& offset, bool dryRun)
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path; qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path; qDebug() << "Destination file:" << dst_path;
m_failedPaths.append(dst_path);
emit copyFailed(relative_dst_path);
return;
} }
m_copied++; m_copied++;
emit fileCopied(relative_dst_path); emit fileCopied(relative_dst_path);
@ -773,9 +778,43 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name));
} }
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
destination += ".command"; // Create the Application
QDir applicationDirectory = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
QFile f(destination); if (!applicationDirectory.mkpath(".")) {
qWarning() << "Couldn't create application directory";
return false;
}
QDir application = applicationDirectory.path() + "/" + name + ".app/";
if (application.exists()) {
qWarning() << "Application already exists!";
return false;
}
if (!application.mkpath(".")) {
qWarning() << "Couldn't create application";
return false;
}
QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/";
QFile info = content.path() + "/Info.plist";
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application";
return false;
}
info.open(QIODevice::WriteOnly | QIODevice::Text);
QFile(icon).rename(resources.path() + "/Icon.icns");
// Create the Command file
QString exec = binaryDir.path() + "/Run.command";
QFile f(exec);
f.open(QIODevice::WriteOnly | QIODevice::Text); f.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream stream(&f); QTextStream stream(&f);
@ -792,6 +831,28 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
// Generate the Info.plist
QTextStream infoStream(&info);
infoStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">\n"
"<dict>\n"
" <key>CFBundleExecutable</key>\n"
" <string>Run.command</string>\n" // The path to the executable
" <key>CFBundleIconFile</key>\n"
" <string>Icon.icns</string>\n"
" <key>CFBundleName</key>\n"
" <string>" << name << "</string>\n" // Name of the application
" <key>CFBundlePackageType</key>\n"
" <string>APPL</string>\n"
" <key>CFBundleShortVersionString</key>\n"
" <string>1.0</string>\n"
" <key>CFBundleVersion</key>\n"
" <string>1.0</string>\n"
"</dict>\n"
"</plist>";
return true; return true;
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated
@ -1077,6 +1138,7 @@ bool clone::operator()(const QString& offset, bool dryRun)
} }
m_cloned = 0; // reset counter m_cloned = 0; // reset counter
m_failedClones.clear();
auto src = PathCombine(m_src.absolutePath(), offset); auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset);
@ -1097,6 +1159,9 @@ bool clone::operator()(const QString& offset, bool dryRun)
qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message()); qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path; qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path; qDebug() << "Destination file:" << dst_path;
m_failedClones.append(qMakePair(src_path, dst_path));
emit cloneFailed(src_path, dst_path);
return;
} }
m_cloned++; m_cloned++;
emit fileCloned(src_path, dst_path); emit fileCloned(src_path, dst_path);
@ -1156,7 +1221,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false; return false;
} }
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) { if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:"; qDebug() << "failed macos_bsd_clonefile:";
@ -1385,7 +1450,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true; return true;
} }
#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_MACOS)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{ {

View File

@ -43,6 +43,7 @@
#include <system_error> #include <system_error>
#include <QDir> #include <QDir>
#include <QPair>
#include <QFlags> #include <QFlags>
#include <QLocalServer> #include <QLocalServer>
#include <QObject> #include <QObject>
@ -112,9 +113,12 @@ class copy : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; } int totalCopied() { return m_copied; }
int totalFailed() { return m_failedPaths.length(); }
QStringList failed() { return m_failedPaths; }
signals: signals:
void fileCopied(const QString& relativeName); void fileCopied(const QString& relativeName);
void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future? // TODO: maybe add a "shouldCopy" signal in the future?
private: private:
@ -127,6 +131,7 @@ class copy : public QObject {
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;
int m_copied; int m_copied;
QStringList m_failedPaths;
}; };
struct LinkPair { struct LinkPair {
@ -471,6 +476,9 @@ class clone : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCloned() { return m_cloned; } int totalCloned() { return m_cloned; }
int totalFailed() { return m_failedClones.length(); }
QList<QPair<QString, QString>> failed() { return m_failedClones; }
signals: signals:
void fileCloned(const QString& src, const QString& dst); void fileCloned(const QString& src, const QString& dst);
@ -485,6 +493,7 @@ class clone : public QObject {
QDir m_src; QDir m_src;
QDir m_dst; QDir m_dst;
int m_cloned; int m_cloned;
QList<QPair<QString, QString>> m_failedClones;
}; };
/** /**

View File

@ -187,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) { switch(m_accountToUse->accountState()) {
case AccountState::Offline: { case AccountState::Offline: {
m_session->wants_online = false; m_session->wants_online = false;
// NOTE: fallthrough is intentional
} }
/* fallthrough */
case AccountState::Online: { case AccountState::Online: {
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
@ -267,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh. // This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: { case AccountState::Unchecked: {
m_accountToUse->refresh(); m_accountToUse->refresh();
// NOTE: fallthrough intentional
} }
/* fallthrough */
case AccountState::Working: { case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed. // refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget); ProgressDialog progDialog(m_parentWidget);

View File

@ -193,33 +193,23 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {
switch(column) if(column == Name && hasRecommended)
{
case Name:
{
if(hasRecommended)
{ {
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool()) if(value.toBool())
{ {
return tr("Recommended"); return tr("Recommended");
} } else if(hasLatest) {
else if(hasLatest)
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool()) if(value.toBool())
{ {
return tr("Latest"); return tr("Latest");
} }
} }
} } else {
}
default:
{
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
} }
} }
}
case Qt::DecorationRole: case Qt::DecorationRole:
{ {
switch(column) switch(column)

View File

@ -45,10 +45,10 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role) switch (role)
{ {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (index.column()) if (index.column() == 0) {
{ return list->humanReadable();
case 0: return list->humanReadable(); } else {
default: break; break;
} }
case UidRole: return list->uid(); case UidRole: return list->uid();
case NameRole: return list->name(); case NameRole: return list->name();

View File

@ -1112,36 +1112,27 @@ JavaVersion MinecraftInstance::getJavaVersion()
std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList()
{ {
if (!m_loader_mod_list) if (!m_loader_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed));
m_loader_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_loader_mod_list; return m_loader_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList()
{ {
if (!m_core_mod_list) if (!m_core_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed));
m_core_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_core_mod_list; return m_core_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList()
{ {
if (!m_nil_mod_list) if (!m_nil_mod_list) {
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false));
m_nil_mod_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction);
} }
return m_nil_mod_list; return m_nil_mod_list;
} }

View File

@ -65,7 +65,8 @@
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{ static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ResourceAPI::Forge}, {"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ResourceAPI::Quilt} {"org.quiltmc.quilt-loader", ResourceAPI::Quilt},
{"com.mumfrey.liteloader", ResourceAPI::LiteLoader}
}; };
PackProfile::PackProfile(MinecraftInstance * instance) PackProfile::PackProfile(MinecraftInstance * instance)

View File

@ -328,6 +328,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: { case AccountState::Gone: {
return tr("Gone", "Account status"); return tr("Gone", "Account status");
} }
default: {
return tr("Unknown", "Account status");
}
} }
} }
@ -354,12 +357,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
return QVariant::fromValue(account); return QVariant::fromValue(account);
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (index.column()) if (index.column() == ProfileNameColumn) {
{
case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
} }
default: default:
return QVariant(); return QVariant();
} }

View File

@ -273,6 +273,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE, AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
); );
return;
} }
default: default:
changeState( changeState(

View File

@ -74,6 +74,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
@ -83,6 +84,7 @@ std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) con
return { 1, type == SortType::PACK_FORMAT }; return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return { -1, type == SortType::PACK_FORMAT };
break;
} }
} }
return { 0, false }; return { 0, false };

View File

@ -91,6 +91,7 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::VERSION: { case SortType::VERSION: {
auto this_ver = Version(version()); auto this_ver = Version(version());
@ -99,11 +100,13 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
return { 1, type == SortType::VERSION }; return { 1, type == SortType::VERSION };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::VERSION }; return { -1, type == SortType::VERSION };
break;
} }
case SortType::PROVIDER: { case SortType::PROVIDER: {
auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return { compare_result, type == SortType::PROVIDER }; return { compare_result, type == SortType::PROVIDER };
break;
} }
} }
return { 0, false }; return { 0, false };
@ -123,7 +126,7 @@ bool Mod::applyFilter(QRegularExpression filter) const
return Resource::applyFilter(filter); return Resource::applyFilter(filter);
} }
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool
{ {
if (!preserve_metadata) { if (!preserve_metadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
@ -136,7 +139,7 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
} }
} }
return Resource::destroy(); return Resource::destroy(attempt_trash);
} }
auto Mod::details() const -> const ModDetails& auto Mod::details() const -> const ModDetails&
@ -166,6 +169,13 @@ auto Mod::homeurl() const -> QString
return details().homeurl; return details().homeurl;
} }
auto Mod::metaurl() const -> QString
{
if (metadata() == nullptr)
return homeurl();
return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
}
auto Mod::description() const -> QString auto Mod::description() const -> QString
{ {
return details().description; return details().description;

View File

@ -70,6 +70,7 @@ public:
auto provider() const -> std::optional<QString>; auto provider() const -> std::optional<QString>;
auto licenses() const -> const QList<ModLicense>&; auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString; auto issueTracker() const -> QString;
auto metaurl() const -> QString;
/** 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; };
@ -92,7 +93,7 @@ public:
[[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
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool;
void finishResolvingWithDetails(ModDetails&& details); void finishResolvingWithDetails(ModDetails&& details);

View File

@ -199,10 +199,10 @@ Task* ModFolderModel::createParseTask(Resource& resource)
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{ {
for(auto mod : allMods()){ for(auto mod : allMods()) {
if(mod->fileinfo().fileName() == filename){ if(mod->fileinfo().fileName() == filename) {
auto index_dir = indexDir(); auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata); mod->destroy(index_dir, preserve_metadata, false);
update(); update();
@ -215,16 +215,11 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
bool ModFolderModel::deleteMods(const QModelIndexList& indexes) bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{ {
if(!m_can_interact) { if (indexes.isEmpty())
return false;
}
if(indexes.isEmpty())
return true; return true;
for (auto i: indexes) for (auto i : indexes) {
{ if (i.column() != 0) {
if(i.column() != 0) {
continue; continue;
} }
auto m = at(i.row()); auto m = at(i.row());

View File

@ -71,6 +71,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED }; return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled()) if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED }; return { -1, type == SortType::ENABLED };
break;
case SortType::NAME: { case SortType::NAME: {
QString this_name{ name() }; QString this_name{ name() };
QString other_name{ other.name() }; QString other_name{ other.name() };
@ -81,12 +82,14 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return { compare_result, type == SortType::NAME }; 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, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged()) if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE }; return { -1, type == SortType::DATE };
break;
} }
return { 0, false }; return { 0, false };
@ -145,14 +148,10 @@ bool Resource::enable(EnableAction action)
return true; return true;
} }
bool Resource::destroy() bool Resource::destroy(bool attemptTrash)
{ {
m_type = ResourceType::UNKNOWN; m_type = ResourceType::UNKNOWN;
return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath());
if (FS::trash(m_file_info.filePath()))
return true;
return FS::deletePath(m_file_info.filePath());
} }
bool Resource::isSymLinkUnder(const QString& instPath) const bool Resource::isSymLinkUnder(const QString& instPath) const

View File

@ -92,7 +92,7 @@ class Resource : public QObject {
} }
// Delete all files of this resource. // Delete all files of this resource.
bool destroy(); bool destroy(bool attemptTrash = true);
[[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); }

View File

@ -1,14 +1,15 @@
#include "ResourceFolderModel.h" #include "ResourceFolderModel.h"
#include <QMessageBox>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QFileInfo> #include <QFileInfo>
#include <QIcon> #include <QIcon>
#include <QMenu>
#include <QMimeData> #include <QMimeData>
#include <QStyle> #include <QStyle>
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QMenu>
#include "Application.h" #include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
@ -18,6 +19,7 @@
#include "settings/Setting.h" #include "settings/Setting.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include "ui/dialogs/CustomMessageBox.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir)
: QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this)
@ -77,10 +79,6 @@ bool ResourceFolderModel::stopWatching(const QStringList paths)
bool ResourceFolderModel::installResource(QString original_path) bool ResourceFolderModel::installResource(QString original_path)
{ {
if (!m_can_interact) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path); original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path); QFileInfo file_info(original_path);
@ -159,7 +157,7 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
{ {
for (auto& resource : m_resources) { for (auto& resource : m_resources) {
if (resource->fileinfo().fileName() == file_name) { if (resource->fileinfo().fileName() == file_name) {
auto res = resource->destroy(); auto res = resource->destroy(false);
update(); update();
@ -171,9 +169,6 @@ bool ResourceFolderModel::uninstallResource(QString file_name)
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{ {
if (!m_can_interact)
return false;
if (indexes.isEmpty()) if (indexes.isEmpty())
return true; return true;
@ -192,11 +187,8 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
return true; return true;
} }
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action) bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{ {
if (!m_can_interact)
return false;
if (indexes.isEmpty()) if (indexes.isEmpty())
return true; return true;
@ -249,7 +241,9 @@ bool ResourceFolderModel::update()
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::finished, this, [=] { connect(
m_current_update_task.get(), &Task::finished, this,
[=] {
m_current_update_task.reset(); m_current_update_task.reset();
if (m_scheduled_update) { if (m_scheduled_update) {
m_scheduled_update = false; m_scheduled_update = false;
@ -257,7 +251,8 @@ bool ResourceFolderModel::update()
} else { } else {
emit updateFinished(); emit updateFinished();
} }
}, Qt::ConnectionType::QueuedConnection); },
Qt::ConnectionType::QueuedConnection);
QThreadPool::globalInstance()->start(m_current_update_task.get()); QThreadPool::globalInstance()->start(m_current_update_task.get());
@ -347,15 +342,9 @@ Qt::DropActions ResourceFolderModel::supportedDropActions() const
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{ {
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags; auto flags = defaultFlags | Qt::ItemIsDropEnabled;
if (!m_can_interact) { if (index.isValid())
flags &= ~Qt::ItemIsDropEnabled;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable; flags |= Qt::ItemIsUserCheckable;
}
}
return flags; return flags;
} }
@ -430,7 +419,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
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."
"\nCanonical Path: %1") "\nCanonical Path: %1")
.arg(at(row).fileinfo().canonicalFilePath());; .arg(at(row).fileinfo().canonicalFilePath());
;
} }
if (at(row).isMoreThanOneHardLink()) { if (at(row).isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
@ -463,8 +453,20 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) if (row < 0 || row >= rowCount(index.parent()) || !index.isValid())
return false; return false;
if (role == Qt::CheckStateRole) if (role == Qt::CheckStateRole) {
if (m_instance != nullptr && m_instance->isRunning()) {
auto response =
CustomMessageBox::selectable(nullptr, "Confirm toggle",
"If you enable/disable this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return false;
}
return setResourceEnabled({ index }, EnableAction::TOGGLE); return setResourceEnabled({ index }, EnableAction::TOGGLE);
}
return false; return false;
} }
@ -583,16 +585,6 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const
return m_column_sort_keys.at(column); return m_column_sort_keys.at(column);
} }
void ResourceFolderModel::enableInteraction(bool enabled)
{
if (m_can_interact == enabled)
return;
m_can_interact = enabled;
if (size())
emit dataChanged(index(0), index(size() - 1));
}
/* Standard Proxy Model for createFilterProxyModel */ /* Standard Proxy Model for createFilterProxyModel */
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const [[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{ {
@ -628,6 +620,7 @@ void ResourceFolderModel::enableInteraction(bool enabled)
return (compare_result.first > 0); return (compare_result.first > 0);
} }
QString ResourceFolderModel::instDirPath() const { QString ResourceFolderModel::instDirPath() const
{
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
} }

View File

@ -14,8 +14,8 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "tasks/Task.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include "tasks/Task.h"
class QSortFilterProxyModel; class QSortFilterProxyModel;
@ -141,10 +141,6 @@ class ResourceFolderModel : public QAbstractListModel {
QString instDirPath() const; QString instDirPath() const;
public slots:
void enableInteraction(bool enabled);
void disableInteraction(bool disabled) { enableInteraction(!disabled); }
signals: signals:
void updateFinished(); void updateFinished();
@ -193,7 +189,11 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse. * if the resource is complex and has more stuff to parse.
*/ */
virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); } virtual void onParseFailed(int ticket, QString resource_id)
{
Q_UNUSED(ticket);
Q_UNUSED(resource_id);
}
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.
@ -203,8 +203,6 @@ class ResourceFolderModel : public QAbstractListModel {
QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")}; QStringList m_column_names_translated = {tr("Enable"), tr("Name"), tr("Last Modified")};
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents };
bool m_can_interact = true;
QDir m_dir; QDir m_dir;
BaseInstance* m_instance; BaseInstance* m_instance;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;

View File

@ -102,6 +102,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
break;
} }
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();
@ -111,6 +112,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
return { 1, type == SortType::PACK_FORMAT }; return { 1, type == SortType::PACK_FORMAT };
if (this_ver < other_ver) if (this_ver < other_ver)
return { -1, type == SortType::PACK_FORMAT }; return { -1, type == SortType::PACK_FORMAT };
break;
} }
} }
return { 0, false }; return { 0, false };

View File

@ -103,7 +103,7 @@ void ModFolderLoadTask::executeTask()
while (iter.hasNext()) { while (iter.hasNext()) {
auto mod = iter.next().value(); auto mod = iter.next().value();
if (mod->status() == ModStatus::NotInstalled) { if (mod->status() == ModStatus::NotInstalled) {
mod->destroy(m_index_dir, false); mod->destroy(m_index_dir, false, false);
iter.remove(); iter.remove();
} }
} }

View File

@ -70,11 +70,17 @@ auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString t
} }
QCryptographicHash hash(algo); QCryptographicHash hash(algo);
if(!hash.addData(device)) if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!"; qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo)); Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() }; return { hash.result().toHex() };
} }
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -128,6 +128,7 @@ struct IndexedPack {
return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
} }
}; };
QString getMetaURL(ResourceProvider provider, QVariant projectID);
struct OverrideDep { struct OverrideDep {
QString quilt; QString quilt;
@ -144,6 +145,7 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
{ "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
{ "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
}; };
QString getMetaURL(ResourceProvider provider, QVariant projectID);
} // namespace ModPlatform } // namespace ModPlatform

View File

@ -23,6 +23,8 @@ class FlameAPI : public NetworkResourceAPI {
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override; [[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); }
private: private:
static int getClassId(ModPlatform::ResourceType type) static int getClassId(ModPlatform::ResourceType type)
{ {

View File

@ -470,8 +470,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) { switch (result.type) {
case Flame::File::Type::Folder: { case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever. // fallthrough intentional, we treat these as plain old mods and dump them wherever.
} }
/* fallthrough */
case Flame::File::Type::SingleFile: case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: { case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) { if (!result.url.isEmpty()) {

View File

@ -0,0 +1,473 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* 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 "FlamePackExportTask.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QCryptographicHash>
#include <QFileInfo>
#include <QMessageBox>
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include "Json.h"
#include "MMCZip.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/Task.h"
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
FlamePackExportTask::FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, author(author)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{}
void FlamePackExportTask::executeTask()
{
setStatus(tr("Searching for files..."));
setProgress(0, 5);
collectFiles();
}
bool FlamePackExportTask::abort()
{
if (task != nullptr) {
task->abort();
task = nullptr;
emitAborted();
return true;
}
if (buildZipFuture.isRunning()) {
buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
void FlamePackExportTask::collectFiles()
{
setAbortable(false);
QCoreApplication::processEvents();
files.clear();
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
emitFailed(tr("Could not search for files"));
return;
}
pendingHashes.clear();
resolvedFiles.clear();
if (mcInstance != nullptr) {
mcInstance->loaderModList()->update();
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
} else
collectHashes();
}
void FlamePackExportTask::collectHashes()
{
setAbortable(true);
setStatus(tr("Finding file hashes..."));
setProgress(1, 5);
auto allMods = mcInstance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask(this, "MakeHashesTask", 10));
task.reset(hashingTask);
for (const QFileInfo& file : files) {
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
}))
continue;
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
continue;
}
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
modIter != allMods.end()) {
const Mod* mod = *modIter;
if (!mod || mod->type() == ResourceType::FOLDER) {
continue;
}
if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) {
resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
{ mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true,
mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") });
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
}
});
connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
hashingTask->addTask(hashTask);
}
}
auto progressStep = std::make_shared<TaskStepProgress>();
connect(hashingTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propogateStepProgress);
connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
hashingTask->start();
}
void FlamePackExportTask::makeApiRequest()
{
if (pendingHashes.isEmpty()) {
buildZip();
return;
}
setStatus(tr("Finding versions for hashes..."));
setProgress(2, 5);
auto response = std::make_shared<QByteArray>();
QList<uint> fingerprints;
for (auto& murmur : pendingHashes.keys()) {
fingerprints.push_back(murmur.toUInt());
}
task.reset(api.matchFingerprints(fingerprints, response));
connect(task.get(), &Task::succeeded, this, [this, response] {
QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
auto docObj = Json::requireObject(doc);
auto dataObj = Json::requireObject(docObj, "data");
auto dataArr = Json::requireArray(dataObj, "exactMatches");
if (dataArr.isEmpty()) {
qWarning() << "No matches found for fingerprint search!";
return;
}
for (auto match : dataArr) {
auto matchObj = Json::ensureObject(match, {});
auto fileObj = Json::ensureObject(matchObj, "file", {});
if (matchObj.isEmpty() || fileObj.isEmpty()) {
qWarning() << "Fingerprint match is empty!";
return;
}
auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
auto mod = pendingHashes.find(fingerprint);
if (mod == pendingHashes.end()) {
qWarning() << "Invalid fingerprint from the API response.";
continue;
}
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"),
mod->enabled, mod->isMod });
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
pendingHashes.clear();
});
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
task->start();
}
void FlamePackExportTask::getProjectsInfo()
{
setStatus(tr("Finding project info from CurseForge..."));
setProgress(3, 5);
QStringList addonIds;
for (const auto& resolved : resolvedFiles) {
if (resolved.slug.isEmpty()) {
addonIds << QString::number(resolved.addonId);
}
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
buildZip();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] {
QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
<< " reason: " << parseError.errorString();
qWarning() << *response;
failed(parseError.errorString());
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1)
entries = { Json::requireObject(Json::requireObject(doc), "data") };
else
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entryObj = Json::requireObject(entry);
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name")));
ModPlatform::IndexedPack pack;
FlameMod::loadIndexedPack(pack, entryObj);
for (auto key : resolvedFiles.keys()) {
auto val = resolvedFiles.value(key);
if (val.addonId == pack.addonId) {
val.name = pack.name;
val.slug = pack.slug;
QStringList authors;
for (auto author : pack.authors)
authors << author.name;
val.authors = authors.join(", ");
resolvedFiles[key] = val;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
buildZip();
});
task.reset(projTask);
task->start();
}
void FlamePackExportTask::buildZip()
{
setStatus(tr("Adding files..."));
setProgress(4, 5);
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
QuaZip zip(output);
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(output);
return BuildZipResult(tr("Could not create file"));
}
if (buildZipFuture.isCanceled())
return BuildZipResult();
QuaZipFile indexFile(&zip);
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("manifest.json"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
indexFile.write(generateIndex());
QuaZipFile modlist(&zip);
if (!modlist.open(QIODevice::WriteOnly, QuaZipNewInfo("modlist.html"))) {
QFile::remove(output);
return BuildZipResult(tr("Could not create index"));
}
QString content = "";
for (auto mod : resolvedFiles) {
if (mod.isMod) {
content += QString(TEMPLATE)
.replace("{name}", mod.name.toHtmlEscaped())
.replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
.replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
}
}
content = "<ul>" + content + "</ul>";
modlist.write(content.toUtf8());
auto progressStep = std::make_shared<TaskStepProgress>();
size_t progress = 0;
for (const QFileInfo& file : files) {
if (buildZipFuture.isCanceled()) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult();
}
progressStep->update(progress, files.length());
stepProgress(*progressStep);
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
if (!resolvedFiles.contains(file.absoluteFilePath()) &&
!JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
QFile::remove(output);
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
}
progress++;
}
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(output);
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
return BuildZipResult(tr("A zip error occurred"));
}
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
return BuildZipResult();
});
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &FlamePackExportTask::finish);
buildZipWatcher.setFuture(buildZipFuture);
}
void FlamePackExportTask::finish()
{
if (buildZipFuture.isCanceled())
emitAborted();
else {
const BuildZipResult result = buildZipFuture.result();
if (result.has_value())
emitFailed(result.value());
else
emitSucceeded();
}
}
QByteArray FlamePackExportTask::generateIndex()
{
QJsonObject obj;
obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1;
obj["name"] = name;
obj["version"] = version;
obj["author"] = author;
obj["overrides"] = "overrides";
if (mcInstance) {
QJsonObject version;
auto profile = mcInstance->getPackProfile();
// collect all supported components
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
// convert all available components to mrpack dependencies
if (minecraft != nullptr)
version["version"] = minecraft->m_version;
QString id;
if (quilt != nullptr)
id = "quilt-" + quilt->getVersion();
else if (fabric != nullptr)
id = "fabric-" + fabric->getVersion();
else if (forge != nullptr)
id = "forge-" + forge->getVersion();
version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) {
QJsonObject loader;
loader["id"] = id;
loader["primary"] = true;
version["modLoaders"] = QJsonArray({ loader });
}
obj["minecraft"] = version;
}
QJsonArray files;
for (auto mod : resolvedFiles) {
QJsonObject file;
file["projectID"] = mod.addonId;
file["fileID"] = mod.version;
file["required"] = mod.enabled;
files << file;
}
obj["files"] = files;
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}

View File

@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* 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 <QFuture>
#include <QFutureWatcher>
#include "BaseInstance.h"
#include "MMCZip.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h"
class FlamePackExportTask : public Task {
public:
FlamePackExportTask(const QString& name,
const QString& version,
const QString& author,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter);
protected:
void executeTask() override;
bool abort() override;
private:
static const QString TEMPLATE;
static const QStringList FILE_EXTENSIONS;
// inputs
const QString name, version, author;
const InstancePtr instance;
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
const MMCZip::FilterFunction filter;
typedef std::optional<QString> BuildZipResult;
struct ResolvedFile {
int addonId;
int version;
bool enabled;
bool isMod;
QString name;
QString slug;
QString authors;
};
struct HashInfo {
QString name;
QString path;
bool enabled;
bool isMod;
};
FlameAPI api;
QFileInfoList files;
QMap<QString, HashInfo> pendingHashes{};
QMap<QString, ResolvedFile> resolvedFiles{};
Task::Ptr task;
QFuture<BuildZipResult> buildZipFuture;
QFutureWatcher<BuildZipResult> buildZipWatcher;
void collectFiles();
void collectHashes();
void makeApiRequest();
void getProjectsInfo();
void buildZip();
void finish();
QByteArray generateIndex();
};

View File

@ -38,7 +38,7 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
{ {
QStringList l; QStringList l;
for (auto loader : { Forge, Fabric, Quilt }) { for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) {
if (types & loader) { if (types & loader) {
l << getModLoaderString(loader); l << getModLoaderString(loader);
} }
@ -92,7 +92,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{ {
if (args.loaders.has_value()) { if (args.loaders.has_value()) {
if (!validateModLoaders(args.loaders.value())) { if (!validateModLoaders(args.loaders.value())) {
qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
return {}; return {};
} }
} }
@ -141,7 +141,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return s.isEmpty() ? QString() : s; return s.isEmpty() ? QString() : s;
} }
inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { return loaders & (Forge | Fabric | Quilt); } static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); }
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override [[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{ {

View File

@ -64,7 +64,8 @@ bool ModrinthPackExportTask::abort()
if (buildZipFuture.isRunning()) { if (buildZipFuture.isRunning()) {
buildZipFuture.cancel(); buildZipFuture.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately. // NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur
// immediately.
return true; return true;
} }
@ -94,6 +95,7 @@ void ModrinthPackExportTask::collectFiles()
void ModrinthPackExportTask::collectHashes() void ModrinthPackExportTask::collectHashes()
{ {
setStatus(tr("Finding file hashes..."));
for (const QFileInfo& file : files) { for (const QFileInfo& file : files) {
QCoreApplication::processEvents(); QCoreApplication::processEvents();
@ -157,6 +159,7 @@ void ModrinthPackExportTask::makeApiRequest()
if (pendingHashes.isEmpty()) if (pendingHashes.isEmpty())
buildZip(); buildZip();
else { else {
setStatus(tr("Finding versions for hashes..."));
auto response = std::make_shared<QByteArray>(); auto response = std::make_shared<QByteArray>();
task = api.currentVersions(pendingHashes.values(), "sha512", response); task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
@ -263,13 +266,13 @@ void ModrinthPackExportTask::finish()
QByteArray ModrinthPackExportTask::generateIndex() QByteArray ModrinthPackExportTask::generateIndex()
{ {
QJsonObject obj; QJsonObject out;
obj["formatVersion"] = 1; out["formatVersion"] = 1;
obj["game"] = "minecraft"; out["game"] = "minecraft";
obj["name"] = name; out["name"] = name;
obj["versionId"] = version; out["versionId"] = version;
if (!summary.isEmpty()) if (!summary.isEmpty())
obj["summary"] = summary; out["summary"] = summary;
if (mcInstance) { if (mcInstance) {
auto profile = mcInstance->getPackProfile(); auto profile = mcInstance->getPackProfile();
@ -290,30 +293,40 @@ QByteArray ModrinthPackExportTask::generateIndex()
if (forge != nullptr) if (forge != nullptr)
dependencies["forge"] = forge->m_version; dependencies["forge"] = forge->m_version;
obj["dependencies"] = dependencies; out["dependencies"] = dependencies;
} }
QJsonArray files; QJsonArray filesOut;
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles); for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) {
while (iterator.hasNext()) { QJsonObject fileOut;
iterator.next();
QString path = iterator.key();
const ResolvedFile& value = iterator.value(); const ResolvedFile& value = iterator.value();
QJsonObject file; // detect disabled mod
file["path"] = iterator.key(); const QFileInfo pathInfo(path);
file["downloads"] = QJsonArray({ iterator.value().url }); if (pathInfo.suffix() == "disabled") {
// rename it
path = pathInfo.dir().filePath(pathInfo.completeBaseName());
// ...and make it optional
QJsonObject env;
env["client"] = "optional";
env["server"] = "optional";
fileOut["env"] = env;
}
fileOut["path"] = path;
fileOut["downloads"] = QJsonArray{ iterator.value().url };
QJsonObject hashes; QJsonObject hashes;
hashes["sha1"] = value.sha1; hashes["sha1"] = value.sha1;
hashes["sha512"] = value.sha512; hashes["sha512"] = value.sha512;
fileOut["hashes"] = hashes;
file["hashes"] = hashes; fileOut["fileSize"] = value.size;
file["fileSize"] = value.size; filesOut << fileOut;
files << file;
} }
obj["files"] = files; out["files"] = filesOut;
return QJsonDocument(obj).toJson(QJsonDocument::Compact); return QJsonDocument(out).toJson(QJsonDocument::Compact);
} }

View File

@ -454,6 +454,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
} }
} }
qWarning("TranslationModel::data not implemented when role is DisplayRole");
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {

View File

@ -107,7 +107,7 @@
#include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CopyInstanceDialog.h"
#include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportMrPackDialog.h" #include "ui/dialogs/ExportPackDialog.h"
#include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/ImportResourceDialog.h"
#include "ui/themes/ITheme.h" #include "ui/themes/ITheme.h"
#include "ui/themes/ThemeManager.h" #include "ui/themes/ThemeManager.h"
@ -205,6 +205,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
auto exportInstanceMenu = new QMenu(this); auto exportInstanceMenu = new QMenu(this);
exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceZip);
exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack);
ui->actionExportInstance->setMenu(exportInstanceMenu); ui->actionExportInstance->setMenu(exportInstanceMenu);
} }
@ -927,21 +928,8 @@ void MainWindow::onCatToggled(bool state)
void MainWindow::setCatBackground(bool enabled) void MainWindow::setCatBackground(bool enabled)
{ {
if (enabled) { view->setPaintCat(enabled);
view->setStyleSheet(QString(R"( view->viewport()->repaint();
InstanceView
{
background-image: url(:/backgrounds/%1);
background-attachment: fixed;
background-clip: padding;
background-position: bottom right;
background-repeat: none;
background-color:palette(base);
})")
.arg(ThemeManager::getCatImage()));
} else {
view->setStyleSheet(QString());
}
} }
void MainWindow::runModalTask(Task *task) void MainWindow::runModalTask(Task *task)
@ -1418,11 +1406,35 @@ void MainWindow::on_actionExportInstanceMrPack_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance)
{ {
ExportMrPackDialog dlg(m_selectedInstance, this); ExportPackDialog dlg(m_selectedInstance, this);
dlg.exec(); dlg.exec();
} }
} }
void MainWindow::on_actionExportInstanceFlamePack_triggered()
{
if (m_selectedInstance) {
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get());
if (instance) {
QString errorMsg;
if (instance->getPackProfile()->getComponent("org.quiltmc.quilt-loader")) {
errorMsg = tr("Quilt is currently not supported by CurseForge modpacks.");
} else if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft");
cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") {
errorMsg = tr("Snapshots are currently not supported by CurseForge modpacks.");
}
if (!errorMsg.isEmpty()) {
QMessageBox msgBox;
msgBox.setText(errorMsg);
msgBox.exec();
return;
}
ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME);
dlg.exec();
}
}
}
void MainWindow::on_actionRenameInstance_triggered() void MainWindow::on_actionRenameInstance_triggered()
{ {
if (m_selectedInstance) if (m_selectedInstance)
@ -1524,11 +1536,39 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
QString iconPath; QString iconPath;
QStringList args; QStringList args;
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
appPath = QApplication::applicationFilePath();
if (appPath.startsWith("/private/var/")) { if (appPath.startsWith("/private/var/")) {
QMessageBox::critical(this, tr("Create instance shortcut"), QMessageBox::critical(this, tr("Create instance shortcut"),
tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
return; return;
} }
auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
if (pIcon == nullptr)
{
pIcon = APPLICATION->icons()->icon("grass");
}
iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly))
{
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
QIcon icon = pIcon->icon();
bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS");
iconFile.close();
if (!success)
{
iconFile.remove();
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (appPath.startsWith("/tmp/.mount_")) { if (appPath.startsWith("/tmp/.mount_")) {
// AppImage! // AppImage!
@ -1611,7 +1651,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
#endif #endif
args.append({ "--launch", m_selectedInstance->id() }); args.append({ "--launch", m_selectedInstance->id() });
if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
#if not defined(Q_OS_MACOS)
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
#else
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!"));
#endif
} else { } else {
#if not defined(Q_OS_MACOS) #if not defined(Q_OS_MACOS)
iconFile.remove(); iconFile.remove();

View File

@ -157,6 +157,7 @@ private slots:
inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }
void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceZip_triggered();
void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceMrPack_triggered();
void on_actionExportInstanceFlamePack_triggered();
void on_actionRenameInstance_triggered(); void on_actionRenameInstance_triggered();

View File

@ -479,6 +479,14 @@
<string>Modrinth (mrpack)</string> <string>Modrinth (mrpack)</string>
</property> </property>
</action> </action>
<action name="actionExportInstanceFlamePack">
<property name="icon">
<iconset theme="flame"/>
</property>
<property name="text">
<string>CurseForge (zip)</string>
</property>
</action>
<action name="actionCreateInstanceShortcut"> <action name="actionCreateInstanceShortcut">
<property name="icon"> <property name="icon">
<iconset theme="shortcut"> <iconset theme="shortcut">

View File

@ -16,11 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "ExportMrPackDialog.h" #include "ExportPackDialog.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui_ExportMrPackDialog.h" #include "ui_ExportPackDialog.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFileSystemModel> #include <QFileSystemModel>
@ -32,17 +34,24 @@
#include "MMCZip.h" #include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h" #include "modplatform/modrinth/ModrinthPackExportTask.h"
ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent) ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
: QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog) : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider)
{ {
ui->setupUi(this); ui->setupUi(this);
ui->name->setText(instance->name()); ui->name->setText(instance->name());
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
setWindowTitle("Export Modrinth Pack");
} else {
setWindowTitle("Export CurseForge Pack");
ui->version->setText("");
ui->summaryLabel->setText("Author");
}
// ensure a valid pack is generated // ensure a valid pack is generated
// the name and version fields mustn't be empty // the name and version fields mustn't be empty
connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate); connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate);
// the instance name can technically be empty // the instance name can technically be empty
validate(); validate();
@ -66,6 +75,7 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get()); MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
if (mcInstance) { if (mcInstance) {
mcInstance->loaderModList()->update();
const QDir index = mcInstance->loaderModList()->indexDir(); const QDir index = mcInstance->loaderModList()->indexDir();
if (index.exists()) if (index.exists())
proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
@ -83,43 +93,54 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
headerView->setSectionResizeMode(0, QHeaderView::Stretch); headerView->setSectionResizeMode(0, QHeaderView::Stretch);
} }
ExportMrPackDialog::~ExportMrPackDialog() ExportPackDialog::~ExportPackDialog()
{ {
delete ui; delete ui;
} }
void ExportMrPackDialog::done(int result) void ExportPackDialog::done(int result)
{ {
if (result == Accepted) { if (result == Accepted) {
const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), QString output;
FS::PathCombine(QDir::homePath(), filename + ".mrpack"), if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
"Modrinth pack (*.mrpack *.zip)", nullptr); output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)",
nullptr);
else
output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr);
if (output.isEmpty()) if (output.isEmpty())
return; return;
Task* task;
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
else
task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1));
connect(&task, &Task::failed, connect(task, &Task::failed,
[this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(&task, &Task::aborted, [this] { connect(task, &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show(); ->show();
}); });
connect(task, &Task::finished, [task] { task->deleteLater(); });
ProgressDialog progress(this); ProgressDialog progress(this);
progress.setSkipButton(true, tr("Abort")); progress.setSkipButton(true, tr("Abort"));
if (progress.execWithTask(&task) != QDialog::Accepted) if (progress.execWithTask(task) != QDialog::Accepted)
return; return;
} }
QDialog::done(result); QDialog::done(result);
} }
void ExportMrPackDialog::validate() void ExportPackDialog::validate()
{ {
const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty(); const bool invalid =
ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty());
ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
} }

View File

@ -22,24 +22,28 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "FastFileIconProvider.h" #include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h" #include "FileIgnoreProxy.h"
#include "modplatform/ModIndex.h"
namespace Ui { namespace Ui {
class ExportMrPackDialog; class ExportPackDialog;
} }
class ExportMrPackDialog : public QDialog { class ExportPackDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr); explicit ExportPackDialog(InstancePtr instance,
~ExportMrPackDialog(); QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog();
void done(int result) override; void done(int result) override;
void validate(); void validate();
private: private:
const InstancePtr instance; const InstancePtr instance;
Ui::ExportMrPackDialog* ui; Ui::ExportPackDialog* ui;
FileIgnoreProxy* proxy; FileIgnoreProxy* proxy;
FastFileIconProvider icons; FastFileIconProvider icons;
const ModPlatform::ResourceProvider m_provider;
}; };

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ExportMrPackDialog</class> <class>ExportPackDialog</class>
<widget class="QDialog" name="ExportMrPackDialog"> <widget class="QDialog" name="ExportPackDialog">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Export Modrinth Pack</string> <string>Export Pack</string>
</property> </property>
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
<bool>true</bool> <bool>true</bool>
@ -24,7 +24,7 @@
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="versionLabel"> <widget class="QLabel" name="summaryLabel">
<property name="text"> <property name="text">
<string>Summary</string> <string>Summary</string>
</property> </property>
@ -41,7 +41,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="summaryLabel"> <widget class="QLabel" name="versionLabel">
<property name="text"> <property name="text">
<string>Version</string> <string>Version</string>
</property> </property>
@ -57,6 +57,7 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -103,7 +104,7 @@
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
<signal>accepted()</signal> <signal>accepted()</signal>
<receiver>ExportMrPackDialog</receiver> <receiver>ExportPackDialog</receiver>
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
@ -119,7 +120,7 @@
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
<signal>rejected()</signal> <signal>rejected()</signal>
<receiver>ExportMrPackDialog</receiver> <receiver>ExportPackDialog</receiver>
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">

View File

@ -34,6 +34,7 @@
*/ */
#include "ProgressDialog.h" #include "ProgressDialog.h"
#include <QPoint>
#include "ui_ProgressDialog.h" #include "ui_ProgressDialog.h"
#include <limits> #include <limits>
@ -66,8 +67,9 @@ ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Pr
ui->taskProgressScrollArea->setHidden(true); ui->taskProgressScrollArea->setHidden(true);
this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true);
setSkipButton(false);
changeProgress(0, 100); changeProgress(0, 100);
updateSize(true);
setSkipButton(false);
} }
void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::setSkipButton(bool present, QString label)
@ -93,24 +95,38 @@ ProgressDialog::~ProgressDialog()
delete ui; delete ui;
} }
void ProgressDialog::updateSize() void ProgressDialog::updateSize(bool recenterParent)
{ {
QSize lastSize = this->size(); QSize lastSize = this->size();
QSize qSize = QSize(480, minimumSizeHint().height()); QPoint lastPos = this->pos();
int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2);
minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing();
if (!ui->taskProgressScrollArea->isHidden())
minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing();
if (ui->skipButton->isVisible())
minHeight += ui->skipButton->height() + ui->verticalLayout->spacing();
minHeight = std::max(minHeight, 60);
QSize minSize = QSize(480, minHeight);
// if the current window is too small setMinimumSize(minSize);
if ((lastSize != qSize) && (lastSize.height() < qSize.height())) adjustSize();
{
resize(qSize);
// keep the dialog in the center after a resize QSize newSize = this->size();
this->move( // if the current window is a different size
this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2, auto parent = this->parentWidget();
this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 2 if (recenterParent && parent) {
); auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2));
auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2));
this->move(newX, newY);
}
else if (lastSize != newSize)
{
// center on old position after resize
QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative
auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2));
auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2));
this->move(newX, newY);
} }
setMinimumSize(qSize);
} }
@ -201,7 +217,9 @@ void ProgressDialog::onTaskSucceeded()
void ProgressDialog::changeStatus(const QString& status) void ProgressDialog::changeStatus(const QString& status)
{ {
ui->globalStatusLabel->setText(task->getStatus()); ui->globalStatusLabel->setText(task->getStatus());
ui->globalStatusLabel->adjustSize();
ui->globalStatusDetailsLabel->setText(task->getDetails()); ui->globalStatusDetailsLabel->setText(task->getDetails());
ui->globalStatusDetailsLabel->adjustSize();
updateSize(); updateSize();
} }

View File

@ -62,7 +62,7 @@ public:
explicit ProgressDialog(QWidget *parent = 0); explicit ProgressDialog(QWidget *parent = 0);
~ProgressDialog(); ~ProgressDialog();
void updateSize(); void updateSize(bool recenterParent = false);
int execWithTask(Task* task); int execWithTask(Task* task);
int execWithTask(std::unique_ptr<Task> &&task); int execWithTask(std::unique_ptr<Task> &&task);

View File

@ -43,6 +43,8 @@
#include "ui/pages/modplatform/flame/FlameResourcePages.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui/widgets/PageContainer.h" #include "ui/widgets/PageContainer.h"
namespace ResourceDownload { namespace ResourceDownload {
@ -277,8 +279,11 @@ QList<BasePage*> ModDownloadDialog::getPages()
{ {
QList<BasePage*> pages; QList<BasePage*> pages;
auto loaders = static_cast<MinecraftInstance*>(m_instance)->getPackProfile()->getModLoaders().value();
if (ModrinthAPI::validateModLoaders(loaders))
pages.append(ModrinthModPage::create(this, *m_instance)); pages.append(ModrinthModPage::create(this, *m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame) if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders))
pages.append(FlameModPage::create(this, *m_instance)); pages.append(FlameModPage::create(this, *m_instance));
return pages; return pages;

View File

@ -248,8 +248,8 @@ bool AccessibleInstanceView::selectColumn(int column)
if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
return false; return false;
} }
// fallthrough intentional
} }
/* fallthrough */
case QAbstractItemView::ContiguousSelection: { case QAbstractItemView::ContiguousSelection: {
if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
view()->clearSelection(); view()->clearSelection();

View File

@ -48,6 +48,7 @@
#include <QAccessible> #include <QAccessible>
#include "VisualGroup.h" #include "VisualGroup.h"
#include "ui/themes/ThemeManager.h"
#include <QDebug> #include <QDebug>
#include <Application.h> #include <Application.h>
@ -73,6 +74,7 @@ InstanceView::InstanceView(QWidget *parent)
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setAcceptDrops(true); setAcceptDrops(true);
setAutoScroll(true); setAutoScroll(true);
setPaintCat(APPLICATION->settings()->get("TheCat").toBool());
} }
InstanceView::~InstanceView() InstanceView::~InstanceView()
@ -498,12 +500,34 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent *event)
} }
} }
void InstanceView::paintEvent(QPaintEvent *event) void InstanceView::setPaintCat(bool visible)
{
m_catVisible = visible;
if (visible)
m_catPixmap.load(QString(":/backgrounds/%1").arg(ThemeManager::getCatImage()));
else
m_catPixmap = QPixmap();
}
void InstanceView::paintEvent(QPaintEvent* event)
{ {
executeDelayedItemsLayout(); executeDelayedItemsLayout();
QPainter painter(this->viewport()); QPainter painter(this->viewport());
if (m_catVisible) {
int widWidth = this->viewport()->width();
int widHeight = this->viewport()->height();
if (m_catPixmap.width() < widWidth)
widWidth = m_catPixmap.width();
if (m_catPixmap.height() < widHeight)
widHeight = m_catPixmap.height();
auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio);
QRect rectOfPixmap = pixmap.rect();
rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight());
painter.drawPixmap(rectOfPixmap.topLeft(), pixmap);
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QStyleOptionViewItem option; QStyleOptionViewItem option;
initViewItemOption(&option); initViewItemOption(&option);

View File

@ -85,10 +85,8 @@ public:
virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override; virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
int spacing() const int spacing() const { return m_spacing; };
{ void setPaintCat(bool visible);
return m_spacing;
};
public slots: public slots:
virtual void updateGeometries() override; virtual void updateGeometries() override;
@ -139,6 +137,8 @@ private:
int m_currentItemsPerRow = -1; int m_currentItemsPerRow = -1;
int m_currentCursorColumn= -1; int m_currentCursorColumn= -1;
mutable QCache<int, QRect> geometryCache; mutable QCache<int, QRect> geometryCache;
bool m_catVisible = false;
QPixmap m_catPixmap;
// point where the currently active mouse action started in geometry coordinates // point where the currently active mouse action started in geometry coordinates
QPoint m_pressedPosition; QPoint m_pressedPosition;

View File

@ -81,6 +81,8 @@ APIPage::APIPage(QWidget *parent) :
connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder);
// This function needs to be called even when the ComboBox's index is still in its default state. // This function needs to be called even when the ComboBox's index is still in its default state.
updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex());
// NOTE: this allows http://, but we replace that with https later anyway
ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL));
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry));
ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID));
ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey));
@ -163,7 +165,7 @@ void APIPage::applySettings()
QString msaClientID = ui->msaClientID->text(); QString msaClientID = ui->msaClientID->text();
s->set("MSAClientIDOverride", msaClientID); s->set("MSAClientIDOverride", msaClientID);
QUrl metaURL = ui->metaURL->text(); QUrl metaURL(ui->metaURL->text());
// Add required trailing slash // Add required trailing slash
if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) if (!metaURL.isEmpty() && !metaURL.path().endsWith('/'))
{ {

View File

@ -169,7 +169,7 @@
<item> <item>
<widget class="QCheckBox" name="metadataDisableBtn"> <widget class="QCheckBox" name="metadataDisableBtn">
<property name="toolTip"> <property name="toolTip">
<string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string> <string>Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Disable using metadata for mods</string> <string>Disable using metadata for mods</string>

View File

@ -151,9 +151,6 @@ void ExternalResourcesPage::retranslate()
void ExternalResourcesPage::itemActivated(const QModelIndex&) void ExternalResourcesPage::itemActivated(const QModelIndex&)
{ {
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
} }
@ -197,9 +194,6 @@ bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev)
void ExternalResourcesPage::addItem() void ExternalResourcesPage::addItem()
{ {
if (!m_controlsEnabled)
return;
auto list = GuiUtil::BrowseForFiles( auto list = GuiUtil::BrowseForFiles(
helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()),
m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
@ -213,9 +207,6 @@ void ExternalResourcesPage::addItem()
void ExternalResourcesPage::removeItem() void ExternalResourcesPage::removeItem()
{ {
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
int count = 0; int count = 0;
@ -259,23 +250,37 @@ void ExternalResourcesPage::removeItem()
void ExternalResourcesPage::removeItems(const QItemSelection& selection) void ExternalResourcesPage::removeItems(const QItemSelection& selection)
{ {
if (m_instance != nullptr && m_instance->isRunning()) {
auto response = CustomMessageBox::selectable(this, "Confirm Delete",
"If you remove this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
m_model->deleteResources(selection.indexes()); m_model->deleteResources(selection.indexes());
} }
void ExternalResourcesPage::enableItem() void ExternalResourcesPage::enableItem()
{ {
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE); m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
} }
void ExternalResourcesPage::disableItem() void ExternalResourcesPage::disableItem()
{ {
if (!m_controlsEnabled) if (m_instance != nullptr && m_instance->isRunning()) {
return; auto response = CustomMessageBox::selectable(this, "Confirm disable",
"If you disable this resource while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
} }

View File

@ -73,7 +73,5 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QString m_fileSelectionFilter; QString m_fileSelectionFilter;
QString m_viewFilter; QString m_viewFilter;
bool m_controlsEnabled = true;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr; std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
}; };

View File

@ -157,6 +157,17 @@
<string>Try to check or update all selected resources (all resources if none are selected)</string> <string>Try to check or update all selected resources (all resources if none are selected)</string>
</property> </property>
</action> </action>
<action name="actionVisitItemPage">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Visit mod's page</string>
</property>
<property name="toolTip">
<string>Go to mods home page</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -60,17 +60,13 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
m_settings = inst->settings(); m_settings = inst->settings();
ui->setupUi(this); ui->setupUi(this);
// As the signal will (probably) not be triggered once we click edit, let's update it manually instead.
updateRunningStatus(m_instance->isRunning());
connect(m_instance, &BaseInstance::runningStatusChanged, this, &InstanceSettingsPage::updateRunningStatus);
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount); connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&InstanceSettingsPage::changeInstanceAccount);
loadSettings(); loadSettings();
updateThresholds(); updateThresholds();
} }
@ -523,8 +519,3 @@ void InstanceSettingsPage::updateThresholds()
ui->labelMaxMemIcon->setPixmap(pix); ui->labelMaxMemIcon->setPixmap(pix);
} }
} }
void InstanceSettingsPage::updateRunningStatus(bool running)
{
setEnabled(!running);
}

View File

@ -79,8 +79,7 @@ public:
void updateThresholds(); void updateThresholds();
private slots: private slots:
void updateRunningStatus(bool running);
void on_javaDetectBtn_clicked(); void on_javaDetectBtn_clicked();
void on_javaTestBtn_clicked(); void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked(); void on_javaBrowseBtn_clicked();

View File

@ -45,6 +45,7 @@
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <algorithm>
#include "Application.h" #include "Application.h"
@ -60,6 +61,7 @@
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h" #include "modplatform/ResourceAPI.h"
#include "Version.h" #include "Version.h"
@ -86,12 +88,28 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem); ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
auto check_allow_update = [this] { ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
return (!m_instance || !m_instance->isRunning()) && (ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); ui->actionsToolbar->addAction(ui->actionVisitItemPage);
}; connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
ui->actionUpdateItem->setEnabled(check_allow_update());
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
auto mods_list = m_model->selectedMods(selection);
auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(),
[](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; });
if (selected <= 1) {
ui->actionVisitItemPage->setText(tr("Visit mod's page"));
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
} else {
ui->actionVisitItemPage->setText(tr("Visit mods' pages"));
ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods"));
}
ui->actionVisitItemPage->setEnabled(selected != 0);
});
connect(mods.get(), &ModFolderModel::rowsInserted, this, connect(mods.get(), &ModFolderModel::rowsInserted, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
@ -101,22 +119,9 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
connect(mods.get(), &ModFolderModel::updateFinished, this, connect(mods.get(), &ModFolderModel::updateFinished, this,
[this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }); [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged);
ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
} }
} }
void ModFolderPage::runningStateChanged(bool running)
{
ui->actionDownloadItem->setEnabled(!running);
ui->actionUpdateItem->setEnabled(!running);
ui->actionAddItem->setEnabled(!running);
ui->actionEnableItem->setEnabled(!running);
ui->actionDisableItem->setEnabled(!running);
ui->actionRemoveItem->setEnabled(!running);
}
bool ModFolderPage::shouldDisplay() const bool ModFolderPage::shouldDisplay() const
{ {
return true; return true;
@ -133,15 +138,23 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
return true; return true;
} }
void ModFolderPage::removeItems(const QItemSelection &selection) void ModFolderPage::removeItems(const QItemSelection& selection)
{ {
if (m_instance != nullptr && m_instance->isRunning()) {
auto response = CustomMessageBox::selectable(this, "Confirm Delete",
"If you remove mods while the game is running it may crash your game.\n"
"Are you sure you want to do this?",
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
m_model->deleteMods(selection.indexes()); m_model->deleteMods(selection.indexes());
} }
void ModFolderPage::installMods() void ModFolderPage::installMods()
{ {
if (!m_controlsEnabled)
return;
if (m_instance->typeName() != "Minecraft") if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance return; // this is a null instance or a legacy instance
@ -207,8 +220,7 @@ void ModFolderPage::updateMods()
message = tr("All selected mods are up-to-date! :)"); message = tr("All selected mods are up-to-date! :)");
} }
} }
CustomMessageBox::selectable(this, tr("Update checker"), message) CustomMessageBox::selectable(this, tr("Update checker"), message)->exec();
->exec();
return; return;
} }
@ -275,3 +287,13 @@ bool NilModFolderPage::shouldDisplay() const
{ {
return m_model->dir().exists(); return m_model->dir().exists();
} }
void ModFolderPage::visitModPages()
{
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
for (auto mod : m_model->selectedMods(selection)) {
auto url = mod->metaurl();
if (!url.isEmpty())
DesktopServices::openUrl(url);
}
}

View File

@ -4,6 +4,7 @@
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* 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
@ -59,11 +60,11 @@ class ModFolderPage : public ExternalResourcesPage {
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots: private slots:
void runningStateChanged(bool running); void removeItems(const QItemSelection& selection) override;
void removeItems(const QItemSelection &selection) override;
void installMods(); void installMods();
void updateMods(); void updateMods();
void visitModPages();
protected: protected:
std::shared_ptr<ModFolderModel> m_model; std::shared_ptr<ModFolderModel> m_model;

View File

@ -67,8 +67,6 @@ bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QMod
void ResourcePackPage::downloadRPs() void ResourcePackPage::downloadRPs()
{ {
if (!m_controlsEnabled)
return;
if (m_instance->typeName() != "Minecraft") if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance return; // this is a null instance or a legacy instance

View File

@ -97,17 +97,13 @@ public:
return; return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
return; return;
int tries = 5;
while (tries)
{
if (!m_cache->stale(m_path)) if (!m_cache->stale(m_path))
return; return;
QImage image(m_path); QImage image(m_path);
if (image.isNull()) if (image.isNull()) {
{ m_resultEmitter.emitResultsFailed(m_path);
QThread::msleep(500); qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?";
tries--; return;
continue;
} }
QImage small; QImage small;
if (image.width() > image.height()) if (image.width() > image.height())
@ -125,9 +121,6 @@ public:
QIcon icon(QPixmap::fromImage(square)); QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon); m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path); m_resultEmitter.emitResultsReady(m_path);
return;
}
m_resultEmitter.emitResultsFailed(m_path);
} }
QString m_path; QString m_path;
SharedIconCachePtr m_cache; SharedIconCachePtr m_cache;
@ -146,9 +139,12 @@ public:
m_thumbnailCache = std::make_shared<SharedIconCache>(); m_thumbnailCache = std::make_shared<SharedIconCache>();
m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder"));
connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
// FIXME: the watched file set is not updated when files are removed
} }
virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); } virtual ~FilterModel() {
m_thumbnailingPool.clear();
if (!m_thumbnailingPool.waitForDone(500))
qDebug() << "Thumbnail pool took longer than 500ms to finish";
}
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
{ {
auto model = sourceModel(); auto model = sourceModel();
@ -215,10 +211,12 @@ private slots:
void fileChanged(QString filepath) void fileChanged(QString filepath)
{ {
m_thumbnailCache->setStale(filepath); m_thumbnailCache->setStale(filepath);
thumbnailImage(filepath);
// reinsert the path... // reinsert the path...
watcher.removePath(filepath); watcher.removePath(filepath);
if (QFile::exists(filepath)) {
watcher.addPath(filepath); watcher.addPath(filepath);
thumbnailImage(filepath);
}
} }
private: private:

View File

@ -46,7 +46,6 @@
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h"
ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget* parent) ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent) : ExternalResourcesPage(instance, model, parent)
{ {
@ -61,8 +60,6 @@ ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr<Shad
void ShaderPackPage::downloadShaders() void ShaderPackPage::downloadShaders()
{ {
if (!m_controlsEnabled)
return;
if (m_instance->typeName() != "Minecraft") if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance return; // this is a null instance or a legacy instance

View File

@ -69,8 +69,6 @@ bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QMode
void TexturePackPage::downloadTPs() void TexturePackPage::downloadTPs()
{ {
if (!m_controlsEnabled)
return;
if (m_instance->typeName() != "Minecraft") if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance return; // this is a null instance or a legacy instance

View File

@ -40,14 +40,13 @@
#include "Application.h" #include "Application.h"
#include <QMessageBox> #include <QAbstractItemModel>
#include <QLabel>
#include <QEvent> #include <QEvent>
#include <QKeyEvent> #include <QKeyEvent>
#include <QMenu> #include <QLabel>
#include <QAbstractItemModel>
#include <QMessageBox>
#include <QListView> #include <QListView>
#include <QMenu>
#include <QMessageBox>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
@ -56,49 +55,42 @@
#include "ui_VersionPage.h" #include "ui_VersionPage.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/dialogs/NewComponentDialog.h" #include "ui/dialogs/NewComponentDialog.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/VersionSelectDialog.h"
#include "ui/GuiUtil.h" #include "ui/GuiUtil.h"
#include "DesktopServices.h"
#include "Exception.h"
#include "Version.h"
#include "icons/IconList.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "minecraft/auth/AccountList.h" #include "minecraft/auth/AccountList.h"
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "icons/IconList.h"
#include "Exception.h"
#include "Version.h"
#include "DesktopServices.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/VersionList.h" #include "meta/VersionList.h"
class IconProxy : public QIdentityProxyModel class IconProxy : public QIdentityProxyModel {
{
Q_OBJECT Q_OBJECT
public: public:
IconProxy(QWidget* parentWidget) : QIdentityProxyModel(parentWidget)
IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget)
{ {
connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone); connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone);
m_parentWidget = parentWidget; m_parentWidget = parentWidget;
} }
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override
{ {
QVariant var = QIdentityProxyModel::data(proxyIndex, role); QVariant var = QIdentityProxyModel::data(proxyIndex, role);
int column = proxyIndex.column(); int column = proxyIndex.column();
if(column == 0 && role == Qt::DecorationRole && m_parentWidget) if (column == 0 && role == Qt::DecorationRole && m_parentWidget) {
{ if (!var.isNull()) {
if(!var.isNull())
{
auto string = var.toString(); auto string = var.toString();
if(string == "warning") if (string == "warning") {
{
return APPLICATION->getThemedIcon("status-yellow"); return APPLICATION->getThemedIcon("status-yellow");
} } else if (string == "error") {
else if(string == "error")
{
return APPLICATION->getThemedIcon("status-bad"); return APPLICATION->getThemedIcon("status-bad");
} }
} }
@ -106,14 +98,11 @@ public:
} }
return var; return var;
} }
private slots: private slots:
void widgetGone() void widgetGone() { m_parentWidget = nullptr; }
{
m_parentWidget = nullptr;
}
private: private:
QWidget *m_parentWidget = nullptr; QWidget* m_parentWidget = nullptr;
}; };
QIcon VersionPage::icon() const QIcon VersionPage::icon() const
@ -145,15 +134,14 @@ void VersionPage::closedImpl()
m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); m_wide_bar_setting->set(ui->toolBar->getVisibilityState());
} }
QMenu * VersionPage::createPopupMenu() QMenu* VersionPage::createPopupMenu()
{ {
QMenu* filteredMenu = QMainWindow::createPopupMenu(); QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); filteredMenu->removeAction(ui->toolBar->toggleViewAction());
return filteredMenu; return filteredMenu;
} }
VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
: QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
{ {
ui->setupUi(this); ui->setupUi(this);
@ -183,10 +171,8 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls);
controlsEnabled = !m_inst->isRunning();
updateVersionControls(); updateVersionControls();
preselect(0); preselect(0);
connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus);
connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged);
} }
@ -203,18 +189,16 @@ void VersionPage::showContextMenu(const QPoint& pos)
delete menu; delete menu;
} }
void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &previous) void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& previous)
{ {
if (!current.isValid()) if (!current.isValid()) {
{
ui->frame->clear(); ui->frame->clear();
return; return;
} }
int row = current.row(); int row = current.row();
auto patch = m_profile->getComponent(row); auto patch = m_profile->getComponent(row);
auto severity = patch->getProblemSeverity(); auto severity = patch->getProblemSeverity();
switch(severity) switch (severity) {
{
case ProblemSeverity::Warning: case ProblemSeverity::Warning:
ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName())); ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break; break;
@ -227,16 +211,12 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
return; return;
} }
auto &problems = patch->getProblems(); auto& problems = patch->getProblems();
QString problemOut; QString problemOut;
for (auto &problem: problems) for (auto& problem : problems) {
{ if (problem.m_severity == ProblemSeverity::Error) {
if(problem.m_severity == ProblemSeverity::Error)
{
problemOut += tr("Error: "); problemOut += tr("Error: ");
} } else if (problem.m_severity == ProblemSeverity::Warning) {
else if(problem.m_severity == ProblemSeverity::Warning)
{
problemOut += tr("Warning: "); problemOut += tr("Warning: ");
} }
problemOut += problem.m_description; problemOut += problem.m_description;
@ -245,14 +225,6 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
ui->frame->setDescription(problemOut); ui->frame->setDescription(problemOut);
} }
void VersionPage::updateRunningStatus(bool running)
{
if(controlsEnabled == running) {
controlsEnabled = !running;
updateVersionControls();
}
}
void VersionPage::updateVersionControls() void VersionPage::updateVersionControls()
{ {
updateButtons(); updateButtons();
@ -260,43 +232,28 @@ void VersionPage::updateVersionControls()
void VersionPage::updateButtons(int row) void VersionPage::updateButtons(int row)
{ {
if(row == -1) if (row == -1)
row = currentRow(); row = currentRow();
auto patch = m_profile->getComponent(row); auto patch = m_profile->getComponent(row);
ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable()); ui->actionRemove->setEnabled(patch && patch->isRemovable());
ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable()); ui->actionMove_down->setEnabled(patch && patch->isMoveable());
ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable()); ui->actionMove_up->setEnabled(patch && patch->isMoveable());
ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable()); ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable());
ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom()); ui->actionEdit->setEnabled(patch && patch->isCustom());
ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable()); ui->actionCustomize->setEnabled(patch && patch->isCustomizable());
ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible()); ui->actionRevert->setEnabled(patch && patch->isRevertible());
ui->actionDownload_All->setEnabled(controlsEnabled);
ui->actionAdd_Empty->setEnabled(controlsEnabled);
ui->actionImport_Components->setEnabled(controlsEnabled);
ui->actionReload->setEnabled(controlsEnabled);
ui->actionInstall_Loader->setEnabled(controlsEnabled);
ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled);
ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
ui->actionAdd_Agents->setEnabled(controlsEnabled);
} }
bool VersionPage::reloadPackProfile() bool VersionPage::reloadPackProfile()
{ {
try try {
{
m_profile->reload(Net::Mode::Online); m_profile->reload(Net::Mode::Online);
return true; return true;
} } catch (const Exception& e) {
catch (const Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause()); QMessageBox::critical(this, tr("Error"), e.cause());
return false; return false;
} } catch (...) {
catch (...) QMessageBox::critical(this, tr("Error"), tr("Couldn't load the instance profile."));
{
QMessageBox::critical(
this, tr("Error"),
tr("Couldn't load the instance profile."));
return false; return false;
} }
} }
@ -309,14 +266,12 @@ void VersionPage::on_actionReload_triggered()
void VersionPage::on_actionRemove_triggered() void VersionPage::on_actionRemove_triggered()
{ {
if (!ui->packageView->currentIndex().isValid()) if (!ui->packageView->currentIndex().isValid()) {
{
return; return;
} }
int index = ui->packageView->currentIndex().row(); int index = ui->packageView->currentIndex().row();
auto component = m_profile->getComponent(index); auto component = m_profile->getComponent(index);
if (component->isCustom()) if (component->isCustom()) {
{
auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"),
tr("You are about to remove \"%1\".\n" tr("You are about to remove \"%1\".\n"
"This is permanent and will completely remove the custom component.\n\n" "This is permanent and will completely remove the custom component.\n\n"
@ -329,8 +284,7 @@ void VersionPage::on_actionRemove_triggered()
return; return;
} }
// FIXME: use actual model, not reloading. // FIXME: use actual model, not reloading.
if (!m_profile->remove(index)) if (!m_profile->remove(index)) {
{
QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
} }
updateButtons(); updateButtons();
@ -340,17 +294,16 @@ void VersionPage::on_actionRemove_triggered()
void VersionPage::on_actionInstall_mods_triggered() void VersionPage::on_actionInstall_mods_triggered()
{ {
if(m_container) if (m_container) {
{
m_container->selectPage("mods"); m_container->selectPage("mods");
} }
} }
void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
{ {
auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"),
if(!list.empty()) APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
{ if (!list.empty()) {
m_profile->installJarMods(list); m_profile->installJarMods(list);
} }
updateButtons(); updateButtons();
@ -358,9 +311,9 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
void VersionPage::on_actionReplace_Minecraft_jar_triggered() void VersionPage::on_actionReplace_Minecraft_jar_triggered()
{ {
auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"),
if(!jarPath.isEmpty()) APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
{ if (!jarPath.isEmpty()) {
m_profile->installCustomJar(jarPath); m_profile->installCustomJar(jarPath);
} }
updateButtons(); updateButtons();
@ -394,12 +347,9 @@ void VersionPage::on_actionAdd_Agents_triggered()
void VersionPage::on_actionMove_up_triggered() void VersionPage::on_actionMove_up_triggered()
{ {
try try {
{
m_profile->move(currentRow(), PackProfile::MoveUp); m_profile->move(currentRow(), PackProfile::MoveUp);
} } catch (const Exception& e) {
catch (const Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause()); QMessageBox::critical(this, tr("Error"), e.cause());
} }
updateButtons(); updateButtons();
@ -407,12 +357,9 @@ void VersionPage::on_actionMove_up_triggered()
void VersionPage::on_actionMove_down_triggered() void VersionPage::on_actionMove_down_triggered()
{ {
try try {
{
m_profile->move(currentRow(), PackProfile::MoveDown); m_profile->move(currentRow(), PackProfile::MoveDown);
} } catch (const Exception& e) {
catch (const Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause()); QMessageBox::critical(this, tr("Error"), e.cause());
} }
updateButtons(); updateButtons();
@ -421,30 +368,26 @@ void VersionPage::on_actionMove_down_triggered()
void VersionPage::on_actionChange_version_triggered() void VersionPage::on_actionChange_version_triggered()
{ {
auto versionRow = currentRow(); auto versionRow = currentRow();
if(versionRow == -1) if (versionRow == -1) {
{
return; return;
} }
auto patch = m_profile->getComponent(versionRow); auto patch = m_profile->getComponent(versionRow);
auto name = patch->getName(); auto name = patch->getName();
auto list = patch->getVersionList(); auto list = patch->getVersionList();
if(!list) if (!list) {
{
return; return;
} }
auto uid = list->uid(); auto uid = list->uid();
VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this);
if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") {
{
vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyString(tr("No intermediary mappings versions are currently available."));
vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!"));
} }
vselect.setExactIfPresentFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); vselect.setExactIfPresentFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
auto currentVersion = patch->getVersion(); auto currentVersion = patch->getVersion();
if(!currentVersion.isEmpty()) if (!currentVersion.isEmpty()) {
{
vselect.setCurrentVersion(currentVersion); vselect.setCurrentVersion(currentVersion);
} }
if (!vselect.exec() || !vselect.selectedVersion()) if (!vselect.exec() || !vselect.selectedVersion())
@ -452,8 +395,7 @@ void VersionPage::on_actionChange_version_triggered()
qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor();
bool important = false; bool important = false;
if(uid == "net.minecraft") if (uid == "net.minecraft") {
{
important = true; important = true;
} }
m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important);
@ -463,19 +405,17 @@ void VersionPage::on_actionChange_version_triggered()
void VersionPage::on_actionDownload_All_triggered() void VersionPage::on_actionDownload_All_triggered()
{ {
if (!APPLICATION->accounts()->anyAccountIsValid()) if (!APPLICATION->accounts()->anyAccountIsValid()) {
{ CustomMessageBox::selectable(this, tr("Error"),
CustomMessageBox::selectable(
this, tr("Error"),
tr("Cannot download Minecraft or update instances unless you have at least " tr("Cannot download Minecraft or update instances unless you have at least "
"one account added.\nPlease add your Mojang or Minecraft account."), "one account added.\nPlease add your Mojang or Minecraft account."),
QMessageBox::Warning)->show(); QMessageBox::Warning)
->show();
return; return;
} }
auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); auto updateTask = m_inst->createUpdateTask(Net::Mode::Online);
if (!updateTask) if (!updateTask) {
{
return; return;
} }
ProgressDialog tDialog(this); ProgressDialog tDialog(this);
@ -497,14 +437,12 @@ void VersionPage::on_actionAdd_Empty_triggered()
{ {
NewComponentDialog compdialog(QString(), QString(), this); NewComponentDialog compdialog(QString(), QString(), this);
QStringList blacklist; QStringList blacklist;
for(int i = 0; i < m_profile->rowCount(); i++) for (int i = 0; i < m_profile->rowCount(); i++) {
{
auto comp = m_profile->getComponent(i); auto comp = m_profile->getComponent(i);
blacklist.push_back(comp->getID()); blacklist.push_back(comp->getID());
} }
compdialog.setBlacklist(blacklist); compdialog.setBlacklist(blacklist);
if (compdialog.exec()) if (compdialog.exec()) {
{
qDebug() << "name:" << compdialog.name(); qDebug() << "name:" << compdialog.name();
qDebug() << "uid:" << compdialog.uid(); qDebug() << "uid:" << compdialog.uid();
m_profile->installEmpty(compdialog.uid(), compdialog.name()); m_profile->installEmpty(compdialog.uid(), compdialog.name());
@ -521,7 +459,7 @@ void VersionPage::on_actionMinecraftFolder_triggered()
DesktopServices::openDirectory(m_inst->gameRoot(), true); DesktopServices::openDirectory(m_inst->gameRoot(), true);
} }
void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &previous) void VersionPage::versionCurrent(const QModelIndex& current, const QModelIndex& previous)
{ {
currentIdx = current.row(); currentIdx = current.row();
updateButtons(currentIdx); updateButtons(currentIdx);
@ -529,16 +467,13 @@ void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &
void VersionPage::preselect(int row) void VersionPage::preselect(int row)
{ {
if(row < 0) if (row < 0) {
{
row = 0; row = 0;
} }
if(row >= m_profile->rowCount(QModelIndex())) if (row >= m_profile->rowCount(QModelIndex())) {
{
row = m_profile->rowCount(QModelIndex()) - 1; row = m_profile->rowCount(QModelIndex()) - 1;
} }
if(row < 0) if (row < 0) {
{
return; return;
} }
auto model_index = m_profile->index(row); auto model_index = m_profile->index(row);
@ -554,8 +489,7 @@ void VersionPage::onGameUpdateError(QString error)
ComponentPtr VersionPage::current() ComponentPtr VersionPage::current()
{ {
auto row = currentRow(); auto row = currentRow();
if(row < 0) if (row < 0) {
{
return nullptr; return nullptr;
} }
return m_profile->getComponent(row); return m_profile->getComponent(row);
@ -563,8 +497,7 @@ ComponentPtr VersionPage::current()
int VersionPage::currentRow() int VersionPage::currentRow()
{ {
if (ui->packageView->selectionModel()->selectedRows().isEmpty()) if (ui->packageView->selectionModel()->selectedRows().isEmpty()) {
{
return -1; return -1;
} }
return ui->packageView->selectionModel()->selectedRows().first().row(); return ui->packageView->selectionModel()->selectedRows().first().row();
@ -573,18 +506,15 @@ int VersionPage::currentRow()
void VersionPage::on_actionCustomize_triggered() void VersionPage::on_actionCustomize_triggered()
{ {
auto version = currentRow(); auto version = currentRow();
if(version == -1) if (version == -1) {
{
return; return;
} }
auto patch = m_profile->getComponent(version); auto patch = m_profile->getComponent(version);
if(!patch->getVersionFile()) if (!patch->getVersionFile()) {
{
// TODO: wait for the update task to finish here... // TODO: wait for the update task to finish here...
return; return;
} }
if(!m_profile->customize(version)) if (!m_profile->customize(version)) {
{
// TODO: some error box here // TODO: some error box here
} }
updateButtons(); updateButtons();
@ -594,13 +524,11 @@ void VersionPage::on_actionCustomize_triggered()
void VersionPage::on_actionEdit_triggered() void VersionPage::on_actionEdit_triggered()
{ {
auto version = current(); auto version = current();
if(!version) if (!version) {
{
return; return;
} }
auto filename = version->getFilename(); auto filename = version->getFilename();
if(!QFileInfo::exists(filename)) if (!QFileInfo::exists(filename)) {
{
qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!"; qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!";
return; return;
} }
@ -610,8 +538,7 @@ void VersionPage::on_actionEdit_triggered()
void VersionPage::on_actionRevert_triggered() void VersionPage::on_actionRevert_triggered()
{ {
auto version = currentRow(); auto version = currentRow();
if(version == -1) if (version == -1) {
{
return; return;
} }
auto component = m_profile->getComponent(version); auto component = m_profile->getComponent(version);
@ -627,8 +554,7 @@ void VersionPage::on_actionRevert_triggered()
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes)
return; return;
if(!m_profile->revertToBase(version)) if (!m_profile->revertToBase(version)) {
{
// TODO: some error box here // TODO: some error box here
} }
updateButtons(); updateButtons();
@ -636,7 +562,7 @@ void VersionPage::on_actionRevert_triggered()
m_container->refreshContainer(); m_container->refreshContainer();
} }
void VersionPage::onFilterTextChanged(const QString &newContents) void VersionPage::onFilterTextChanged(const QString& newContents)
{ {
m_filterModel->setFilterFixedString(newContents); m_filterModel->setFilterFixedString(newContents);
} }

View File

@ -46,38 +46,27 @@
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
namespace Ui namespace Ui {
{
class VersionPage; class VersionPage;
} }
class VersionPage : public QMainWindow, public BasePage class VersionPage : public QMainWindow, public BasePage {
{
Q_OBJECT Q_OBJECT
public: public:
explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0); explicit VersionPage(MinecraftInstance* inst, QWidget* parent = 0);
virtual ~VersionPage(); virtual ~VersionPage();
virtual QString displayName() const override virtual QString displayName() const override { return tr("Version"); }
{
return tr("Version");
}
virtual QIcon icon() const override; virtual QIcon icon() const override;
virtual QString id() const override virtual QString id() const override { return "version"; }
{ virtual QString helpPage() const override { return "Instance-Version"; }
return "version";
}
virtual QString helpPage() const override
{
return "Instance-Version";
}
virtual bool shouldDisplay() const override; virtual bool shouldDisplay() const override;
void retranslate() override; void retranslate() override;
void openedImpl() override; void openedImpl() override;
void closedImpl() override; void closedImpl() override;
private slots: private slots:
void on_actionChange_version_triggered(); void on_actionChange_version_triggered();
void on_actionInstall_Loader_triggered(); void on_actionInstall_Loader_triggered();
void on_actionAdd_Empty_triggered(); void on_actionAdd_Empty_triggered();
@ -100,36 +89,34 @@ private slots:
void updateVersionControls(); void updateVersionControls();
private: private:
ComponentPtr current(); ComponentPtr current();
int currentRow(); int currentRow();
void updateButtons(int row = -1); void updateButtons(int row = -1);
void preselect(int row = 0); void preselect(int row = 0);
int doUpdate(); int doUpdate();
protected: protected:
QMenu * createPopupMenu() override; QMenu* createPopupMenu() override;
/// FIXME: this shouldn't be necessary! /// FIXME: this shouldn't be necessary!
bool reloadPackProfile(); bool reloadPackProfile();
private: private:
Ui::VersionPage *ui; Ui::VersionPage* ui;
QSortFilterProxyModel *m_filterModel; QSortFilterProxyModel* m_filterModel;
std::shared_ptr<PackProfile> m_profile; std::shared_ptr<PackProfile> m_profile;
MinecraftInstance *m_inst; MinecraftInstance* m_inst;
int currentIdx = 0; int currentIdx = 0;
bool controlsEnabled = false;
std::shared_ptr<Setting> m_wide_bar_setting = nullptr; std::shared_ptr<Setting> m_wide_bar_setting = nullptr;
public slots: public slots:
void versionCurrent(const QModelIndex &current, const QModelIndex &previous); void versionCurrent(const QModelIndex& current, const QModelIndex& previous);
private slots: private slots:
void updateRunningStatus(bool running);
void onGameUpdateError(QString error); void onGameUpdateError(QString error);
void packageCurrent(const QModelIndex &current, const QModelIndex &previous); void packageCurrent(const QModelIndex& current, const QModelIndex& previous);
void showContextMenu(const QPoint &pos); void showContextMenu(const QPoint& pos);
void onFilterTextChanged(const QString & newContents); void onFilterTextChanged(const QString& newContents);
}; };

View File

@ -339,6 +339,7 @@ void WorldListPage::mceditState(LoggedProcess::State state)
{ {
failed = true; failed = true;
} }
/* fallthrough */
case LoggedProcess::Running: case LoggedProcess::Running:
case LoggedProcess::Finished: case LoggedProcess::Finished:
{ {

View File

@ -69,6 +69,7 @@ bool JavaWizardPage::validatePage()
case JavaSettingsWidget::ValidationStatus::AllOK: case JavaSettingsWidget::ValidationStatus::AllOK:
{ {
settings->set("JavaPath", m_java_widget->javaPath()); settings->set("JavaPath", m_java_widget->javaPath());
return true;
} }
case JavaSettingsWidget::ValidationStatus::JavaBad: case JavaSettingsWidget::ValidationStatus::JavaBad:
{ {

View File

@ -1,54 +1,70 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - 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 *
* it under the terms of the GNU General Public License as published by * This program is free software: you can redistribute it and/or modify
* the Free Software Foundation, version 3. * 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 * This program is distributed in the hope that it will be useful,
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * but WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU General Public License for more details. * 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/>. * 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: * This file incorporates work covered by the following copyright and
* * permission notice:
* Copyright 2013-2021 MultiMC Contributors *
* * 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. * Licensed under the Apache License, Version 2.0 (the "License");
* You may obtain a copy of the License at * 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 *
* * 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, * Unless required by applicable law or agreed to in writing, software
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * distributed under the License is distributed on an "AS IS" BASIS,
* See the License for the specific language governing permissions and * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* limitations under the License. * See the License for the specific language governing permissions and
*/ * limitations under the License.
*/
#include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QToolTip>
#include "InfoFrame.h" #include "InfoFrame.h"
#include "ui_InfoFrame.h" #include "ui_InfoFrame.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
InfoFrame::InfoFrame(QWidget *parent) : void setupLinkToolTip(QLabel* label)
QFrame(parent), {
ui(new Ui::InfoFrame) QObject::connect(label, &QLabel::linkHovered, [label](const QString& link) {
if (auto url = QUrl(link); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https"))
return;
label->setToolTip(link);
});
}
InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame)
{ {
ui->setupUi(this); ui->setupUi(this);
ui->descriptionLabel->setHidden(true); ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true); ui->nameLabel->setHidden(true);
ui->licenseLabel->setHidden(true); ui->licenseLabel->setHidden(true);
ui->issueTrackerLabel->setHidden(true); ui->issueTrackerLabel->setHidden(true);
setupLinkToolTip(ui->iconLabel);
setupLinkToolTip(ui->descriptionLabel);
setupLinkToolTip(ui->nameLabel);
setupLinkToolTip(ui->licenseLabel);
setupLinkToolTip(ui->issueTrackerLabel);
updateHiddenState(); updateHiddenState();
} }
@ -59,38 +75,36 @@ InfoFrame::~InfoFrame()
void InfoFrame::updateWithMod(Mod const& m) void InfoFrame::updateWithMod(Mod const& m)
{ {
if (m.type() == ResourceType::FOLDER) if (m.type() == ResourceType::FOLDER) {
{
clear(); clear();
return; return;
} }
QString text = ""; QString text = "";
QString name = ""; QString name = "";
QString link = m.metaurl();
if (m.name().isEmpty()) if (m.name().isEmpty())
name = m.internal_id(); name = m.internal_id();
else else
name = m.name(); name = m.name();
if (m.homeurl().isEmpty()) if (link.isEmpty())
text = name; text = name;
else else {
text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>"; text = "<a href=\"" + link + "\">" + name + "</a>";
}
if (!m.authors().isEmpty()) if (!m.authors().isEmpty())
text += " by " + m.authors().join(", "); text += " by " + m.authors().join(", ");
setName(text); setName(text);
if (m.description().isEmpty()) if (m.description().isEmpty()) {
{
setDescription(QString()); setDescription(QString());
} } else {
else
{
setDescription(m.description()); setDescription(m.description());
} }
setImage(m.icon({64,64})); setImage(m.icon({ 64, 64 }));
auto licenses = m.licenses(); auto licenses = m.licenses();
QString licenseText = ""; QString licenseText = "";
@ -133,7 +147,8 @@ void InfoFrame::updateWithResource(const Resource& resource)
setImage(); setImage();
} }
QString InfoFrame::renderColorCodes(QString input) { QString InfoFrame::renderColorCodes(QString input)
{
// We have to manually set the colors for use. // We have to manually set the colors for use.
// //
// A color is set using §x, with x = a hex number from 0 to f. // A color is set using §x, with x = a hex number from 0 to f.
@ -144,16 +159,12 @@ QString InfoFrame::renderColorCodes(QString input) {
// TODO: Wrap links inside <a> tags // TODO: Wrap links inside <a> tags
// https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes // https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes
const QMap<QChar, QString> color_codes_map = { const QMap<QChar, QString> color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" },
{'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, { '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" },
{'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, { '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" },
{'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } };
{'f', "#FFFFFF"}
};
// https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes // https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes
const QMap<QChar, QString> formatting_codes_map = { const QMap<QChar, QString> formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } };
{'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"}
};
QString html("<html>"); QString html("<html>");
QList<QString> tags{}; QList<QString> tags{};
@ -198,14 +209,14 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
{ {
setName(renderColorCodes(resource_pack.name())); setName(renderColorCodes(resource_pack.name()));
setDescription(renderColorCodes(resource_pack.description())); setDescription(renderColorCodes(resource_pack.description()));
setImage(resource_pack.image({64, 64})); setImage(resource_pack.image({ 64, 64 }));
} }
void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
{ {
setName(renderColorCodes(texture_pack.name())); setName(renderColorCodes(texture_pack.name()));
setDescription(renderColorCodes(texture_pack.description())); setDescription(renderColorCodes(texture_pack.description()));
setImage(texture_pack.image({64, 64})); setImage(texture_pack.image({ 64, 64 }));
} }
void InfoFrame::clear() void InfoFrame::clear()
@ -229,12 +240,9 @@ void InfoFrame::updateHiddenState()
void InfoFrame::setName(QString text) void InfoFrame::setName(QString text)
{ {
if(text.isEmpty()) if (text.isEmpty()) {
{
ui->nameLabel->setHidden(true); ui->nameLabel->setHidden(true);
} } else {
else
{
ui->nameLabel->setText(text); ui->nameLabel->setText(text);
ui->nameLabel->setHidden(false); ui->nameLabel->setHidden(false);
} }
@ -243,14 +251,11 @@ void InfoFrame::setName(QString text)
void InfoFrame::setDescription(QString text) void InfoFrame::setDescription(QString text)
{ {
if(text.isEmpty()) if (text.isEmpty()) {
{
ui->descriptionLabel->setHidden(true); ui->descriptionLabel->setHidden(true);
updateHiddenState(); updateHiddenState();
return; return;
} } else {
else
{
ui->descriptionLabel->setHidden(false); ui->descriptionLabel->setHidden(false);
updateHiddenState(); updateHiddenState();
} }
@ -260,9 +265,8 @@ void InfoFrame::setDescription(QString text)
QChar rem('\n'); QChar rem('\n');
QString finaltext; QString finaltext;
finaltext.reserve(intermediatetext.size()); finaltext.reserve(intermediatetext.size());
foreach(const QChar& c, intermediatetext) foreach (const QChar& c, intermediatetext) {
{ if (c == rem && prev) {
if(c == rem && prev){
continue; continue;
} }
prev = c == rem; prev = c == rem;
@ -270,17 +274,14 @@ void InfoFrame::setDescription(QString text)
} }
QString labeltext; QString labeltext;
labeltext.reserve(300); labeltext.reserve(300);
if(finaltext.length() > 290) if (finaltext.length() > 290) {
{
ui->descriptionLabel->setOpenExternalLinks(false); ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text; m_description = text;
// This allows injecting HTML here. // This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>"); labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
} } else {
else
{
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext); labeltext.append(finaltext);
} }
@ -289,14 +290,11 @@ void InfoFrame::setDescription(QString text)
void InfoFrame::setLicense(QString text) void InfoFrame::setLicense(QString text)
{ {
if(text.isEmpty()) if (text.isEmpty()) {
{
ui->licenseLabel->setHidden(true); ui->licenseLabel->setHidden(true);
updateHiddenState(); updateHiddenState();
return; return;
} } else {
else
{
ui->licenseLabel->setHidden(false); ui->licenseLabel->setHidden(false);
updateHiddenState(); updateHiddenState();
} }
@ -306,9 +304,8 @@ void InfoFrame::setLicense(QString text)
QChar rem('\n'); QChar rem('\n');
QString finaltext; QString finaltext;
finaltext.reserve(intermediatetext.size()); finaltext.reserve(intermediatetext.size());
foreach(const QChar& c, intermediatetext) foreach (const QChar& c, intermediatetext) {
{ if (c == rem && prev) {
if(c == rem && prev){
continue; continue;
} }
prev = c == rem; prev = c == rem;
@ -316,17 +313,14 @@ void InfoFrame::setLicense(QString text)
} }
QString labeltext; QString labeltext;
labeltext.reserve(300); labeltext.reserve(300);
if(finaltext.length() > 290) if (finaltext.length() > 290) {
{
ui->licenseLabel->setOpenExternalLinks(false); ui->licenseLabel->setOpenExternalLinks(false);
ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text; m_description = text;
// This allows injecting HTML here. // This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>"); labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
} } else {
else
{
ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext); labeltext.append(finaltext);
} }
@ -335,12 +329,9 @@ void InfoFrame::setLicense(QString text)
void InfoFrame::setIssueTracker(QString text) void InfoFrame::setIssueTracker(QString text)
{ {
if(text.isEmpty()) if (text.isEmpty()) {
{
ui->issueTrackerLabel->setHidden(true); ui->issueTrackerLabel->setHidden(true);
} } else {
else
{
ui->issueTrackerLabel->setText(text); ui->issueTrackerLabel->setText(text);
ui->issueTrackerLabel->setHidden(false); ui->issueTrackerLabel->setHidden(false);
} }
@ -359,28 +350,22 @@ void InfoFrame::setImage(QPixmap img)
void InfoFrame::descriptionEllipsisHandler(QString link) void InfoFrame::descriptionEllipsisHandler(QString link)
{ {
if(!m_current_box) if (!m_current_box) {
{
m_current_box = CustomMessageBox::selectable(this, "", m_description); m_current_box = CustomMessageBox::selectable(this, "", m_description);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show(); m_current_box->show();
} } else {
else
{
m_current_box->setText(m_description); m_current_box->setText(m_description);
} }
} }
void InfoFrame::licenseEllipsisHandler(QString link) void InfoFrame::licenseEllipsisHandler(QString link)
{ {
if(!m_current_box) if (!m_current_box) {
{
m_current_box = CustomMessageBox::selectable(this, "", m_license); m_current_box = CustomMessageBox::selectable(this, "", m_license);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show(); m_current_box->show();
} } else {
else
{
m_current_box->setText(m_license); m_current_box->setText(m_license);
} }
} }

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,8 +41,7 @@
#include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/TexturePack.h" #include "minecraft/mod/TexturePack.h"
namespace Ui namespace Ui {
{
class InfoFrame; class InfoFrame;
} }

View File

@ -116,12 +116,21 @@ void WideBar::insertActionAfter(QAction* after, QAction* action)
if (iter == m_entries.end()) if (iter == m_entries.end())
return; return;
iter++;
// the action to insert after is present
// however, the element after it isn't valid
if (iter == m_entries.end()) {
// append the action instead of inserting it
addAction(action);
return;
}
BarEntry entry; BarEntry entry;
entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this, m_use_default_action)); entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action));
entry.menu_action = action; entry.menu_action = action;
entry.type = BarEntry::Type::Action; entry.type = BarEntry::Type::Action;
m_entries.insert(iter + 1, entry); m_entries.insert(iter, entry);
m_menu_state = MenuState::Dirty; m_menu_state = MenuState::Dirty;
} }

View File

@ -89,8 +89,10 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev)
switch (prev.len) { switch (prev.len) {
case 3: case 3:
prev.h ^= data[2] << 16; prev.h ^= data[2] << 16;
/* fall through */
case 2: case 2:
prev.h ^= data[1] << 8; prev.h ^= data[1] << 8;
/* fall through */
case 1: case 1:
prev.h ^= data[0]; prev.h ^= data[0];
prev.h *= m; prev.h *= m;