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

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2023-07-15 20:35:11 +03:00
commit 21ffed3bf5
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
82 changed files with 1809 additions and 965 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

View File

@ -85,6 +85,38 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets # set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" on)
# If this is a Debug build turn on address sanitiser
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER)
message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off")
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# using clang with clang-cl front end
message(STATUS "Address Sanitizer available on Clang MSVC frontend")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}")
endif()
endif()
option(ENABLE_LTO "Enable Link Time Optimization" off) option(ENABLE_LTO "Enable Link Time Optimization" off)
if(ENABLE_LTO) if(ENABLE_LTO)
@ -332,7 +364,7 @@ elseif(UNIX)
set(BINARY_DEST_DIR "bin") set(BINARY_DEST_DIR "bin")
set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}")
set(JARS_DEST_DIR "share/${Launcher_APP_BINARY_NAME}") set(JARS_DEST_DIR "share/${Launcher_Name}")
# install as bundle with no dependencies included # install as bundle with no dependencies included
set(INSTALL_BUNDLE "nodeps") set(INSTALL_BUNDLE "nodeps")
@ -345,7 +377,7 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "${KDE_INSTALL_DATADIR}/${Launcher_Name}") install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
if(Launcher_ManPage) if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")

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

@ -433,7 +433,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
} }
// seach root path // seach root path
if(!foundLoggingRules) { if(!foundLoggingRules) {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
qDebug() << "Testing" << logRulesPath << "..."; qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath); foundLoggingRules = QFile::exists(logRulesPath);
} }
@ -568,6 +572,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Language // Language
m_settings->registerSetting("Language", QString()); m_settings->registerSetting("Language", QString());
m_settings->registerSetting("UseSystemLocale", false);
// Console // Console
m_settings->registerSetting("ShowConsole", false); m_settings->registerSetting("ShowConsole", false);
@ -687,8 +692,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase"); m_settings->reset("PastebinCustomAPIBase");
} }
} }
// meta URL {
m_settings->registerSetting("MetaURLOverride", ""); // Meta URL
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);
@ -910,12 +923,7 @@ bool Application::createSetupWizard()
} }
return false; return false;
}(); }();
bool languageRequired = [&]() bool languageRequired = settings()->get("Language").toString().isEmpty();
{
if (settings()->get("Language").toString().isEmpty())
return true;
return false;
}();
bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
@ -1559,7 +1567,7 @@ QString Application::getJarPath(QString jarFile)
{ {
QStringList potentialPaths = { QStringList potentialPaths = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME), FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif #endif
FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "jars"),

View File

@ -524,6 +524,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
@ -917,8 +919,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
@ -1065,7 +1067,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

@ -40,6 +40,7 @@
#include <QFileSystemModel> #include <QFileSystemModel>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QStack> #include <QStack>
#include <algorithm>
#include "FileSystem.h" #include "FileSystem.h"
#include "SeparatorPrefixTree.h" #include "SeparatorPrefixTree.h"
#include "StringUtils.h" #include "StringUtils.h"
@ -254,3 +255,25 @@ bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex&
return true; return true;
} }
bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto fileInfo = fsm->fileInfo(index);
return !ignoreFile(fileInfo);
}
bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
{
auto fileName = fileInfo.fileName();
auto path = relPath(fileInfo.absoluteFilePath());
return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) ||
m_ignoreFilePaths.covers(path);
}
bool FileIgnoreProxy::filterFile(const QString& fileName) const
{
return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
}

View File

@ -36,6 +36,7 @@
#pragma once #pragma once
#include <QFileInfo>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include "SeparatorPrefixTree.h" #include "SeparatorPrefixTree.h"
@ -63,10 +64,22 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
// list of file names that need to be removed completely from model
inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
// list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
bool filterFile(const QString& fileName) const;
protected: protected:
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
bool ignoreFile(QFileInfo file) const;
private: private:
const QString root; const QString root;
SeparatorPrefixTree<'/'> blocked; SeparatorPrefixTree<'/'> blocked;
QStringList m_ignoreFiles;
SeparatorPrefixTree<'/'> m_ignoreFilePaths;
}; };

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

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,11 +357,12 @@ 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) {
{ return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
case ProfileNameColumn: } else {
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; 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,15 +241,18 @@ 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.reset(); m_current_update_task.get(), &Task::finished, this,
if (m_scheduled_update) { [=] {
m_scheduled_update = false; m_current_update_task.reset();
update(); if (m_scheduled_update) {
} else { m_scheduled_update = false;
emit updateFinished(); update();
} } else {
}, Qt::ConnectionType::QueuedConnection); emit updateFinished();
}
},
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; flags |= Qt::ItemIsUserCheckable;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags; return flags;
} }
@ -428,16 +417,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
if (column == NAME_COLUMN) { if (column == NAME_COLUMN) {
if (at(row).isSymLinkUnder(instDirPath())) { if (at(row).isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() + return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\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() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
} }
} }
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::DecorationRole: { case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
@ -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

@ -44,7 +44,11 @@ static const QMap<PackedResourceType, QString> s_packed_type_names = {
namespace ResourceUtils { namespace ResourceUtils {
PackedResourceType identify(QFileInfo file){ PackedResourceType identify(QFileInfo file){
if (file.exists() && file.isFile()) { if (file.exists() && file.isFile()) {
if (ResourcePackUtils::validate(file)) { if (ModUtils::validate(file)) {
// mods can contain resource and data packs so they must be tested first
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (ResourcePackUtils::validate(file)) {
qDebug() << file.fileName() << "is a resource pack"; qDebug() << file.fileName() << "is a resource pack";
return PackedResourceType::ResourcePack; return PackedResourceType::ResourcePack;
} else if (TexturePackUtils::validate(file)) { } else if (TexturePackUtils::validate(file)) {
@ -53,9 +57,6 @@ PackedResourceType identify(QFileInfo file){
} else if (DataPackUtils::validate(file)) { } else if (DataPackUtils::validate(file)) {
qDebug() << file.fileName() << "is a data pack"; qDebug() << file.fileName() << "is a data pack";
return PackedResourceType::DataPack; return PackedResourceType::DataPack;
} else if (ModUtils::validate(file)) {
qDebug() << file.fileName() << "is a mod";
return PackedResourceType::Mod;
} else if (WorldSaveUtils::validate(file)) { } else if (WorldSaveUtils::validate(file)) {
qDebug() << file.fileName() << "is a world save"; qDebug() << file.fileName() << "is a world save";
return PackedResourceType::WorldSave; return PackedResourceType::WorldSave;

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

@ -145,7 +145,8 @@ void EnsureMetadataTask::executeTask()
connect(project_task.get(), &Task::finished, this, [=] { connect(project_task.get(), &Task::finished, this, [=] {
invalidade_leftover(); invalidade_leftover();
project_task->deleteLater(); project_task->deleteLater();
m_current_task = nullptr; if (m_current_task)
m_current_task.reset();
}); });
m_current_task = project_task; m_current_task = project_task;
@ -154,7 +155,8 @@ void EnsureMetadataTask::executeTask()
connect(version_task.get(), &Task::finished, [=] { connect(version_task.get(), &Task::finished, [=] {
version_task->deleteLater(); version_task->deleteLater();
m_current_task = nullptr; if (m_current_task)
m_current_task.reset();
}); });
if (m_mods.size() > 1) if (m_mods.size() > 1)

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

@ -472,8 +472,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()) {
@ -564,6 +565,8 @@ void FlameCreationTask::validateZIPResouces()
if (FS::move(localPath, destPath)) { if (FS::move(localPath, destPath)) {
return destPath; return destPath;
} }
} else {
qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
} }
return localPath; return localPath;
}; };
@ -585,6 +588,9 @@ void FlameCreationTask::validateZIPResouces()
QString worldPath; QString worldPath;
switch (type) { switch (type) {
case PackedResourceType::Mod :
validatePath(fileName, targetFolder, "mods");
break;
case PackedResourceType::ResourcePack : case PackedResourceType::ResourcePack :
validatePath(fileName, targetFolder, "resourcepacks"); validatePath(fileName, targetFolder, "resourcepacks");
break; break;
@ -594,9 +600,6 @@ void FlameCreationTask::validateZIPResouces()
case PackedResourceType::DataPack : case PackedResourceType::DataPack :
validatePath(fileName, targetFolder, "datapacks"); validatePath(fileName, targetFolder, "datapacks");
break; break;
case PackedResourceType::Mod :
validatePath(fileName, targetFolder, "mods");
break;
case PackedResourceType::ShaderPack : case PackedResourceType::ShaderPack :
// in theroy flame API can't do this but who knows, that *may* change ? // in theroy flame API can't do this but who knows, that *may* change ?
// better to handle it if it *does* occure in the future // better to handle it if it *does* occure in the future

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

@ -76,13 +76,8 @@ bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked
// It is also optional // It is also optional
type = File::Type::SingleFile; type = File::Type::SingleFile;
if (fileName.endsWith(".zip")) { targetFolder = "mods";
// this is probably a resource pack
targetFolder = "resourcepacks";
} else {
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods";
}
// get the hash // get the hash
hash = QString(); hash = QString();
auto hashes = Json::ensureArray(obj, "hashes"); auto hashes = Json::ensureArray(obj, "hashes");

View File

@ -37,16 +37,16 @@
#include <QtConcurrent> #include <QtConcurrent>
#include "MMCZip.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "settings/INISettingsObject.h" #include "MMCZip.h"
#include "minecraft/GradleSpecifier.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "minecraft/GradleSpecifier.h" #include "settings/INISettingsObject.h"
#include "BuildConfig.h"
#include "Application.h" #include "Application.h"
#include "BuildConfig.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
@ -67,6 +67,7 @@ void PackInstallTask::executeTask()
void PackInstallTask::downloadPack() void PackInstallTask::downloadPack()
{ {
setStatus(tr("Downloading zip for %1").arg(m_pack.name)); setStatus(tr("Downloading zip for %1").arg(m_pack.name));
setProgress(1, 4);
setAbortable(false); setAbortable(false);
archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
@ -80,11 +81,10 @@ void PackInstallTask::downloadPack()
} }
netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath)); netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath));
connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress); connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propogateStepProgress);
connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
netJobContainer->start(); netJobContainer->start();
@ -92,27 +92,6 @@ void PackInstallTask::downloadPack()
progress(1, 4); progress(1, 4);
} }
void PackInstallTask::onDownloadSucceeded()
{
unzip();
}
void PackInstallTask::onDownloadFailed(QString reason)
{
emitFailed(reason);
}
void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
{
progress(current, total * 4);
setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
}
void PackInstallTask::onDownloadAborted()
{
emitAborted();
}
void PackInstallTask::unzip() void PackInstallTask::unzip()
{ {
setStatus(tr("Extracting modpack")); setStatus(tr("Extracting modpack"));
@ -122,16 +101,17 @@ void PackInstallTask::unzip()
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
m_packZip.reset(new QuaZip(archivePath)); m_packZip.reset(new QuaZip(archivePath));
if(!m_packZip->open(QuaZip::mdUnzip)) if (!m_packZip->open(QuaZip::mdUnzip)) {
{
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
return; return;
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
extractDir.absolutePath() + "/unzip");
#else #else
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); m_extractFuture =
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
#endif #endif
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled); connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
@ -153,11 +133,9 @@ void PackInstallTask::install()
setStatus(tr("Installing modpack")); setStatus(tr("Installing modpack"));
progress(3, 4); progress(3, 4);
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
if(unzipMcDir.exists()) if (unzipMcDir.exists()) {
{ // ok, found minecraft dir, move contents to instance dir
//ok, found minecraft dir, move contents to instance dir if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
{
emitFailed(tr("Failed to move unzipped Minecraft!")); emitFailed(tr("Failed to move unzipped Minecraft!"));
return; return;
} }
@ -174,23 +152,20 @@ void PackInstallTask::install()
bool fallback = true; bool fallback = true;
//handle different versions // handle different versions
QFile packJson(m_stagingPath + "/.minecraft/pack.json"); QFile packJson(m_stagingPath + "/.minecraft/pack.json");
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
if(packJson.exists()) if (packJson.exists()) {
{
packJson.open(QIODevice::ReadOnly | QIODevice::Text); packJson.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
packJson.close(); packJson.close();
//we only care about the libs // we only care about the libs
QJsonArray libs = doc.object().value("libraries").toArray(); QJsonArray libs = doc.object().value("libraries").toArray();
foreach (const QJsonValue &value, libs) foreach (const QJsonValue& value, libs) {
{
QString nameValue = value.toObject().value("name").toString(); QString nameValue = value.toObject().value("name").toString();
if(!nameValue.startsWith("net.minecraftforge")) if (!nameValue.startsWith("net.minecraftforge")) {
{
continue; continue;
} }
@ -201,16 +176,13 @@ void PackInstallTask::install()
fallback = false; fallback = false;
break; break;
} }
} }
if(jarmodDir.exists()) if (jarmodDir.exists()) {
{
qDebug() << "Found jarmods, installing..."; qDebug() << "Found jarmods, installing...";
QStringList jarmods; QStringList jarmods;
for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
{
qDebug() << "Jarmod:" << info.fileName(); qDebug() << "Jarmod:" << info.fileName();
jarmods.push_back(info.absoluteFilePath()); jarmods.push_back(info.absoluteFilePath());
} }
@ -219,12 +191,11 @@ void PackInstallTask::install()
fallback = false; fallback = false;
} }
//just nuke unzip directory, it s not needed anymore // just nuke unzip directory, it s not needed anymore
FS::deletePath(m_stagingPath + "/unzip"); FS::deletePath(m_stagingPath + "/unzip");
if(fallback) if (fallback) {
{ // TODO: Some fallback mechanism... or just keep failing!
//TODO: Some fallback mechanism... or just keep failing!
emitFailed(tr("No installation method found!")); emitFailed(tr("No installation method found!"));
return; return;
} }
@ -234,8 +205,7 @@ void PackInstallTask::install()
progress(4, 4); progress(4, 4);
instance.setName(name()); instance.setName(name());
if(m_instIcon == "default") if (m_instIcon == "default") {
{
m_instIcon = "ftb_logo"; m_instIcon = "ftb_logo";
} }
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
@ -254,4 +224,4 @@ bool PackInstallTask::abort()
return InstanceTask::abort(); return InstanceTask::abort();
} }
} } // namespace LegacyFTB

View File

@ -1,12 +1,12 @@
#pragma once #pragma once
#include "InstanceTask.h"
#include "net/NetJob.h"
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipdir.h> #include <quazip/quazipdir.h>
#include "InstanceTask.h"
#include "PackHelpers.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/Version.h" #include "meta/Version.h"
#include "meta/VersionList.h" #include "meta/VersionList.h"
#include "PackHelpers.h" #include "net/NetJob.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -14,36 +14,31 @@
namespace LegacyFTB { namespace LegacyFTB {
class PackInstallTask : public InstanceTask class PackInstallTask : public InstanceTask {
{
Q_OBJECT Q_OBJECT
public: public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version); explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version);
virtual ~PackInstallTask(){} virtual ~PackInstallTask() {}
bool canAbort() const override { return true; } bool canAbort() const override { return true; }
bool abort() override; bool abort() override;
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
virtual void executeTask() override; virtual void executeTask() override;
private: private:
void downloadPack(); void downloadPack();
void unzip(); void unzip();
void install(); void install();
private slots: private slots:
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
void onDownloadProgress(qint64 current, qint64 total);
void onDownloadAborted();
void onUnzipFinished(); void onUnzipFinished();
void onUnzipCanceled(); void onUnzipCanceled();
private: /* data */ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false; bool abortable = false;
std::unique_ptr<QuaZip> m_packZip; std::unique_ptr<QuaZip> m_packZip;
@ -56,4 +51,4 @@ private: /* data */
QString m_version; QString m_version;
}; };
} } // namespace LegacyFTB

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

@ -42,6 +42,7 @@
#include <QDir> #include <QDir>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QDebug> #include <QDebug>
#include <locale>
#include "FileSystem.h" #include "FileSystem.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -454,6 +455,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:
{ {
@ -526,34 +528,34 @@ Language * TranslationsModel::findLanguage(const QString& key)
} }
} }
void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
{
APPLICATION->settings()->set("UseSystemLocale", useSystemLocale);
QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode));
}
bool TranslationsModel::selectLanguage(QString key) bool TranslationsModel::selectLanguage(QString key)
{ {
QString &langCode = key; QString& langCode = key;
auto langPtr = findLanguage(key); auto langPtr = findLanguage(key);
if (langCode.isEmpty()) if (langCode.isEmpty()) {
{
d->no_language_set = true; d->no_language_set = true;
} }
if(!langPtr) if (!langPtr) {
{
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode; langCode = defaultLangCode;
} } else {
else
{
langCode = langPtr->key; langCode = langPtr->key;
} }
// uninstall existing translators if there are any // uninstall existing translators if there are any
if (d->m_app_translator) if (d->m_app_translator) {
{
QCoreApplication::removeTranslator(d->m_app_translator.get()); QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset(); d->m_app_translator.reset();
} }
if (d->m_qt_translator) if (d->m_qt_translator) {
{
QCoreApplication::removeTranslator(d->m_qt_translator.get()); QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset(); d->m_qt_translator.reset();
} }
@ -563,8 +565,9 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant. * This function is not reentrant.
*/ */
QLocale locale = QLocale(langCode); QLocale::setDefault(
QLocale::setDefault(locale); QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode));
// if it's the default UI language, finish // if it's the default UI language, finish
if(langCode == defaultLangCode) if(langCode == defaultLangCode)

View File

@ -20,17 +20,16 @@
struct Language; struct Language;
class TranslationsModel : public QAbstractListModel class TranslationsModel : public QAbstractListModel {
{
Q_OBJECT Q_OBJECT
public: public:
explicit TranslationsModel(QString path, QObject *parent = 0); explicit TranslationsModel(QString path, QObject* parent = 0);
virtual ~TranslationsModel(); virtual ~TranslationsModel();
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex & parent) const override; int columnCount(const QModelIndex& parent) const override;
bool selectLanguage(QString key); bool selectLanguage(QString key);
void updateLanguage(QString key); void updateLanguage(QString key);
@ -38,27 +37,27 @@ public:
QString selectedLanguage(); QString selectedLanguage();
void downloadIndex(); void downloadIndex();
void setUseSystemLocale(bool useSystemLocale);
private: private:
Language *findLanguage(const QString & key); Language* findLanguage(const QString& key);
void reloadLocalFiles(); void reloadLocalFiles();
void downloadTranslation(QString key); void downloadTranslation(QString key);
void downloadNext(); void downloadNext();
// hide copy constructor // hide copy constructor
TranslationsModel(const TranslationsModel &) = delete; TranslationsModel(const TranslationsModel&) = delete;
// hide assign op // hide assign op
TranslationsModel &operator=(const TranslationsModel &) = delete; TranslationsModel& operator=(const TranslationsModel&) = delete;
private slots: private slots:
void indexReceived(); void indexReceived();
void indexFailed(QString reason); void indexFailed(QString reason);
void dlFailed(QString reason); void dlFailed(QString reason);
void dlGood(); void dlGood();
void translationDirChanged(const QString &path); void translationDirChanged(const QString& path);
private: /* data */
private: /* data */
struct Private; struct Private;
std::unique_ptr<Private> d; std::unique_ptr<Private> d;
}; };

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)
@ -1292,7 +1280,17 @@ void MainWindow::globalSettingsClosed()
void MainWindow::on_actionEditInstance_triggered() void MainWindow::on_actionEditInstance_triggered()
{ {
APPLICATION->showInstanceWindow(m_selectedInstance);
if (!m_selectedInstance)
return;
if (m_selectedInstance->canEdit()) {
APPLICATION->showInstanceWindow(m_selectedInstance);
} else {
CustomMessageBox::selectable(this, tr("Instance not editable"),
tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."),
QMessageBox::Critical)->show();
}
} }
void MainWindow::on_actionManageAccounts_triggered() void MainWindow::on_actionManageAccounts_triggered()
@ -1418,11 +1416,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 +1546,39 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
QString iconPath; QString iconPath;
QStringList args; QStringList args;
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
if (appPath.startsWith("/private/var/")) { appPath = QApplication::applicationFilePath();
QMessageBox::critical(this, tr("Create instance shortcut"), if (appPath.startsWith("/private/var/")) {
tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); QMessageBox::critical(this, tr("Create instance shortcut"),
return; tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
} 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 +1661,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)) {
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); #if not defined(Q_OS_MACOS)
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

@ -35,24 +35,26 @@
*/ */
#include "ExportInstanceDialog.h" #include "ExportInstanceDialog.h"
#include "ui_ExportInstanceDialog.h"
#include <BaseInstance.h> #include <BaseInstance.h>
#include <MMCZip.h> #include <MMCZip.h>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox>
#include <QFileSystemModel> #include <QFileSystemModel>
#include <QMessageBox>
#include "FileIgnoreProxy.h"
#include "ui_ExportInstanceDialog.h"
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QSaveFile>
#include <QStack>
#include <QFileInfo>
#include "SeparatorPrefixTree.h"
#include "Application.h"
#include <icons/IconList.h>
#include <FileSystem.h> #include <FileSystem.h>
#include <icons/IconList.h>
#include <QDebug>
#include <QFileInfo>
#include <QSaveFile>
#include <QSortFilterProxyModel>
#include <QStack>
#include <functional>
#include "Application.h"
#include "SeparatorPrefixTree.h"
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
{ {
ui->setupUi(this); ui->setupUi(this);
@ -60,8 +62,12 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent
model->setIconProvider(&icons); model->setIconProvider(&icons);
auto root = instance->instanceRoot(); auto root = instance->instanceRoot();
proxyModel = new FileIgnoreProxy(root, this); proxyModel = new FileIgnoreProxy(root, this);
loadPackIgnore();
proxyModel->setSourceModel(model); proxyModel->setSourceModel(model);
auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot());
proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") });
proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
loadPackIgnore();
ui->treeView->setModel(proxyModel); ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder); ui->treeView->sortByColumn(0, Qt::AscendingOrder);
@ -133,11 +139,9 @@ bool ExportInstanceDialog::doExport()
SaveIcon(m_instance); SaveIcon(m_instance);
auto & blocked = proxyModel->blockedPaths();
using std::placeholders::_1;
auto files = QFileInfoList(); auto files = QFileInfoList();
if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files,
std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) {
QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
return false; return false;
} }

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());
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
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();
@ -52,8 +61,9 @@ ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
// use the game root - everything outside cannot be exported // use the game root - everything outside cannot be exported
const QDir root(instance->gameRoot()); const QDir root(instance->gameRoot());
proxy = new FileIgnoreProxy(instance->gameRoot(), this); proxy = new FileIgnoreProxy(instance->gameRoot(), this);
proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports" });
proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" });
proxy->setSourceModel(model); proxy->setSourceModel(model);
proxy->setFilterRegularExpression("^(?!(\\.DS_Store)|([tT]humbs\\.db)).+$");
const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
@ -65,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()));
@ -82,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;
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));
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, connect(task, &Task::failed,
[this](const QString& path) { return proxy->blockedPaths().covers(path); });
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); QSize newSize = this->size();
// if the current window is a different size
// keep the dialog in the center after a resize auto parent = this->parentWidget();
this->move( if (recenterParent && parent) {
this->parentWidget()->x() + (this->parentWidget()->width() - this->width()) / 2, auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2));
this->parentWidget()->y() + (this->parentWidget()->height() - this->height()) / 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 {
@ -281,8 +283,11 @@ QList<BasePage*> ModDownloadDialog::getPages()
{ {
QList<BasePage*> pages; QList<BasePage*> pages;
pages.append(ModrinthModPage::create(this, *m_instance)); auto loaders = static_cast<MinecraftInstance*>(m_instance)->getPackProfile()->getModLoaders().value();
if (APPLICATION->capabilities() & Application::SupportsFlame)
if (ModrinthAPI::validateModLoaders(loaders))
pages.append(ModrinthModPage::create(this, *m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders))
pages.append(FlameModPage::create(this, *m_instance)); pages.append(FlameModPage::create(this, *m_instance));
m_selectedPage = dynamic_cast<ModPage*>(pages[0]); m_selectedPage = dynamic_cast<ModPage*>(pages[0]);

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

@ -71,7 +71,6 @@ class NoBigComboBoxStyle : public QProxyStyle {
private: private:
NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {}
}; };
ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent)
@ -93,13 +92,13 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi
// NOTE: GTK2 themes crash with the proxy style. // NOTE: GTK2 themes crash with the proxy style.
// This seems like an upstream bug, so there's not much else that can be done. // This seems like an upstream bug, so there's not much else that can be done.
if (!QStyleFactory::keys().contains("gtk2")){ if (!QStyleFactory::keys().contains("gtk2")) {
auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style()); auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style());
ui->versionsComboBox->setStyle(comboStyle); ui->versionsComboBox->setStyle(comboStyle);
} }
ui->reloadButton->setVisible(false); ui->reloadButton->setVisible(false);
connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) {
ui->reloadButton->setVisible(false); ui->reloadButton->setVisible(false);
m_loaded = false; m_loaded = false;
@ -228,7 +227,8 @@ void ModrinthManagedPackPage::parseManagedPack()
QString id = m_inst->getManagedPackID(); QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); m_fetch_job->addNetAction(
Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
QJsonParseError parse_error{}; QJsonParseError parse_error{};
@ -269,7 +269,6 @@ void ModrinthManagedPackPage::parseManagedPack()
if (version.version == m_inst->getManagedPackVersionName()) if (version.version == m_inst->getManagedPackVersionName())
name = tr("%1 (Current)").arg(name); name = tr("%1 (Current)").arg(name);
ui->versionsComboBox->addItem(name, QVariant(version.id)); ui->versionsComboBox->addItem(name, QVariant(version.id));
} }
@ -293,6 +292,10 @@ QString ModrinthManagedPackPage::url() const
void ModrinthManagedPackPage::suggestVersion() void ModrinthManagedPackPage::suggestVersion()
{ {
auto index = ui->versionsComboBox->currentIndex(); auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8()));
@ -303,6 +306,10 @@ void ModrinthManagedPackPage::suggestVersion()
void ModrinthManagedPackPage::update() void ModrinthManagedPackPage::update()
{ {
auto index = ui->versionsComboBox->currentIndex(); auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info; QMap<QString, QString> extra_info;
@ -431,6 +438,10 @@ QString FlameManagedPackPage::url() const
void FlameManagedPackPage::suggestVersion() void FlameManagedPackPage::suggestVersion()
{ {
auto index = ui->versionsComboBox->currentIndex(); auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId));
@ -441,6 +452,10 @@ void FlameManagedPackPage::suggestVersion()
void FlameManagedPackPage::update() void FlameManagedPackPage::update()
{ {
auto index = ui->versionsComboBox->currentIndex(); auto index = ui->versionsComboBox->currentIndex();
if (m_pack.versions.length() == 0) {
setFailState();
return;
}
auto version = m_pack.versions.at(index); auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info; QMap<QString, QString> extra_info;

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,37 +97,30 @@ public:
return; return;
if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
return; return;
int tries = 5; if (!m_cache->stale(m_path))
while (tries) return;
{ QImage image(m_path);
if (!m_cache->stale(m_path)) if (image.isNull()) {
return; m_resultEmitter.emitResultsFailed(m_path);
QImage image(m_path); qDebug() << "Error loading screenshot: " + m_path + ". Perhaps too large?";
if (image.isNull())
{
QThread::msleep(500);
tries--;
continue;
}
QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(m_path);
return; return;
} }
m_resultEmitter.emitResultsFailed(m_path); QImage small;
if (image.width() > image.height())
small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
else
small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
QImage square(QSize(256, 256), QImage::Format_ARGB32);
square.fill(Qt::transparent);
QPainter painter(&square);
painter.drawImage(offset, small);
painter.end();
QIcon icon(QPixmap::fromImage(square));
m_cache->add(m_path, icon);
m_resultEmitter.emitResultsReady(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);
watcher.addPath(filepath); if (QFile::exists(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>
@ -55,49 +54,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");
} }
} }
@ -105,14 +97,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
@ -144,15 +133,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);
@ -182,10 +170,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);
} }
@ -202,18 +188,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;
@ -226,16 +210,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;
@ -244,71 +224,47 @@ 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()
{ {
// FIXME: this is a dirty hack // FIXME: this is a dirty hack
auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
ui->actionInstall_Forge->setEnabled(controlsEnabled);
bool supportsFabric = minecraftVersion >= Version("1.14"); bool supportsFabric = minecraftVersion >= Version("1.14");
ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); ui->actionInstall_Fabric->setEnabled(supportsFabric);
bool supportsQuilt = minecraftVersion >= Version("1.14"); bool supportsQuilt = minecraftVersion >= Version("1.14");
ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt); ui->actionInstall_Quilt->setEnabled(supportsQuilt);
bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); bool supportsLiteLoader = minecraftVersion <= Version("1.12.2");
ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); ui->actionInstall_LiteLoader->setEnabled(supportsLiteLoader);
updateButtons(); updateButtons();
} }
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->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
ui->actionAdd_to_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;
} }
} }
@ -321,14 +277,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"
@ -341,8 +295,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();
@ -352,17 +305,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();
@ -370,9 +322,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();
@ -406,12 +358,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();
@ -419,12 +368,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();
@ -433,39 +379,32 @@ 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();
// FIXME: this is a horrible HACK. Get version filtering information from the actual metadata... // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata...
if(uid == "net.minecraftforge") if (uid == "net.minecraftforge") {
{
on_actionInstall_Forge_triggered(); on_actionInstall_Forge_triggered();
return; return;
} } else if (uid == "com.mumfrey.liteloader") {
else if (uid == "com.mumfrey.liteloader")
{
on_actionInstall_LiteLoader_triggered(); on_actionInstall_LiteLoader_triggered();
return; return;
} }
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.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); vselect.setExactFilter(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())
@ -473,8 +412,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);
@ -484,19 +422,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( tr("Cannot download Minecraft or update instances unless you have at least "
this, tr("Error"), "one account added.\nPlease add your Mojang or Minecraft account."),
tr("Cannot download Minecraft or update instances unless you have at least " QMessageBox::Warning)
"one account added.\nPlease add your Mojang or Minecraft account."), ->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);
@ -510,28 +446,26 @@ void VersionPage::on_actionDownload_All_triggered()
void VersionPage::on_actionInstall_Forge_triggered() void VersionPage::on_actionInstall_Forge_triggered()
{ {
auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge");
if(!vlist) if (!vlist) {
{
return; return;
} }
VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") +
m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.minecraftforge"); auto currentVersion = m_profile->getComponentVersion("net.minecraftforge");
if(!currentVersion.isEmpty()) if (!currentVersion.isEmpty()) {
{
vselect.setCurrentVersion(currentVersion); vselect.setCurrentVersion(currentVersion);
} }
if (vselect.exec() && vselect.selectedVersion()) if (vselect.exec() && vselect.selectedVersion()) {
{
auto vsn = vselect.selectedVersion(); auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor());
m_profile->resolve(Net::Mode::Online); m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion(); // m_profile->installVersion();
preselect(m_profile->rowCount(QModelIndex())-1); preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer(); m_container->refreshContainer();
} }
} }
@ -539,8 +473,7 @@ void VersionPage::on_actionInstall_Forge_triggered()
void VersionPage::on_actionInstall_Fabric_triggered() void VersionPage::on_actionInstall_Fabric_triggered()
{ {
auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader"); auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader");
if(!vlist) if (!vlist) {
{
return; return;
} }
VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this); VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this);
@ -548,17 +481,15 @@ void VersionPage::on_actionInstall_Fabric_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!")); vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader"); auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader");
if(!currentVersion.isEmpty()) if (!currentVersion.isEmpty()) {
{
vselect.setCurrentVersion(currentVersion); vselect.setCurrentVersion(currentVersion);
} }
if (vselect.exec() && vselect.selectedVersion()) if (vselect.exec() && vselect.selectedVersion()) {
{
auto vsn = vselect.selectedVersion(); auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor()); m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online); m_profile->resolve(Net::Mode::Online);
preselect(m_profile->rowCount(QModelIndex())-1); preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer(); m_container->refreshContainer();
} }
} }
@ -566,8 +497,7 @@ void VersionPage::on_actionInstall_Fabric_triggered()
void VersionPage::on_actionInstall_Quilt_triggered() void VersionPage::on_actionInstall_Quilt_triggered()
{ {
auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader");
if(!vlist) if (!vlist) {
{
return; return;
} }
VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this);
@ -575,17 +505,15 @@ void VersionPage::on_actionInstall_Quilt_triggered()
vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!"));
auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader");
if(!currentVersion.isEmpty()) if (!currentVersion.isEmpty()) {
{
vselect.setCurrentVersion(currentVersion); vselect.setCurrentVersion(currentVersion);
} }
if (vselect.exec() && vselect.selectedVersion()) if (vselect.exec() && vselect.selectedVersion()) {
{
auto vsn = vselect.selectedVersion(); auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online); m_profile->resolve(Net::Mode::Online);
preselect(m_profile->rowCount(QModelIndex())-1); preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer(); m_container->refreshContainer();
} }
} }
@ -594,14 +522,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());
@ -611,28 +537,26 @@ void VersionPage::on_actionAdd_Empty_triggered()
void VersionPage::on_actionInstall_LiteLoader_triggered() void VersionPage::on_actionInstall_LiteLoader_triggered()
{ {
auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader");
if(!vlist) if (!vlist) {
{
return; return;
} }
VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this);
vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") +
m_profile->getComponentVersion("net.minecraft"));
vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!"));
auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader"); auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader");
if(!currentVersion.isEmpty()) if (!currentVersion.isEmpty()) {
{
vselect.setCurrentVersion(currentVersion); vselect.setCurrentVersion(currentVersion);
} }
if (vselect.exec() && vselect.selectedVersion()) if (vselect.exec() && vselect.selectedVersion()) {
{
auto vsn = vselect.selectedVersion(); auto vsn = vselect.selectedVersion();
m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor());
m_profile->resolve(Net::Mode::Online); m_profile->resolve(Net::Mode::Online);
// m_profile->installVersion(vselect.selectedVersion()); // m_profile->installVersion(vselect.selectedVersion());
preselect(m_profile->rowCount(QModelIndex())-1); preselect(m_profile->rowCount(QModelIndex()) - 1);
m_container->refreshContainer(); m_container->refreshContainer();
} }
} }
@ -647,7 +571,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);
@ -655,16 +579,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);
@ -680,8 +601,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);
@ -689,8 +609,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();
@ -699,18 +618,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();
@ -720,13 +636,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;
} }
@ -736,8 +650,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);
@ -753,8 +666,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();
@ -762,7 +674,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_Forge_triggered(); void on_actionInstall_Forge_triggered();
void on_actionInstall_Fabric_triggered(); void on_actionInstall_Fabric_triggered();
@ -103,36 +92,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,45 +75,43 @@ 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 = "";
if (!licenses.empty()) { if (!licenses.empty()) {
for (auto l : licenses) { for (auto l : licenses) {
if (!licenseText.isEmpty()) { if (!licenseText.isEmpty()) {
licenseText += "\n"; // add newline between licenses licenseText += "\n"; // add newline between licenses
} }
if (!l.name.isEmpty()) { if (!l.name.isEmpty()) {
if (l.url.isEmpty()) { if (l.url.isEmpty()) {
@ -109,9 +123,9 @@ void InfoFrame::updateWithMod(Mod const& m)
licenseText += "<a href=\"" + l.url + "\">" + l.url + "</a>"; licenseText += "<a href=\"" + l.url + "\">" + l.url + "</a>";
} }
if (!l.description.isEmpty() && l.description != l.name) { if (!l.description.isEmpty() && l.description != l.name) {
licenseText += " " + l.description; licenseText += " " + l.description;
} }
} }
} }
if (!licenseText.isEmpty()) { if (!licenseText.isEmpty()) {
setLicense(tr("License: %1").arg(licenseText)); setLicense(tr("License: %1").arg(licenseText));
@ -123,7 +137,7 @@ void InfoFrame::updateWithMod(Mod const& m)
if (!m.issueTracker().isEmpty()) { if (!m.issueTracker().isEmpty()) {
issueTracker += tr("Report issues to: "); issueTracker += tr("Report issues to: ");
issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>"; issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>";
} }
setIssueTracker(issueTracker); setIssueTracker(issueTracker);
} }
@ -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,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors // SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * This program is free software: you can redistribute it and/or modify
* you may not use this file except in compliance with the License. * it under the terms of the GNU General Public License as published by
* You may obtain a copy of the License at * the Free Software Foundation, version 3.
* *
* http://www.apache.org/licenses/LICENSE-2.0 * 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.
* *
* Unless required by applicable law or agreed to in writing, software * You should have received a copy of the GNU General Public License
* distributed under the License is distributed on an "AS IS" BASIS, * along with this program. If not, see <https://www.gnu.org/licenses/>.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and * This file incorporates work covered by the following copyright and
* limitations under the License. * permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
#pragma once #pragma once
@ -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

@ -1,16 +1,16 @@
#include "LanguageSelectionWidget.h" #include "LanguageSelectionWidget.h"
#include <QVBoxLayout> #include <QCheckBox>
#include <QTreeView>
#include <QHeaderView> #include <QHeaderView>
#include <QLabel> #include <QLabel>
#include <QTreeView>
#include <QVBoxLayout>
#include "Application.h" #include "Application.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "translations/TranslationsModel.h"
#include "settings/Setting.h" #include "settings/Setting.h"
#include "translations/TranslationsModel.h"
LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent) : QWidget(parent)
QWidget(parent)
{ {
verticalLayout = new QVBoxLayout(this); verticalLayout = new QVBoxLayout(this);
verticalLayout->setObjectName(QStringLiteral("verticalLayout")); verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
@ -31,6 +31,13 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
helpUsLabel->setWordWrap(true); helpUsLabel->setWordWrap(true);
verticalLayout->addWidget(helpUsLabel); verticalLayout->addWidget(helpUsLabel);
formatCheckbox = new QCheckBox(this);
formatCheckbox->setObjectName(QStringLiteral("formatCheckbox"));
formatCheckbox->setCheckState(APPLICATION->settings()->get("UseSystemLocale").toBool() ? Qt::Checked : Qt::Unchecked);
connect(formatCheckbox, &QCheckBox::stateChanged,
[this]() { APPLICATION->translations()->setUseSystemLocale(formatCheckbox->isChecked()); });
verticalLayout->addWidget(formatCheckbox);
auto translations = APPLICATION->translations(); auto translations = APPLICATION->translations();
auto index = translations->selectedIndex(); auto index = translations->selectedIndex();
languageView->setModel(translations.get()); languageView->setModel(translations.get());
@ -38,7 +45,7 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged); connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged);
verticalLayout->setContentsMargins(0,0,0,0); verticalLayout->setContentsMargins(0, 0, 0, 0);
auto language_setting = APPLICATION->settings()->getSetting("Language"); auto language_setting = APPLICATION->settings()->getSetting("Language");
connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged); connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged);
@ -53,15 +60,14 @@ QString LanguageSelectionWidget::getSelectedLanguageKey() const
void LanguageSelectionWidget::retranslate() void LanguageSelectionWidget::retranslate()
{ {
QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>") QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>")
.arg(BuildConfig.TRANSLATIONS_URL); .arg(BuildConfig.TRANSLATIONS_URL);
helpUsLabel->setText(text); helpUsLabel->setText(text);
formatCheckbox->setText(tr("Use system locales"));
} }
void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous) void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous)
{ {
if (current == previous) if (current == previous) {
{
return; return;
} }
auto translations = APPLICATION->translations(); auto translations = APPLICATION->translations();
@ -70,7 +76,7 @@ void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, con
translations->updateLanguage(key); translations->updateLanguage(key);
} }
void LanguageSelectionWidget::languageSettingChanged(const Setting &, const QVariant) void LanguageSelectionWidget::languageSettingChanged(const Setting&, const QVariant)
{ {
auto translations = APPLICATION->translations(); auto translations = APPLICATION->translations();
auto index = translations->selectedIndex(); auto index = translations->selectedIndex();

View File

@ -21,23 +21,24 @@ class QVBoxLayout;
class QTreeView; class QTreeView;
class QLabel; class QLabel;
class Setting; class Setting;
class QCheckBox;
class LanguageSelectionWidget: public QWidget class LanguageSelectionWidget : public QWidget {
{
Q_OBJECT Q_OBJECT
public: public:
explicit LanguageSelectionWidget(QWidget *parent = 0); explicit LanguageSelectionWidget(QWidget* parent = 0);
virtual ~LanguageSelectionWidget() { }; virtual ~LanguageSelectionWidget(){};
QString getSelectedLanguageKey() const; QString getSelectedLanguageKey() const;
void retranslate(); void retranslate();
protected slots: protected slots:
void languageRowChanged(const QModelIndex &current, const QModelIndex &previous); void languageRowChanged(const QModelIndex& current, const QModelIndex& previous);
void languageSettingChanged(const Setting &, const QVariant); void languageSettingChanged(const Setting&, const QVariant);
private: private:
QVBoxLayout *verticalLayout = nullptr; QVBoxLayout* verticalLayout = nullptr;
QTreeView *languageView = nullptr; QTreeView* languageView = nullptr;
QLabel *helpUsLabel = nullptr; QLabel* helpUsLabel = nullptr;
QCheckBox* formatCheckbox = nullptr;
}; };

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;

View File

@ -42,6 +42,10 @@ class LinkTask : public Task {
m_lnk->debug(true); m_lnk->debug(true);
} }
~LinkTask() {
delete m_lnk;
}
void matcher(const IPathMatcher *filter) void matcher(const IPathMatcher *filter)
{ {
m_lnk->matcher(filter); m_lnk->matcher(filter);
@ -219,7 +223,8 @@ slots:
qDebug() << tempDir.path(); qDebug() << tempDir.path();
qDebug() << target_dir.path(); qDebug() << target_dir.path();
FS::copy c(folder, target_dir.path()); FS::copy c(folder, target_dir.path());
c.matcher(new RegexpMatcher("[.]?mcmeta")); RegexpMatcher re("[.]?mcmeta");
c.matcher(&re);
c(); c();
for(auto entry: target_dir.entryList()) for(auto entry: target_dir.entryList())
@ -253,7 +258,8 @@ slots:
qDebug() << tempDir.path(); qDebug() << tempDir.path();
qDebug() << target_dir.path(); qDebug() << target_dir.path();
FS::copy c(folder, target_dir.path()); FS::copy c(folder, target_dir.path());
c.matcher(new RegexpMatcher("[.]?mcmeta")); RegexpMatcher re("[.]?mcmeta");
c.matcher(&re);
c.whitelist(true); c.whitelist(true);
c(); c();
@ -460,7 +466,8 @@ slots:
qDebug() << target_dir.path(); qDebug() << target_dir.path();
LinkTask lnk_tsk(folder, target_dir.path()); LinkTask lnk_tsk(folder, target_dir.path());
lnk_tsk.matcher(new RegexpMatcher("[.]?mcmeta")); RegexpMatcher re("[.]?mcmeta");
lnk_tsk.matcher(&re);
lnk_tsk.linkRecursively(true); lnk_tsk.linkRecursively(true);
QObject::connect(&lnk_tsk, &Task::finished, [&]{ QObject::connect(&lnk_tsk, &Task::finished, [&]{
QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been.");
@ -511,7 +518,8 @@ slots:
qDebug() << target_dir.path(); qDebug() << target_dir.path();
LinkTask lnk_tsk(folder, target_dir.path()); LinkTask lnk_tsk(folder, target_dir.path());
lnk_tsk.matcher(new RegexpMatcher("[.]?mcmeta")); RegexpMatcher re("[.]?mcmeta");
lnk_tsk.matcher(&re);
lnk_tsk.linkRecursively(true); lnk_tsk.linkRecursively(true);
lnk_tsk.whitelist(true); lnk_tsk.whitelist(true);
QObject::connect(&lnk_tsk, &Task::finished, [&]{ QObject::connect(&lnk_tsk, &Task::finished, [&]{

View File

@ -38,6 +38,7 @@ class DummyResourceModel : public ResourceModel {
public: public:
DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} DummyResourceModel() : ResourceModel(new DummyResourceAPI) {}
~DummyResourceModel() {}
[[nodiscard]] auto metaEntryBase() const -> QString override { return ""; }; [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; };
@ -58,7 +59,10 @@ class DummyResourceModel : public ResourceModel {
class ResourceModelTest : public QObject { class ResourceModelTest : public QObject {
Q_OBJECT Q_OBJECT
private slots: private slots:
void test_abstract_item_model() { [[maybe_unused]] auto tester = new QAbstractItemModelTester(new DummyResourceModel); } void test_abstract_item_model() {
auto dummy = DummyResourceModel();
auto tester = QAbstractItemModelTester(&dummy);
}
void test_search() void test_search()
{ {
@ -78,6 +82,8 @@ class ResourceModelTest : public QObject {
QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id")); QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id"));
QVERIFY(processed_pack->description == Json::requireString(processed_response, "description")); QVERIFY(processed_pack->description == Json::requireString(processed_response, "description"));
QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author")); QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author"));
delete model;
} }
}; };

View File

@ -1,6 +1,6 @@
#include <QTest> #include <QTest>
#include <QTimer>
#include <QThread> #include <QThread>
#include <QTimer>
#include <tasks/ConcurrentTask.h> #include <tasks/ConcurrentTask.h>
#include <tasks/MultipleOptionsTask.h> #include <tasks/MultipleOptionsTask.h>
@ -19,10 +19,7 @@ class BasicTask : public Task {
BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {}
private: private:
void executeTask() override void executeTask() override { emitSucceeded(); };
{
emitSucceeded();
};
}; };
/* Does nothing. Only used for testing. */ /* Does nothing. Only used for testing. */
@ -34,7 +31,7 @@ class BasicTask_MultiStep : public Task {
private: private:
auto isMultiStep() const -> bool override { return true; } auto isMultiStep() const -> bool override { return true; }
void executeTask() override {}; void executeTask() override{};
}; };
class BigConcurrentTask : public ConcurrentTask { class BigConcurrentTask : public ConcurrentTask {
@ -44,7 +41,7 @@ class BigConcurrentTask : public ConcurrentTask {
{ {
// This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^))
// Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least.
[[maybe_unused]] volatile std::array<uint32_t, 1024> some_data_on_the_stack {}; [[maybe_unused]] volatile std::array<uint32_t, 1024> some_data_on_the_stack{};
ConcurrentTask::startNext(); ConcurrentTask::startNext();
} }
@ -53,49 +50,42 @@ class BigConcurrentTask : public ConcurrentTask {
class BigConcurrentTaskThread : public QThread { class BigConcurrentTaskThread : public QThread {
Q_OBJECT Q_OBJECT
BigConcurrentTask big_task; QTimer m_deadline;
void run() override void run() override
{ {
QTimer deadline; BigConcurrentTask big_task;
deadline.setInterval(10000); m_deadline.setInterval(10000);
connect(&deadline, &QTimer::timeout, this, [this]{ passed_the_deadline = true; });
deadline.start();
// NOTE: Arbitrary value that manages to trigger a problem when there is one. // NOTE: Arbitrary value that manages to trigger a problem when there is one.
// Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack, // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack,
// this number is enough to fill up 16 MiB of stack, more than enough to cause a problem. // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem.
static const unsigned s_num_tasks = 1 << 12; static const unsigned s_num_tasks = 1 << 12;
auto sub_tasks = new BasicTask::Ptr[s_num_tasks];
for (unsigned i = 0; i < s_num_tasks; i++) { for (unsigned i = 0; i < s_num_tasks; i++) {
auto sub_task = makeShared<BasicTask>(false); auto sub_task = makeShared<BasicTask>(false);
sub_tasks[i] = sub_task;
big_task.addTask(sub_task); big_task.addTask(sub_task);
} }
connect(&big_task, &Task::finished, this, &QThread::quit);
connect(&m_deadline, &QTimer::timeout, this, [&] { passed_the_deadline = true; quit(); });
m_deadline.start();
big_task.run(); big_task.run();
while (!big_task.isFinished() && !passed_the_deadline) exec();
QCoreApplication::processEvents();
emit finished();
} }
public: public:
bool passed_the_deadline = false; bool passed_the_deadline = false;
signals:
void finished();
}; };
class TaskTest : public QObject { class TaskTest : public QObject {
Q_OBJECT Q_OBJECT
private slots: private slots:
void test_SetStatus_NoMultiStep(){ void test_SetStatus_NoMultiStep()
{
BasicTask t; BasicTask t;
QString status {"test status"}; QString status{ "test status" };
t.setStatus(status); t.setStatus(status);
@ -103,9 +93,10 @@ class TaskTest : public QObject {
QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty());
} }
void test_SetStatus_MultiStep(){ void test_SetStatus_MultiStep()
{
BasicTask_MultiStep t; BasicTask_MultiStep t;
QString status {"test status"}; QString status{ "test status" };
t.setStatus(status); t.setStatus(status);
@ -115,7 +106,8 @@ class TaskTest : public QObject {
QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty());
} }
void test_SetProgress(){ void test_SetProgress()
{
BasicTask t; BasicTask t;
int current = 42; int current = 42;
int total = 207; int total = 207;
@ -126,17 +118,18 @@ class TaskTest : public QObject {
QCOMPARE(t.getTotalProgress(), total); QCOMPARE(t.getTotalProgress(), total);
} }
void test_basicRun(){ void test_basicRun()
{
BasicTask t; BasicTask t;
QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); QObject::connect(&t, &Task::finished,
[&] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); });
t.start(); t.start();
QVERIFY2(QTest::qWaitFor([&]() { QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should.");
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
} }
void test_basicConcurrentRun(){ void test_basicConcurrentRun()
{
auto t1 = makeShared<BasicTask>(); auto t1 = makeShared<BasicTask>();
auto t2 = makeShared<BasicTask>(); auto t2 = makeShared<BasicTask>();
auto t3 = makeShared<BasicTask>(); auto t3 = makeShared<BasicTask>();
@ -147,21 +140,20 @@ class TaskTest : public QObject {
t.addTask(t2); t.addTask(t2);
t.addTask(t3); t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{ QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] {
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
QVERIFY(t1->wasSuccessful()); QVERIFY(t1->wasSuccessful());
QVERIFY(t2->wasSuccessful()); QVERIFY(t2->wasSuccessful());
QVERIFY(t3->wasSuccessful()); QVERIFY(t3->wasSuccessful());
}); });
t.start(); t.start();
QVERIFY2(QTest::qWaitFor([&]() { QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should.");
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
} }
// Tests if starting new tasks after the 6 initial ones is working // Tests if starting new tasks after the 6 initial ones is working
void test_moreConcurrentRun(){ void test_moreConcurrentRun()
{
auto t1 = makeShared<BasicTask>(); auto t1 = makeShared<BasicTask>();
auto t2 = makeShared<BasicTask>(); auto t2 = makeShared<BasicTask>();
auto t3 = makeShared<BasicTask>(); auto t3 = makeShared<BasicTask>();
@ -184,26 +176,25 @@ class TaskTest : public QObject {
t.addTask(t8); t.addTask(t8);
t.addTask(t9); t.addTask(t9);
QObject::connect(&t, &Task::finished, [&]{ QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] {
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
QVERIFY(t1->wasSuccessful()); QVERIFY(t1->wasSuccessful());
QVERIFY(t2->wasSuccessful()); QVERIFY(t2->wasSuccessful());
QVERIFY(t3->wasSuccessful()); QVERIFY(t3->wasSuccessful());
QVERIFY(t4->wasSuccessful()); QVERIFY(t4->wasSuccessful());
QVERIFY(t5->wasSuccessful()); QVERIFY(t5->wasSuccessful());
QVERIFY(t6->wasSuccessful()); QVERIFY(t6->wasSuccessful());
QVERIFY(t7->wasSuccessful()); QVERIFY(t7->wasSuccessful());
QVERIFY(t8->wasSuccessful()); QVERIFY(t8->wasSuccessful());
QVERIFY(t9->wasSuccessful()); QVERIFY(t9->wasSuccessful());
}); });
t.start(); t.start();
QVERIFY2(QTest::qWaitFor([&]() { QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should.");
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
} }
void test_basicSequentialRun(){ void test_basicSequentialRun()
{
auto t1 = makeShared<BasicTask>(); auto t1 = makeShared<BasicTask>();
auto t2 = makeShared<BasicTask>(); auto t2 = makeShared<BasicTask>();
auto t3 = makeShared<BasicTask>(); auto t3 = makeShared<BasicTask>();
@ -214,20 +205,19 @@ class TaskTest : public QObject {
t.addTask(t2); t.addTask(t2);
t.addTask(t3); t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{ QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] {
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
QVERIFY(t1->wasSuccessful()); QVERIFY(t1->wasSuccessful());
QVERIFY(t2->wasSuccessful()); QVERIFY(t2->wasSuccessful());
QVERIFY(t3->wasSuccessful()); QVERIFY(t3->wasSuccessful());
}); });
t.start(); t.start();
QVERIFY2(QTest::qWaitFor([&]() { QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should.");
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
} }
void test_basicMultipleOptionsRun(){ void test_basicMultipleOptionsRun()
{
auto t1 = makeShared<BasicTask>(); auto t1 = makeShared<BasicTask>();
auto t2 = makeShared<BasicTask>(); auto t2 = makeShared<BasicTask>();
auto t3 = makeShared<BasicTask>(); auto t3 = makeShared<BasicTask>();
@ -238,33 +228,30 @@ class TaskTest : public QObject {
t.addTask(t2); t.addTask(t2);
t.addTask(t3); t.addTask(t3);
QObject::connect(&t, &Task::finished, [&]{ QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] {
QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
QVERIFY(t1->wasSuccessful()); QVERIFY(t1->wasSuccessful());
QVERIFY(!t2->wasSuccessful()); QVERIFY(!t2->wasSuccessful());
QVERIFY(!t3->wasSuccessful()); QVERIFY(!t3->wasSuccessful());
}); });
t.start(); t.start();
QVERIFY2(QTest::qWaitFor([&]() { QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should.");
return t.isFinished();
}, 1000), "Task didn't finish as it should.");
} }
void test_stackOverflowInConcurrentTask() void test_stackOverflowInConcurrentTask()
{ {
QEventLoop loop; QEventLoop loop;
auto thread = new BigConcurrentTaskThread; BigConcurrentTaskThread thread;
connect(thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); connect(&thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit);
thread->start(); thread.start();
loop.exec(); loop.exec();
QVERIFY(!thread->passed_the_deadline); QVERIFY(!thread.passed_the_deadline);
thread->deleteLater();
} }
}; };

View File

@ -20,6 +20,8 @@
class VersionTest : public QObject { class VersionTest : public QObject {
Q_OBJECT Q_OBJECT
QStringList m_flex_test_names = {};
void addDataColumns() void addDataColumns()
{ {
QTest::addColumn<QString>("first"); QTest::addColumn<QString>("first");
@ -101,8 +103,9 @@ class VersionTest : public QObject {
QString first{split_line.first().simplified()}; QString first{split_line.first().simplified()};
QString second{split_line.last().simplified()}; QString second{split_line.last().simplified()};
auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan").toLatin1().data(); auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan");
QTest::newRow(new_test_name) << first << second << true << false; m_flex_test_names.append(new_test_name);
QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << true << false;
continue; continue;
} }
@ -112,8 +115,9 @@ class VersionTest : public QObject {
QString first{split_line.first().simplified()}; QString first{split_line.first().simplified()};
QString second{split_line.last().simplified()}; QString second{split_line.last().simplified()};
auto new_test_name = test_name_template.arg(QString::number(test_number), "equals").toLatin1().data(); auto new_test_name = test_name_template.arg(QString::number(test_number), "equals");
QTest::newRow(new_test_name) << first << second << false << true; m_flex_test_names.append(new_test_name);
QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << true;
continue; continue;
} }
@ -123,8 +127,9 @@ class VersionTest : public QObject {
QString first{split_line.first().simplified()}; QString first{split_line.first().simplified()};
QString second{split_line.last().simplified()}; QString second{split_line.last().simplified()};
auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan").toLatin1().data(); auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan");
QTest::newRow(new_test_name) << first << second << false << false; m_flex_test_names.append(new_test_name);
QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << false;
continue; continue;
} }