Merge branch 'develop' into macos-add-to-path

This commit is contained in:
Ryan Cao 2022-11-14 19:26:31 +08:00 committed by GitHub
commit 2c9452efaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1286 additions and 262 deletions

View File

@ -126,7 +126,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 5) set(Launcher_VERSION_MAJOR 6)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")

View File

@ -9,7 +9,7 @@
Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC.
## Installation ## Installation
@ -18,35 +18,35 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC.
<img src="https://repology.org/badge/vertical-allrepos/prismlauncher.svg" alt="Packaging status" align="right"> <img src="https://repology.org/badge/vertical-allrepos/prismlauncher.svg" alt="Packaging status" align="right">
</a> </a>
- All downloads and instructions for Prism Launcher can be found [on our website](https://prismlauncher.org/download/). - All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download/).
- Last build status can be found [here](https://github.com/PrismLauncher/PrismLauncher/actions). - Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions).
### Development Builds ### Development Builds
There are development builds available [here](https://github.com/PrismLauncher/PrismLauncher/actions). These have debug information in the binaries, so their file sizes are relatively larger. There are development builds available [here](https://github.com/PrismLauncher/PrismLauncher/actions). These have debug information in the binaries, so their file sizes are relatively larger.
Portable builds are provided for Linux, Windows, and macOS. Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**.
For Arch, Debian and Gentoo, respectively, you can use these packages to get compiled development versions: For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions:
[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square)](https://packages.gentoo.org/packages/games-action/prismlauncher) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?style=flat-square&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher)
## Help & Support ## Community & Support
Feel free to create an issue if you need help. Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple communities that can also help you.
#### Join our Discord server: #### Join our Discord server:
[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher) [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher)
#### Join our Matrix space: #### Join our Matrix space (Will be opened at a later date):
[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge)](https://matrix.to/#/#prismlauncher:matrix.org) [![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org)
#### Join our SubReddit: #### Join our SubReddit:
[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge)](https://www.reddit.com/r/PrismLauncher/) [![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/)
## Building ## Building
If you want to build Prism Launcher yourself, check [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/) for build instructions. If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/).
## Translations ## Translations
@ -97,6 +97,6 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/),
All launcher code is available under the GPL-3.0-only license. All launcher code is available under the GPL-3.0-only license.
![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge) ![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge&logo=gnu&color=C4282D)
The logo and related assets are under the CC BY-SA 4.0 license. The logo and related assets are under the CC BY-SA 4.0 license.

View File

@ -563,7 +563,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Memory // Memory
m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096); m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, suitableMaxMem());
m_settings->registerSetting("PermGen", 128); m_settings->registerSetting("PermGen", 128);
// Java Settings // Java Settings
@ -611,6 +611,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// The cat // The cat
m_settings->registerSetting("TheCat", false); m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("ToolbarsLocked", false);
m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("SelectedInstance", QString()); m_settings->registerSetting("SelectedInstance", QString());
@ -1589,3 +1591,17 @@ QString Application::getUserAgentUncached()
return BuildConfig.USER_AGENT_UNCACHED; return BuildConfig.USER_AGENT_UNCACHED;
} }
int Application::suitableMaxMem()
{
float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte;
int maxMemoryAlloc;
// If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB
if (totalRAM < (4096 * 1.5))
maxMemoryAlloc = (int) (totalRAM / 1.5);
else
maxMemoryAlloc = 4096;
return maxMemoryAlloc;
}

View File

@ -200,6 +200,8 @@ public:
void ShowGlobalSettings(class QWidget * parent, QString open_page = QString()); void ShowGlobalSettings(class QWidget * parent, QString open_page = QString());
int suitableMaxMem();
signals: signals:
void updateAllowedChanged(bool status); void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen(); void globalSettingsAboutToOpen();

View File

@ -24,13 +24,15 @@ set(CORE_SOURCES
NullInstance.h NullInstance.h
MMCZip.h MMCZip.h
MMCZip.cpp MMCZip.cpp
MMCStrings.h StringUtils.h
MMCStrings.cpp StringUtils.cpp
RuntimeContext.h RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask) # Basic instance manipulation tasks (derived from InstanceTask)
InstanceCreationTask.h InstanceCreationTask.h
InstanceCreationTask.cpp InstanceCreationTask.cpp
InstanceCopyPrefs.h
InstanceCopyPrefs.cpp
InstanceCopyTask.h InstanceCopyTask.h
InstanceCopyTask.cpp InstanceCopyTask.cpp
InstanceImportTask.h InstanceImportTask.h
@ -1064,7 +1066,7 @@ if(INSTALL_BUNDLE STREQUAL "full")
# Image formats # Image formats
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
CONFIGURATIONS Debug RelWithDebInfo CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE REGEX "tga|tiff|mng" EXCLUDE
@ -1082,7 +1084,7 @@ if(INSTALL_BUNDLE STREQUAL "full")
# Icon engines # Icon engines
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines" DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
CONFIGURATIONS Debug RelWithDebInfo CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
REGEX "fontawesome" EXCLUDE REGEX "fontawesome" EXCLUDE
@ -1100,7 +1102,7 @@ if(INSTALL_BUNDLE STREQUAL "full")
# Platform plugins # Platform plugins
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms" DIRECTORY "${QT_PLUGINS_DIR}/platforms"
CONFIGURATIONS Debug RelWithDebInfo CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE REGEX "minimal|linuxfb|offscreen" EXCLUDE
@ -1119,7 +1121,7 @@ if(INSTALL_BUNDLE STREQUAL "full")
if(EXISTS "${QT_PLUGINS_DIR}/styles") if(EXISTS "${QT_PLUGINS_DIR}/styles")
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/styles" DIRECTORY "${QT_PLUGINS_DIR}/styles"
CONFIGURATIONS Debug RelWithDebInfo CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
) )
@ -1137,7 +1139,7 @@ if(INSTALL_BUNDLE STREQUAL "full")
if(EXISTS "${QT_PLUGINS_DIR}/tls") if(EXISTS "${QT_PLUGINS_DIR}/tls")
install( install(
DIRECTORY "${QT_PLUGINS_DIR}/tls" DIRECTORY "${QT_PLUGINS_DIR}/tls"
CONFIGURATIONS Debug RelWithDebInfo CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
) )

View File

@ -44,7 +44,9 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <QTextStream> #include <QTextStream>
#include <QUrl> #include <QUrl>
#include "DesktopServices.h" #include "DesktopServices.h"
#include "StringUtils.h"
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#include <objbase.h> #include <objbase.h>
@ -79,22 +81,6 @@ namespace fs = std::filesystem;
namespace fs = ghc::filesystem; namespace fs = ghc::filesystem;
#endif #endif
#if defined Q_OS_WIN32
std::wstring toStdString(QString s)
{
return s.toStdWString();
}
#else
std::string toStdString(QString s)
{
return s.toStdString();
}
#endif
namespace FS { namespace FS {
void ensureExists(const QDir& dir) void ensureExists(const QDir& dir)
@ -163,6 +149,9 @@ bool ensureFolderPathExists(QString foldernamepath)
return success; return success;
} }
/// @brief Copies a directory and it's contents from src to dest
/// @param offset subdirectory form src to copy to dest
/// @return if there was an error during the filecopy
bool copy::operator()(const QString& offset) bool copy::operator()(const QString& offset)
{ {
using copy_opts = fs::copy_options; using copy_opts = fs::copy_options;
@ -191,7 +180,7 @@ bool copy::operator()(const QString& offset)
auto dst_path = PathCombine(dst, relative_dst_path); auto dst_path = PathCombine(dst, relative_dst_path);
ensureFilePathExists(dst_path); ensureFilePathExists(dst_path);
fs::copy(toStdString(src_path), toStdString(dst_path), opt, err); fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err);
if (err) { if (err) {
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;
@ -213,7 +202,7 @@ bool copy::operator()(const QString& offset)
} }
// If the root src is not a directory, the previous iterator won't run. // If the root src is not a directory, the previous iterator won't run.
if (!fs::is_directory(toStdString(src))) if (!fs::is_directory(StringUtils::toStdString(src)))
copy_file(src, ""); copy_file(src, "");
return err.value() == 0; return err.value() == 0;
@ -223,7 +212,7 @@ bool deletePath(QString path)
{ {
std::error_code err; std::error_code err;
fs::remove_all(toStdString(path), err); fs::remove_all(StringUtils::toStdString(path), err);
if (err) { if (err) {
qWarning() << "Failed to remove files:" << QString::fromStdString(err.message()); qWarning() << "Failed to remove files:" << QString::fromStdString(err.message());
@ -414,7 +403,7 @@ bool overrideFolder(QString overwritten_path, QString override_path)
fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing;
// FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows? // FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows?
fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err); fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err);
if (err) { if (err) {
qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path);

View File

@ -75,6 +75,7 @@ bool ensureFilePathExists(QString filenamepath);
*/ */
bool ensureFolderPathExists(QString filenamepath); bool ensureFolderPathExists(QString filenamepath);
/// @brief Copies a directory and it's contents from src to dest
class copy { class copy {
public: public:
copy(const QString& src, const QString& dst) copy(const QString& src, const QString& dst)

View File

@ -0,0 +1,135 @@
//
// Created by marcelohdez on 10/22/22.
//
#include "InstanceCopyPrefs.h"
bool InstanceCopyPrefs::allTrue() const
{
return copySaves &&
keepPlaytime &&
copyGameOptions &&
copyResourcePacks &&
copyShaderPacks &&
copyServers &&
copyMods &&
copyScreenshots;
}
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
{
QStringList filters;
if(!copySaves)
filters << "saves";
if(!copyGameOptions)
filters << "options.txt";
if(!copyResourcePacks)
filters << "resourcepacks" << "texturepacks";
if(!copyShaderPacks)
filters << "shaderpacks";
if(!copyServers)
filters << "servers.dat" << "servers.dat_old" << "server-resource-packs";
if(!copyMods)
filters << "coremods" << "mods" << "config";
if(!copyScreenshots)
filters << "screenshots";
// If we have any filters to add, join them as a single regex string to return:
if (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/";
// Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"):
return MC_ROOT + filters.join("|" + MC_ROOT);
}
return {};
}
// ======= Getters =======
bool InstanceCopyPrefs::isCopySavesEnabled() const
{
return copySaves;
}
bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const
{
return keepPlaytime;
}
bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const
{
return copyGameOptions;
}
bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const
{
return copyResourcePacks;
}
bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const
{
return copyShaderPacks;
}
bool InstanceCopyPrefs::isCopyServersEnabled() const
{
return copyServers;
}
bool InstanceCopyPrefs::isCopyModsEnabled() const
{
return copyMods;
}
bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
{
return copyScreenshots;
}
// ======= Setters =======
void InstanceCopyPrefs::enableCopySaves(bool b)
{
copySaves = b;
}
void InstanceCopyPrefs::enableKeepPlaytime(bool b)
{
keepPlaytime = b;
}
void InstanceCopyPrefs::enableCopyGameOptions(bool b)
{
copyGameOptions = b;
}
void InstanceCopyPrefs::enableCopyResourcePacks(bool b)
{
copyResourcePacks = b;
}
void InstanceCopyPrefs::enableCopyShaderPacks(bool b)
{
copyShaderPacks = b;
}
void InstanceCopyPrefs::enableCopyServers(bool b)
{
copyServers = b;
}
void InstanceCopyPrefs::enableCopyMods(bool b)
{
copyMods = b;
}
void InstanceCopyPrefs::enableCopyScreenshots(bool b)
{
copyScreenshots = b;
}

View File

@ -0,0 +1,41 @@
//
// Created by marcelohdez on 10/22/22.
//
#pragma once
#include <QStringList>
struct InstanceCopyPrefs {
public:
[[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const;
// Getters
[[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const;
[[nodiscard]] bool isCopyGameOptionsEnabled() const;
[[nodiscard]] bool isCopyResourcePacksEnabled() const;
[[nodiscard]] bool isCopyShaderPacksEnabled() const;
[[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() const;
[[nodiscard]] bool isCopyScreenshotsEnabled() const;
// Setters
void enableCopySaves(bool b);
void enableKeepPlaytime(bool b);
void enableCopyGameOptions(bool b);
void enableCopyResourcePacks(bool b);
void enableCopyShaderPacks(bool b);
void enableCopyServers(bool b);
void enableCopyMods(bool b);
void enableCopyScreenshots(bool b);
protected: // data
bool copySaves = true;
bool keepPlaytime = true;
bool copyGameOptions = true;
bool copyResourcePacks = true;
bool copyShaderPacks = true;
bool copyServers = true;
bool copyMods = true;
bool copyScreenshots = true;
};

View File

@ -5,15 +5,17 @@
#include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/RegexpMatcher.h"
#include <QtConcurrentRun> #include <QtConcurrentRun>
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime) InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{ {
m_origInstance = origInstance; m_origInstance = origInstance;
m_keepPlaytime = keepPlaytime; m_keepPlaytime = prefs.isKeepPlaytimeEnabled();
if(!copySaves) QString filters = prefs.getSelectedFiltersAsRegex();
if (!filters.isEmpty())
{ {
// Set regex filter:
// FIXME: get this from the original instance type... // FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); auto matcherReal = new RegexpMatcher(filters);
matcherReal->caseSensitive(false); matcherReal->caseSensitive(false);
m_matcher.reset(matcherReal); m_matcher.reset(matcherReal);
} }

View File

@ -1,20 +1,21 @@
#pragma once #pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include <QFuture> #include <QFuture>
#include <QFutureWatcher> #include <QFutureWatcher>
#include "settings/SettingsObject.h" #include <QUrl>
#include "BaseVersion.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include "BaseVersion.h"
#include "InstanceCopyPrefs.h"
#include "InstanceTask.h" #include "InstanceTask.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include "tasks/Task.h"
class InstanceCopyTask : public InstanceTask class InstanceCopyTask : public InstanceTask
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime); explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs);
protected: protected:
//! Entry point for tasks. //! Entry point for tasks.
@ -22,7 +23,8 @@ protected:
void copyFinished(); void copyFinished();
void copyAborted(); void copyAborted();
private: /* data */ private:
/* data */
InstancePtr m_origInstance; InstancePtr m_origInstance;
QFuture<bool> m_copyFuture; QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher; QFutureWatcher<bool> m_copyFutureWatcher;

View File

@ -36,7 +36,7 @@
#include "JavaCommon.h" #include "JavaCommon.h"
#include "java/JavaUtils.h" #include "java/JavaUtils.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include <MMCStrings.h>
#include <QRegularExpression> #include <QRegularExpression>
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent)

View File

@ -1,8 +0,0 @@
#pragma once
#include <QString>
namespace Strings
{
int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs);
}

View File

@ -1,26 +1,28 @@
#include "MMCStrings.h" #include "StringUtils.h"
/// If you're wondering where these came from exactly, then know you're not the only one =D
/// TAKEN FROM Qt, because it doesn't expose it intelligently /// TAKEN FROM Qt, because it doesn't expose it intelligently
static inline QChar getNextChar(const QString &s, int location) static inline QChar getNextChar(const QString& s, int location)
{ {
return (location < s.length()) ? s.at(location) : QChar(); return (location < s.length()) ? s.at(location) : QChar();
} }
/// TAKEN FROM Qt, because it doesn't expose it intelligently /// TAKEN FROM Qt, because it doesn't expose it intelligently
int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{ {
for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) int l1 = 0, l2 = 0;
{ while (l1 <= s1.count() && l2 <= s2.count()) {
// skip spaces, tabs and 0's // skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1); QChar c1 = getNextChar(s1, l1);
while (c1.isSpace()) while (c1.isSpace())
c1 = getNextChar(s1, ++l1); c1 = getNextChar(s1, ++l1);
QChar c2 = getNextChar(s2, l2); QChar c2 = getNextChar(s2, l2);
while (c2.isSpace()) while (c2.isSpace())
c2 = getNextChar(s2, ++l2); c2 = getNextChar(s2, ++l2);
if (c1.isDigit() && c2.isDigit()) if (c1.isDigit() && c2.isDigit()) {
{
while (c1.digitValue() == 0) while (c1.digitValue() == 0)
c1 = getNextChar(s1, ++l1); c1 = getNextChar(s1, ++l1);
while (c2.digitValue() == 0) while (c2.digitValue() == 0)
@ -30,11 +32,8 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
int lookAheadLocation2 = l2; int lookAheadLocation2 = l2;
int currentReturnValue = 0; int currentReturnValue = 0;
// find the last digit, setting currentReturnValue as we go if it isn't equal // find the last digit, setting currentReturnValue as we go if it isn't equal
for (QChar lookAhead1 = c1, lookAhead2 = c2; for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
(lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) {
lookAhead1 = getNextChar(s1, ++lookAheadLocation1),
lookAhead2 = getNextChar(s2, ++lookAheadLocation2))
{
bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit();
bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit();
if (!is1ADigit && !is2ADigit) if (!is1ADigit && !is2ADigit)
@ -43,14 +42,10 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
return -1; return -1;
if (!is2ADigit) if (!is2ADigit)
return 1; return 1;
if (currentReturnValue == 0) if (currentReturnValue == 0) {
{ if (lookAhead1 < lookAhead2) {
if (lookAhead1 < lookAhead2)
{
currentReturnValue = -1; currentReturnValue = -1;
} } else if (lookAhead1 > lookAhead2) {
else if (lookAhead1 > lookAhead2)
{
currentReturnValue = 1; currentReturnValue = 1;
} }
} }
@ -58,19 +53,24 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit
if (currentReturnValue != 0) if (currentReturnValue != 0)
return currentReturnValue; return currentReturnValue;
} }
if (cs == Qt::CaseInsensitive)
{ if (cs == Qt::CaseInsensitive) {
if (!c1.isLower()) if (!c1.isLower())
c1 = c1.toLower(); c1 = c1.toLower();
if (!c2.isLower()) if (!c2.isLower())
c2 = c2.toLower(); c2 = c2.toLower();
} }
int r = QString::localeAwareCompare(c1, c2); int r = QString::localeAwareCompare(c1, c2);
if (r < 0) if (r < 0)
return -1; return -1;
if (r > 0) if (r > 0)
return 1; return 1;
l1 += 1;
l2 += 1;
} }
// The two strings are the same (02 == 2) so fall back to the normal sort // The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs); return QString::compare(s1, s2, cs);
} }

32
launcher/StringUtils.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <QString>
namespace StringUtils {
#if defined Q_OS_WIN32
using string = std::wstring;
inline string toStdString(QString s)
{
return s.toStdWString();
}
inline QString fromStdString(string s)
{
return QString::fromStdWString(s);
}
#else
using string = std::string;
inline string toStdString(QString s)
{
return s.toStdString();
}
inline QString fromStdString(string s)
{
return QString::fromStdString(s);
}
#endif
int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs);
} // namespace StringUtils

View File

@ -1,9 +1,10 @@
#include "JavaInstall.h" #include "JavaInstall.h"
#include <MMCStrings.h>
#include "StringUtils.h"
bool JavaInstall::operator<(const JavaInstall &rhs) bool JavaInstall::operator<(const JavaInstall &rhs)
{ {
auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
if(archCompare != 0) if(archCompare != 0)
return archCompare < 0; return archCompare < 0;
if(id < rhs.id) if(id < rhs.id)
@ -14,7 +15,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs)
{ {
return false; return false;
} }
return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
} }
bool JavaInstall::operator==(const JavaInstall &rhs) bool JavaInstall::operator==(const JavaInstall &rhs)

View File

@ -41,7 +41,6 @@
#include "java/JavaInstallList.h" #include "java/JavaInstallList.h"
#include "java/JavaCheckerJob.h" #include "java/JavaCheckerJob.h"
#include "java/JavaUtils.h" #include "java/JavaUtils.h"
#include "MMCStrings.h"
#include "minecraft/VersionFilterData.h" #include "minecraft/VersionFilterData.h"
JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent)

View File

@ -1,5 +1,6 @@
#include "JavaVersion.h" #include "JavaVersion.h"
#include <MMCStrings.h>
#include "StringUtils.h"
#include <QRegularExpression> #include <QRegularExpression>
#include <QString> #include <QString>
@ -98,12 +99,12 @@ bool JavaVersion::operator<(const JavaVersion &rhs)
else if(thisPre && rhsPre) else if(thisPre && rhsPre)
{ {
// both are prereleases - use natural compare... // both are prereleases - use natural compare...
return Strings::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0;
} }
// neither is prerelease, so they are the same -> this cannot be less than rhs // neither is prerelease, so they are the same -> this cannot be less than rhs
return false; return false;
} }
else return Strings::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; else return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0;
} }
bool JavaVersion::operator==(const JavaVersion &rhs) bool JavaVersion::operator==(const JavaVersion &rhs)

View File

@ -37,7 +37,6 @@
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include "MessageLevel.h" #include "MessageLevel.h"
#include "MMCStrings.h"
#include "java/JavaChecker.h" #include "java/JavaChecker.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include <QDebug> #include <QDebug>

View File

@ -91,5 +91,7 @@ int main(int argc, char *argv[])
return 1; return 1;
case Application::Succeeded: case Application::Succeeded:
return 0; return 0;
default:
return -1;
} }
} }

View File

@ -43,7 +43,6 @@
#include "settings/SettingsObject.h" #include "settings/SettingsObject.h"
#include "Application.h" #include "Application.h"
#include "MMCStrings.h"
#include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/RegexpMatcher.h"
#include "pathmatcher/MultiMatcher.h" #include "pathmatcher/MultiMatcher.h"
#include "FileSystem.h" #include "FileSystem.h"

View File

@ -36,7 +36,7 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack&
} }
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); SetFileAttributesW(index_dir.path().toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif #endif
} }

View File

@ -372,13 +372,20 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
auto results = m_mod_id_resolver->getResults(); auto results = m_mod_id_resolver->getResults();
// first check for blocked mods // first check for blocked mods
QString text; QList<BlockedMod> blocked_mods;
QList<QUrl> urls;
auto anyBlocked = false; auto anyBlocked = false;
for (const auto& result : results.files.values()) { for (const auto& result : results.files.values()) {
if (!result.resolved || result.url.isEmpty()) { if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
urls.append(QUrl(result.websiteUrl)); BlockedMod blocked_mod;
blocked_mod.name = result.fileName;
blocked_mod.websiteUrl = result.websiteUrl;
blocked_mod.hash = result.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
blocked_mods.append(blocked_mod);
anyBlocked = true; anyBlocked = true;
} }
} }
@ -388,11 +395,12 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>" tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"), "You will need to manually download them and add them to the modpack"),
text, blocked_mods);
urls);
message_dialog->setModal(true); message_dialog->setModal(true);
if (message_dialog->exec()) { if (message_dialog->exec()) {
qDebug() << "Post dialog blocked mods list: " << blocked_mods;
copyBlockedMods(blocked_mods);
setupDownloadJob(loop); setupDownloadJob(loop);
} else { } else {
m_mod_id_resolver.reset(); m_mod_id_resolver.reset();
@ -404,6 +412,38 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
} }
} }
/// @brief copy the matched blocked mods to the instance staging area
/// @param blocked_mods list of the blocked mods and their matched paths
void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
{
setStatus(tr("Copying Blocked Mods..."));
setAbortable(false);
int i = 0;
int total = blocked_mods.length();
setProgress(i, total);
for (auto const& mod : blocked_mods) {
if (!mod.matched) {
qDebug() << mod.name << "was not matched to a local file, skipping copy";
continue;
}
auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod.name);
setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
if (!FS::copy(mod.localPath, dest_path)()) {
qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
}
i++;
setProgress(i, total);
}
setAbortable(true);
}
void FlameCreationTask::setupDownloadJob(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{ {
m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); m_files_job = new NetJob(tr("Mod download"), APPLICATION->network());
@ -449,7 +489,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
m_files_job.reset(); m_files_job.reset();
setError(reason); setError(reason);
}); });
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); }); connect(m_files_job.get(), &NetJob::progress, this, &FlameCreationTask::setProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods...")); setStatus(tr("Downloading mods..."));

View File

@ -10,6 +10,8 @@
#include "net/NetJob.h" #include "net/NetJob.h"
#include "ui/dialogs/BlockedModsDialog.h"
class FlameCreationTask final : public InstanceCreationTask { class FlameCreationTask final : public InstanceCreationTask {
Q_OBJECT Q_OBJECT
@ -29,6 +31,7 @@ class FlameCreationTask final : public InstanceCreationTask {
private slots: private slots:
void idResolverSucceeded(QEventLoop&); void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&); void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
private: private:
QWidget* m_parent = nullptr; QWidget* m_parent = nullptr;

View File

@ -4,6 +4,7 @@
#include <QFile> #include <QFile>
#include "FileSystem.h" #include "FileSystem.h"
#include "StringUtils.h"
#include <MurmurHash2.h> #include <MurmurHash2.h>
@ -35,6 +36,18 @@ Hasher::Ptr createFlameHasher(QString file_path)
return new FlameHasher(file_path); return new FlameHasher(file_path);
} }
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider)
{
return new BlockedModHasher(file_path, provider);
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type)
{
auto hasher = new BlockedModHasher(file_path, provider);
hasher->useHashType(type);
return hasher;
}
void ModrinthHasher::executeTask() void ModrinthHasher::executeTask()
{ {
QFile file(m_path); QFile file(m_path);
@ -66,7 +79,7 @@ void FlameHasher::executeTask()
// CF-specific // CF-specific
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary); std::ifstream file_stream(StringUtils::toStdString(m_path), std::ifstream::binary);
// TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread. // TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
// How do we make this non-blocking then? // How do we make this non-blocking then?
m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out)); m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
@ -78,4 +91,50 @@ void FlameHasher::executeTask()
} }
} }
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider)
: Hasher(file_path), provider(provider) {
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
}
void BlockedModHasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
}
m_hash = ProviderCaps.hash(provider, &file, hash_type);
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
}
}
QStringList BlockedModHasher::getHashTypes() {
return ProviderCaps.hashType(provider);
}
bool BlockedModHasher::useHashType(QString type) {
auto types = ProviderCaps.hashType(provider);
if (types.contains(type)) {
hash_type = type;
return true;
}
qDebug() << "Bad hash type " << type << " for provider";
return false;
}
} // namespace Hashing } // namespace Hashing

View File

@ -40,8 +40,23 @@ class ModrinthHasher : public Hasher {
void executeTask() override; void executeTask() override;
}; };
class BlockedModHasher : public Hasher {
public:
BlockedModHasher(QString file_path, ModPlatform::Provider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
ModPlatform::Provider provider;
QString hash_type;
};
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider); Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
Hasher::Ptr createFlameHasher(QString file_path); Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path); Hasher::Ptr createModrinthHasher(QString file_path);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type);
} // namespace Hashing } // namespace Hashing

View File

@ -176,8 +176,6 @@ void PackInstallTask::resolveMods()
void PackInstallTask::onResolveModsSucceeded() void PackInstallTask::onResolveModsSucceeded()
{ {
QString text;
QList<QUrl> urls;
auto anyBlocked = false; auto anyBlocked = false;
Flame::Manifest results = m_mod_id_resolver_task->getResults(); Flame::Manifest results = m_mod_id_resolver_task->getResults();
@ -191,11 +189,16 @@ void PackInstallTask::onResolveModsSucceeded()
// First check for blocked mods // First check for blocked mods
if (!results_file.resolved || results_file.url.isEmpty()) { if (!results_file.resolved || results_file.url.isEmpty()) {
QString type(local_file.type);
type[0] = type[0].toUpper(); BlockedMod blocked_mod;
text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl); blocked_mod.name = local_file.name;
urls.append(QUrl(results_file.websiteUrl)); blocked_mod.websiteUrl = results_file.websiteUrl;
blocked_mod.hash = results_file.hash;
blocked_mod.matched = false;
blocked_mod.localPath = "";
m_blocked_mods.append(blocked_mod);
anyBlocked = true; anyBlocked = true;
} else { } else {
local_file.url = results_file.url.toString(); local_file.url = results_file.url.toString();
@ -210,13 +213,16 @@ void PackInstallTask::onResolveModsSucceeded()
auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"), auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"),
tr("The following files are not available for download in third party launchers.<br/>" tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."), "You will need to manually download them and add them to the instance."),
text, m_blocked_mods);
urls);
if (message_dialog->exec() == QDialog::Accepted) if (message_dialog->exec() == QDialog::Accepted) {
qDebug() << "Post dialog blocked mods list: " << m_blocked_mods;
createInstance(); createInstance();
else }
else {
abort(); abort();
}
} else { } else {
createInstance(); createInstance();
} }
@ -320,6 +326,9 @@ void PackInstallTask::downloadPack()
void PackInstallTask::onModDownloadSucceeded() void PackInstallTask::onModDownloadSucceeded()
{ {
m_net_job.reset(); m_net_job.reset();
if (!m_blocked_mods.isEmpty()) {
copyBlockedMods();
}
emitSucceeded(); emitSucceeded();
} }
@ -343,4 +352,35 @@ void PackInstallTask::onModDownloadFailed(QString reason)
emitFailed(reason); emitFailed(reason);
} }
/// @brief copy the matched blocked mods to the instance staging area
void PackInstallTask::copyBlockedMods()
{
setStatus(tr("Copying Blocked Mods..."));
setAbortable(false);
int i = 0;
int total = m_blocked_mods.length();
setProgress(i, total);
for (auto const& mod : m_blocked_mods) {
if (!mod.matched) {
qDebug() << mod.name << "was not matched to a local file, skipping copy";
continue;
}
auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod.name);
setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path;
if (!FS::copy(mod.localPath, dest_path)()) {
qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed";
}
i++;
setProgress(i, total);
}
setAbortable(true);
}
} // namespace ModpacksCH } // namespace ModpacksCH

View File

@ -43,6 +43,7 @@
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include <QWidget> #include <QWidget>
@ -76,6 +77,7 @@ private:
void resolveMods(); void resolveMods();
void createInstance(); void createInstance();
void downloadPack(); void downloadPack();
void copyBlockedMods();
private: private:
NetJob::Ptr m_net_job = nullptr; NetJob::Ptr m_net_job = nullptr;
@ -90,6 +92,7 @@ private:
Version m_version; Version m_version;
QMap<QString, QString> m_files_to_copy; QMap<QString, QString> m_files_to_copy;
QList<BlockedMod> m_blocked_mods;
//FIXME: nuke //FIXME: nuke
QWidget* m_parent; QWidget* m_parent;

View File

@ -22,10 +22,14 @@
#include <QDir> #include <QDir>
#include <QObject> #include <QObject>
#include <toml++/toml.h> #include "FileSystem.h"
#include "StringUtils.h"
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include <toml++/toml.h>
namespace Packwiz { namespace Packwiz {
auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString
@ -63,22 +67,22 @@ static inline auto indexFileName(QString const& mod_slug) -> QString
static ModPlatform::ProviderCapabilities ProviderCaps; static ModPlatform::ProviderCapabilities ProviderCaps;
// Helper functions for extracting data from the TOML file // Helper functions for extracting data from the TOML file
auto stringEntry(toml::table table, const std::string entry_name) -> QString auto stringEntry(toml::table table, QString entry_name) -> QString
{ {
auto node = table[entry_name]; auto node = table[StringUtils::toStdString(entry_name)];
if (!node) { if (!node) {
qCritical() << QString::fromStdString("Failed to read str property '" + entry_name + "' in mod metadata."); qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata.";
return {}; return {};
} }
return QString::fromStdString(node.value_or("")); return node.value_or("");
} }
auto intEntry(toml::table table, const std::string entry_name) -> int auto intEntry(toml::table table, QString entry_name) -> int
{ {
auto node = table[entry_name]; auto node = table[StringUtils::toStdString(entry_name)];
if (!node) { if (!node) {
qCritical() << QString::fromStdString("Failed to read int property '" + entry_name + "' in mod metadata."); qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata.";
return {}; return {};
} }
@ -145,6 +149,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
// they want to do! // they want to do!
if (index_file.exists()) { if (index_file.exists()) {
index_file.remove(); index_file.remove();
} else {
FS::ensureFilePathExists(index_file.fileName());
} }
if (!index_file.open(QIODevice::ReadWrite)) { if (!index_file.open(QIODevice::ReadWrite)) {
@ -228,14 +234,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
toml::table table; toml::table table;
#if TOML_EXCEPTIONS #if TOML_EXCEPTIONS
try { try {
table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
} catch (const toml::parse_error& err) { } catch (const toml::parse_error& err) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(err.what()); qWarning() << "Reason: " << QString(err.what());
return {}; return {};
} }
#else #else
table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
if (!table) { if (!table) {
qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(table.error().what()); qWarning() << "Reason: " << QString(table.error().what());

View File

@ -24,7 +24,6 @@
#include <QUrl> #include <QUrl>
#include <QVariant> #include <QVariant>
struct toml_table_t;
class QDir; class QDir;
// Mod from launcher/minecraft/mod/Mod.h // Mod from launcher/minecraft/mod/Mod.h
@ -34,9 +33,6 @@ namespace Packwiz {
auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString;
auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString;
auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
class V1 { class V1 {
public: public:
struct Mod { struct Mod {

View File

@ -4,12 +4,14 @@
<file alias="kitteh">kitteh.png</file> <file alias="kitteh">kitteh.png</file>
<file alias="kitteh-xmas">kitteh-xmas.png</file> <file alias="kitteh-xmas">kitteh-xmas.png</file>
<file alias="kitteh-bday">kitteh-bday.png</file> <file alias="kitteh-bday">kitteh-bday.png</file>
<file alias="kitteh-ween">kitteh-ween.png</file> <file alias="kitteh-spooky">kitteh-spooky.png</file>
<file alias="rory">rory.png</file> <file alias="rory">rory.png</file>
<file alias="rory-xmas">rory-xmas.png</file> <file alias="rory-xmas">rory-xmas.png</file>
<file alias="rory-bday">rory-bday.png</file> <file alias="rory-bday">rory-bday.png</file>
<file alias="rory-spooky">rory-spooky.png</file>
<file alias="rory-flat">rory-flat.png</file> <file alias="rory-flat">rory-flat.png</file>
<file alias="rory-flat-xmas">rory-flat-xmas.png</file> <file alias="rory-flat-xmas">rory-flat-xmas.png</file>
<file alias="rory-flat-bday">rory-flat-bday.png</file> <file alias="rory-flat-bday">rory-flat-bday.png</file>
<file alias="rory-flat-spooky">rory-flat-spooky.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -266,6 +266,8 @@ public:
TranslatedAction actionNoAccountsAdded; TranslatedAction actionNoAccountsAdded;
TranslatedAction actionNoDefaultAccount; TranslatedAction actionNoDefaultAccount;
TranslatedAction actionLockToolbars;
QVector<TranslatedToolButton *> all_toolbuttons; QVector<TranslatedToolButton *> all_toolbuttons;
QWidget *centralWidget = nullptr; QWidget *centralWidget = nullptr;
@ -432,6 +434,12 @@ public:
actionManageAccounts->setCheckable(false); actionManageAccounts->setCheckable(false);
actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts"));
all_actions.append(&actionManageAccounts); all_actions.append(&actionManageAccounts);
actionLockToolbars = TranslatedAction(MainWindow);
actionLockToolbars->setObjectName(QStringLiteral("actionLockToolbars"));
actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars"));
actionLockToolbars->setCheckable(true);
all_actions.append(&actionLockToolbars);
} }
void createMainToolbar(QMainWindow *MainWindow) void createMainToolbar(QMainWindow *MainWindow)
@ -439,7 +447,6 @@ public:
mainToolBar = TranslatedToolbar(MainWindow); mainToolBar = TranslatedToolbar(MainWindow);
mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool());
mainToolBar->setObjectName(QStringLiteral("mainToolBar")); mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
mainToolBar->setMovable(true);
mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
mainToolBar->setFloatable(false); mainToolBar->setFloatable(false);
@ -540,6 +547,8 @@ public:
viewMenu->addAction(actionCAT); viewMenu->addAction(actionCAT);
viewMenu->addSeparator(); viewMenu->addSeparator();
viewMenu->addAction(actionLockToolbars);
menuBar->addMenu(foldersMenu); menuBar->addMenu(foldersMenu);
profileMenu = menuBar->addMenu(tr("&Accounts")); profileMenu = menuBar->addMenu(tr("&Accounts"));
@ -620,7 +629,6 @@ public:
{ {
newsToolBar = TranslatedToolbar(MainWindow); newsToolBar = TranslatedToolbar(MainWindow);
newsToolBar->setObjectName(QStringLiteral("newsToolBar")); newsToolBar->setObjectName(QStringLiteral("newsToolBar"));
newsToolBar->setMovable(true);
newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
newsToolBar->setIconSize(QSize(16, 16)); newsToolBar->setIconSize(QSize(16, 16));
newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
@ -755,7 +763,6 @@ public:
instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); instanceToolBar->setObjectName(QStringLiteral("instanceToolBar"));
// disabled until we have an instance selected // disabled until we have an instance selected
instanceToolBar->setEnabled(false); instanceToolBar->setEnabled(false);
instanceToolBar->setMovable(true);
// Qt doesn't like vertical moving toolbars, so we have to force them... // Qt doesn't like vertical moving toolbars, so we have to force them...
// See https://github.com/PolyMC/PolyMC/issues/493 // See https://github.com/PolyMC/PolyMC/issues/493
connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); });
@ -937,6 +944,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool)));
setCatBackground(cat_enable); setCatBackground(cat_enable);
} }
// Lock toolbars
{
bool toolbarsLocked = APPLICATION->settings()->get("ToolbarsLocked").toBool();
ui->actionLockToolbars->setChecked(toolbarsLocked);
connect(ui->actionLockToolbars, &QAction::toggled, this, &MainWindow::lockToolbars);
lockToolbars(toolbarsLocked);
}
// start instance when double-clicked // start instance when double-clicked
connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated); connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated);
@ -1092,8 +1107,19 @@ QMenu * MainWindow::createPopupMenu()
{ {
QMenu* filteredMenu = QMainWindow::createPopupMenu(); QMenu* filteredMenu = QMainWindow::createPopupMenu();
filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() ); filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() );
filteredMenu->addAction(ui->actionLockToolbars);
return filteredMenu; return filteredMenu;
} }
void MainWindow::lockToolbars(bool state)
{
ui->mainToolBar->setMovable(!state);
ui->instanceToolBar->setMovable(!state);
ui->newsToolBar->setMovable(!state);
APPLICATION->settings()->set("ToolbarsLocked", state);
}
void MainWindow::konamiTriggered() void MainWindow::konamiTriggered()
{ {
@ -1583,8 +1609,8 @@ void MainWindow::setCatBackground(bool enabled)
QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); QString cat = APPLICATION->settings()->get("BackgroundCat").toString();
if (non_stupid_abs(now.daysTo(xmas)) <= 4) { if (non_stupid_abs(now.daysTo(xmas)) <= 4) {
cat += "-xmas"; cat += "-xmas";
} else if (cat == "kitteh" && non_stupid_abs(now.daysTo(halloween)) <= 4) { } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) {
cat += "-ween"; cat += "-spooky";
} else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) {
cat += "-bday"; cat += "-bday";
} }
@ -1644,7 +1670,7 @@ void MainWindow::on_actionCopyInstance_triggered()
if (!copyInstDlg.exec()) if (!copyInstDlg.exec())
return; return;
auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime()); auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.getChosenOptions());
copyTask->setName(copyInstDlg.instName()); copyTask->setName(copyInstDlg.instName());
copyTask->setGroup(copyInstDlg.instGroup()); copyTask->setGroup(copyInstDlg.instGroup());
copyTask->setIcon(copyInstDlg.iconKey()); copyTask->setIcon(copyInstDlg.iconKey());
@ -1919,6 +1945,7 @@ void MainWindow::on_actionReportBug_triggered()
void MainWindow::on_actionClearMetadata_triggered() void MainWindow::on_actionClearMetadata_triggered()
{ {
APPLICATION->metacache()->evictAll(); APPLICATION->metacache()->evictAll();
APPLICATION->metacache()->SaveNow();
} }
#ifdef Q_OS_MAC #ifdef Q_OS_MAC

View File

@ -207,6 +207,8 @@ private slots:
void globalSettingsClosed(); void globalSettingsClosed();
void lockToolbars(bool);
#ifndef Q_OS_MAC #ifndef Q_OS_MAC
void keyReleaseEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override;
#endif #endif

View File

@ -1,28 +1,186 @@
#include "BlockedModsDialog.h" #include "BlockedModsDialog.h"
#include "ui_BlockedModsDialog.h"
#include <QPushButton>
#include <QDialogButtonBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDialogButtonBox>
#include <QPushButton>
#include "Application.h"
#include "ui_BlockedModsDialog.h"
#include <QDebug>
#include <QStandardPaths>
BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls) : BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods)
QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) { : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods)
{
ui->setupUi(this); ui->setupUi(this);
auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole);
connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll);
connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged);
hashing_task = shared_qobject_ptr<ConcurrentTask>(new ConcurrentTask(this, "MakeHashesTask", 10));
qDebug() << "Mods List: " << mods;
setupWatch();
scanPaths();
this->setWindowTitle(title); this->setWindowTitle(title);
ui->label->setText(text); ui->label->setText(text);
ui->textBrowser->setText(body); ui->labelModsFound->setText(tr("Please download the missing mods."));
update();
} }
BlockedModsDialog::~BlockedModsDialog() { BlockedModsDialog::~BlockedModsDialog()
{
delete ui; delete ui;
} }
void BlockedModsDialog::openAll() { void BlockedModsDialog::openAll()
for(auto &url : urls) { {
QDesktopServices::openUrl(url); for (auto& mod : mods) {
QDesktopServices::openUrl(mod.websiteUrl);
} }
} }
/// @brief update UI with current status of the blocked mod detection
void BlockedModsDialog::update()
{
QString text;
QString span;
for (auto& mod : mods) {
if (mod.matched) {
// &#x2714; -> html for HEAVY CHECK MARK : ✔
span = QString(tr("<span style=\"color:green\"> &#x2714; Found at %1 </span>")).arg(mod.localPath);
} else {
// &#x2718; -> html for HEAVY BALLOT X : ✘
span = QString(tr("<span style=\"color:red\"> &#x2718; Not Found </span>"));
}
text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>")).arg(mod.name, mod.websiteUrl, mod.hash, span);
}
ui->textBrowser->setText(text);
if (allModsMatched()) {
ui->labelModsFound->setText(tr("All mods found ✔"));
} else {
ui->labelModsFound->setText(tr("Please download the missing mods."));
}
}
/// @brief Signal fired when a watched direcotry has changed
/// @param path the path to the changed directory
void BlockedModsDialog::directoryChanged(QString path)
{
qDebug() << "Directory changed: " << path;
scanPath(path);
}
/// @brief add the user downloads folder and the global mods folder to the filesystem watcher
void BlockedModsDialog::setupWatch()
{
const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString();
watcher.addPath(downloadsFolder);
watcher.addPath(modsFolder);
}
/// @brief scan all watched folder
void BlockedModsDialog::scanPaths()
{
for (auto& dir : watcher.directories()) {
scanPath(dir);
}
}
/// @brief Scan the directory at path, skip paths that do not contain a file name
/// of a blocked mod we are looking for
/// @param path the directory to scan
void BlockedModsDialog::scanPath(QString path)
{
QDir scan_dir(path);
QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags);
while (scan_it.hasNext()) {
QString file = scan_it.next();
if (!checkValidPath(file)) {
continue;
}
auto hash_task = Hashing::createBlockedModHasher(file, ModPlatform::Provider::FLAME, "sha1");
qDebug() << "Creating Hash task for path: " << file;
connect(hash_task.get(), &Task::succeeded, [this, hash_task, file] { checkMatchHash(hash_task->getResult(), file); });
connect(hash_task.get(), &Task::failed, [file] { qDebug() << "Failed to hash path: " << file; });
hashing_task->addTask(hash_task);
}
hashing_task->start();
}
/// @brief check if the computed hash for the provided path matches a blocked
/// mod we are looking for
/// @param hash the computed hash for the provided path
/// @param path the path to the local file being compared
void BlockedModsDialog::checkMatchHash(QString hash, QString path)
{
bool match = false;
qDebug() << "Checking for match on hash: " << hash << "| From path:" << path;
for (auto& mod : mods) {
if (mod.matched) {
continue;
}
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
mod.matched = true;
mod.localPath = path;
match = true;
qDebug() << "Hash match found:" << mod.name << hash << "| From path:" << path;
break;
}
}
if (match) {
update();
}
}
/// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for
/// @param path the path to check
/// @return boolean: did the path match the name of a blocked mod?
bool BlockedModsDialog::checkValidPath(QString path)
{
QFileInfo file = QFileInfo(path);
QString filename = file.fileName();
for (auto& mod : mods) {
if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) {
qDebug() << "Name match found:" << mod.name << "| From path:" << path;
return true;
}
}
return false;
}
bool BlockedModsDialog::allModsMatched()
{
return std::all_of(mods.begin(), mods.end(), [](auto const& mod) { return mod.matched; });
}
/// qDebug print support for the BlockedMod struct
QDebug operator<<(QDebug debug, const BlockedMod& m)
{
QDebugStateSaver saver(debug);
debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl << ", hash: " << m.hash << ", matched: " << m.matched
<< ", localPath: " << m.localPath << "}";
return debug;
}

View File

@ -1,7 +1,23 @@
#pragma once #pragma once
#include <QDialog> #include <QDialog>
#include <QString>
#include <QList>
#include <QFileSystemWatcher>
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
struct BlockedMod {
QString name;
QString websiteUrl;
QString hash;
bool matched;
QString localPath;
};
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
namespace Ui { class BlockedModsDialog; } namespace Ui { class BlockedModsDialog; }
@ -11,12 +27,27 @@ class BlockedModsDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls); BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList<BlockedMod> &mods);
~BlockedModsDialog() override; ~BlockedModsDialog() override;
private: private:
Ui::BlockedModsDialog *ui; Ui::BlockedModsDialog *ui;
const QList<QUrl> &urls; QList<BlockedMod> &mods;
QFileSystemWatcher watcher;
shared_qobject_ptr<ConcurrentTask> hashing_task;
void openAll(); void openAll();
void update();
void directoryChanged(QString path);
void setupWatch();
void scanPaths();
void scanPath(QString path);
void checkMatchHash(QString hash, QString path);
bool checkValidPath(QString path);
bool allModsMatched();
}; };
QDebug operator<<(QDebug debug, const BlockedMod &m);

View File

@ -13,8 +13,8 @@
<property name="windowTitle"> <property name="windowTitle">
<string notr="true">BlockedModsDialog</string> <string notr="true">BlockedModsDialog</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item row="0" column="0"> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string notr="true"/> <string notr="true"/>
@ -24,17 +24,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTextBrowser" name="textBrowser"> <widget class="QTextBrowser" name="textBrowser">
<property name="acceptRichText"> <property name="acceptRichText">
<bool>true</bool> <bool>true</bool>
@ -44,6 +34,27 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="bottomBoxH">
<item>
<widget class="QLabel" name="labelModsFound">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -44,7 +44,6 @@
#include "BaseVersion.h" #include "BaseVersion.h"
#include "icons/IconList.h" #include "icons/IconList.h"
#include "tasks/Task.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include "InstanceList.h" #include "InstanceList.h"
@ -78,8 +77,14 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
} }
ui->groupBox->setCurrentIndex(index); ui->groupBox->setCurrentIndex(index);
ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
ui->copySavesCheckbox->setChecked(m_copySaves); ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled());
ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.isKeepPlaytimeEnabled());
ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.isCopyGameOptionsEnabled());
ui->copyResPacksCheckbox->setChecked(m_selectedOptions.isCopyResourcePacksEnabled());
ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled());
ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled());
ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled());
ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled());
} }
CopyInstanceDialog::~CopyInstanceDialog() CopyInstanceDialog::~CopyInstanceDialog()
@ -117,6 +122,31 @@ QString CopyInstanceDialog::instGroup() const
return ui->groupBox->currentText(); return ui->groupBox->currentText();
} }
const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const
{
return m_selectedOptions;
}
void CopyInstanceDialog::checkAllCheckboxes(const bool& b)
{
ui->keepPlaytimeCheckbox->setChecked(b);
ui->copySavesCheckbox->setChecked(b);
ui->copyGameOptionsCheckbox->setChecked(b);
ui->copyResPacksCheckbox->setChecked(b);
ui->copyShaderPacksCheckbox->setChecked(b);
ui->copyServersCheckbox->setChecked(b);
ui->copyModsCheckbox->setChecked(b);
ui->copyScreenshotsCheckbox->setChecked(b);
}
// Check the "Select all" checkbox if all options are already selected:
void CopyInstanceDialog::updateSelectAllCheckbox()
{
ui->selectAllCheckbox->blockSignals(true);
ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue());
ui->selectAllCheckbox->blockSignals(false);
}
void CopyInstanceDialog::on_iconButton_clicked() void CopyInstanceDialog::on_iconButton_clicked()
{ {
IconPickerDialog dlg(this); IconPickerDialog dlg(this);
@ -129,42 +159,64 @@ void CopyInstanceDialog::on_iconButton_clicked()
} }
} }
void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
{ {
updateDialogState(); updateDialogState();
} }
bool CopyInstanceDialog::shouldCopySaves() const void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state)
{ {
return m_copySaves; bool checked;
checked = (state == Qt::Checked);
checkAllCheckboxes(checked);
} }
void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
{ {
if(state == Qt::Unchecked) m_selectedOptions.enableCopySaves(state == Qt::Checked);
{ updateSelectAllCheckbox();
m_copySaves = false;
}
else if(state == Qt::Checked)
{
m_copySaves = true;
}
}
bool CopyInstanceDialog::shouldKeepPlaytime() const
{
return m_keepPlaytime;
} }
void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
{ {
if(state == Qt::Unchecked) m_selectedOptions.enableKeepPlaytime(state == Qt::Checked);
{ updateSelectAllCheckbox();
m_keepPlaytime = false; }
}
else if(state == Qt::Checked) void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state)
{ {
m_keepPlaytime = true; m_selectedOptions.enableCopyGameOptions(state == Qt::Checked);
} updateSelectAllCheckbox();
}
void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopyResourcePacks(state == Qt::Checked);
updateSelectAllCheckbox();
}
void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopyShaderPacks(state == Qt::Checked);
updateSelectAllCheckbox();
}
void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopyServers(state == Qt::Checked);
updateSelectAllCheckbox();
}
void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopyMods(state == Qt::Checked);
updateSelectAllCheckbox();
}
void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state)
{
m_selectedOptions.enableCopyScreenshots(state == Qt::Checked);
updateSelectAllCheckbox();
} }

View File

@ -17,7 +17,7 @@
#include <QDialog> #include <QDialog>
#include "BaseVersion.h" #include "BaseVersion.h"
#include <BaseInstance.h> #include "InstanceCopyPrefs.h"
class BaseInstance; class BaseInstance;
@ -39,20 +39,29 @@ public:
QString instName() const; QString instName() const;
QString instGroup() const; QString instGroup() const;
QString iconKey() const; QString iconKey() const;
bool shouldCopySaves() const; const InstanceCopyPrefs& getChosenOptions() const;
bool shouldKeepPlaytime() const;
private private
slots: slots:
void on_iconButton_clicked(); void on_iconButton_clicked();
void on_instNameTextBox_textChanged(const QString &arg1); void on_instNameTextBox_textChanged(const QString &arg1);
// Checkboxes
void on_selectAllCheckbox_stateChanged(int state);
void on_copySavesCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state);
void on_keepPlaytimeCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state);
void on_copyGameOptionsCheckbox_stateChanged(int state);
void on_copyResPacksCheckbox_stateChanged(int state);
void on_copyShaderPacksCheckbox_stateChanged(int state);
void on_copyServersCheckbox_stateChanged(int state);
void on_copyModsCheckbox_stateChanged(int state);
void on_copyScreenshotsCheckbox_stateChanged(int state);
private: private:
void checkAllCheckboxes(const bool& b);
void updateSelectAllCheckbox();
/* data */
Ui::CopyInstanceDialog *ui; Ui::CopyInstanceDialog *ui;
QString InstIconKey; QString InstIconKey;
InstancePtr m_original; InstancePtr m_original;
bool m_copySaves = true; InstanceCopyPrefs m_selectedOptions;
bool m_keepPlaytime = true;
}; };

View File

@ -9,8 +9,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>345</width> <width>341</width>
<height>323</height> <height>399</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -33,7 +33,7 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>60</width>
<height>20</height> <height>20</height>
</size> </size>
</property> </property>
@ -60,7 +60,7 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>60</width>
<height>20</height> <height>20</height>
</size> </size>
</property> </property>
@ -83,7 +83,10 @@
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="groupDropdownLayout">
<property name="verticalSpacing">
<number>6</number>
</property>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="labelVersion_3"> <widget class="QLabel" name="labelVersion_3">
<property name="text"> <property name="text">
@ -110,18 +113,96 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="copySavesCheckbox"> <layout class="QHBoxLayout" name="selectAllButtonLayout">
<property name="text"> <item>
<string>Copy saves</string> <widget class="QCheckBox" name="selectAllCheckbox">
</property> <property name="sizePolicy">
</widget> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Select all</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="keepPlaytimeCheckbox"> <layout class="QGridLayout" name="copyOptionsLayout">
<property name="text"> <item row="6" column="1">
<string>Keep play time</string> <widget class="QCheckBox" name="copyModsCheckbox">
</property> <property name="toolTip">
</widget> <string>Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs.</string>
</property>
<property name="text">
<string>Copy mods</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="copyGameOptionsCheckbox">
<property name="toolTip">
<string>Copy the in-game options like FOV, max framerate, etc.</string>
</property>
<property name="text">
<string>Copy game options</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="copySavesCheckbox">
<property name="text">
<string>Copy saves</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="copyShaderPacksCheckbox">
<property name="text">
<string>Copy shader packs</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="copyServersCheckbox">
<property name="text">
<string>Copy servers</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="copyResPacksCheckbox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Copy resource packs</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="keepPlaytimeCheckbox">
<property name="text">
<string>Keep play time</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="copyScreenshotsCheckbox">
<property name="text">
<string>Copy screenshots</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
@ -139,8 +220,6 @@
<tabstop>iconButton</tabstop> <tabstop>iconButton</tabstop>
<tabstop>instNameTextBox</tabstop> <tabstop>instNameTextBox</tabstop>
<tabstop>groupBox</tabstop> <tabstop>groupBox</tabstop>
<tabstop>copySavesCheckbox</tabstop>
<tabstop>keepPlaytimeCheckbox</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../graphics.qrc"/> <include location="../../graphics.qrc"/>
@ -153,8 +232,8 @@
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
<x>248</x> <x>254</x>
<y>254</y> <y>316</y>
</hint> </hint>
<hint type="destinationlabel"> <hint type="destinationlabel">
<x>157</x> <x>157</x>
@ -169,8 +248,8 @@
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
<x>316</x> <x>322</x>
<y>260</y> <y>316</y>
</hint> </hint>
<hint type="destinationlabel"> <hint type="destinationlabel">
<x>286</x> <x>286</x>

View File

@ -39,13 +39,12 @@
#include <MMCZip.h> #include <MMCZip.h>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <qfilesystemmodel.h> #include <QFileSystemModel>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QDebug> #include <QDebug>
#include <qstack.h>
#include <QSaveFile> #include <QSaveFile>
#include "MMCStrings.h" #include "StringUtils.h"
#include "SeparatorPrefixTree.h" #include "SeparatorPrefixTree.h"
#include "Application.h" #include "Application.h"
#include <icons/IconList.h> #include <icons/IconList.h>
@ -85,7 +84,7 @@ public:
// sort and proxy model breaks the original model... // sort and proxy model breaks the original model...
if (sortColumn() == 0) if (sortColumn() == 0)
{ {
return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
Qt::CaseInsensitive) < 0; Qt::CaseInsensitive) < 0;
} }
if (sortColumn() == 1) if (sortColumn() == 1)
@ -94,7 +93,7 @@ public:
auto rightSize = rightFileInfo.size(); auto rightSize = rightFileInfo.size();
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
{ {
return Strings::naturalCompare(leftFileInfo.fileName(), return StringUtils::naturalCompare(leftFileInfo.fileName(),
rightFileInfo.fileName(), rightFileInfo.fileName(),
Qt::CaseInsensitive) < 0 Qt::CaseInsensitive) < 0
? asc ? asc

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* 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
@ -131,6 +132,8 @@ QList<BasePage*> ModDownloadDialog::getPages()
if (APPLICATION->capabilities() & Application::SupportsFlame) if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameModPage::create(this, m_instance)); pages.append(FlameModPage::create(this, m_instance));
m_selectedPage = dynamic_cast<ModPage*>(pages[0]);
return pages; return pages;
} }
@ -178,12 +181,22 @@ void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* select
return; return;
} }
auto* selected_page = dynamic_cast<ModPage*>(selected); m_selectedPage = dynamic_cast<ModPage*>(selected);
if (!selected_page) { if (!m_selectedPage) {
qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!";
return; return;
} }
// Same effect as having a global search bar // Same effect as having a global search bar
selected_page->setSearchTerm(prev_page->getSearchTerm()); m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
}
bool ModDownloadDialog::selectPage(QString pageId)
{
return m_container->selectPage(pageId);
}
ModPage* ModDownloadDialog::getSelectedPage()
{
return m_selectedPage;
} }

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* 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
@ -32,13 +33,14 @@ class ModDownloadDialog;
class PageContainer; class PageContainer;
class QDialogButtonBox; class QDialogButtonBox;
class ModPage;
class ModrinthModPage; class ModrinthModPage;
class ModDownloadDialog final : public QDialog, public BasePageProvider class ModDownloadDialog final : public QDialog, public BasePageProvider
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
~ModDownloadDialog() override = default; ~ModDownloadDialog() override = default;
@ -51,22 +53,26 @@ public:
bool isModSelected(QString name) const; bool isModSelected(QString name) const;
const QList<ModDownloadTask*> getTasks(); const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods; const std::shared_ptr<ModFolderModel>& mods;
public slots: bool selectPage(QString pageId);
ModPage* getSelectedPage();
public slots:
void confirm(); void confirm();
void accept() override; void accept() override;
void reject() override; void reject() override;
private slots: private slots:
void selectedPageChanged(BasePage* previous, BasePage* selected); void selectedPageChanged(BasePage* previous, BasePage* selected);
private: private:
Ui::ModDownloadDialog *ui = nullptr; Ui::ModDownloadDialog* ui = nullptr;
PageContainer * m_container = nullptr; PageContainer* m_container = nullptr;
QDialogButtonBox * m_buttons = nullptr; QDialogButtonBox* m_buttons = nullptr;
QVBoxLayout *m_verticalLayout = nullptr; QVBoxLayout* m_verticalLayout = nullptr;
ModPage* m_selectedPage = nullptr;
QHash<QString, ModDownloadTask*> modTask; QHash<QString, ModDownloadTask*> modTask;
BaseInstance *m_instance; BaseInstance* m_instance;
}; };

View File

@ -27,11 +27,7 @@
<item row="4" column="1" colspan="3"> <item row="4" column="1" colspan="3">
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="filterEdit"> <widget class="QLineEdit" name="filterEdit"/>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="filterLabel"> <widget class="QLabel" name="filterLabel">

View File

@ -48,11 +48,7 @@
<item> <item>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="filterEdit"> <widget class="QLineEdit" name="filterEdit"/>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="filterLabel"> <widget class="QLabel" name="filterLabel">

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* 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
@ -37,7 +38,9 @@
#include "Application.h" #include "Application.h"
#include "ui_ModPage.h" #include "ui_ModPage.h"
#include <QDesktopServices>
#include <QKeyEvent> #include <QKeyEvent>
#include <QRegularExpression>
#include <memory> #include <memory>
#include <HoeDown.h> #include <HoeDown.h>
@ -80,6 +83,8 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packView->installEventFilter(this); ui->packView->installEventFilter(this);
connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl);
} }
ModPage::~ModPage() ModPage::~ModPage()
@ -158,8 +163,8 @@ void ModPage::triggerSearch()
{ {
auto changed = m_filter_widget->changed(); auto changed = m_filter_widget->changed();
m_filter = m_filter_widget->getFilter(); m_filter = m_filter_widget->getFilter();
if(changed){ if (changed) {
ui->packView->clearSelection(); ui->packView->clearSelection();
ui->packDescription->clear(); ui->packDescription->clear();
ui->versionSelectionBox->clear(); ui->versionSelectionBox->clear();
@ -241,6 +246,79 @@ void ModPage::onModSelected()
ui->packView->adjustSize(); ui->packView->adjustSize();
} }
static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"));
static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"));
static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"));
void ModPage::openUrl(const QUrl& url)
{
// do not allow other url schemes for security reasons
if (!(url.scheme() == "http" || url.scheme() == "https")) {
qWarning() << "Unsupported scheme" << url.scheme();
return;
}
// detect mod URLs and search instead
const QString address = url.host() + url.path();
QRegularExpressionMatch match;
const char* page;
match = modrinth.match(address);
if (match.hasMatch())
page = "modrinth";
else if (APPLICATION->capabilities() & Application::SupportsFlame) {
match = curseForge.match(address);
if (!match.hasMatch())
match = curseForgeOld.match(address);
if (match.hasMatch())
page = "curseforge";
}
if (match.hasMatch()) {
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
if (slug != current.slug) {
dialog->selectPage(page);
ModPage* newPage = dialog->getSelectedPage();
QLineEdit* searchEdit = newPage->ui->searchEdit;
ModPlatform::ListModel* model = newPage->listModel;
QListView* view = newPage->ui->packView;
auto jump = [url, slug, model, view] {
for (int row = 0; row < model->rowCount({}); row++) {
const QModelIndex index = model->index(row);
const auto pack = model->data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
if (pack.slug == slug) {
view->setCurrentIndex(index);
return;
}
}
// The final fallback.
QDesktopServices::openUrl(url);
};
searchEdit->setText(slug);
newPage->triggerSearch();
if (model->activeJob())
connect(model->activeJob(), &Task::finished, jump);
else
jump();
return;
}
}
// open in the user's web browser
QDesktopServices::openUrl(url);
}
/******** Make changes to the UI ********/ /******** Make changes to the UI ********/
@ -270,8 +348,8 @@ void ModPage::updateModVersions(int prev_count)
if ((valid || m_filter->versions.empty()) && !optedOut(version)) if ((valid || m_filter->versions.empty()) && !optedOut(version))
ui->versionSelectionBox->addItem(version.version, QVariant(i)); ui->versionSelectionBox->addItem(version.version, QVariant(i));
} }
if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { if (ui->versionSelectionBox->count() == 0 && prev_count != 0) {
ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); ui->modSelectionButton->setText(tr("Cannot select invalid version :("));
} }
@ -317,8 +395,7 @@ void ModPage::updateUi()
text += "<br>" + tr(" by ") + authorStrs.join(", "); text += "<br>" + tr(" by ") + authorStrs.join(", ");
} }
if (current.extraDataLoaded) {
if(current.extraDataLoaded) {
if (!current.extraData.donate.isEmpty()) { if (!current.extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: "); text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {

View File

@ -82,6 +82,7 @@ class ModPage : public QWidget, public BasePage {
void onSelectionChanged(QModelIndex first, QModelIndex second); void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data); void onVersionSelectionChanged(QString data);
void onModSelected(); void onModSelected();
virtual void openUrl(const QUrl& url);
protected: protected:
Ui::ModPage* ui = nullptr; Ui::ModPage* ui = nullptr;

View File

@ -16,10 +16,10 @@
<item row="1" column="2"> <item row="1" column="2">
<widget class="ProjectDescriptionPage" name="packDescription"> <widget class="ProjectDescriptionPage" name="packDescription">
<property name="openExternalLinks"> <property name="openExternalLinks">
<bool>true</bool> <bool>false</bool>
</property> </property>
<property name="openLinks"> <property name="openLinks">
<bool>true</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -20,7 +20,8 @@
#include <modplatform/atlauncher/ATLPackIndex.h> #include <modplatform/atlauncher/ATLPackIndex.h>
#include <Version.h> #include <Version.h>
#include <MMCStrings.h>
#include "StringUtils.h"
namespace Atl { namespace Atl {
@ -86,7 +87,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co
return lv < rv; return lv < rv;
} }
else if (currentSorting == ByName) { else if (currentSorting == ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
} }
// Invalid sorting set, somehow... // Invalid sorting set, somehow...

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* 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
@ -39,7 +40,7 @@
#include "FlameModModel.h" #include "FlameModModel.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
: ModPage(dialog, instance, new FlameAPI()) : ModPage(dialog, instance, new FlameAPI())
{ {
listModel = new FlameMod::ListModel(this); listModel = new FlameMod::ListModel(this);
@ -53,7 +54,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
ui->sortByBox->addItem(tr("Sort by Author")); ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Downloads"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor... // so it's best not to connect them in the parent's contructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged);
@ -78,3 +79,19 @@ bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const
// other mod providers start loading before being selected, at least with // other mod providers start loading before being selected, at least with
// my Qt, so we need to implement this in every derived class... // my Qt, so we need to implement this in every derived class...
auto FlameModPage::shouldDisplay() const -> bool { return true; } auto FlameModPage::shouldDisplay() const -> bool { return true; }
void FlameModPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
QString query = url.query(QUrl::FullyDecoded);
if (query.startsWith("remoteUrl=")) {
// attempt to resolve url from warning page
query.remove(0, 10);
ModPage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary
return;
}
}
ModPage::openUrl(url);
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* *
* 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
@ -64,4 +65,6 @@ class FlameModPage : public ModPage {
bool optedOut(ModPlatform::IndexedVersion& ver) const override; bool optedOut(ModPlatform::IndexedVersion& ver) const override;
auto shouldDisplay() const -> bool override; auto shouldDisplay() const -> bool override;
void openUrl(const QUrl& url) override;
}; };

View File

@ -3,7 +3,6 @@
#include "Application.h" #include "Application.h"
#include "ui/widgets/ProjectItem.h" #include "ui/widgets/ProjectItem.h"
#include <MMCStrings.h>
#include <Version.h> #include <Version.h>
#include <QtMath> #include <QtMath>

View File

@ -19,7 +19,8 @@
#include <QDebug> #include <QDebug>
#include "modplatform/modpacksch/FTBPackManifest.h" #include "modplatform/modpacksch/FTBPackManifest.h"
#include <MMCStrings.h>
#include "StringUtils.h"
namespace Ftb { namespace Ftb {
@ -81,7 +82,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co
return leftPack.installs < rightPack.installs; return leftPack.installs < rightPack.installs;
} }
else if (currentSorting == ByName) { else if (currentSorting == ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
} }
// Invalid sorting set, somehow... // Invalid sorting set, somehow...

View File

@ -36,7 +36,7 @@
#include "ListModel.h" #include "ListModel.h"
#include "Application.h" #include "Application.h"
#include <MMCStrings.h> #include "StringUtils.h"
#include <Version.h> #include <Version.h>
#include <QtMath> #include <QtMath>
@ -66,7 +66,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co
return lv < rv; return lv < rv;
} else if(currentSorting == Sorting::ByName) { } else if(currentSorting == Sorting::ByName) {
return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
} }
//UHM, some inavlid value set?! //UHM, some inavlid value set?!

View File

@ -53,7 +53,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan
ui->sortByBox->addItem(tr("Sort by Last Updated")); ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Newest")); ui->sortByBox->addItem(tr("Sort by Newest"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's constructor... // so it's best not to connect them in the parent's constructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged);

@ -1 +1 @@
Subproject commit 4b166b69f28e70a416a1a04a98f365d2aeb90de8 Subproject commit cc741c9f5f2a62856a2a2e9e275f61eb0591c09c

View File

@ -110,6 +110,142 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@"
VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@"
VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@"
;--------------------------------
; Shell Associate Macros
!macro APP_SETUP DESCRIPTION ICON APP_ID APP_NAME APP_EXE COMMANDTEXT COMMAND ; VERB APP_COMPANY
; setup APP_ID
WriteRegStr ShCtx "Software\Classes\${APP_ID}" "" `${DESCRIPTION}`
WriteRegStr ShCtx "Software\Classes\${APP_ID}\DefaultIcon" "" `${ICON}`
; default open verb
WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell" "" "open"
WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open\command" "" `${COMMAND}`
; if you want the app to use it's own implementation of a verb
;WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\${VERB}" "" "${DESCRIPTION}"
;WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\${VERB}\command" "" `${COMMAND}`
WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\shell\open\command" "" `${COMMAND}`
WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}" "FriendlyAppName" `${APP_NAME}` ; [Optional]
;WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}" "ApplicationCompany" `${APP_COMPANY}` ; [Optional]
;WriteRegNone ShCtx "Software\Classes\Applications\${APP_EXE}\SupportedTypes" ".${EXT}" ; [Optional] Only allow "Open With" with specific extension(s) on WinXP+
# Register "Default Programs" [Optional]
!ifdef REGISTER_DEFAULTPROGRAMS
WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" "ApplicationDescription" `${DESCRIPTION}`
WriteRegStr ShCtx "Software\RegisteredApplications" `${APP_NAME}` "Software\Classes\Applications\${APP_EXE}\Capabilities"
!endif
!macroend
!macro APP_ASSOCIATE EXT APP_ID APP_EXE OVERWIRTE
; Backup the previously associated file class
${If} ${OVERWIRTE} == true
ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" ""
WriteRegStr ShCtx "Software\Classes\${EXT}" "${APP_ID}_backup" "$R0"
WriteRegStr ShCtx "Software\Classes\${EXT}" "" "${APP_ID}"
${EndIf}
WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" ; Win2000+
WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" ; WinXP+
# Register "Default Programs" [Optional]
!ifdef REGISTER_DEFAULTPROGRAMS
WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities\FileAssociations" "${EXT}" "${APP_ID}"
!endif
!macroend
!macro APP_UNASSOCIATE EXT APP_ID
# Unregister file type
ClearErrors
; restore backup
ReadRegStr $R1 ShCtx "Software\Classes\${EXT}" ""
${If} $R1 == "${APP_ID}"
ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" `${APP_ID}_backup`
WriteRegStr ShCtx "Software\Classes\${EXT}" "" "$R0"
${Else}
ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" ""
${EndIf}
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}"
${IfNot} ${Errors}
${AndIf} $R0 == "${APP_ID}"
DeleteRegValue ShCtx "Software\Classes\${EXT}" ""
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}"
${EndIf}
DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}"
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithList"
DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}"
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithProgids"
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}"
# Attempt to clean up junk left behind by the Windows shell
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "${APP_ID}_${EXT}"
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "Applications\${APP_EXE}_${EXT}"
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids" "${APP_ID}"
DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids"
DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithList"
DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}"
;DeleteRegKey HKCU "Software\Microsoft\Windows\Roaming\OpenWith\FileExts\.${EXT}"
;DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs\.${EXT}"
!macroend
!macro APP_TEARDOWN APP_ID APP_NAME APP_EXE
# Unregister file type
ClearErrors
DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}\shell"
${IfNot} ${Errors}
DeleteRegKey ShCtx "Software\Classes\${APP_ID}\DefaultIcon"
${EndIf}
# Unregister "Open With"
DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}"
# Unregister "Default Programs"
!ifdef REGISTER_DEFAULTPROGRAMS
DeleteRegValue ShCtx "Software\RegisteredApplications" `${APP_NAME}`
DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities"
DeleteRegKey /IfEmpty ShCtx "Software\Classes\Applications\${APP_EXE}"
!endif
DeleteRegKey ShCtx `Software\Classes\${APP_ID}`
DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}"
# Attempt to clean up junk left behind by the Windows shell
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Search\JumplistData" "$INSTDIR\${APP_EXE}"
DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.FriendlyAppName"
DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.ApplicationCompany"
DeleteRegValue HKCU "Software\Microsoft\Windows\ShellNoRoam\MUICache" "$INSTDIR\${APP_EXE}" ; WinXP
DeleteRegValue HKCU "Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Compatibility Assistant\Store" "$INSTDIR\${APP_EXE}"
!macroend
; !defines for use with SHChangeNotify
!ifdef SHCNE_ASSOCCHANGED
!undef SHCNE_ASSOCCHANGED
!endif
!define SHCNE_ASSOCCHANGED 0x08000000
!ifdef SHCNF_FLUSH
!undef SHCNF_FLUSH
!endif
!define SHCNF_FLUSH 0x1000
# ensure this is called at the end of any section that changes shell keys
!macro NotifyShell_AssocChanged
; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
; can update the shell.
System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
!macroend
;-------------------------------- ;--------------------------------
; The stuff to install ; The stuff to install
@ -171,6 +307,27 @@ Section /o "Desktop Shortcut" DESKTOP_SHORTCUTS
SectionEnd SectionEnd
!define APP_ID "@Launcher_CommonName@.App"
!define APP_EXE "@Launcher_APP_BINARY_NAME@.exe"
!define APP_ICON "$INSTDIR\${APP_EXE},0"
!define APP_DESCRIPTION "@Launcher_DisplayName@"
!define APP_NAME "@Launcher_DisplayName@"
!define APP_CMD_TEXT "Minecraft Modpack"
!define REGISTER_DEFAULTPROGRAMS ; value doesn't matter
Section -ShellAssoc
!insertmacro APP_SETUP `${APP_DESCRIPTION}` `${APP_ICON}` `${APP_ID}` `${APP_CMD_TEXT}` `${APP_EXE}` `${APP_CMD_TEXT}` '$INSTDIR\${APP_EXE} -I "%1"'
!insertmacro APP_ASSOCIATE ".zip" `${APP_ID}` `${APP_EXE}` false
!insertmacro APP_ASSOCIATE ".mrpack" `${APP_ID}` `${APP_EXE}` true
!insertmacro NotifyShell_AssocChanged
SectionEnd
;-------------------------------- ;--------------------------------
; Uninstaller ; Uninstaller
@ -202,6 +359,16 @@ Section "Uninstall"
SectionEnd SectionEnd
Section -un.ShellAssoc
!insertmacro APP_TEARDOWN `${APP_ID}` `${APP_NAME}` `${APP_EXE}`
!insertmacro APP_UNASSOCIATE ".zip" `${APP_ID}`
!insertmacro APP_UNASSOCIATE ".mrpack" `${APP_ID}`
!insertmacro NotifyShell_AssocChanged
SectionEnd
;-------------------------------- ;--------------------------------
; Extra command line parameters ; Extra command line parameters