diff options
59 files changed, 1283 insertions, 259 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 22692dae..0db05f98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 version numbers ######## -set(Launcher_VERSION_MAJOR 5) +set(Launcher_VERSION_MAJOR 6) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") @@ -9,7 +9,7 @@ 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 @@ -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"> </a> -- 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). +- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download/). +- Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions). ### 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. -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: -[![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: -[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge)](https://matrix.to/#/#prismlauncher:matrix.org) +#### 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&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org) #### 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 -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 @@ -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. -![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. diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 579942f4..45cd9422 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -563,7 +563,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); - m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, suitableMaxMem()); m_settings->registerSetting("PermGen", 128); // Java Settings @@ -611,6 +611,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); + m_settings->registerSetting("ToolbarsLocked", false); + m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("SelectedInstance", QString()); @@ -1589,3 +1591,17 @@ QString Application::getUserAgentUncached() 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; +} diff --git a/launcher/Application.h b/launcher/Application.h index 78ab8fbd..4c2f62d4 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -200,6 +200,8 @@ public: void ShowGlobalSettings(class QWidget * parent, QString open_page = QString()); + int suitableMaxMem(); + signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index bc1f5d5e..8db93429 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -24,13 +24,15 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp - MMCStrings.h - MMCStrings.cpp + StringUtils.h + StringUtils.cpp RuntimeContext.h # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h InstanceCreationTask.cpp + InstanceCopyPrefs.h + InstanceCopyPrefs.cpp InstanceCopyTask.h InstanceCopyTask.cpp InstanceImportTask.h @@ -1064,7 +1066,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Image formats install( DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "tga|tiff|mng" EXCLUDE @@ -1082,7 +1084,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Icon engines install( DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "fontawesome" EXCLUDE @@ -1100,7 +1102,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Platform plugins install( DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "minimal|linuxfb|offscreen" EXCLUDE @@ -1119,7 +1121,7 @@ if(INSTALL_BUNDLE STREQUAL "full") if(EXISTS "${QT_PLUGINS_DIR}/styles") install( DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime ) @@ -1137,7 +1139,7 @@ if(INSTALL_BUNDLE STREQUAL "full") if(EXISTS "${QT_PLUGINS_DIR}/tls") install( DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime ) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 4026d6c1..4a8f4bd3 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -44,7 +44,9 @@ #include <QStandardPaths> #include <QTextStream> #include <QUrl> + #include "DesktopServices.h" +#include "StringUtils.h" #if defined Q_OS_WIN32 #include <objbase.h> @@ -79,22 +81,6 @@ namespace fs = std::filesystem; namespace fs = ghc::filesystem; #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 { void ensureExists(const QDir& dir) @@ -163,6 +149,9 @@ bool ensureFolderPathExists(QString foldernamepath) 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) { using copy_opts = fs::copy_options; @@ -191,7 +180,7 @@ bool copy::operator()(const QString& offset) auto dst_path = PathCombine(dst, relative_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) { qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); 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 (!fs::is_directory(toStdString(src))) + if (!fs::is_directory(StringUtils::toStdString(src))) copy_file(src, ""); return err.value() == 0; @@ -223,7 +212,7 @@ bool deletePath(QString path) { std::error_code err; - fs::remove_all(toStdString(path), err); + fs::remove_all(StringUtils::toStdString(path), err); if (err) { 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; // 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) { qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b46f3281..b7e175fd 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -75,6 +75,7 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); +/// @brief Copies a directory and it's contents from src to dest class copy { public: copy(const QString& src, const QString& dst) diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp new file mode 100644 index 00000000..7b93a516 --- /dev/null +++ b/launcher/InstanceCopyPrefs.cpp @@ -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; +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h new file mode 100644 index 00000000..6988b2df --- /dev/null +++ b/launcher/InstanceCopyPrefs.h @@ -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; +}; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index b1e33884..a4ea947d 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -5,15 +5,17 @@ #include "pathmatcher/RegexpMatcher.h" #include <QtConcurrentRun> -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime) +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { 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... - auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); + auto matcherReal = new RegexpMatcher(filters); matcherReal->caseSensitive(false); m_matcher.reset(matcherReal); } diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 82901732..1f29b854 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -1,20 +1,21 @@ #pragma once -#include "tasks/Task.h" -#include "net/NetJob.h" -#include <QUrl> #include <QFuture> #include <QFutureWatcher> -#include "settings/SettingsObject.h" -#include "BaseVersion.h" +#include <QUrl> #include "BaseInstance.h" +#include "BaseVersion.h" +#include "InstanceCopyPrefs.h" #include "InstanceTask.h" +#include "net/NetJob.h" +#include "settings/SettingsObject.h" +#include "tasks/Task.h" class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime); + explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. @@ -22,7 +23,8 @@ protected: void copyFinished(); void copyAborted(); -private: /* data */ +private: + /* data */ InstancePtr m_origInstance; QFuture<bool> m_copyFuture; QFutureWatcher<bool> m_copyFutureWatcher; diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index aa4d1123..52cc868a 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -36,7 +36,7 @@ #include "JavaCommon.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" -#include <MMCStrings.h> + #include <QRegularExpression> bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) diff --git a/launcher/MMCStrings.h b/launcher/MMCStrings.h deleted file mode 100644 index 48052a00..00000000 --- a/launcher/MMCStrings.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include <QString> - -namespace Strings -{ - int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); -} diff --git a/launcher/MMCStrings.cpp b/launcher/StringUtils.cpp index dc91c8d6..0f3c3669 100644 --- a/launcher/MMCStrings.cpp +++ b/launcher/StringUtils.cpp @@ -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 -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(); } /// 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 QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); while (c2.isSpace()) c2 = getNextChar(s2, ++l2); - if (c1.isDigit() && c2.isDigit()) - { + if (c1.isDigit() && c2.isDigit()) { while (c1.digitValue() == 0) c1 = getNextChar(s1, ++l1); while (c2.digitValue() == 0) @@ -30,11 +32,8 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit int lookAheadLocation2 = l2; int currentReturnValue = 0; // find the last digit, setting currentReturnValue as we go if it isn't equal - for (QChar lookAhead1 = c1, lookAhead2 = c2; - (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); - lookAhead1 = getNextChar(s1, ++lookAheadLocation1), - lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) - { + for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) { bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); if (!is1ADigit && !is2ADigit) @@ -43,14 +42,10 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit return -1; if (!is2ADigit) return 1; - if (currentReturnValue == 0) - { - if (lookAhead1 < lookAhead2) - { + if (currentReturnValue == 0) { + if (lookAhead1 < lookAhead2) { currentReturnValue = -1; - } - else if (lookAhead1 > lookAhead2) - { + } else if (lookAhead1 > lookAhead2) { currentReturnValue = 1; } } @@ -58,19 +53,24 @@ int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensit if (currentReturnValue != 0) return currentReturnValue; } - if (cs == Qt::CaseInsensitive) - { + + if (cs == Qt::CaseInsensitive) { if (!c1.isLower()) c1 = c1.toLower(); if (!c2.isLower()) c2 = c2.toLower(); } + int r = QString::localeAwareCompare(c1, c2); if (r < 0) return -1; if (r > 0) return 1; + + l1 += 1; + l2 += 1; } + // The two strings are the same (02 == 2) so fall back to the normal sort return QString::compare(s1, s2, cs); } diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h new file mode 100644 index 00000000..1799605b --- /dev/null +++ b/launcher/StringUtils.h @@ -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 diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 5bcf7bcb..d5932bcb 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -1,9 +1,10 @@ #include "JavaInstall.h" -#include <MMCStrings.h> + +#include "StringUtils.h" 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) return archCompare < 0; if(id < rhs.id) @@ -14,7 +15,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs) { 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) diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 155c956a..e2f0aa00 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -41,7 +41,6 @@ #include "java/JavaInstallList.h" #include "java/JavaCheckerJob.h" #include "java/JavaUtils.h" -#include "MMCStrings.h" #include "minecraft/VersionFilterData.h" JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index 179ccd8d..0e4fc1d3 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -1,5 +1,6 @@ #include "JavaVersion.h" -#include <MMCStrings.h> + +#include "StringUtils.h" #include <QRegularExpression> #include <QString> @@ -98,12 +99,12 @@ bool JavaVersion::operator<(const JavaVersion &rhs) else if(thisPre && rhsPre) { // 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 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) diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 28fcc4f4..9e1794b3 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,7 +37,6 @@ #include "launch/LaunchTask.h" #include "MessageLevel.h" -#include "MMCStrings.h" #include "java/JavaChecker.h" #include "tasks/Task.h" #include <QDebug> diff --git a/launcher/main.cpp b/launcher/main.cpp index e2116f38..df596449 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -91,5 +91,7 @@ int main(int argc, char *argv[]) return 1; case Application::Succeeded: return 0; + default: + return -1; } } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3a820951..de22b365 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -43,7 +43,6 @@ #include "settings/SettingsObject.h" #include "Application.h" -#include "MMCStrings.h" #include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/MultiMatcher.h" #include "FileSystem.h" diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 4b878918..cc4e252c 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -36,7 +36,7 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& } #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 } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 48ac02e0..f0fbdc96 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -372,13 +372,20 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) auto results = m_mod_id_resolver->getResults(); // first check for blocked mods - QString text; - QList<QUrl> urls; + QList<BlockedMod> blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { 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; } } @@ -388,11 +395,12 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), tr("The following mods were blocked on third party launchers.<br/>" "You will need to manually download them and add them to the modpack"), - text, - urls); + blocked_mods); message_dialog->setModal(true); if (message_dialog->exec()) { + qDebug() << "Post dialog blocked mods list: " << blocked_mods; + copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { 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) { m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); @@ -449,7 +489,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) m_files_job.reset(); 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); setStatus(tr("Downloading mods...")); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index ded0e2ce..fbc7d5bf 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -10,6 +10,8 @@ #include "net/NetJob.h" +#include "ui/dialogs/BlockedModsDialog.h" + class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT @@ -29,6 +31,7 @@ class FlameCreationTask final : public InstanceCreationTask { private slots: void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); + void copyBlockedMods(QList<BlockedMod> const& blocked_mods); private: QWidget* m_parent = nullptr; diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index a7bbaba5..f1e4759e 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -4,6 +4,7 @@ #include <QFile> #include "FileSystem.h" +#include "StringUtils.h" #include <MurmurHash2.h> @@ -35,6 +36,18 @@ Hasher::Ptr createFlameHasher(QString 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() { QFile file(m_path); @@ -66,7 +79,7 @@ void FlameHasher::executeTask() // CF-specific 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. // How do we make this non-blocking then? 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 diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 38fddf03..fa3244f6 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -40,8 +40,23 @@ class ModrinthHasher : public Hasher { 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 createFlameHasher(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 diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 7b112d8f..40aee82b 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -176,8 +176,6 @@ void PackInstallTask::resolveMods() void PackInstallTask::onResolveModsSucceeded() { - QString text; - QList<QUrl> urls; auto anyBlocked = false; Flame::Manifest results = m_mod_id_resolver_task->getResults(); @@ -191,11 +189,16 @@ void PackInstallTask::onResolveModsSucceeded() // First check for blocked mods if (!results_file.resolved || results_file.url.isEmpty()) { - QString type(local_file.type); - type[0] = type[0].toUpper(); - text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl); - urls.append(QUrl(results_file.websiteUrl)); + BlockedMod blocked_mod; + blocked_mod.name = local_file.name; + 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; } else { 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"), 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."), - text, - urls); + m_blocked_mods); - if (message_dialog->exec() == QDialog::Accepted) + if (message_dialog->exec() == QDialog::Accepted) { + qDebug() << "Post dialog blocked mods list: " << m_blocked_mods; createInstance(); - else + } + else { abort(); + } + } else { createInstance(); } @@ -320,6 +326,9 @@ void PackInstallTask::downloadPack() void PackInstallTask::onModDownloadSucceeded() { m_net_job.reset(); + if (!m_blocked_mods.isEmpty()) { + copyBlockedMods(); + } emitSucceeded(); } @@ -343,4 +352,35 @@ void PackInstallTask::onModDownloadFailed(QString 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 diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index 7c6fbeb9..2cd4d729 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -43,6 +43,7 @@ #include "QObjectPtr.h" #include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" +#include "ui/dialogs/BlockedModsDialog.h" #include <QWidget> @@ -76,6 +77,7 @@ private: void resolveMods(); void createInstance(); void downloadPack(); + void copyBlockedMods(); private: NetJob::Ptr m_net_job = nullptr; @@ -90,6 +92,7 @@ private: Version m_version; QMap<QString, QString> m_files_to_copy; + QList<BlockedMod> m_blocked_mods; //FIXME: nuke QWidget* m_parent; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index b1fe963e..0ed29311 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -22,10 +22,14 @@ #include <QDir> #include <QObject> -#include <toml++/toml.h> +#include "FileSystem.h" +#include "StringUtils.h" + #include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" +#include <toml++/toml.h> + namespace Packwiz { 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; // 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) { - 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 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) { - 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 {}; } @@ -145,6 +149,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) // they want to do! if (index_file.exists()) { index_file.remove(); + } else { + FS::ensureFilePathExists(index_file.fileName()); } if (!index_file.open(QIODevice::ReadWrite)) { @@ -228,14 +234,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod toml::table table; #if TOML_EXCEPTIONS 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) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(err.what()); return {}; } #else - table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); + table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); if (!table) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(table.error().what()); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 3ec80377..9754e5c4 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -24,7 +24,6 @@ #include <QUrl> #include <QVariant> -struct toml_table_t; class QDir; // 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 stringEntry(toml_table_t* parent, const char* entry_name) -> QString; -auto intEntry(toml_table_t* parent, const char* entry_name) -> int; - class V1 { public: struct Mod { diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 652e7084..e55faf15 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -4,12 +4,14 @@ <file alias="kitteh">kitteh.png</file> <file alias="kitteh-xmas">kitteh-xmas.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-xmas">rory-xmas.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-xmas">rory-flat-xmas.png</file> <file alias="rory-flat-bday">rory-flat-bday.png</file> + <file alias="rory-flat-spooky">rory-flat-spooky.png</file> </qresource> </RCC> diff --git a/launcher/resources/backgrounds/kitteh-ween.png b/launcher/resources/backgrounds/kitteh-spooky.png Binary files differindex deb0bebb..deb0bebb 100644 --- a/launcher/resources/backgrounds/kitteh-ween.png +++ b/launcher/resources/backgrounds/kitteh-spooky.png diff --git a/launcher/resources/backgrounds/rory-flat-spooky.png b/launcher/resources/backgrounds/rory-flat-spooky.png Binary files differnew file mode 100644 index 00000000..6360c612 --- /dev/null +++ b/launcher/resources/backgrounds/rory-flat-spooky.png diff --git a/launcher/resources/backgrounds/rory-spooky.png b/launcher/resources/backgrounds/rory-spooky.png Binary files differnew file mode 100644 index 00000000..a727619b --- /dev/null +++ b/launcher/resources/backgrounds/rory-spooky.png diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7140831b..9375039b 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -266,6 +266,8 @@ public: TranslatedAction actionNoAccountsAdded; TranslatedAction actionNoDefaultAccount; + TranslatedAction actionLockToolbars; + QVector<TranslatedToolButton *> all_toolbuttons; QWidget *centralWidget = nullptr; @@ -432,6 +434,12 @@ public: actionManageAccounts->setCheckable(false); actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); 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) @@ -439,7 +447,6 @@ public: mainToolBar = TranslatedToolbar(MainWindow); mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setMovable(true); mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mainToolBar->setFloatable(false); @@ -540,6 +547,8 @@ public: viewMenu->addAction(actionCAT); viewMenu->addSeparator(); + viewMenu->addAction(actionLockToolbars); + menuBar->addMenu(foldersMenu); profileMenu = menuBar->addMenu(tr("&Accounts")); @@ -620,7 +629,6 @@ public: { newsToolBar = TranslatedToolbar(MainWindow); newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setMovable(true); newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); newsToolBar->setIconSize(QSize(16, 16)); newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -755,7 +763,6 @@ public: instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); // disabled until we have an instance selected instanceToolBar->setEnabled(false); - instanceToolBar->setMovable(true); // Qt doesn't like vertical moving toolbars, so we have to force them... // See https://github.com/PolyMC/PolyMC/issues/493 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))); 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 connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated); @@ -1092,8 +1107,19 @@ QMenu * MainWindow::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() ); + + filteredMenu->addAction(ui->actionLockToolbars); + 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() { @@ -1583,8 +1609,8 @@ void MainWindow::setCatBackground(bool enabled) QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); if (non_stupid_abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; - } else if (cat == "kitteh" && non_stupid_abs(now.daysTo(halloween)) <= 4) { - cat += "-ween"; + } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) { + cat += "-spooky"; } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { cat += "-bday"; } @@ -1644,7 +1670,7 @@ void MainWindow::on_actionCopyInstance_triggered() if (!copyInstDlg.exec()) return; - auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime()); + auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.getChosenOptions()); copyTask->setName(copyInstDlg.instName()); copyTask->setGroup(copyInstDlg.instGroup()); copyTask->setIcon(copyInstDlg.iconKey()); @@ -1919,6 +1945,7 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { APPLICATION->metacache()->evictAll(); + APPLICATION->metacache()->SaveNow(); } #ifdef Q_OS_MAC diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index c9ab8bfb..6aeeccca 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -207,6 +207,8 @@ private slots: void globalSettingsClosed(); + void lockToolbars(bool); + #ifndef Q_OS_MAC void keyReleaseEvent(QKeyEvent *event) override; #endif diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index fe87b517..2cf94250 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,28 +1,186 @@ #include "BlockedModsDialog.h" -#include "ui_BlockedModsDialog.h" -#include <QPushButton> -#include <QDialogButtonBox> #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) : - QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) { +BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods) + : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) +{ ui->setupUi(this); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); 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); ui->label->setText(text); - ui->textBrowser->setText(body); + ui->labelModsFound->setText(tr("Please download the missing mods.")); + update(); } -BlockedModsDialog::~BlockedModsDialog() { +BlockedModsDialog::~BlockedModsDialog() +{ delete ui; } -void BlockedModsDialog::openAll() { - for(auto &url : urls) { - QDesktopServices::openUrl(url); +void BlockedModsDialog::openAll() +{ + 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) { + // ✔ -> html for HEAVY CHECK MARK : ✔ + span = QString(tr("<span style=\"color:green\"> ✔ Found at %1 </span>")).arg(mod.localPath); + } else { + // ✘ -> html for HEAVY BALLOT X : ✘ + span = QString(tr("<span style=\"color:red\"> ✘ 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; } diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 5f5bd61b..0a5c90db 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -1,7 +1,23 @@ #pragma once #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 namespace Ui { class BlockedModsDialog; } @@ -11,12 +27,27 @@ class BlockedModsDialog : public QDialog { Q_OBJECT 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; + private: Ui::BlockedModsDialog *ui; - const QList<QUrl> &urls; + QList<BlockedMod> &mods; + QFileSystemWatcher watcher; + shared_qobject_ptr<ConcurrentTask> hashing_task; + 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); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index f4ae95b6..371549cf 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -13,8 +13,8 @@ <property name="windowTitle"> <string notr="true">BlockedModsDialog</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> <widget class="QLabel" name="label"> <property name="text"> <string notr="true"/> @@ -24,17 +24,7 @@ </property> </widget> </item> - <item row="2" column="0"> - <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"> + <item> <widget class="QTextBrowser" name="textBrowser"> <property name="acceptRichText"> <bool>true</bool> @@ -44,6 +34,27 @@ </property> </widget> </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> </widget> <resources/> diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 9ec341bc..3f5122f6 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -44,7 +44,6 @@ #include "BaseVersion.h" #include "icons/IconList.h" -#include "tasks/Task.h" #include "BaseInstance.h" #include "InstanceList.h" @@ -78,8 +77,14 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) } ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); - ui->copySavesCheckbox->setChecked(m_copySaves); - ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); + ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled()); + 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() @@ -117,6 +122,31 @@ QString CopyInstanceDialog::instGroup() const 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() { IconPickerDialog dlg(this); @@ -129,42 +159,64 @@ void CopyInstanceDialog::on_iconButton_clicked() } } + void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) { 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) { - if(state == Qt::Unchecked) - { - m_copySaves = false; - } - else if(state == Qt::Checked) - { - m_copySaves = true; - } + m_selectedOptions.enableCopySaves(state == Qt::Checked); + updateSelectAllCheckbox(); } -bool CopyInstanceDialog::shouldKeepPlaytime() const + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { - return m_keepPlaytime; + m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); + updateSelectAllCheckbox(); } +void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyGameOptions(state == Qt::Checked); + updateSelectAllCheckbox(); +} -void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_keepPlaytime = false; - } - else if(state == Qt::Checked) - { - m_keepPlaytime = true; - } + 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(); } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index bf3cd920..884501d1 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -17,7 +17,7 @@ #include <QDialog> #include "BaseVersion.h" -#include <BaseInstance.h> +#include "InstanceCopyPrefs.h" class BaseInstance; @@ -39,20 +39,29 @@ public: QString instName() const; QString instGroup() const; QString iconKey() const; - bool shouldCopySaves() const; - bool shouldKeepPlaytime() const; + const InstanceCopyPrefs& getChosenOptions() const; private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString &arg1); + // Checkboxes + void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_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: + void checkAllCheckboxes(const bool& b); + void updateSelectAllCheckbox(); + /* data */ Ui::CopyInstanceDialog *ui; QString InstIconKey; InstancePtr m_original; - bool m_copySaves = true; - bool m_keepPlaytime = true; + InstanceCopyPrefs m_selectedOptions; }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index f4b191e2..b7828fe3 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -9,8 +9,8 @@ <rect> <x>0</x> <y>0</y> - <width>345</width> - <height>323</height> + <width>341</width> + <height>399</height> </rect> </property> <property name="windowTitle"> @@ -33,7 +33,7 @@ </property> <property name="sizeHint" stdset="0"> <size> - <width>40</width> + <width>60</width> <height>20</height> </size> </property> @@ -60,7 +60,7 @@ </property> <property name="sizeHint" stdset="0"> <size> - <width>40</width> + <width>60</width> <height>20</height> </size> </property> @@ -83,7 +83,10 @@ </widget> </item> <item> - <layout class="QGridLayout" name="gridLayout"> + <layout class="QGridLayout" name="groupDropdownLayout"> + <property name="verticalSpacing"> + <number>6</number> + </property> <item row="0" column="0"> <widget class="QLabel" name="labelVersion_3"> <property name="text"> @@ -110,18 +113,96 @@ </layout> </item> <item> - <widget class="QCheckBox" name="copySavesCheckbox"> - <property name="text"> - <string>Copy saves</string> - </property> - </widget> + <layout class="QHBoxLayout" name="selectAllButtonLayout"> + <item> + <widget class="QCheckBox" name="selectAllCheckbox"> + <property name="sizePolicy"> + <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> - <widget class="QCheckBox" name="keepPlaytimeCheckbox"> - <property name="text"> - <string>Keep play time</string> - </property> - </widget> + <layout class="QGridLayout" name="copyOptionsLayout"> + <item row="6" column="1"> + <widget class="QCheckBox" name="copyModsCheckbox"> + <property name="toolTip"> + <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> <widget class="QDialogButtonBox" name="buttonBox"> @@ -139,8 +220,6 @@ <tabstop>iconButton</tabstop> <tabstop>instNameTextBox</tabstop> <tabstop>groupBox</tabstop> - <tabstop>copySavesCheckbox</tabstop> - <tabstop>keepPlaytimeCheckbox</tabstop> </tabstops> <resources> <include location="../../graphics.qrc"/> @@ -153,8 +232,8 @@ <slot>accept()</slot> <hints> <hint type="sourcelabel"> - <x>248</x> - <y>254</y> + <x>254</x> + <y>316</y> </hint> <hint type="destinationlabel"> <x>157</x> @@ -169,8 +248,8 @@ <slot>reject()</slot> <hints> <hint type="sourcelabel"> - <x>316</x> - <y>260</y> + <x>322</x> + <y>316</y> </hint> <hint type="destinationlabel"> <x>286</x> diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 9f32dd8e..88552b23 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -39,13 +39,12 @@ #include <MMCZip.h> #include <QFileDialog> #include <QMessageBox> -#include <qfilesystemmodel.h> +#include <QFileSystemModel> #include <QSortFilterProxyModel> #include <QDebug> -#include <qstack.h> #include <QSaveFile> -#include "MMCStrings.h" +#include "StringUtils.h" #include "SeparatorPrefixTree.h" #include "Application.h" #include <icons/IconList.h> @@ -85,7 +84,7 @@ public: // sort and proxy model breaks the original model... if (sortColumn() == 0) { - return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), + return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0; } if (sortColumn() == 1) @@ -94,7 +93,7 @@ public: auto rightSize = rightFileInfo.size(); if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) { - return Strings::naturalCompare(leftFileInfo.fileName(), + return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index d740c8cb..24d23ba9 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -1,7 +1,8 @@ // 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 TheKodeToad <TheKodeToad@proton.me> * * 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 @@ -131,6 +132,8 @@ QList<BasePage*> ModDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameModPage::create(this, m_instance)); + m_selectedPage = dynamic_cast<ModPage*>(pages[0]); + return pages; } @@ -178,12 +181,22 @@ void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* select return; } - auto* selected_page = dynamic_cast<ModPage*>(selected); - if (!selected_page) { + m_selectedPage = dynamic_cast<ModPage*>(selected); + if (!m_selectedPage) { qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; return; } // 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; } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 18a5f0f3..fcf6f4fc 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -1,7 +1,8 @@ // 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 TheKodeToad <TheKodeToad@proton.me> * * 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 @@ -32,13 +33,14 @@ class ModDownloadDialog; class PageContainer; class QDialogButtonBox; +class ModPage; class ModrinthModPage; class ModDownloadDialog final : public QDialog, public BasePageProvider { Q_OBJECT -public: + public: explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); ~ModDownloadDialog() override = default; @@ -51,22 +53,26 @@ public: bool isModSelected(QString name) const; 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 accept() override; void reject() override; -private slots: + private slots: void selectedPageChanged(BasePage* previous, BasePage* selected); -private: - Ui::ModDownloadDialog *ui = nullptr; - PageContainer * m_container = nullptr; - QDialogButtonBox * m_buttons = nullptr; - QVBoxLayout *m_verticalLayout = nullptr; + private: + Ui::ModDownloadDialog* ui = nullptr; + PageContainer* m_container = nullptr; + QDialogButtonBox* m_buttons = nullptr; + QVBoxLayout* m_verticalLayout = nullptr; + ModPage* m_selectedPage = nullptr; QHash<QString, ModDownloadTask*> modTask; - BaseInstance *m_instance; + BaseInstance* m_instance; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 76f8ec18..33a03336 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -27,11 +27,7 @@ <item row="4" column="1" colspan="3"> <layout class="QGridLayout" name="gridLayout_2"> <item row="0" column="1"> - <widget class="QLineEdit" name="filterEdit"> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> + <widget class="QLineEdit" name="filterEdit"/> </item> <item row="0" column="0"> <widget class="QLabel" name="filterLabel"> diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index fcba5598..14b7cd9f 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -48,11 +48,7 @@ <item> <layout class="QGridLayout" name="gridLayout_2"> <item row="0" column="1"> - <widget class="QLineEdit" name="filterEdit"> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> + <widget class="QLineEdit" name="filterEdit"/> </item> <item row="0" column="0"> <widget class="QLabel" name="filterLabel"> diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f2c1746f..234f9f36 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,7 +1,8 @@ // 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 TheKodeToad <TheKodeToad@proton.me> * * 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 @@ -37,7 +38,9 @@ #include "Application.h" #include "ui_ModPage.h" +#include <QDesktopServices> #include <QKeyEvent> +#include <QRegularExpression> #include <memory> #include <HoeDown.h> @@ -80,6 +83,8 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packView->installEventFilter(this); + + connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl); } ModPage::~ModPage() @@ -158,8 +163,8 @@ void ModPage::triggerSearch() { auto changed = m_filter_widget->changed(); m_filter = m_filter_widget->getFilter(); - - if(changed){ + + if (changed) { ui->packView->clearSelection(); ui->packDescription->clear(); ui->versionSelectionBox->clear(); @@ -241,6 +246,79 @@ void ModPage::onModSelected() 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 ********/ @@ -270,8 +348,8 @@ void ModPage::updateModVersions(int prev_count) if ((valid || m_filter->versions.empty()) && !optedOut(version)) ui->versionSelectionBox->addItem(version.version, QVariant(i)); } - if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { - ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); + if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { + ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); } @@ -317,8 +395,7 @@ void ModPage::updateUi() text += "<br>" + tr(" by ") + authorStrs.join(", "); } - - if(current.extraDataLoaded) { + if (current.extraDataLoaded) { if (!current.extraData.donate.isEmpty()) { text += "<br><br>" + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index ae3d7e77..c9ccbaf2 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -82,6 +82,7 @@ class ModPage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); void onModSelected(); + virtual void openUrl(const QUrl& url); protected: Ui::ModPage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ModPage.ui index 943f02aa..94365aa5 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -16,10 +16,10 @@ <item row="1" column="2"> <widget class="ProjectDescriptionPage" name="packDescription"> <property name="openExternalLinks"> - <bool>true</bool> + <bool>false</bool> </property> <property name="openLinks"> - <bool>true</bool> + <bool>false</bool> </property> </widget> </item> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index c1ab166b..0887ebee 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -20,7 +20,8 @@ #include <modplatform/atlauncher/ATLPackIndex.h> #include <Version.h> -#include <MMCStrings.h> + +#include "StringUtils.h" namespace Atl { @@ -86,7 +87,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return lv < rv; } 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... diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index fd6e32ff..bad78c97 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,7 +1,8 @@ // 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 TheKodeToad <TheKodeToad@proton.me> * * 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 @@ -39,7 +40,7 @@ #include "FlameModModel.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) : ModPage(dialog, instance, new FlameAPI()) { 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 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... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); 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 // my Qt, so we need to implement this in every derived class... 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); +} diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 50dedd6f..58479ab9 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,7 +1,8 @@ // 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 TheKodeToad <TheKodeToad@proton.me> * * 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 @@ -64,4 +65,6 @@ class FlameModPage : public ModPage { bool optedOut(ModPlatform::IndexedVersion& ver) const override; auto shouldDisplay() const -> bool override; + + void openUrl(const QUrl& url) override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 9f8605eb..debae8c3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -3,7 +3,6 @@ #include "Application.h" #include "ui/widgets/ProjectItem.h" -#include <MMCStrings.h> #include <Version.h> #include <QtMath> diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp index cbf347fc..e2b548f2 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -19,7 +19,8 @@ #include <QDebug> #include "modplatform/modpacksch/FTBPackManifest.h" -#include <MMCStrings.h> + +#include "StringUtils.h" namespace Ftb { @@ -81,7 +82,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return leftPack.installs < rightPack.installs; } 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... diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 2d135e59..6f11cc95 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -36,7 +36,7 @@ #include "ListModel.h" #include "Application.h" -#include <MMCStrings.h> +#include "StringUtils.h" #include <Version.h> #include <QtMath> @@ -66,7 +66,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return lv < rv; } 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?! diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index 62e417c8..c531ea90 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -53,7 +53,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan ui->sortByBox->addItem(tr("Sort by Last Updated")); 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... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); diff --git a/libraries/tomlplusplus b/libraries/tomlplusplus -Subproject 4b166b69f28e70a416a1a04a98f365d2aeb90de +Subproject cc741c9f5f2a62856a2a2e9e275f61eb0591c09 diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 0cd7ea11..42a9e762 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -110,6 +110,142 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@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 @@ -171,6 +307,27 @@ Section /o "Desktop Shortcut" DESKTOP_SHORTCUTS 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 @@ -202,6 +359,16 @@ Section "Uninstall" 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 |